From d741c46f1fc3a37231bd8cc00061a6ea64c408e9 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Wed, 14 Jan 2026 05:05:36 +0100 Subject: [PATCH] [update] intersect --- src/index.js | 4 +- src/utils/meta.js | 3 +- src/utils/updates.js | 90 +++++++++++++++++++++++++++++++++++++----- test.html | 1 + tests/updates.tests.js | 12 ++++++ 5 files changed, 97 insertions(+), 13 deletions(-) diff --git a/src/index.js b/src/index.js index 36c57443..8042fb28 100644 --- a/src/index.js +++ b/src/index.js @@ -118,7 +118,9 @@ export { getPathTo, Attributions, filterIdMap, - undoContentIds + undoContentIds, + intersectUpdateWithContentIds, + intersectUpdateWithContentIdsV2 } from './internals.js' export * from './utils/meta.js' diff --git a/src/utils/meta.js b/src/utils/meta.js index e41b2dab..06222a6c 100644 --- a/src/utils/meta.js +++ b/src/utils/meta.js @@ -120,7 +120,7 @@ export const encodeContentIds = contentIds => { export const readContentIds = decoder => createContentIds( idset.readIdSet(decoder), idset.readIdSet(decoder) -) +) /** * @param {Uint8Array} buf @@ -188,4 +188,3 @@ export const decodeContentMap = buf => readContentMap(new IdSetDecoderV2(decodin * @param {(c:Array>)=>boolean} deletePredicate */ export const filterContentMap = (contentMap, insertPredicate, deletePredicate) => createContentMap(idmap.filterIdMap(contentMap.inserts, insertPredicate), idmap.filterIdMap(contentMap.deletes, deletePredicate)) - diff --git a/src/utils/updates.js b/src/utils/updates.js index a34c0ef9..1955841e 100644 --- a/src/utils/updates.js +++ b/src/utils/updates.js @@ -36,6 +36,8 @@ import { createIdSet } from '../internals.js' +import * as idset from './IdSet.js' + /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder */ @@ -390,7 +392,7 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U } if (firstClient !== currWrite.struct.id.client) { - writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset, 0) currWrite = { struct: curr, offset: 0 } currDecoder.next() } else { @@ -400,7 +402,7 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U // extend existing skip currWrite.struct.length = curr.id.clock + curr.length - currWrite.struct.id.clock } else { - writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset, 0) const diff = curr.id.clock - currWrite.struct.id.clock - currWrite.struct.length /** * @type {Skip} @@ -419,7 +421,7 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U } } if (!currWrite.struct.mergeWith(/** @type {any} */ (curr))) { - writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset, 0) currWrite = { struct: curr, offset: 0 } currDecoder.next() } @@ -434,12 +436,12 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U next !== null && next.id.client === firstClient && next.id.clock === currWrite.struct.id.clock + currWrite.struct.length && next.constructor !== Skip; next = currDecoder.next() ) { - writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset, 0) currWrite = { struct: next, offset: 0 } } } if (currWrite !== null) { - writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset) + writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset, 0) currWrite = null } finishLazyStructWriting(lazyStructEncoder) @@ -451,6 +453,7 @@ export const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = U } /** + * @deprecated * @param {Uint8Array} update * @param {Uint8Array} sv * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder] @@ -472,10 +475,10 @@ export const diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = continue } if (curr.id.clock + curr.length > svClock) { - writeStructToLazyStructWriter(lazyStructWriter, curr, math.max(svClock - curr.id.clock, 0)) + writeStructToLazyStructWriter(lazyStructWriter, curr, math.max(svClock - curr.id.clock, 0), 0) reader.next() while (reader.curr && reader.curr.id.client === currClient) { - writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0) + writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0, 0) reader.next() } } else { @@ -493,6 +496,9 @@ export const diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = } /** + * @deprecated + * @todo remove this in favor of intersectupdate + * * @param {Uint8Array} update * @param {Uint8Array} sv */ @@ -513,8 +519,9 @@ const flushLazyStructWriter = lazyWriter => { * @param {LazyStructWriter} lazyWriter * @param {Item | GC} struct * @param {number} offset + * @param {number} offsetEnd */ -const writeStructToLazyStructWriter = (lazyWriter, struct, offset) => { +const writeStructToLazyStructWriter = (lazyWriter, struct, offset, offsetEnd) => { // flush curr if we start another client if (lazyWriter.written > 0 && lazyWriter.currClient !== struct.id.client) { flushLazyStructWriter(lazyWriter) @@ -526,7 +533,7 @@ const writeStructToLazyStructWriter = (lazyWriter, struct, offset) => { // write startClock encoding.writeVarUint(lazyWriter.encoder.restEncoder, struct.id.clock + offset) } - struct.write(lazyWriter.encoder, offset, 0) + struct.write(lazyWriter.encoder, offset, offsetEnd) lazyWriter.written++ } /** @@ -574,7 +581,7 @@ export const convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder const updateEncoder = new YEncoder() const lazyWriter = new LazyStructWriter(updateEncoder) for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) { - writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0) + writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0, 0) } finishLazyStructWriting(lazyWriter) const ds = readIdSet(updateDecoder) @@ -709,3 +716,66 @@ export const convertUpdateFormatV1ToV2 = update => convertUpdateFormat(update, f * @param {Uint8Array} update */ export const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, f.id, UpdateDecoderV2, UpdateEncoderV1) + +/** + * Filter an update to only include content specified by a ContentIds pattern. + * + * This function extracts a subset of an update, keeping only the structs whose IDs + * are present in `contentIds.inserts` and only the delete set entries that are + * present in `contentIds.deletes`. + * + * Note: If a struct partially overlaps with the contentIds pattern, only the + * overlapping portion is included in the result. + * + * @param {Uint8Array} update + * @param {import('./meta.js').ContentIds} contentIds - Pattern specifying which content to include + * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder] + * @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder] + * @return {Uint8Array} + */ +export const intersectUpdateWithContentIdsV2 = (update, contentIds, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => { + const { inserts, deletes } = contentIds + const encoder = new YEncoder() + const lazyStructWriter = new LazyStructWriter(encoder) + const decoder = new YDecoder(decoding.createDecoder(update)) + const reader = new LazyStructReader(decoder, true) + + while (reader.curr) { + const currClientId = reader.curr.id.client + let nextClock = reader.curr.id.clock + let firstWrite = false + while (reader.curr != null && reader.curr.id.client === currClientId) { + const curr = reader.curr + for (const slice of inserts.slice(currClientId, nextClock, curr.length)) { + if (slice.exists) { + const skipLen = slice.clock - nextClock + if (skipLen > 0 && firstWrite) { + // write missing skip + writeStructToLazyStructWriter(lazyStructWriter, new Skip(createID(currClientId, nextClock), skipLen), 0, 0) + } + // write sliced content + writeStructToLazyStructWriter(lazyStructWriter, curr, slice.clock - curr.id.clock, (curr.id.clock + curr.length) - (slice.clock + slice.len)) + nextClock = slice.clock + slice.len + firstWrite = true + } + } + reader.next() + } + } + finishLazyStructWriting(lazyStructWriter) + // Filter the delete set to only include entries in contentIds.deletes + const ds = readIdSet(decoder) + const filteredDs = idset.intersectSets(ds, deletes) + writeIdSet(encoder, filteredDs) + return encoder.toUint8Array() +} + +/** + * Filter an update (V1 format) to only include content specified by a ContentIds pattern. + * + * @param {Uint8Array} update + * @param {import('./meta.js').ContentIds} contentIds - Pattern specifying which content to include + * @return {Uint8Array} + */ +export const intersectUpdateWithContentIds = (update, contentIds) => + intersectUpdateWithContentIdsV2(update, contentIds, UpdateDecoderV1, UpdateEncoderV1) diff --git a/test.html b/test.html index 91679364..0ab3a3ca 100644 --- a/test.html +++ b/test.html @@ -8,6 +8,7 @@ "imports": { "@y/y": "./src/index.js", "@y/y/internals": "./src/internals.js", + "@y/y/meta": "./src/utils/meta.js", "@y/y/testHelper": "./tests/testHelper.js", "@y/y/package.json": "./package.json", "lib0/package.json": "./node_modules/lib0/package.json", diff --git a/tests/updates.tests.js b/tests/updates.tests.js index 2c97ef43..3afaec8f 100644 --- a/tests/updates.tests.js +++ b/tests/updates.tests.js @@ -359,3 +359,15 @@ export const testObfuscateUpdates = _tc => { // test subdoc t.assert(osubdoc.guid !== subdoc.guid) } + +export const testIntersectDoc = () => { + const ydoc = new Y.Doc() + ydoc.get().setAttr('k', 1) + const c1 = Y.createContentIdsFromDoc(ydoc) + ydoc.get().setAttr('k', 2) + + const v1 = Y.intersectUpdateWithContentIds(Y.encodeStateAsUpdate(ydoc), c1) + const y1 = new Y.Doc() + Y.applyUpdate(y1, v1) + t.assert(ydoc.get().getAttr('k')) +}