mirror of
https://github.com/yjs/yjs.git
synced 2025-12-16 19:57:45 +01:00
be able to intersect idmaps and idsets
This commit is contained in:
@@ -518,10 +518,14 @@ export const typeListGetContent = (type, am) => {
|
||||
}
|
||||
for (let i = 0; i < cs.length; i++) {
|
||||
const { content, deleted, attrs } = cs[i]
|
||||
const attribution = createAttributionFromAttributionItems(attrs, deleted)
|
||||
const { attribution, retainOnly } = createAttributionFromAttributionItems(attrs, deleted)
|
||||
if (retainOnly) {
|
||||
d.retain(content.getLength())
|
||||
} else if (content.isCountable()) {
|
||||
d.insert(content.getContent(), null, attribution)
|
||||
}
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
@@ -1008,7 +1012,7 @@ export const typeMapGetContent = (parent, am) => {
|
||||
am.readContent(cs, item, false)
|
||||
const { deleted, attrs, content } = cs[cs.length - 1]
|
||||
const c = array.last(content.getContent())
|
||||
const attribution = createAttributionFromAttributionItems(attrs, deleted)
|
||||
const { attribution } = createAttributionFromAttributionItems(attrs, deleted)
|
||||
if (deleted) {
|
||||
mapcontent[key] = { prevValue: c, value: undefined, attribution }
|
||||
} else {
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
updateMarkerChanges,
|
||||
ContentType,
|
||||
warnPrematureAccess,
|
||||
noAttributionsManager, AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, Transaction, // eslint-disable-line
|
||||
IdSet, noAttributionsManager, AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, Transaction, // eslint-disable-line
|
||||
createAttributionFromAttributionItems
|
||||
} from '../internals.js'
|
||||
|
||||
@@ -678,7 +678,7 @@ export class YTextEvent extends YEvent {
|
||||
am.readContent(cs, item, freshDelete) // do item.right after calling this
|
||||
for (let i = 0; i < cs.length; i++) {
|
||||
const c = cs[i]
|
||||
const attribution = createAttributionFromAttributionItems(c.attrs, c.deleted)
|
||||
const { attribution } = createAttributionFromAttributionItems(c.attrs, c.deleted)
|
||||
switch (c.content.constructor) {
|
||||
case ContentType:
|
||||
case ContentEmbed:
|
||||
@@ -983,15 +983,23 @@ export class YText extends AbstractType {
|
||||
}
|
||||
for (let i = 0; i < cs.length; i++) {
|
||||
const { content, deleted, attrs } = cs[i]
|
||||
const attribution = createAttributionFromAttributionItems(attrs, deleted)
|
||||
const { attribution, retainOnly } = createAttributionFromAttributionItems(attrs, deleted)
|
||||
switch (content.constructor) {
|
||||
case ContentString: {
|
||||
if (retainOnly) {
|
||||
d.retain(content.getLength(), null, attribution)
|
||||
} else {
|
||||
d.insert(/** @type {ContentString} */ (content).str, null, attribution)
|
||||
}
|
||||
break
|
||||
}
|
||||
case ContentType:
|
||||
case ContentEmbed: {
|
||||
if (retainOnly) {
|
||||
d.retain(content.getLength(), null, attribution)
|
||||
} else {
|
||||
d.insert(/** @type {ContentEmbed | ContentType} */ (content).getContent()[0], null, attribution)
|
||||
}
|
||||
break
|
||||
}
|
||||
case ContentFormat: {
|
||||
|
||||
@@ -31,13 +31,14 @@ import * as error from 'lib0/error'
|
||||
/**
|
||||
* @param {Array<import('./IdMap.js').AttributionItem<any>>?} attrs
|
||||
* @param {boolean} deleted - whether the attributed item is deleted
|
||||
* @return {Attribution?}
|
||||
* @return {{ attribution: Attribution?, retainOnly: boolean }}
|
||||
*/
|
||||
export const createAttributionFromAttributionItems = (attrs, deleted) => {
|
||||
/**
|
||||
* @type {Attribution?}
|
||||
*/
|
||||
let attribution = null
|
||||
let retainOnly = false
|
||||
if (attrs != null) {
|
||||
attribution = {}
|
||||
if (deleted) {
|
||||
@@ -47,6 +48,9 @@ export const createAttributionFromAttributionItems = (attrs, deleted) => {
|
||||
}
|
||||
attrs.forEach(attr => {
|
||||
switch (attr.name) {
|
||||
case 'retain':
|
||||
retainOnly = true
|
||||
break
|
||||
case 'insert':
|
||||
case 'delete':
|
||||
case 'suggest': {
|
||||
@@ -63,7 +67,7 @@ export const createAttributionFromAttributionItems = (attrs, deleted) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
return attribution
|
||||
return { attribution, retainOnly }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,7 +124,7 @@ export class TwosetAttributionManager {
|
||||
*/
|
||||
readContent (contents, item, forceRead) {
|
||||
const deleted = item.deleted
|
||||
const slice = (deleted ? this.deletes : this.inserts).slice(item.id, item.length)
|
||||
const slice = (deleted ? this.deletes : this.inserts).sliceId(item.id, item.length)
|
||||
let content = slice.length === 1 ? item.content : item.content.copy()
|
||||
slice.forEach(s => {
|
||||
const c = content
|
||||
@@ -221,7 +225,7 @@ export class DiffAttributionManager {
|
||||
*/
|
||||
readContent (contents, item, forceRead) {
|
||||
const deleted = item.deleted || /** @type {any} */ (item.parent).doc !== this._nextDoc
|
||||
const slice = (deleted ? this.deletes : this.inserts).slice(item.id, item.length)
|
||||
const slice = (deleted ? this.deletes : this.inserts).sliceId(item.id, item.length)
|
||||
let content = slice.length === 1 ? item.content : item.content.copy()
|
||||
if (content instanceof ContentDeleted && slice[0].attrs != null && !this.inserts.hasId(item.id)) {
|
||||
// Retrieved item is never more fragmented than the newer item.
|
||||
@@ -290,7 +294,7 @@ export class SnapshotAttributionManager {
|
||||
*/
|
||||
readContent (contents, item, forceRead) {
|
||||
if ((this.nextSnapshot.sv.get(item.id.client) ?? 0) <= item.id.clock) return // future item that should not be displayed
|
||||
const slice = this.attrs.slice(item.id, item.length)
|
||||
const slice = this.attrs.sliceId(item.id, item.length)
|
||||
let content = slice.length === 1 ? item.content : item.content.copy()
|
||||
slice.forEach(s => {
|
||||
const deleted = this.nextSnapshot.ds.has(item.id.client, s.clock)
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
findRangeStartInIdRanges,
|
||||
_deleteRangeFromIdSet,
|
||||
DSDecoderV1, DSDecoderV2, IdSetEncoderV1, IdSetEncoderV2, IdSet, ID, // eslint-disable-line
|
||||
_insertIntoIdSet
|
||||
_insertIntoIdSet,
|
||||
_intersectSets
|
||||
} from '../internals.js'
|
||||
|
||||
import * as array from 'lib0/array'
|
||||
@@ -360,8 +361,20 @@ export class IdMap {
|
||||
* @param {number} len
|
||||
* @return {Array<MaybeAttrRange<Attrs>>}
|
||||
*/
|
||||
slice (id, len) {
|
||||
const dr = this.clients.get(id.client)
|
||||
sliceId (id, len) {
|
||||
return this.slice(id.client, id.clock, len)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return attributions for a slice of ids.
|
||||
*
|
||||
* @param {number} client
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
* @return {Array<MaybeAttrRange<Attrs>>}
|
||||
*/
|
||||
slice (client, clock, len) {
|
||||
const dr = this.clients.get(client)
|
||||
/**
|
||||
* @type {Array<MaybeAttrRange<Attrs>>}
|
||||
*/
|
||||
@@ -371,19 +384,19 @@ export class IdMap {
|
||||
* @type {Array<AttrRange<Attrs>>}
|
||||
*/
|
||||
const ranges = dr.getIds()
|
||||
let index = findRangeStartInIdRanges(ranges, id.clock)
|
||||
let index = findRangeStartInIdRanges(ranges, clock)
|
||||
if (index !== null) {
|
||||
let prev = null
|
||||
while (index < ranges.length) {
|
||||
let r = ranges[index]
|
||||
if (r.clock < id.clock) {
|
||||
r = new AttrRange(id.clock, r.len - (id.clock - r.clock), r.attrs)
|
||||
if (r.clock < clock) {
|
||||
r = new AttrRange(clock, r.len - (clock - r.clock), r.attrs)
|
||||
}
|
||||
if (r.clock + r.len > id.clock + len) {
|
||||
r = new AttrRange(r.clock, id.clock + len - r.clock, r.attrs)
|
||||
if (r.clock + r.len > clock + len) {
|
||||
r = new AttrRange(r.clock, clock + len - r.clock, r.attrs)
|
||||
}
|
||||
if (r.len <= 0) break
|
||||
const prevEnd = prev != null ? prev.clock + prev.len : id.clock
|
||||
const prevEnd = prev != null ? prev.clock + prev.len : clock
|
||||
if (prevEnd < r.clock) {
|
||||
res.push(createMaybeAttrRange(prevEnd, r.clock - prevEnd, null))
|
||||
}
|
||||
@@ -396,11 +409,11 @@ export class IdMap {
|
||||
if (res.length > 0) {
|
||||
const last = res[res.length - 1]
|
||||
const end = last.clock + last.len
|
||||
if (end < id.clock + len) {
|
||||
res.push(createMaybeAttrRange(end, id.clock + len - end, null))
|
||||
if (end < clock + len) {
|
||||
res.push(createMaybeAttrRange(end, clock + len - end, null))
|
||||
}
|
||||
} else {
|
||||
res.push(createMaybeAttrRange(id.clock, len, null))
|
||||
res.push(createMaybeAttrRange(clock, len, null))
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -610,3 +623,5 @@ export const diffIdMap = (set, exclude) => {
|
||||
diffed.attrsH = set.attrsH
|
||||
return diffed
|
||||
}
|
||||
|
||||
export const intersectMaps = _intersectSets
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
UpdateEncoderV2,
|
||||
IdMap,
|
||||
AttrRanges,
|
||||
AbstractStruct, DSDecoderV1, IdSetEncoderV1, DSDecoderV2, IdSetEncoderV2, Item, GC, StructStore, Transaction, ID // eslint-disable-line
|
||||
AttrRange,
|
||||
AbstractStruct, DSDecoderV1, IdSetEncoderV1, DSDecoderV2, IdSetEncoderV2, Item, GC, StructStore, Transaction, ID, AttributionItem, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as array from 'lib0/array'
|
||||
@@ -37,7 +38,46 @@ export class IdRange {
|
||||
copyWith (clock, len) {
|
||||
return new IdRange(clock, len)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method making this compatible with IdMap.
|
||||
*
|
||||
* @return {Array<import('./IdMap.js').AttributionItem<any>>}
|
||||
*/
|
||||
get attrs () {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export class MaybeIdRange {
|
||||
/**
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
* @param {boolean} exists
|
||||
*/
|
||||
constructor (clock, len, exists) {
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
this.clock = clock
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
this.len = len
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.exists = exists
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
* @param {boolean} exists
|
||||
* @return {MaybeIdRange}
|
||||
*/
|
||||
export const createMaybeIdRange = (clock, len, exists) => new MaybeIdRange(clock, len, exists)
|
||||
|
||||
class IdRanges {
|
||||
/**
|
||||
@@ -144,6 +184,59 @@ export class IdSet {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return slices of ids that exist in this idset.
|
||||
*
|
||||
* @param {number} client
|
||||
* @param {number} clock
|
||||
* @param {number} len
|
||||
* @return {Array<MaybeIdRange>}
|
||||
*/
|
||||
slice (client, clock, len) {
|
||||
const dr = this.clients.get(client)
|
||||
/**
|
||||
* @type {Array<MaybeIdRange>}
|
||||
*/
|
||||
const res = []
|
||||
if (dr) {
|
||||
/**
|
||||
* @type {Array<IdRange>}
|
||||
*/
|
||||
const ranges = dr.getIds()
|
||||
let index = findRangeStartInIdRanges(ranges, clock)
|
||||
if (index !== null) {
|
||||
let prev = null
|
||||
while (index < ranges.length) {
|
||||
let r = ranges[index]
|
||||
if (r.clock < clock) {
|
||||
r = new IdRange(clock, r.len - (clock - r.clock))
|
||||
}
|
||||
if (r.clock + r.len > clock + len) {
|
||||
r = new IdRange(r.clock, clock + len - r.clock)
|
||||
}
|
||||
if (r.len <= 0) break
|
||||
const prevEnd = prev != null ? prev.clock + prev.len : clock
|
||||
if (prevEnd < r.clock) {
|
||||
res.push(createMaybeIdRange(prevEnd, r.clock - prevEnd, false))
|
||||
}
|
||||
prev = r
|
||||
res.push(createMaybeIdRange(r.clock, r.len, true))
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
if (res.length > 0) {
|
||||
const last = res[res.length - 1]
|
||||
const end = last.clock + last.len
|
||||
if (end < clock + len) {
|
||||
res.push(createMaybeIdRange(end, clock + len - end, false))
|
||||
}
|
||||
} else {
|
||||
res.push(createMaybeIdRange(clock, len, false))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} client
|
||||
* @param {number} clock
|
||||
@@ -399,6 +492,55 @@ export const _diffSet = (set, exclude) => {
|
||||
*/
|
||||
export const diffIdSet = _diffSet
|
||||
|
||||
/**
|
||||
* @template {IdSet | IdMap<any>} SetA
|
||||
* @template {IdSet | IdMap<any>} SetB
|
||||
* @param {SetA} setA
|
||||
* @param {SetB} setB
|
||||
* @return {SetA extends IdMap<infer A> ? (SetB extends IdMap<infer B> ? IdMap<A | B> : IdMap<A>) : IdSet}
|
||||
*/
|
||||
export const _intersectSets = (setA, setB) => {
|
||||
/**
|
||||
* @type {IdMap<any> | IdSet}
|
||||
*/
|
||||
const res = /** @type {any } */ (setA instanceof IdSet ? new IdSet() : new IdMap())
|
||||
const Ranges = setA instanceof IdSet ? IdRanges : AttrRanges
|
||||
setA.clients.forEach((_aRanges, client) => {
|
||||
/**
|
||||
* @type {Array<IdRange>}
|
||||
*/
|
||||
const resRanges = []
|
||||
const _bRanges = setB.clients.get(client)
|
||||
const aRanges = _aRanges.getIds()
|
||||
if (_bRanges != null) {
|
||||
const bRanges = _bRanges.getIds()
|
||||
for (let a = 0, b = 0; a < aRanges.length && b < bRanges.length;) {
|
||||
const aRange = aRanges[a]
|
||||
const bRange = bRanges[b]
|
||||
// construct overlap
|
||||
const clock = math.max(aRange.clock, bRange.clock)
|
||||
const len = math.min(aRange.len - (clock - aRange.clock), bRange.len - (clock - bRange.clock))
|
||||
if (len > 0) {
|
||||
resRanges.push(aRange instanceof AttrRange
|
||||
? new AttrRange(clock, len, /** @type {Array<AttributionItem<any>>} */ (aRange.attrs).concat(bRange.attrs))
|
||||
: new IdRange(clock, len)
|
||||
)
|
||||
}
|
||||
if (aRange.clock + aRange.len < bRange.clock + bRange.len) {
|
||||
a++
|
||||
} else {
|
||||
b++
|
||||
}
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
if (resRanges.length > 0) res.clients.set(client, /** @type {any} */ (new Ranges(resRanges)))
|
||||
})
|
||||
return /** @type {any} */ (res)
|
||||
}
|
||||
|
||||
export const intersectSets = _intersectSets
|
||||
|
||||
/**
|
||||
* @param {IdSet} idSet
|
||||
* @param {number} client
|
||||
|
||||
@@ -94,7 +94,7 @@ export const testRepeatMergingMultipleIdMaps = tc => {
|
||||
const mergedHas = merged.hasId(new ID(iclient, iclock))
|
||||
const oneHas = sets.some(ids => ids.hasId(new ID(iclient, iclock)))
|
||||
t.assert(mergedHas === oneHas)
|
||||
const mergedAttrs = merged.slice(new ID(iclient, iclock), 1)
|
||||
const mergedAttrs = merged.sliceId(new ID(iclient, iclock), 1)
|
||||
mergedAttrs.forEach(a => {
|
||||
if (a.attrs != null) {
|
||||
composed.add(iclient, a.clock, a.len, a.attrs)
|
||||
@@ -161,3 +161,36 @@ export const testRepeatRandomDeletes = tc => {
|
||||
}
|
||||
compareIdMaps(idset, diffed)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testrepeatRandomIntersects = tc => {
|
||||
const clients = 4
|
||||
const clockRange = 100
|
||||
const ids1 = createRandomIdMap(tc.prng, clients, clockRange, [1])
|
||||
const ids2 = createRandomIdMap(tc.prng, clients, clockRange, ['two'])
|
||||
const intersected = idmap.intersectMaps(ids1, ids2)
|
||||
for (let client = 0; client < clients; client++) {
|
||||
for (let clock = 0; clock < clockRange; clock++) {
|
||||
t.assert((ids1.has(client, clock) && ids2.has(client, clock)) === intersected.has(client, clock))
|
||||
/**
|
||||
* @type {Array<any>?}
|
||||
*/
|
||||
const slice1 = ids1.slice(client, clock, 1)[0].attrs
|
||||
/**
|
||||
* @type {Array<any>?}
|
||||
*/
|
||||
const slice2 = ids2.slice(client, clock, 1)[0].attrs
|
||||
/**
|
||||
* @type {Array<any>?}
|
||||
*/
|
||||
const expectedAttrs = (slice1 != null && slice2 != null) ? slice1.concat(slice2) : null
|
||||
const attrs = intersected.slice(client, clock, 1)[0].attrs
|
||||
t.assert(attrs?.length === expectedAttrs?.length)
|
||||
}
|
||||
}
|
||||
const diffed1 = idmap.diffIdMap(ids1, ids2)
|
||||
const altDiffed1 = idmap.diffIdMap(ids1, intersected)
|
||||
compareIdMaps(diffed1, altDiffed1)
|
||||
}
|
||||
|
||||
@@ -221,3 +221,22 @@ export const testRepeatRandomDiffing2 = tc => {
|
||||
const excludedMerged = d.mergeIdSets([e1, e2])
|
||||
compareIdSets(mergedExcluded, excludedMerged)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testrepeatRandomIntersects = tc => {
|
||||
const clients = 4
|
||||
const clockRange = 100
|
||||
const ids1 = createRandomIdSet(tc.prng, clients, clockRange)
|
||||
const ids2 = createRandomIdSet(tc.prng, clients, clockRange)
|
||||
const intersected = d.intersectSets(ids1, ids2)
|
||||
for (let client = 0; client < clients; client++) {
|
||||
for (let clock = 0; clock < clockRange; clock++) {
|
||||
t.assert((ids1.has(client, clock) && ids2.has(client, clock)) === intersected.has(client, clock))
|
||||
}
|
||||
}
|
||||
const diffed1 = d.diffIdSet(ids1, ids2)
|
||||
const altDiffed1 = d.diffIdSet(ids1, intersected)
|
||||
compareIdSets(diffed1, altDiffed1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user