lib0 1.0.0-rc.2 → 1.0.0-rc.21

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.
Files changed (141) hide show
  1. package/README.md +0 -1315
  2. package/{types → dist}/buffer.d.ts +2 -10
  3. package/{types → dist}/delta/delta.d.ts +582 -144
  4. package/dist/delta/position.d.ts +65 -0
  5. package/dist/delta/rdt/delta.d.ts +57 -0
  6. package/dist/delta/rdt/dom.d.ts +102 -0
  7. package/dist/delta/rdt.d.ts +180 -0
  8. package/dist/delta/transformer/attr.d.ts +64 -0
  9. package/dist/delta/transformer/attribution-to-format.d.ts +52 -0
  10. package/dist/delta/transformer/children.d.ts +69 -0
  11. package/dist/delta/transformer/conform.d.ts +83 -0
  12. package/dist/delta/transformer/core.d.ts +189 -0
  13. package/dist/delta/transformer/full-attributions.d.ts +37 -0
  14. package/dist/delta/transformer/id.d.ts +2 -0
  15. package/dist/delta/transformer/inline.d.ts +89 -0
  16. package/dist/delta/transformer/pipe.d.ts +182 -0
  17. package/dist/delta/transformer/project.d.ts +223 -0
  18. package/dist/delta/transformer/rename-attrs.d.ts +75 -0
  19. package/dist/delta/transformer/rename.d.ts +41 -0
  20. package/dist/delta/transformer/value.d.ts +92 -0
  21. package/dist/delta/transformer.d.ts +14 -0
  22. package/dist/diff/patience.d.ts +25 -0
  23. package/{types → dist}/environment.d.ts +10 -0
  24. package/dist/hash/fnv1a.d.ts +11 -0
  25. package/{types → dist}/schema.d.ts +49 -38
  26. package/{types → dist}/ts.d.ts +4 -0
  27. package/dist/webcrypto.d.ts +3 -0
  28. package/package.json +148 -82
  29. package/src/bin/0serve.js +11 -3
  30. package/src/broadcastchannel.js +6 -1
  31. package/src/buffer.js +35 -25
  32. package/src/decoding.js +1 -1
  33. package/src/delta/delta.js +2531 -564
  34. package/src/delta/position.js +165 -0
  35. package/src/delta/rdt/delta.js +105 -0
  36. package/src/delta/rdt/dom.js +307 -0
  37. package/src/delta/rdt.js +255 -0
  38. package/src/delta/transformer/attr.js +126 -0
  39. package/src/delta/transformer/attribution-to-format.js +335 -0
  40. package/src/delta/transformer/children.js +219 -0
  41. package/src/delta/transformer/conform.js +401 -0
  42. package/src/delta/transformer/core.js +323 -0
  43. package/src/delta/transformer/full-attributions.js +282 -0
  44. package/src/delta/transformer/id.js +13 -0
  45. package/src/delta/transformer/inline.js +818 -0
  46. package/src/delta/transformer/pipe.js +272 -0
  47. package/src/delta/transformer/project.js +526 -0
  48. package/src/delta/transformer/rename-attrs.js +152 -0
  49. package/src/delta/transformer/rename.js +84 -0
  50. package/src/delta/transformer/value.js +214 -0
  51. package/src/delta/transformer.js +38 -0
  52. package/src/diff/patience.js +22 -11
  53. package/src/dom.js +2 -2
  54. package/src/encoding.js +1 -1
  55. package/src/environment.js +15 -0
  56. package/src/hash/fnv1a.js +82 -0
  57. package/src/number.js +1 -3
  58. package/src/performance.node.js +3 -3
  59. package/src/schema.js +231 -177
  60. package/src/string.js +3 -5
  61. package/src/testing.js +1 -1
  62. package/src/trait/equality.js +1 -1
  63. package/src/trait/fingerprint.js +1 -1
  64. package/src/ts.js +11 -0
  65. package/src/bin/gendocs.js +0 -117
  66. package/src/component.js +0 -414
  67. package/src/delta/binding.js +0 -372
  68. package/src/tree.js +0 -546
  69. package/types/bin/gendocs.d.ts +0 -3
  70. package/types/component.d.ts +0 -86
  71. package/types/delta/binding.d.ts +0 -106
  72. package/types/diff/patience.d.ts +0 -16
  73. package/types/tree.d.ts +0 -96
  74. package/types/webcrypto.d.ts +0 -3
  75. /package/{types → dist}/array.d.ts +0 -0
  76. /package/{types → dist}/bin/0ecdsa-generate-keypair.d.ts +0 -0
  77. /package/{types → dist}/bin/0serve.d.ts +0 -0
  78. /package/{types → dist}/bin/gentesthtml.d.ts +0 -0
  79. /package/{types → dist}/binary.d.ts +0 -0
  80. /package/{types → dist}/broadcastchannel.d.ts +0 -0
  81. /package/{types → dist}/cache.d.ts +0 -0
  82. /package/{types → dist}/conditions.d.ts +0 -0
  83. /package/{types → dist}/crypto/aes-gcm.d.ts +0 -0
  84. /package/{types → dist}/crypto/common.d.ts +0 -0
  85. /package/{types → dist}/crypto/ecdsa.d.ts +0 -0
  86. /package/{types → dist}/crypto/jwt.d.ts +0 -0
  87. /package/{types → dist}/crypto/rsa-oaep.d.ts +0 -0
  88. /package/{types → dist}/decoding.d.ts +0 -0
  89. /package/{types → dist}/diff.d.ts +0 -0
  90. /package/{types → dist}/dom.d.ts +0 -0
  91. /package/{types → dist}/encoding.d.ts +0 -0
  92. /package/{types → dist}/error.d.ts +0 -0
  93. /package/{types → dist}/eventloop.d.ts +0 -0
  94. /package/{types → dist}/function.d.ts +0 -0
  95. /package/{types → dist}/hash/rabin-gf2-polynomial.d.ts +0 -0
  96. /package/{types → dist}/hash/rabin-uncached.d.ts +0 -0
  97. /package/{types → dist}/hash/rabin.d.ts +0 -0
  98. /package/{types → dist}/hash/sha256.d.ts +0 -0
  99. /package/{types → dist}/hash/sha256.node.d.ts +0 -0
  100. /package/{types → dist}/indexeddb.d.ts +0 -0
  101. /package/{types → dist}/indexeddbV2.d.ts +0 -0
  102. /package/{types → dist}/iterator.d.ts +0 -0
  103. /package/{types → dist}/json.d.ts +0 -0
  104. /package/{types → dist}/list.d.ts +0 -0
  105. /package/{types → dist}/logging.common.d.ts +0 -0
  106. /package/{types → dist}/logging.d.ts +0 -0
  107. /package/{types → dist}/logging.node.d.ts +0 -0
  108. /package/{types → dist}/map.d.ts +0 -0
  109. /package/{types → dist}/math.d.ts +0 -0
  110. /package/{types → dist}/metric.d.ts +0 -0
  111. /package/{types → dist}/mutex.d.ts +0 -0
  112. /package/{types → dist}/number.d.ts +0 -0
  113. /package/{types → dist}/object.d.ts +0 -0
  114. /package/{types → dist}/observable.d.ts +0 -0
  115. /package/{types → dist}/pair.d.ts +0 -0
  116. /package/{types → dist}/performance.d.ts +0 -0
  117. /package/{types → dist}/performance.node.d.ts +0 -0
  118. /package/{types → dist}/pledge.d.ts +0 -0
  119. /package/{types → dist}/prng/Mt19937.d.ts +0 -0
  120. /package/{types → dist}/prng/Xoroshiro128plus.d.ts +0 -0
  121. /package/{types → dist}/prng/Xorshift32.d.ts +0 -0
  122. /package/{types → dist}/prng.d.ts +0 -0
  123. /package/{types → dist}/promise.d.ts +0 -0
  124. /package/{types → dist}/queue.d.ts +0 -0
  125. /package/{types → dist}/random.d.ts +0 -0
  126. /package/{types → dist}/set.d.ts +0 -0
  127. /package/{types → dist}/sort.d.ts +0 -0
  128. /package/{types → dist}/statistics.d.ts +0 -0
  129. /package/{types → dist}/storage.d.ts +0 -0
  130. /package/{types → dist}/string.d.ts +0 -0
  131. /package/{types → dist}/symbol.d.ts +0 -0
  132. /package/{types → dist}/testing.d.ts +0 -0
  133. /package/{types → dist}/time.d.ts +0 -0
  134. /package/{types → dist}/trait/equality.d.ts +0 -0
  135. /package/{types → dist}/trait/fingerprint.d.ts +0 -0
  136. /package/{types → dist}/trait/traits.d.ts +0 -0
  137. /package/{types → dist}/url.d.ts +0 -0
  138. /package/{types → dist}/webcrypto.deno.d.ts +0 -0
  139. /package/{types → dist}/webcrypto.node.d.ts +0 -0
  140. /package/{types → dist}/webcrypto.react-native.d.ts +0 -0
  141. /package/{types → dist}/websocket.d.ts +0 -0
