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.
- package/README.md +0 -1315
- package/{types → dist}/buffer.d.ts +2 -10
- package/{types → dist}/delta/delta.d.ts +582 -144
- package/dist/delta/position.d.ts +65 -0
- package/dist/delta/rdt/delta.d.ts +57 -0
- package/dist/delta/rdt/dom.d.ts +102 -0
- package/dist/delta/rdt.d.ts +180 -0
- package/dist/delta/transformer/attr.d.ts +64 -0
- package/dist/delta/transformer/attribution-to-format.d.ts +52 -0
- package/dist/delta/transformer/children.d.ts +69 -0
- package/dist/delta/transformer/conform.d.ts +83 -0
- package/dist/delta/transformer/core.d.ts +189 -0
- package/dist/delta/transformer/full-attributions.d.ts +37 -0
- package/dist/delta/transformer/id.d.ts +2 -0
- package/dist/delta/transformer/inline.d.ts +89 -0
- package/dist/delta/transformer/pipe.d.ts +182 -0
- package/dist/delta/transformer/project.d.ts +223 -0
- package/dist/delta/transformer/rename-attrs.d.ts +75 -0
- package/dist/delta/transformer/rename.d.ts +41 -0
- package/dist/delta/transformer/value.d.ts +92 -0
- package/dist/delta/transformer.d.ts +14 -0
- package/dist/diff/patience.d.ts +25 -0
- package/{types → dist}/environment.d.ts +10 -0
- package/dist/hash/fnv1a.d.ts +11 -0
- package/{types → dist}/schema.d.ts +49 -38
- package/{types → dist}/ts.d.ts +4 -0
- package/dist/webcrypto.d.ts +3 -0
- package/package.json +148 -82
- package/src/bin/0serve.js +11 -3
- package/src/broadcastchannel.js +6 -1
- package/src/buffer.js +35 -25
- package/src/decoding.js +1 -1
- package/src/delta/delta.js +2531 -564
- package/src/delta/position.js +165 -0
- package/src/delta/rdt/delta.js +105 -0
- package/src/delta/rdt/dom.js +307 -0
- package/src/delta/rdt.js +255 -0
- package/src/delta/transformer/attr.js +126 -0
- package/src/delta/transformer/attribution-to-format.js +335 -0
- package/src/delta/transformer/children.js +219 -0
- package/src/delta/transformer/conform.js +401 -0
- package/src/delta/transformer/core.js +323 -0
- package/src/delta/transformer/full-attributions.js +282 -0
- package/src/delta/transformer/id.js +13 -0
- package/src/delta/transformer/inline.js +818 -0
- package/src/delta/transformer/pipe.js +272 -0
- package/src/delta/transformer/project.js +526 -0
- package/src/delta/transformer/rename-attrs.js +152 -0
- package/src/delta/transformer/rename.js +84 -0
- package/src/delta/transformer/value.js +214 -0
- package/src/delta/transformer.js +38 -0
- package/src/diff/patience.js +22 -11
- package/src/dom.js +2 -2
- package/src/encoding.js +1 -1
- package/src/environment.js +15 -0
- package/src/hash/fnv1a.js +82 -0
- package/src/number.js +1 -3
- package/src/performance.node.js +3 -3
- package/src/schema.js +231 -177
- package/src/string.js +3 -5
- package/src/testing.js +1 -1
- package/src/trait/equality.js +1 -1
- package/src/trait/fingerprint.js +1 -1
- package/src/ts.js +11 -0
- package/src/bin/gendocs.js +0 -117
- package/src/component.js +0 -414
- package/src/delta/binding.js +0 -372
- package/src/tree.js +0 -546
- package/types/bin/gendocs.d.ts +0 -3
- package/types/component.d.ts +0 -86
- package/types/delta/binding.d.ts +0 -106
- package/types/diff/patience.d.ts +0 -16
- package/types/tree.d.ts +0 -96
- package/types/webcrypto.d.ts +0 -3
- /package/{types → dist}/array.d.ts +0 -0
- /package/{types → dist}/bin/0ecdsa-generate-keypair.d.ts +0 -0
- /package/{types → dist}/bin/0serve.d.ts +0 -0
- /package/{types → dist}/bin/gentesthtml.d.ts +0 -0
- /package/{types → dist}/binary.d.ts +0 -0
- /package/{types → dist}/broadcastchannel.d.ts +0 -0
- /package/{types → dist}/cache.d.ts +0 -0
- /package/{types → dist}/conditions.d.ts +0 -0
- /package/{types → dist}/crypto/aes-gcm.d.ts +0 -0
- /package/{types → dist}/crypto/common.d.ts +0 -0
- /package/{types → dist}/crypto/ecdsa.d.ts +0 -0
- /package/{types → dist}/crypto/jwt.d.ts +0 -0
- /package/{types → dist}/crypto/rsa-oaep.d.ts +0 -0
- /package/{types → dist}/decoding.d.ts +0 -0
- /package/{types → dist}/diff.d.ts +0 -0
- /package/{types → dist}/dom.d.ts +0 -0
- /package/{types → dist}/encoding.d.ts +0 -0
- /package/{types → dist}/error.d.ts +0 -0
- /package/{types → dist}/eventloop.d.ts +0 -0
- /package/{types → dist}/function.d.ts +0 -0
- /package/{types → dist}/hash/rabin-gf2-polynomial.d.ts +0 -0
- /package/{types → dist}/hash/rabin-uncached.d.ts +0 -0
- /package/{types → dist}/hash/rabin.d.ts +0 -0
- /package/{types → dist}/hash/sha256.d.ts +0 -0
- /package/{types → dist}/hash/sha256.node.d.ts +0 -0
- /package/{types → dist}/indexeddb.d.ts +0 -0
- /package/{types → dist}/indexeddbV2.d.ts +0 -0
- /package/{types → dist}/iterator.d.ts +0 -0
- /package/{types → dist}/json.d.ts +0 -0
- /package/{types → dist}/list.d.ts +0 -0
- /package/{types → dist}/logging.common.d.ts +0 -0
- /package/{types → dist}/logging.d.ts +0 -0
- /package/{types → dist}/logging.node.d.ts +0 -0
- /package/{types → dist}/map.d.ts +0 -0
- /package/{types → dist}/math.d.ts +0 -0
- /package/{types → dist}/metric.d.ts +0 -0
- /package/{types → dist}/mutex.d.ts +0 -0
- /package/{types → dist}/number.d.ts +0 -0
- /package/{types → dist}/object.d.ts +0 -0
- /package/{types → dist}/observable.d.ts +0 -0
- /package/{types → dist}/pair.d.ts +0 -0
- /package/{types → dist}/performance.d.ts +0 -0
- /package/{types → dist}/performance.node.d.ts +0 -0
- /package/{types → dist}/pledge.d.ts +0 -0
- /package/{types → dist}/prng/Mt19937.d.ts +0 -0
- /package/{types → dist}/prng/Xoroshiro128plus.d.ts +0 -0
- /package/{types → dist}/prng/Xorshift32.d.ts +0 -0
- /package/{types → dist}/prng.d.ts +0 -0
- /package/{types → dist}/promise.d.ts +0 -0
- /package/{types → dist}/queue.d.ts +0 -0
- /package/{types → dist}/random.d.ts +0 -0
- /package/{types → dist}/set.d.ts +0 -0
- /package/{types → dist}/sort.d.ts +0 -0
- /package/{types → dist}/statistics.d.ts +0 -0
- /package/{types → dist}/storage.d.ts +0 -0
- /package/{types → dist}/string.d.ts +0 -0
- /package/{types → dist}/symbol.d.ts +0 -0
- /package/{types → dist}/testing.d.ts +0 -0
- /package/{types → dist}/time.d.ts +0 -0
- /package/{types → dist}/trait/equality.d.ts +0 -0
- /package/{types → dist}/trait/fingerprint.d.ts +0 -0
- /package/{types → dist}/trait/traits.d.ts +0 -0
- /package/{types → dist}/url.d.ts +0 -0
- /package/{types → dist}/webcrypto.deno.d.ts +0 -0
- /package/{types → dist}/webcrypto.node.d.ts +0 -0
- /package/{types → dist}/webcrypto.react-native.d.ts +0 -0
- /package/{types → dist}/websocket.d.ts +0 -0
package/src/delta/rdt.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* # Replicated Data Types (RDTs) and bindings
|
|
3
|
+
*
|
|
4
|
+
* An **RDT** ("replicated data type") is any object that represents some state and communicates changes
|
|
5
|
+
* to that state as {@link import('./delta.js').Delta deltas}. Every RDT is an
|
|
6
|
+
* {@link import('../observable.js').ObservableV2 observable}:
|
|
7
|
+
* - it **emits** a `'delta'` event carrying a delta and its {@link RDT origin} whenever its own state
|
|
8
|
+
* changes, and
|
|
9
|
+
* - it **accepts** foreign changes through `applyDelta(delta, origin)`, which mutates its state to match.
|
|
10
|
+
*
|
|
11
|
+
* Because the changes are deltas, two different representations of "the same" data can be kept in sync
|
|
12
|
+
* even when their shapes differ. That is the job of a {@link Binding}: it connects two RDTs `a` and `b`
|
|
13
|
+
* through a {@link dt.Template transformer template}. The template is instantiated once (against `a`'s
|
|
14
|
+
* schema) into a stateful {@link dt.Transformer}, and every change is routed through it:
|
|
15
|
+
*
|
|
16
|
+
* - a change on `a` is fed to `transformer.applyA(d)`, whose result `{ a, b }` is pushed back via
|
|
17
|
+
* `a.applyDelta` (self-heal) and `b.applyDelta` (the mapped change), and symmetrically for `b` via `applyB`.
|
|
18
|
+
*
|
|
19
|
+
* Feeding the mapped change back into the other RDT makes it emit its own `'delta'`, which would loop
|
|
20
|
+
* forever; a {@link mux mutex} shared per side breaks that echo so a change is only transformed once.
|
|
21
|
+
*
|
|
22
|
+
* Applying a mapped change can make the receiving RDT enforce its own invariants and return a **fix**
|
|
23
|
+
* (see {@link RDT}) — a further change it made to itself. Because the echo mutex swallows the RDT's
|
|
24
|
+
* re-emit during propagation, the binding reads that fix from `applyDelta`'s return value and feeds it
|
|
25
|
+
* back as a fresh change on its side. A fix on one side and a fix on the other are concurrent, so they
|
|
26
|
+
* are rebased against each other: the pending pair `{ a, b }` is run through `transformer.apply`, whose
|
|
27
|
+
* machinery already transposes the two sides. This repeats until neither side reports a fix, so fixes
|
|
28
|
+
* must converge to a fixpoint (a well-behaved RDT applies them idempotently).
|
|
29
|
+
*
|
|
30
|
+
* On creation a binding first **synchronizes the initial state**: `a`'s current state (`a.delta`)
|
|
31
|
+
* is projected through the transformer, and the projection is diffed against `b`'s current state
|
|
32
|
+
* (`b.delta`); the resulting difference is applied to `b` so it matches `a`'s projection (and any
|
|
33
|
+
* self-heal correction is applied back to `a`). `a` is the source of truth — pre-existing state on `b`
|
|
34
|
+
* that `a` does not project to is overwritten.
|
|
35
|
+
*
|
|
36
|
+
* Two reference RDTs live in `./rdt/`:
|
|
37
|
+
* - `./rdt/delta.js` ({@link RDT}) — an in-memory delta whose state is just the accumulated delta.
|
|
38
|
+
* - `./rdt/dom.js` — a live DOM subtree, observed with a `MutationObserver`, that turns DOM mutations
|
|
39
|
+
* into deltas and applies incoming deltas back onto the DOM.
|
|
40
|
+
*
|
|
41
|
+
* @module delta/rdt
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import * as dt from './transformer.js'
|
|
45
|
+
import * as delta from './delta.js'
|
|
46
|
+
import * as mux from '../mutex.js'
|
|
47
|
+
import * as s from '../schema.js'
|
|
48
|
+
|
|
49
|
+
// Re-export the two reference RDTs this module's doc references, so `bind`, `deltaRDT` and `domRDT` are
|
|
50
|
+
// reachable from one import. The leaf modules (`lib0/delta/rdt/delta`, `lib0/delta/rdt/dom`) stay
|
|
51
|
+
// independently importable, so a consumer who wants only one keeps it tree-shakeable.
|
|
52
|
+
export { deltaRDT } from './rdt/delta.js'
|
|
53
|
+
export { $domDelta, domRDT } from './rdt/dom.js'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @template T
|
|
57
|
+
* @typedef {import('../schema.js').Schema<T>} Schema
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Abstract interface for a delta-based Replicated Data Type.
|
|
62
|
+
*
|
|
63
|
+
* An RDT is an observable that emits a `'delta'` (and, on teardown, a `'destroy'`) event, exposes the
|
|
64
|
+
* {@link Schema schema} of the deltas it produces via `$delta` (so a {@link Binding} can initialize a
|
|
65
|
+
* transformer for it), exposes its current state as a delta via the `delta` getter, and accepts foreign
|
|
66
|
+
* deltas via `applyDelta`.
|
|
67
|
+
*
|
|
68
|
+
* `applyDelta(d, origin)` applies `d` and then, if the change would violate the RDT's own invariants,
|
|
69
|
+
* applies a **fix** of its own (e.g. reversing part of `d`, or inserting missing content) and returns
|
|
70
|
+
* that fix — `null` when none was needed. The fix is a regular change on this RDT, so a {@link Binding}
|
|
71
|
+
* maps it onto the other side just like any other change. `applyDelta` also emits the *effective* change
|
|
72
|
+
* (`d` together with the fix) on the `'delta'` channel.
|
|
73
|
+
*
|
|
74
|
+
* ## Origins
|
|
75
|
+
*
|
|
76
|
+
* Every `'delta'` event carries a second argument, `origin` — an opaque `any` value (modelled after
|
|
77
|
+
* {@link https://docs.yjs.dev/api/transactions Yjs transaction origins}) that identifies **where the
|
|
78
|
+
* change came from**. Whoever produces the change sets it, and it is usually the producing instance
|
|
79
|
+
* itself: a communication provider, an editor binding, or the {@link Binding} that mapped the change
|
|
80
|
+
* over from the other side. A change an RDT observes locally (e.g. a `MutationObserver` firing on a DOM
|
|
81
|
+
* edit) uses that RDT itself as the origin.
|
|
82
|
+
*
|
|
83
|
+
* Origins let a listener tell **its own** changes apart from foreign ones so it can skip the ones it
|
|
84
|
+
* already knows about — a change it produced is not looped back to where it came from. The canonical use
|
|
85
|
+
* is a network provider: it tags every remote update it applies with itself (`rdt.applyDelta(update,
|
|
86
|
+
* this)`) and then ignores `'delta'` events whose `origin === this`, so it never re-broadcasts an update
|
|
87
|
+
* it just received. The `origin` argument to `applyDelta` is optional and defaults to `null` (an
|
|
88
|
+
* anonymous/local change); the emitted event always carries whatever value was supplied.
|
|
89
|
+
*
|
|
90
|
+
* ## `Delta` vs `DeltaBuilder`
|
|
91
|
+
*
|
|
92
|
+
* The interface is parameterized by a {@link import('./delta.js').DeltaConf DeltaConf} (like a
|
|
93
|
+
* {@link import('./transformer.js').Transformer transformer}). Almost everything it exposes is the
|
|
94
|
+
* *read* form — {@link import('./delta.js').Delta Delta} — because those values are **shared** and must
|
|
95
|
+
* not be mutated in place: `$delta` is the delta schema, the `delta` getter returns a state snapshot,
|
|
96
|
+
* `applyDelta` reads a change into the RDT, and the `'delta'` event payload is broadcast to every
|
|
97
|
+
* listener. The one *owned, mutable* form — {@link import('./delta.js').DeltaBuilder DeltaBuilder} — is
|
|
98
|
+
* the fix `applyDelta` returns to the {@link Binding}. The binding never mutates a shared read delta: it
|
|
99
|
+
* {@link import('./delta.js').cloneDeep deep-clones} every change into a private builder before the
|
|
100
|
+
* transformer rebases it (transformers manipulate deltas in place — see {@link Binding}).
|
|
101
|
+
*
|
|
102
|
+
* @template {import('./delta.js').DeltaConf} Conf
|
|
103
|
+
* @typedef {import('../observable.js').ObservableV2<{ delta: (delta: delta.Delta<Conf>, origin: any) => void, destroy: (rdt: RDT<Conf>) => void }> & {
|
|
104
|
+
* $delta: Schema<delta.Delta<Conf>>,
|
|
105
|
+
* delta: delta.Delta<Conf>,
|
|
106
|
+
* applyDelta: (delta: delta.Delta<Conf>, origin?: any) => delta.DeltaBuilder<Conf> | null,
|
|
107
|
+
* destroy: () => void
|
|
108
|
+
* }} RDT
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Schema guard for any {@link RDT}. RDTs are **duck-typed** — they have several independent
|
|
113
|
+
* implementations (`deltaRDT`, `domRDT`, …) and no common constructor — so this matches by shape
|
|
114
|
+
* rather than by `instanceof`: an object exposing a `$delta` {@link Schema}, an `applyDelta` method,
|
|
115
|
+
* and the observable `on`/`destroy` channel. The (possibly expensive) `delta` getter is not invoked.
|
|
116
|
+
*
|
|
117
|
+
* @type {Schema<RDT<any>>}
|
|
118
|
+
*/
|
|
119
|
+
export const $rdt = /** @type {Schema<RDT<any>>} */ (/* @__PURE__ */ s.$custom(o =>
|
|
120
|
+
o != null &&
|
|
121
|
+
s.$$schema.check(o.$delta) &&
|
|
122
|
+
typeof o.applyDelta === 'function' &&
|
|
123
|
+
typeof o.on === 'function' &&
|
|
124
|
+
typeof o.destroy === 'function'
|
|
125
|
+
))
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Propagate a pair of concurrent changes between the two sides of `binding` until they converge.
|
|
129
|
+
*
|
|
130
|
+
* `ta` / `tb` are changes that have ALREADY been applied to `a` / `b` (an incoming `'delta'`, or a fix
|
|
131
|
+
* returned by a previous `applyDelta`). Each is mapped onto the other side via the transformer —
|
|
132
|
+
* `apply` rebases the two against each other — and the mapped results are applied, which may yield
|
|
133
|
+
* further RDT fixes. We repeat until neither side reports a fix. Every change it applies to either side
|
|
134
|
+
* uses the `binding` itself as its {@link RDT origin} — from each side's perspective the binding is what
|
|
135
|
+
* produced the mapped change.
|
|
136
|
+
*
|
|
137
|
+
* `ta`/`tb` are shared read deltas (an event payload, or a fix that was also written into the producing
|
|
138
|
+
* RDT's state), so they are {@link delta.cloneDeep deep-cloned} into private builders before the
|
|
139
|
+
* transformer — which rebases its inputs in place — touches them (see {@link Binding}).
|
|
140
|
+
*
|
|
141
|
+
* @template {delta.DeltaConf} A
|
|
142
|
+
* @template {delta.DeltaConf} B
|
|
143
|
+
* @param {Binding<A,B>} binding
|
|
144
|
+
* @param {any} ta change already applied on `a` — a shared read delta (event payload or a fix), or `null`
|
|
145
|
+
* @param {any} tb change already applied on `b` — a shared read delta (event payload or a fix), or `null`
|
|
146
|
+
*/
|
|
147
|
+
const propagate = (binding, ta, tb) => {
|
|
148
|
+
while ((ta != null && !ta.isEmpty()) || (tb != null && !tb.isEmpty())) {
|
|
149
|
+
const tres = binding.t.apply(dt.createTransformResult(
|
|
150
|
+
ta != null ? delta.cloneDeep(ta) : null,
|
|
151
|
+
tb != null ? delta.cloneDeep(tb) : null
|
|
152
|
+
))
|
|
153
|
+
ta = tres.a != null ? binding.a.applyDelta(tres.a, binding) : null
|
|
154
|
+
tb = tres.b != null ? binding.b.applyDelta(tres.b, binding) : null
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @typedef {object} BindOptions
|
|
160
|
+
* @property {delta.DiffOptions['compare']} [diffCompare] **Experimental** (may be changed or removed
|
|
161
|
+
* in a future release). Forwarded as `compare` to {@link delta.diff} when the initial-state sync diffs
|
|
162
|
+
* `a`'s projection against `b`'s current state, controlling how nodes are paired into `modify` ops.
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Connects two RDTs so that changes on either side are transformed and reflected on the other.
|
|
167
|
+
*
|
|
168
|
+
* The {@link dt.Transformer transformer} it drives **manipulates deltas in place** (it splices op lists
|
|
169
|
+
* and rebases nodes directly, rather than only via `apply`/`rebase`) — a deliberate exception to the
|
|
170
|
+
* general rule that deltas are edited through `apply`/`rebase`. Consequently every change handed to it
|
|
171
|
+
* must be a privately-owned, fully-mutable delta, so the binding {@link delta.cloneDeep deep-clones}
|
|
172
|
+
* each incoming change (which is a shared read delta — an event payload or a snapshot) before the
|
|
173
|
+
* transformer touches it. See the initial-state sync below and {@link propagate}.
|
|
174
|
+
*
|
|
175
|
+
* @template {delta.DeltaConf} A
|
|
176
|
+
* @template {delta.DeltaConf} B
|
|
177
|
+
*/
|
|
178
|
+
export class Binding {
|
|
179
|
+
/**
|
|
180
|
+
* @param {RDT<A>} a
|
|
181
|
+
* @param {RDT<B>} b
|
|
182
|
+
* @param {dt.TemplateFactory<A,B>} [template] a `$d => Template` factory; defaults to the
|
|
183
|
+
* {@link dt.id identity} transformer. `bind` injects `a`'s schema, then materializes via `.init()`.
|
|
184
|
+
* @param {BindOptions} [options]
|
|
185
|
+
*/
|
|
186
|
+
constructor (a, b, template = /** @type {dt.TemplateFactory<A,B>} */ (dt.id), options = {}) {
|
|
187
|
+
/**
|
|
188
|
+
* @type {dt.Transformer<A,B>}
|
|
189
|
+
*/
|
|
190
|
+
this.t = template(a.$delta).init()
|
|
191
|
+
/**
|
|
192
|
+
* @type {RDT<A>}
|
|
193
|
+
*/
|
|
194
|
+
this.a = a
|
|
195
|
+
/**
|
|
196
|
+
* @type {RDT<B>}
|
|
197
|
+
*/
|
|
198
|
+
this.b = b
|
|
199
|
+
this._mux = mux.createMutex()
|
|
200
|
+
this._achanged = this.a.on('delta', d => this._mux(() => propagate(this, d, null)))
|
|
201
|
+
this._bchanged = this.b.on('delta', d => this._mux(() => propagate(this, null, d)))
|
|
202
|
+
this.a.on('destroy', this.destroy)
|
|
203
|
+
this.b.on('destroy', this.destroy)
|
|
204
|
+
// Sync the initial state. `a` is the source of truth: project its current state through the
|
|
205
|
+
// transformer (`applyA`) — which also yields any self-heal correction for `a` (`tres.a`) — then
|
|
206
|
+
// diff the projection against `b`'s current state and apply the difference so `b` ends up
|
|
207
|
+
// matching `a`'s projection. Any fixes the two sides report are reconciled via `propagate`.
|
|
208
|
+
// NOTE: `delta.diff` is content-only (it excludes marks), so cursor marks present on `a` at bind
|
|
209
|
+
// time are NOT transferred to `b` here; marks ride only on subsequent live `applyA`/`applyB`.
|
|
210
|
+
// Wrapped in the mutex so these `applyDelta` calls don't echo back through the listeners above.
|
|
211
|
+
this._mux(() => {
|
|
212
|
+
// `a.delta` is a shared read snapshot (for an in-memory RDT it IS the live state, whose nested
|
|
213
|
+
// children are frozen `done`), so DEEP-clone it into a private, fully-mutable builder before the
|
|
214
|
+
// transformer — which rebases its input in place — projects it. A shallow `clone` would still share
|
|
215
|
+
// those frozen children and corrupt / fail on the live state (see `delta.cloneDeep`).
|
|
216
|
+
const tres = this.t.applyA(delta.cloneDeep(this.a.delta))
|
|
217
|
+
const fa = tres.a ? this.a.applyDelta(tres.a, this) : null
|
|
218
|
+
// `diff` shares nested children with `tres.b` by default, and `tres.b` is a (possibly stateful)
|
|
219
|
+
// transformer's output that may still alias its internal state — so `clone` keeps the diff
|
|
220
|
+
// independent, since it is then applied into `b` (which freezes it) and propagated onward.
|
|
221
|
+
const diffB = tres.b ? delta.diff(this.b.delta, tres.b, { clone: true, compare: options.diffCompare }) : null
|
|
222
|
+
const fb = diffB ? this.b.applyDelta(diffB, this) : null
|
|
223
|
+
propagate(this, fa, fb)
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Tear the binding down: unsubscribe both sides' `'delta'` and `'destroy'` listeners so changes are no
|
|
229
|
+
* longer propagated. Call this when you no longer need the two RDTs kept in sync. It is also invoked
|
|
230
|
+
* automatically when either bound RDT emits `'destroy'`. The RDTs themselves are NOT destroyed.
|
|
231
|
+
*/
|
|
232
|
+
destroy = () => {
|
|
233
|
+
this.a.off('destroy', this.destroy)
|
|
234
|
+
this.b.off('destroy', this.destroy)
|
|
235
|
+
this.a.off('delta', this._achanged)
|
|
236
|
+
this.b.off('delta', this._bchanged)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Connect two RDTs through a transformer template. Changes on `a` are mapped onto `b` and vice versa.
|
|
242
|
+
* Without a `template` the {@link dt.id identity} transformer is used, keeping both sides equal. Call
|
|
243
|
+
* {@link Binding#destroy} on the returned binding to stop syncing (it also self-destroys when either RDT
|
|
244
|
+
* emits `'destroy'`).
|
|
245
|
+
*
|
|
246
|
+
* @template {delta.DeltaConf} A
|
|
247
|
+
* @template {delta.DeltaConf} B
|
|
248
|
+
* @param {RDT<A>} a
|
|
249
|
+
* @param {RDT<B>} b
|
|
250
|
+
* @param {dt.TemplateFactory<A,B>} [template] a `$d => Template` factory (e.g. `dt.id` or
|
|
251
|
+
* `$d => dt.renameAttrs($d, {a:'b'})`)
|
|
252
|
+
* @param {BindOptions} [options] `diffCompare` is **experimental** — see {@link BindOptions}.
|
|
253
|
+
* @return {Binding<A,B>}
|
|
254
|
+
*/
|
|
255
|
+
export const bind = (a, b, template, options = {}) => new Binding(a, b, template, options)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as delta from '../delta.js'
|
|
2
|
+
import * as s from '../../schema.js'
|
|
3
|
+
import { Transformer, Template, createTransformResult, attrsShapeOf } from './core.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @template {string} AttrName
|
|
7
|
+
* @template {delta.DeltaConf} IN
|
|
8
|
+
* @typedef {import('./core.js').ResolveOut<{ name: 'lib0:value', attrs: { value: IN extends { attrs: { [K in AttrName]: infer V } } ? V : never }}>} ApplyAttr
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {delta.DeltaBuilderAny} outDelta
|
|
13
|
+
* @param {string|number} from
|
|
14
|
+
* @param {string|number} to
|
|
15
|
+
* @param {delta.DeltaAny} inDelta
|
|
16
|
+
*/
|
|
17
|
+
const attrTransformHelper = (outDelta, from, to, inDelta) => {
|
|
18
|
+
const attrOp = inDelta.attrs[from]
|
|
19
|
+
if (attrOp != null) {
|
|
20
|
+
const c = attrOp.clone()
|
|
21
|
+
// reason: retarget the cloned attr op to the dynamic key `to`; `key` is readonly and `attrs` is
|
|
22
|
+
// a mapped type over fixed conf keys, so neither write is expressible in the JSDoc types.
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
c.key = to
|
|
25
|
+
const oattrs = /** @type {any} */ (outDelta.attrs)
|
|
26
|
+
oattrs[to] = c
|
|
27
|
+
}
|
|
28
|
+
// Move a mark on the projected attribute (`from` -> `to`); drop root marks on any other attribute or
|
|
29
|
+
// content (only this one attribute exists on the other side). A mark *inside* the attribute value
|
|
30
|
+
// rides on the cloned attr op above, and a mark *delete* (id-keyed) rides verbatim via mergeRootMarks.
|
|
31
|
+
delta.mergeRootMarks(outDelta, inDelta, k => k === from ? to : null)
|
|
32
|
+
// flag conservatively: the direct attr assignment above bypasses the builder, so a mark inside the
|
|
33
|
+
// cloned attribute value would otherwise not set the flag (marksToPositions self-corrects a false +ve)
|
|
34
|
+
outDelta.maybeHasMarks ||= inDelta.maybeHasMarks
|
|
35
|
+
return outDelta
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Projects a single node attribute into a `lib0:value` node's `value` attribute (and back).
|
|
40
|
+
*
|
|
41
|
+
* @template {string} AttrName
|
|
42
|
+
* @template {delta.DeltaConf} [IN=any]
|
|
43
|
+
* @extends {Template<IN, ApplyAttr<AttrName, IN>>}
|
|
44
|
+
*/
|
|
45
|
+
export class Attr extends Template {
|
|
46
|
+
/**
|
|
47
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
48
|
+
* @param {AttrName} attrName
|
|
49
|
+
*/
|
|
50
|
+
constructor ($d, attrName) {
|
|
51
|
+
const m = attrsShapeOf($d)
|
|
52
|
+
const $val = (m && m[attrName]) || s.$any
|
|
53
|
+
super($d, /** @type {any} */ (delta.$delta('lib0:value', { attrs: { value: $val } })))
|
|
54
|
+
this.attrName = attrName
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get name () { return 'lib0:attr:' + this.attrName }
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @return {Transformer<IN, ApplyAttr<AttrName, IN>>}
|
|
61
|
+
*/
|
|
62
|
+
init () {
|
|
63
|
+
return /** @type {any} */ (new AttrTransformer(this.$in, this.$out, this.attrName))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @template {delta.DeltaConf} A
|
|
69
|
+
* @template {delta.DeltaConf} B
|
|
70
|
+
* @extends {Transformer<A,B>}
|
|
71
|
+
*/
|
|
72
|
+
export class AttrTransformer extends Transformer {
|
|
73
|
+
/**
|
|
74
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $in
|
|
75
|
+
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $out
|
|
76
|
+
* @param {string} attrName
|
|
77
|
+
*/
|
|
78
|
+
constructor ($in, $out, attrName) {
|
|
79
|
+
super($in, $out)
|
|
80
|
+
this.attrName = /** @type {keyof delta.DeltaConfGetAttrs<A> & (string|number)} */ (attrName)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {delta.DeltaBuilder<A>} d
|
|
85
|
+
* @return {import('./core.js').TransformResultAny}
|
|
86
|
+
*/
|
|
87
|
+
applyA (d) {
|
|
88
|
+
return createTransformResult(
|
|
89
|
+
null,
|
|
90
|
+
attrTransformHelper(
|
|
91
|
+
delta.create('lib0:value'),
|
|
92
|
+
this.attrName,
|
|
93
|
+
'value',
|
|
94
|
+
d
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {delta.DeltaBuilder<B>} d
|
|
101
|
+
* @return {import('./core.js').TransformResultAny}
|
|
102
|
+
*/
|
|
103
|
+
applyB (d) {
|
|
104
|
+
return createTransformResult(
|
|
105
|
+
attrTransformHelper(
|
|
106
|
+
delta.create(),
|
|
107
|
+
'value',
|
|
108
|
+
this.attrName,
|
|
109
|
+
d
|
|
110
|
+
),
|
|
111
|
+
null
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Project a single node attribute `attrName` into a `lib0:value` carrier (and back). Returns a
|
|
118
|
+
* reusable {@link Attr} template (a `project` hole, or `.init()` for a standalone transformer).
|
|
119
|
+
*
|
|
120
|
+
* @template {string} AttrName
|
|
121
|
+
* @template {delta.DeltaConf} IN
|
|
122
|
+
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
|
|
123
|
+
* @param {AttrName} attrName
|
|
124
|
+
* @return {Attr<AttrName, IN>}
|
|
125
|
+
*/
|
|
126
|
+
export const attr = ($d, attrName) => new Attr($d, attrName)
|