implement attribution class that is de-duplicated in IdMap

This commit is contained in:
Kevin Jahns
2025-04-19 00:21:40 +02:00
parent c9a6d113bb
commit 1f041913c8
6 changed files with 120 additions and 13 deletions

View File

@@ -103,7 +103,9 @@ export {
equalIdSets, equalIdSets,
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
IdMap, IdMap,
createIdMap createIdMap,
createAttribution,
Attribution
} from './internals.js' } from './internals.js'
const glo = /** @type {any} */ (typeof globalThis !== 'undefined' const glo = /** @type {any} */ (typeof globalThis !== 'undefined'

View File

@@ -5,6 +5,50 @@ import {
} from '../internals.js' } from '../internals.js'
import * as array from 'lib0/array' import * as array from 'lib0/array'
import * as map from 'lib0/map'
import * as encoding from 'lib0/encoding'
import * as buf from 'lib0/buffer'
import * as rabin from 'lib0/hash/rabin'
/**
* @template V
*/
export class Attribution {
/**
* @param {string} name
* @param {V} val
*/
constructor (name, val) {
this.name = name
this.val = val
}
hash () {
const encoder = encoding.createEncoder()
encoding.writeVarString(encoder, this.name)
encoding.writeAny(encoder, /** @type {any} */ (this.val))
return buf.toBase64(rabin.fingerprint(rabin.StandardIrreducible128, encoding.toUint8Array(encoder)))
}
}
/**
* @param {Attribution<any>} attr
*/
const _hashAttribution = attr => {
const encoder = encoding.createEncoder()
encoding.writeVarString(encoder, attr.name)
encoding.writeAny(encoder, attr.val)
return buf.toBase64(rabin.fingerprint(rabin.StandardIrreducible128, encoding.toUint8Array(encoder)))
}
/**
* @template V
* @param {string} name
* @param {V} val
* @return {Attribution<V>}
*/
export const createAttribution = (name, val) => new Attribution(name, val)
/** /**
* @template T * @template T
@@ -35,7 +79,7 @@ export class AttrRange {
/** /**
* @param {number} clock * @param {number} clock
* @param {number} len * @param {number} len
* @param {Array<Attrs>} attrs * @param {Array<Attribution<Attrs>>} attrs
*/ */
constructor (clock, len, attrs) { constructor (clock, len, attrs) {
/** /**
@@ -79,7 +123,7 @@ export class AttrRanges {
/** /**
* @param {number} clock * @param {number} clock
* @param {number} length * @param {number} length
* @param {Array<Attrs>} attrs * @param {Array<Attribution<Attrs>>} attrs
*/ */
add (clock, length, attrs) { add (clock, length, attrs) {
this.sorted = false this.sorted = false
@@ -168,11 +212,20 @@ export class AttrRanges {
} }
/** /**
* Merge multiple idmaps. Ensures that there are no redundant attribution definitions (two
* Attributions that describe the same thing).
*
* @template T * @template T
* @param {Array<IdMap<T>>} ams * @param {Array<IdMap<T>>} ams
* @return {IdMap<T>} A fresh IdSet * @return {IdMap<T>} A fresh IdSet
*/ */
export const mergeIdMaps = ams => { export const mergeIdMaps = ams => {
/**
* Maps attribution to the attribution of the merged idmap.
*
* @type {Map<Attribution<any>,Attribution<any>>}
*/
const attrMapper = new Map()
const merged = createIdMap() const merged = createIdMap()
for (let amsI = 0; amsI < ams.length; amsI++) { for (let amsI = 0; amsI < ams.length; amsI++) {
ams[amsI].clients.forEach((rangesLeft, client) => { ams[amsI].clients.forEach((rangesLeft, client) => {
@@ -186,6 +239,14 @@ export const mergeIdMaps = ams => {
array.appendTo(ids, nextIds.getIds()) array.appendTo(ids, nextIds.getIds())
} }
} }
ids.forEach(id => {
// @ts-ignore
id.attrs = id.attrs.map(attr =>
map.setIfUndefined(attrMapper, attr, () =>
_ensureAttrs(merged, [attr])[0]
)
)
})
merged.clients.set(client, new AttrRanges(ids)) merged.clients.set(client, new AttrRanges(ids))
} }
}) })
@@ -202,6 +263,14 @@ export class IdMap {
* @type {Map<number,AttrRanges<Attrs>>} * @type {Map<number,AttrRanges<Attrs>>}
*/ */
this.clients = new Map() this.clients = new Map()
/**
* @type {Map<string, Attribution<Attrs>>}
*/
this.attrsH = new Map()
/**
* @type {Set<Attribution<Attrs>>}
*/
this.attrs = new Set()
} }
/** /**
@@ -253,9 +322,10 @@ export class IdMap {
* @param {number} client * @param {number} client
* @param {number} clock * @param {number} clock
* @param {number} len * @param {number} len
* @param {Array<Attrs>} attrs * @param {Array<Attribution<Attrs>>} attrs
*/ */
add (client, clock, len, attrs) { add (client, clock, len, attrs) {
attrs = _ensureAttrs(this, attrs)
const ranges = this.clients.get(client) const ranges = this.clients.get(client)
if (ranges == null) { if (ranges == null) {
this.clients.set(client, new AttrRanges([new AttrRange(clock, len, attrs)])) this.clients.set(client, new AttrRanges([new AttrRange(clock, len, attrs)]))
@@ -265,15 +335,27 @@ export class IdMap {
} }
} }
/**
* @template Attrs
* @param {IdMap<Attrs>} idmap
* @param {Array<Attribution<Attrs>>} attrs
* @return {Array<Attribution<Attrs>>}
*/
const _ensureAttrs = (idmap, attrs) => attrs.map(attr =>
idmap.attrs.has(attr) ? attr : map.setIfUndefined(idmap.attrsH, _hashAttribution(attr), () => {
idmap.attrs.add(attr)
return attr
}))
export const createIdMap = () => new IdMap() export const createIdMap = () => new IdMap()
/** /**
* Remove all ranges from `exclude` from `ds`. The result is a fresh IdMap containing all ranges from `idSet` that are not * Remove all ranges from `exclude` from `ds`. The result is a fresh IdMap containing all ranges from `idSet` that are not
* in `exclude`. * in `exclude`.
* *
* @template {IdMap<any>} Set * @template {IdMap<any>} ISet
* @param {Set} set * @param {ISet} set
* @param {IdSet | IdMap<any>} exclude * @param {IdSet | IdMap<any>} exclude
* @return {Set} * @return {ISet}
*/ */
export const diffIdMap = _diffSet export const diffIdMap = _diffSet

View File

@@ -24,6 +24,7 @@ export class StructStore {
*/ */
this.pendingDs = null this.pendingDs = null
} }
get ds () { get ds () {
return createDeleteSetFromStructStore(this) return createDeleteSetFromStructStore(this)
} }

View File

@@ -11,7 +11,7 @@ import {
createID, createID,
cleanupYTextAfterTransaction, cleanupYTextAfterTransaction,
IdSet, UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc, // eslint-disable-line IdSet, UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc, // eslint-disable-line
insertIntoIdSet // insertIntoIdSet
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error' import * as error from 'lib0/error'

View File

@@ -1,6 +1,6 @@
import * as t from 'lib0/testing' import * as t from 'lib0/testing'
import * as am from '../src/utils/IdMap.js' import * as am from '../src/utils/IdMap.js'
import { compareIdmaps, createIdMap, ID, createRandomIdSet, createRandomIdMap } from './testHelper.js' import { compareIdmaps, createIdMap, ID, createRandomIdSet, createRandomIdMap, createAttribution } from './testHelper.js'
/** /**
* @template T * @template T
@@ -9,7 +9,7 @@ import { compareIdmaps, createIdMap, ID, createRandomIdSet, createRandomIdMap }
const simpleConstructAttrs = ops => { const simpleConstructAttrs = ops => {
const attrs = createIdMap() const attrs = createIdMap()
ops.forEach(op => { ops.forEach(op => {
attrs.add(op[0], op[1], op[2], op[3]) attrs.add(op[0], op[1], op[2], op[3].map(v => createAttribution('', v)))
}) })
return attrs return attrs
} }

View File

@@ -8,7 +8,7 @@ import * as map from 'lib0/map'
import * as Y from '../src/index.js' import * as Y from '../src/index.js'
import * as math from 'lib0/math' import * as math from 'lib0/math'
import { import {
idmapAttrsEqual, createIdSet, createIdMap, addToIdSet createIdSet, createIdMap, addToIdSet
} from '../src/internals.js' } from '../src/internals.js'
export * from '../src/index.js' export * from '../src/index.js'
@@ -327,6 +327,28 @@ export const compareIdSets = (idSet1, idSet2) => {
return true return true
} }
/**
* only use for testing
*
* @template T
* @param {Array<Y.Attribution<T>>} attrs
* @param {Y.Attribution<T>} attr
*
*/
const _idmapAttrsHas = (attrs, attr) => {
const hash = attr.hash()
return attrs.find(a => a.hash() === hash)
}
/**
* only use for testing
*
* @template T
* @param {Array<Y.Attribution<T>>} a
* @param {Array<Y.Attribution<T>>} b
*/
export const _idmapAttrsEqual = (a, b) => a.length === b.length && a.every(v => _idmapAttrsHas(b, v))
/** /**
* @template T * @template T
* @param {Y.IdMap<T>} am1 * @param {Y.IdMap<T>} am1
@@ -341,7 +363,7 @@ export const compareIdmaps = (am1, am2) => {
for (let i = 0; i < items1.length; i++) { for (let i = 0; i < items1.length; i++) {
const di1 = items1[i] const di1 = items1[i]
const di2 = /** @type {Array<import('../src/utils/IdMap.js').AttrRange<T>>} */ (items2)[i] const di2 = /** @type {Array<import('../src/utils/IdMap.js').AttrRange<T>>} */ (items2)[i]
t.assert(di1.clock === di2.clock && di1.len === di2.len && idmapAttrsEqual(di1.attrs, di2.attrs)) t.assert(di1.clock === di2.clock && di1.len === di2.len && _idmapAttrsEqual(di1.attrs, di2.attrs))
} }
} }
return true return true
@@ -392,7 +414,7 @@ export const createRandomIdMap = (gen, clients, clockRange, attrChoices) => {
attrs.push(a) attrs.push(a)
} }
} }
idMap.add(client, clockStart, len, attrs) idMap.add(client, clockStart, len, attrs.map(v => Y.createAttribution('', v)))
} }
return idMap return idMap
} }