mirror of
https://github.com/yjs/yjs.git
synced 2025-12-24 07:29:24 +01:00
implement attribution class that is de-duplicated in IdMap
This commit is contained in:
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export class StructStore {
|
|||||||
*/
|
*/
|
||||||
this.pendingDs = null
|
this.pendingDs = null
|
||||||
}
|
}
|
||||||
|
|
||||||
get ds () {
|
get ds () {
|
||||||
return createDeleteSetFromStructStore(this)
|
return createDeleteSetFromStructStore(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user