@@ -0,0 +1,165 @@
1
+ /**
2
+ * # Positions in a delta tree
3
+ *
4
+ * A {@link Pos} is a resolution-independent location in a *final* (insert-only) {@link
5
+ * import('./delta.js').Delta delta} tree — a cursor between two characters, a point inside a nested
6
+ * node, or a reference to an attribute value. It is the value an editor stores for a caret/selection
7
+ * and the thing a transformer maps from one bound RDT to the other (see `./transformer/`).
8
+ *
9
+ * A position is a `path` of steps plus an association (gravity):
10
+ * - a **string** step descends into the attribute with that key,
11
+ * - a **number** step is a *content* index (each character and each array element counts as 1,
12
+ * matching {@link import('./delta.js').Delta}'s `childCnt`): a non-terminal number descends into
13
+ * the child node at that slot, and the **trailing** number is the terminal cursor *gap*.
14
+ *
15
+ * ```
16
+ * [5] cursor gap at offset 5 in the root
17
+ * ['a', 1] inside attribute 'a', cursor gap at offset 1
18
+ * [1, 5] descend into the child node at slot 1, cursor gap at offset 5 inside it
19
+ * ['a'] the value of attribute 'a' (attribute leaf; no offset)
20
+ * [] the root node itself
21
+ * ```
22
+ *
23
+ * Numbers are always read as child indices. A *number-keyed attribute* therefore has no unambiguous
24
+ * position: {@link marksToPositions} still emits a mark inside one, but its numeric attribute step is
25
+ * indistinguishable from a content index, so such a {@link Pos} mis-resolves and must not be round-
26
+ * tripped through {@link import('./delta.js').DeltaBuilder#addMark}. Use string attribute keys for any
27
+ * node that may carry marks (numeric attribute keys are not used in practice).
28
+ *
29
+ * @module delta/position
30
+ */
31
+
32
+ import * as s from '../schema.js'
33
+ import * as fun from '../function.js'
34
+ import * as delta from './delta.js'
35
+
36
+ /**
37
+ * A single step of a {@link Pos} path: a `string` attribute key, or a `number` content index.
38
+ *
39
+ * @typedef {string|number} PosStep
40
+ */
41
+
42
+ /**
43
+ * A location in a delta tree. `path` descends from the node the position is relative to and ends in
44
+ * the terminal (a trailing-number cursor gap, or a trailing-string attribute leaf). `assoc` is the
45
+ * gravity at a boundary: `-1` binds to the preceding content, `1` to the following content. `attrs` is
46
+ * optional immutable user data carried with the position (e.g. an RDT's clientID/user metadata); it is
47
+ * stored on the {@link import('./delta.js').createMark mark} when the position is written with
48
+ * {@link import('./delta.js').DeltaBuilder#addMark} and read back by {@link marksToPositions}.
49
+ *
50
+ * @typedef {{ path: Array<PosStep>, assoc: -1|1, attrs?: object|null }} Pos
51
+ */
52
+
53
+ /**
54
+ * A {@link Pos} tagged with the unique id of a stored {@link import('./delta.js').createMark mark} —
55
+ * what {@link marksToPositions} returns (its `attrs` is the mark's stored metadata). Add marks with
56
+ * {@link import('./delta.js').DeltaBuilder#addMark}.
57
+ *
58
+ * @typedef {{ id: string } & Pos} MarkPos
59
+ */
60
+
61
+ /**
62
+ * Schema for a {@link Pos}.
63
+ *
64
+ * @type {s.Schema<Pos>}
65
+ */
66
+ export const $pos = /* @__PURE__ */ s.$object({
67
+ path: s.$array(s.$union(s.$string, s.$number)),
68
+ assoc: s.$literal(-1, 1),
69
+ attrs: s.$any.optional
70
+ })
71
+
72
+ /**
73
+ * Create a {@link Pos} from a path, an (optional) association (gravity, default right `1`), and
74
+ * (optional) immutable `attrs` carried with the mark. `attrs` is omitted from the result when `null`,
75
+ * so a plain cursor position stays `{ path, assoc }`.
76
+ *
77
+ * @param {Array<PosStep>} path
78
+ * @param {-1|1} [assoc]
79
+ * @param {object?} [attrs]
80
+ * @return {Pos}
81
+ */
82
+ export const create = (path, assoc = 1, attrs = null) => attrs === null ? { path, assoc } : { path, assoc, attrs }
83
+
84
+ /**
85
+ * Structural equality of two positions: same `assoc`, `id` (a plain {@link Pos} has none ⇒ both
86
+ * `undefined`), `attrs` (deep), and `path`.
87
+ *
88
+ * @param {Pos|MarkPos} a
89
+ * @param {Pos|MarkPos} b
90
+ * @return {boolean}
91
+ */
92
+ export const equals = (a, b) => a.assoc === b.assoc && /** @type {any} */ (a).id === /** @type {any} */ (b).id && fun.equalityDeep(a.attrs ?? null, b.attrs ?? null) && a.path.length === b.path.length && a.path.every((step, i) => step === b.path[i])
93
+
94
+ /**
95
+ * Schema for a {@link MarkPos}.
96
+ *
97
+ * @type {s.Schema<MarkPos>}
98
+ */
99
+ export const $markPos = /* @__PURE__ */ s.$object({
100
+ id: s.$string,
101
+ path: s.$array(s.$union(s.$string, s.$number)),
102
+ assoc: s.$literal(-1, 1),
103
+ attrs: s.$any.optional
104
+ })
105
+
106
+ /**
107
+ * Reconstruct every {@link MarkPos} stored as a {@link import('./delta.js').Mark mark} inside `d` (a
108
+ * settled delta). A mark's `path` is the content indices / attribute keys walked to reach it, plus the
109
+ * mark's own `key`. Add marks with {@link import('./delta.js').DeltaBuilder#addMark}.
110
+ *
111
+ * Subtrees are pruned via each node's conservative `maybeHasMarks` flag (`false` ⇒ guaranteed empty,
112
+ * skipped). This is also the **sole resetter** of that flag: a descended subtree that turns out to hold
113
+ * no marks has its (stale) `true` cleared to `false`, so later calls prune it. The flag is therefore a
114
+ * cheap hint that this read keeps eventually-accurate (a write during a read, like a lazy cache).
115
+ *
116
+ * @param {delta.DeltaAny} d
117
+ * @return {Array<MarkPos>}
118
+ */
119
+ export const marksToPositions = d => {
120
+ /** @type {Array<MarkPos>} */
121
+ const out = []
122
+ /**
123
+ * Emit the marks in `node`'s subtree and return whether any were found (used to self-correct the flag).
124
+ *
125
+ * @param {delta.DeltaAny} node
126
+ * @param {Array<PosStep>} path
127
+ * @return {boolean}
128
+ */
129
+ const walk = (node, path) => {
130
+ if (!node.maybeHasMarks) return false // guaranteed no marks in this subtree
131
+ let found = false
132
+ if (node.marks !== null) {
133
+ for (const m of node.marks) {
134
+ found = true
135
+ out.push(m.attrs === null
136
+ ? { id: m.id, path: [...path, m.key], assoc: m.assoc }
137
+ : { id: m.id, path: [...path, m.key], assoc: m.assoc, attrs: m.attrs })
138
+ }
139
+ }
140
+ // a settled delta has only text/insert children; descend the delta-valued embeds
141
+ let i = 0
142
+ for (const op of node.children) {
143
+ if (delta.$insertOp.check(op)) {
144
+ for (const el of op.insert) {
145
+ if (delta.$deltaAny.check(el) && el.maybeHasMarks && walk(el, [...path, i])) found = true
146
+ i++
147
+ }
148
+ } else {
149
+ i += op.length
150
+ }
151
+ }
152
+ for (const op of node.attrs) {
153
+ // descend `setAttr` (a materialized value) AND `modifyAttr` (an incremental change into a not-yet-
154
+ // materialized sub-document attribute, which `apply` may leave on a settled node) - both hold a
155
+ // delta value reachable here.
156
+ if ((delta.$setAttrOp.check(op) || delta.$modifyAttrOp.check(op)) && delta.$deltaAny.check(op.value) && op.value.maybeHasMarks && walk(op.value, [...path, op.key])) {
157
+ found = true
158
+ }
159
+ }
160
+ node.maybeHasMarks = found // self-correct: clear a stale `true` so later reads prune this subtree
161
+ return found
162
+ }
163
+ walk(d, [])
164
+ return out
165
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * # In-memory delta RDT
3
+ *
4
+ * {@link deltaRDT} creates an RDT (see `../rdt.js`) whose state is the accumulated delta. Calling
5
+ * `applyDelta(delta, origin)` merges the incoming delta into `state` and re-emits it (with the given
6
+ * {@link import('../rdt.js').RDT origin}) as a `'delta'` event, so it can be bound to any other RDT. It
7
+ * accepts every valid delta as-is, so it never returns a fix.
8
+ *
9
+ * `state` is kept as a **final** delta (`isFinal`): it represents the current document, so a `delete`
10
+ * or `deleteAttr` removes the content/attribute outright instead of accumulating a delete-op marker.
11
+ * The `delta` getter therefore always returns a clean insert-only document.
12
+ *
13
+ * @module delta/rdt/delta
14
+ */
15
+
16
+ import { ObservableV2 } from '../../observable.js'
17
+ import * as delta from '../delta.js'
18
+ import * as mux from '../../mutex.js'
19
+
20
+ /**
21
+ * @template T
22
+ * @typedef {import('../../schema.js').Schema<T>} Schema
23
+ */
24
+
25
+ /**
26
+ * @template {delta.DeltaConf} Conf
27
+ * @typedef {import('../rdt.js').RDT<Conf>} RDT
28
+ */
29
+
30
+ /**
31
+ * An in-memory RDT whose state is the accumulated delta. Calling `applyDelta` merges the incoming delta
32
+ * into `state` (via {@link delta.DeltaBuilder#apply}) and re-emits it as a `'delta'`.
33
+ *
34
+ * @template {delta.DeltaConf} Conf
35
+ * @implements {RDT<Conf>}
36
+ * @extends {ObservableV2<{ delta: (delta: delta.Delta<Conf>, origin: any) => void, destroy: (rdt: DeltaRDT<Conf>) => void }>}
37
+ */
38
+ class DeltaRDT extends ObservableV2 {
39
+ /**
40
+ * @param {Schema<delta.Delta<Conf>>} $delta schema of the deltas this RDT produces
41
+ */
42
+ constructor ($delta) {
43
+ super()
44
+ this.$delta = $delta
45
+ /**
46
+ * @type {delta.DeltaBuilderAny?}
47
+ */
48
+ this.state = null
49
+ this._mux = mux.createMutex()
50
+ }
51
+
52
+ /**
53
+ * Merge an incoming delta into the current state and notify observers. Always returns `null`: this
54
+ * RDT accepts every valid delta unchanged, so it never produces a fix.
55
+ *
56
+ * @param {delta.Delta<Conf>} d
57
+ * @param {any} [origin] who produced the change; forwarded verbatim on the emitted `'delta'` event so
58
+ * listeners can recognise (and skip) changes they made themselves — see {@link RDT} “Origins”. Defaults
59
+ * to `null` (an anonymous/local change).
60
+ * @return {null}
61
+ */
62
+ applyDelta (d, origin = null) {
63
+ if (d.isEmpty()) return null
64
+ this._mux(() => {
65
+ if (this.state == null) {
66
+ // start from an empty *final* document (see module doc) so deletes/deleteAttrs — even in the
67
+ // very first delta — remove content instead of leaving delete-op markers
68
+ this.state = delta.create(d.name)
69
+ this.state.isFinal = true
70
+ }
71
+ this.state.apply(d) // `final` defaults to `state.isFinal`, so deletes clean up the state
72
+ })
73
+ // emit AFTER releasing the mutex. When bound to a fix-producing RDT, that side's fix is mapped
74
+ // back here and applied via a synchronous re-entrant `applyDelta` while this call is still on the
75
+ // stack; with the emit inside the mutex that re-entrant mutation would be dropped. The binding's
76
+ // own mutex still breaks the echo loop. `d` is emitted as a shared read delta (the RDT only read it
77
+ // into `state`); a consumer that needs to mutate it deep-clones it first (see the `RDT` typedef).
78
+ this.emit('delta', [d, origin])
79
+ return null
80
+ }
81
+
82
+ /**
83
+ * The current state as a delta: the accumulated `state`, or an empty delta when nothing has been
84
+ * applied yet.
85
+ *
86
+ * @return {delta.Delta<Conf>}
87
+ */
88
+ get delta () {
89
+ // `state` is a loosely-typed `DeltaBuilderAny`; narrow it to this RDT's read `Delta<Conf>` view.
90
+ return /** @type {delta.Delta<Conf>} */ (/** @type {unknown} */ (this.state ?? delta.create(this.$delta)))
91
+ }
92
+
93
+ destroy () {
94
+ this.emit('destroy', [this])
95
+ super.destroy()
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Create a {@link DeltaRDT} for deltas described by `$delta`.
101
+ *
102
+ * @template {delta.DeltaConf} Conf
103
+ * @param {Schema<delta.Delta<Conf>>} $delta
104
+ */
105
+ export const deltaRDT = $delta => new DeltaRDT($delta)
@@ -0,0 +1,307 @@
1
+ /* global MutationObserver */
2
+
3
+ /**
4
+ * # DOM delta RDT
5
+ *
6
+ * {@link domRDT} creates an RDT (see `../rdt.js`) backed by a live DOM subtree. DOM mutations are
7
+ * observed with a `MutationObserver`, turned into deltas (by diffing the new DOM state against the
8
+ * last-known one) and emitted as `'delta'` events (with the RDT itself as their
9
+ * {@link import('../rdt.js').RDT origin}); incoming deltas are applied back onto the DOM. This lets a
10
+ * DOM subtree be bound to any other RDT.
11
+ *
12
+ * @module delta/rdt/dom
13
+ */
14
+
15
+ import { ObservableV2 } from '../../observable.js'
16
+ import * as delta from '../delta.js'
17
+ import * as dom from '../../dom.js'
18
+ import * as error from '../../error.js'
19
+ import * as math from '../../math.js'
20
+ import * as s from '../../schema.js'
21
+
22
+ /**
23
+ * @template T
24
+ * @typedef {import('../../schema.js').Schema<T>} Schema
25
+ */
26
+
27
+ /**
28
+ * @template {delta.DeltaConf} Conf
29
+ * @typedef {import('../rdt.js').RDT<Conf>} RDT
30
+ */
31
+
32
+ /**
33
+ * Recursively convert a DOM node into a {@link DomDelta} (an "insert everything" delta describing the
34
+ * node, its attributes, and its subtree).
35
+ *
36
+ * @param {Node} domNode
37
+ * @return {DomDelta}
38
+ */
39
+ const domToDelta = domNode => {
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
+ }
54
+
55
+ /**
56
+ * Render a {@link DomDelta} into a fresh DOM element (the inverse of {@link domToDelta}).
57
+ *
58
+ * @param {DomDelta} d
59
+ * @return {Element}
60
+ */
61
+ const deltaToDom = d => {
62
+ if (delta.$deltaAny.check(d)) {
63
+ const n = dom.element(/** @type {string} */ (d.name))
64
+ for (const change of d.attrs) {
65
+ if (delta.$setAttrOp.check(change)) {
66
+ n.setAttribute(change.key, change.value)
67
+ }
68
+ }
69
+ d.children.forEach(child => {
70
+ if (delta.$insertOp.check(child)) {
71
+ n.append(...child.insert.map(el => deltaToDom(/** @type {DomDelta} */ (el))))
72
+ } else if (delta.$textOp.check(child)) {
73
+ n.append(dom.text(child.insert))
74
+ }
75
+ })
76
+ return n
77
+ }
78
+ /* c8 ignore next */ // defensive: deltaToDom is only ever called on delta nodes
79
+ error.unexpectedCase()
80
+ }
81
+
82
+ /**
83
+ * Apply a {@link DomDelta} as an incremental change onto an existing DOM element, mutating it in place.
84
+ *
85
+ * The element's children are addressed in *content* coordinates: each element child is one position,
86
+ * each character of a text node is one position. `childIndex` walks `el.childNodes`; `childOffset` is
87
+ * the offset within the current text node. `child` is re-read on every loop step because the previous
88
+ * step may have inserted, removed or split nodes.
89
+ *
90
+ * @param {Element} el
91
+ * @param {DomDelta} d
92
+ */
93
+ const applyDeltaToDom = (el, d) => {
94
+ for (const change of d.attrs) {
95
+ if (delta.$deleteAttrOp.check(change)) {
96
+ el.removeAttribute(change.key)
97
+ } else if (delta.$setAttrOp.check(change)) {
98
+ el.setAttribute(change.key, change.value)
99
+ }
100
+ }
101
+ let childIndex = 0
102
+ let childOffset = 0
103
+ d.children.forEach(change => {
104
+ if (delta.$retainOp.check(change)) {
105
+ let len = change.retain
106
+ while (len > 0) {
107
+ const child = el.childNodes[childIndex]
108
+ /* c8 ignore next */ // defensive: a well-formed delta never advances past the children
109
+ if (child == null) break
110
+ if (dom.$text.check(child)) {
111
+ const remaining = child.length - childOffset
112
+ if (remaining <= len) {
113
+ len -= remaining
114
+ childOffset = 0
115
+ childIndex++
116
+ } else {
117
+ childOffset += len
118
+ len = 0
119
+ }
120
+ } else {
121
+ childIndex++
122
+ len--
123
+ }
124
+ }
125
+ } else if (delta.$deleteOp.check(change)) {
126
+ let len = change.length
127
+ while (len > 0) {
128
+ const child = el.childNodes[childIndex]
129
+ /* c8 ignore next */ // defensive: a well-formed delta never deletes past the children
130
+ if (child == null) break
131
+ if (dom.$text.check(child)) {
132
+ const childLen = child.length
133
+ if (childOffset === 0 && childLen <= len) {
134
+ child.remove()
135
+ len -= childLen
136
+ } else {
137
+ const spliceLen = math.min(len, childLen - childOffset)
138
+ child.deleteData(childOffset, spliceLen)
139
+ len -= spliceLen
140
+ if (child.length <= childOffset) {
141
+ childOffset = 0
142
+ childIndex++
143
+ }
144
+ }
145
+ } else {
146
+ child.remove()
147
+ len--
148
+ }
149
+ }
150
+ } else if (delta.$insertOp.check(change) || delta.$textOp.check(change)) {
151
+ if (childOffset > 0) {
152
+ dom.$text.cast(el.childNodes[childIndex]).splitText(childOffset)
153
+ childIndex++
154
+ childOffset = 0
155
+ }
156
+ const ref = el.childNodes[childIndex] ?? null
157
+ if (delta.$textOp.check(change)) {
158
+ el.insertBefore(dom.text(change.insert), ref)
159
+ childIndex++
160
+ } else {
161
+ el.insertBefore(dom.fragment(change.insert.map(c => deltaToDom(/** @type {DomDelta} */ (c)))), ref)
162
+ childIndex += change.insert.length
163
+ }
164
+ } else if (delta.$modifyOp.check(change)) {
165
+ applyDeltaToDom(dom.$element.cast(el.childNodes[childIndex]), /** @type {DomDelta} */ (change.value))
166
+ childIndex++
167
+ /* c8 ignore next 3 */ // unreachable: $domDelta children are only retain/delete/insert/text/modify
168
+ } else {
169
+ error.unexpectedCase()
170
+ }
171
+ })
172
+ }
173
+
174
+ /**
175
+ * Schema describing the deltas produced/consumed by a {@link DomRDT}: a recursive node with a string
176
+ * name, string→string attributes, and text content.
177
+ */
178
+ export const $domDelta = /* @__PURE__ */ delta.$delta({ name: s.$string, attrs: s.$record(s.$string, s.$string), children: s.$never, text: true, recursiveChildren: true })
179
+
180
+ /**
181
+ * The {@link delta.DeltaConf} of the deltas a {@link DomRDT} produces/consumes (see {@link $domDelta}).
182
+ *
183
+ * @typedef {{ name: string, attrs: { [key:string]: string }, text: true, recursiveChildren: true }} DomConf
184
+ */
185
+
186
+ /**
187
+ * @typedef {delta.Delta<DomConf>} DomDelta
188
+ */
189
+
190
+ /**
191
+ * An RDT backed by a live DOM subtree. DOM mutations observed via `MutationObserver` are diffed
192
+ * against the last-known state and emitted as deltas; incoming deltas are applied back onto the DOM.
193
+ *
194
+ * @implements {RDT<DomConf>}
195
+ * @extends {ObservableV2<{ delta: (delta: delta.Delta<DomConf>, origin: any) => void, destroy: (rdt: DomRDT) => void }>}
196
+ */
197
+ class DomRDT extends ObservableV2 {
198
+ /**
199
+ * @param {Element} observedNode
200
+ */
201
+ constructor (observedNode) {
202
+ super()
203
+ /**
204
+ * @type {Schema<DomDelta>}
205
+ */
206
+ this.$delta = /** @type {any} */ ($domDelta)
207
+ this.observedNode = observedNode
208
+ /**
209
+ * Last-known DOM state. The observe path diffs the live DOM against this to compute the change to
210
+ * emit, then advances it; `applyDelta` re-syncs it after mutating the DOM.
211
+ *
212
+ * @type {DomDelta}
213
+ */
214
+ this._state = domToDelta(observedNode)
215
+ this.observer = new MutationObserver(this._mutationHandler)
216
+ this.observer.observe(observedNode, {
217
+ subtree: true,
218
+ childList: true,
219
+ attributes: true,
220
+ characterDataOldValue: true
221
+ })
222
+ }
223
+
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
+ /**
248
+ * Apply a foreign delta onto the DOM.
249
+ *
250
+ * The DOM may have been edited locally (`b`) since the last sync, concurrently with the incoming
251
+ * change (`d`). We pull `b` first and rebase the two against each other (OT) so neither is lost:
252
+ * `d` rebased onto `b` is applied to the DOM, and `b` rebased onto `d` is returned as the fix so the
253
+ * binding pipes it back through the transformer to the other side.
254
+ *
255
+ * @param {delta.Delta<DomConf>} d
256
+ * @param {any} [origin] who produced `d`; forwarded verbatim on the emitted `'delta'` event so
257
+ * listeners can recognise (and skip) their own changes — see {@link RDT} “Origins”. Defaults to `null`
258
+ * (an anonymous/local change).
259
+ * @return {delta.DeltaBuilder<DomConf> | null} the rebased local change (`b`), or `null` when there
260
+ * were no concurrent edits
261
+ */
262
+ applyDelta (d, origin = null) {
263
+ const b = this._pull()
264
+ /** @type {DomDelta} */
265
+ let toApply = d
266
+ /** @type {DomDelta?} */
267
+ let fix = null
268
+ if (!b.isEmpty()) {
269
+ // clone both sides so neither original is mutated by the in-place `rebase`
270
+ toApply = delta.clone(d).rebase(b, true) // d on top of the local DOM (which has b)
271
+ const bOnD = delta.clone(b).rebase(d, false) // b on top of d, for the other side
272
+ fix = bOnD.isEmpty() ? null : bOnD
273
+ }
274
+ applyDeltaToDom(this.observedNode, toApply)
275
+ this._state = domToDelta(this.observedNode)
276
+ // MutationObserver callbacks are async, so the Binding's (synchronous) mutex cannot suppress the
277
+ // echo of our own write — draining the self-caused records here is what prevents it.
278
+ this.observer.takeRecords()
279
+ // Forward the effective change so a chained binding on the other side picks it up; the binding
280
+ // that fed us `d` swallows this re-emit via its own mutex (see ../rdt.js). `fix` is a freshly
281
+ // rebased builder at runtime, returned as the owned change the binding maps on.
282
+ this.emit('delta', [toApply, origin])
283
+ return /** @type {delta.DeltaBuilder<DomConf>?} */ (fix)
284
+ }
285
+
286
+ /**
287
+ * The current state as a delta: an "insert everything" delta describing the observed subtree.
288
+ *
289
+ * @return {delta.Delta<DomConf>}
290
+ */
291
+ get delta () {
292
+ return domToDelta(this.observedNode)
293
+ }
294
+
295
+ destroy () {
296
+ this.emit('destroy', [this])
297
+ super.destroy()
298
+ this.observer.disconnect()
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Create a {@link DomRDT} that observes and edits `dom`.
304
+ *
305
+ * @param {Element} dom
306
+ */
307
+ export const domRDT = dom => new DomRDT(dom)