Compare commits

...

8 Commits

Author SHA1 Message Date
Kevin Jahns
00ef472d68 13.6.2 2023-06-08 11:19:06 +02:00
Kevin Jahns
719858201a implement snapshotContainsUpdate 2023-06-08 11:14:49 +02:00
Kevin Jahns
5db1eed181 Merge pull request #528 from jamesgpearce/patch-1
Add missing getting-started steps
2023-05-05 12:21:56 +02:00
Kevin Jahns
2e9a648d08 13.6.1 2023-05-04 11:29:08 +02:00
Kevin Jahns
83712cb1a6 update typings of getAttributes 2023-05-04 11:26:11 +02:00
Kevin Jahns
30b56d5ae9 Enable typings for inserting custom attrs in YXmlElement - fixes #531 2023-05-04 10:07:05 +02:00
James Pearce
61eeaef226 Add missing getting-started steps 2023-04-30 16:31:46 -04:00
Kevin Jahns
adaa95ebb8 add example to createDocFromSnapshot - #159 2023-04-27 18:08:28 +02:00
12 changed files with 195 additions and 68 deletions

View File

@@ -169,6 +169,9 @@ PORT=1234 node ./node_modules/y-websocket/bin/server.js
### Example: Observe types
```js
import * as Y from 'yjs';
const doc = new Y.Doc();
const yarray = doc.getArray('my-array')
yarray.observe(event => {
console.log('yarray was modified')

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "yjs",
"version": "13.6.0",
"version": "13.6.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "yjs",
"version": "13.6.0",
"version": "13.6.2",
"license": "MIT",
"dependencies": {
"lib0": "^0.2.74"

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.6.0",
"version": "13.6.2",
"description": "Shared Editing Library",
"main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs",

View File

@@ -92,7 +92,9 @@ export {
convertUpdateFormatV2ToV1,
obfuscateUpdate,
obfuscateUpdateV2,
UpdateEncoderV1
UpdateEncoderV1,
equalDeleteSets,
snapshotContainsUpdate
} from './internals.js'
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'

View File

@@ -206,9 +206,11 @@ export class YMap extends AbstractType {
/**
* 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 {MapType} value The value of the element to add
* @param {VAL} value The value of the element to add
* @return {VAL}
*/
set (key, value) {
if (this.doc !== null) {

View File

@@ -1,3 +1,4 @@
import * as object from 'lib0/object'
import {
YXmlFragment,
@@ -12,12 +13,18 @@ import {
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
} from '../internals.js'
/**
* @typedef {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} ValueTypes
*/
/**
* An YXmlElement imitates the behavior of a
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
*
* * An YXmlElement has attributes (key value pairs)
* * An YXmlElement has childElements that must inherit from YXmlElement
*
* @template {{ [key: string]: ValueTypes }} [KV={ [key: string]: string }]
*/
export class YXmlElement extends YXmlFragment {
constructor (nodeName = 'UNDEFINED') {
@@ -73,14 +80,19 @@ export class YXmlElement extends YXmlFragment {
}
/**
* @return {YXmlElement}
* @return {YXmlElement<KV>}
*/
clone () {
/**
* @type {YXmlElement<KV>}
*/
const el = new YXmlElement(this.nodeName)
const attrs = this.getAttributes()
for (const key in attrs) {
el.setAttribute(key, attrs[key])
}
object.forEach(attrs, (value, key) => {
if (typeof value === 'string') {
el.setAttribute(key, value)
}
})
// @ts-ignore
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
return el
@@ -116,7 +128,7 @@ export class YXmlElement extends YXmlFragment {
/**
* 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
*/
@@ -133,8 +145,10 @@ export class YXmlElement extends YXmlFragment {
/**
* Sets or updates an attribute.
*
* @param {String} attributeName The attribute name that is to be set.
* @param {String} attributeValue The attribute value that is to be set.
* @template {keyof KV & string} KEY
*
* @param {KEY} attributeName The attribute name that is to be set.
* @param {KV[KEY]} attributeValue The attribute value that is to be set.
*
* @public
*/
@@ -151,9 +165,11 @@ export class YXmlElement extends YXmlFragment {
/**
* 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.
* @return {String} The queried attribute value.
* @return {KV[KEY]|undefined} The queried attribute value.
*
* @public
*/
@@ -164,7 +180,7 @@ export class YXmlElement extends YXmlFragment {
/**
* 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.
*
* @public
@@ -176,12 +192,12 @@ export class YXmlElement extends YXmlFragment {
/**
* 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
*/
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 attrs = this.getAttributes()
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 => {
dom.appendChild(yxml.toDOM(_document, hooks, binding))

View File

@@ -328,3 +328,23 @@ export const readAndApplyDeleteSet = (decoder, transaction, store) => {
}
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
}

View File

@@ -15,7 +15,10 @@ import {
findIndexSS,
UpdateEncoderV2,
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'
import * as map from 'lib0/map'
@@ -147,12 +150,20 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
getItemCleanStart(transaction, createID(client, clock))
}
})
iterateDeletedStructs(transaction, snapshot.ds, item => {})
iterateDeletedStructs(transaction, snapshot.ds, _item => {})
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 {Snapshot} snapshot
* @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()) => {
if (originDoc.gc) {
// 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
@@ -199,3 +210,28 @@ export const createDocFromSnapshot = (originDoc, snapshot, newDoc = new Doc()) =
applyUpdateV2(newDoc, encoder.toUint8Array(), 'snapshot')
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)

View File

@@ -88,7 +88,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
sm.set(client, clock)
}
})
getStateVector(store).forEach((clock, client) => {
getStateVector(store).forEach((_clock, client) => {
if (!_sm.has(client)) {
sm.set(client, 0)
}
@@ -98,8 +98,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
// Write items with higher client ids first
// This heavily improves the conflict algorithm.
array.from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
// @ts-ignore
writeStructs(encoder, store.clients.get(client), client, clock)
writeStructs(encoder, /** @type {Array<GC|Item>} */ (store.clients.get(client)), client, clock)
})
}

View File

@@ -3,9 +3,21 @@ import * as t from 'lib0/testing'
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 })
doc.getArray('array').insert(0, ['hello'])
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 snap = Y.snapshot(doc)
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 })
doc.getArray('array').insert(0, [new Y.Map()])
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 })
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 })
doc.getArray('array').insert(0, ['item1'])
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 })
doc.getArray('array').insert(0, ['item1'])
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 })
doc.getArray('array').insert(0, ['item1', 'item2', 'item3'])
doc.getArray('array').delete(1)
@@ -169,3 +181,28 @@ export const testDependentChanges = tc => {
const docRestored1 = Y.createDocFromSnapshot(array1.doc, snap)
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]))
}

View File

@@ -356,8 +356,9 @@ export const compare = users => {
return true
})
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)
t.compare(Y.encodeSnapshot(Y.snapshot(users[i])), Y.encodeSnapshot(Y.snapshot(users[i + 1])))
}
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
* @callback InitTestObjectCallback

View File

@@ -3,6 +3,33 @@ import * as Y from '../src/index.js'
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
*/
@@ -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 ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
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 yxml = ydoc.getXmlFragment()
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 yxml = ydoc.getXmlFragment()
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 yxml = ydoc.getXmlFragment()
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 yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
const delta = [