From 2d87301af2fb8fb05e339b4da5dd60fa60f43959 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Sat, 19 Apr 2025 00:21:40 +0200 Subject: [PATCH] implement attribution class that is de-duplicated in IdMap --- src/index.js | 4 +- src/utils/IdMap.js | 94 +++++++++++++++++++++++++++++++++++++--- src/utils/StructStore.js | 1 + src/utils/Transaction.js | 2 +- tests/IdMap.tests.js | 4 +- tests/testHelper.js | 28 ++++++++++-- 6 files changed, 120 insertions(+), 13 deletions(-) diff --git a/src/index.js b/src/index.js index 5f9433c4..325092a2 100644 --- a/src/index.js +++ b/src/index.js @@ -103,7 +103,9 @@ export { equalIdSets, createDeleteSetFromStructStore, IdMap, - createIdMap + createIdMap, + createAttribution, + Attribution } from './internals.js' const glo = /** @type {any} */ (typeof globalThis !== 'undefined' diff --git a/src/utils/IdMap.js b/src/utils/IdMap.js index 03add22c..d91f9e02 100644 --- a/src/utils/IdMap.js +++ b/src/utils/IdMap.js @@ -5,6 +5,50 @@ import { } from '../internals.js' 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} 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} + */ +export const createAttribution = (name, val) => new Attribution(name, val) /** * @template T @@ -35,7 +79,7 @@ export class AttrRange { /** * @param {number} clock * @param {number} len - * @param {Array} attrs + * @param {Array>} attrs */ constructor (clock, len, attrs) { /** @@ -79,7 +123,7 @@ export class AttrRanges { /** * @param {number} clock * @param {number} length - * @param {Array} attrs + * @param {Array>} attrs */ add (clock, length, attrs) { 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 * @param {Array>} ams * @return {IdMap} A fresh IdSet */ export const mergeIdMaps = ams => { + /** + * Maps attribution to the attribution of the merged idmap. + * + * @type {Map,Attribution>} + */ + const attrMapper = new Map() const merged = createIdMap() for (let amsI = 0; amsI < ams.length; amsI++) { ams[amsI].clients.forEach((rangesLeft, client) => { @@ -186,6 +239,14 @@ export const mergeIdMaps = ams => { 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)) } }) @@ -202,6 +263,14 @@ export class IdMap { * @type {Map>} */ this.clients = new Map() + /** + * @type {Map>} + */ + this.attrsH = new Map() + /** + * @type {Set>} + */ + this.attrs = new Set() } /** @@ -253,9 +322,10 @@ export class IdMap { * @param {number} client * @param {number} clock * @param {number} len - * @param {Array} attrs + * @param {Array>} attrs */ add (client, clock, len, attrs) { + attrs = _ensureAttrs(this, attrs) const ranges = this.clients.get(client) if (ranges == null) { this.clients.set(client, new AttrRanges([new AttrRange(clock, len, attrs)])) @@ -265,15 +335,27 @@ export class IdMap { } } +/** + * @template Attrs + * @param {IdMap} idmap + * @param {Array>} attrs + * @return {Array>} + */ +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() /** * Remove all ranges from `exclude` from `ds`. The result is a fresh IdMap containing all ranges from `idSet` that are not * in `exclude`. * - * @template {IdMap} Set - * @param {Set} set + * @template {IdMap} ISet + * @param {ISet} set * @param {IdSet | IdMap} exclude - * @return {Set} + * @return {ISet} */ export const diffIdMap = _diffSet diff --git a/src/utils/StructStore.js b/src/utils/StructStore.js index 8aa0000a..72438dbb 100644 --- a/src/utils/StructStore.js +++ b/src/utils/StructStore.js @@ -24,6 +24,7 @@ export class StructStore { */ this.pendingDs = null } + get ds () { return createDeleteSetFromStructStore(this) } diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js index 255b3325..c90f3b76 100644 --- a/src/utils/Transaction.js +++ b/src/utils/Transaction.js @@ -11,7 +11,7 @@ import { createID, cleanupYTextAfterTransaction, IdSet, UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc, // eslint-disable-line - insertIntoIdSet + // insertIntoIdSet } from '../internals.js' import * as error from 'lib0/error' diff --git a/tests/IdMap.tests.js b/tests/IdMap.tests.js index a6692257..24388e32 100644 --- a/tests/IdMap.tests.js +++ b/tests/IdMap.tests.js @@ -1,6 +1,6 @@ import * as t from 'lib0/testing' 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 @@ -9,7 +9,7 @@ import { compareIdmaps, createIdMap, ID, createRandomIdSet, createRandomIdMap } const simpleConstructAttrs = ops => { const attrs = createIdMap() 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 } diff --git a/tests/testHelper.js b/tests/testHelper.js index 8b146797..6e55c746 100644 --- a/tests/testHelper.js +++ b/tests/testHelper.js @@ -8,7 +8,7 @@ import * as map from 'lib0/map' import * as Y from '../src/index.js' import * as math from 'lib0/math' import { - idmapAttrsEqual, createIdSet, createIdMap, addToIdSet + createIdSet, createIdMap, addToIdSet } from '../src/internals.js' export * from '../src/index.js' @@ -327,6 +327,28 @@ export const compareIdSets = (idSet1, idSet2) => { return true } +/** + * only use for testing + * + * @template T + * @param {Array>} attrs + * @param {Y.Attribution} attr + * + */ +const _idmapAttrsHas = (attrs, attr) => { + const hash = attr.hash() + return attrs.find(a => a.hash() === hash) +} + +/** + * only use for testing + * + * @template T + * @param {Array>} a + * @param {Array>} b + */ +export const _idmapAttrsEqual = (a, b) => a.length === b.length && a.every(v => _idmapAttrsHas(b, v)) + /** * @template T * @param {Y.IdMap} am1 @@ -341,7 +363,7 @@ export const compareIdmaps = (am1, am2) => { for (let i = 0; i < items1.length; i++) { const di1 = items1[i] const di2 = /** @type {Array>} */ (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 @@ -392,7 +414,7 @@ export const createRandomIdMap = (gen, clients, clockRange, attrChoices) => { attrs.push(a) } } - idMap.add(client, clockStart, len, attrs) + idMap.add(client, clockStart, len, attrs.map(v => Y.createAttribution('', v))) } return idMap }