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.
@@ -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;
@@ -46,7 +46,7 @@ declare class DeltaRDT<D extends delta.DeltaAny> extends ObservableV2<{
46
46
  *
47
47
  * @return {D}
48
48
  */
49
- toDelta(): D;
49
+ get delta(): D;
50
50
  }
51
51
  import { ObservableV2 } from '../../observable.js';
52
52
  import * as mux from '../../mutex.js';
@@ -83,7 +83,7 @@ declare class DomRDT<D extends DomDelta = DomDelta> extends ObservableV2<{
83
83
  *
84
84
  * @return {D}
85
85
  */
86
- toDelta(): D;
86
+ get delta(): D;
87
87
  }
88
88
  import { ObservableV2 } from '../../observable.js';
89
89
  export {};
@@ -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 `toDelta`, and accepts foreign deltas
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
- toDelta: () => D;
93
+ delta: D;
59
94
  applyDelta: (delta: D) => D | null;
60
95
  destroy: () => void;
61
96
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lib0",
3
- "version": "1.0.0-rc.16",
3
+ "version": "1.0.0-rc.17",
4
4
  "description": "",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -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
- return {
879
- type: this.type,
880
- value: this.value.toJSON()
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
- this.attrs[key] = /** @type {any} */ (new ModifyAttrOp(key, modify))
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
- if (offset > 0 && opsI != null && op.format != null && !$deleteOp.check(opsI) && (/** @type {InsertOp<any>|RetainOp|ModifyOp} */ (opsI).format == null || !fun.equalityDeep(opsI.format, op.format))) {
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 (!$deleteOp.check(op)) {
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
- // set random attrs
3165
- prng.bool(gen) && d.setAttrs(s.random(gen, $attrs, random))
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
- if (prng.bool(gen)) {
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 formattingNeedsDiff = false
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
- formattingNeedsDiff ||= left1.format != null
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
- formattingNeedsDiff ||= left2.format != null
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 (formattingNeedsDiff) {
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
- if (fun.equalityDeep(bFormat, aFormat)) {
3524
- formattingDiff.retain(minForward)
3525
- } else {
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 fupdate = {}
3612
+ const fu = {}
3530
3613
  bFormat != null && object.forEach(bFormat, (v, k) => {
3531
3614
  if (!fun.equalityDeep(v, aFormat?.[k] || null)) {
3532
- fupdate[k] = v
3615
+ fu[k] = v
3533
3616
  }
3534
3617
  })
3535
3618
  aFormat && object.forEach(aFormat, (_, k) => {
3536
3619
  if (b?.format?.[k] === undefined) {
3537
- fupdate[k] = null
3620
+ fu[k] = null
3538
3621
  }
3539
3622
  })
3540
- formattingDiff.retain(minForward, fupdate)
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
- d.modifyAttr(key, diff(prevVal, nextVal, options))
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 {
@@ -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
- * `toDelta()` therefore always returns a clean insert-only document.
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
- toDelta () {
83
+ get delta () {
84
84
  return /** @type {any} */ (this.state ?? delta.create(this.$delta))
85
85
  }
86
86
 
@@ -277,7 +277,7 @@ class DomRDT extends ObservableV2 {
277
277
  *
278
278
  * @return {D}
279
279
  */
280
- toDelta () {
280
+ get delta () {
281
281
  return /** @type {D} */ (domToDelta(this.observedNode))
282
282
  }
283
283
 
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.toDelta()`)
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.toDelta()`); the resulting difference is applied to `b` so it matches `a`'s projection (and any
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 `toDelta`, and accepts foreign deltas
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
- * toDelta: () => D,
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.toDelta())
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.toDelta()), tres.b) : null
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
  })