lib0 1.0.0-rc.16 → 1.0.0-rc.17
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 +12 -3
- package/dist/delta/rdt/delta.d.ts +1 -1
- package/dist/delta/rdt/dom.d.ts +1 -1
- package/dist/delta/rdt.d.ts +38 -3
- package/package.json +1 -1
- package/src/delta/delta.js +144 -51
- package/src/delta/rdt/delta.js +2 -2
- package/src/delta/rdt/dom.js +1 -1
- package/src/delta/rdt.js +24 -7
package/dist/delta/delta.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export const $attribution: s.Schema<Attribution>;
|
|
|
32
32
|
* @typedef {{ type: 'insert', insert: string|Array<any>, format?: { [key: string]: any }, attribution?: Attribution } | { delete: number } | { type: 'retain', retain: number, format?: { [key:string]: any }, attribution?: Attribution } | { type: 'modify', value: object }} DeltaListOpJSON
|
|
33
33
|
*/
|
|
34
34
|
/**
|
|
35
|
-
* @typedef {{ type: 'insert', value: any, prevValue?: any, attribution?: Attribution } | { type: 'delete', prevValue?: any, attribution?: Attribution } | { type: 'modify', value: DeltaJSON }} DeltaAttrOpJSON
|
|
35
|
+
* @typedef {{ type: 'insert', value: any, prevValue?: any, attribution?: Attribution } | { type: 'delete', prevValue?: any, attribution?: Attribution } | { type: 'modify', value: DeltaJSON, attribution?: Attribution }} DeltaAttrOpJSON
|
|
36
36
|
*/
|
|
37
37
|
/**
|
|
38
38
|
* @typedef {TextOp|InsertOp<any>|DeleteOp|RetainOp|ModifyOp<any>} ChildrenOpAny
|
|
@@ -491,8 +491,9 @@ export class ModifyAttrOp<Modifier extends DeltaAny = DeltaAny, K extends string
|
|
|
491
491
|
/**
|
|
492
492
|
* @param {K} key
|
|
493
493
|
* @param {Modifier} delta
|
|
494
|
+
* @param {Attribution?} attribution
|
|
494
495
|
*/
|
|
495
|
-
constructor(key: K, delta: Modifier);
|
|
496
|
+
constructor(key: K, delta: Modifier, attribution: Attribution | null);
|
|
496
497
|
/**
|
|
497
498
|
* @readonly
|
|
498
499
|
* @type {K}
|
|
@@ -503,6 +504,11 @@ export class ModifyAttrOp<Modifier extends DeltaAny = DeltaAny, K extends string
|
|
|
503
504
|
* @type {Modifier}
|
|
504
505
|
*/
|
|
505
506
|
readonly value: Modifier;
|
|
507
|
+
/**
|
|
508
|
+
* @readonly
|
|
509
|
+
* @type {Attribution?}
|
|
510
|
+
*/
|
|
511
|
+
readonly attribution: Attribution | null;
|
|
506
512
|
/**
|
|
507
513
|
* @type {string|null}
|
|
508
514
|
*/
|
|
@@ -751,9 +757,10 @@ export class DeltaBuilder<Conf extends DeltaConf = {}, FixedConf extends boolean
|
|
|
751
757
|
* @template {Extract<DeltaConfGetAllowedAttrs<Conf, FixedConf>[Key],DeltaAny>} D
|
|
752
758
|
* @param {Key} key
|
|
753
759
|
* @param {D} modify
|
|
760
|
+
* @param {Attribution?} attribution
|
|
754
761
|
* @return {DeltaBuilder<DeltaConfOverwrite<Conf,{attrs:AddToAttrs<DeltaConfGetAttrs<Conf>,Key,D>}>, FixedConf>}
|
|
755
762
|
*/
|
|
756
|
-
modifyAttr<Key extends DeltaConfGetAllowedAttrs<Conf, FixedConf> extends infer As ? { [K in keyof As]: Extract<As[K], DeltaAny> extends never ? never : K; }[keyof As] : never, D extends Extract<DeltaConfGetAllowedAttrs<Conf, FixedConf>[Key], DeltaAny>>(key: Key, modify: D): DeltaBuilder<DeltaConfOverwrite<Conf, {
|
|
763
|
+
modifyAttr<Key extends DeltaConfGetAllowedAttrs<Conf, FixedConf> extends infer As ? { [K in keyof As]: Extract<As[K], DeltaAny> extends never ? never : K; }[keyof As] : never, D extends Extract<DeltaConfGetAllowedAttrs<Conf, FixedConf>[Key], DeltaAny>>(key: Key, modify: D, attribution?: Attribution | null): DeltaBuilder<DeltaConfOverwrite<Conf, {
|
|
757
764
|
attrs: AddToAttrs<DeltaConfGetAttrs<Conf>, Key, D>;
|
|
758
765
|
}>, FixedConf>;
|
|
759
766
|
/**
|
|
@@ -891,6 +898,7 @@ export function random<Conf_1 extends DeltaConf>(gen: prng.PRNG, $d: s.Schema<De
|
|
|
891
898
|
source?: DeltaAny | null | undefined;
|
|
892
899
|
minChildOps?: number | undefined;
|
|
893
900
|
maxChildOps?: number | undefined;
|
|
901
|
+
attribution?: boolean | undefined;
|
|
894
902
|
}): DeltaBuilder<Conf_1>;
|
|
895
903
|
/**
|
|
896
904
|
* @overload
|
|
@@ -1080,6 +1088,7 @@ export type DeltaAttrOpJSON = {
|
|
|
1080
1088
|
} | {
|
|
1081
1089
|
type: "modify";
|
|
1082
1090
|
value: DeltaJSON;
|
|
1091
|
+
attribution?: Attribution;
|
|
1083
1092
|
};
|
|
1084
1093
|
export type ChildrenOpAny = TextOp | InsertOp<any> | DeleteOp | RetainOp | ModifyOp<any>;
|
|
1085
1094
|
export type AttrOpAny = SetAttrOp<any> | DeleteAttrOp<any> | ModifyAttrOp;
|
package/dist/delta/rdt/dom.d.ts
CHANGED
package/dist/delta/rdt.d.ts
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
1
|
export { deltaRDT } from "./rdt/delta.js";
|
|
2
|
+
/**
|
|
3
|
+
* @template T
|
|
4
|
+
* @typedef {import('../schema.js').Schema<T>} Schema
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Abstract interface for a delta-based Replicated Data Type.
|
|
8
|
+
*
|
|
9
|
+
* An RDT is an observable that emits a `'delta'` (and, on teardown, a `'destroy'`) event, exposes the
|
|
10
|
+
* {@link Schema schema} of the deltas it produces via `$delta` (so a {@link Binding} can initialize a
|
|
11
|
+
* transformer for it), exposes its current state as a delta via the `delta` getter, and accepts foreign
|
|
12
|
+
* deltas via `applyDelta`.
|
|
13
|
+
*
|
|
14
|
+
* `applyDelta(d)` applies `d` and then, if the change would violate the RDT's own invariants, applies
|
|
15
|
+
* a **fix** of its own (e.g. reversing part of `d`, or inserting missing content) and returns that fix
|
|
16
|
+
* — `null` when none was needed. The fix is a regular change on this RDT, so a {@link Binding} maps it
|
|
17
|
+
* onto the other side just like any other change. `applyDelta` also emits the *effective* change
|
|
18
|
+
* (`d` together with the fix) on the `'delta'` channel.
|
|
19
|
+
*
|
|
20
|
+
* @template {import('./delta.js').DeltaAny} D
|
|
21
|
+
* @typedef {import('../observable.js').ObservableV2<{ delta: (delta: D) => void, destroy: (rdt: RDT<D>) => void }> & {
|
|
22
|
+
* $delta: Schema<D>,
|
|
23
|
+
* delta: D,
|
|
24
|
+
* applyDelta: (delta: D) => D | null,
|
|
25
|
+
* destroy: () => void
|
|
26
|
+
* }} RDT
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Schema guard for any {@link RDT}. RDTs are **duck-typed** — they have several independent
|
|
30
|
+
* implementations (`deltaRDT`, `domRDT`, …) and no common constructor — so this matches by shape
|
|
31
|
+
* rather than by `instanceof`: an object exposing a `$delta` {@link Schema}, an `applyDelta` method,
|
|
32
|
+
* and the observable `on`/`destroy` channel. The (possibly expensive) `delta` getter is not invoked.
|
|
33
|
+
*
|
|
34
|
+
* @type {Schema<RDT<any>>}
|
|
35
|
+
*/
|
|
36
|
+
export const $rdt: Schema<RDT<any>>;
|
|
2
37
|
/**
|
|
3
38
|
* Connects two RDTs so that changes on either side are transformed and reflected on the other.
|
|
4
39
|
*
|
|
@@ -41,8 +76,8 @@ export type Schema<T> = import("../schema.js").Schema<T>;
|
|
|
41
76
|
*
|
|
42
77
|
* An RDT is an observable that emits a `'delta'` (and, on teardown, a `'destroy'`) event, exposes the
|
|
43
78
|
* {@link Schema schema} of the deltas it produces via `$delta` (so a {@link Binding} can initialize a
|
|
44
|
-
* transformer for it), exposes its current state as a delta via `
|
|
45
|
-
* via `applyDelta`.
|
|
79
|
+
* transformer for it), exposes its current state as a delta via the `delta` getter, and accepts foreign
|
|
80
|
+
* deltas via `applyDelta`.
|
|
46
81
|
*
|
|
47
82
|
* `applyDelta(d)` applies `d` and then, if the change would violate the RDT's own invariants, applies
|
|
48
83
|
* a **fix** of its own (e.g. reversing part of `d`, or inserting missing content) and returns that fix
|
|
@@ -55,7 +90,7 @@ export type RDT<D extends import("./delta.js").DeltaAny> = import("../observable
|
|
|
55
90
|
destroy: (rdt: RDT<D>) => void;
|
|
56
91
|
}> & {
|
|
57
92
|
$delta: Schema<D>;
|
|
58
|
-
|
|
93
|
+
delta: D;
|
|
59
94
|
applyDelta: (delta: D) => D | null;
|
|
60
95
|
destroy: () => void;
|
|
61
96
|
};
|
package/package.json
CHANGED
package/src/delta/delta.js
CHANGED
|
@@ -86,7 +86,7 @@ export const $attribution = /* @__PURE__ */(() => s.$object({
|
|
|
86
86
|
*/
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* @typedef {{ type: 'insert', value: any, prevValue?: any, attribution?: Attribution } | { type: 'delete', prevValue?: any, attribution?: Attribution } | { type: 'modify', value: DeltaJSON }} DeltaAttrOpJSON
|
|
89
|
+
* @typedef {{ type: 'insert', value: any, prevValue?: any, attribution?: Attribution } | { type: 'delete', prevValue?: any, attribution?: Attribution } | { type: 'modify', value: DeltaJSON, attribution?: Attribution }} DeltaAttrOpJSON
|
|
90
90
|
*/
|
|
91
91
|
|
|
92
92
|
/**
|
|
@@ -106,7 +106,7 @@ export const $attribution = /* @__PURE__ */(() => s.$object({
|
|
|
106
106
|
*/
|
|
107
107
|
export const $deltaMapChangeJson = /* @__PURE__ */(() => s.$union(
|
|
108
108
|
s.$object({ type: s.$literal('insert'), value: s.$any, prevValue: s.$any.optional, attribution: $attribution.optional }),
|
|
109
|
-
s.$object({ type: s.$literal('modify'), value: s.$any }),
|
|
109
|
+
s.$object({ type: s.$literal('modify'), value: s.$any, attribution: $attribution.optional }),
|
|
110
110
|
s.$object({ type: s.$literal('delete'), prevValue: s.$any.optional, attribution: $attribution.optional })
|
|
111
111
|
))()
|
|
112
112
|
|
|
@@ -116,6 +116,14 @@ export const $deltaMapChangeJson = /* @__PURE__ */(() => s.$union(
|
|
|
116
116
|
* @return {Attrs}
|
|
117
117
|
*/
|
|
118
118
|
const _cloneAttrs = attrs => attrs == null ? attrs : { ...attrs }
|
|
119
|
+
/**
|
|
120
|
+
* A settled op never stores an empty attribution — `{}` normalizes to `null` ("no attributions").
|
|
121
|
+
* (A `retain` *change* op keeps `{}` verbatim, where it means "clear".)
|
|
122
|
+
*
|
|
123
|
+
* @param {Attribution?} attribution
|
|
124
|
+
* @return {Attribution?}
|
|
125
|
+
*/
|
|
126
|
+
const normalizeAttribution = attribution => object.isEmpty(attribution) ? null : attribution
|
|
119
127
|
/**
|
|
120
128
|
* @template {any} MaybeDelta
|
|
121
129
|
* @param {MaybeDelta} maybeDelta
|
|
@@ -192,6 +200,7 @@ export class TextOp extends list.ListNode {
|
|
|
192
200
|
encoding.writeVarUint(encoder, 0) // textOp type: 0
|
|
193
201
|
encoding.writeVarString(encoder, this.insert)
|
|
194
202
|
encoding.writeAny(encoder, this.format)
|
|
203
|
+
encoding.writeAny(encoder, this.attribution)
|
|
195
204
|
})))
|
|
196
205
|
}
|
|
197
206
|
|
|
@@ -323,6 +332,7 @@ export class InsertOp extends list.ListNode {
|
|
|
323
332
|
encoding.writeVarString(encoder, fingerprintTrait.fingerprint(/** @type {any} */ (ins)))
|
|
324
333
|
})
|
|
325
334
|
encoding.writeAny(encoder, this.format)
|
|
335
|
+
encoding.writeAny(encoder, this.attribution)
|
|
326
336
|
})))
|
|
327
337
|
}
|
|
328
338
|
|
|
@@ -499,6 +509,7 @@ export class RetainOp extends list.ListNode {
|
|
|
499
509
|
encoding.writeVarUint(encoder, 3) // retainOp type: 3
|
|
500
510
|
encoding.writeVarUint(encoder, this.retain)
|
|
501
511
|
encoding.writeAny(encoder, this.format)
|
|
512
|
+
encoding.writeAny(encoder, this.attribution)
|
|
502
513
|
})))
|
|
503
514
|
}
|
|
504
515
|
|
|
@@ -605,6 +616,7 @@ export class ModifyOp extends list.ListNode {
|
|
|
605
616
|
encoding.writeVarUint(encoder, 4) // modifyOp type: 4
|
|
606
617
|
encoding.writeVarString(encoder, this.value.fingerprint)
|
|
607
618
|
encoding.writeAny(encoder, this.format)
|
|
619
|
+
encoding.writeAny(encoder, this.attribution)
|
|
608
620
|
})))
|
|
609
621
|
}
|
|
610
622
|
|
|
@@ -720,6 +732,7 @@ export class SetAttrOp {
|
|
|
720
732
|
encoding.writeUint8(encoder, 1)
|
|
721
733
|
encoding.writeAny(encoder, /** @type {any} */ (this.value))
|
|
722
734
|
}
|
|
735
|
+
encoding.writeAny(encoder, this.attribution)
|
|
723
736
|
})))
|
|
724
737
|
}
|
|
725
738
|
|
|
@@ -791,6 +804,7 @@ export class DeleteAttrOp {
|
|
|
791
804
|
return this._fingerprint || (this._fingerprint = buffer.toBase64(encoding.encode(encoder => {
|
|
792
805
|
encoding.writeVarUint(encoder, 6) // map delete type: 6
|
|
793
806
|
encoding.writeAny(encoder, this.key)
|
|
807
|
+
encoding.writeAny(encoder, this.attribution)
|
|
794
808
|
})))
|
|
795
809
|
}
|
|
796
810
|
|
|
@@ -833,8 +847,9 @@ export class ModifyAttrOp {
|
|
|
833
847
|
/**
|
|
834
848
|
* @param {K} key
|
|
835
849
|
* @param {Modifier} delta
|
|
850
|
+
* @param {Attribution?} attribution
|
|
836
851
|
*/
|
|
837
|
-
constructor (key, delta) {
|
|
852
|
+
constructor (key, delta, attribution) {
|
|
838
853
|
/**
|
|
839
854
|
* @readonly
|
|
840
855
|
* @type {K}
|
|
@@ -845,6 +860,11 @@ export class ModifyAttrOp {
|
|
|
845
860
|
* @type {Modifier}
|
|
846
861
|
*/
|
|
847
862
|
this.value = delta
|
|
863
|
+
/**
|
|
864
|
+
* @readonly
|
|
865
|
+
* @type {Attribution?}
|
|
866
|
+
*/
|
|
867
|
+
this.attribution = attribution
|
|
848
868
|
/**
|
|
849
869
|
* @type {string|null}
|
|
850
870
|
*/
|
|
@@ -861,6 +881,7 @@ export class ModifyAttrOp {
|
|
|
861
881
|
encoding.writeVarUint(encoder, 7) // map modify type: 7
|
|
862
882
|
encoding.writeAny(encoder, this.key)
|
|
863
883
|
encoding.writeVarString(encoder, this.value.fingerprint)
|
|
884
|
+
encoding.writeAny(encoder, this.attribution)
|
|
864
885
|
})))
|
|
865
886
|
}
|
|
866
887
|
|
|
@@ -875,17 +896,21 @@ export class ModifyAttrOp {
|
|
|
875
896
|
* @return {DeltaAttrOpJSON}
|
|
876
897
|
*/
|
|
877
898
|
toJSON () {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
899
|
+
const attribution = this.attribution
|
|
900
|
+
return object.assign(
|
|
901
|
+
{
|
|
902
|
+
type: this.type,
|
|
903
|
+
value: this.value.toJSON()
|
|
904
|
+
},
|
|
905
|
+
attribution != null ? { attribution } : {}
|
|
906
|
+
)
|
|
882
907
|
}
|
|
883
908
|
|
|
884
909
|
/**
|
|
885
910
|
* @param {ModifyAttrOp<Modifier>} other
|
|
886
911
|
*/
|
|
887
912
|
[equalityTrait.EqualityTraitSymbol] (other) {
|
|
888
|
-
return $modifyAttrOp.check(other) && this.key === other.key && this.value[equalityTrait.EqualityTraitSymbol](other.value)
|
|
913
|
+
return $modifyAttrOp.check(other) && this.key === other.key && this.value[equalityTrait.EqualityTraitSymbol](other.value) && fun.equalityDeep(this.attribution, other.attribution)
|
|
889
914
|
}
|
|
890
915
|
|
|
891
916
|
/**
|
|
@@ -894,7 +919,7 @@ export class ModifyAttrOp {
|
|
|
894
919
|
* @return {ModifyAttrOp<Modifier,K>}
|
|
895
920
|
*/
|
|
896
921
|
clone () {
|
|
897
|
-
return new ModifyAttrOp(this.key, /** @type {Modifier} */ (this.value.done()))
|
|
922
|
+
return new ModifyAttrOp(this.key, /** @type {Modifier} */ (this.value.done()), _cloneAttrs(this.attribution))
|
|
898
923
|
}
|
|
899
924
|
}
|
|
900
925
|
|
|
@@ -1734,7 +1759,7 @@ export class DeltaBuilder extends Delta {
|
|
|
1734
1759
|
insert (insert, formatting = null, attribution = null) {
|
|
1735
1760
|
modDeltaCheck(this)
|
|
1736
1761
|
const mergedAttributes = mergeFormats(this.usedAttributes, formatting)
|
|
1737
|
-
const mergedAttribution = mergeAttributions(this.usedAttribution, attribution)
|
|
1762
|
+
const mergedAttribution = normalizeAttribution(mergeAttributions(this.usedAttribution, attribution))
|
|
1738
1763
|
/**
|
|
1739
1764
|
* @param {TextOp | InsertOp<any>} lastOp
|
|
1740
1765
|
*/
|
|
@@ -1772,7 +1797,7 @@ export class DeltaBuilder extends Delta {
|
|
|
1772
1797
|
modify (modify, formatting = null, attribution = null) {
|
|
1773
1798
|
modDeltaCheck(this)
|
|
1774
1799
|
const mergedAttributes = mergeFormats(this.usedAttributes, formatting)
|
|
1775
|
-
const mergedAttribution = mergeAttributions(this.usedAttribution, attribution)
|
|
1800
|
+
const mergedAttribution = normalizeAttribution(mergeAttributions(this.usedAttribution, attribution))
|
|
1776
1801
|
list.pushEnd(this.children, new ModifyOp(modify, object.isEmpty(mergedAttributes) ? null : mergedAttributes, mergedAttribution))
|
|
1777
1802
|
this.childCnt += 1
|
|
1778
1803
|
// the modify target carries marks in its subtree (own root marks + nested) - flag conservatively
|
|
@@ -1870,7 +1895,7 @@ export class DeltaBuilder extends Delta {
|
|
|
1870
1895
|
modDeltaCheck(this)
|
|
1871
1896
|
// @ts-ignore
|
|
1872
1897
|
this.attrs[key] /** @type {any} */ =
|
|
1873
|
-
(new SetAttrOp(/** @type {any} */ (key), val, prevValue, mergeAttributions(this.usedAttribution, attribution)))
|
|
1898
|
+
(new SetAttrOp(/** @type {any} */ (key), val, prevValue, normalizeAttribution(mergeAttributions(this.usedAttribution, attribution))))
|
|
1874
1899
|
// a delta-valued attribute carries marks in its subtree - flag conservatively (never decremented)
|
|
1875
1900
|
if ($deltaAny.check(val)) this.maybeHasMarks ||= val.maybeHasMarks
|
|
1876
1901
|
return /** @type {any} */ (this)
|
|
@@ -1909,7 +1934,7 @@ export class DeltaBuilder extends Delta {
|
|
|
1909
1934
|
// is left as-is (never decremented - marksToPositions self-corrects it)
|
|
1910
1935
|
// @ts-ignore
|
|
1911
1936
|
this.attrs[key] /** @type {any} */ =
|
|
1912
|
-
(new DeleteAttrOp(/** @type {any} */ (key), prevValue, mergeAttributions(this.usedAttribution, attribution)))
|
|
1937
|
+
(new DeleteAttrOp(/** @type {any} */ (key), prevValue, normalizeAttribution(mergeAttributions(this.usedAttribution, attribution))))
|
|
1913
1938
|
return /** @type {any} */ (this)
|
|
1914
1939
|
}
|
|
1915
1940
|
|
|
@@ -1918,11 +1943,13 @@ export class DeltaBuilder extends Delta {
|
|
|
1918
1943
|
* @template {Extract<DeltaConfGetAllowedAttrs<Conf, FixedConf>[Key],DeltaAny>} D
|
|
1919
1944
|
* @param {Key} key
|
|
1920
1945
|
* @param {D} modify
|
|
1946
|
+
* @param {Attribution?} attribution
|
|
1921
1947
|
* @return {DeltaBuilder<DeltaConfOverwrite<Conf,{attrs:AddToAttrs<DeltaConfGetAttrs<Conf>,Key,D>}>, FixedConf>}
|
|
1922
1948
|
*/
|
|
1923
|
-
modifyAttr (key, modify) {
|
|
1949
|
+
modifyAttr (key, modify, attribution = null) {
|
|
1924
1950
|
modDeltaCheck(this)
|
|
1925
|
-
|
|
1951
|
+
const mergedAttribution = normalizeAttribution(mergeAttributions(this.usedAttribution, attribution))
|
|
1952
|
+
this.attrs[key] = /** @type {any} */ (new ModifyAttrOp(key, modify, mergedAttribution))
|
|
1926
1953
|
// the modify value carries marks in its subtree - flag conservatively (never decremented)
|
|
1927
1954
|
this.maybeHasMarks ||= /** @type {DeltaAny} */ (modify).maybeHasMarks
|
|
1928
1955
|
return /** @type {any} */ (this)
|
|
@@ -1964,6 +1991,11 @@ export class DeltaBuilder extends Delta {
|
|
|
1964
1991
|
const tgt = c._modValue
|
|
1965
1992
|
tgt.apply(op.value, { final, move })
|
|
1966
1993
|
this.maybeHasMarks ||= tgt.maybeHasMarks // flag the attr subtree's marks up
|
|
1994
|
+
if (op.attribution != null) {
|
|
1995
|
+
// modify carries an attribution update for the target attr op (replace-whole; {} clears to null)
|
|
1996
|
+
;/** @type {any} */ (c).attribution = normalizeAttribution(op.attribution)
|
|
1997
|
+
;/** @type {any} */ (c)._fingerprint = null
|
|
1998
|
+
}
|
|
1967
1999
|
} else {
|
|
1968
2000
|
// then this is a simple modify (the attribute did not previously hold a delta)
|
|
1969
2001
|
// @ts-ignore
|
|
@@ -2056,13 +2088,18 @@ export class DeltaBuilder extends Delta {
|
|
|
2056
2088
|
insertClonedOp(op)
|
|
2057
2089
|
} else if ($retainOp.check(op)) {
|
|
2058
2090
|
let retainLen = op.length
|
|
2059
|
-
|
|
2091
|
+
// split the current op when this retain carries a format and/or attribution update that the
|
|
2092
|
+
// existing op does not already match (format diff per-key; attribution is replace-whole)
|
|
2093
|
+
if (offset > 0 && opsI != null && !$deleteOp.check(opsI) && (
|
|
2094
|
+
(op.format != null && (/** @type {InsertOp<any>|RetainOp|ModifyOp} */ (opsI).format == null || !fun.equalityDeep(opsI.format, op.format))) ||
|
|
2095
|
+
(op.attribution != null && !fun.equalityDeep(/** @type {any} */ (opsI).attribution, op.attribution))
|
|
2096
|
+
)) {
|
|
2060
2097
|
// need to split current op
|
|
2061
2098
|
splitHere()
|
|
2062
2099
|
}
|
|
2063
2100
|
while (opsI != null && opsI.length - offset <= retainLen) {
|
|
2064
|
-
if (op.format != null) {
|
|
2065
|
-
updateOpFormat(opsI, op.format)
|
|
2101
|
+
if (op.format != null || op.attribution != null) {
|
|
2102
|
+
updateOpFormat(opsI, op.format, op.attribution)
|
|
2066
2103
|
scheduleForMerge(opsI)
|
|
2067
2104
|
}
|
|
2068
2105
|
retainLen -= opsI.length - offset
|
|
@@ -2070,19 +2107,21 @@ export class DeltaBuilder extends Delta {
|
|
|
2070
2107
|
offset = 0
|
|
2071
2108
|
}
|
|
2072
2109
|
if (opsI != null) {
|
|
2073
|
-
if (op.format != null && retainLen > 0) {
|
|
2110
|
+
if ((op.format != null || op.attribution != null) && retainLen > 0) {
|
|
2074
2111
|
// accumulate onto the existing offset — the else-branch below uses
|
|
2075
2112
|
// `offset += retainLen`, and we must agree with it when prior
|
|
2076
2113
|
// iterations have advanced offset into opsI without splitting (e.g.
|
|
2077
2114
|
// a format-less retain followed by a same-format retain).
|
|
2078
2115
|
offset += retainLen
|
|
2079
2116
|
splitHere()
|
|
2080
|
-
updateOpFormat(/** @type {ChildrenOpAny} */ (opsI.prev), op.format)
|
|
2117
|
+
updateOpFormat(/** @type {ChildrenOpAny} */ (opsI.prev), op.format, op.attribution)
|
|
2081
2118
|
scheduleForMerge(opsI.prev)
|
|
2082
2119
|
} else {
|
|
2083
2120
|
offset += retainLen
|
|
2084
2121
|
}
|
|
2085
2122
|
} else if (retainLen > 0) {
|
|
2123
|
+
// append the retain verbatim — a `{}` attribution here is a meaningful "clear" change op (e.g. when
|
|
2124
|
+
// a formatting/attribution diff is applied onto an as-yet-empty change delta), not a no-op to drop
|
|
2086
2125
|
_insertChild(this, null, scheduleForMerge(new RetainOp(retainLen, op.format, op.attribution)))
|
|
2087
2126
|
}
|
|
2088
2127
|
} else if ($deleteOp.check(op)) {
|
|
@@ -2149,7 +2188,7 @@ export class DeltaBuilder extends Delta {
|
|
|
2149
2188
|
/* c8 ignore stop */
|
|
2150
2189
|
}
|
|
2151
2190
|
} else if ($modifyOp.check(op)) {
|
|
2152
|
-
if (opsI != null && op.format != null && (!$deleteOp.check(opsI) && !$retainOp.check(opsI))) { // retain handles splitting seperately, without copying attrs
|
|
2191
|
+
if (opsI != null && (op.format != null || op.attribution != null) && (!$deleteOp.check(opsI) && !$retainOp.check(opsI))) { // retain handles splitting seperately, without copying attrs
|
|
2153
2192
|
splitHere()
|
|
2154
2193
|
if (opsI.length > 1) {
|
|
2155
2194
|
offset = 1
|
|
@@ -2157,7 +2196,7 @@ export class DeltaBuilder extends Delta {
|
|
|
2157
2196
|
opsI = /** @type {InsertOp<any>} */ (opsI.prev)
|
|
2158
2197
|
}
|
|
2159
2198
|
// at this point, opsI is guaranteed to be !deleteOp && !retainOp and of length 1
|
|
2160
|
-
updateOpFormat(opsI, op.format)
|
|
2199
|
+
updateOpFormat(opsI, op.format, op.attribution)
|
|
2161
2200
|
scheduleForMerge(opsI)
|
|
2162
2201
|
}
|
|
2163
2202
|
if (opsI == null) {
|
|
@@ -2180,7 +2219,7 @@ export class DeltaBuilder extends Delta {
|
|
|
2180
2219
|
splitHere()
|
|
2181
2220
|
const insertModOp = scheduleForMerge(move ? op : op.clone(0, 1, keep))
|
|
2182
2221
|
this.maybeHasMarks ||= /** @type {DeltaAny} */ (op.value).maybeHasMarks // the inserted modify brings its value's root marks
|
|
2183
|
-
opsI.format && updateOpFormat(insertModOp, opsI.format)
|
|
2222
|
+
;(opsI.format != null || opsI.attribution != null) && updateOpFormat(insertModOp, opsI.format, opsI.attribution)
|
|
2184
2223
|
// the modify replaces one retain position with a length-1 modify (net childCnt: +1 here, -1 below)
|
|
2185
2224
|
_insertChild(this, opsI, insertModOp)
|
|
2186
2225
|
if (opsI.length === 1) {
|
|
@@ -2465,11 +2504,16 @@ export class DeltaBuilder extends Delta {
|
|
|
2465
2504
|
}
|
|
2466
2505
|
|
|
2467
2506
|
/**
|
|
2507
|
+
* Apply a format update (incremental, per-key) and/or an attribution update (replace-whole, opaque) to
|
|
2508
|
+
* an op. `format`/`attribution` are now fingerprinted, so any change nulls the op's `_fingerprint`.
|
|
2509
|
+
*
|
|
2468
2510
|
* @param {ChildrenOpAny} op
|
|
2469
|
-
* @param {{[k:string]:any}} formatUpdate
|
|
2511
|
+
* @param {{[k:string]:any}?} formatUpdate incremental: `{k:v}` sets, `{k:null}` removes; `null` skips
|
|
2512
|
+
* @param {Attribution?} attributionUpdate replace-whole: `{…}` overwrites, `{}` clears to null; `null` skips
|
|
2470
2513
|
*/
|
|
2471
|
-
const updateOpFormat = (op, formatUpdate) => {
|
|
2472
|
-
if (
|
|
2514
|
+
const updateOpFormat = (op, formatUpdate, attributionUpdate) => {
|
|
2515
|
+
if ($deleteOp.check(op)) return
|
|
2516
|
+
if (formatUpdate != null) {
|
|
2473
2517
|
if ($retainOp.check(op) || $modifyOp.check(op)) {
|
|
2474
2518
|
// never modify formats
|
|
2475
2519
|
/** @type {any} */ (op).format = object.assign({}, op.format, formatUpdate)
|
|
@@ -2487,6 +2531,17 @@ const updateOpFormat = (op, formatUpdate) => {
|
|
|
2487
2531
|
}
|
|
2488
2532
|
}
|
|
2489
2533
|
}
|
|
2534
|
+
if (attributionUpdate != null) {
|
|
2535
|
+
// attribution is replace-whole (opaque). On a retain/modify *change* op keep `{}` verbatim — it is a
|
|
2536
|
+
// "clear" instruction that must propagate when this op is later applied; on a settled content op
|
|
2537
|
+
// (insert/text) `{}` normalizes to null (no attributions). Mirrors the format retain-vs-content split.
|
|
2538
|
+
/** @type {any} */ (op).attribution = ($retainOp.check(op) || $modifyOp.check(op))
|
|
2539
|
+
? attributionUpdate
|
|
2540
|
+
: normalizeAttribution(attributionUpdate)
|
|
2541
|
+
}
|
|
2542
|
+
if (formatUpdate != null || attributionUpdate != null) {
|
|
2543
|
+
/** @type {any} */ (op)._fingerprint = null
|
|
2544
|
+
}
|
|
2490
2545
|
}
|
|
2491
2546
|
|
|
2492
2547
|
/**
|
|
@@ -3145,6 +3200,31 @@ const childNodeAt = (source, pos) => {
|
|
|
3145
3200
|
const matchingNodeSchemas = ($children, node) => (s.$$union.check($children) ? $children.shape : [$children])
|
|
3146
3201
|
.filter($c => $$delta.check($c) && $c.check(node))
|
|
3147
3202
|
|
|
3203
|
+
/**
|
|
3204
|
+
* Random {@link Attribution} for fuzz tests. There is no schema for attributions; the canonical shape is
|
|
3205
|
+
* `(insert|delete + …At)` intersected with an optional format part. The format part exists only on
|
|
3206
|
+
* children (content ops), never on node attribute ops. Sometimes returns `null` (skip) or `{}` (clear).
|
|
3207
|
+
*
|
|
3208
|
+
* @param {prng.PRNG} gen
|
|
3209
|
+
* @param {boolean} withFormat include the children-only `format`/`formatAt` part
|
|
3210
|
+
* @return {Attribution?}
|
|
3211
|
+
*/
|
|
3212
|
+
const randomAttribution = (gen, withFormat) => {
|
|
3213
|
+
const r = prng.uint32(gen, 0, 5)
|
|
3214
|
+
if (r <= 2) return null // ~half: no attribution change
|
|
3215
|
+
if (r === 3) return {} // clear all attribution
|
|
3216
|
+
const user = () => [prng.oneOf(gen, ['alice', 'bob', 'carol'])]
|
|
3217
|
+
/** @type {Attribution} */
|
|
3218
|
+
const attribution = prng.bool(gen)
|
|
3219
|
+
? { insert: user(), insertAt: prng.uint32(gen, 0, 100) }
|
|
3220
|
+
: { delete: user(), deleteAt: prng.uint32(gen, 0, 100) }
|
|
3221
|
+
if (withFormat && prng.bool(gen)) {
|
|
3222
|
+
attribution.format = { [prng.oneOf(gen, ['bold', 'italic'])]: user() }
|
|
3223
|
+
attribution.formatAt = prng.uint32(gen, 0, 100)
|
|
3224
|
+
}
|
|
3225
|
+
return attribution
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3148
3228
|
/**
|
|
3149
3229
|
* @template {DeltaConf} Conf
|
|
3150
3230
|
* @param {prng.PRNG} gen
|
|
@@ -3153,19 +3233,23 @@ const matchingNodeSchemas = ($children, node) => (s.$$union.check($children) ? $
|
|
|
3153
3233
|
* @param {DeltaAny?} [conf.source]
|
|
3154
3234
|
* @param {number} [conf.minChildOps]
|
|
3155
3235
|
* @param {number} [conf.maxChildOps]
|
|
3236
|
+
* @param {boolean} [conf.attribution] generate random attributions (off by default; consumes no PRNG
|
|
3237
|
+
* draws when off, so existing seeds are unaffected). Rebase does not converge attributions, so leave
|
|
3238
|
+
* this off for rebase-convergence fuzzing and enable it for diff fuzzing.
|
|
3156
3239
|
* @return {DeltaBuilder<Conf>}
|
|
3157
3240
|
*/
|
|
3158
3241
|
export const random = (gen, $d, conf = {}) => {
|
|
3159
|
-
const { source = null, minChildOps = 1, maxChildOps = 9 } = conf
|
|
3242
|
+
const { source = null, minChildOps = 1, maxChildOps = 9, attribution = false } = conf
|
|
3160
3243
|
let sourceLen = source == null ? 0 : source.childCnt
|
|
3161
3244
|
const { $name, $attrs, $children, hasText, $formats: $formats_ } = /** @type {$Delta<any>} */ (/** @type {any} */ ($d)).shape
|
|
3162
3245
|
const d = s.$$any.check($name) ? create($deltaAny) : create(s.random(gen, $name), $deltaAny)
|
|
3163
3246
|
const $formats = s.$$any.check($formats_) ? s.$null : $formats_
|
|
3164
|
-
|
|
3165
|
-
|
|
3247
|
+
const genAttribution = /** @param {boolean} withFormat */ withFormat => attribution ? randomAttribution(gen, withFormat) : null
|
|
3248
|
+
// set random attrs (node attribute ops carry the insert/delete part of an attribution only — no format part)
|
|
3249
|
+
prng.bool(gen) && d.setAttrs(s.random(gen, $attrs, random), genAttribution(false))
|
|
3166
3250
|
// delete a single attr
|
|
3167
3251
|
if (source && !object.isEmpty(source.attrs) && prng.bool(gen)) {
|
|
3168
|
-
d.deleteAttr(prng.oneOf(gen, object.keys(source.attrs)))
|
|
3252
|
+
d.deleteAttr(prng.oneOf(gen, object.keys(source.attrs)), genAttribution(false))
|
|
3169
3253
|
}
|
|
3170
3254
|
for (let i = prng.uint32(gen, minChildOps, maxChildOps); i > 0; i--) {
|
|
3171
3255
|
/**
|
|
@@ -3174,7 +3258,7 @@ export const random = (gen, $d, conf = {}) => {
|
|
|
3174
3258
|
const possibleOps = []
|
|
3175
3259
|
if (hasText) {
|
|
3176
3260
|
possibleOps.push(() => {
|
|
3177
|
-
d.insert(prng.oneOf(gen, ['a', 'b', ' ', '\n', '.']), s.random(gen, $formats))
|
|
3261
|
+
d.insert(prng.oneOf(gen, ['a', 'b', ' ', '\n', '.']), s.random(gen, $formats), genAttribution(true))
|
|
3178
3262
|
})
|
|
3179
3263
|
}
|
|
3180
3264
|
if (!s.$$never.check($children)) {
|
|
@@ -3187,7 +3271,7 @@ export const random = (gen, $d, conf = {}) => {
|
|
|
3187
3271
|
while (insN--) {
|
|
3188
3272
|
ins.push(s.random(gen, $children, random))
|
|
3189
3273
|
}
|
|
3190
|
-
d.insert(ins, s.random(gen, $formats))
|
|
3274
|
+
d.insert(ins, s.random(gen, $formats), genAttribution(true))
|
|
3191
3275
|
})
|
|
3192
3276
|
}
|
|
3193
3277
|
if (sourceLen > 0) {
|
|
@@ -3199,11 +3283,7 @@ export const random = (gen, $d, conf = {}) => {
|
|
|
3199
3283
|
possibleOps.push(() => {
|
|
3200
3284
|
const len = prng.uint32(gen, 1, sourceLen)
|
|
3201
3285
|
sourceLen -= len
|
|
3202
|
-
|
|
3203
|
-
d.retain(len)
|
|
3204
|
-
} else {
|
|
3205
|
-
d.retain(len, s.random(gen, $formats))
|
|
3206
|
-
}
|
|
3286
|
+
d.retain(len, prng.bool(gen) ? null : s.random(gen, $formats), genAttribution(true))
|
|
3207
3287
|
})
|
|
3208
3288
|
// if the op we currently point at is a node, it's also a choice to modify it: find the child
|
|
3209
3289
|
// schema that matches the node and recursively generate a change against it.
|
|
@@ -3212,7 +3292,7 @@ export const random = (gen, $d, conf = {}) => {
|
|
|
3212
3292
|
const $nodeMatches = node != null ? matchingNodeSchemas($children, node) : []
|
|
3213
3293
|
if ($nodeMatches.length > 0) {
|
|
3214
3294
|
possibleOps.push(() => {
|
|
3215
|
-
d.modify(random(gen, prng.oneOf(gen, $nodeMatches), { source: node }))
|
|
3295
|
+
d.modify(random(gen, prng.oneOf(gen, $nodeMatches), { source: node }), null, genAttribution(true))
|
|
3216
3296
|
sourceLen -= 1
|
|
3217
3297
|
})
|
|
3218
3298
|
}
|
|
@@ -3434,7 +3514,7 @@ export const diff = (d1, d2, options = {}) => {
|
|
|
3434
3514
|
*/
|
|
3435
3515
|
let right2 = d2.children.end
|
|
3436
3516
|
// whether we need to diff formatting
|
|
3437
|
-
let
|
|
3517
|
+
let formattingOrAttributionNeedsDiff = false
|
|
3438
3518
|
let commonPrefixOffset = 0
|
|
3439
3519
|
// perform a patience sort
|
|
3440
3520
|
// 1) remove common prefix and suffix
|
|
@@ -3469,7 +3549,7 @@ export const diff = (d1, d2, options = {}) => {
|
|
|
3469
3549
|
} else {
|
|
3470
3550
|
throw error.create('[lib0/delta] diffing deletes unsupported')
|
|
3471
3551
|
}
|
|
3472
|
-
|
|
3552
|
+
formattingOrAttributionNeedsDiff ||= left1.format != null || left1.attribution != null
|
|
3473
3553
|
left1 = left1.next
|
|
3474
3554
|
}
|
|
3475
3555
|
}
|
|
@@ -3487,7 +3567,7 @@ export const diff = (d1, d2, options = {}) => {
|
|
|
3487
3567
|
error.unexpectedCase()
|
|
3488
3568
|
}
|
|
3489
3569
|
/* c8 ignore stop */
|
|
3490
|
-
|
|
3570
|
+
formattingOrAttributionNeedsDiff ||= left2.format != null || left2.attribution != null
|
|
3491
3571
|
left2 = left2.next
|
|
3492
3572
|
}
|
|
3493
3573
|
}
|
|
@@ -3503,7 +3583,7 @@ export const diff = (d1, d2, options = {}) => {
|
|
|
3503
3583
|
// split all
|
|
3504
3584
|
const changeset4 = diffChangesetWithSeparator(changeset3, /./g)
|
|
3505
3585
|
applyChangesetToDelta(d, changeset4, compare, options)
|
|
3506
|
-
if (
|
|
3586
|
+
if (formattingOrAttributionNeedsDiff) {
|
|
3507
3587
|
const formattingDiff = create()
|
|
3508
3588
|
// update opsIs with content diff. then we can figure out the formatting diff.
|
|
3509
3589
|
const originalUpdated = clone(d1)
|
|
@@ -3520,24 +3600,36 @@ export const diff = (d1, d2, options = {}) => {
|
|
|
3520
3600
|
const minForward = math.min(b.length - bOffset, a.length - aOffset)
|
|
3521
3601
|
aOffset += minForward
|
|
3522
3602
|
bOffset += minForward
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3603
|
+
// format: incremental per-key diff (null when unchanged)
|
|
3604
|
+
/**
|
|
3605
|
+
* @type {FormattingAttributes?}
|
|
3606
|
+
*/
|
|
3607
|
+
let fupdate = null
|
|
3608
|
+
if (!fun.equalityDeep(bFormat, aFormat)) {
|
|
3526
3609
|
/**
|
|
3527
3610
|
* @type {FormattingAttributes}
|
|
3528
3611
|
*/
|
|
3529
|
-
const
|
|
3612
|
+
const fu = {}
|
|
3530
3613
|
bFormat != null && object.forEach(bFormat, (v, k) => {
|
|
3531
3614
|
if (!fun.equalityDeep(v, aFormat?.[k] || null)) {
|
|
3532
|
-
|
|
3615
|
+
fu[k] = v
|
|
3533
3616
|
}
|
|
3534
3617
|
})
|
|
3535
3618
|
aFormat && object.forEach(aFormat, (_, k) => {
|
|
3536
3619
|
if (b?.format?.[k] === undefined) {
|
|
3537
|
-
|
|
3620
|
+
fu[k] = null
|
|
3538
3621
|
}
|
|
3539
3622
|
})
|
|
3540
|
-
|
|
3623
|
+
fupdate = fu
|
|
3624
|
+
}
|
|
3625
|
+
// attribution: replace-whole (opaque). null = unchanged; {} = clear
|
|
3626
|
+
const attributionUpdate = fun.equalityDeep(a.attribution, b.attribution)
|
|
3627
|
+
? null
|
|
3628
|
+
: (b.attribution ?? {})
|
|
3629
|
+
if (fupdate == null && attributionUpdate == null) {
|
|
3630
|
+
formattingDiff.retain(minForward)
|
|
3631
|
+
} else {
|
|
3632
|
+
formattingDiff.retain(minForward, fupdate, attributionUpdate)
|
|
3541
3633
|
}
|
|
3542
3634
|
// update offset and iterators
|
|
3543
3635
|
if (bOffset >= b.length) {
|
|
@@ -3571,9 +3663,10 @@ export const diff = (d1, d2, options = {}) => {
|
|
|
3571
3663
|
const prevVal = attr1?.value
|
|
3572
3664
|
const nextVal = attr2.value
|
|
3573
3665
|
if ($deltaAny.check(prevVal) && $deltaAny.check(nextVal) && compare(prevVal, nextVal)) {
|
|
3574
|
-
|
|
3666
|
+
// modifyAttr carries the new attribution; apply updates the target attr op's value & attribution
|
|
3667
|
+
d.modifyAttr(key, diff(prevVal, nextVal, options), attr2.attribution)
|
|
3575
3668
|
} else {
|
|
3576
|
-
d.setAttr(key, nextVal)
|
|
3669
|
+
d.setAttr(key, nextVal, attr2.attribution)
|
|
3577
3670
|
}
|
|
3578
3671
|
/* c8 ignore start */
|
|
3579
3672
|
} else {
|
package/src/delta/rdt/delta.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* `state` is kept as a **final** delta (`isFinal`): it represents the current document, so a `delete`
|
|
9
9
|
* or `deleteAttr` removes the content/attribute outright instead of accumulating a delete-op marker.
|
|
10
|
-
* `
|
|
10
|
+
* The `delta` getter therefore always returns a clean insert-only document.
|
|
11
11
|
*
|
|
12
12
|
* @module delta/rdt/delta
|
|
13
13
|
*/
|
|
@@ -80,7 +80,7 @@ class DeltaRDT extends ObservableV2 {
|
|
|
80
80
|
*
|
|
81
81
|
* @return {D}
|
|
82
82
|
*/
|
|
83
|
-
|
|
83
|
+
get delta () {
|
|
84
84
|
return /** @type {any} */ (this.state ?? delta.create(this.$delta))
|
|
85
85
|
}
|
|
86
86
|
|
package/src/delta/rdt/dom.js
CHANGED
package/src/delta/rdt.js
CHANGED
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
* machinery already transposes the two sides. This repeats until neither side reports a fix, so fixes
|
|
27
27
|
* must converge to a fixpoint (a well-behaved RDT applies them idempotently).
|
|
28
28
|
*
|
|
29
|
-
* On creation a binding first **synchronizes the initial state**: `a`'s current state (`a.
|
|
29
|
+
* On creation a binding first **synchronizes the initial state**: `a`'s current state (`a.delta`)
|
|
30
30
|
* is projected through the transformer, and the projection is diffed against `b`'s current state
|
|
31
|
-
* (`b.
|
|
31
|
+
* (`b.delta`); the resulting difference is applied to `b` so it matches `a`'s projection (and any
|
|
32
32
|
* self-heal correction is applied back to `a`). `a` is the source of truth — pre-existing state on `b`
|
|
33
33
|
* that `a` does not project to is overwritten.
|
|
34
34
|
*
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
import * as dt from './transformer.js'
|
|
44
44
|
import * as delta from './delta.js'
|
|
45
45
|
import * as mux from '../mutex.js'
|
|
46
|
+
import * as s from '../schema.js'
|
|
46
47
|
|
|
47
48
|
// Re-export the two reference RDTs this module's doc references, so `bind`, `deltaRDT` and `domRDT` are
|
|
48
49
|
// reachable from one import. The leaf modules (`lib0/delta/rdt/delta`, `lib0/delta/rdt/dom`) stay
|
|
@@ -60,8 +61,8 @@ export { $domDelta, domRDT } from './rdt/dom.js'
|
|
|
60
61
|
*
|
|
61
62
|
* An RDT is an observable that emits a `'delta'` (and, on teardown, a `'destroy'`) event, exposes the
|
|
62
63
|
* {@link Schema schema} of the deltas it produces via `$delta` (so a {@link Binding} can initialize a
|
|
63
|
-
* transformer for it), exposes its current state as a delta via `
|
|
64
|
-
* via `applyDelta`.
|
|
64
|
+
* transformer for it), exposes its current state as a delta via the `delta` getter, and accepts foreign
|
|
65
|
+
* deltas via `applyDelta`.
|
|
65
66
|
*
|
|
66
67
|
* `applyDelta(d)` applies `d` and then, if the change would violate the RDT's own invariants, applies
|
|
67
68
|
* a **fix** of its own (e.g. reversing part of `d`, or inserting missing content) and returns that fix
|
|
@@ -72,12 +73,28 @@ export { $domDelta, domRDT } from './rdt/dom.js'
|
|
|
72
73
|
* @template {import('./delta.js').DeltaAny} D
|
|
73
74
|
* @typedef {import('../observable.js').ObservableV2<{ delta: (delta: D) => void, destroy: (rdt: RDT<D>) => void }> & {
|
|
74
75
|
* $delta: Schema<D>,
|
|
75
|
-
*
|
|
76
|
+
* delta: D,
|
|
76
77
|
* applyDelta: (delta: D) => D | null,
|
|
77
78
|
* destroy: () => void
|
|
78
79
|
* }} RDT
|
|
79
80
|
*/
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Schema guard for any {@link RDT}. RDTs are **duck-typed** — they have several independent
|
|
84
|
+
* implementations (`deltaRDT`, `domRDT`, …) and no common constructor — so this matches by shape
|
|
85
|
+
* rather than by `instanceof`: an object exposing a `$delta` {@link Schema}, an `applyDelta` method,
|
|
86
|
+
* and the observable `on`/`destroy` channel. The (possibly expensive) `delta` getter is not invoked.
|
|
87
|
+
*
|
|
88
|
+
* @type {Schema<RDT<any>>}
|
|
89
|
+
*/
|
|
90
|
+
export const $rdt = /** @type {Schema<RDT<any>>} */ (/* @__PURE__ */ s.$custom(o =>
|
|
91
|
+
o != null &&
|
|
92
|
+
s.$$schema.check(o.$delta) &&
|
|
93
|
+
typeof o.applyDelta === 'function' &&
|
|
94
|
+
typeof o.on === 'function' &&
|
|
95
|
+
typeof o.destroy === 'function'
|
|
96
|
+
))
|
|
97
|
+
|
|
81
98
|
/**
|
|
82
99
|
* Propagate a pair of concurrent changes between the two sides of `binding` until they converge.
|
|
83
100
|
*
|
|
@@ -140,9 +157,9 @@ export class Binding {
|
|
|
140
157
|
// time are NOT transferred to `b` here; marks ride only on subsequent live `applyA`/`applyB`.
|
|
141
158
|
// Wrapped in the mutex so these `applyDelta` calls don't echo back through the listeners above.
|
|
142
159
|
this._mux(() => {
|
|
143
|
-
const tres = this.t.applyA(this.a.
|
|
160
|
+
const tres = this.t.applyA(this.a.delta)
|
|
144
161
|
const fa = tres.a ? this.a.applyDelta(tres.a) : null
|
|
145
|
-
const diffB = tres.b ? delta.diff(/** @type {delta.DeltaAny} */ (this.b.
|
|
162
|
+
const diffB = tres.b ? delta.diff(/** @type {delta.DeltaAny} */ (this.b.delta), tres.b) : null
|
|
146
163
|
const fb = diffB ? this.b.applyDelta(diffB) : null
|
|
147
164
|
propagate(this, fa, fb)
|
|
148
165
|
})
|