Compare commits

...

20 Commits

Author SHA1 Message Date
Kevin Jahns
e90d9de5ed 13.5.22 2021-11-19 13:48:52 +01:00
Kevin Jahns
9a7250f192 fix undoing of content containing subdocs 2021-11-19 13:47:10 +01:00
Kevin Jahns
4154b12f14 handle local/remote autoload edge cases 2021-11-19 13:27:14 +01:00
Kevin Jahns
9df5016667 13.5.21 2021-11-15 14:00:04 +01:00
Kevin Jahns
1becaccdd9 bump lib0 dependency for bugfix dmonad/lib0#34 2021-11-15 13:58:22 +01:00
Kevin Jahns
ea4e9a0007 change order of logging statement for debugging 2021-11-14 13:10:52 +01:00
Kevin Jahns
a4e48d1ddf 13.5.20 2021-11-09 16:53:35 +01:00
Kevin Jahns
0a39a92b33 export testHelper 2021-11-09 16:51:54 +01:00
Kevin Jahns
bd819243eb 13.5.19 2021-11-09 16:39:58 +01:00
Kevin Jahns
2ec19defcb export testHelper esm properly 2021-11-06 15:55:59 +01:00
Kevin Jahns
336f7b1b1d 13.5.18 2021-11-06 14:39:55 +01:00
Kevin Jahns
8abf5b85ff fix #344 - formatting attribute assign bug 2021-11-06 14:35:04 +01:00
Kevin Jahns
320e8cbe18 add transaction to subdocs event 2021-11-02 23:24:28 +01:00
Kevin Jahns
49150f4adb add ydoc as argument in subdocs event 2021-10-29 22:04:59 +02:00
Kevin Jahns
e22fed7af3 13.5.17 2021-10-29 21:55:55 +02:00
Kevin Jahns
c91945228f inherid collectionid 2021-10-29 21:53:21 +02:00
Kevin Jahns
3586d91925 fire subdocs event only when something changed 2021-10-29 17:49:30 +02:00
Kevin Jahns
f915ebda1b 13.5.16 2021-10-15 19:18:51 +02:00
Kevin Jahns
a9b92b9099 13.5.15 2021-10-15 19:17:08 +02:00
Kevin Jahns
cbddf6ef90 add warning when Yjs was already imported 2021-10-15 19:10:11 +02:00
15 changed files with 286 additions and 93 deletions

