lib0 1.0.0-rc.21 → 1.0.0-rc.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/array.d.ts +4 -4
- package/dist/broadcastchannel.d.ts +3 -3
- package/dist/decoding.d.ts +3 -3
- package/dist/delta/delta.d.ts +0 -8
- package/dist/delta/rdt/dom.d.ts +19 -25
- package/dist/delta/rdt.d.ts +24 -0
- package/dist/diff.d.ts +1 -1
- package/dist/encoding.d.ts +4 -4
- package/dist/environment.common.d.ts +21 -0
- package/dist/environment.d.ts +2 -8
- package/dist/error.d.ts +21 -3
- package/dist/eventloop.d.ts +1 -1
- package/dist/indexeddb.d.ts +3 -3
- package/dist/indexeddbV2.d.ts +3 -3
- package/dist/iterator.d.ts +3 -3
- package/dist/list.d.ts +6 -6
- package/dist/logging.common.d.ts +1 -1
- package/dist/logging.d.ts +1 -1
- package/dist/logging.node.d.ts +1 -1
- package/dist/map.d.ts +4 -4
- package/dist/object.d.ts +2 -2
- package/dist/observable.d.ts +2 -2
- package/dist/pair.d.ts +2 -2
- package/dist/pledge.d.ts +10 -10
- package/dist/promise.d.ts +2 -2
- package/dist/schema.d.ts +19 -1
- package/dist/sort.d.ts +3 -3
- package/dist/storage.d.ts +2 -2
- package/dist/testing.d.ts +18 -11
- package/package.json +3 -3
- package/src/array.js +5 -5
- package/src/broadcastchannel.js +4 -4
- package/src/cache.js +1 -1
- package/src/decoding.js +2 -2
- package/src/delta/delta.js +13 -26
- package/src/delta/rdt/dom.js +176 -63
- package/src/delta/rdt.js +39 -10
- package/src/diff.js +1 -1
- package/src/encoding.js +2 -2
- package/src/environment.common.js +41 -0
- package/src/environment.js +2 -21
- package/src/error.js +10 -4
- package/src/eventloop.js +3 -3
- package/src/function.js +1 -1
- package/src/indexeddb.js +4 -4
- package/src/indexeddbV2.js +4 -4
- package/src/iterator.js +4 -4
- package/src/list.js +4 -4
- package/src/logging.common.js +3 -3
- package/src/logging.js +3 -3
- package/src/logging.node.js +3 -3
- package/src/map.js +4 -4
- package/src/mutex.js +2 -2
- package/src/object.js +2 -2
- package/src/observable.js +1 -1
- package/src/pair.js +2 -2
- package/src/pledge.js +6 -6
- package/src/promise.js +3 -3
- package/src/schema.js +316 -3
- package/src/sort.js +4 -4
- package/src/storage.js +8 -4
- package/src/testing.js +15 -15
package/dist/testing.d.ts
CHANGED
|
@@ -36,26 +36,33 @@ export class TestCase {
|
|
|
36
36
|
get prng(): prng.PRNG;
|
|
37
37
|
}
|
|
38
38
|
export const repetitionTime: number;
|
|
39
|
-
export function run(moduleName: string, name: string, f: (
|
|
39
|
+
export function run(moduleName: string, name: string, f: (tc: TestCase) => void | Promise<any>, i: number, numberOfTests: number): Promise<boolean>;
|
|
40
40
|
export function describe(description: string, info?: string): void;
|
|
41
41
|
export function info(info: string): void;
|
|
42
42
|
export const printDom: (_createNode: () => Node) => void;
|
|
43
43
|
export const printCanvas: (canvas: HTMLCanvasElement, height: number) => void;
|
|
44
|
-
export function group(description: string, f: (...args: any
|
|
45
|
-
export function groupAsync(description: string, f: (...args: any
|
|
46
|
-
export function measureTime(message: string, f: (...args: any
|
|
47
|
-
export function measureTimeAsync(message: string, f: (...args: any
|
|
44
|
+
export function group(description: string, f: (...args: Array<any>) => void): void;
|
|
45
|
+
export function groupAsync(description: string, f: (...args: Array<any>) => Promise<any>): Promise<void>;
|
|
46
|
+
export function measureTime(message: string, f: (...args: Array<any>) => void): number;
|
|
47
|
+
export function measureTimeAsync(message: string, f: (...args: Array<any>) => Promise<any>): Promise<number>;
|
|
48
48
|
export function compareArrays<T>(as: Array<T>, bs: Array<T>, m?: string): boolean;
|
|
49
49
|
export function compareStrings(a: string, b: string, m?: string): void;
|
|
50
50
|
export function compareObjects<K, V>(a: any, b: any, m?: string): void;
|
|
51
|
-
export function compare<T>(a: T, b: T, message?: string | null, customCompare?: (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
export function compare<T>(a: T, b: T, message?: string | null, customCompare?: (constructor: any, a: T, b: T, path: string, compareValues: any) => boolean): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* `@type` (not `@param`/`@return`) is required — an assertion signature only narrows if the
|
|
54
|
+
* callee is declared with an explicit type annotation.
|
|
55
|
+
*
|
|
56
|
+
* @throws {TestError}
|
|
57
|
+
* @type {<T>(property: T, message?: string|null) => asserts property is NonNullable<T>}
|
|
58
|
+
*/
|
|
59
|
+
export const assert: <T>(property: T, message?: string | null) => asserts property is NonNullable<T>;
|
|
60
|
+
export function promiseRejected(f: (...args: Array<any>) => Promise<any>): Promise<void>;
|
|
61
|
+
export function fails(f: (...args: Array<any>) => void): void;
|
|
62
|
+
export function failsAsync(f: (...args: Array<any>) => Promise<any>): Promise<undefined>;
|
|
56
63
|
export function runTests(tests: {
|
|
57
64
|
[x: string]: {
|
|
58
|
-
[x: string]: (
|
|
65
|
+
[x: string]: (tc: TestCase) => any | Promise<any>;
|
|
59
66
|
};
|
|
60
67
|
}): Promise<boolean>;
|
|
61
68
|
export function fail(reason: string): never;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lib0",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.23",
|
|
4
4
|
"description": "",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -332,9 +332,9 @@
|
|
|
332
332
|
},
|
|
333
333
|
"devDependencies": {
|
|
334
334
|
"@types/node": "^26.0.0",
|
|
335
|
-
"c8": "^
|
|
335
|
+
"c8": "^12.0.0",
|
|
336
336
|
"dpdm": "^4.0.1",
|
|
337
|
-
"jsdom": "^
|
|
337
|
+
"jsdom": "^30.0.1",
|
|
338
338
|
"standard": "^17.1.0",
|
|
339
339
|
"typescript": "^6.0.3"
|
|
340
340
|
},
|
package/src/array.js
CHANGED
|
@@ -108,7 +108,7 @@ export const flatten = arr => fold(arr, /** @type {Array<ELEM>} */ ([]), (acc, v
|
|
|
108
108
|
/**
|
|
109
109
|
* @template T
|
|
110
110
|
* @param {number} len
|
|
111
|
-
* @param {
|
|
111
|
+
* @param {(index: number, arr: Array<T>) => T} f
|
|
112
112
|
* @return {Array<T>}
|
|
113
113
|
*/
|
|
114
114
|
export const unfold = (len, f) => {
|
|
@@ -124,7 +124,7 @@ export const unfold = (len, f) => {
|
|
|
124
124
|
* @template RESULT
|
|
125
125
|
* @param {Array<T>} arr
|
|
126
126
|
* @param {RESULT} seed
|
|
127
|
-
* @param {
|
|
127
|
+
* @param {(acc: RESULT, val: T, index: number) => RESULT} folder
|
|
128
128
|
*/
|
|
129
129
|
export const fold = (arr, seed, folder) => arr.reduce(folder, seed)
|
|
130
130
|
|
|
@@ -145,7 +145,7 @@ export const unique = arr => from(set.from(arr))
|
|
|
145
145
|
* @template T
|
|
146
146
|
* @template M
|
|
147
147
|
* @param {ArrayLike<T>} arr
|
|
148
|
-
* @param {
|
|
148
|
+
* @param {(val: T) => M} mapper
|
|
149
149
|
* @return {Array<T>}
|
|
150
150
|
*/
|
|
151
151
|
export const uniqueBy = (arr, mapper) => {
|
|
@@ -170,10 +170,10 @@ export const uniqueBy = (arr, mapper) => {
|
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
172
|
* @template {ArrayLike<any>} ARR
|
|
173
|
-
* @template {
|
|
173
|
+
* @template {(val: ARR extends ArrayLike<infer T> ? T : never, index: number, arr: ARR) => any} MAPPER
|
|
174
174
|
* @param {ARR} arr
|
|
175
175
|
* @param {MAPPER} mapper
|
|
176
|
-
* @return {Array<MAPPER extends
|
|
176
|
+
* @return {Array<MAPPER extends ((...args: Array<any>) => infer M) ? M : never>}
|
|
177
177
|
*/
|
|
178
178
|
export const map = (arr, mapper) => {
|
|
179
179
|
/**
|
package/src/broadcastchannel.js
CHANGED
|
@@ -24,7 +24,7 @@ import * as storage from './storage.js'
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* @typedef {Object} Channel
|
|
27
|
-
* @property {Set<
|
|
27
|
+
* @property {Set<(data: any, origin: any) => any>} Channel.subs
|
|
28
28
|
* @property {any} Channel.bc
|
|
29
29
|
*/
|
|
30
30
|
|
|
@@ -41,7 +41,7 @@ class LocalStoragePolyfill {
|
|
|
41
41
|
constructor (room) {
|
|
42
42
|
this.room = room
|
|
43
43
|
/**
|
|
44
|
-
* @type {null|
|
|
44
|
+
* @type {null|((e: {data:Uint8Array}) => void)}
|
|
45
45
|
*/
|
|
46
46
|
this.onmessage = null
|
|
47
47
|
/**
|
|
@@ -96,7 +96,7 @@ const getChannel = room =>
|
|
|
96
96
|
*
|
|
97
97
|
* @function
|
|
98
98
|
* @param {string} room
|
|
99
|
-
* @param {
|
|
99
|
+
* @param {(data: any, origin: any) => any} f
|
|
100
100
|
*/
|
|
101
101
|
export const subscribe = (room, f) => {
|
|
102
102
|
getChannel(room).subs.add(f)
|
|
@@ -108,7 +108,7 @@ export const subscribe = (room, f) => {
|
|
|
108
108
|
*
|
|
109
109
|
* @function
|
|
110
110
|
* @param {string} room
|
|
111
|
-
* @param {
|
|
111
|
+
* @param {(data: any, origin: any) => any} f
|
|
112
112
|
*/
|
|
113
113
|
export const unsubscribe = (room, f) => {
|
|
114
114
|
const channel = getChannel(room)
|
package/src/cache.js
CHANGED
|
@@ -173,7 +173,7 @@ export const remove = (cache, key) => {
|
|
|
173
173
|
*
|
|
174
174
|
* @param {Cache<K, V>} cache
|
|
175
175
|
* @param {K} key
|
|
176
|
-
* @param {
|
|
176
|
+
* @param {() => Promise<V>} init
|
|
177
177
|
* @param {boolean} removeNull Optional argument that automatically removes values that resolve to null/undefined from the cache.
|
|
178
178
|
* @return {Promise<V> | V}
|
|
179
179
|
*/
|
package/src/decoding.js
CHANGED
|
@@ -460,7 +460,7 @@ export const readBigInt64 = decoder => /** @type {any} */ (readFromDataView(deco
|
|
|
460
460
|
export const readBigUint64 = decoder => /** @type {any} */ (readFromDataView(decoder, 8)).getBigUint64(0, false)
|
|
461
461
|
|
|
462
462
|
/**
|
|
463
|
-
* @type {Array<
|
|
463
|
+
* @type {Array<(decoder: Decoder) => any>}
|
|
464
464
|
*/
|
|
465
465
|
const readAnyLookupTable = [
|
|
466
466
|
decoder => undefined, // CASE 127: undefined
|
|
@@ -508,7 +508,7 @@ export const readAny = decoder => readAnyLookupTable[127 - readUint8(decoder)](d
|
|
|
508
508
|
export class RleDecoder extends Decoder {
|
|
509
509
|
/**
|
|
510
510
|
* @param {Uint8Array} uint8Array
|
|
511
|
-
* @param {
|
|
511
|
+
* @param {(decoder: Decoder) => T} reader
|
|
512
512
|
*/
|
|
513
513
|
constructor (uint8Array, reader) {
|
|
514
514
|
super(uint8Array)
|
package/src/delta/delta.js
CHANGED
|
@@ -355,15 +355,6 @@ export class TextOp extends list.ListNode {
|
|
|
355
355
|
this._fingerprint = null
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
/**
|
|
359
|
-
* @param {string} newVal
|
|
360
|
-
*/
|
|
361
|
-
_updateInsert (newVal) {
|
|
362
|
-
// @ts-ignore
|
|
363
|
-
this.insert = newVal
|
|
364
|
-
this._fingerprint = null
|
|
365
|
-
}
|
|
366
|
-
|
|
367
358
|
/**
|
|
368
359
|
* @return {'insert'}
|
|
369
360
|
*/
|
|
@@ -462,18 +453,6 @@ export class InsertOp extends list.ListNode {
|
|
|
462
453
|
this._fingerprint = null
|
|
463
454
|
}
|
|
464
455
|
|
|
465
|
-
/* c8 ignore start */
|
|
466
|
-
/**
|
|
467
|
-
* @param {ArrayContent} _newVal
|
|
468
|
-
*/
|
|
469
|
-
_updateInsert (_newVal) {
|
|
470
|
-
// Mirror of TextOp._updateInsert; not currently called on InsertOp because
|
|
471
|
-
// adjacent inserts are merged in-place via `end.insert.push(...)`. Kept for
|
|
472
|
-
// parity with TextOp's API.
|
|
473
|
-
error.unexpectedCase() // throw if called
|
|
474
|
-
}
|
|
475
|
-
/* c8 ignore stop */
|
|
476
|
-
|
|
477
456
|
/**
|
|
478
457
|
* @return {'insert'}
|
|
479
458
|
*/
|
|
@@ -1437,7 +1416,7 @@ export class Delta extends DeltaData {
|
|
|
1437
1416
|
})
|
|
1438
1417
|
encoding.writeVarUint(encoder, keys.length)
|
|
1439
1418
|
for (const key of keys) {
|
|
1440
|
-
encoding.writeVarString(encoder, /** @type {any} */ (this.attrs[/** @type {keyof
|
|
1419
|
+
encoding.writeVarString(encoder, /** @type {any} */ (this.attrs[/** @type {keyof typeof this.attrs} */ (key)]).fingerprint)
|
|
1441
1420
|
}
|
|
1442
1421
|
encoding.writeVarUint(encoder, this.children.len)
|
|
1443
1422
|
for (const child of this.children) {
|
|
@@ -1810,13 +1789,15 @@ export const _mergeChildWithPrev = (d, op) => {
|
|
|
1810
1789
|
} else if ($deleteOp.check(op)) {
|
|
1811
1790
|
/** @type {DeleteOp<any>} */ (prevOp).delete += op.delete
|
|
1812
1791
|
} else if ($textOp.check(op)) {
|
|
1813
|
-
|
|
1792
|
+
// @ts-ignore
|
|
1793
|
+
/** @type {TextOp} */ (prevOp).insert += op.insert
|
|
1814
1794
|
/* c8 ignore start */
|
|
1815
1795
|
} else {
|
|
1816
1796
|
// unreachable: the constructor check at the top of the function already
|
|
1817
1797
|
// limits `op` to one of the four kinds tested above
|
|
1818
1798
|
error.unexpectedCase()
|
|
1819
1799
|
}
|
|
1800
|
+
prevOp._fingerprint = null
|
|
1820
1801
|
/* c8 ignore stop */
|
|
1821
1802
|
list.remove(d.children, op)
|
|
1822
1803
|
return true
|
|
@@ -2100,7 +2081,9 @@ export class DeltaBuilder extends Delta {
|
|
|
2100
2081
|
const end = this.children.end
|
|
2101
2082
|
if (s.$string.check(insert)) {
|
|
2102
2083
|
if ($textOp.check(end) && checkMergedEquals(end)) {
|
|
2103
|
-
|
|
2084
|
+
// @ts-ignore
|
|
2085
|
+
end.insert += insert
|
|
2086
|
+
end._fingerprint = null
|
|
2104
2087
|
} else if (insert.length > 0) {
|
|
2105
2088
|
list.pushEnd(this.children, new TextOp(insert, mergedFormats, mergedAttribution))
|
|
2106
2089
|
}
|
|
@@ -3659,7 +3642,7 @@ export const random = (gen, $d, conf = {}) => {
|
|
|
3659
3642
|
}
|
|
3660
3643
|
for (let i = prng.uint32(gen, minChildOps, maxChildOps); i > 0; i--) {
|
|
3661
3644
|
/**
|
|
3662
|
-
* @type {Array<
|
|
3645
|
+
* @type {Array<() => void>}
|
|
3663
3646
|
*/
|
|
3664
3647
|
const possibleOps = []
|
|
3665
3648
|
if (hasText) {
|
|
@@ -3818,7 +3801,11 @@ export const create = (nodeNameOrSchema, attrsOrSchema, children) => {
|
|
|
3818
3801
|
* }>}
|
|
3819
3802
|
*/
|
|
3820
3803
|
/**
|
|
3821
|
-
*
|
|
3804
|
+
* `...X` (not `Array<X>`) is the rest-parameter form — `@param {Array<X>} args` declares a single
|
|
3805
|
+
* array-typed parameter, which makes the overloads above incompatible with this signature. TS only
|
|
3806
|
+
* cross-checks overloads against the implementation for function declarations, so it stayed silent.
|
|
3807
|
+
*
|
|
3808
|
+
* @param {...(string|null|{[K:string|number]:any}|Array<any>)} args
|
|
3822
3809
|
* @return {DeltaBuilder<{}>}
|
|
3823
3810
|
*/
|
|
3824
3811
|
export const from = (...args) => {
|
package/src/delta/rdt/dom.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* # DOM delta RDT
|
|
5
5
|
*
|
|
6
|
-
* {@link domRDT} creates an RDT (see `../rdt.js`) backed by a live DOM subtree.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* {@link
|
|
10
|
-
*
|
|
6
|
+
* {@link domRDT} creates an RDT (see `../rdt.js`) backed by a live DOM subtree. A `MutationObserver`
|
|
7
|
+
* reports which nodes changed; the mirror ({@link DomRDT#_state}) is reconciled *incrementally* — only
|
|
8
|
+
* the changed paths are re-read from the DOM, every unchanged subtree is reused by reference (via the
|
|
9
|
+
* {@link DomRDT#_nodes} `WeakMap`), and the rebuilt tree is diffed against the previous mirror to emit a
|
|
10
|
+
* minimal `'delta'` (with the RDT itself as its {@link import('../rdt.js').RDT origin}). Incoming deltas
|
|
11
|
+
* are applied back onto the DOM. This lets a DOM subtree be bound to any other RDT.
|
|
11
12
|
*
|
|
12
13
|
* @module delta/rdt/dom
|
|
13
14
|
*/
|
|
@@ -30,30 +31,15 @@ import * as s from '../../schema.js'
|
|
|
30
31
|
*/
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
34
|
+
* A shared, never-mutated empty `onPath` set for the initial full read (see {@link reconcile}), where an
|
|
35
|
+
* empty `_nodes` map means nothing is reused anyway.
|
|
35
36
|
*
|
|
36
|
-
* @
|
|
37
|
-
* @return {DomDelta}
|
|
37
|
+
* @type {Set<Node>}
|
|
38
38
|
*/
|
|
39
|
-
const
|
|
40
|
-
if (dom.$element.check(domNode)) {
|
|
41
|
-
const d = delta.create(domNode.nodeName.toLowerCase())
|
|
42
|
-
for (let i = 0; i < domNode.attributes.length; i++) {
|
|
43
|
-
const attr = /** @type {Attr} */ (domNode.attributes.item(i))
|
|
44
|
-
d.setAttr(attr.nodeName, attr.value)
|
|
45
|
-
}
|
|
46
|
-
domNode.childNodes.forEach(child => {
|
|
47
|
-
d.insert(dom.$text.check(child) ? (child.textContent ?? '') : [domToDelta(child)])
|
|
48
|
-
})
|
|
49
|
-
return /** @type {DomDelta} */ (d)
|
|
50
|
-
}
|
|
51
|
-
/* c8 ignore next */ // defensive: only element nodes are ever rendered/observed
|
|
52
|
-
error.unexpectedCase()
|
|
53
|
-
}
|
|
39
|
+
const EMPTY_ON_PATH = new Set()
|
|
54
40
|
|
|
55
41
|
/**
|
|
56
|
-
* Render a {@link DomDelta} into a fresh DOM element (the inverse of {@link
|
|
42
|
+
* Render a {@link DomDelta} into a fresh DOM element (the inverse of {@link reconcile}).
|
|
57
43
|
*
|
|
58
44
|
* @param {DomDelta} d
|
|
59
45
|
* @return {Element}
|
|
@@ -187,9 +173,143 @@ export const $domDelta = /* @__PURE__ */ delta.$delta({ name: s.$string, attrs:
|
|
|
187
173
|
* @typedef {delta.Delta<DomConf>} DomDelta
|
|
188
174
|
*/
|
|
189
175
|
|
|
176
|
+
// The mirror is maintained by these module-private functions (kept as functions, not methods, per the
|
|
177
|
+
// lib0 style guide — methods are reserved for the duck-typed RDT interface: `applyDelta`/`delta`/
|
|
178
|
+
// `destroy`). Each takes the {@link DomRDT} whose `_state`/`_nodes`/`observedNode` it reads or advances.
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Reconcile `node` against the DOM, reusing subtrees by reference. When `node` is not on the dirty
|
|
182
|
+
* `onPath` and is already mapped in `rdt._nodes`, its existing mirror subtree is returned unread;
|
|
183
|
+
* otherwise the node's shell (name + attributes + child list) is rebuilt — text children become an
|
|
184
|
+
* `insert` of their content (adjacent text coalesces, normalising `splitText`), element children recurse
|
|
185
|
+
* — and re-registered in `rdt._nodes`. Purely constructive: it never mutates an existing mirror node,
|
|
186
|
+
* only allocates fresh shells and shares unchanged subtrees.
|
|
187
|
+
*
|
|
188
|
+
* @param {DomRDT} rdt
|
|
189
|
+
* @param {Node} node
|
|
190
|
+
* @param {Set<Node>} onPath dirty elements ∪ their ancestors up to the observed root
|
|
191
|
+
* @return {DomDelta}
|
|
192
|
+
*/
|
|
193
|
+
const reconcile = (rdt, node, onPath) => {
|
|
194
|
+
if (!onPath.has(node) && rdt._nodes.has(node)) {
|
|
195
|
+
return /** @type {DomDelta} */ (rdt._nodes.get(node))
|
|
196
|
+
}
|
|
197
|
+
if (dom.$element.check(node)) {
|
|
198
|
+
const d = delta.create(node.nodeName.toLowerCase())
|
|
199
|
+
for (let i = 0; i < node.attributes.length; i++) {
|
|
200
|
+
const attr = /** @type {Attr} */ (node.attributes.item(i))
|
|
201
|
+
d.setAttr(attr.nodeName, attr.value)
|
|
202
|
+
}
|
|
203
|
+
node.childNodes.forEach(child => {
|
|
204
|
+
d.insert(dom.$text.check(child) ? (child.textContent ?? '') : [reconcile(rdt, child, onPath)])
|
|
205
|
+
})
|
|
206
|
+
rdt._nodes.set(node, /** @type {DomDelta} */ (d))
|
|
207
|
+
return /** @type {DomDelta} */ (d)
|
|
208
|
+
}
|
|
209
|
+
/* c8 ignore next */ // defensive: only element nodes are ever rendered/observed
|
|
210
|
+
error.unexpectedCase()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The set of elements whose OWN content (attributes, character data, or child list) a mutation batch
|
|
215
|
+
* changed: a `characterData` record dirties the text node's parent, every other record its target.
|
|
216
|
+
* Targets no longer under the observed root (removed in this same batch) are dropped — their removal is
|
|
217
|
+
* captured by a surviving ancestor's `childList` record.
|
|
218
|
+
*
|
|
219
|
+
* @param {DomRDT} rdt
|
|
220
|
+
* @param {MutationRecord[]} records
|
|
221
|
+
* @return {Set<Element>}
|
|
222
|
+
*/
|
|
223
|
+
const dirtyElements = (rdt, records) => {
|
|
224
|
+
/** @type {Set<Element>} */
|
|
225
|
+
const dirty = new Set()
|
|
226
|
+
for (const r of records) {
|
|
227
|
+
const el = r.type === 'characterData' ? r.target.parentNode : r.target
|
|
228
|
+
// `contains` is reflexive, so it admits the observed root itself and drops disconnected targets
|
|
229
|
+
if (el != null && dom.$element.check(el) && rdt.observedNode.contains(el)) {
|
|
230
|
+
dirty.add(el)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return dirty
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The dirty elements together with all their ancestors up to (and including) the observed root — the
|
|
238
|
+
* nodes whose shells {@link reconcile} must rebuild (everything off this path is reused).
|
|
239
|
+
*
|
|
240
|
+
* @param {DomRDT} rdt
|
|
241
|
+
* @param {Set<Element>} dirty
|
|
242
|
+
* @return {Set<Node>}
|
|
243
|
+
*/
|
|
244
|
+
const computeOnPath = (rdt, dirty) => {
|
|
245
|
+
/** @type {Set<Node>} */
|
|
246
|
+
const onPath = new Set()
|
|
247
|
+
for (const el of dirty) {
|
|
248
|
+
/** @type {Node?} */
|
|
249
|
+
let p = el
|
|
250
|
+
while (p != null && !onPath.has(p)) {
|
|
251
|
+
onPath.add(p)
|
|
252
|
+
if (p === rdt.observedNode) break
|
|
253
|
+
p = p.parentNode
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return onPath
|
|
257
|
+
}
|
|
258
|
+
|
|
190
259
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
260
|
+
* Reconcile the mirror against the DOM described by `records`, returning the freshly-rebuilt root — or
|
|
261
|
+
* `null` when nothing relevant changed (in which case no `rdt._nodes` entries were touched, so the
|
|
262
|
+
* caller may safely leave `rdt._state` as-is).
|
|
263
|
+
*
|
|
264
|
+
* @param {DomRDT} rdt
|
|
265
|
+
* @param {MutationRecord[]} records
|
|
266
|
+
* @return {DomDelta?}
|
|
267
|
+
*/
|
|
268
|
+
const reconcileFromRecords = (rdt, records) => {
|
|
269
|
+
const dirty = dirtyElements(rdt, records)
|
|
270
|
+
if (dirty.size === 0) return null
|
|
271
|
+
return reconcile(rdt, rdt.observedNode, computeOnPath(rdt, dirty))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Reconcile against the DOM described by `records` and return the change to emit (the diff of the
|
|
276
|
+
* previous mirror against the rebuilt one), advancing `rdt._state`. Returns an empty delta when nothing
|
|
277
|
+
* relevant changed.
|
|
278
|
+
*
|
|
279
|
+
* @param {DomRDT} rdt
|
|
280
|
+
* @param {MutationRecord[]} records
|
|
281
|
+
* @return {DomDelta}
|
|
282
|
+
*/
|
|
283
|
+
const pull = (rdt, records) => {
|
|
284
|
+
const fresh = reconcileFromRecords(rdt, records)
|
|
285
|
+
if (fresh == null) return /** @type {DomDelta} */ (delta.create())
|
|
286
|
+
// `clone: true` is REQUIRED here (not merely an optimisation as on a throwaway diff): `fresh` becomes
|
|
287
|
+
// the persistent `_state`, so the emitted change must share none of its subtrees — a consumer that
|
|
288
|
+
// freezes/mutates a shared child would otherwise corrupt the live mirror.
|
|
289
|
+
const change = /** @type {DomDelta} */ (delta.diff(rdt._state, fresh, { clone: true }))
|
|
290
|
+
// MUST commit: `reconcile` already re-pointed `rdt._nodes` at the rebuilt shells, so keeping the old
|
|
291
|
+
// `_state` would desync it. (Skipping is safe only when nothing was reconciled — the `fresh == null`
|
|
292
|
+
// short-circuit above.)
|
|
293
|
+
rdt._state = fresh
|
|
294
|
+
return change
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The `MutationObserver` callback: reconcile the batch and, if it produced a change, emit it with the
|
|
299
|
+
* RDT itself as origin (a locally-observed DOM edit is produced by this RDT — see {@link RDT}).
|
|
300
|
+
*
|
|
301
|
+
* @param {DomRDT} rdt
|
|
302
|
+
* @param {MutationRecord[]} mutations
|
|
303
|
+
*/
|
|
304
|
+
const mutationHandler = (rdt, mutations) => {
|
|
305
|
+
const change = pull(rdt, mutations)
|
|
306
|
+
if (!change.isEmpty()) rdt.emit('delta', [change, rdt])
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* An RDT backed by a live DOM subtree. DOM mutations observed via `MutationObserver` reconcile the
|
|
311
|
+
* mirror incrementally (re-reading only changed paths, reusing unchanged subtrees by reference) and the
|
|
312
|
+
* resulting minimal diff is emitted as a delta; incoming deltas are applied back onto the DOM.
|
|
193
313
|
*
|
|
194
314
|
* @implements {RDT<DomConf>}
|
|
195
315
|
* @extends {ObservableV2<{ delta: (delta: delta.Delta<DomConf>, origin: any) => void, destroy: (rdt: DomRDT) => void }>}
|
|
@@ -206,44 +326,30 @@ class DomRDT extends ObservableV2 {
|
|
|
206
326
|
this.$delta = /** @type {any} */ ($domDelta)
|
|
207
327
|
this.observedNode = observedNode
|
|
208
328
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
329
|
+
* Maps each observed DOM node to its mirror delta node in {@link DomRDT#_state}. A {@link reconcile}
|
|
330
|
+
* reuses an unchanged subtree by reference (skipping its DOM read) via this map; entries for removed
|
|
331
|
+
* nodes are reclaimed automatically.
|
|
332
|
+
*
|
|
333
|
+
* @type {WeakMap<Node, DomDelta>}
|
|
334
|
+
*/
|
|
335
|
+
this._nodes = new WeakMap()
|
|
336
|
+
/**
|
|
337
|
+
* The live delta mirror of the observed subtree, maintained incrementally. A reconcile rebuilds only
|
|
338
|
+
* the changed paths and swaps this whole root (see {@link pull}); it is never mutated in place, so a
|
|
339
|
+
* mirror handed out by {@link DomRDT#delta} stays a valid immutable snapshot.
|
|
211
340
|
*
|
|
212
341
|
* @type {DomDelta}
|
|
213
342
|
*/
|
|
214
|
-
this._state =
|
|
215
|
-
this.observer = new MutationObserver(this
|
|
343
|
+
this._state = reconcile(this, observedNode, EMPTY_ON_PATH)
|
|
344
|
+
this.observer = new MutationObserver(mutations => mutationHandler(this, mutations))
|
|
216
345
|
this.observer.observe(observedNode, {
|
|
217
346
|
subtree: true,
|
|
218
347
|
childList: true,
|
|
219
348
|
attributes: true,
|
|
220
|
-
|
|
349
|
+
characterData: true
|
|
221
350
|
})
|
|
222
351
|
}
|
|
223
352
|
|
|
224
|
-
/**
|
|
225
|
-
* Pull the local DOM changes accumulated since the last sync as a delta (by diffing the live DOM
|
|
226
|
-
* against `_state`), advancing `_state` to the current DOM.
|
|
227
|
-
*
|
|
228
|
-
* @return {DomDelta}
|
|
229
|
-
*/
|
|
230
|
-
_pull () {
|
|
231
|
-
const next = domToDelta(this.observedNode)
|
|
232
|
-
const change = delta.diff(this._state, next)
|
|
233
|
-
this._state = next
|
|
234
|
-
return change
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* @param {MutationRecord[]} mutations
|
|
239
|
-
*/
|
|
240
|
-
_mutationHandler = mutations => {
|
|
241
|
-
if (mutations.length === 0) return
|
|
242
|
-
const change = this._pull()
|
|
243
|
-
// a locally-observed DOM edit: this RDT is the producer, so it is the origin (see {@link RDT})
|
|
244
|
-
if (!change.isEmpty()) this.emit('delta', [change, this])
|
|
245
|
-
}
|
|
246
|
-
|
|
247
353
|
/**
|
|
248
354
|
* Apply a foreign delta onto the DOM.
|
|
249
355
|
*
|
|
@@ -257,10 +363,12 @@ class DomRDT extends ObservableV2 {
|
|
|
257
363
|
* listeners can recognise (and skip) their own changes — see {@link RDT} “Origins”. Defaults to `null`
|
|
258
364
|
* (an anonymous/local change).
|
|
259
365
|
* @return {delta.DeltaBuilder<DomConf> | null} the rebased local change (`b`), or `null` when there
|
|
260
|
-
* were no concurrent edits
|
|
366
|
+
* were no concurrent edits. A binding maps a returned fix back onto the other side with the
|
|
367
|
+
* `correction` origin (see `../rdt.js`)
|
|
261
368
|
*/
|
|
262
369
|
applyDelta (d, origin = null) {
|
|
263
|
-
|
|
370
|
+
// Drain + diff any pending local edits (their async MutationObserver callback may not have fired).
|
|
371
|
+
const b = pull(this, this.observer.takeRecords())
|
|
264
372
|
/** @type {DomDelta} */
|
|
265
373
|
let toApply = d
|
|
266
374
|
/** @type {DomDelta?} */
|
|
@@ -272,10 +380,13 @@ class DomRDT extends ObservableV2 {
|
|
|
272
380
|
fix = bOnD.isEmpty() ? null : bOnD
|
|
273
381
|
}
|
|
274
382
|
applyDeltaToDom(this.observedNode, toApply)
|
|
275
|
-
|
|
276
|
-
// MutationObserver callbacks are async, so the Binding's
|
|
277
|
-
//
|
|
278
|
-
|
|
383
|
+
// Re-sync `_state` from the DOM our own write produced, reconciling from its self-caused records
|
|
384
|
+
// (taken here — MutationObserver callbacks are async, so the Binding's synchronous mutex cannot
|
|
385
|
+
// suppress this echo; draining the records is what prevents it). We reconcile rather than
|
|
386
|
+
// `_state.apply(toApply)` because apply's copy-on-write would re-clone nodes out of `_nodes`, and
|
|
387
|
+
// its inserted nodes carry no mapping — the DOM (incl. its text coalescing) is the authority.
|
|
388
|
+
const fresh = reconcileFromRecords(this, this.observer.takeRecords())
|
|
389
|
+
if (fresh != null) this._state = fresh
|
|
279
390
|
// Forward the effective change so a chained binding on the other side picks it up; the binding
|
|
280
391
|
// that fed us `d` swallows this re-emit via its own mutex (see ../rdt.js). `fix` is a freshly
|
|
281
392
|
// rebased builder at runtime, returned as the owned change the binding maps on.
|
|
@@ -284,12 +395,14 @@ class DomRDT extends ObservableV2 {
|
|
|
284
395
|
}
|
|
285
396
|
|
|
286
397
|
/**
|
|
287
|
-
* The current state as a delta: an "insert everything" delta
|
|
398
|
+
* The current state as a delta: the live mirror ({@link DomRDT#_state}), an "insert everything" delta
|
|
399
|
+
* describing the observed subtree. A shared read snapshot — consumers clone before mutating (see
|
|
400
|
+
* {@link RDT}); the mirror is never mutated in place, so a returned snapshot stays valid.
|
|
288
401
|
*
|
|
289
402
|
* @return {delta.Delta<DomConf>}
|
|
290
403
|
*/
|
|
291
404
|
get delta () {
|
|
292
|
-
return
|
|
405
|
+
return this._state
|
|
293
406
|
}
|
|
294
407
|
|
|
295
408
|
destroy () {
|