lib0 1.0.0-rc.19 → 1.0.0-rc.20
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/delta/delta.d.ts +10 -0
- package/dist/delta/rdt/delta.d.ts +21 -18
- package/dist/delta/rdt/dom.d.ts +31 -19
- package/dist/delta/rdt.d.ts +99 -34
- package/dist/delta/transformer/core.d.ts +15 -0
- package/package.json +2 -2
- package/src/delta/delta.js +111 -6
- package/src/delta/rdt/delta.js +22 -16
- package/src/delta/rdt/dom.js +31 -20
- package/src/delta/rdt.js +92 -40
- package/src/delta/transformer/core.js +15 -0
package/dist/delta/delta.d.ts
CHANGED
|
@@ -631,6 +631,7 @@ export class Delta<Conf extends DeltaConf = {}> extends DeltaData<DeltaConfGetNa
|
|
|
631
631
|
}
|
|
632
632
|
export function slice<Conf_1 extends DeltaConf>(d: Delta<Conf_1>, start?: number, end?: number, currNode?: ChildrenOpAny | null): DeltaBuilder<Conf_1>;
|
|
633
633
|
export function clone<D extends DeltaAny>(d: D): D extends Delta<infer Conf_1> ? DeltaBuilder<Conf_1> : never;
|
|
634
|
+
export function cloneDeep<Conf_1 extends DeltaConf>(d: Delta<Conf_1>): DeltaBuilder<Conf_1>;
|
|
634
635
|
export function cloneShallow<D extends DeltaAny>(d: D): D extends Delta<infer Conf_1> ? DeltaBuilder<Conf_1> : never;
|
|
635
636
|
export function _mergeChildWithPrev(d: DeltaBuilderAny, op: InsertOp<any> | RetainOp | DeleteOp<any> | TextOp | ModifyOp<any>): boolean;
|
|
636
637
|
export function _splitChildAt(d: DeltaBuilderAny, op: ChildrenOpAny, offset: number): ChildrenOpAny;
|
|
@@ -1244,6 +1245,15 @@ export type DiffOptions = {
|
|
|
1244
1245
|
* (`(d1, d2) => d1.name === d2.name`). Called as `compare(fromNode, toNode)`.
|
|
1245
1246
|
*/
|
|
1246
1247
|
compare?: ((d1: DeltaAny, d2: DeltaAny) => boolean) | undefined;
|
|
1248
|
+
/**
|
|
1249
|
+
* By default `diff` does **not** produce a fully fresh delta: wherever
|
|
1250
|
+
* it emits content from `d2` — a wholesale-inserted child node, or a delta-valued attribute — it *shares*
|
|
1251
|
+
* that nested delta by reference, so the result aliases `d2`'s subtrees. Set `true` to
|
|
1252
|
+
* {@link cloneDeep deep-clone} every such delta (children **and** attributes) as it is included in the
|
|
1253
|
+
* output, yielding a result that shares no structure with `d2` (and is safe to hand to something that
|
|
1254
|
+
* manipulates deltas in place, e.g. a transformer).
|
|
1255
|
+
*/
|
|
1256
|
+
clone?: boolean | undefined;
|
|
1247
1257
|
};
|
|
1248
1258
|
import * as s from '../schema.js';
|
|
1249
1259
|
import * as list from '../list.js';
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
export function deltaRDT<
|
|
1
|
+
export function deltaRDT<Conf extends delta.DeltaConf>($delta: Schema<delta.Delta<Conf>>): DeltaRDT<Conf>;
|
|
2
2
|
export type Schema<T> = import("../../schema.js").Schema<T>;
|
|
3
|
-
export type RDT<
|
|
3
|
+
export type RDT<Conf extends delta.DeltaConf> = import("../rdt.js").RDT<Conf>;
|
|
4
4
|
import * as delta from '../delta.js';
|
|
5
5
|
/**
|
|
6
6
|
* @template T
|
|
7
7
|
* @typedef {import('../../schema.js').Schema<T>} Schema
|
|
8
8
|
*/
|
|
9
9
|
/**
|
|
10
|
-
* @template {delta.
|
|
11
|
-
* @typedef {import('../rdt.js').RDT<
|
|
10
|
+
* @template {delta.DeltaConf} Conf
|
|
11
|
+
* @typedef {import('../rdt.js').RDT<Conf>} RDT
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
14
|
* An in-memory RDT whose state is the accumulated delta. Calling `applyDelta` merges the incoming delta
|
|
15
15
|
* into `state` (via {@link delta.DeltaBuilder#apply}) and re-emits it as a `'delta'`.
|
|
16
16
|
*
|
|
17
|
-
* @template {delta.
|
|
18
|
-
* @implements {RDT<
|
|
19
|
-
* @extends {ObservableV2<{ delta: (delta:
|
|
17
|
+
* @template {delta.DeltaConf} Conf
|
|
18
|
+
* @implements {RDT<Conf>}
|
|
19
|
+
* @extends {ObservableV2<{ delta: (delta: delta.Delta<Conf>, origin: any) => void, destroy: (rdt: DeltaRDT<Conf>) => void }>}
|
|
20
20
|
*/
|
|
21
|
-
declare class DeltaRDT<
|
|
22
|
-
delta: (delta:
|
|
23
|
-
destroy: (rdt: DeltaRDT<
|
|
24
|
-
}> implements RDT<
|
|
21
|
+
declare class DeltaRDT<Conf extends delta.DeltaConf> extends ObservableV2<{
|
|
22
|
+
delta: (delta: delta.Delta<Conf>, origin: any) => void;
|
|
23
|
+
destroy: (rdt: DeltaRDT<Conf>) => void;
|
|
24
|
+
}> implements RDT<Conf> {
|
|
25
25
|
/**
|
|
26
|
-
* @param {Schema<
|
|
26
|
+
* @param {Schema<delta.Delta<Conf>>} $delta schema of the deltas this RDT produces
|
|
27
27
|
*/
|
|
28
|
-
constructor($delta: Schema<
|
|
29
|
-
$delta: import("../../schema.js").Schema<
|
|
28
|
+
constructor($delta: Schema<delta.Delta<Conf>>);
|
|
29
|
+
$delta: import("../../schema.js").Schema<delta.Delta<Conf>>;
|
|
30
30
|
/**
|
|
31
31
|
* @type {delta.DeltaBuilderAny?}
|
|
32
32
|
*/
|
|
@@ -36,17 +36,20 @@ declare class DeltaRDT<D extends delta.DeltaAny> extends ObservableV2<{
|
|
|
36
36
|
* Merge an incoming delta into the current state and notify observers. Always returns `null`: this
|
|
37
37
|
* RDT accepts every valid delta unchanged, so it never produces a fix.
|
|
38
38
|
*
|
|
39
|
-
* @param {
|
|
39
|
+
* @param {delta.Delta<Conf>} d
|
|
40
|
+
* @param {any} [origin] who produced the change; forwarded verbatim on the emitted `'delta'` event so
|
|
41
|
+
* listeners can recognise (and skip) changes they made themselves — see {@link RDT} “Origins”. Defaults
|
|
42
|
+
* to `null` (an anonymous/local change).
|
|
40
43
|
* @return {null}
|
|
41
44
|
*/
|
|
42
|
-
applyDelta(d:
|
|
45
|
+
applyDelta(d: delta.Delta<Conf>, origin?: any): null;
|
|
43
46
|
/**
|
|
44
47
|
* The current state as a delta: the accumulated `state`, or an empty delta when nothing has been
|
|
45
48
|
* applied yet.
|
|
46
49
|
*
|
|
47
|
-
* @return {
|
|
50
|
+
* @return {delta.Delta<Conf>}
|
|
48
51
|
*/
|
|
49
|
-
get delta():
|
|
52
|
+
get delta(): delta.Delta<Conf>;
|
|
50
53
|
}
|
|
51
54
|
import { ObservableV2 } from '../../observable.js';
|
|
52
55
|
import * as mux from '../../mutex.js';
|
package/dist/delta/rdt/dom.d.ts
CHANGED
|
@@ -10,42 +10,50 @@ export const $domDelta: s.Schema<delta.Delta<{
|
|
|
10
10
|
text: true;
|
|
11
11
|
recursiveChildren: true;
|
|
12
12
|
}>>;
|
|
13
|
-
export function domRDT(dom: Element): DomRDT
|
|
13
|
+
export function domRDT(dom: Element): DomRDT;
|
|
14
14
|
export type Schema<T> = import("../../schema.js").Schema<T>;
|
|
15
|
-
export type RDT<
|
|
16
|
-
|
|
15
|
+
export type RDT<Conf extends delta.DeltaConf> = import("../rdt.js").RDT<Conf>;
|
|
16
|
+
/**
|
|
17
|
+
* The {@link delta.DeltaConf} of the deltas a {@link DomRDT} produces/consumes (see {@link $domDelta}).
|
|
18
|
+
*/
|
|
19
|
+
export type DomConf = {
|
|
17
20
|
name: string;
|
|
18
21
|
attrs: {
|
|
19
22
|
[key: string]: string;
|
|
20
23
|
};
|
|
21
24
|
text: true;
|
|
22
25
|
recursiveChildren: true;
|
|
23
|
-
}
|
|
26
|
+
};
|
|
27
|
+
export type DomDelta = delta.Delta<DomConf>;
|
|
24
28
|
import * as delta from '../delta.js';
|
|
25
29
|
import * as s from '../../schema.js';
|
|
26
30
|
/**
|
|
27
|
-
* @
|
|
31
|
+
* The {@link delta.DeltaConf} of the deltas a {@link DomRDT} produces/consumes (see {@link $domDelta}).
|
|
32
|
+
*
|
|
33
|
+
* @typedef {{ name: string, attrs: { [key:string]: string }, text: true, recursiveChildren: true }} DomConf
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {delta.Delta<DomConf>} DomDelta
|
|
28
37
|
*/
|
|
29
38
|
/**
|
|
30
39
|
* An RDT backed by a live DOM subtree. DOM mutations observed via `MutationObserver` are diffed
|
|
31
40
|
* against the last-known state and emitted as deltas; incoming deltas are applied back onto the DOM.
|
|
32
41
|
*
|
|
33
|
-
* @
|
|
34
|
-
* @
|
|
35
|
-
* @extends {ObservableV2<{ delta: (delta: D) => void, destroy: (rdt: DomRDT<D>) => void }>}
|
|
42
|
+
* @implements {RDT<DomConf>}
|
|
43
|
+
* @extends {ObservableV2<{ delta: (delta: delta.Delta<DomConf>, origin: any) => void, destroy: (rdt: DomRDT) => void }>}
|
|
36
44
|
*/
|
|
37
|
-
declare class DomRDT
|
|
38
|
-
delta: (delta:
|
|
39
|
-
destroy: (rdt: DomRDT
|
|
40
|
-
}> implements RDT<
|
|
45
|
+
declare class DomRDT extends ObservableV2<{
|
|
46
|
+
delta: (delta: delta.Delta<DomConf>, origin: any) => void;
|
|
47
|
+
destroy: (rdt: DomRDT) => void;
|
|
48
|
+
}> implements RDT<DomConf> {
|
|
41
49
|
/**
|
|
42
50
|
* @param {Element} observedNode
|
|
43
51
|
*/
|
|
44
52
|
constructor(observedNode: Element);
|
|
45
53
|
/**
|
|
46
|
-
* @type {Schema<
|
|
54
|
+
* @type {Schema<DomDelta>}
|
|
47
55
|
*/
|
|
48
|
-
$delta: Schema<
|
|
56
|
+
$delta: Schema<DomDelta>;
|
|
49
57
|
observedNode: Element;
|
|
50
58
|
/**
|
|
51
59
|
* Last-known DOM state. The observe path diffs the live DOM against this to compute the change to
|
|
@@ -74,16 +82,20 @@ declare class DomRDT<D extends DomDelta = DomDelta> extends ObservableV2<{
|
|
|
74
82
|
* `d` rebased onto `b` is applied to the DOM, and `b` rebased onto `d` is returned as the fix so the
|
|
75
83
|
* binding pipes it back through the transformer to the other side.
|
|
76
84
|
*
|
|
77
|
-
* @param {
|
|
78
|
-
* @
|
|
85
|
+
* @param {delta.Delta<DomConf>} d
|
|
86
|
+
* @param {any} [origin] who produced `d`; forwarded verbatim on the emitted `'delta'` event so
|
|
87
|
+
* listeners can recognise (and skip) their own changes — see {@link RDT} “Origins”. Defaults to `null`
|
|
88
|
+
* (an anonymous/local change).
|
|
89
|
+
* @return {delta.DeltaBuilder<DomConf> | null} the rebased local change (`b`), or `null` when there
|
|
90
|
+
* were no concurrent edits
|
|
79
91
|
*/
|
|
80
|
-
applyDelta(d:
|
|
92
|
+
applyDelta(d: delta.Delta<DomConf>, origin?: any): delta.DeltaBuilder<DomConf> | null;
|
|
81
93
|
/**
|
|
82
94
|
* The current state as a delta: an "insert everything" delta describing the observed subtree.
|
|
83
95
|
*
|
|
84
|
-
* @return {
|
|
96
|
+
* @return {delta.Delta<DomConf>}
|
|
85
97
|
*/
|
|
86
|
-
get delta():
|
|
98
|
+
get delta(): delta.Delta<DomConf>;
|
|
87
99
|
}
|
|
88
100
|
import { ObservableV2 } from '../../observable.js';
|
|
89
101
|
export {};
|
package/dist/delta/rdt.d.ts
CHANGED
|
@@ -11,17 +11,45 @@ export { deltaRDT } from "./rdt/delta.js";
|
|
|
11
11
|
* transformer for it), exposes its current state as a delta via the `delta` getter, and accepts foreign
|
|
12
12
|
* deltas via `applyDelta`.
|
|
13
13
|
*
|
|
14
|
-
* `applyDelta(d)` applies `d` and then, if the change would violate the RDT's own invariants,
|
|
15
|
-
* a **fix** of its own (e.g. reversing part of `d`, or inserting missing content) and returns
|
|
16
|
-
* — `null` when none was needed. The fix is a regular change on this RDT, so a {@link Binding}
|
|
17
|
-
* onto the other side just like any other change. `applyDelta` also emits the *effective* change
|
|
14
|
+
* `applyDelta(d, origin)` applies `d` and then, if the change would violate the RDT's own invariants,
|
|
15
|
+
* applies a **fix** of its own (e.g. reversing part of `d`, or inserting missing content) and returns
|
|
16
|
+
* that fix — `null` when none was needed. The fix is a regular change on this RDT, so a {@link Binding}
|
|
17
|
+
* maps it onto the other side just like any other change. `applyDelta` also emits the *effective* change
|
|
18
18
|
* (`d` together with the fix) on the `'delta'` channel.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* ## Origins
|
|
21
|
+
*
|
|
22
|
+
* Every `'delta'` event carries a second argument, `origin` — an opaque `any` value (modelled after
|
|
23
|
+
* {@link https://docs.yjs.dev/api/transactions Yjs transaction origins}) that identifies **where the
|
|
24
|
+
* change came from**. Whoever produces the change sets it, and it is usually the producing instance
|
|
25
|
+
* itself: a communication provider, an editor binding, or the {@link Binding} that mapped the change
|
|
26
|
+
* over from the other side. A change an RDT observes locally (e.g. a `MutationObserver` firing on a DOM
|
|
27
|
+
* edit) uses that RDT itself as the origin.
|
|
28
|
+
*
|
|
29
|
+
* Origins let a listener tell **its own** changes apart from foreign ones so it can skip the ones it
|
|
30
|
+
* already knows about — a change it produced is not looped back to where it came from. The canonical use
|
|
31
|
+
* is a network provider: it tags every remote update it applies with itself (`rdt.applyDelta(update,
|
|
32
|
+
* this)`) and then ignores `'delta'` events whose `origin === this`, so it never re-broadcasts an update
|
|
33
|
+
* it just received. The `origin` argument to `applyDelta` is optional and defaults to `null` (an
|
|
34
|
+
* anonymous/local change); the emitted event always carries whatever value was supplied.
|
|
35
|
+
*
|
|
36
|
+
* ## `Delta` vs `DeltaBuilder`
|
|
37
|
+
*
|
|
38
|
+
* The interface is parameterized by a {@link import('./delta.js').DeltaConf DeltaConf} (like a
|
|
39
|
+
* {@link import('./transformer.js').Transformer transformer}). Almost everything it exposes is the
|
|
40
|
+
* *read* form — {@link import('./delta.js').Delta Delta} — because those values are **shared** and must
|
|
41
|
+
* not be mutated in place: `$delta` is the delta schema, the `delta` getter returns a state snapshot,
|
|
42
|
+
* `applyDelta` reads a change into the RDT, and the `'delta'` event payload is broadcast to every
|
|
43
|
+
* listener. The one *owned, mutable* form — {@link import('./delta.js').DeltaBuilder DeltaBuilder} — is
|
|
44
|
+
* the fix `applyDelta` returns to the {@link Binding}. The binding never mutates a shared read delta: it
|
|
45
|
+
* {@link import('./delta.js').cloneDeep deep-clones} every change into a private builder before the
|
|
46
|
+
* transformer rebases it (transformers manipulate deltas in place — see {@link Binding}).
|
|
47
|
+
*
|
|
48
|
+
* @template {import('./delta.js').DeltaConf} Conf
|
|
49
|
+
* @typedef {import('../observable.js').ObservableV2<{ delta: (delta: delta.Delta<Conf>, origin: any) => void, destroy: (rdt: RDT<Conf>) => void }> & {
|
|
50
|
+
* $delta: Schema<delta.Delta<Conf>>,
|
|
51
|
+
* delta: delta.Delta<Conf>,
|
|
52
|
+
* applyDelta: (delta: delta.Delta<Conf>, origin?: any) => delta.DeltaBuilder<Conf> | null,
|
|
25
53
|
* destroy: () => void
|
|
26
54
|
* }} RDT
|
|
27
55
|
*/
|
|
@@ -37,31 +65,39 @@ export const $rdt: Schema<RDT<any>>;
|
|
|
37
65
|
/**
|
|
38
66
|
* Connects two RDTs so that changes on either side are transformed and reflected on the other.
|
|
39
67
|
*
|
|
40
|
-
* @
|
|
68
|
+
* The {@link dt.Transformer transformer} it drives **manipulates deltas in place** (it splices op lists
|
|
69
|
+
* and rebases nodes directly, rather than only via `apply`/`rebase`) — a deliberate exception to the
|
|
70
|
+
* general rule that deltas are edited through `apply`/`rebase`. Consequently every change handed to it
|
|
71
|
+
* must be a privately-owned, fully-mutable delta, so the binding {@link delta.cloneDeep deep-clones}
|
|
72
|
+
* each incoming change (which is a shared read delta — an event payload or a snapshot) before the
|
|
73
|
+
* transformer touches it. See the initial-state sync below and {@link propagate}.
|
|
74
|
+
*
|
|
75
|
+
* @template {delta.DeltaConf} A
|
|
76
|
+
* @template {delta.DeltaConf} B
|
|
41
77
|
*/
|
|
42
|
-
export class Binding<
|
|
78
|
+
export class Binding<A extends delta.DeltaConf, B extends delta.DeltaConf> {
|
|
43
79
|
/**
|
|
44
|
-
* @param {RDT<
|
|
45
|
-
* @param {RDT<
|
|
46
|
-
* @param {dt.TemplateFactory<
|
|
80
|
+
* @param {RDT<A>} a
|
|
81
|
+
* @param {RDT<B>} b
|
|
82
|
+
* @param {dt.TemplateFactory<A,B>} [template] a `$d => Template` factory; defaults to the
|
|
47
83
|
* {@link dt.id identity} transformer. `bind` injects `a`'s schema, then materializes via `.init()`.
|
|
48
84
|
*/
|
|
49
|
-
constructor(a: RDT<
|
|
85
|
+
constructor(a: RDT<A>, b: RDT<B>, template?: dt.TemplateFactory<A, B>);
|
|
50
86
|
/**
|
|
51
|
-
* @type {dt.Transformer<
|
|
87
|
+
* @type {dt.Transformer<A,B>}
|
|
52
88
|
*/
|
|
53
|
-
t: dt.Transformer<
|
|
89
|
+
t: dt.Transformer<A, B>;
|
|
54
90
|
/**
|
|
55
|
-
* @type {RDT<
|
|
91
|
+
* @type {RDT<A>}
|
|
56
92
|
*/
|
|
57
|
-
a: RDT<
|
|
93
|
+
a: RDT<A>;
|
|
58
94
|
/**
|
|
59
|
-
* @type {RDT<
|
|
95
|
+
* @type {RDT<B>}
|
|
60
96
|
*/
|
|
61
|
-
b: RDT<
|
|
97
|
+
b: RDT<B>;
|
|
62
98
|
_mux: mux.mutex;
|
|
63
|
-
_achanged: (delta: any) => void;
|
|
64
|
-
_bchanged: (delta: any) => void;
|
|
99
|
+
_achanged: (delta: delta.Delta<A>, origin: any) => void;
|
|
100
|
+
_bchanged: (delta: delta.Delta<B>, origin: any) => void;
|
|
65
101
|
/**
|
|
66
102
|
* Tear the binding down: unsubscribe both sides' `'delta'` and `'destroy'` listeners so changes are no
|
|
67
103
|
* longer propagated. Call this when you no longer need the two RDTs kept in sync. It is also invoked
|
|
@@ -69,7 +105,7 @@ export class Binding<DeltaA extends import("./delta.js").DeltaAny> {
|
|
|
69
105
|
*/
|
|
70
106
|
destroy: () => void;
|
|
71
107
|
}
|
|
72
|
-
export function bind<
|
|
108
|
+
export function bind<A extends delta.DeltaConf, B extends delta.DeltaConf>(a: RDT<A>, b: RDT<B>, template?: dt.TemplateFactory<A, B>): Binding<A, B>;
|
|
73
109
|
export type Schema<T> = import("../schema.js").Schema<T>;
|
|
74
110
|
/**
|
|
75
111
|
* Abstract interface for a delta-based Replicated Data Type.
|
|
@@ -79,21 +115,50 @@ export type Schema<T> = import("../schema.js").Schema<T>;
|
|
|
79
115
|
* transformer for it), exposes its current state as a delta via the `delta` getter, and accepts foreign
|
|
80
116
|
* deltas via `applyDelta`.
|
|
81
117
|
*
|
|
82
|
-
* `applyDelta(d)` applies `d` and then, if the change would violate the RDT's own invariants,
|
|
83
|
-
* a **fix** of its own (e.g. reversing part of `d`, or inserting missing content) and returns
|
|
84
|
-
* — `null` when none was needed. The fix is a regular change on this RDT, so a {@link Binding}
|
|
85
|
-
* onto the other side just like any other change. `applyDelta` also emits the *effective* change
|
|
118
|
+
* `applyDelta(d, origin)` applies `d` and then, if the change would violate the RDT's own invariants,
|
|
119
|
+
* applies a **fix** of its own (e.g. reversing part of `d`, or inserting missing content) and returns
|
|
120
|
+
* that fix — `null` when none was needed. The fix is a regular change on this RDT, so a {@link Binding}
|
|
121
|
+
* maps it onto the other side just like any other change. `applyDelta` also emits the *effective* change
|
|
86
122
|
* (`d` together with the fix) on the `'delta'` channel.
|
|
123
|
+
*
|
|
124
|
+
* ## Origins
|
|
125
|
+
*
|
|
126
|
+
* Every `'delta'` event carries a second argument, `origin` — an opaque `any` value (modelled after
|
|
127
|
+
* {@link https://docs.yjs.dev/api/transactions Yjs transaction origins}) that identifies **where the
|
|
128
|
+
* change came from**. Whoever produces the change sets it, and it is usually the producing instance
|
|
129
|
+
* itself: a communication provider, an editor binding, or the {@link Binding} that mapped the change
|
|
130
|
+
* over from the other side. A change an RDT observes locally (e.g. a `MutationObserver` firing on a DOM
|
|
131
|
+
* edit) uses that RDT itself as the origin.
|
|
132
|
+
*
|
|
133
|
+
* Origins let a listener tell **its own** changes apart from foreign ones so it can skip the ones it
|
|
134
|
+
* already knows about — a change it produced is not looped back to where it came from. The canonical use
|
|
135
|
+
* is a network provider: it tags every remote update it applies with itself (`rdt.applyDelta(update,
|
|
136
|
+
* this)`) and then ignores `'delta'` events whose `origin === this`, so it never re-broadcasts an update
|
|
137
|
+
* it just received. The `origin` argument to `applyDelta` is optional and defaults to `null` (an
|
|
138
|
+
* anonymous/local change); the emitted event always carries whatever value was supplied.
|
|
139
|
+
*
|
|
140
|
+
* ## `Delta` vs `DeltaBuilder`
|
|
141
|
+
*
|
|
142
|
+
* The interface is parameterized by a {@link import ('./delta.js').DeltaConf DeltaConf} (like a
|
|
143
|
+
* {@link import ('./transformer.js').Transformer transformer}). Almost everything it exposes is the
|
|
144
|
+
* *read* form — {@link import ('./delta.js').Delta Delta} — because those values are **shared** and must
|
|
145
|
+
* not be mutated in place: `$delta` is the delta schema, the `delta` getter returns a state snapshot,
|
|
146
|
+
* `applyDelta` reads a change into the RDT, and the `'delta'` event payload is broadcast to every
|
|
147
|
+
* listener. The one *owned, mutable* form — {@link import ('./delta.js').DeltaBuilder DeltaBuilder} — is
|
|
148
|
+
* the fix `applyDelta` returns to the {@link Binding}. The binding never mutates a shared read delta: it
|
|
149
|
+
* {@link import ('./delta.js').cloneDeep deep-clones} every change into a private builder before the
|
|
150
|
+
* transformer rebases it (transformers manipulate deltas in place — see {@link Binding}).
|
|
87
151
|
*/
|
|
88
|
-
export type RDT<
|
|
89
|
-
delta: (delta:
|
|
90
|
-
destroy: (rdt: RDT<
|
|
152
|
+
export type RDT<Conf extends import("./delta.js").DeltaConf> = import("../observable.js").ObservableV2<{
|
|
153
|
+
delta: (delta: delta.Delta<Conf>, origin: any) => void;
|
|
154
|
+
destroy: (rdt: RDT<Conf>) => void;
|
|
91
155
|
}> & {
|
|
92
|
-
$delta: Schema<
|
|
93
|
-
delta:
|
|
94
|
-
applyDelta: (delta:
|
|
156
|
+
$delta: Schema<delta.Delta<Conf>>;
|
|
157
|
+
delta: delta.Delta<Conf>;
|
|
158
|
+
applyDelta: (delta: delta.Delta<Conf>, origin?: any) => delta.DeltaBuilder<Conf> | null;
|
|
95
159
|
destroy: () => void;
|
|
96
160
|
};
|
|
161
|
+
import * as delta from './delta.js';
|
|
97
162
|
import * as dt from './transformer.js';
|
|
98
163
|
import * as mux from '../mutex.js';
|
|
99
164
|
export { $domDelta, domRDT } from "./rdt/dom.js";
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
export function $tresult<A extends import("../delta.js").DeltaConf, B extends import("../delta.js").DeltaConf>($a: s.Schema<import("../delta.js").Delta<A>>, $b: s.Schema<import("../delta.js").Delta<B>>): s.Schema<TransformResult<A, B>>;
|
|
2
2
|
export function createTransformResult<DeltaA extends import("../delta.js").DeltaBuilderAny = delta.DeltaBuilderAny, DeltaB extends import("../delta.js").DeltaBuilderAny = delta.DeltaBuilderAny>(a: DeltaA | null, b: DeltaB | null): TransformResult<any, any>;
|
|
3
3
|
/**
|
|
4
|
+
* A stateful, schema-bound mapper between changes of side A and side B, materialized from a
|
|
5
|
+
* {@link Template} via `init()`.
|
|
6
|
+
*
|
|
7
|
+
* ## Deltas are manipulated in place
|
|
8
|
+
*
|
|
9
|
+
* `apply`/`applyA`/`applyB` **mutate their input deltas directly** — they rebase nodes and splice op
|
|
10
|
+
* lists in place, and some transformers `apply(other, { move: true })` the input into an accumulator —
|
|
11
|
+
* rather than editing only through the copy-on-write `apply`/`rebase` surface. This is a deliberate
|
|
12
|
+
* exception to the general rule (a delta is normally edited only via `apply`/`rebase`, which re-clone
|
|
13
|
+
* frozen content on first touch); it keeps the transform allocation-free, but it means a transformer is
|
|
14
|
+
* an *owning* consumer of the delta it is given. **Callers must pass a privately-owned, fully-mutable
|
|
15
|
+
* delta** — never a shared or `done` (frozen) one. A caller holding a shared read delta must
|
|
16
|
+
* {@link import('../delta.js').cloneDeep deep-clone} it first (this is exactly what a
|
|
17
|
+
* {@link import('../rdt.js').Binding} does before routing a change through its transformer).
|
|
18
|
+
*
|
|
4
19
|
* @template {import('../delta.js').DeltaConf} A
|
|
5
20
|
* @template {import('../delta.js').DeltaConf} B
|
|
6
21
|
*/
|
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.20",
|
|
4
4
|
"description": "",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -331,7 +331,7 @@
|
|
|
331
331
|
}
|
|
332
332
|
},
|
|
333
333
|
"devDependencies": {
|
|
334
|
-
"@types/node": "^
|
|
334
|
+
"@types/node": "^26.0.0",
|
|
335
335
|
"c8": "^11.0.0",
|
|
336
336
|
"dpdm": "^4.0.1",
|
|
337
337
|
"jsdom": "^29.1.1",
|
package/src/delta/delta.js
CHANGED
|
@@ -1596,6 +1596,101 @@ export const slice = (d, start = 0, end = d.childCnt, currNode = d.children.star
|
|
|
1596
1596
|
*/
|
|
1597
1597
|
export const clone = d => /** @type {any} */ (slice(d, 0, d.childCnt))
|
|
1598
1598
|
|
|
1599
|
+
/**
|
|
1600
|
+
* Deep-clone a value that may be a nested delta: a fresh {@link cloneDeep} of it when it is a delta,
|
|
1601
|
+
* otherwise the value unchanged (plain JSON content is treated as immutable and shared).
|
|
1602
|
+
*
|
|
1603
|
+
* @param {any} v
|
|
1604
|
+
* @return {any}
|
|
1605
|
+
*/
|
|
1606
|
+
const _cloneMaybeDeltaDeep = v => $deltaAny.check(v) ? cloneDeep(v) : v
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Deep-clone one content (child) op for {@link cloneDeep}: a fresh op whose nested deltas (an insert's
|
|
1610
|
+
* delta content, a modify's value) are themselves deep-cloned. `format`/`attribution` objects are
|
|
1611
|
+
* retained (shared) — they are always copied before being mutated (see {@link cloneDeep}). Ops without a
|
|
1612
|
+
* nested delta a clone keeps (`text`/`retain`/`delete` — `delete` drops its prevValue) use `op.clone()`.
|
|
1613
|
+
*
|
|
1614
|
+
* @param {ChildrenOpAny} op
|
|
1615
|
+
* @return {ChildrenOpAny}
|
|
1616
|
+
*/
|
|
1617
|
+
const _cloneChildOpDeep = op =>
|
|
1618
|
+
$insertOp.check(op)
|
|
1619
|
+
? new InsertOp(op.insert.map(_cloneMaybeDeltaDeep), op.format, op.attribution)
|
|
1620
|
+
: ($modifyOp.check(op)
|
|
1621
|
+
? new ModifyOp(cloneDeep(op.value), op.format, op.attribution)
|
|
1622
|
+
: op.clone())
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Deep-clone one attribute op for {@link cloneDeep} — the attribute-dimension mirror of
|
|
1626
|
+
* {@link _cloneChildOpDeep}: a `setAttr`'s value/prevValue and a `modifyAttr`'s value may be nested
|
|
1627
|
+
* deltas and are deep-cloned.
|
|
1628
|
+
*
|
|
1629
|
+
* @param {AttrOpAny} op
|
|
1630
|
+
* @return {AttrOpAny}
|
|
1631
|
+
*/
|
|
1632
|
+
const _cloneAttrOpDeep = op =>
|
|
1633
|
+
$setAttrOp.check(op)
|
|
1634
|
+
? new SetAttrOp(op.key, _cloneMaybeDeltaDeep(op.value), _cloneMaybeDeltaDeep(op.prevValue), op.attribution)
|
|
1635
|
+
: ($modifyAttrOp.check(op)
|
|
1636
|
+
? new ModifyAttrOp(op.key, cloneDeep(op.value), op.attribution)
|
|
1637
|
+
// the only remaining attr op is a deleteAttr (its prevValue may be a nested delta)
|
|
1638
|
+
: new DeleteAttrOp(op.key, _cloneMaybeDeltaDeep(op.prevValue), op.attribution))
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* A **deep** clone of `d`: like {@link clone}, but every nested delta — an insert's delta content, a
|
|
1642
|
+
* `modify`/`modifyAttr` value, a delta-valued attribute — is itself recursively cloned into a fresh,
|
|
1643
|
+
* **mutable** node, instead of being frozen (`done`) and shared as {@link clone} does. The result and
|
|
1644
|
+
* its whole subtree are therefore independently editable.
|
|
1645
|
+
*
|
|
1646
|
+
* ## What is cloned, and when to reach for this
|
|
1647
|
+
*
|
|
1648
|
+
* A delta owns two kinds of nested state, treated differently on purpose:
|
|
1649
|
+
* - **Ops and (nested) deltas are cloned.** They carry structure edited *in place* — `apply`/`rebase`
|
|
1650
|
+
* splice op lists and mutate op fields — so a shared copy would be corrupted by a later edit of either
|
|
1651
|
+
* side.
|
|
1652
|
+
* - **`format`/`attribution` objects and plain JSON insert values are retained (shared).** They are
|
|
1653
|
+
* always *copied before being changed* (a format/attribution update allocates a new object — see
|
|
1654
|
+
* {@link mergeAttr}), so sharing them is safe and avoids needless allocation.
|
|
1655
|
+
*
|
|
1656
|
+
* {@link clone} suffices for the common case because a delta is only ever edited through `apply`/`rebase`,
|
|
1657
|
+
* which re-clone a `done` nested delta the first time they touch it (see {@link InsertOp#_modValue}) — the
|
|
1658
|
+
* shared, frozen children are never mutated in place. Reach for `cloneDeep` when a nested delta will be
|
|
1659
|
+
* **manipulated directly** instead of via `apply`/`rebase` — most notably by a
|
|
1660
|
+
* {@link import('./transformer/core.js').Transformer transformer}, which walks and mutates a delta's
|
|
1661
|
+
* ops/children in place. A `done` (frozen) delta must always be cloned before such direct manipulation
|
|
1662
|
+
* (a plain `apply`/`rebase` handles the freeze itself, so it needs no pre-clone).
|
|
1663
|
+
*
|
|
1664
|
+
* @template {DeltaConf} Conf
|
|
1665
|
+
* @param {Delta<Conf>} d
|
|
1666
|
+
* @return {DeltaBuilder<Conf>}
|
|
1667
|
+
*/
|
|
1668
|
+
export const cloneDeep = d => {
|
|
1669
|
+
const cpy = /** @type {DeltaAny} */ (new DeltaBuilder(d.name, d.$schema))
|
|
1670
|
+
cpy.origin = d.origin
|
|
1671
|
+
cpy.isFinal = d.isFinal
|
|
1672
|
+
for (const op of d.attrs) {
|
|
1673
|
+
// @ts-ignore dynamic attr key (the same limitation `slice` suppresses when copying attrs)
|
|
1674
|
+
cpy.attrs[op.key] = _cloneAttrOpDeep(op)
|
|
1675
|
+
}
|
|
1676
|
+
for (const op of d.children) {
|
|
1677
|
+
list.pushEnd(cpy.children, _cloneChildOpDeep(op))
|
|
1678
|
+
}
|
|
1679
|
+
cpy.childCnt = d.childCnt
|
|
1680
|
+
// marks ride along verbatim — a full clone never clamps number-keyed root marks, and Mark objects are
|
|
1681
|
+
// immutable so they are shared (mirrors slice's full-copy path); a change delta's deleteMarks is copied.
|
|
1682
|
+
if (d.maybeHasMarks) {
|
|
1683
|
+
if (d.marks !== null) {
|
|
1684
|
+
const cpyMarks = new Marks()
|
|
1685
|
+
for (const m of d.marks) cpyMarks.add(m)
|
|
1686
|
+
if (cpyMarks.size > 0) cpy.marks = cpyMarks
|
|
1687
|
+
}
|
|
1688
|
+
cpy.maybeHasMarks = true
|
|
1689
|
+
}
|
|
1690
|
+
if (d.deleteMarks !== null) cpy.deleteMarks = new Set(d.deleteMarks)
|
|
1691
|
+
return /** @type {any} */ (cpy)
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1599
1694
|
/**
|
|
1600
1695
|
* A *shallow* clone of `d`: a fresh {@link DeltaBuilder} carrying `d`'s name, attribute ops, and own
|
|
1601
1696
|
* (root) marks — but **no children**. Content-transforming
|
|
@@ -3695,6 +3790,12 @@ class _DiffStringWrapper {
|
|
|
3695
3790
|
* @property {(d1: DeltaAny, d2: DeltaAny) => boolean} [compare] Predicate deciding when two nodes
|
|
3696
3791
|
* are paired into a `modify` (vs. replaced wholesale). Defaults to comparing names
|
|
3697
3792
|
* (`(d1, d2) => d1.name === d2.name`). Called as `compare(fromNode, toNode)`.
|
|
3793
|
+
* @property {boolean} [clone=false] By default `diff` does **not** produce a fully fresh delta: wherever
|
|
3794
|
+
* it emits content from `d2` — a wholesale-inserted child node, or a delta-valued attribute — it *shares*
|
|
3795
|
+
* that nested delta by reference, so the result aliases `d2`'s subtrees. Set `true` to
|
|
3796
|
+
* {@link cloneDeep deep-clone} every such delta (children **and** attributes) as it is included in the
|
|
3797
|
+
* output, yielding a result that shares no structure with `d2` (and is safe to hand to something that
|
|
3798
|
+
* manipulates deltas in place, e.g. a transformer).
|
|
3698
3799
|
*/
|
|
3699
3800
|
|
|
3700
3801
|
/**
|
|
@@ -3880,8 +3981,9 @@ export const diff = (d1, d2, options = {}) => {
|
|
|
3880
3981
|
// while modifyAttr's param is typed Attribution for API ergonomics
|
|
3881
3982
|
d.modifyAttr(key, diff(prevVal, nextVal, options), /** @type {Attribution|null|undefined} */ (diffDim(attr1?.attribution, attr2.attribution, true)))
|
|
3882
3983
|
} else {
|
|
3883
|
-
// setAttr replaces the whole attr, so it carries the new attribution as data (object-or-none)
|
|
3884
|
-
|
|
3984
|
+
// setAttr replaces the whole attr, so it carries the new attribution as data (object-or-none).
|
|
3985
|
+
// `nextVal` is shared from `d2`; deep-clone it when the caller wants an independent result.
|
|
3986
|
+
d.setAttr(key, options.clone ? _cloneMaybeDeltaDeep(nextVal) : nextVal, attr2.attribution)
|
|
3885
3987
|
}
|
|
3886
3988
|
/* c8 ignore start */
|
|
3887
3989
|
} else {
|
|
@@ -3923,13 +4025,16 @@ const contentLen = c => typeof c === 'string' ? c.length : 1
|
|
|
3923
4025
|
*/
|
|
3924
4026
|
const applyRemoves = (d, crem, len) => { len > 0 && d.delete(crem.splice(0, len).map(contentLen).reduce(math.add, 0)) }
|
|
3925
4027
|
/**
|
|
3926
|
-
* Apply inserts from c.insert to d, mutating c.insert to exclude the applied items
|
|
4028
|
+
* Apply inserts from c.insert to d, mutating c.insert to exclude the applied items. A wholesale-inserted
|
|
4029
|
+
* child is a nested delta shared from `d2`; deep-clone it when `cloneChildren` (the diff's `clone` option)
|
|
4030
|
+
* so the result aliases nothing in `d2` (see {@link DiffOptions}).
|
|
3927
4031
|
*
|
|
3928
4032
|
* @param {DeltaBuilderAny} d
|
|
3929
4033
|
* @param {Array<any>} cins
|
|
3930
4034
|
* @param {number} len
|
|
4035
|
+
* @param {boolean} [cloneChildren]
|
|
3931
4036
|
*/
|
|
3932
|
-
const applyInserts = (d, cins, len) => { len > 0 && cins.splice(0, len).forEach(ins => d.insert(typeof ins === 'string' ? ins : [ins instanceof _DiffStringWrapper ? ins.str : ins])) }
|
|
4037
|
+
const applyInserts = (d, cins, len, cloneChildren) => { len > 0 && cins.splice(0, len).forEach(ins => d.insert(typeof ins === 'string' ? ins : [ins instanceof _DiffStringWrapper ? ins.str : (cloneChildren ? _cloneMaybeDeltaDeep(ins) : ins)])) }
|
|
3933
4038
|
|
|
3934
4039
|
/**
|
|
3935
4040
|
* @param {DeltaBuilderAny} d
|
|
@@ -3954,13 +4059,13 @@ const applyChangesetToDelta = (d, changeset, compare, options) => {
|
|
|
3954
4059
|
continue
|
|
3955
4060
|
}
|
|
3956
4061
|
applyRemoves(d, c.remove, cremoveDeltaIndex)
|
|
3957
|
-
applyInserts(d, c.insert, cinsertDeltaIndex)
|
|
4062
|
+
applyInserts(d, c.insert, cinsertDeltaIndex, options.clone)
|
|
3958
4063
|
d.modify(diff(c.remove[0], c.insert[0], options))
|
|
3959
4064
|
c.remove.splice(0, 1)
|
|
3960
4065
|
c.insert.splice(0, 1)
|
|
3961
4066
|
}
|
|
3962
4067
|
applyRemoves(d, c.remove, c.remove.length)
|
|
3963
|
-
applyInserts(d, c.insert, c.insert.length)
|
|
4068
|
+
applyInserts(d, c.insert, c.insert.length, options.clone)
|
|
3964
4069
|
}
|
|
3965
4070
|
return d
|
|
3966
4071
|
}
|
package/src/delta/rdt/delta.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* # In-memory delta RDT
|
|
3
3
|
*
|
|
4
4
|
* {@link deltaRDT} creates an RDT (see `../rdt.js`) whose state is the accumulated delta. Calling
|
|
5
|
-
* `applyDelta` merges the incoming delta into `state` and re-emits it
|
|
6
|
-
*
|
|
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.
|
|
7
8
|
*
|
|
8
9
|
* `state` is kept as a **final** delta (`isFinal`): it represents the current document, so a `delete`
|
|
9
10
|
* or `deleteAttr` removes the content/attribute outright instead of accumulating a delete-op marker.
|
|
@@ -22,21 +23,21 @@ import * as mux from '../../mutex.js'
|
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* @template {delta.
|
|
26
|
-
* @typedef {import('../rdt.js').RDT<
|
|
26
|
+
* @template {delta.DeltaConf} Conf
|
|
27
|
+
* @typedef {import('../rdt.js').RDT<Conf>} RDT
|
|
27
28
|
*/
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* An in-memory RDT whose state is the accumulated delta. Calling `applyDelta` merges the incoming delta
|
|
31
32
|
* into `state` (via {@link delta.DeltaBuilder#apply}) and re-emits it as a `'delta'`.
|
|
32
33
|
*
|
|
33
|
-
* @template {delta.
|
|
34
|
-
* @implements {RDT<
|
|
35
|
-
* @extends {ObservableV2<{ delta: (delta:
|
|
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 }>}
|
|
36
37
|
*/
|
|
37
38
|
class DeltaRDT extends ObservableV2 {
|
|
38
39
|
/**
|
|
39
|
-
* @param {Schema<
|
|
40
|
+
* @param {Schema<delta.Delta<Conf>>} $delta schema of the deltas this RDT produces
|
|
40
41
|
*/
|
|
41
42
|
constructor ($delta) {
|
|
42
43
|
super()
|
|
@@ -52,10 +53,13 @@ class DeltaRDT extends ObservableV2 {
|
|
|
52
53
|
* Merge an incoming delta into the current state and notify observers. Always returns `null`: this
|
|
53
54
|
* RDT accepts every valid delta unchanged, so it never produces a fix.
|
|
54
55
|
*
|
|
55
|
-
* @param {
|
|
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).
|
|
56
60
|
* @return {null}
|
|
57
61
|
*/
|
|
58
|
-
applyDelta (d) {
|
|
62
|
+
applyDelta (d, origin = null) {
|
|
59
63
|
if (d.isEmpty()) return null
|
|
60
64
|
this._mux(() => {
|
|
61
65
|
if (this.state == null) {
|
|
@@ -69,8 +73,9 @@ class DeltaRDT extends ObservableV2 {
|
|
|
69
73
|
// emit AFTER releasing the mutex. When bound to a fix-producing RDT, that side's fix is mapped
|
|
70
74
|
// back here and applied via a synchronous re-entrant `applyDelta` while this call is still on the
|
|
71
75
|
// stack; with the emit inside the mutex that re-entrant mutation would be dropped. The binding's
|
|
72
|
-
// own mutex still breaks the echo loop.
|
|
73
|
-
|
|
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])
|
|
74
79
|
return null
|
|
75
80
|
}
|
|
76
81
|
|
|
@@ -78,10 +83,11 @@ class DeltaRDT extends ObservableV2 {
|
|
|
78
83
|
* The current state as a delta: the accumulated `state`, or an empty delta when nothing has been
|
|
79
84
|
* applied yet.
|
|
80
85
|
*
|
|
81
|
-
* @return {
|
|
86
|
+
* @return {delta.Delta<Conf>}
|
|
82
87
|
*/
|
|
83
88
|
get delta () {
|
|
84
|
-
|
|
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)))
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
destroy () {
|
|
@@ -93,7 +99,7 @@ class DeltaRDT extends ObservableV2 {
|
|
|
93
99
|
/**
|
|
94
100
|
* Create a {@link DeltaRDT} for deltas described by `$delta`.
|
|
95
101
|
*
|
|
96
|
-
* @template {delta.
|
|
97
|
-
* @param {Schema<
|
|
102
|
+
* @template {delta.DeltaConf} Conf
|
|
103
|
+
* @param {Schema<delta.Delta<Conf>>} $delta
|
|
98
104
|
*/
|
|
99
105
|
export const deltaRDT = $delta => new DeltaRDT($delta)
|
package/src/delta/rdt/dom.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* {@link domRDT} creates an RDT (see `../rdt.js`) backed by a live DOM subtree. DOM mutations are
|
|
7
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
|
|
9
|
-
*
|
|
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.
|
|
10
11
|
*
|
|
11
12
|
* @module delta/rdt/dom
|
|
12
13
|
*/
|
|
@@ -24,8 +25,8 @@ import * as s from '../../schema.js'
|
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
* @template {delta.
|
|
28
|
-
* @typedef {import('../rdt.js').RDT<
|
|
28
|
+
* @template {delta.DeltaConf} Conf
|
|
29
|
+
* @typedef {import('../rdt.js').RDT<Conf>} RDT
|
|
29
30
|
*/
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -177,16 +178,21 @@ const applyDeltaToDom = (el, d) => {
|
|
|
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 })
|
|
178
179
|
|
|
179
180
|
/**
|
|
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
|
|
181
188
|
*/
|
|
182
189
|
|
|
183
190
|
/**
|
|
184
191
|
* An RDT backed by a live DOM subtree. DOM mutations observed via `MutationObserver` are diffed
|
|
185
192
|
* against the last-known state and emitted as deltas; incoming deltas are applied back onto the DOM.
|
|
186
193
|
*
|
|
187
|
-
* @
|
|
188
|
-
* @
|
|
189
|
-
* @extends {ObservableV2<{ delta: (delta: D) => void, destroy: (rdt: DomRDT<D>) => void }>}
|
|
194
|
+
* @implements {RDT<DomConf>}
|
|
195
|
+
* @extends {ObservableV2<{ delta: (delta: delta.Delta<DomConf>, origin: any) => void, destroy: (rdt: DomRDT) => void }>}
|
|
190
196
|
*/
|
|
191
197
|
class DomRDT extends ObservableV2 {
|
|
192
198
|
/**
|
|
@@ -195,7 +201,7 @@ class DomRDT extends ObservableV2 {
|
|
|
195
201
|
constructor (observedNode) {
|
|
196
202
|
super()
|
|
197
203
|
/**
|
|
198
|
-
* @type {Schema<
|
|
204
|
+
* @type {Schema<DomDelta>}
|
|
199
205
|
*/
|
|
200
206
|
this.$delta = /** @type {any} */ ($domDelta)
|
|
201
207
|
this.observedNode = observedNode
|
|
@@ -233,8 +239,9 @@ class DomRDT extends ObservableV2 {
|
|
|
233
239
|
*/
|
|
234
240
|
_mutationHandler = mutations => {
|
|
235
241
|
if (mutations.length === 0) return
|
|
236
|
-
const change =
|
|
237
|
-
|
|
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])
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
/**
|
|
@@ -245,10 +252,14 @@ class DomRDT extends ObservableV2 {
|
|
|
245
252
|
* `d` rebased onto `b` is applied to the DOM, and `b` rebased onto `d` is returned as the fix so the
|
|
246
253
|
* binding pipes it back through the transformer to the other side.
|
|
247
254
|
*
|
|
248
|
-
* @param {
|
|
249
|
-
* @
|
|
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
|
|
250
261
|
*/
|
|
251
|
-
applyDelta (d) {
|
|
262
|
+
applyDelta (d, origin = null) {
|
|
252
263
|
const b = this._pull()
|
|
253
264
|
/** @type {DomDelta} */
|
|
254
265
|
let toApply = d
|
|
@@ -266,19 +277,19 @@ class DomRDT extends ObservableV2 {
|
|
|
266
277
|
// echo of our own write — draining the self-caused records here is what prevents it.
|
|
267
278
|
this.observer.takeRecords()
|
|
268
279
|
// Forward the effective change so a chained binding on the other side picks it up; the binding
|
|
269
|
-
// that fed us `d` swallows this re-emit via its own mutex (see ../rdt.js).
|
|
270
|
-
|
|
271
|
-
this.emit('delta', [
|
|
272
|
-
return /** @type {
|
|
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)
|
|
273
284
|
}
|
|
274
285
|
|
|
275
286
|
/**
|
|
276
287
|
* The current state as a delta: an "insert everything" delta describing the observed subtree.
|
|
277
288
|
*
|
|
278
|
-
* @return {
|
|
289
|
+
* @return {delta.Delta<DomConf>}
|
|
279
290
|
*/
|
|
280
291
|
get delta () {
|
|
281
|
-
return
|
|
292
|
+
return domToDelta(this.observedNode)
|
|
282
293
|
}
|
|
283
294
|
|
|
284
295
|
destroy () {
|
package/src/delta/rdt.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* An **RDT** ("replicated data type") is any object that represents some state and communicates changes
|
|
5
5
|
* to that state as {@link import('./delta.js').Delta deltas}. Every RDT is an
|
|
6
6
|
* {@link import('../observable.js').ObservableV2 observable}:
|
|
7
|
-
* - it **emits** a `'delta'` event carrying a delta whenever its own state
|
|
8
|
-
*
|
|
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.
|
|
9
10
|
*
|
|
10
11
|
* Because the changes are deltas, two different representations of "the same" data can be kept in sync
|
|
11
12
|
* even when their shapes differ. That is the job of a {@link Binding}: it connects two RDTs `a` and `b`
|
|
@@ -64,17 +65,45 @@ export { $domDelta, domRDT } from './rdt/dom.js'
|
|
|
64
65
|
* transformer for it), exposes its current state as a delta via the `delta` getter, and accepts foreign
|
|
65
66
|
* deltas via `applyDelta`.
|
|
66
67
|
*
|
|
67
|
-
* `applyDelta(d)` applies `d` and then, if the change would violate the RDT's own invariants,
|
|
68
|
-
* a **fix** of its own (e.g. reversing part of `d`, or inserting missing content) and returns
|
|
69
|
-
* — `null` when none was needed. The fix is a regular change on this RDT, so a {@link Binding}
|
|
70
|
-
* onto the other side just like any other change. `applyDelta` also emits the *effective* change
|
|
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
|
|
71
72
|
* (`d` together with the fix) on the `'delta'` channel.
|
|
72
73
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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,
|
|
78
107
|
* destroy: () => void
|
|
79
108
|
* }} RDT
|
|
80
109
|
*/
|
|
@@ -101,47 +130,62 @@ export const $rdt = /** @type {Schema<RDT<any>>} */ (/* @__PURE__ */ s.$custom(o
|
|
|
101
130
|
* `ta` / `tb` are changes that have ALREADY been applied to `a` / `b` (an incoming `'delta'`, or a fix
|
|
102
131
|
* returned by a previous `applyDelta`). Each is mapped onto the other side via the transformer —
|
|
103
132
|
* `apply` rebases the two against each other — and the mapped results are applied, which may yield
|
|
104
|
-
* further RDT fixes. We repeat until neither side reports a fix.
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
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`
|
|
109
146
|
*/
|
|
110
147
|
const propagate = (binding, ta, tb) => {
|
|
111
148
|
while ((ta != null && !ta.isEmpty()) || (tb != null && !tb.isEmpty())) {
|
|
112
|
-
const tres = binding.t.apply(dt.createTransformResult(
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
115
155
|
}
|
|
116
156
|
}
|
|
117
157
|
|
|
118
158
|
/**
|
|
119
159
|
* Connects two RDTs so that changes on either side are transformed and reflected on the other.
|
|
120
160
|
*
|
|
121
|
-
* @
|
|
161
|
+
* The {@link dt.Transformer transformer} it drives **manipulates deltas in place** (it splices op lists
|
|
162
|
+
* and rebases nodes directly, rather than only via `apply`/`rebase`) — a deliberate exception to the
|
|
163
|
+
* general rule that deltas are edited through `apply`/`rebase`. Consequently every change handed to it
|
|
164
|
+
* must be a privately-owned, fully-mutable delta, so the binding {@link delta.cloneDeep deep-clones}
|
|
165
|
+
* each incoming change (which is a shared read delta — an event payload or a snapshot) before the
|
|
166
|
+
* transformer touches it. See the initial-state sync below and {@link propagate}.
|
|
167
|
+
*
|
|
168
|
+
* @template {delta.DeltaConf} A
|
|
169
|
+
* @template {delta.DeltaConf} B
|
|
122
170
|
*/
|
|
123
171
|
export class Binding {
|
|
124
172
|
/**
|
|
125
|
-
* @param {RDT<
|
|
126
|
-
* @param {RDT<
|
|
127
|
-
* @param {dt.TemplateFactory<
|
|
173
|
+
* @param {RDT<A>} a
|
|
174
|
+
* @param {RDT<B>} b
|
|
175
|
+
* @param {dt.TemplateFactory<A,B>} [template] a `$d => Template` factory; defaults to the
|
|
128
176
|
* {@link dt.id identity} transformer. `bind` injects `a`'s schema, then materializes via `.init()`.
|
|
129
177
|
*/
|
|
130
|
-
constructor (a, b, template = dt.id) {
|
|
178
|
+
constructor (a, b, template = /** @type {dt.TemplateFactory<A,B>} */ (dt.id)) {
|
|
131
179
|
/**
|
|
132
|
-
* @type {dt.Transformer<
|
|
180
|
+
* @type {dt.Transformer<A,B>}
|
|
133
181
|
*/
|
|
134
182
|
this.t = template(a.$delta).init()
|
|
135
|
-
// reason: the `'delta'` channel carries `Delta` values while the transformer is typed for the
|
|
136
|
-
// mutable `DeltaBuilder` it consumes/produces, and the binding is generic over both sides; typing
|
|
137
|
-
// the internal handles as `RDT<any>` lets changes pass through the transformer without unsound
|
|
138
|
-
// cross-casts, while `bind`/the constructor keep precise public signatures.
|
|
139
183
|
/**
|
|
140
|
-
* @type {RDT<
|
|
184
|
+
* @type {RDT<A>}
|
|
141
185
|
*/
|
|
142
186
|
this.a = a
|
|
143
187
|
/**
|
|
144
|
-
* @type {RDT<
|
|
188
|
+
* @type {RDT<B>}
|
|
145
189
|
*/
|
|
146
190
|
this.b = b
|
|
147
191
|
this._mux = mux.createMutex()
|
|
@@ -157,10 +201,17 @@ export class Binding {
|
|
|
157
201
|
// time are NOT transferred to `b` here; marks ride only on subsequent live `applyA`/`applyB`.
|
|
158
202
|
// Wrapped in the mutex so these `applyDelta` calls don't echo back through the listeners above.
|
|
159
203
|
this._mux(() => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
204
|
+
// `a.delta` is a shared read snapshot (for an in-memory RDT it IS the live state, whose nested
|
|
205
|
+
// children are frozen `done`), so DEEP-clone it into a private, fully-mutable builder before the
|
|
206
|
+
// transformer — which rebases its input in place — projects it. A shallow `clone` would still share
|
|
207
|
+
// those frozen children and corrupt / fail on the live state (see `delta.cloneDeep`).
|
|
208
|
+
const tres = this.t.applyA(delta.cloneDeep(this.a.delta))
|
|
209
|
+
const fa = tres.a ? this.a.applyDelta(tres.a, this) : null
|
|
210
|
+
// `diff` shares nested children with `tres.b` by default, and `tres.b` is a (possibly stateful)
|
|
211
|
+
// transformer's output that may still alias its internal state — so `clone` keeps the diff
|
|
212
|
+
// independent, since it is then applied into `b` (which freezes it) and propagated onward.
|
|
213
|
+
const diffB = tres.b ? delta.diff(this.b.delta, tres.b, { clone: true }) : null
|
|
214
|
+
const fb = diffB ? this.b.applyDelta(diffB, this) : null
|
|
164
215
|
propagate(this, fa, fb)
|
|
165
216
|
})
|
|
166
217
|
}
|
|
@@ -184,11 +235,12 @@ export class Binding {
|
|
|
184
235
|
* {@link Binding#destroy} on the returned binding to stop syncing (it also self-destroys when either RDT
|
|
185
236
|
* emits `'destroy'`).
|
|
186
237
|
*
|
|
187
|
-
* @template {
|
|
188
|
-
* @
|
|
189
|
-
* @param {RDT<
|
|
190
|
-
* @param {
|
|
238
|
+
* @template {delta.DeltaConf} A
|
|
239
|
+
* @template {delta.DeltaConf} B
|
|
240
|
+
* @param {RDT<A>} a
|
|
241
|
+
* @param {RDT<B>} b
|
|
242
|
+
* @param {dt.TemplateFactory<A,B>} [template] a `$d => Template` factory (e.g. `dt.id` or
|
|
191
243
|
* `$d => dt.renameAttrs($d, {a:'b'})`)
|
|
192
|
-
* @return {Binding<
|
|
244
|
+
* @return {Binding<A,B>}
|
|
193
245
|
*/
|
|
194
246
|
export const bind = (a, b, template) => new Binding(a, b, template)
|
|
@@ -89,6 +89,21 @@ export const $tresult = ($a, $b) => /** @type {any} */ (s.$instanceOf(TransformR
|
|
|
89
89
|
export const createTransformResult = (a, b) => new TransformResult(a, b)
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
+
* A stateful, schema-bound mapper between changes of side A and side B, materialized from a
|
|
93
|
+
* {@link Template} via `init()`.
|
|
94
|
+
*
|
|
95
|
+
* ## Deltas are manipulated in place
|
|
96
|
+
*
|
|
97
|
+
* `apply`/`applyA`/`applyB` **mutate their input deltas directly** — they rebase nodes and splice op
|
|
98
|
+
* lists in place, and some transformers `apply(other, { move: true })` the input into an accumulator —
|
|
99
|
+
* rather than editing only through the copy-on-write `apply`/`rebase` surface. This is a deliberate
|
|
100
|
+
* exception to the general rule (a delta is normally edited only via `apply`/`rebase`, which re-clone
|
|
101
|
+
* frozen content on first touch); it keeps the transform allocation-free, but it means a transformer is
|
|
102
|
+
* an *owning* consumer of the delta it is given. **Callers must pass a privately-owned, fully-mutable
|
|
103
|
+
* delta** — never a shared or `done` (frozen) one. A caller holding a shared read delta must
|
|
104
|
+
* {@link import('../delta.js').cloneDeep deep-clone} it first (this is exactly what a
|
|
105
|
+
* {@link import('../rdt.js').Binding} does before routing a change through its transformer).
|
|
106
|
+
*
|
|
92
107
|
* @template {import('../delta.js').DeltaConf} A
|
|
93
108
|
* @template {import('../delta.js').DeltaConf} B
|
|
94
109
|
*/
|