diff --git a/src/index.js b/src/index.js index d23c37f5..781e8eb4 100644 --- a/src/index.js +++ b/src/index.js @@ -92,7 +92,9 @@ export { convertUpdateFormatV2ToV1, obfuscateUpdate, obfuscateUpdateV2, - UpdateEncoderV1 + UpdateEncoderV1, + equalDeleteSets, + snapshotContainsUpdate } from './internals.js' const glo = /** @type {any} */ (typeof globalThis !== 'undefined' diff --git a/src/utils/DeleteSet.js b/src/utils/DeleteSet.js index 429c3b91..e5e964dd 100644 --- a/src/utils/DeleteSet.js +++ b/src/utils/DeleteSet.js @@ -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} */ (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 +} diff --git a/src/utils/Snapshot.js b/src/utils/Snapshot.js index a13bb559..dfd82c86 100644 --- a/src/utils/Snapshot.js +++ b/src/utils/Snapshot.js @@ -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,7 +150,7 @@ export const splitSnapshotAffectedStructs = (transaction, snapshot) => { getItemCleanStart(transaction, createID(client, clock)) } }) - iterateDeletedStructs(transaction, snapshot.ds, item => {}) + iterateDeletedStructs(transaction, snapshot.ds, _item => {}) meta.add(snapshot) } } @@ -207,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) diff --git a/src/utils/encoding.js b/src/utils/encoding.js index 56e5dac1..83e1b91f 100644 --- a/src/utils/encoding.js +++ b/src/utils/encoding.js @@ -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} */ (store.clients.get(client)), client, clock) }) } diff --git a/tests/snapshot.tests.js b/tests/snapshot.tests.js index cd3e4773..d72e7ba7 100644 --- a/tests/snapshot.tests.js +++ b/tests/snapshot.tests.js @@ -3,9 +3,9 @@ import * as t from 'lib0/testing' import { init } from './testHelper.js' /** - * @param {t.TestCase} tc + * @param {t.TestCase} _tc */ -export const testBasic = tc => { +export const testBasic = _tc => { const ydoc = new Y.Doc({ gc: false }) ydoc.getText().insert(0, 'world!') const snapshot = Y.snapshot(ydoc) @@ -15,9 +15,9 @@ export const testBasic = tc => { } /** - * @param {t.TestCase} tc + * @param {t.TestCase} _tc */ -export const testBasicRestoreSnapshot = tc => { +export const testBasicRestoreSnapshot = _tc => { const doc = new Y.Doc({ gc: false }) doc.getArray('array').insert(0, ['hello']) const snap = Y.snapshot(doc) @@ -30,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) @@ -50,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) @@ -73,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']) @@ -89,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) @@ -107,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) @@ -123,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) @@ -181,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} + */ + 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])) +} diff --git a/tests/testHelper.js b/tests/testHelper.js index 8c90a099..f1ff4756 100644 --- a/tests/testHelper.js +++ b/tests/testHelper.js @@ -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} */ (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