Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aedd4c8bf3 | ||
|
|
9563612126 | ||
|
|
ce098d0ac2 | ||
|
|
08801dd406 | ||
|
|
3741f43a11 | ||
|
|
00ef472d68 | ||
|
|
719858201a | ||
|
|
5db1eed181 | ||
|
|
2e9a648d08 | ||
|
|
83712cb1a6 | ||
|
|
30b56d5ae9 | ||
|
|
61eeaef226 | ||
|
|
adaa95ebb8 | ||
|
|
1f2f08ef7e | ||
|
|
39167e6e2a | ||
|
|
5a8519d2c2 | ||
|
|
d039d48b3f |
27
README.md
27
README.md
@@ -169,6 +169,9 @@ PORT=1234 node ./node_modules/y-websocket/bin/server.js
|
|||||||
### Example: Observe types
|
### Example: Observe types
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
const doc = new Y.Doc();
|
||||||
const yarray = doc.getArray('my-array')
|
const yarray = doc.getArray('my-array')
|
||||||
yarray.observe(event => {
|
yarray.observe(event => {
|
||||||
console.log('yarray was modified')
|
console.log('yarray was modified')
|
||||||
@@ -753,6 +756,30 @@ currentState1 = Y.mergeUpdates([currentState1, diff2])
|
|||||||
currentState1 = Y.mergeUpdates([currentState1, diff1])
|
currentState1 = Y.mergeUpdates([currentState1, diff1])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Obfuscating Updates
|
||||||
|
|
||||||
|
If one of your users runs into a weird bug (e.g. the rich-text editor throws
|
||||||
|
error messages), then you don't have to request the full document from your
|
||||||
|
user. Instead, they can obfuscate the document (i.e. replace the content with
|
||||||
|
meaningless generated content) before sending it to you. Note that someone might
|
||||||
|
still deduce the type of content by looking at the general structure of the
|
||||||
|
document. But this is much better than requesting the original document.
|
||||||
|
|
||||||
|
Obfuscated updates contain all the CRDT-related data that is required for
|
||||||
|
merging. So it is safe to merge obfuscated updates.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
// perform some changes..
|
||||||
|
ydoc.getText().insert(0, 'hello world')
|
||||||
|
const update = Y.encodeStateAsUpdate(ydoc)
|
||||||
|
// the below update contains scrambled data
|
||||||
|
const obfuscatedUpdate = Y.obfuscateUpdate(update)
|
||||||
|
const ydoc2 = new Y.Doc()
|
||||||
|
Y.applyUpdate(ydoc2, obfuscatedUpdate)
|
||||||
|
ydoc2.getText().toString() // => "00000000000"
|
||||||
|
```
|
||||||
|
|
||||||
#### Using V2 update format
|
#### Using V2 update format
|
||||||
|
|
||||||
Yjs implements two update formats. By default you are using the V1 update format.
|
Yjs implements two update formats. By default you are using the V1 update format.
|
||||||
|
|||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.5.52",
|
"version": "13.6.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.5.52",
|
"version": "13.6.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib0": "^0.2.72"
|
"lib0": "^0.2.74"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^24.0.1",
|
"@rollup/plugin-commonjs": "^24.0.1",
|
||||||
@@ -2481,9 +2481,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lib0": {
|
"node_modules/lib0": {
|
||||||
"version": "0.2.73",
|
"version": "0.2.74",
|
||||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.73.tgz",
|
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.74.tgz",
|
||||||
"integrity": "sha512-aJJIElCLWnHMcYZPtsM07QoSfHwpxCy4VUzBYGXFYEmh/h2QS5uZNbCCfL0CqnkOE30b7Tp9DVfjXag+3qzZjQ==",
|
"integrity": "sha512-roj9i46/JwG5ik5KNTkxP2IytlnrssAkD/OhlAVtE+GqectrdkfR+pttszVLrOzMDeXNs1MPt6yo66MUolWSiA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isomorphic.js": "^0.2.4"
|
"isomorphic.js": "^0.2.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.5.52",
|
"version": "13.6.3",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://docs.yjs.dev",
|
"homepage": "https://docs.yjs.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lib0": "^0.2.72"
|
"lib0": "^0.2.74"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^24.0.1",
|
"@rollup/plugin-commonjs": "^24.0.1",
|
||||||
|
|||||||
@@ -90,7 +90,11 @@ export {
|
|||||||
diffUpdateV2,
|
diffUpdateV2,
|
||||||
convertUpdateFormatV1ToV2,
|
convertUpdateFormatV1ToV2,
|
||||||
convertUpdateFormatV2ToV1,
|
convertUpdateFormatV2ToV1,
|
||||||
UpdateEncoderV1
|
obfuscateUpdate,
|
||||||
|
obfuscateUpdateV2,
|
||||||
|
UpdateEncoderV1,
|
||||||
|
equalDeleteSets,
|
||||||
|
snapshotContainsUpdate
|
||||||
} from './internals.js'
|
} from './internals.js'
|
||||||
|
|
||||||
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
|
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
|
||||||
|
|||||||
@@ -206,9 +206,11 @@ export class YMap extends AbstractType {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds or updates an element with a specified key and value.
|
* Adds or updates an element with a specified key and value.
|
||||||
|
* @template {MapType} VAL
|
||||||
*
|
*
|
||||||
* @param {string} key The key of the element to add to this YMap
|
* @param {string} key The key of the element to add to this YMap
|
||||||
* @param {MapType} value The value of the element to add
|
* @param {VAL} value The value of the element to add
|
||||||
|
* @return {VAL}
|
||||||
*/
|
*/
|
||||||
set (key, value) {
|
set (key, value) {
|
||||||
if (this.doc !== null) {
|
if (this.doc !== null) {
|
||||||
|
|||||||
@@ -476,6 +476,56 @@ export const cleanupYTextFormatting = type => {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will be called by the transction once the event handlers are called to potentially cleanup
|
||||||
|
* formatting attributes.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
export const cleanupYTextAfterTransaction = transaction => {
|
||||||
|
/**
|
||||||
|
* @type {Set<YText>}
|
||||||
|
*/
|
||||||
|
const needFullCleanup = new Set()
|
||||||
|
// check if another formatting item was inserted
|
||||||
|
const doc = transaction.doc
|
||||||
|
for (const [client, afterClock] of transaction.afterState.entries()) {
|
||||||
|
const clock = transaction.beforeState.get(client) || 0
|
||||||
|
if (afterClock === clock) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
|
||||||
|
if (
|
||||||
|
!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC
|
||||||
|
) {
|
||||||
|
needFullCleanup.add(/** @type {any} */ (item).parent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// cleanup in a new transaction
|
||||||
|
transact(doc, (t) => {
|
||||||
|
iterateDeletedStructs(transaction, transaction.deleteSet, item => {
|
||||||
|
if (item instanceof GC || needFullCleanup.has(/** @type {YText} */ (item.parent))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parent = /** @type {YText} */ (item.parent)
|
||||||
|
if (item.content.constructor === ContentFormat) {
|
||||||
|
needFullCleanup.add(parent)
|
||||||
|
} else {
|
||||||
|
// If no formatting attribute was inserted or deleted, we can make due with contextless
|
||||||
|
// formatting cleanups.
|
||||||
|
// Contextless: it is not necessary to compute currentAttributes for the affected position.
|
||||||
|
cleanupContextlessFormattingGap(t, item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// If a formatting item was inserted, we simply clean the whole type.
|
||||||
|
// We need to compute currentAttributes for the current position anyway.
|
||||||
|
for (const yText of needFullCleanup) {
|
||||||
|
cleanupYTextFormatting(yText)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {ItemTextListPosition} currPos
|
* @param {ItemTextListPosition} currPos
|
||||||
@@ -631,36 +681,39 @@ export class YTextEvent extends YEvent {
|
|||||||
/**
|
/**
|
||||||
* @type {any}
|
* @type {any}
|
||||||
*/
|
*/
|
||||||
let op
|
let op = null
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
op = { delete: deleteLen }
|
if (deleteLen > 0) {
|
||||||
|
op = { delete: deleteLen }
|
||||||
|
}
|
||||||
deleteLen = 0
|
deleteLen = 0
|
||||||
break
|
break
|
||||||
case 'insert':
|
case 'insert':
|
||||||
op = { insert }
|
if (typeof insert === 'object' || insert.length > 0) {
|
||||||
if (currentAttributes.size > 0) {
|
op = { insert }
|
||||||
op.attributes = {}
|
if (currentAttributes.size > 0) {
|
||||||
currentAttributes.forEach((value, key) => {
|
op.attributes = {}
|
||||||
if (value !== null) {
|
currentAttributes.forEach((value, key) => {
|
||||||
op.attributes[key] = value
|
if (value !== null) {
|
||||||
}
|
op.attributes[key] = value
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
insert = ''
|
insert = ''
|
||||||
break
|
break
|
||||||
case 'retain':
|
case 'retain':
|
||||||
op = { retain }
|
if (retain > 0) {
|
||||||
if (Object.keys(attributes).length > 0) {
|
op = { retain }
|
||||||
op.attributes = {}
|
if (!object.isEmpty(attributes)) {
|
||||||
for (const key in attributes) {
|
op.attributes = object.assign({}, attributes)
|
||||||
op.attributes[key] = attributes[key]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
retain = 0
|
retain = 0
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
delta.push(op)
|
if (op) delta.push(op)
|
||||||
action = null
|
action = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -856,55 +909,10 @@ export class YText extends AbstractType {
|
|||||||
_callObserver (transaction, parentSubs) {
|
_callObserver (transaction, parentSubs) {
|
||||||
super._callObserver(transaction, parentSubs)
|
super._callObserver(transaction, parentSubs)
|
||||||
const event = new YTextEvent(this, transaction, parentSubs)
|
const event = new YTextEvent(this, transaction, parentSubs)
|
||||||
const doc = transaction.doc
|
|
||||||
callTypeObservers(this, transaction, event)
|
callTypeObservers(this, transaction, event)
|
||||||
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
||||||
if (!transaction.local) {
|
if (!transaction.local) {
|
||||||
// check if another formatting item was inserted
|
transaction._needFormattingCleanup = true
|
||||||
let foundFormattingItem = false
|
|
||||||
for (const [client, afterClock] of transaction.afterState.entries()) {
|
|
||||||
const clock = transaction.beforeState.get(client) || 0
|
|
||||||
if (afterClock === clock) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
|
|
||||||
if (!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat) {
|
|
||||||
foundFormattingItem = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (foundFormattingItem) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!foundFormattingItem) {
|
|
||||||
iterateDeletedStructs(transaction, transaction.deleteSet, item => {
|
|
||||||
if (item instanceof GC || foundFormattingItem) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (item.parent === this && item.content.constructor === ContentFormat) {
|
|
||||||
foundFormattingItem = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
transact(doc, (t) => {
|
|
||||||
if (foundFormattingItem) {
|
|
||||||
// If a formatting item was inserted, we simply clean the whole type.
|
|
||||||
// We need to compute currentAttributes for the current position anyway.
|
|
||||||
cleanupYTextFormatting(this)
|
|
||||||
} else {
|
|
||||||
// If no formatting attribute was inserted, we can make due with contextless
|
|
||||||
// formatting cleanups.
|
|
||||||
// Contextless: it is not necessary to compute currentAttributes for the affected position.
|
|
||||||
iterateDeletedStructs(t, t.deleteSet, item => {
|
|
||||||
if (item instanceof GC) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (item.parent === this) {
|
|
||||||
cleanupContextlessFormattingGap(t, item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as object from 'lib0/object'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
YXmlFragment,
|
YXmlFragment,
|
||||||
@@ -12,12 +13,18 @@ import {
|
|||||||
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
|
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} ValueTypes
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An YXmlElement imitates the behavior of a
|
* An YXmlElement imitates the behavior of a
|
||||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
|
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
|
||||||
*
|
*
|
||||||
* * An YXmlElement has attributes (key value pairs)
|
* * An YXmlElement has attributes (key value pairs)
|
||||||
* * An YXmlElement has childElements that must inherit from YXmlElement
|
* * An YXmlElement has childElements that must inherit from YXmlElement
|
||||||
|
*
|
||||||
|
* @template {{ [key: string]: ValueTypes }} [KV={ [key: string]: string }]
|
||||||
*/
|
*/
|
||||||
export class YXmlElement extends YXmlFragment {
|
export class YXmlElement extends YXmlFragment {
|
||||||
constructor (nodeName = 'UNDEFINED') {
|
constructor (nodeName = 'UNDEFINED') {
|
||||||
@@ -73,14 +80,19 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {YXmlElement}
|
* @return {YXmlElement<KV>}
|
||||||
*/
|
*/
|
||||||
clone () {
|
clone () {
|
||||||
|
/**
|
||||||
|
* @type {YXmlElement<KV>}
|
||||||
|
*/
|
||||||
const el = new YXmlElement(this.nodeName)
|
const el = new YXmlElement(this.nodeName)
|
||||||
const attrs = this.getAttributes()
|
const attrs = this.getAttributes()
|
||||||
for (const key in attrs) {
|
object.forEach(attrs, (value, key) => {
|
||||||
el.setAttribute(key, attrs[key])
|
if (typeof value === 'string') {
|
||||||
}
|
el.setAttribute(key, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
el.insert(0, this.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
|
||||||
@@ -116,7 +128,7 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
/**
|
/**
|
||||||
* Removes an attribute from this YXmlElement.
|
* Removes an attribute from this YXmlElement.
|
||||||
*
|
*
|
||||||
* @param {String} attributeName The attribute name that is to be removed.
|
* @param {string} attributeName The attribute name that is to be removed.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@@ -133,8 +145,10 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
/**
|
/**
|
||||||
* Sets or updates an attribute.
|
* Sets or updates an attribute.
|
||||||
*
|
*
|
||||||
* @param {String} attributeName The attribute name that is to be set.
|
* @template {keyof KV & string} KEY
|
||||||
* @param {String} attributeValue The attribute value that is to be set.
|
*
|
||||||
|
* @param {KEY} attributeName The attribute name that is to be set.
|
||||||
|
* @param {KV[KEY]} attributeValue The attribute value that is to be set.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@@ -151,9 +165,11 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
/**
|
/**
|
||||||
* Returns an attribute value that belongs to the attribute name.
|
* Returns an attribute value that belongs to the attribute name.
|
||||||
*
|
*
|
||||||
* @param {String} attributeName The attribute name that identifies the
|
* @template {keyof KV & string} KEY
|
||||||
|
*
|
||||||
|
* @param {KEY} attributeName The attribute name that identifies the
|
||||||
* queried value.
|
* queried value.
|
||||||
* @return {String} The queried attribute value.
|
* @return {KV[KEY]|undefined} The queried attribute value.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@@ -164,7 +180,7 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
/**
|
/**
|
||||||
* Returns whether an attribute exists
|
* Returns whether an attribute exists
|
||||||
*
|
*
|
||||||
* @param {String} attributeName The attribute name to check for existence.
|
* @param {string} attributeName The attribute name to check for existence.
|
||||||
* @return {boolean} whether the attribute exists.
|
* @return {boolean} whether the attribute exists.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
@@ -176,12 +192,12 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
/**
|
/**
|
||||||
* Returns all attribute name/value pairs in a JSON Object.
|
* Returns all attribute name/value pairs in a JSON Object.
|
||||||
*
|
*
|
||||||
* @return {Object<string, any>} A JSON Object that describes the attributes.
|
* @return {{ [Key in Extract<keyof KV,string>]?: KV[Key]}} A JSON Object that describes the attributes.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getAttributes () {
|
getAttributes () {
|
||||||
return typeMapGetAll(this)
|
return /** @type {any} */ (typeMapGetAll(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -203,7 +219,10 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
const dom = _document.createElement(this.nodeName)
|
const dom = _document.createElement(this.nodeName)
|
||||||
const attrs = this.getAttributes()
|
const attrs = this.getAttributes()
|
||||||
for (const key in attrs) {
|
for (const key in attrs) {
|
||||||
dom.setAttribute(key, attrs[key])
|
const value = attrs[key]
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
dom.setAttribute(key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
typeListForEach(this, yxml => {
|
typeListForEach(this, yxml => {
|
||||||
dom.appendChild(yxml.toDOM(_document, hooks, binding))
|
dom.appendChild(yxml.toDOM(_document, hooks, binding))
|
||||||
|
|||||||
@@ -328,3 +328,23 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds1
|
||||||
|
* @param {DeleteSet} ds2
|
||||||
|
*/
|
||||||
|
export const equalDeleteSets = (ds1, ds2) => {
|
||||||
|
if (ds1.clients.size !== ds2.clients.size) return false
|
||||||
|
ds1.clients.forEach((deleteItems1, client) => {
|
||||||
|
const deleteItems2 = /** @type {Array<import('../internals.js').DeleteItem>} */ (ds2.clients.get(client))
|
||||||
|
if (deleteItems2 === undefined || deleteItems1.length !== deleteItems2.length) return false
|
||||||
|
for (let i = 0; i < deleteItems1.length; i++) {
|
||||||
|
const di1 = deleteItems1[i]
|
||||||
|
const di2 = deleteItems2[i]
|
||||||
|
if (di1.clock !== di2.clock || di1.len !== di2.len) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import {
|
|||||||
findIndexSS,
|
findIndexSS,
|
||||||
UpdateEncoderV2,
|
UpdateEncoderV2,
|
||||||
applyUpdateV2,
|
applyUpdateV2,
|
||||||
DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item // eslint-disable-line
|
LazyStructReader,
|
||||||
|
equalDeleteSets,
|
||||||
|
UpdateDecoderV1, UpdateDecoderV2, DSEncoderV1, DSEncoderV2, DSDecoderV1, DSDecoderV2, Transaction, Doc, DeleteSet, Item, // eslint-disable-line
|
||||||
|
mergeDeleteSets
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as map from 'lib0/map'
|
import * as map from 'lib0/map'
|
||||||
@@ -147,12 +150,20 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
|
|||||||
getItemCleanStart(transaction, createID(client, clock))
|
getItemCleanStart(transaction, createID(client, clock))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
iterateDeletedStructs(transaction, snapshot.ds, item => {})
|
iterateDeletedStructs(transaction, snapshot.ds, _item => {})
|
||||||
meta.add(snapshot)
|
meta.add(snapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @example
|
||||||
|
* const ydoc = new Y.Doc({ gc: false })
|
||||||
|
* ydoc.getText().insert(0, 'world!')
|
||||||
|
* const snapshot = Y.snapshot(ydoc)
|
||||||
|
* ydoc.getText().insert(0, 'hello ')
|
||||||
|
* const restored = Y.createDocFromSnapshot(ydoc, snapshot)
|
||||||
|
* assert(restored.getText().toString() === 'world!')
|
||||||
|
*
|
||||||
* @param {Doc} originDoc
|
* @param {Doc} originDoc
|
||||||
* @param {Snapshot} snapshot
|
* @param {Snapshot} snapshot
|
||||||
* @param {Doc} [newDoc] Optionally, you may define the Yjs document that receives the data from originDoc
|
* @param {Doc} [newDoc] Optionally, you may define the Yjs document that receives the data from originDoc
|
||||||
@@ -161,7 +172,7 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
|
|||||||
export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) => {
|
export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) => {
|
||||||
if (originDoc.gc) {
|
if (originDoc.gc) {
|
||||||
// we should not try to restore a GC-ed document, because some of the restored items might have their content deleted
|
// we should not try to restore a GC-ed document, because some of the restored items might have their content deleted
|
||||||
throw new Error('originDoc must not be garbage collected')
|
throw new Error('Garbage-collection must be disabled in `originDoc`!')
|
||||||
}
|
}
|
||||||
const { sv, ds } = snapshot
|
const { sv, ds } = snapshot
|
||||||
|
|
||||||
@@ -199,3 +210,28 @@ export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) =
|
|||||||
applyUpdateV2(newDoc, encoder.toUint8Array(), 'snapshot')
|
applyUpdateV2(newDoc, encoder.toUint8Array(), 'snapshot')
|
||||||
return newDoc
|
return newDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Snapshot} snapshot
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} [YDecoder]
|
||||||
|
*/
|
||||||
|
export const snapshotContainsUpdateV2 = (snapshot, update, YDecoder = UpdateDecoderV2) => {
|
||||||
|
const structs = []
|
||||||
|
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
||||||
|
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
||||||
|
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
||||||
|
structs.push(curr)
|
||||||
|
if ((snapshot.sv.get(curr.id.client) || 0) < curr.id.clock + curr.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mergedDS = mergeDeleteSets([snapshot.ds, readDeleteSet(updateDecoder)])
|
||||||
|
return equalDeleteSets(snapshot.ds, mergedDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Snapshot} snapshot
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
*/
|
||||||
|
export const snapshotContainsUpdate = (snapshot, update) => snapshotContainsUpdateV2(snapshot, update, UpdateDecoderV1)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Item,
|
Item,
|
||||||
generateNewClientId,
|
generateNewClientId,
|
||||||
createID,
|
createID,
|
||||||
|
cleanupYTextAfterTransaction,
|
||||||
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
@@ -114,6 +115,10 @@ export class Transaction {
|
|||||||
* @type {Set<Doc>}
|
* @type {Set<Doc>}
|
||||||
*/
|
*/
|
||||||
this.subdocsLoaded = new Set()
|
this.subdocsLoaded = new Set()
|
||||||
|
/**
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this._needFormattingCleanup = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +300,9 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
|
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
|
||||||
})
|
})
|
||||||
callAll(fs, [])
|
callAll(fs, [])
|
||||||
|
if (transaction._needFormattingCleanup) {
|
||||||
|
cleanupYTextAfterTransaction(transaction)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Replace deleted items with ItemDeleted / GC.
|
// Replace deleted items with ItemDeleted / GC.
|
||||||
// This is where content is actually remove from the Yjs Doc.
|
// This is where content is actually remove from the Yjs Doc.
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
|||||||
sm.set(client, clock)
|
sm.set(client, clock)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
getStateVector(store).forEach((clock, client) => {
|
getStateVector(store).forEach((_clock, client) => {
|
||||||
if (!_sm.has(client)) {
|
if (!_sm.has(client)) {
|
||||||
sm.set(client, 0)
|
sm.set(client, 0)
|
||||||
}
|
}
|
||||||
@@ -98,8 +98,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
|
|||||||
// Write items with higher client ids first
|
// Write items with higher client ids first
|
||||||
// This heavily improves the conflict algorithm.
|
// This heavily improves the conflict algorithm.
|
||||||
array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
|
||||||
// @ts-ignore
|
writeStructs(encoder, /** @type {Array<GC|Item>} */ (store.clients.get(client)), client, clock)
|
||||||
writeStructs(encoder, store.clients.get(client), client, clock)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,40 @@
|
|||||||
import * as binary from 'lib0/binary'
|
import * as binary from 'lib0/binary'
|
||||||
import * as decoding from 'lib0/decoding'
|
import * as decoding from 'lib0/decoding'
|
||||||
import * as encoding from 'lib0/encoding'
|
import * as encoding from 'lib0/encoding'
|
||||||
|
import * as error from 'lib0/error'
|
||||||
|
import * as f from 'lib0/function'
|
||||||
import * as logging from 'lib0/logging'
|
import * as logging from 'lib0/logging'
|
||||||
|
import * as map from 'lib0/map'
|
||||||
import * as math from 'lib0/math'
|
import * as math from 'lib0/math'
|
||||||
|
import * as string from 'lib0/string'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ContentAny,
|
||||||
|
ContentBinary,
|
||||||
|
ContentDeleted,
|
||||||
|
ContentDoc,
|
||||||
|
ContentEmbed,
|
||||||
|
ContentFormat,
|
||||||
|
ContentJSON,
|
||||||
|
ContentString,
|
||||||
|
ContentType,
|
||||||
createID,
|
createID,
|
||||||
readItemContent,
|
decodeStateVector,
|
||||||
readDeleteSet,
|
|
||||||
writeDeleteSet,
|
|
||||||
Skip,
|
|
||||||
mergeDeleteSets,
|
|
||||||
DSEncoderV1,
|
DSEncoderV1,
|
||||||
DSEncoderV2,
|
DSEncoderV2,
|
||||||
decodeStateVector,
|
GC,
|
||||||
Item, GC, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
|
Item,
|
||||||
|
mergeDeleteSets,
|
||||||
|
readDeleteSet,
|
||||||
|
readItemContent,
|
||||||
|
Skip,
|
||||||
|
UpdateDecoderV1,
|
||||||
|
UpdateDecoderV2,
|
||||||
|
UpdateEncoderV1,
|
||||||
|
UpdateEncoderV2,
|
||||||
|
writeDeleteSet,
|
||||||
|
YXmlElement,
|
||||||
|
YXmlHook
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -552,17 +573,17 @@ const finishLazyStructWriting = (lazyWriter) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} update
|
* @param {Uint8Array} update
|
||||||
|
* @param {function(Item|GC|Skip):Item|GC|Skip} blockTransformer
|
||||||
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
|
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
|
||||||
* @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder
|
* @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder
|
||||||
*/
|
*/
|
||||||
export const convertUpdateFormat = (update, YDecoder, YEncoder) => {
|
export const convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder) => {
|
||||||
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
||||||
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
||||||
const updateEncoder = new YEncoder()
|
const updateEncoder = new YEncoder()
|
||||||
const lazyWriter = new LazyStructWriter(updateEncoder)
|
const lazyWriter = new LazyStructWriter(updateEncoder)
|
||||||
|
|
||||||
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
||||||
writeStructToLazyStructWriter(lazyWriter, curr, 0)
|
writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0)
|
||||||
}
|
}
|
||||||
finishLazyStructWriting(lazyWriter)
|
finishLazyStructWriting(lazyWriter)
|
||||||
const ds = readDeleteSet(updateDecoder)
|
const ds = readDeleteSet(updateDecoder)
|
||||||
@@ -571,11 +592,132 @@ export const convertUpdateFormat = (update, YDecoder, YEncoder) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} update
|
* @typedef {Object} ObfuscatorOptions
|
||||||
|
* @property {boolean} [ObfuscatorOptions.formatting=true]
|
||||||
|
* @property {boolean} [ObfuscatorOptions.subdocs=true]
|
||||||
|
* @property {boolean} [ObfuscatorOptions.yxml=true] Whether to obfuscate nodeName / hookName
|
||||||
*/
|
*/
|
||||||
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, UpdateDecoderV1, UpdateEncoderV2)
|
|
||||||
|
/**
|
||||||
|
* @param {ObfuscatorOptions} obfuscator
|
||||||
|
*/
|
||||||
|
const createObfuscator = ({ formatting = true, subdocs = true, yxml = true } = {}) => {
|
||||||
|
let i = 0
|
||||||
|
const mapKeyCache = map.create()
|
||||||
|
const nodeNameCache = map.create()
|
||||||
|
const formattingKeyCache = map.create()
|
||||||
|
const formattingValueCache = map.create()
|
||||||
|
formattingValueCache.set(null, null) // end of a formatting range should always be the end of a formatting range
|
||||||
|
/**
|
||||||
|
* @param {Item|GC|Skip} block
|
||||||
|
* @return {Item|GC|Skip}
|
||||||
|
*/
|
||||||
|
return block => {
|
||||||
|
switch (block.constructor) {
|
||||||
|
case GC:
|
||||||
|
case Skip:
|
||||||
|
return block
|
||||||
|
case Item: {
|
||||||
|
const item = /** @type {Item} */ (block)
|
||||||
|
const content = item.content
|
||||||
|
switch (content.constructor) {
|
||||||
|
case ContentDeleted:
|
||||||
|
break
|
||||||
|
case ContentType: {
|
||||||
|
if (yxml) {
|
||||||
|
const type = /** @type {ContentType} */ (content).type
|
||||||
|
if (type instanceof YXmlElement) {
|
||||||
|
type.nodeName = map.setIfUndefined(nodeNameCache, type.nodeName, () => 'node-' + i)
|
||||||
|
}
|
||||||
|
if (type instanceof YXmlHook) {
|
||||||
|
type.hookName = map.setIfUndefined(nodeNameCache, type.hookName, () => 'hook-' + i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentAny: {
|
||||||
|
const c = /** @type {ContentAny} */ (content)
|
||||||
|
c.arr = c.arr.map(() => i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentBinary: {
|
||||||
|
const c = /** @type {ContentBinary} */ (content)
|
||||||
|
c.content = new Uint8Array([i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentDoc: {
|
||||||
|
const c = /** @type {ContentDoc} */ (content)
|
||||||
|
if (subdocs) {
|
||||||
|
c.opts = {}
|
||||||
|
c.doc.guid = i + ''
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentEmbed: {
|
||||||
|
const c = /** @type {ContentEmbed} */ (content)
|
||||||
|
c.embed = {}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentFormat: {
|
||||||
|
const c = /** @type {ContentFormat} */ (content)
|
||||||
|
if (formatting) {
|
||||||
|
c.key = map.setIfUndefined(formattingKeyCache, c.key, () => i + '')
|
||||||
|
c.value = map.setIfUndefined(formattingValueCache, c.value, () => ({ i }))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentJSON: {
|
||||||
|
const c = /** @type {ContentJSON} */ (content)
|
||||||
|
c.arr = c.arr.map(() => i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentString: {
|
||||||
|
const c = /** @type {ContentString} */ (content)
|
||||||
|
c.str = string.repeat((i % 10) + '', c.str.length)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// unknown content type
|
||||||
|
error.unexpectedCase()
|
||||||
|
}
|
||||||
|
if (item.parentSub) {
|
||||||
|
item.parentSub = map.setIfUndefined(mapKeyCache, item.parentSub, () => i + '')
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
return block
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// unknown block-type
|
||||||
|
error.unexpectedCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function obfuscates the content of a Yjs update. This is useful to share
|
||||||
|
* buggy Yjs documents while significantly limiting the possibility that a
|
||||||
|
* developer can on the user. Note that it might still be possible to deduce
|
||||||
|
* some information by analyzing the "structure" of the document or by analyzing
|
||||||
|
* the typing behavior using the CRDT-related metadata that is still kept fully
|
||||||
|
* intact.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {ObfuscatorOptions} [opts]
|
||||||
|
*/
|
||||||
|
export const obfuscateUpdate = (update, opts) => convertUpdateFormat(update, createObfuscator(opts), UpdateDecoderV1, UpdateEncoderV1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {ObfuscatorOptions} [opts]
|
||||||
|
*/
|
||||||
|
export const obfuscateUpdateV2 = (update, opts) => convertUpdateFormat(update, createObfuscator(opts), UpdateDecoderV2, UpdateEncoderV2)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Uint8Array} update
|
* @param {Uint8Array} update
|
||||||
*/
|
*/
|
||||||
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, UpdateDecoderV2, UpdateEncoderV1)
|
export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, f.id, UpdateDecoderV1, UpdateEncoderV2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
*/
|
||||||
|
export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, f.id, UpdateDecoderV2, UpdateEncoderV1)
|
||||||
|
|||||||
@@ -3,9 +3,21 @@ import * as t from 'lib0/testing'
|
|||||||
import { init } from './testHelper.js'
|
import { init } from './testHelper.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testBasicRestoreSnapshot = tc => {
|
export const testBasic = _tc => {
|
||||||
|
const ydoc = new Y.Doc({ gc: false })
|
||||||
|
ydoc.getText().insert(0, 'world!')
|
||||||
|
const snapshot = Y.snapshot(ydoc)
|
||||||
|
ydoc.getText().insert(0, 'hello ')
|
||||||
|
const restored = Y.createDocFromSnapshot(ydoc, snapshot)
|
||||||
|
t.assert(restored.getText().toString() === 'world!')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} _tc
|
||||||
|
*/
|
||||||
|
export const testBasicRestoreSnapshot = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
doc.getArray('array').insert(0, ['hello'])
|
doc.getArray('array').insert(0, ['hello'])
|
||||||
const snap = Y.snapshot(doc)
|
const snap = Y.snapshot(doc)
|
||||||
@@ -18,9 +30,9 @@ export const testBasicRestoreSnapshot = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testEmptyRestoreSnapshot = tc => {
|
export const testEmptyRestoreSnapshot = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
const snap = Y.snapshot(doc)
|
const snap = Y.snapshot(doc)
|
||||||
snap.sv.set(9999, 0)
|
snap.sv.set(9999, 0)
|
||||||
@@ -38,9 +50,9 @@ export const testEmptyRestoreSnapshot = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testRestoreSnapshotWithSubType = tc => {
|
export const testRestoreSnapshotWithSubType = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
doc.getArray('array').insert(0, [new Y.Map()])
|
doc.getArray('array').insert(0, [new Y.Map()])
|
||||||
const subMap = doc.getArray('array').get(0)
|
const subMap = doc.getArray('array').get(0)
|
||||||
@@ -61,9 +73,9 @@ export const testRestoreSnapshotWithSubType = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testRestoreDeletedItem1 = tc => {
|
export const testRestoreDeletedItem1 = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
doc.getArray('array').insert(0, ['item1', 'item2'])
|
doc.getArray('array').insert(0, ['item1', 'item2'])
|
||||||
|
|
||||||
@@ -77,9 +89,9 @@ export const testRestoreDeletedItem1 = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testRestoreLeftItem = tc => {
|
export const testRestoreLeftItem = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
doc.getArray('array').insert(0, ['item1'])
|
doc.getArray('array').insert(0, ['item1'])
|
||||||
doc.getMap('map').set('test', 1)
|
doc.getMap('map').set('test', 1)
|
||||||
@@ -95,9 +107,9 @@ export const testRestoreLeftItem = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testDeletedItemsBase = tc => {
|
export const testDeletedItemsBase = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
doc.getArray('array').insert(0, ['item1'])
|
doc.getArray('array').insert(0, ['item1'])
|
||||||
doc.getArray('array').delete(0)
|
doc.getArray('array').delete(0)
|
||||||
@@ -111,9 +123,9 @@ export const testDeletedItemsBase = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testDeletedItems2 = tc => {
|
export const testDeletedItems2 = _tc => {
|
||||||
const doc = new Y.Doc({ gc: false })
|
const doc = new Y.Doc({ gc: false })
|
||||||
doc.getArray('array').insert(0, ['item1', 'item2', 'item3'])
|
doc.getArray('array').insert(0, ['item1', 'item2', 'item3'])
|
||||||
doc.getArray('array').delete(1)
|
doc.getArray('array').delete(1)
|
||||||
@@ -169,3 +181,28 @@ export const testDependentChanges = tc => {
|
|||||||
const docRestored1 = Y.createDocFromSnapshot(array1.doc, snap)
|
const docRestored1 = Y.createDocFromSnapshot(array1.doc, snap)
|
||||||
t.compare(docRestored1.getArray('array').toArray(), ['user1item1', 'user2item1'])
|
t.compare(docRestored1.getArray('array').toArray(), ['user1item1', 'user2item1'])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} _tc
|
||||||
|
*/
|
||||||
|
export const testContainsUpdate = _tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
/**
|
||||||
|
* @type {Array<Uint8Array>}
|
||||||
|
*/
|
||||||
|
const updates = []
|
||||||
|
ydoc.on('update', update => {
|
||||||
|
updates.push(update)
|
||||||
|
})
|
||||||
|
const yarr = ydoc.getArray()
|
||||||
|
const snapshot1 = Y.snapshot(ydoc)
|
||||||
|
yarr.insert(0, [1])
|
||||||
|
const snapshot2 = Y.snapshot(ydoc)
|
||||||
|
yarr.delete(0, 1)
|
||||||
|
const snapshotFinal = Y.snapshot(ydoc)
|
||||||
|
t.assert(!Y.snapshotContainsUpdate(snapshot1, updates[0]))
|
||||||
|
t.assert(!Y.snapshotContainsUpdate(snapshot2, updates[1]))
|
||||||
|
t.assert(Y.snapshotContainsUpdate(snapshot2, updates[0]))
|
||||||
|
t.assert(Y.snapshotContainsUpdate(snapshotFinal, updates[0]))
|
||||||
|
t.assert(Y.snapshotContainsUpdate(snapshotFinal, updates[1]))
|
||||||
|
}
|
||||||
|
|||||||
@@ -356,8 +356,9 @@ export const compare = users => {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
t.compare(Y.encodeStateVector(users[i]), Y.encodeStateVector(users[i + 1]))
|
t.compare(Y.encodeStateVector(users[i]), Y.encodeStateVector(users[i + 1]))
|
||||||
compareDS(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
|
Y.equalDeleteSets(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
|
||||||
compareStructStores(users[i].store, users[i + 1].store)
|
compareStructStores(users[i].store, users[i + 1].store)
|
||||||
|
t.compare(Y.encodeSnapshot(Y.snapshot(users[i])), Y.encodeSnapshot(Y.snapshot(users[i + 1])))
|
||||||
}
|
}
|
||||||
users.map(u => u.destroy())
|
users.map(u => u.destroy())
|
||||||
}
|
}
|
||||||
@@ -412,25 +413,6 @@ export const compareStructStores = (ss1, ss2) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('../src/internals.js').DeleteSet} ds1
|
|
||||||
* @param {import('../src/internals.js').DeleteSet} ds2
|
|
||||||
*/
|
|
||||||
export const compareDS = (ds1, ds2) => {
|
|
||||||
t.assert(ds1.clients.size === ds2.clients.size)
|
|
||||||
ds1.clients.forEach((deleteItems1, client) => {
|
|
||||||
const deleteItems2 = /** @type {Array<import('../src/internals.js').DeleteItem>} */ (ds2.clients.get(client))
|
|
||||||
t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
|
|
||||||
for (let i = 0; i < deleteItems1.length; i++) {
|
|
||||||
const di1 = deleteItems1[i]
|
|
||||||
const di2 = deleteItems2[i]
|
|
||||||
if (di1.clock !== di2.clock || di1.len !== di2.len) {
|
|
||||||
t.fail('DeleteSets dont match')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @callback InitTestObjectCallback
|
* @callback InitTestObjectCallback
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as Y from '../src/index.js'
|
|||||||
import { readClientsStructRefs, readDeleteSet, UpdateDecoderV2, UpdateEncoderV2, writeDeleteSet } from '../src/internals.js'
|
import { readClientsStructRefs, readDeleteSet, UpdateDecoderV2, UpdateEncoderV2, writeDeleteSet } from '../src/internals.js'
|
||||||
import * as encoding from 'lib0/encoding'
|
import * as encoding from 'lib0/encoding'
|
||||||
import * as decoding from 'lib0/decoding'
|
import * as decoding from 'lib0/decoding'
|
||||||
|
import * as object from 'lib0/object'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Enc
|
* @typedef {Object} Enc
|
||||||
@@ -138,7 +139,6 @@ export const testKeyEncoding = tc => {
|
|||||||
*/
|
*/
|
||||||
const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
||||||
const cases = []
|
const cases = []
|
||||||
|
|
||||||
// Case 1: Simple case, simply merge everything
|
// Case 1: Simple case, simply merge everything
|
||||||
cases.push(enc.mergeUpdates(updates))
|
cases.push(enc.mergeUpdates(updates))
|
||||||
|
|
||||||
@@ -304,3 +304,54 @@ export const testMergePendingUpdates = tc => {
|
|||||||
const yText5 = yDoc5.getText('textBlock')
|
const yText5 = yDoc5.getText('textBlock')
|
||||||
t.compareStrings(yText5.toString(), 'nenor')
|
t.compareStrings(yText5.toString(), 'nenor')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testObfuscateUpdates = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const ytext = ydoc.getText('text')
|
||||||
|
const ymap = ydoc.getMap('map')
|
||||||
|
const yarray = ydoc.getArray('array')
|
||||||
|
// test ytext
|
||||||
|
ytext.applyDelta([{ insert: 'text', attributes: { bold: true } }, { insert: { href: 'supersecreturl' } }])
|
||||||
|
// test ymap
|
||||||
|
ymap.set('key', 'secret1')
|
||||||
|
ymap.set('key', 'secret2')
|
||||||
|
// test yarray with subtype & subdoc
|
||||||
|
const subtype = new Y.XmlElement('secretnodename')
|
||||||
|
const subdoc = new Y.Doc({ guid: 'secret' })
|
||||||
|
subtype.setAttribute('attr', 'val')
|
||||||
|
yarray.insert(0, ['teststring', 42, subtype, subdoc])
|
||||||
|
// obfuscate the content and put it into a new document
|
||||||
|
const obfuscatedUpdate = Y.obfuscateUpdate(Y.encodeStateAsUpdate(ydoc))
|
||||||
|
const odoc = new Y.Doc()
|
||||||
|
Y.applyUpdate(odoc, obfuscatedUpdate)
|
||||||
|
const otext = odoc.getText('text')
|
||||||
|
const omap = odoc.getMap('map')
|
||||||
|
const oarray = odoc.getArray('array')
|
||||||
|
// test ytext
|
||||||
|
const delta = otext.toDelta()
|
||||||
|
t.assert(delta.length === 2)
|
||||||
|
t.assert(delta[0].insert !== 'text' && delta[0].insert.length === 4)
|
||||||
|
t.assert(object.length(delta[0].attributes) === 1)
|
||||||
|
t.assert(!object.hasProperty(delta[0].attributes, 'bold'))
|
||||||
|
t.assert(object.length(delta[1]) === 1)
|
||||||
|
t.assert(object.hasProperty(delta[1], 'insert'))
|
||||||
|
// test ymap
|
||||||
|
t.assert(omap.size === 1)
|
||||||
|
t.assert(!omap.has('key'))
|
||||||
|
// test yarray with subtype & subdoc
|
||||||
|
const result = oarray.toArray()
|
||||||
|
t.assert(result.length === 4)
|
||||||
|
t.assert(result[0] !== 'teststring')
|
||||||
|
t.assert(result[1] !== 42)
|
||||||
|
const osubtype = /** @type {Y.XmlElement} */ (result[2])
|
||||||
|
const osubdoc = result[3]
|
||||||
|
// test subtype
|
||||||
|
t.assert(osubtype.nodeName !== subtype.nodeName)
|
||||||
|
t.assert(object.length(osubtype.getAttributes()) === 1)
|
||||||
|
t.assert(osubtype.getAttribute('attr') === undefined)
|
||||||
|
// test subdoc
|
||||||
|
t.assert(osubdoc.guid !== subdoc.guid)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,33 @@ import * as Y from '../src/index.js'
|
|||||||
|
|
||||||
import * as t from 'lib0/testing'
|
import * as t from 'lib0/testing'
|
||||||
|
|
||||||
|
export const testCustomTypings = () => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const ymap = ydoc.getMap()
|
||||||
|
/**
|
||||||
|
* @type {Y.XmlElement<{ num: number, str: string, [k:string]: object|number|string }>}
|
||||||
|
*/
|
||||||
|
const yxml = ymap.set('yxml', new Y.XmlElement('test'))
|
||||||
|
/**
|
||||||
|
* @type {number|undefined}
|
||||||
|
*/
|
||||||
|
const num = yxml.getAttribute('num')
|
||||||
|
/**
|
||||||
|
* @type {string|undefined}
|
||||||
|
*/
|
||||||
|
const str = yxml.getAttribute('str')
|
||||||
|
/**
|
||||||
|
* @type {object|number|string|undefined}
|
||||||
|
*/
|
||||||
|
const dtrn = yxml.getAttribute('dtrn')
|
||||||
|
const attrs = yxml.getAttributes()
|
||||||
|
/**
|
||||||
|
* @type {object|number|string|undefined}
|
||||||
|
*/
|
||||||
|
const any = attrs.shouldBeAny
|
||||||
|
console.log({ num, str, dtrn, attrs, any })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@@ -92,9 +119,9 @@ export const testTreewalker = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testYtextAttributes = tc => {
|
export const testYtextAttributes = _tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
const ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
||||||
ytext.observe(event => {
|
ytext.observe(event => {
|
||||||
@@ -106,9 +133,9 @@ export const testYtextAttributes = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testSiblings = tc => {
|
export const testSiblings = _tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const yxml = ydoc.getXmlFragment()
|
const yxml = ydoc.getXmlFragment()
|
||||||
const first = new Y.XmlText()
|
const first = new Y.XmlText()
|
||||||
@@ -122,9 +149,9 @@ export const testSiblings = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testInsertafter = tc => {
|
export const testInsertafter = _tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const yxml = ydoc.getXmlFragment()
|
const yxml = ydoc.getXmlFragment()
|
||||||
const first = new Y.XmlText()
|
const first = new Y.XmlText()
|
||||||
@@ -152,9 +179,9 @@ export const testInsertafter = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testClone = tc => {
|
export const testClone = _tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const yxml = ydoc.getXmlFragment()
|
const yxml = ydoc.getXmlFragment()
|
||||||
const first = new Y.XmlText('text')
|
const first = new Y.XmlText('text')
|
||||||
@@ -170,9 +197,9 @@ export const testClone = tc => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
export const testFormattingBug = tc => {
|
export const testFormattingBug = _tc => {
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
||||||
const delta = [
|
const delta = [
|
||||||
|
|||||||
Reference in New Issue
Block a user