Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a08e54c2fc | ||
|
|
2b377cd46d | ||
|
|
df9bfbe778 | ||
|
|
f1ab417570 | ||
|
|
371f2b6d55 | ||
|
|
85a7ad148f | ||
|
|
7ec1b3a19e | ||
|
|
633eb9033c | ||
|
|
4707fc46ac | ||
|
|
89b4320a8e | ||
|
|
0ea0a35521 | ||
|
|
15ea4ee805 | ||
|
|
744469d363 | ||
|
|
311dd50f1b | ||
|
|
89c5541ee6 | ||
|
|
28d8db86f0 | ||
|
|
0c34216ed0 | ||
|
|
9aa518bc14 | ||
|
|
27b1190a28 | ||
|
|
f3d8db491b | ||
|
|
e9905602f8 | ||
|
|
2b8154fa16 | ||
|
|
5ddb7eefed | ||
|
|
4b35de5ad5 | ||
|
|
097b9e8208 | ||
|
|
5cac153a17 | ||
|
|
a7e4724edd | ||
|
|
71d8da6513 |
@@ -100,6 +100,7 @@ are implemented in separate modules.
|
|||||||
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
|
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
|
||||||
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
||||||
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
|
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
|
||||||
|
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
|
||||||
|
|
||||||
### Providers
|
### Providers
|
||||||
|
|
||||||
@@ -334,6 +335,8 @@ or any of its children.
|
|||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>get(index:number)</code></b>
|
<b><code>get(index:number)</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
|
<b><code>clear()</code></b>
|
||||||
|
<dd>Removes all elements from this YMap.</dd>
|
||||||
<b><code>clone():Y.Map</code></b>
|
<b><code>clone():Y.Map</code></b>
|
||||||
<dd>Clone this type into a fresh Yjs type.</dd>
|
<dd>Clone this type into a fresh Yjs type.</dd>
|
||||||
<b><code>toJSON():Object<string, Object|boolean|Array|string|number|Uint8Array></code></b>
|
<b><code>toJSON():Object<string, Object|boolean|Array|string|number|Uint8Array></code></b>
|
||||||
@@ -986,7 +989,7 @@ order of the structs anymore (e.g. if the parent was deleted).
|
|||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
1. If a user inserts elements in sequence, the struct will be merged into a
|
1. If a user inserts elements in sequence, the struct will be merged into a
|
||||||
single struct. E.g. `array.insert(0, ['a']), array.insert(0, ['b']);` is
|
single struct. E.g. `text.insert(0, 'a'), text.insert(1, 'b');` is
|
||||||
first represented as two structs (`[{id: {client, clock: 0}, content: 'a'},
|
first represented as two structs (`[{id: {client, clock: 0}, content: 'a'},
|
||||||
{id: {client, clock: 1}, content: 'b'}`) and then merged into a single
|
{id: {client, clock: 1}, content: 'b'}`) and then merged into a single
|
||||||
struct: `[{id: {client, clock: 0}, content: 'ab'}]`.
|
struct: `[{id: {client, clock: 0}, content: 'ab'}]`.
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.5.8",
|
"version": "13.5.13",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.5.8",
|
"version": "13.5.13",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export {
|
|||||||
snapshot,
|
snapshot,
|
||||||
emptySnapshot,
|
emptySnapshot,
|
||||||
findRootTypeKey,
|
findRootTypeKey,
|
||||||
|
findIndexSS,
|
||||||
getItem,
|
getItem,
|
||||||
typeListToArraySnapshot,
|
typeListToArraySnapshot,
|
||||||
typeMapGetSnapshot,
|
typeMapGetSnapshot,
|
||||||
|
|||||||
@@ -679,6 +679,8 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
|
|||||||
packJsonContent()
|
packJsonContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lengthExceeded = error.create('Length exceeded!')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {AbstractType<any>} parent
|
* @param {AbstractType<any>} parent
|
||||||
@@ -689,6 +691,9 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const typeListInsertGenerics = (transaction, parent, index, content) => {
|
export const typeListInsertGenerics = (transaction, parent, index, content) => {
|
||||||
|
if (index > parent._length) {
|
||||||
|
throw lengthExceeded
|
||||||
|
}
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
if (parent._searchMarker) {
|
if (parent._searchMarker) {
|
||||||
updateMarkerChanges(parent._searchMarker, index, content.length)
|
updateMarkerChanges(parent._searchMarker, index, content.length)
|
||||||
@@ -766,7 +771,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
|
|||||||
n = n.right
|
n = n.right
|
||||||
}
|
}
|
||||||
if (length > 0) {
|
if (length > 0) {
|
||||||
throw error.create('array length exceeded')
|
throw lengthExceeded
|
||||||
}
|
}
|
||||||
if (parent._searchMarker) {
|
if (parent._searchMarker) {
|
||||||
updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */)
|
updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */)
|
||||||
|
|||||||
@@ -237,6 +237,21 @@ export class YMap extends AbstractType {
|
|||||||
return typeMapHas(this, key)
|
return typeMapHas(this, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all elements from this YMap.
|
||||||
|
*/
|
||||||
|
clear () {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
this.forEach(function (value, key, map) {
|
||||||
|
typeMapDelete(transaction, map, key)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Map<string, any>} */ (this._prelimContent).clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -164,12 +164,13 @@ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes
|
|||||||
}
|
}
|
||||||
const doc = transaction.doc
|
const doc = transaction.doc
|
||||||
const ownClientId = doc.clientID
|
const ownClientId = doc.clientID
|
||||||
let nextFormat = currPos.left
|
|
||||||
const right = currPos.right
|
|
||||||
negatedAttributes.forEach((val, key) => {
|
negatedAttributes.forEach((val, key) => {
|
||||||
nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), nextFormat, nextFormat && nextFormat.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
const left = currPos.left
|
||||||
|
const right = currPos.right
|
||||||
|
const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
||||||
nextFormat.integrate(transaction, 0)
|
nextFormat.integrate(transaction, 0)
|
||||||
currPos.right = nextFormat
|
currPos.right = nextFormat
|
||||||
|
currPos.forward()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
YXmlFragment,
|
YXmlFragment,
|
||||||
transact,
|
transact,
|
||||||
typeMapDelete,
|
typeMapDelete,
|
||||||
|
typeMapHas,
|
||||||
typeMapSet,
|
typeMapSet,
|
||||||
typeMapGet,
|
typeMapGet,
|
||||||
typeMapGetAll,
|
typeMapGetAll,
|
||||||
@@ -81,7 +82,7 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
el.setAttribute(key, attrs[key])
|
el.setAttribute(key, attrs[key])
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
el.insert(0, el.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
||||||
return el
|
return el
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +161,18 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
return /** @type {any} */ (typeMapGet(this, attributeName))
|
return /** @type {any} */ (typeMapGet(this, attributeName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether an attribute exists
|
||||||
|
*
|
||||||
|
* @param {String} attributeName The attribute name to check for existence.
|
||||||
|
* @return {boolean} whether the attribute exists.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
hasAttribute (attributeName) {
|
||||||
|
return /** @type {any} */ (typeMapHas(this, attributeName))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all attribute name/value pairs in a JSON Object.
|
* Returns all attribute name/value pairs in a JSON Object.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export class YXmlFragment extends AbstractType {
|
|||||||
clone () {
|
clone () {
|
||||||
const el = new YXmlFragment()
|
const el = new YXmlFragment()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
el.insert(0, el.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
||||||
return el
|
return el
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,11 @@ import {
|
|||||||
DSEncoderV2,
|
DSEncoderV2,
|
||||||
DSDecoderV1,
|
DSDecoderV1,
|
||||||
DSEncoderV1,
|
DSEncoderV1,
|
||||||
|
mergeUpdates,
|
||||||
mergeUpdatesV2,
|
mergeUpdatesV2,
|
||||||
Skip,
|
Skip,
|
||||||
diffUpdateV2,
|
diffUpdateV2,
|
||||||
|
convertUpdateFormatV2ToV1,
|
||||||
DSDecoderV2, Doc, Transaction, GC, Item, StructStore // eslint-disable-line
|
DSDecoderV2, Doc, Transaction, GC, Item, StructStore // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
@@ -380,6 +382,8 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
|
|||||||
*/
|
*/
|
||||||
export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) =>
|
export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) =>
|
||||||
transact(ydoc, transaction => {
|
transact(ydoc, transaction => {
|
||||||
|
// force that transaction.local is set to non-local
|
||||||
|
transaction.local = false
|
||||||
let retry = false
|
let retry = false
|
||||||
const doc = transaction.doc
|
const doc = transaction.doc
|
||||||
const store = doc.store
|
const store = doc.store
|
||||||
@@ -521,15 +525,16 @@ export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector = new Uint8A
|
|||||||
writeStateAsUpdate(encoder, doc, targetStateVector)
|
writeStateAsUpdate(encoder, doc, targetStateVector)
|
||||||
const updates = [encoder.toUint8Array()]
|
const updates = [encoder.toUint8Array()]
|
||||||
// also add the pending updates (if there are any)
|
// also add the pending updates (if there are any)
|
||||||
// @todo support diffirent encoders
|
if (doc.store.pendingDs) {
|
||||||
if (encoder.constructor === UpdateEncoderV2) {
|
updates.push(doc.store.pendingDs)
|
||||||
if (doc.store.pendingDs) {
|
}
|
||||||
updates.push(doc.store.pendingDs)
|
if (doc.store.pendingStructs) {
|
||||||
}
|
updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector))
|
||||||
if (doc.store.pendingStructs) {
|
}
|
||||||
updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector))
|
if (updates.length > 1) {
|
||||||
}
|
if (encoder.constructor === UpdateEncoderV1) {
|
||||||
if (updates.length > 1) {
|
return mergeUpdates(updates.map((update, i) => i === 0 ? update : convertUpdateFormatV2ToV1(update)))
|
||||||
|
} else if (encoder.constructor === UpdateEncoderV2) {
|
||||||
return mergeUpdatesV2(updates)
|
return mergeUpdatesV2(updates)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,23 +149,27 @@ export const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1,
|
|||||||
*/
|
*/
|
||||||
export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YDecoder = UpdateDecoderV2) => {
|
export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YDecoder = UpdateDecoderV2) => {
|
||||||
const encoder = new YEncoder()
|
const encoder = new YEncoder()
|
||||||
const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), true)
|
const updateDecoder = new LazyStructReader(new YDecoder(decoding.createDecoder(update)), false)
|
||||||
let curr = updateDecoder.curr
|
let curr = updateDecoder.curr
|
||||||
if (curr !== null) {
|
if (curr !== null) {
|
||||||
let size = 1
|
let size = 0
|
||||||
let currClient = curr.id.client
|
let currClient = curr.id.client
|
||||||
let currClock = curr.id.clock
|
let stopCounting = curr.id.clock !== 0 // must start at 0
|
||||||
let stopCounting = false
|
let currClock = stopCounting ? 0 : curr.id.clock + curr.length
|
||||||
for (; curr !== null; curr = updateDecoder.next()) {
|
for (; curr !== null; curr = updateDecoder.next()) {
|
||||||
if (currClient !== curr.id.client) {
|
if (currClient !== curr.id.client) {
|
||||||
size++
|
if (currClock !== 0) {
|
||||||
// We found a new client
|
size++
|
||||||
// write what we have to the encoder
|
// We found a new client
|
||||||
encoding.writeVarUint(encoder.restEncoder, currClient)
|
// write what we have to the encoder
|
||||||
encoding.writeVarUint(encoder.restEncoder, currClock)
|
encoding.writeVarUint(encoder.restEncoder, currClient)
|
||||||
|
encoding.writeVarUint(encoder.restEncoder, currClock)
|
||||||
|
}
|
||||||
currClient = curr.id.client
|
currClient = curr.id.client
|
||||||
stopCounting = false
|
currClock = 0
|
||||||
|
stopCounting = curr.id.clock !== 0
|
||||||
}
|
}
|
||||||
|
// we ignore skips
|
||||||
if (curr.constructor === Skip) {
|
if (curr.constructor === Skip) {
|
||||||
stopCounting = true
|
stopCounting = true
|
||||||
}
|
}
|
||||||
@@ -174,8 +178,11 @@ export const encodeStateVectorFromUpdateV2 = (update, YEncoder = DSEncoderV2, YD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// write what we have
|
// write what we have
|
||||||
encoding.writeVarUint(encoder.restEncoder, currClient)
|
if (currClock !== 0) {
|
||||||
encoding.writeVarUint(encoder.restEncoder, currClock)
|
size++
|
||||||
|
encoding.writeVarUint(encoder.restEncoder, currClient)
|
||||||
|
encoding.writeVarUint(encoder.restEncoder, currClock)
|
||||||
|
}
|
||||||
// prepend the size of the state vector
|
// prepend the size of the state vector
|
||||||
const enc = encoding.createEncoder()
|
const enc = encoding.createEncoder()
|
||||||
encoding.writeVarUint(enc, size)
|
encoding.writeVarUint(enc, size)
|
||||||
@@ -280,6 +287,9 @@ const sliceStruct = (left, diff) => {
|
|||||||
* @return {Uint8Array}
|
* @return {Uint8Array}
|
||||||
*/
|
*/
|
||||||
export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
|
export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
|
||||||
|
if (updates.length === 1) {
|
||||||
|
return updates[0]
|
||||||
|
}
|
||||||
const updateDecoders = updates.map(update => new YDecoder(decoding.createDecoder(update)))
|
const updateDecoders = updates.map(update => new YDecoder(decoding.createDecoder(update)))
|
||||||
let lazyStructDecoders = updateDecoders.map(decoder => new LazyStructReader(decoder, true))
|
let lazyStructDecoders = updateDecoders.map(decoder => new LazyStructReader(decoder, true))
|
||||||
|
|
||||||
@@ -305,8 +315,9 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U
|
|||||||
if (dec1.curr.id.client === dec2.curr.id.client) {
|
if (dec1.curr.id.client === dec2.curr.id.client) {
|
||||||
const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock
|
const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock
|
||||||
if (clockDiff === 0) {
|
if (clockDiff === 0) {
|
||||||
|
// @todo remove references to skip since the structDecoders must filter Skips.
|
||||||
return dec1.curr.constructor === dec2.curr.constructor ? 0 : (
|
return dec1.curr.constructor === dec2.curr.constructor ? 0 : (
|
||||||
dec1.curr.constructor === Skip ? 1 : -1
|
dec1.curr.constructor === Skip ? 1 : -1 // we are filtering skips anyway.
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return clockDiff
|
return clockDiff
|
||||||
@@ -326,13 +337,19 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U
|
|||||||
|
|
||||||
if (currWrite !== null) {
|
if (currWrite !== null) {
|
||||||
let curr = /** @type {Item | GC | null} */ (currDecoder.curr)
|
let curr = /** @type {Item | GC | null} */ (currDecoder.curr)
|
||||||
|
let iterated = false
|
||||||
|
|
||||||
// iterate until we find something that we haven't written already
|
// iterate until we find something that we haven't written already
|
||||||
// remember: first the high client-ids are written
|
// remember: first the high client-ids are written
|
||||||
while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) {
|
while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) {
|
||||||
curr = currDecoder.next()
|
curr = currDecoder.next()
|
||||||
|
iterated = true
|
||||||
}
|
}
|
||||||
if (curr === null || curr.id.client !== firstClient) {
|
if (
|
||||||
|
curr === null || // current decoder is empty
|
||||||
|
curr.id.client !== firstClient || // check whether there is another decoder that has has updates from `firstClient`
|
||||||
|
(iterated && curr.id.clock > currWrite.struct.id.clock + currWrite.struct.length) // the above while loop was used and we are potentially missing updates
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,3 +525,33 @@ const finishLazyStructWriting = (lazyWriter) => {
|
|||||||
encoding.writeUint8Array(restEncoder, partStructs.restEncoder)
|
encoding.writeUint8Array(restEncoder, partStructs.restEncoder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
|
||||||
|
* @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder
|
||||||
|
*/
|
||||||
|
export const convertUpdateFormat = (update, YDecoder, YEncoder) => {
|
||||||
|
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
||||||
|
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
||||||
|
const updateEncoder = new YEncoder()
|
||||||
|
const lazyWriter = new LazyStructWriter(updateEncoder)
|
||||||
|
|
||||||
|
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
||||||
|
writeStructToLazyStructWriter(lazyWriter, curr, 0)
|
||||||
|
}
|
||||||
|
finishLazyStructWriting(lazyWriter)
|
||||||
|
const ds = readDeleteSet(updateDecoder)
|
||||||
|
writeDeleteSet(updateEncoder, ds)
|
||||||
|
return updateEncoder.toUint8Array()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
*/
|
||||||
|
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, UpdateDecoderV1, UpdateEncoderV2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
*/
|
||||||
|
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, UpdateDecoderV2, UpdateEncoderV1)
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
applyUpdate
|
applyUpdate
|
||||||
} from '../src/internals.js'
|
} from '../src/internals.js'
|
||||||
|
|
||||||
|
import * as Y from '../src/index.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@@ -62,3 +64,45 @@ export const testPermanentUserData = async tc => {
|
|||||||
const pd3 = new PermanentUserData(ydoc3)
|
const pd3 = new PermanentUserData(ydoc3)
|
||||||
pd3.setUserMapping(ydoc3, ydoc3.clientID, 'user a')
|
pd3.setUserMapping(ydoc3, ydoc3.clientID, 'user a')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reported here: https://github.com/yjs/yjs/issues/308
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testDiffStateVectorOfUpdateIsEmpty = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
/**
|
||||||
|
* @type {null | Uint8Array}
|
||||||
|
*/
|
||||||
|
let sv = /* any */ (null)
|
||||||
|
ydoc.getText().insert(0, 'a')
|
||||||
|
ydoc.on('update', update => {
|
||||||
|
sv = Y.encodeStateVectorFromUpdate(update)
|
||||||
|
})
|
||||||
|
// should produce an update with an empty state vector (because previous ops are missing)
|
||||||
|
ydoc.getText().insert(0, 'a')
|
||||||
|
t.assert(sv !== null && sv.byteLength === 1 && sv[0] === 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reported here: https://github.com/yjs/yjs/issues/308
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testDiffStateVectorOfUpdateIgnoresSkips = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
/**
|
||||||
|
* @type {Array<Uint8Array>}
|
||||||
|
*/
|
||||||
|
const updates = []
|
||||||
|
ydoc.on('update', update => {
|
||||||
|
updates.push(update)
|
||||||
|
})
|
||||||
|
ydoc.getText().insert(0, 'a')
|
||||||
|
ydoc.getText().insert(0, 'b')
|
||||||
|
ydoc.getText().insert(0, 'c')
|
||||||
|
const update13 = Y.mergeUpdates([updates[0], updates[2]])
|
||||||
|
const sv = Y.encodeStateVectorFromUpdate(update13)
|
||||||
|
const state = Y.decodeStateVector(sv)
|
||||||
|
t.assert(state.get(ydoc.clientID) === 1)
|
||||||
|
t.assert(state.size === 1)
|
||||||
|
}
|
||||||
|
|||||||
@@ -166,9 +166,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
|||||||
const targetSV = Y.encodeStateVectorFromUpdateV2(Y.mergeUpdatesV2(updates.slice(0, j)))
|
const targetSV = Y.encodeStateVectorFromUpdateV2(Y.mergeUpdatesV2(updates.slice(0, j)))
|
||||||
const diffed = enc.diffUpdate(mergedUpdates, targetSV)
|
const diffed = enc.diffUpdate(mergedUpdates, targetSV)
|
||||||
const diffedMeta = enc.parseUpdateMeta(diffed)
|
const diffedMeta = enc.parseUpdateMeta(diffed)
|
||||||
const decDiffedSV = Y.decodeStateVector(enc.encodeStateVectorFromUpdate(diffed))
|
|
||||||
t.compare(partMeta, diffedMeta)
|
t.compare(partMeta, diffedMeta)
|
||||||
t.compare(decDiffedSV, partMeta.to)
|
|
||||||
{
|
{
|
||||||
// We can'd do the following
|
// We can'd do the following
|
||||||
// - t.compare(diffed, mergedDeletes)
|
// - t.compare(diffed, mergedDeletes)
|
||||||
@@ -242,5 +240,49 @@ export const testMergeUpdates2 = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo be able to apply Skip structs to Yjs docs
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
|
export const testMergePendingUpdates = tc => {
|
||||||
|
const yDoc = new Y.Doc()
|
||||||
|
/**
|
||||||
|
* @type {Array<Uint8Array>}
|
||||||
|
*/
|
||||||
|
const serverUpdates = []
|
||||||
|
yDoc.on('update', (update, origin, c) => {
|
||||||
|
serverUpdates.splice(serverUpdates.length, 0, update)
|
||||||
|
})
|
||||||
|
const yText = yDoc.getText('textBlock')
|
||||||
|
yText.applyDelta([{ insert: 'r' }])
|
||||||
|
yText.applyDelta([{ insert: 'o' }])
|
||||||
|
yText.applyDelta([{ insert: 'n' }])
|
||||||
|
yText.applyDelta([{ insert: 'e' }])
|
||||||
|
yText.applyDelta([{ insert: 'n' }])
|
||||||
|
|
||||||
|
const yDoc1 = new Y.Doc()
|
||||||
|
Y.applyUpdate(yDoc1, serverUpdates[0])
|
||||||
|
const update1 = Y.encodeStateAsUpdate(yDoc1)
|
||||||
|
|
||||||
|
const yDoc2 = new Y.Doc()
|
||||||
|
Y.applyUpdate(yDoc2, update1)
|
||||||
|
Y.applyUpdate(yDoc2, serverUpdates[1])
|
||||||
|
const update2 = Y.encodeStateAsUpdate(yDoc2)
|
||||||
|
|
||||||
|
const yDoc3 = new Y.Doc()
|
||||||
|
Y.applyUpdate(yDoc3, update2)
|
||||||
|
Y.applyUpdate(yDoc3, serverUpdates[3])
|
||||||
|
const update3 = Y.encodeStateAsUpdate(yDoc3)
|
||||||
|
|
||||||
|
const yDoc4 = new Y.Doc()
|
||||||
|
Y.applyUpdate(yDoc4, update3)
|
||||||
|
Y.applyUpdate(yDoc4, serverUpdates[2])
|
||||||
|
const update4 = Y.encodeStateAsUpdate(yDoc4)
|
||||||
|
|
||||||
|
const yDoc5 = new Y.Doc()
|
||||||
|
Y.applyUpdate(yDoc5, update4)
|
||||||
|
Y.applyUpdate(yDoc5, serverUpdates[4])
|
||||||
|
// @ts-ignore
|
||||||
|
const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
|
||||||
|
|
||||||
|
const yText5 = yDoc5.getText('textBlock')
|
||||||
|
t.compareStrings(yText5.toString(), 'nenor')
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,6 +61,49 @@ export const testLengthIssue = tc => {
|
|||||||
t.assert(arr.length === arr.toArray().length)
|
t.assert(arr.length === arr.toArray().length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debugging yjs#314
|
||||||
|
*
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testLengthIssue2 = tc => {
|
||||||
|
const doc = new Y.Doc()
|
||||||
|
const next = doc.getArray()
|
||||||
|
doc.transact(() => {
|
||||||
|
next.insert(0, ['group2'])
|
||||||
|
})
|
||||||
|
doc.transact(() => {
|
||||||
|
next.insert(1, ['rectangle3'])
|
||||||
|
})
|
||||||
|
doc.transact(() => {
|
||||||
|
next.delete(0)
|
||||||
|
next.insert(0, ['rectangle3'])
|
||||||
|
})
|
||||||
|
next.delete(1)
|
||||||
|
doc.transact(() => {
|
||||||
|
next.insert(1, ['ellipse4'])
|
||||||
|
})
|
||||||
|
doc.transact(() => {
|
||||||
|
next.insert(2, ['ellipse3'])
|
||||||
|
})
|
||||||
|
doc.transact(() => {
|
||||||
|
next.insert(3, ['ellipse2'])
|
||||||
|
})
|
||||||
|
doc.transact(() => {
|
||||||
|
doc.transact(() => {
|
||||||
|
t.fails(() => {
|
||||||
|
next.insert(5, ['rectangle2'])
|
||||||
|
})
|
||||||
|
next.insert(4, ['rectangle2'])
|
||||||
|
})
|
||||||
|
doc.transact(() => {
|
||||||
|
// this should not throw an error message
|
||||||
|
next.delete(4)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
console.log(next.toArray())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -189,6 +189,49 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => {
|
|||||||
compare(users)
|
compare(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testSetAndClearOfMapProperties = tc => {
|
||||||
|
const { testConnector, users, map0 } = init(tc, { users: 1 })
|
||||||
|
map0.set('stuff', 'c0')
|
||||||
|
map0.set('otherstuff', 'c1')
|
||||||
|
map0.clear()
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
for (const user of users) {
|
||||||
|
const u = user.getMap('map')
|
||||||
|
t.assert(u.get('stuff') === undefined)
|
||||||
|
t.assert(u.get('otherstuff') === undefined)
|
||||||
|
t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`)
|
||||||
|
}
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testSetAndClearOfMapPropertiesWithConflicts = tc => {
|
||||||
|
const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 })
|
||||||
|
map0.set('stuff', 'c0')
|
||||||
|
map1.set('stuff', 'c1')
|
||||||
|
map1.set('stuff', 'c2')
|
||||||
|
map2.set('stuff', 'c3')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
map0.set('otherstuff', 'c0')
|
||||||
|
map1.set('otherstuff', 'c1')
|
||||||
|
map2.set('otherstuff', 'c2')
|
||||||
|
map3.set('otherstuff', 'c3')
|
||||||
|
map3.clear()
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
for (const user of users) {
|
||||||
|
const u = user.getMap('map')
|
||||||
|
t.assert(u.get('stuff') === undefined)
|
||||||
|
t.assert(u.get('otherstuff') === undefined)
|
||||||
|
t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`)
|
||||||
|
}
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@@ -335,6 +378,30 @@ export const testThrowsAddAndUpdateAndDeleteEvents = tc => {
|
|||||||
compare(users)
|
compare(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testThrowsDeleteEventsOnClear = tc => {
|
||||||
|
const { users, map0 } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
let event = {}
|
||||||
|
map0.observe(e => {
|
||||||
|
event = e // just put it on event, should be thrown synchronously anyway
|
||||||
|
})
|
||||||
|
// set values
|
||||||
|
map0.set('stuff', 4)
|
||||||
|
map0.set('otherstuff', new Y.Array())
|
||||||
|
// clear
|
||||||
|
map0.clear()
|
||||||
|
compareEvent(event, {
|
||||||
|
keysChanged: new Set(['stuff', 'otherstuff']),
|
||||||
|
target: map0
|
||||||
|
})
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -455,6 +455,44 @@ export const testSplitSurrogateCharacter = tc => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search marker bug https://github.com/yjs/yjs/issues/307
|
||||||
|
*
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testSearchMarkerBug1 = tc => {
|
||||||
|
const { users, text0, text1, testConnector } = init(tc, { users: 2 })
|
||||||
|
|
||||||
|
users[0].on('update', update => {
|
||||||
|
users[0].transact(() => {
|
||||||
|
Y.applyUpdate(users[0], update)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
users[0].on('update', update => {
|
||||||
|
users[1].transact(() => {
|
||||||
|
Y.applyUpdate(users[1], update)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
text0.insert(0, 'a_a')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
text0.insert(2, 's')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
text1.insert(3, 'd')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
text0.delete(0, 5)
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
text0.insert(0, 'a_a')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
text0.insert(2, 's')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
text1.insert(3, 'd')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.compareStrings(text0.toString(), text1.toString())
|
||||||
|
t.compareStrings(text0.toString(), 'a_sda')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
// RANDOM TESTS
|
// RANDOM TESTS
|
||||||
|
|
||||||
let charCounter = 0
|
let charCounter = 0
|
||||||
|
|||||||
@@ -15,6 +15,23 @@ export const testSetProperty = tc => {
|
|||||||
compare(users)
|
compare(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testHasProperty = tc => {
|
||||||
|
const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 })
|
||||||
|
xml0.setAttribute('height', '10')
|
||||||
|
t.assert(xml0.hasAttribute('height'), 'Simple set+has works')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.assert(xml1.hasAttribute('height'), 'Simple set+has works (remote)')
|
||||||
|
|
||||||
|
xml0.removeAttribute('height')
|
||||||
|
t.assert(!xml0.hasAttribute('height'), 'Simple set+remove+has works')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.assert(!xml1.hasAttribute('height'), 'Simple set+remove+has works (remote)')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@@ -133,3 +150,36 @@ export const testInsertafter = tc => {
|
|||||||
el.insertAfter(deepsecond1, [new Y.XmlText()])
|
el.insertAfter(deepsecond1, [new Y.XmlText()])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testClone = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const yxml = ydoc.getXmlFragment()
|
||||||
|
const first = new Y.XmlText('text')
|
||||||
|
const second = new Y.XmlElement('p')
|
||||||
|
const third = new Y.XmlElement('p')
|
||||||
|
yxml.push([first, second, third])
|
||||||
|
t.compareArrays(yxml.toArray(), [first, second, third])
|
||||||
|
|
||||||
|
const cloneYxml = yxml.clone()
|
||||||
|
ydoc.getArray('copyarr').insert(0, [cloneYxml])
|
||||||
|
t.assert(cloneYxml.length === 3)
|
||||||
|
t.compare(cloneYxml.toJSON(), yxml.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testFormattingBug = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
||||||
|
const delta = [
|
||||||
|
{ insert: 'A', attributes: { em: {}, strong: {} } },
|
||||||
|
{ insert: 'B', attributes: { em: {} } },
|
||||||
|
{ insert: 'C', attributes: { em: {}, strong: {} } }
|
||||||
|
]
|
||||||
|
yxml.applyDelta(delta)
|
||||||
|
t.compare(yxml.toDelta(), delta)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user