14
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.5.14", "version": "13.5.22",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -1711,9 +1711,9 @@
} }
}, },
"lib0": { "lib0": {
"version": "0.2.42", "version": "0.2.43",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.42.tgz", "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.43.tgz",
"integrity": "sha512-8BNM4MiokEKzMvSxTOC3gnCBisJH+jL67CnSnqzHv3jli3pUvGC8wz+0DQ2YvGr4wVQdb2R2uNNPw9LEpVvJ4Q==", "integrity": "sha512-MJ1KLoz5p3gljIUBfdjjNuL/wlWHHK6+DrcIRhzSRLvtAu1XNdRtRGATYM51KSTI0P2nxJZFQM8rwCH6ga9KUw==",
"requires": { "requires": {
"isomorphic.js": "^0.2.4" "isomorphic.js": "^0.2.4"
} }
@@ -2494,9 +2494,9 @@
} }
}, },
"rollup": { "rollup": {
"version": "2.58.0", "version": "2.60.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.0.tgz",
"integrity": "sha512-NOXpusKnaRpbS7ZVSzcEXqxcLDOagN6iFS8p45RkoiMqPHDLwJm758UF05KlMoCRbLBTZsPOIa887gZJ1AiXvw==", "integrity": "sha512-cHdv9GWd58v58rdseC8e8XIaPUo8a9cgZpnCMMDGZFDZKEODOiPPEQFXLriWr/TjXzhPPmG5bkAztPsOARIcGQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"fsevents": "~2.3.2" "fsevents": "~2.3.2"

View File

@@ -1,10 +1,11 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.5.14", "version": "13.5.22",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.cjs", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",
"types": "./dist/src/index.d.ts", "types": "./dist/src/index.d.ts",
"type": "module",
"sideEffects": false, "sideEffects": false,
"funding": { "funding": {
"type": "GitHub Sponsors ❤", "type": "GitHub Sponsors ❤",
@@ -31,6 +32,7 @@
}, },
"./src/index.js": "./src/index.js", "./src/index.js": "./src/index.js",
"./tests/testHelper.js": "./tests/testHelper.js", "./tests/testHelper.js": "./tests/testHelper.js",
"./testHelper": "./dist/testHelper.mjs",
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"files": [ "files": [
@@ -38,6 +40,7 @@
"dist/src", "dist/src",
"src", "src",
"tests/testHelper.js", "tests/testHelper.js",
"dist/testHelper.mjs",
"sponsor-y.js" "sponsor-y.js"
], ],
"dictionaries": { "dictionaries": {
@@ -71,7 +74,7 @@
}, },
"homepage": "https://docs.yjs.dev", "homepage": "https://docs.yjs.dev",
"dependencies": { "dependencies": {
"lib0": "^0.2.42" "lib0": "^0.2.43"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-commonjs": "^17.0.0",
@@ -80,7 +83,7 @@
"http-server": "^0.12.3", "http-server": "^0.12.3",
"jsdoc": "^3.6.7", "jsdoc": "^3.6.7",
"markdownlint-cli": "^0.23.2", "markdownlint-cli": "^0.23.2",
"rollup": "^2.58.0", "rollup": "^2.60.0",
"standard": "^16.0.4", "standard": "^16.0.4",
"tui-jsdoc-template": "^1.2.2", "tui-jsdoc-template": "^1.2.2",
"typescript": "^4.4.4", "typescript": "^4.4.4",

View File

@@ -60,6 +60,23 @@ export default [{
sourcemap: true sourcemap: true
}, },
external: id => /^lib0\//.test(id) external: id => /^lib0\//.test(id)
}, {
input: './tests/testHelper.js',
output: {
name: 'Y',
file: 'dist/testHelper.mjs',
format: 'esm',
sourcemap: true
},
external: id => /^lib0\//.test(id) || id === 'yjs',
plugins: [{
resolveId (importee) {
if (importee === '../src/index.js') {
return 'yjs'
}
return null
}
}]
}, { }, {
input: './tests/index.js', input: './tests/index.js',
output: { output: {

View File

@@ -1,3 +1,4 @@
/** eslint-env browser */
export { export {
Doc, Doc,
@@ -26,12 +27,13 @@ export {
ContentString, ContentString,
ContentType, ContentType,
AbstractType, AbstractType,
RelativePosition,
getTypeChildren, getTypeChildren,
createRelativePositionFromTypeIndex, createRelativePositionFromTypeIndex,
createRelativePositionFromJSON, createRelativePositionFromJSON,
createAbsolutePositionFromRelativePosition, createAbsolutePositionFromRelativePosition,
compareRelativePositions, compareRelativePositions,
AbsolutePosition,
RelativePosition,
ID, ID,
createID, createID,
compareIDs, compareIDs,
@@ -40,6 +42,7 @@ export {
createSnapshot, createSnapshot,
createDeleteSet, createDeleteSet,
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
cleanupYTextFormatting,
snapshot, snapshot,
emptySnapshot, emptySnapshot,
findRootTypeKey, findRootTypeKey,
@@ -84,3 +87,25 @@ export {
diffUpdate, diffUpdate,
diffUpdateV2 diffUpdateV2
} from './internals.js' } from './internals.js'
const glo = /** @type {any} */ (typeof window !== 'undefined'
? window
: typeof global !== 'undefined' ? global : {})
const importIdentifier = '__ $YJS$ __'
if (glo[importIdentifier] === true) {
/**
* Dear reader of this warning message. Please take this seriously.
*
* If you see this message, please make sure that you only import one version of Yjs. In many cases,
* your package manager installs two versions of Yjs that are used by different packages within your project.
* Another reason for this message is that some parts of your project use the commonjs version of Yjs
* and others use the EcmaScript version of Yjs.
*
* This often leads to issues that are hard to debug. We often need to perform constructor checks,
* e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to
* do the constructor checks anymore - which might break the CRDT algorithm.
*/
console.warn('Yjs was already imported. Importing different versions of Yjs often leads to issues.')
}
glo[importIdentifier] = true

View File

@@ -5,6 +5,12 @@ import {
import * as error from 'lib0/error' import * as error from 'lib0/error'
/**
* @param {string} guid
* @param {Object<string, any>} opts
*/
const createDocFromOpts = (guid, opts) => new Doc({ guid, ...opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false })
/** /**
* @private * @private
*/ */
@@ -61,7 +67,7 @@ export class ContentDoc {
* @return {ContentDoc} * @return {ContentDoc}
*/ */
copy () { copy () {
return new ContentDoc(this.doc) return new ContentDoc(createDocFromOpts(this.doc.guid, this.opts))
} }
/** /**
@@ -132,4 +138,4 @@ export class ContentDoc {
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentDoc} * @return {ContentDoc}
*/ */
export const readContentDoc = decoder => new ContentDoc(new Doc({ guid: decoder.readString(), ...decoder.readAny() })) export const readContentDoc = decoder => new ContentDoc(createDocFromOpts(decoder.readString(), decoder.readAny()))

View File

@@ -706,9 +706,9 @@ export class YTextEvent extends YEvent {
addOp() addOp()
} }
if (value === null) { if (value === null) {
attributes[key] = value
} else {
delete attributes[key] delete attributes[key]
} else {
attributes[key] = value
} }
} else { } else {
item.delete(transaction) item.delete(transaction)

View File

@@ -25,8 +25,10 @@ export const generateNewClientId = random.uint32
* @property {boolean} [DocOpts.gc=true] Disable garbage collection (default: gc=true) * @property {boolean} [DocOpts.gc=true] Disable garbage collection (default: gc=true)
* @property {function(Item):boolean} [DocOpts.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item. * @property {function(Item):boolean} [DocOpts.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item.
* @property {string} [DocOpts.guid] Define a globally unique identifier for this document * @property {string} [DocOpts.guid] Define a globally unique identifier for this document
* @property {string | null} [DocOpts.collectionid] Associate this document with a collection. This only plays a role if your provider has a concept of collection.
* @property {any} [DocOpts.meta] Any kind of meta information you want to associate with this document. If this is a subdocument, remote peers will store the meta information as well. * @property {any} [DocOpts.meta] Any kind of meta information you want to associate with this document. If this is a subdocument, remote peers will store the meta information as well.
* @property {boolean} [DocOpts.autoLoad] If a subdocument, automatically load document. If this is a subdocument, remote peers will load the document as well automatically. * @property {boolean} [DocOpts.autoLoad] If a subdocument, automatically load document. If this is a subdocument, remote peers will load the document as well automatically.
* @property {boolean} [DocOpts.shouldLoad] Whether the document should be synced by the provider now. This is toggled to true when you call ydoc.load()
*/ */
/** /**
@@ -37,12 +39,13 @@ export class Doc extends Observable {
/** /**
* @param {DocOpts} [opts] configuration * @param {DocOpts} [opts] configuration
*/ */
constructor ({ guid = random.uuidv4(), gc = true, gcFilter = () => true, meta = null, autoLoad = false } = {}) { constructor ({ guid = random.uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) {
super() super()
this.gc = gc this.gc = gc
this.gcFilter = gcFilter this.gcFilter = gcFilter
this.clientID = generateNewClientId() this.clientID = generateNewClientId()
this.guid = guid this.guid = guid
this.collectionid = collectionid
/** /**
* @type {Map<string, AbstractType<YEvent>>} * @type {Map<string, AbstractType<YEvent>>}
*/ */
@@ -65,7 +68,7 @@ export class Doc extends Observable {
* @type {Item?} * @type {Item?}
*/ */
this._item = null this._item = null
this.shouldLoad = autoLoad this.shouldLoad = shouldLoad
this.autoLoad = autoLoad this.autoLoad = autoLoad
this.meta = meta this.meta = meta
} }
@@ -246,16 +249,12 @@ export class Doc extends Observable {
if (item !== null) { if (item !== null) {
this._item = null this._item = null
const content = /** @type {ContentDoc} */ (item.content) const content = /** @type {ContentDoc} */ (item.content)
if (item.deleted) { content.doc = new Doc({ guid: this.guid, ...content.opts, shouldLoad: false })
// @ts-ignore content.doc._item = item
content.doc = null
} else {
content.doc = new Doc({ guid: this.guid, ...content.opts })
content.doc._item = item
}
transact(/** @type {any} */ (item).parent.doc, transaction => { transact(/** @type {any} */ (item).parent.doc, transaction => {
const doc = content.doc
if (!item.deleted) { if (!item.deleted) {
transaction.subdocsAdded.add(content.doc) transaction.subdocsAdded.add(doc)
} }
transaction.subdocsRemoved.add(this) transaction.subdocsRemoved.add(this)
}, null, true) }, null, true)

View File

@@ -331,8 +331,8 @@ const cleanupTransactions = (transactionCleanups, i) => {
} }
} }
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) { if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
doc.clientID = generateNewClientId()
logging.print(logging.ORANGE, logging.BOLD, '[yjs] ', logging.UNBOLD, logging.RED, 'Changed the client-id because another client seems to be using it.') logging.print(logging.ORANGE, logging.BOLD, '[yjs] ', logging.UNBOLD, logging.RED, 'Changed the client-id because another client seems to be using it.')
doc.clientID = generateNewClientId()
} }
// @todo Merge all the transactions into one and provide send the data as a single update message // @todo Merge all the transactions into one and provide send the data as a single update message
doc.emit('afterTransactionCleanup', [transaction, doc]) doc.emit('afterTransactionCleanup', [transaction, doc])
@@ -350,11 +350,19 @@ const cleanupTransactions = (transactionCleanups, i) => {
doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction]) doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction])
} }
} }
transaction.subdocsAdded.forEach(subdoc => doc.subdocs.add(subdoc)) const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction
transaction.subdocsRemoved.forEach(subdoc => doc.subdocs.delete(subdoc)) if (subdocsAdded.size > 0 || subdocsRemoved.size > 0 || subdocsLoaded.size > 0) {
subdocsAdded.forEach(subdoc => {
doc.emit('subdocs', [{ loaded: transaction.subdocsLoaded, added: transaction.subdocsAdded, removed: transaction.subdocsRemoved }]) subdoc.clientID = doc.clientID
transaction.subdocsRemoved.forEach(subdoc => subdoc.destroy()) if (subdoc.collectionid == null) {
subdoc.collectionid = doc.collectionid
}
doc.subdocs.add(subdoc)
})
subdocsRemoved.forEach(subdoc => doc.subdocs.delete(subdoc))
doc.emit('subdocs', [{ loaded: subdocsLoaded, added: subdocsAdded, removed: subdocsRemoved }, doc, transaction])
subdocsRemoved.forEach(subdoc => subdoc.destroy())
}
if (transactionCleanups.length <= i + 1) { if (transactionCleanups.length <= i + 1) {
doc._transactionCleanups = [] doc._transactionCleanups = []

View File

@@ -601,7 +601,7 @@ export const decodeStateVector = decodedState => readStateVector(new DSDecoderV1
*/ */
export const writeStateVector = (encoder, sv) => { export const writeStateVector = (encoder, sv) => {
encoding.writeVarUint(encoder.restEncoder, sv.size) encoding.writeVarUint(encoder.restEncoder, sv.size)
sv.forEach((clock, client) => { Array.from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
encoding.writeVarUint(encoder.restEncoder, client) // @todo use a special client decoder that is based on mapping encoding.writeVarUint(encoder.restEncoder, client) // @todo use a special client decoder that is based on mapping
encoding.writeVarUint(encoder.restEncoder, clock) encoding.writeVarUint(encoder.restEncoder, clock)
}) })

View File

@@ -88,7 +88,7 @@ export const testSubdoc = tc => {
subdocs.get('a').load() subdocs.get('a').load()
t.compare(event, [[], [], ['a']]) t.compare(event, [[], [], ['a']])
subdocs.set('b', new Y.Doc({ guid: 'a' })) subdocs.set('b', new Y.Doc({ guid: 'a', shouldLoad: false }))
t.compare(event, [['a'], [], []]) t.compare(event, [['a'], [], []])
subdocs.get('b').load() subdocs.get('b').load()
t.compare(event, [[], [], ['a']]) t.compare(event, [[], [], ['a']])
@@ -124,3 +124,107 @@ export const testSubdoc = tc => {
t.compare(Array.from(doc2.getSubdocGuids()), ['a', 'c']) t.compare(Array.from(doc2.getSubdocGuids()), ['a', 'c'])
} }
} }
/**
* @param {t.TestCase} tc
*/
export const testSubdocLoadEdgeCases = tc => {
const ydoc = new Y.Doc()
const yarray = ydoc.getArray()
const subdoc1 = new Y.Doc()
/**
* @type {any}
*/
let lastEvent = null
ydoc.on('subdocs', event => {
lastEvent = event
})
yarray.insert(0, [subdoc1])
t.assert(subdoc1.shouldLoad)
t.assert(subdoc1.autoLoad === false)
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc1))
t.assert(lastEvent !== null && lastEvent.added.has(subdoc1))
// destroy and check whether lastEvent adds it again to added (it shouldn't)
subdoc1.destroy()
const subdoc2 = yarray.get(0)
t.assert(subdoc1 !== subdoc2)
t.assert(lastEvent !== null && lastEvent.added.has(subdoc2))
t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc2))
// load
subdoc2.load()
t.assert(lastEvent !== null && !lastEvent.added.has(subdoc2))
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc2))
// apply from remote
const ydoc2 = new Y.Doc()
ydoc2.on('subdocs', event => {
lastEvent = event
})
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc))
const subdoc3 = ydoc2.getArray().get(0)
t.assert(subdoc3.shouldLoad === false)
t.assert(subdoc3.autoLoad === false)
t.assert(lastEvent !== null && lastEvent.added.has(subdoc3))
t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc3))
// load
subdoc3.load()
t.assert(subdoc3.shouldLoad)
t.assert(lastEvent !== null && !lastEvent.added.has(subdoc3))
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc3))
}
/**
* @param {t.TestCase} tc
*/
export const testSubdocLoadEdgeCasesAutoload = tc => {
const ydoc = new Y.Doc()
const yarray = ydoc.getArray()
const subdoc1 = new Y.Doc({ autoLoad: true })
/**
* @type {any}
*/
let lastEvent = null
ydoc.on('subdocs', event => {
lastEvent = event
})
yarray.insert(0, [subdoc1])
t.assert(subdoc1.shouldLoad)
t.assert(subdoc1.autoLoad)
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc1))
t.assert(lastEvent !== null && lastEvent.added.has(subdoc1))
// destroy and check whether lastEvent adds it again to added (it shouldn't)
subdoc1.destroy()
const subdoc2 = yarray.get(0)
t.assert(subdoc1 !== subdoc2)
t.assert(lastEvent !== null && lastEvent.added.has(subdoc2))
t.assert(lastEvent !== null && !lastEvent.loaded.has(subdoc2))
// load
subdoc2.load()
t.assert(lastEvent !== null && !lastEvent.added.has(subdoc2))
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc2))
// apply from remote
const ydoc2 = new Y.Doc()
ydoc2.on('subdocs', event => {
lastEvent = event
})
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc))
const subdoc3 = ydoc2.getArray().get(0)
t.assert(subdoc1.shouldLoad)
t.assert(subdoc1.autoLoad)
t.assert(lastEvent !== null && lastEvent.added.has(subdoc3))
t.assert(lastEvent !== null && lastEvent.loaded.has(subdoc3))
}
/**
* @param {t.TestCase} tc
*/
export const testSubdocsUndo = tc => {
const ydoc = new Y.Doc()
const elems = ydoc.getXmlFragment()
const undoManager = new Y.UndoManager(elems)
const subdoc = new Y.Doc()
// @ts-ignore
elems.insert(0, [subdoc])
undoManager.undo()
undoManager.redo()
t.assert(elems.length === 1)
}

