[update] intersect

This commit is contained in:
Kevin Jahns
2026-01-14 05:05:36 +01:00
parent bee0afb38d
commit d741c46f1f
5 changed files with 97 additions and 13 deletions

View File

@@ -118,7 +118,9 @@ export {
getPathTo,
Attributions,
filterIdMap,
undoContentIds
undoContentIds,
intersectUpdateWithContentIds,
intersectUpdateWithContentIdsV2
} from './internals.js'
export * from './utils/meta.js'

View File

@@ -120,7 +120,7 @@ export const encodeContentIds = contentIds => {
export const readContentIds = decoder => createContentIds(
idset.readIdSet(decoder),
idset.readIdSet(decoder)
)
)
/**
* @param {Uint8Array<any>} buf
@@ -188,4 +188,3 @@ export const decodeContentMap = buf => readContentMap(new IdSetDecoderV2(decodin
* @param {(c:Array<idmap.ContentAttribute<any>>)=>boolean} deletePredicate
*/
export const filterContentMap = (contentMap, insertPredicate, deletePredicate) => createContentMap(idmap.filterIdMap(contentMap.inserts, insertPredicate), idmap.filterIdMap(contentMap.deletes, deletePredicate))

View File

@@ -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)

View File

@@ -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",

View File

@@ -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'))
}