View File

@@ -1,9 +1,9 @@
import * as Y from '../src/internals' import * as Y from '../src/index.js'
import * as t from 'lib0/testing' import * as t from 'lib0/testing'
/** /**
* @param {Y.YText} ytext * @param {Y.Text} ytext
*/ */
const checkRelativePositions = ytext => { const checkRelativePositions = ytext => {
// test if all positions are encoded and restored correctly // test if all positions are encoded and restored correctly

View File

@@ -1,17 +1,17 @@
import { createDocFromSnapshot, Doc, snapshot, YMap } from '../src/internals' import * as Y from '../src/index.js'
import * as t from 'lib0/testing' import * as t from 'lib0/testing'
import { init } from './testHelper' import { init } from './testHelper.js'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testBasicRestoreSnapshot = tc => { export const testBasicRestoreSnapshot = tc => {
const doc = new Doc({ gc: false }) const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, ['hello']) doc.getArray('array').insert(0, ['hello'])
const snap = snapshot(doc) const snap = Y.snapshot(doc)
doc.getArray('array').insert(1, ['world']) doc.getArray('array').insert(1, ['world'])
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['hello']) t.compare(docRestored.getArray('array').toArray(), ['hello'])
t.compare(doc.getArray('array').toArray(), ['hello', 'world']) t.compare(doc.getArray('array').toArray(), ['hello', 'world'])
@@ -21,19 +21,19 @@ export const testBasicRestoreSnapshot = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testEmptyRestoreSnapshot = tc => { export const testEmptyRestoreSnapshot = tc => {
const doc = new Doc({ gc: false }) const doc = new Y.Doc({ gc: false })
const snap = snapshot(doc) const snap = Y.snapshot(doc)
snap.sv.set(9999, 0) snap.sv.set(9999, 0)
doc.getArray().insert(0, ['world']) doc.getArray().insert(0, ['world'])
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray().toArray(), []) t.compare(docRestored.getArray().toArray(), [])
t.compare(doc.getArray().toArray(), ['world']) t.compare(doc.getArray().toArray(), ['world'])
// now this snapshot reflects the latest state. It shoult still work. // now this snapshot reflects the latest state. It shoult still work.
const snap2 = snapshot(doc) const snap2 = Y.snapshot(doc)
const docRestored2 = createDocFromSnapshot(doc, snap2) const docRestored2 = Y.createDocFromSnapshot(doc, snap2)
t.compare(docRestored2.getArray().toArray(), ['world']) t.compare(docRestored2.getArray().toArray(), ['world'])
} }
@@ -41,15 +41,15 @@ export const testEmptyRestoreSnapshot = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testRestoreSnapshotWithSubType = tc => { export const testRestoreSnapshotWithSubType = tc => {
const doc = new Doc({ gc: false }) const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, [new YMap()]) doc.getArray('array').insert(0, [new Y.Map()])
const subMap = doc.getArray('array').get(0) const subMap = doc.getArray('array').get(0)
subMap.set('key1', 'value1') subMap.set('key1', 'value1')
const snap = snapshot(doc) const snap = Y.snapshot(doc)
subMap.set('key2', 'value2') subMap.set('key2', 'value2')
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toJSON(), [{ t.compare(docRestored.getArray('array').toJSON(), [{
key1: 'value1' key1: 'value1'
@@ -64,13 +64,13 @@ export const testRestoreSnapshotWithSubType = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testRestoreDeletedItem1 = tc => { export const testRestoreDeletedItem1 = tc => {
const doc = new Doc({ gc: false }) const doc = new Y.Doc({ gc: false })
doc.getArray('array').insert(0, ['item1', 'item2']) doc.getArray('array').insert(0, ['item1', 'item2'])
const snap = snapshot(doc) const snap = Y.snapshot(doc)
doc.getArray('array').delete(0) doc.getArray('array').delete(0)
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['item1', 'item2']) t.compare(docRestored.getArray('array').toArray(), ['item1', 'item2'])
t.compare(doc.getArray('array').toArray(), ['item2']) t.compare(doc.getArray('array').toArray(), ['item2'])
@@ -80,15 +80,15 @@ export const testRestoreDeletedItem1 = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testRestoreLeftItem = tc => { export const testRestoreLeftItem = tc => {
const doc = new 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)
doc.getArray('array').insert(0, ['item0']) doc.getArray('array').insert(0, ['item0'])
const snap = snapshot(doc) const snap = Y.snapshot(doc)
doc.getArray('array').delete(1) doc.getArray('array').delete(1)
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['item0', 'item1']) t.compare(docRestored.getArray('array').toArray(), ['item0', 'item1'])
t.compare(doc.getArray('array').toArray(), ['item0']) t.compare(doc.getArray('array').toArray(), ['item0'])
@@ -98,13 +98,13 @@ export const testRestoreLeftItem = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testDeletedItemsBase = tc => { export const testDeletedItemsBase = tc => {
const doc = new 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)
const snap = snapshot(doc) const snap = Y.snapshot(doc)
doc.getArray('array').insert(0, ['item0']) doc.getArray('array').insert(0, ['item0'])
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), []) t.compare(docRestored.getArray('array').toArray(), [])
t.compare(doc.getArray('array').toArray(), ['item0']) t.compare(doc.getArray('array').toArray(), ['item0'])
@@ -114,13 +114,13 @@ export const testDeletedItemsBase = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testDeletedItems2 = tc => { export const testDeletedItems2 = tc => {
const doc = new 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)
const snap = snapshot(doc) const snap = Y.snapshot(doc)
doc.getArray('array').insert(0, ['item0']) doc.getArray('array').insert(0, ['item0'])
const docRestored = createDocFromSnapshot(doc, snap) const docRestored = Y.createDocFromSnapshot(doc, snap)
t.compare(docRestored.getArray('array').toArray(), ['item1', 'item3']) t.compare(docRestored.getArray('array').toArray(), ['item1', 'item3'])
t.compare(doc.getArray('array').toArray(), ['item0', 'item1', 'item3']) t.compare(doc.getArray('array').toArray(), ['item0', 'item1', 'item3'])
@@ -140,11 +140,11 @@ export const testDependentChanges = tc => {
} }
/** /**
* @type Doc * @type {Y.Doc}
*/ */
const doc0 = array0.doc const doc0 = array0.doc
/** /**
* @type Doc * @type {Y.Doc}
*/ */
const doc1 = array1.doc const doc1 = array1.doc
@@ -156,16 +156,16 @@ export const testDependentChanges = tc => {
array1.insert(1, ['user2item1']) array1.insert(1, ['user2item1'])
testConnector.syncAll() testConnector.syncAll()
const snap = snapshot(array0.doc) const snap = Y.snapshot(array0.doc)
array0.insert(2, ['user1item2']) array0.insert(2, ['user1item2'])
testConnector.syncAll() testConnector.syncAll()
array1.insert(3, ['user2item2']) array1.insert(3, ['user2item2'])
testConnector.syncAll() testConnector.syncAll()
const docRestored0 = createDocFromSnapshot(array0.doc, snap) const docRestored0 = Y.createDocFromSnapshot(array0.doc, snap)
t.compare(docRestored0.getArray('array').toArray(), ['user1item1', 'user2item1']) t.compare(docRestored0.getArray('array').toArray(), ['user1item1', 'user2item1'])
const docRestored1 = 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'])
} }

View File

@@ -3,10 +3,10 @@ import * as t from 'lib0/testing'
import * as prng from 'lib0/prng' import * as prng from 'lib0/prng'
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 syncProtocol from 'y-protocols/sync.js' import * as syncProtocol from 'y-protocols/sync'
import * as object from 'lib0/object' import * as object from 'lib0/object'
import * as Y from '../src/internals.js' import * as Y from '../src/index.js'
export * from '../src/internals.js' export * from '../src/index.js'
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// @ts-ignore // @ts-ignore
@@ -279,7 +279,7 @@ export class TestConnector {
* @param {t.TestCase} tc * @param {t.TestCase} tc
* @param {{users?:number}} conf * @param {{users?:number}} conf
* @param {InitTestObjectCallback<T>} [initTestObject] * @param {InitTestObjectCallback<T>} [initTestObject]
* @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.YArray<any>,array1:Y.YArray<any>,array2:Y.YArray<any>,map0:Y.YMap<any>,map1:Y.YMap<any>,map2:Y.YMap<any>,map3:Y.YMap<any>,text0:Y.YText,text1:Y.YText,text2:Y.YText,xml0:Y.YXmlElement,xml1:Y.YXmlElement,xml2:Y.YXmlElement}} * @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Array<any>,array1:Y.Array<any>,array2:Y.Array<any>,map0:Y.Map<any>,map1:Y.Map<any>,map2:Y.Map<any>,map3:Y.Map<any>,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:Y.XmlElement,xml1:Y.XmlElement,xml2:Y.XmlElement}}
*/ */
export const init = (tc, { users = 5 } = {}, initTestObject) => { export const init = (tc, { users = 5 } = {}, initTestObject) => {
/** /**
@@ -304,7 +304,7 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
result.users.push(y) result.users.push(y)
result['array' + i] = y.getArray('array') result['array' + i] = y.getArray('array')
result['map' + i] = y.getMap('map') result['map' + i] = y.getMap('map')
result['xml' + i] = y.get('xml', Y.YXmlElement) result['xml' + i] = y.get('xml', Y.XmlElement)
result['text' + i] = y.getText('text') result['text' + i] = y.getText('text')
} }
testConnector.syncAll() testConnector.syncAll()
@@ -335,7 +335,7 @@ export const compare = users => {
users.push(.../** @type {any} */(mergedDocs)) users.push(.../** @type {any} */(mergedDocs))
const userArrayValues = users.map(u => u.getArray('array').toJSON()) const userArrayValues = users.map(u => u.getArray('array').toJSON())
const userMapValues = users.map(u => u.getMap('map').toJSON()) const userMapValues = users.map(u => u.getMap('map').toJSON())
const userXmlValues = users.map(u => u.get('xml', Y.YXmlElement).toString()) const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString())
const userTextValues = users.map(u => u.getText('text').toDelta()) const userTextValues = users.map(u => u.getText('text').toDelta())
for (const u of users) { for (const u of users) {
t.assert(u.store.pendingDs === null) t.assert(u.store.pendingDs === null)
@@ -370,7 +370,7 @@ export const compare = users => {
} }
return true return true
}) })
t.compare(Y.getStateVector(users[i].store), Y.getStateVector(users[i + 1].store)) t.compare(Y.encodeStateVector(users[i]), Y.encodeStateVector(users[i + 1]))
compareDS(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store)) compareDS(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)
} }
@@ -385,8 +385,8 @@ export const compare = users => {
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id)) export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
/** /**
* @param {Y.StructStore} ss1 * @param {import('../src/internals').StructStore} ss1
* @param {Y.StructStore} ss2 * @param {import('../src/internals').StructStore} ss2
*/ */
export const compareStructStores = (ss1, ss2) => { export const compareStructStores = (ss1, ss2) => {
t.assert(ss1.clients.size === ss2.clients.size) t.assert(ss1.clients.size === ss2.clients.size)
@@ -428,13 +428,13 @@ export const compareStructStores = (ss1, ss2) => {
} }
/** /**
* @param {Y.DeleteSet} ds1 * @param {import('../src/internals').DeleteSet} ds1
* @param {Y.DeleteSet} ds2 * @param {import('../src/internals').DeleteSet} ds2
*/ */
export const compareDS = (ds1, ds2) => { export const compareDS = (ds1, ds2) => {
t.assert(ds1.clients.size === ds2.clients.size) t.assert(ds1.clients.size === ds2.clients.size)
ds1.clients.forEach((deleteItems1, client) => { ds1.clients.forEach((deleteItems1, client) => {
const deleteItems2 = /** @type {Array<Y.DeleteItem>} */ (ds2.clients.get(client)) const deleteItems2 = /** @type {Array<import('../src/internals').DeleteItem>} */ (ds2.clients.get(client))
t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length) t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
for (let i = 0; i < deleteItems1.length; i++) { for (let i = 0; i < deleteItems1.length; i++) {
const di1 = deleteItems1[i] const di1 = deleteItems1[i]

View File

@@ -1,9 +1,5 @@
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
import {
UndoManager
} from '../src/internals.js'
import * as Y from '../src/index.js' import * as Y from '../src/index.js'
import * as t from 'lib0/testing' import * as t from 'lib0/testing'
@@ -12,7 +8,7 @@ import * as t from 'lib0/testing'
*/ */
export const testUndoText = tc => { export const testUndoText = tc => {
const { testConnector, text0, text1 } = init(tc, { users: 3 }) const { testConnector, text0, text1 } = init(tc, { users: 3 })
const undoManager = new UndoManager(text0) const undoManager = new Y.UndoManager(text0)
// items that are added & deleted in the same transaction won't be undo // items that are added & deleted in the same transaction won't be undo
text0.insert(0, 'test') text0.insert(0, 'test')
@@ -81,7 +77,7 @@ export const testDoubleUndo = tc => {
export const testUndoMap = tc => { export const testUndoMap = tc => {
const { testConnector, map0, map1 } = init(tc, { users: 2 }) const { testConnector, map0, map1 } = init(tc, { users: 2 })
map0.set('a', 0) map0.set('a', 0)
const undoManager = new UndoManager(map0) const undoManager = new Y.UndoManager(map0)
map0.set('a', 1) map0.set('a', 1)
undoManager.undo() undoManager.undo()
t.assert(map0.get('a') === 0) t.assert(map0.get('a') === 0)
@@ -120,7 +116,7 @@ export const testUndoMap = tc => {
*/ */
export const testUndoArray = tc => { export const testUndoArray = tc => {
const { testConnector, array0, array1 } = init(tc, { users: 3 }) const { testConnector, array0, array1 } = init(tc, { users: 3 })
const undoManager = new UndoManager(array0) const undoManager = new Y.UndoManager(array0)
array0.insert(0, [1, 2, 3]) array0.insert(0, [1, 2, 3])
array1.insert(0, [4, 5, 6]) array1.insert(0, [4, 5, 6])
testConnector.syncAll() testConnector.syncAll()
@@ -171,7 +167,7 @@ export const testUndoArray = tc => {
*/ */
export const testUndoXml = tc => { export const testUndoXml = tc => {
const { xml0 } = init(tc, { users: 3 }) const { xml0 } = init(tc, { users: 3 })
const undoManager = new UndoManager(xml0) const undoManager = new Y.UndoManager(xml0)
const child = new Y.XmlElement('p') const child = new Y.XmlElement('p')
xml0.insert(0, [child]) xml0.insert(0, [child])
const textchild = new Y.XmlText('content') const textchild = new Y.XmlText('content')
@@ -196,7 +192,7 @@ export const testUndoXml = tc => {
*/ */
export const testUndoEvents = tc => { export const testUndoEvents = tc => {
const { text0 } = init(tc, { users: 3 }) const { text0 } = init(tc, { users: 3 })
const undoManager = new UndoManager(text0) const undoManager = new Y.UndoManager(text0)
let counter = 0 let counter = 0
let receivedMetadata = -1 let receivedMetadata = -1
undoManager.on('stack-item-added', /** @param {any} event */ event => { undoManager.on('stack-item-added', /** @param {any} event */ event => {
@@ -222,7 +218,7 @@ export const testUndoEvents = tc => {
export const testTrackClass = tc => { export const testTrackClass = tc => {
const { users, text0 } = init(tc, { users: 3 }) const { users, text0 } = init(tc, { users: 3 })
// only track origins that are numbers // only track origins that are numbers
const undoManager = new UndoManager(text0, { trackedOrigins: new Set([Number]) }) const undoManager = new Y.UndoManager(text0, { trackedOrigins: new Set([Number]) })
users[0].transact(() => { users[0].transact(() => {
text0.insert(0, 'abc') text0.insert(0, 'abc')
}, 42) }, 42)
@@ -240,8 +236,8 @@ export const testTypeScope = tc => {
const text0 = new Y.Text() const text0 = new Y.Text()
const text1 = new Y.Text() const text1 = new Y.Text()
array0.insert(0, [text0, text1]) array0.insert(0, [text0, text1])
const undoManager = new UndoManager(text0) const undoManager = new Y.UndoManager(text0)
const undoManagerBoth = new UndoManager([text0, text1]) const undoManagerBoth = new Y.UndoManager([text0, text1])
text1.insert(0, 'abc') text1.insert(0, 'abc')
t.assert(undoManager.undoStack.length === 0) t.assert(undoManager.undoStack.length === 0)
t.assert(undoManagerBoth.undoStack.length === 1) t.assert(undoManagerBoth.undoStack.length === 1)
@@ -260,7 +256,7 @@ export const testUndoDeleteFilter = tc => {
* @type {Array<Y.Map<any>>} * @type {Array<Y.Map<any>>}
*/ */
const array0 = /** @type {any} */ (init(tc, { users: 3 }).array0) const array0 = /** @type {any} */ (init(tc, { users: 3 }).array0)
const undoManager = new UndoManager(array0, { deleteFilter: item => !(item instanceof Y.Item) || (item.content instanceof Y.ContentType && item.content.type._map.size === 0) }) const undoManager = new Y.UndoManager(array0, { deleteFilter: item => !(item instanceof Y.Item) || (item.content instanceof Y.ContentType && item.content.type._map.size === 0) })
const map0 = new Y.Map() const map0 = new Y.Map()
map0.set('hi', 1) map0.set('hi', 1)
const map1 = new Y.Map() const map1 = new Y.Map()

View File

@@ -157,7 +157,7 @@ export const testGetDeltaWithEmbeds = tc => {
export const testTypesAsEmbed = tc => { export const testTypesAsEmbed = tc => {
const { text0, text1, testConnector } = init(tc, { users: 2 }) const { text0, text1, testConnector } = init(tc, { users: 2 })
text0.applyDelta([{ text0.applyDelta([{
insert: new Y.YMap([['key', 'val']]) insert: new Y.Map([['key', 'val']])
}]) }])
t.compare(text0.toDelta()[0].insert.toJSON(), { key: 'val' }) t.compare(text0.toDelta()[0].insert.toJSON(), { key: 'val' })
let firedEvent = false let firedEvent = false
@@ -288,6 +288,41 @@ export const testFormattingRemovedInMidText = tc => {
t.assert(Y.getTypeChildren(text0).length === 3) t.assert(Y.getTypeChildren(text0).length === 3)
} }
/**
* Reported in https://github.com/yjs/yjs/issues/344
*
* @param {t.TestCase} tc
*/
export const testFormattingDeltaUnnecessaryAttributeChange = tc => {
const { text0, text1, testConnector } = init(tc, { users: 2 })
text0.insert(0, '\n', {
PARAGRAPH_STYLES: 'normal',
LIST_STYLES: 'bullet'
})
text0.insert(1, 'abc', {
PARAGRAPH_STYLES: 'normal'
})
testConnector.flushAllMessages()
/**
* @type {Array<any>}
*/
const deltas = []
text0.observe(event => {
deltas.push(event.delta)
})
text1.observe(event => {
deltas.push(event.delta)
})
text1.format(0, 1, { LIST_STYLES: 'number' })
testConnector.flushAllMessages()
const filteredDeltas = deltas.filter(d => d.length > 0)
t.assert(filteredDeltas.length === 2)
t.compare(filteredDeltas[0], [
{ retain: 1, attributes: { LIST_STYLES: 'number' } }
])
t.compare(filteredDeltas[0], filteredDeltas[1])
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -679,7 +714,7 @@ const qChanges = [
if (prng.bool(gen)) { if (prng.bool(gen)) {
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' }) ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' })
} else { } else {
ytext.insertEmbed(insertPos, new Y.YMap([[prng.word(gen), prng.word(gen)]])) ytext.insertEmbed(insertPos, new Y.Map([[prng.word(gen), prng.word(gen)]]))
} }
}, },
/** /**