lib0 1.0.0-rc.20 → 1.0.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,7 +55,7 @@ export const $attribution: s.Schema<Attribution>;
55
55
  * @typedef {{ type: 'insert', insert: string|Array<any>, format?: { [key: string]: any }, attribution?: Attribution } | { delete: number } | { type: 'retain', retain: number, format?: { [key:string]: any }|null, attribution?: Attribution|null } | { type: 'modify', value: object, format?: { [key:string]: any }|null, attribution?: Attribution|null }} DeltaListOpJSON
56
56
  */
57
57
  /**
58
- * @typedef {{ type: 'insert', value: any, prevValue?: any, attribution?: Attribution } | { type: 'delete', prevValue?: any, attribution?: Attribution } | { type: 'modify', value: DeltaJSON, attribution?: Attribution|null }} DeltaAttrOpJSON
58
+ * @typedef {{ type: 'insert', value: any, attribution?: Attribution } | { type: 'delete', attribution?: Attribution } | { type: 'modify', value: DeltaJSON, attribution?: Attribution|null }} DeltaAttrOpJSON
59
59
  */
60
60
  /**
61
61
  * @typedef {TextOp|InsertOp<any>|DeleteOp|RetainOp|ModifyOp<any>} ChildrenOpAny
@@ -224,11 +224,9 @@ export class InsertOp<ArrayContent extends unknown> extends list.ListNode {
224
224
  export class DeleteOp<Conf extends DeltaConf = {}> extends list.ListNode {
225
225
  /**
226
226
  * @param {number} len
227
- * @param {DeltaBuilder<any>?} prevValue
228
227
  */
229
- constructor(len: number, prevValue: DeltaBuilder<any> | null);
228
+ constructor(len: number);
230
229
  delete: number;
231
- prevValue: Delta<Conf> | null;
232
230
  /**
233
231
  * @type {string|null}
234
232
  */
@@ -242,10 +240,10 @@ export class DeleteOp<Conf extends DeltaConf = {}> extends list.ListNode {
242
240
  /**
243
241
  * Remove a part of the operation (similar to Array.splice)
244
242
  *
245
- * @param {number} offset
243
+ * @param {number} _offset
246
244
  * @param {number} len
247
245
  */
248
- _splice(offset: number, len: number): this;
246
+ _splice(_offset: number, len: number): this;
249
247
  /**
250
248
  * @return {DeltaListOpJSON}
251
249
  */
@@ -254,7 +252,7 @@ export class DeleteOp<Conf extends DeltaConf = {}> extends list.ListNode {
254
252
  * @param {number} [start]
255
253
  * @param {number} [end]
256
254
  * @param {boolean} [_markAsDone] accepted for a uniform children-op `clone` signature; ignored (a
257
- * cloned delete carries no prevValue).
255
+ * delete holds no nested content).
258
256
  * @return {DeleteOp}
259
257
  */
260
258
  clone(start?: number, end?: number, _markAsDone?: boolean): DeleteOp;
@@ -400,10 +398,9 @@ export class SetAttrOp<V extends unknown = any, K extends string | number = any>
400
398
  /**
401
399
  * @param {K} key
402
400
  * @param {V} value
403
- * @param {V|undefined} prevValue
404
401
  * @param {Attribution?} attribution
405
402
  */
406
- constructor(key: K, value: V, prevValue: V | undefined, attribution: Attribution | null);
403
+ constructor(key: K, value: V, attribution: Attribution | null);
407
404
  /**
408
405
  * @readonly
409
406
  * @type {K}
@@ -414,11 +411,6 @@ export class SetAttrOp<V extends unknown = any, K extends string | number = any>
414
411
  * @type {V}
415
412
  */
416
413
  readonly value: V;
417
- /**
418
- * @readonly
419
- * @type {V|undefined}
420
- */
421
- readonly prevValue: V | undefined;
422
414
  /**
423
415
  * @readonly
424
416
  * @type {Attribution?}
@@ -466,18 +458,13 @@ export class SetAttrOp<V extends unknown = any, K extends string | number = any>
466
458
  export class DeleteAttrOp<V = any, K extends string | number = string | number> {
467
459
  /**
468
460
  * @param {K} key
469
- * @param {V|undefined} prevValue
470
461
  * @param {Attribution?} attribution
471
462
  */
472
- constructor(key: K, prevValue: V | undefined, attribution: Attribution | null);
463
+ constructor(key: K, attribution: Attribution | null);
473
464
  /**
474
465
  * @type {K}
475
466
  */
476
467
  key: K;
477
- /**
478
- * @type {V|undefined}
479
- */
480
- prevValue: V | undefined;
481
468
  attribution: Attribution | null;
482
469
  /**
483
470
  * @type {string|null}
@@ -654,20 +641,72 @@ export class DeltaBuilder<Conf extends DeltaConf = {}, FixedConf extends boolean
654
641
  /**
655
642
  * @type {Formats?}
656
643
  */
657
- usedFormats: Formats | null;
644
+ _usedFormats: Formats | null;
658
645
  /**
659
646
  * @type {Attribution?}
660
647
  */
661
- usedAttribution: Attribution | null;
648
+ _usedAttribution: Attribution | null;
649
+ /**
650
+ * Resolved data-companion cache for the `used*` contexts ({@link resolveUsedData}): what data
651
+ * ops inherit instead of the raw context. `undefined` = stale, recomputed on the next data-op
652
+ * consumption ({@link DeltaBuilder#_getUsedFormatsData}) — {@link DeltaBuilder#useFormats} & co
653
+ * clear it whenever they swap the context (which is why the public slots are read-only).
654
+ *
655
+ * @type {Formats|null|undefined}
656
+ */
657
+ _usedFormatsData: Formats | null | undefined;
658
+ /**
659
+ * @type {Attribution|null|undefined}
660
+ */
661
+ _usedAttributionData: Attribution | null | undefined;
662
+ /**
663
+ * The ambient formats context inherited by subsequent ops. Read-only: set it via
664
+ * {@link DeltaBuilder#useFormats} / {@link DeltaBuilder#updateUsedFormats}, which also invalidate
665
+ * the cached data companion — a direct overwrite would leave it stale.
666
+ *
667
+ * @return {Formats?}
668
+ */
669
+ get usedFormats(): Formats | null;
662
670
  /**
671
+ * The ambient attribution context inherited by subsequent ops. Read-only — see
672
+ * {@link DeltaBuilder#usedFormats}; set it via {@link DeltaBuilder#useAttribution} /
673
+ * {@link DeltaBuilder#updateUsedAttribution}.
674
+ *
675
+ * @return {Attribution?}
676
+ */
677
+ get usedAttribution(): Attribution | null;
678
+ /**
679
+ * Set the ambient attribution context inherited by subsequent ops (`undefined`/empty dim args).
680
+ * The object is interned — instruction ops store it, and data ops store its resolved companion
681
+ * ({@link resolveUsedData}), BY REFERENCE. Never mutate it after setting; pass a fresh object
682
+ * instead (same invariant as the objects stored on ops).
683
+ *
663
684
  * @param {Attribution?} attribution
664
685
  */
665
686
  useAttribution(attribution: Attribution | null): this;
666
687
  /**
688
+ * Set the ambient formats context inherited by subsequent ops (`undefined`/empty dim args). The
689
+ * object is interned — see {@link DeltaBuilder#useAttribution}: never mutate it after setting.
690
+ *
667
691
  * @param {Formats?} attributes
668
692
  * @return {this}
669
693
  */
670
694
  useFormats(attributes: Formats | null): this;
695
+ /**
696
+ * The `usedFormats` context resolved for data-op consumption ({@link resolveUsedData}): the slot
697
+ * object itself when it carries no `{k:null}` removals, one shared stripped copy otherwise.
698
+ * Cached until the context is swapped — no per-op traversal.
699
+ *
700
+ * @return {Formats?}
701
+ */
702
+ _getUsedFormatsData(): Formats | null;
703
+ /**
704
+ * See {@link DeltaBuilder#_getUsedFormatsData} — the `usedAttribution` analogue (its `format` key
705
+ * resolves one level deeper).
706
+ *
707
+ * @return {Attribution?}
708
+ */
709
+ _getUsedAttributionData(): Attribution | null;
671
710
  /**
672
711
  * @param {string} name
673
712
  * @param {any} value
@@ -736,19 +775,17 @@ export class DeltaBuilder<Conf extends DeltaConf = {}, FixedConf extends boolean
736
775
  retain(len: number, format?: Formats | null, attribution?: Attribution | null): this;
737
776
  /**
738
777
  * @param {number} len
739
- * @param {DeltaBuilder<any>|null} prevValue
740
778
  */
741
- delete(len: number, prevValue?: DeltaBuilder<any> | null): this;
779
+ delete(len: number): this;
742
780
  /**
743
781
  * @template {Extract<keyof DeltaConfGetAllowedAttrs<Conf, FixedConf>,string|number>} Key
744
782
  * @template {DeltaConfGetAllowedAttrs<Conf, FixedConf>[Key]} Val
745
783
  * @param {Key} key
746
784
  * @param {Val} val
747
785
  * @param {Attribution?} [attribution] provenance for this attr (data: object or `null`/none)
748
- * @param {Val|undefined} [prevValue]
749
786
  * @return {DeltaBuilder<DeltaConfOverwrite<Conf,{attrs:AddToAttrs<DeltaConfGetAttrs<Conf>,Key,Val>}>, FixedConf>}
750
787
  */
751
- setAttr<Key extends Extract<keyof DeltaConfGetAllowedAttrs<Conf, FixedConf>, string | number>, Val extends DeltaConfGetAllowedAttrs<Conf, FixedConf>[Key]>(key: Key, val: Val, attribution?: Attribution | null, prevValue?: Val | undefined): DeltaBuilder<DeltaConfOverwrite<Conf, {
788
+ setAttr<Key extends Extract<keyof DeltaConfGetAllowedAttrs<Conf, FixedConf>, string | number>, Val extends DeltaConfGetAllowedAttrs<Conf, FixedConf>[Key]>(key: Key, val: Val, attribution?: Attribution | null): DeltaBuilder<DeltaConfOverwrite<Conf, {
752
789
  attrs: AddToAttrs<DeltaConfGetAttrs<Conf>, Key, Val>;
753
790
  }>, FixedConf>;
754
791
  /**
@@ -768,12 +805,11 @@ export class DeltaBuilder<Conf extends DeltaConf = {}, FixedConf extends boolean
768
805
  * @template {Extract<keyof DeltaConfGetAllowedAttrs<Conf, FixedConf>,string|number>} Key
769
806
  * @param {Key} key
770
807
  * @param {Attribution?} [attribution] provenance for the deletion (data: object or `null`/none)
771
- * @param {any} [prevValue]
772
808
  * @return {DeltaBuilder<DeltaConfOverwrite<Conf, {
773
809
  * attrs: AddToAttrs<DeltaConfGetAttrs<Conf>,Key,never>
774
810
  * }>, FixedConf>}
775
811
  */
776
- deleteAttr<Key extends Extract<keyof DeltaConfGetAllowedAttrs<Conf, FixedConf>, string | number>>(key: Key, attribution?: Attribution | null, prevValue?: any): DeltaBuilder<DeltaConfOverwrite<Conf, {
812
+ deleteAttr<Key extends Extract<keyof DeltaConfGetAllowedAttrs<Conf, FixedConf>, string | number>>(key: Key, attribution?: Attribution | null): DeltaBuilder<DeltaConfOverwrite<Conf, {
777
813
  attrs: AddToAttrs<DeltaConfGetAttrs<Conf>, Key, never>;
778
814
  }>, FixedConf>;
779
815
  /**
@@ -1050,6 +1086,7 @@ export function from<NodeName extends string | null, Attrs extends {
1050
1086
  text: Extract<Children, string> extends never ? false : true;
1051
1087
  }>;
1052
1088
  export function diff<Conf_1 extends DeltaConf>(d1: Delta<Conf_1>, d2: NoInfer<Delta<Conf_1>>, options?: DiffOptions): Delta<Conf_1>;
1089
+ export function inverse<Conf_1 extends DeltaConf>(d: DeltaAny, base: Delta<Conf_1>): Delta<Conf_1>;
1053
1090
  export function diffChangesetWithSeparator(changeset: Array<{
1054
1091
  index: number;
1055
1092
  remove: Array<any>;
@@ -1132,11 +1169,9 @@ export type DeltaListOpJSON = {
1132
1169
  export type DeltaAttrOpJSON = {
1133
1170
  type: "insert";
1134
1171
  value: any;
1135
- prevValue?: any;
1136
1172
  attribution?: Attribution;
1137
1173
  } | {
1138
1174
  type: "delete";
1139
- prevValue?: any;
1140
1175
  attribution?: Attribution;
1141
1176
  } | {
1142
1177
  type: "modify";
@@ -62,6 +62,12 @@ export { deltaRDT } from "./rdt/delta.js";
62
62
  * @type {Schema<RDT<any>>}
63
63
  */
64
64
  export const $rdt: Schema<RDT<any>>;
65
+ /**
66
+ * @typedef {object} BindOptions
67
+ * @property {delta.DiffOptions['compare']} [diffCompare] **Experimental** (may be changed or removed
68
+ * in a future release). Forwarded as `compare` to {@link delta.diff} when the initial-state sync diffs
69
+ * `a`'s projection against `b`'s current state, controlling how nodes are paired into `modify` ops.
70
+ */
65
71
  /**
66
72
  * Connects two RDTs so that changes on either side are transformed and reflected on the other.
67
73
  *
@@ -81,8 +87,9 @@ export class Binding<A extends delta.DeltaConf, B extends delta.DeltaConf> {
81
87
  * @param {RDT<B>} b
82
88
  * @param {dt.TemplateFactory<A,B>} [template] a `$d => Template` factory; defaults to the
83
89
  * {@link dt.id identity} transformer. `bind` injects `a`'s schema, then materializes via `.init()`.
90
+ * @param {BindOptions} [options]
84
91
  */
85
- constructor(a: RDT<A>, b: RDT<B>, template?: dt.TemplateFactory<A, B>);
92
+ constructor(a: RDT<A>, b: RDT<B>, template?: dt.TemplateFactory<A, B>, options?: BindOptions);
86
93
  /**
87
94
  * @type {dt.Transformer<A,B>}
88
95
  */
@@ -105,7 +112,7 @@ export class Binding<A extends delta.DeltaConf, B extends delta.DeltaConf> {
105
112
  */
106
113
  destroy: () => void;
107
114
  }
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>;
115
+ export function bind<A extends delta.DeltaConf, B extends delta.DeltaConf>(a: RDT<A>, b: RDT<B>, template?: dt.TemplateFactory<A, B>, options?: BindOptions): Binding<A, B>;
109
116
  export type Schema<T> = import("../schema.js").Schema<T>;
110
117
  /**
111
118
  * Abstract interface for a delta-based Replicated Data Type.
@@ -158,6 +165,14 @@ export type RDT<Conf extends import("./delta.js").DeltaConf> = import("../observ
158
165
  applyDelta: (delta: delta.Delta<Conf>, origin?: any) => delta.DeltaBuilder<Conf> | null;
159
166
  destroy: () => void;
160
167
  };
168
+ export type BindOptions = {
169
+ /**
170
+ * **Experimental** (may be changed or removed
171
+ * in a future release). Forwarded as `compare` to {@link delta.diff} when the initial-state sync diffs
172
+ * `a`'s projection against `b`'s current state, controlling how nodes are paired into `modify` ops.
173
+ */
174
+ diffCompare?: delta.DiffOptions["compare"];
175
+ };
161
176
  import * as delta from './delta.js';
162
177
  import * as dt from './transformer.js';
163
178
  import * as mux from '../mutex.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lib0",
3
- "version": "1.0.0-rc.20",
3
+ "version": "1.0.0-rc.21",
4
4
  "description": "",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -6,7 +6,7 @@
6
6
  * This module exports a large surface, but a consumer only needs a handful of it:
7
7
  * - **build / apply / inspect:** {@link create} (the constructor) and the {@link DeltaBuilder} methods
8
8
  * it returns (`insert`/`delete`/`retain`/`modify`/`setAttr`/`addMark`/…, `apply`, `rebase`, `done`);
9
- * {@link clone}, {@link slice}, {@link diff}, and `toJSON`/`equals`/`isEmpty` on the result.
9
+ * {@link clone}, {@link slice}, {@link diff}, {@link inverse}, and `toJSON`/`equals`/`isEmpty` on the result.
10
10
  * - **schemas:** {@link $delta} (define a typed delta schema) and {@link $deltaAny} (the catch-all).
11
11
  * - **types:** `Delta`, `DeltaBuilder`, `DeltaAny`, `DeltaConf` for annotations.
12
12
  *
@@ -109,7 +109,7 @@ export const $attribution = /* @__PURE__ */(() => s.$object({
109
109
  */
110
110
 
111
111
  /**
112
- * @typedef {{ type: 'insert', value: any, prevValue?: any, attribution?: Attribution } | { type: 'delete', prevValue?: any, attribution?: Attribution } | { type: 'modify', value: DeltaJSON, attribution?: Attribution|null }} DeltaAttrOpJSON
112
+ * @typedef {{ type: 'insert', value: any, attribution?: Attribution } | { type: 'delete', attribution?: Attribution } | { type: 'modify', value: DeltaJSON, attribution?: Attribution|null }} DeltaAttrOpJSON
113
113
  */
114
114
 
115
115
  /**
@@ -128,9 +128,9 @@ export const $attribution = /* @__PURE__ */(() => s.$object({
128
128
  * @type {s.Schema<DeltaAttrOpJSON>}
129
129
  */
130
130
  export const $deltaMapChangeJson = /* @__PURE__ */(() => s.$union(
131
- s.$object({ type: s.$literal('insert'), value: s.$any, prevValue: s.$any.optional, attribution: $attribution.optional }),
131
+ s.$object({ type: s.$literal('insert'), value: s.$any, attribution: $attribution.optional }),
132
132
  s.$object({ type: s.$literal('modify'), value: s.$any, attribution: $attribution.optional }),
133
- s.$object({ type: s.$literal('delete'), prevValue: s.$any.optional, attribution: $attribution.optional })
133
+ s.$object({ type: s.$literal('delete'), attribution: $attribution.optional })
134
134
  ))()
135
135
 
136
136
  /**
@@ -144,8 +144,10 @@ const _cloneAttrs = attrs => attrs == null ? attrs : { ...attrs }
144
144
  * as the inner step of {@link mergeAttr}). Per key: `undefined` skips, `null` removes, anything else sets.
145
145
  * `resolve` true (data semantics) applies a `null` removal by deleting the key; false (instruction semantics)
146
146
  * keeps the `null` verbatim so it still removes when the instruction is later applied. When `resolve` is true
147
- * the `base` is canonicalised too — a `{k:null}` already present in `base` (e.g. from a `useAttribution`
148
- * context) has nothing to clear on settled data, so it is dropped rather than copied through.
147
+ * the `base` is canonicalised too — a `{k:null}` already present in `base` has nothing to clear on settled
148
+ * data, so it is dropped rather than copied through. (The used-context call sites pass pre-resolved
149
+ * companions — see {@link resolveUsedData} — so for them this is a no-op safety net; it matters for the
150
+ * apply/diff paths that merge instructions onto stored values.)
149
151
  *
150
152
  * @param {{[k:string]:any}|null|undefined} base
151
153
  * @param {{[k:string]:any}} update
@@ -225,46 +227,83 @@ const resolveBeyond = (arg, deep) => {
225
227
  return object.isEmpty(m) ? undefined : m
226
228
  }
227
229
  /**
228
- * Combine a builder's `used*` context (set via {@link DeltaBuilder#useFormats}/`useAttribution`) with
229
- * a per-call format/attribution argument under the unified tri-state: `undefined` inherits the context
230
- * (or stays `undefined` "skip" when there is none); `null` clears (ignoring the context); an object
231
- * merges over the context. `resolve` true (data op) applies any `{k:null}` removal against the context so the
232
- * stored value is canonical (no `null` leaves); false (instruction op) keeps removals verbatim for later.
230
+ * Resolve a `used*` context (set via {@link DeltaBuilder#useFormats}/`useAttribution`) for **data**
231
+ * consumption: settled data stores canonical values only
232
+ * (no `null` leavessee {@link combineData}), while the slot itself keeps removal instructions
233
+ * verbatim for the instruction ops it governs. Returns the SAME object when it is already canonical
234
+ * (the common case provenance contexts carry no removals), so every data op under one context shares
235
+ * one interned object; otherwise strips the removals ONCE into a shared copy (`{}` collapses to `null`).
236
+ * Never called per op — {@link DeltaBuilder#_getUsedFormatsData} caches the result per context object.
233
237
  *
234
238
  * @param {{[k:string]:any}|null} used
235
- * @param {{[k:string]:any}|null|undefined} arg
236
- * @param {boolean} deep `true` for the `attribution` dimension (its `format` key merges per inner key via
237
- * {@link mergeAttr}); `false` for `format` (shallow, {@link mergeShallow}).
238
- * @param {boolean} resolve see above ({@link mergeShallow})
239
- * @return {{[k:string]:any}|null|undefined}
239
+ * @param {boolean} deep `true` for the `attribution` dimension (its `format` key resolves one level
240
+ * deeper via {@link mergeAttr}); `false` for `format` (shallow, {@link mergeShallow})
241
+ * @return {{[k:string]:any}|null}
240
242
  */
241
- const combineUsed = (used, arg, deep, resolve) =>
242
- arg === undefined
243
- ? (used === null ? undefined : used)
244
- : arg === null
245
- ? null
246
- : (deep ? mergeAttr(used, arg, resolve) : mergeShallow(used, arg, resolve))
243
+ const resolveUsedData = (used, deep) => {
244
+ if (used === null) return null
245
+ let empty = true
246
+ let canonical = true
247
+ for (const k in used) {
248
+ empty = false
249
+ const v = used[k]
250
+ if (v === null) { canonical = false; break } // a top-level removal (incl. `format: null`)
251
+ if (deep && k === 'format' && s.$objectAny.check(v)) {
252
+ // mirror mergeAttr's resolve-mode base handling: an emptied `format` is dropped on data, and an
253
+ // inner `{k:null}` removal is stripped — either makes the context non-canonical for data ops
254
+ let formatCanonical = false // empty `format: {}` ⇒ non-canonical
255
+ for (const ik in v) {
256
+ if (v[ik] === null) { formatCanonical = false; break }
257
+ formatCanonical = true
258
+ }
259
+ if (!formatCanonical) { canonical = false; break }
260
+ }
261
+ }
262
+ if (empty) return null
263
+ if (canonical) return used
264
+ const m = deep ? mergeAttr(used, {}, true) : mergeShallow(used, {}, true)
265
+ return object.isEmpty(m) ? null : m
266
+ }
247
267
  /**
248
- * Combine + normalize for an **instruction** op (`retain`/`modify`/`modifyAttr`): keep the tri-state
249
- * `undefined` (skip), `null` (clear), or a non-empty object; an empty merge result `{}` is a no-op → `undefined`.
268
+ * Combine a builder's `used*` context with a per-call format/attribution argument for an
269
+ * **instruction** op (`retain`/`modify`/`modifyAttr`) under the unified tri-state: `undefined` or an
270
+ * empty object, an empty update — inherits the context BY IDENTITY (no traversal, no copy; contexts
271
+ * are interned); `null` clears (ignoring the context); a non-empty object merges over the context,
272
+ * keeping `{k:null}` removals verbatim so they still remove when the instruction is later applied.
273
+ * An empty result is a no-op → `undefined` (skip).
250
274
  *
251
- * @param {{[k:string]:any}|null} used
275
+ * @param {{[k:string]:any}|null} used the raw context slot (removal instructions intact)
252
276
  * @param {{[k:string]:any}|null|undefined} arg
253
- * @param {boolean} deep see {@link combineUsed}
277
+ * @param {boolean} deep `true` for the `attribution` dimension (its `format` key merges per inner key via
278
+ * {@link mergeAttr}); `false` for `format` (shallow, {@link mergeShallow}).
254
279
  * @return {{[k:string]:any}|null|undefined}
255
280
  */
256
- const combineInstr = (used, arg, deep) => { const r = combineUsed(used, arg, deep, false); return r !== null && object.isEmpty(r) ? undefined : r }
281
+ const combineInstr = (used, arg, deep) => {
282
+ if (arg === null) return null // explicit clear — must precede the isEmpty check (object.isEmpty(null) is true)
283
+ if (arg === undefined || object.isEmpty(arg)) return used === null || object.isEmpty(used) ? undefined : used
284
+ const r = deep ? mergeAttr(used, arg, false) : mergeShallow(used, arg, false)
285
+ return object.isEmpty(r) ? undefined : r
286
+ }
257
287
  /**
258
- * Combine + normalize for a **data** op (`insert`/`text`/`setAttr`/`deleteAttr`): the stored value is a
259
- * canonical object or `null` ("none") — `{k:null}` removals are resolved against the context, and
260
- * `undefined`/`null`/`{}` all collapse to `null`.
288
+ * Combine for a **data** op (`insert`/`text`/`setAttr`/`deleteAttr`): the stored value is a canonical
289
+ * object or `null` ("none") — never a `{k:null}` removal, there is nothing to remove on settled data.
290
+ * `usedData` is the builder's RESOLVED context companion ({@link resolveUsedData}, cached by
291
+ * {@link DeltaBuilder#_getUsedFormatsData}) and thus already canonical (`null` or a non-empty
292
+ * no-`null` object), so `undefined` — or an empty object, an empty update — inherits it BY IDENTITY
293
+ * (no traversal, no copy); `null` clears; a non-empty object merges over it, resolving the argument's
294
+ * own `{k:null}` removals. `{}` results collapse to `null`.
261
295
  *
262
- * @param {{[k:string]:any}|null} used
296
+ * @param {{[k:string]:any}|null} usedData
263
297
  * @param {{[k:string]:any}|null|undefined} arg
264
- * @param {boolean} deep see {@link combineUsed}
298
+ * @param {boolean} deep see {@link combineInstr}
265
299
  * @return {{[k:string]:any}|null}
266
300
  */
267
- const combineData = (used, arg, deep) => { const r = combineUsed(used, arg, deep, true); return object.isEmpty(r) ? null : /** @type {{[k:string]:any}} */ (r) }
301
+ const combineData = (usedData, arg, deep) => {
302
+ if (arg === null) return null // explicit clear — must precede the isEmpty check (object.isEmpty(null) is true)
303
+ if (arg === undefined || object.isEmpty(arg)) return usedData
304
+ const r = deep ? mergeAttr(usedData, arg, true) : mergeShallow(usedData, arg, true)
305
+ return object.isEmpty(r) ? null : /** @type {{[k:string]:any}} */ (r)
306
+ }
268
307
  /**
269
308
  * @template {any} MaybeDelta
270
309
  * @param {MaybeDelta} maybeDelta
@@ -531,12 +570,10 @@ export class InsertOp extends list.ListNode {
531
570
  export class DeleteOp extends list.ListNode {
532
571
  /**
533
572
  * @param {number} len
534
- * @param {DeltaBuilder<any>?} prevValue
535
573
  */
536
- constructor (len, prevValue) {
574
+ constructor (len) {
537
575
  super()
538
576
  this.delete = len
539
- this.prevValue = /** @type {Delta<Conf>?} */ (prevValue)
540
577
  /**
541
578
  * @type {string|null}
542
579
  */
@@ -564,13 +601,10 @@ export class DeleteOp extends list.ListNode {
564
601
  /**
565
602
  * Remove a part of the operation (similar to Array.splice)
566
603
  *
567
- * @param {number} offset
604
+ * @param {number} _offset
568
605
  * @param {number} len
569
606
  */
570
- _splice (offset, len) {
571
- if (this.prevValue) {
572
- /** @type {DeltaBuilder<any>} */ (this.prevValue).apply(create().retain(offset).delete(len))
573
- }
607
+ _splice (_offset, len) {
574
608
  this._fingerprint = null
575
609
  this.delete -= len
576
610
  return this
@@ -594,11 +628,11 @@ export class DeleteOp extends list.ListNode {
594
628
  * @param {number} [start]
595
629
  * @param {number} [end]
596
630
  * @param {boolean} [_markAsDone] accepted for a uniform children-op `clone` signature; ignored (a
597
- * cloned delete carries no prevValue).
631
+ * delete holds no nested content).
598
632
  * @return {DeleteOp}
599
633
  */
600
634
  clone (start = 0, end = this.delete, _markAsDone = true) {
601
- return new DeleteOp(end - start, null)
635
+ return new DeleteOp(end - start)
602
636
  }
603
637
  }
604
638
 
@@ -822,10 +856,9 @@ export class SetAttrOp {
822
856
  /**
823
857
  * @param {K} key
824
858
  * @param {V} value
825
- * @param {V|undefined} prevValue
826
859
  * @param {Attribution?} attribution
827
860
  */
828
- constructor (key, value, prevValue, attribution) {
861
+ constructor (key, value, attribution) {
829
862
  /**
830
863
  * @readonly
831
864
  * @type {K}
@@ -836,11 +869,6 @@ export class SetAttrOp {
836
869
  * @type {V}
837
870
  */
838
871
  this.value = value
839
- /**
840
- * @readonly
841
- * @type {V|undefined}
842
- */
843
- this.prevValue = prevValue
844
872
  /**
845
873
  * @readonly
846
874
  * @type {Attribution?}
@@ -905,7 +933,7 @@ export class SetAttrOp {
905
933
  * @return {SetAttrOp<V,K>}
906
934
  */
907
935
  clone () {
908
- return new SetAttrOp(this.key, _markMaybeDeltaAsDone(this.value), _markMaybeDeltaAsDone(this.prevValue), _cloneAttrs(this.attribution))
936
+ return new SetAttrOp(this.key, _markMaybeDeltaAsDone(this.value), _cloneAttrs(this.attribution))
909
937
  }
910
938
  }
911
939
 
@@ -917,18 +945,13 @@ export class SetAttrOp {
917
945
  export class DeleteAttrOp {
918
946
  /**
919
947
  * @param {K} key
920
- * @param {V|undefined} prevValue
921
948
  * @param {Attribution?} attribution
922
949
  */
923
- constructor (key, prevValue, attribution) {
950
+ constructor (key, attribution) {
924
951
  /**
925
952
  * @type {K}
926
953
  */
927
954
  this.key = key
928
- /**
929
- * @type {V|undefined}
930
- */
931
- this.prevValue = prevValue
932
955
  this.attribution = attribution
933
956
  /**
934
957
  * @type {string|null}
@@ -977,7 +1000,7 @@ export class DeleteAttrOp {
977
1000
  * @return {DeleteAttrOp<V,K>}
978
1001
  */
979
1002
  clone () {
980
- return new DeleteAttrOp(this.key, _markMaybeDeltaAsDone(this.prevValue), _cloneAttrs(this.attribution))
1003
+ return new DeleteAttrOp(this.key, _cloneAttrs(this.attribution))
981
1004
  }
982
1005
  }
983
1006
 
@@ -1609,7 +1632,7 @@ const _cloneMaybeDeltaDeep = v => $deltaAny.check(v) ? cloneDeep(v) : v
1609
1632
  * Deep-clone one content (child) op for {@link cloneDeep}: a fresh op whose nested deltas (an insert's
1610
1633
  * delta content, a modify's value) are themselves deep-cloned. `format`/`attribution` objects are
1611
1634
  * 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()`.
1635
+ * nested delta (`text`/`retain`/`delete`) use `op.clone()`.
1613
1636
  *
1614
1637
  * @param {ChildrenOpAny} op
1615
1638
  * @return {ChildrenOpAny}
@@ -1623,7 +1646,7 @@ const _cloneChildOpDeep = op =>
1623
1646
 
1624
1647
  /**
1625
1648
  * 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
1649
+ * {@link _cloneChildOpDeep}: a `setAttr`'s value and a `modifyAttr`'s value may be nested
1627
1650
  * deltas and are deep-cloned.
1628
1651
  *
1629
1652
  * @param {AttrOpAny} op
@@ -1631,11 +1654,11 @@ const _cloneChildOpDeep = op =>
1631
1654
  */
1632
1655
  const _cloneAttrOpDeep = op =>
1633
1656
  $setAttrOp.check(op)
1634
- ? new SetAttrOp(op.key, _cloneMaybeDeltaDeep(op.value), _cloneMaybeDeltaDeep(op.prevValue), op.attribution)
1657
+ ? new SetAttrOp(op.key, _cloneMaybeDeltaDeep(op.value), op.attribution)
1635
1658
  : ($modifyAttrOp.check(op)
1636
1659
  ? 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))
1660
+ // the only remaining attr op is a deleteAttr
1661
+ : new DeleteAttrOp(op.key, op.attribution))
1639
1662
 
1640
1663
  /**
1641
1664
  * A **deep** clone of `d`: like {@link clone}, but every nested delta — an insert's delta content, a
@@ -1919,32 +1942,100 @@ export class DeltaBuilder extends Delta {
1919
1942
  /**
1920
1943
  * @type {Formats?}
1921
1944
  */
1922
- this.usedFormats = null
1945
+ this._usedFormats = null
1923
1946
  /**
1924
1947
  * @type {Attribution?}
1925
1948
  */
1926
- this.usedAttribution = null
1949
+ this._usedAttribution = null
1950
+ /**
1951
+ * Resolved data-companion cache for the `used*` contexts ({@link resolveUsedData}): what data
1952
+ * ops inherit instead of the raw context. `undefined` = stale, recomputed on the next data-op
1953
+ * consumption ({@link DeltaBuilder#_getUsedFormatsData}) — {@link DeltaBuilder#useFormats} & co
1954
+ * clear it whenever they swap the context (which is why the public slots are read-only).
1955
+ *
1956
+ * @type {Formats|null|undefined}
1957
+ */
1958
+ this._usedFormatsData = undefined
1959
+ /**
1960
+ * @type {Attribution|null|undefined}
1961
+ */
1962
+ this._usedAttributionData = undefined
1927
1963
  }
1928
1964
 
1929
1965
  /**
1966
+ * The ambient formats context inherited by subsequent ops. Read-only: set it via
1967
+ * {@link DeltaBuilder#useFormats} / {@link DeltaBuilder#updateUsedFormats}, which also invalidate
1968
+ * the cached data companion — a direct overwrite would leave it stale.
1969
+ *
1970
+ * @return {Formats?}
1971
+ */
1972
+ get usedFormats () { return this._usedFormats }
1973
+
1974
+ /**
1975
+ * The ambient attribution context inherited by subsequent ops. Read-only — see
1976
+ * {@link DeltaBuilder#usedFormats}; set it via {@link DeltaBuilder#useAttribution} /
1977
+ * {@link DeltaBuilder#updateUsedAttribution}.
1978
+ *
1979
+ * @return {Attribution?}
1980
+ */
1981
+ get usedAttribution () { return this._usedAttribution }
1982
+
1983
+ /**
1984
+ * Set the ambient attribution context inherited by subsequent ops (`undefined`/empty dim args).
1985
+ * The object is interned — instruction ops store it, and data ops store its resolved companion
1986
+ * ({@link resolveUsedData}), BY REFERENCE. Never mutate it after setting; pass a fresh object
1987
+ * instead (same invariant as the objects stored on ops).
1988
+ *
1930
1989
  * @param {Attribution?} attribution
1931
1990
  */
1932
1991
  useAttribution (attribution) {
1933
1992
  modDeltaCheck(this)
1934
- this.usedAttribution = attribution
1993
+ if (this._usedAttribution !== attribution) {
1994
+ this._usedAttribution = attribution
1995
+ this._usedAttributionData = undefined // clear the data companion — recomputed on next consumption
1996
+ }
1935
1997
  return this
1936
1998
  }
1937
1999
 
1938
2000
  /**
2001
+ * Set the ambient formats context inherited by subsequent ops (`undefined`/empty dim args). The
2002
+ * object is interned — see {@link DeltaBuilder#useAttribution}: never mutate it after setting.
2003
+ *
1939
2004
  * @param {Formats?} attributes
1940
2005
  * @return {this}
1941
2006
  */
1942
2007
  useFormats (attributes) {
1943
2008
  modDeltaCheck(this)
1944
- this.usedFormats = attributes
2009
+ if (this._usedFormats !== attributes) {
2010
+ this._usedFormats = attributes
2011
+ this._usedFormatsData = undefined // clear the data companion — recomputed on next consumption
2012
+ }
1945
2013
  return this
1946
2014
  }
1947
2015
 
2016
+ /**
2017
+ * The `usedFormats` context resolved for data-op consumption ({@link resolveUsedData}): the slot
2018
+ * object itself when it carries no `{k:null}` removals, one shared stripped copy otherwise.
2019
+ * Cached until the context is swapped — no per-op traversal.
2020
+ *
2021
+ * @return {Formats?}
2022
+ */
2023
+ _getUsedFormatsData () {
2024
+ const d = this._usedFormatsData
2025
+ return d !== undefined ? d : (this._usedFormatsData = /** @type {Formats?} */ (resolveUsedData(this._usedFormats, false)))
2026
+ }
2027
+
2028
+ /**
2029
+ * See {@link DeltaBuilder#_getUsedFormatsData} — the `usedAttribution` analogue (its `format` key
2030
+ * resolves one level deeper).
2031
+ *
2032
+ * @return {Attribution?}
2033
+ */
2034
+ _getUsedAttributionData () {
2035
+ const d = this._usedAttributionData
2036
+ return d !== undefined ? d : (this._usedAttributionData = /** @type {Attribution?} */ (resolveUsedData(this._usedAttribution, true)))
2037
+ }
2038
+
1948
2039
  /**
1949
2040
  * @param {string} name
1950
2041
  * @param {any} value
@@ -1952,14 +2043,16 @@ export class DeltaBuilder extends Delta {
1952
2043
  updateUsedFormats (name, value) {
1953
2044
  modDeltaCheck(this)
1954
2045
  if (value == null) {
1955
- this.usedFormats = object.assign({}, this.usedFormats)
1956
- delete this.usedFormats?.[name]
1957
- if (object.isEmpty(this.usedFormats)) {
1958
- this.usedFormats = null
2046
+ this._usedFormats = object.assign({}, this._usedFormats)
2047
+ delete this._usedFormats?.[name]
2048
+ if (object.isEmpty(this._usedFormats)) {
2049
+ this._usedFormats = null
1959
2050
  }
1960
- } else if (!fun.equalityDeep(this.usedFormats?.[name], value)) {
1961
- this.usedFormats = object.assign({}, this.usedFormats)
1962
- this.usedFormats[name] = value
2051
+ this._usedFormatsData = undefined // clear the data companion — recomputed on next consumption
2052
+ } else if (!fun.equalityDeep(this._usedFormats?.[name], value)) {
2053
+ this._usedFormats = object.assign({}, this._usedFormats)
2054
+ this._usedFormats[name] = value
2055
+ this._usedFormatsData = undefined // clear the data companion — recomputed on next consumption
1963
2056
  }
1964
2057
  return this
1965
2058
  }
@@ -1972,14 +2065,16 @@ export class DeltaBuilder extends Delta {
1972
2065
  updateUsedAttribution (name, value) {
1973
2066
  modDeltaCheck(this)
1974
2067
  if (value == null) {
1975
- this.usedAttribution = object.assign({}, this.usedAttribution)
1976
- delete this.usedAttribution?.[name]
1977
- if (object.isEmpty(this.usedAttribution)) {
1978
- this.usedAttribution = null
2068
+ this._usedAttribution = object.assign({}, this._usedAttribution)
2069
+ delete this._usedAttribution?.[name]
2070
+ if (object.isEmpty(this._usedAttribution)) {
2071
+ this._usedAttribution = null
1979
2072
  }
1980
- } else if (!fun.equalityDeep(this.usedAttribution?.[name], value)) {
1981
- this.usedAttribution = object.assign({}, this.usedAttribution)
1982
- this.usedAttribution[name] = value
2073
+ this._usedAttributionData = undefined // clear the data companion — recomputed on next consumption
2074
+ } else if (!fun.equalityDeep(this._usedAttribution?.[name], value)) {
2075
+ this._usedAttribution = object.assign({}, this._usedAttribution)
2076
+ this._usedAttribution[name] = value
2077
+ this._usedAttributionData = undefined // clear the data companion — recomputed on next consumption
1983
2078
  }
1984
2079
  return this
1985
2080
  }
@@ -1996,8 +2091,8 @@ export class DeltaBuilder extends Delta {
1996
2091
  */
1997
2092
  insert (insert, formatting, attribution) {
1998
2093
  modDeltaCheck(this)
1999
- const mergedFormats = combineData(this.usedFormats, formatting, false)
2000
- const mergedAttribution = combineData(this.usedAttribution, attribution, true)
2094
+ const mergedFormats = combineData(this._getUsedFormatsData(), formatting, false)
2095
+ const mergedAttribution = combineData(this._getUsedAttributionData(), attribution, true)
2001
2096
  /**
2002
2097
  * @param {TextOp | InsertOp<any>} lastOp
2003
2098
  */
@@ -2102,19 +2197,15 @@ export class DeltaBuilder extends Delta {
2102
2197
 
2103
2198
  /**
2104
2199
  * @param {number} len
2105
- * @param {DeltaBuilder<any>|null} prevValue
2106
2200
  */
2107
- delete (len, prevValue = null) {
2201
+ delete (len) {
2108
2202
  modDeltaCheck(this)
2109
2203
  const lastOp = /** @type {DeleteOp<any>|InsertOp<any>} */ (this.children.end)
2110
- if ($deleteOp.check(lastOp) && typeof lastOp.prevValue === typeof prevValue) {
2204
+ if ($deleteOp.check(lastOp)) {
2111
2205
  lastOp.delete += len
2112
2206
  lastOp._fingerprint = null
2113
- if (prevValue != null) {
2114
- /** @type {DeltaBuilder<any>} */ (lastOp.prevValue).append(prevValue)
2115
- }
2116
2207
  } else if (len > 0) {
2117
- list.pushEnd(this.children, new DeleteOp(len, prevValue))
2208
+ list.pushEnd(this.children, new DeleteOp(len))
2118
2209
  }
2119
2210
  this.childCnt += len
2120
2211
  return this
@@ -2126,14 +2217,13 @@ export class DeltaBuilder extends Delta {
2126
2217
  * @param {Key} key
2127
2218
  * @param {Val} val
2128
2219
  * @param {Attribution?} [attribution] provenance for this attr (data: object or `null`/none)
2129
- * @param {Val|undefined} [prevValue]
2130
2220
  * @return {DeltaBuilder<DeltaConfOverwrite<Conf,{attrs:AddToAttrs<DeltaConfGetAttrs<Conf>,Key,Val>}>, FixedConf>}
2131
2221
  */
2132
- setAttr (key, val, attribution, prevValue) {
2222
+ setAttr (key, val, attribution) {
2133
2223
  modDeltaCheck(this)
2134
2224
  // @ts-ignore
2135
2225
  this.attrs[key] /** @type {any} */ =
2136
- (new SetAttrOp(/** @type {any} */ (key), val, prevValue, combineData(this.usedAttribution, attribution, true)))
2226
+ (new SetAttrOp(/** @type {any} */ (key), val, combineData(this._getUsedAttributionData(), attribution, true)))
2137
2227
  // a delta-valued attribute carries marks in its subtree - flag conservatively (never decremented)
2138
2228
  if ($deltaAny.check(val)) this.maybeHasMarks ||= val.maybeHasMarks
2139
2229
  return /** @type {any} */ (this)
@@ -2161,18 +2251,17 @@ export class DeltaBuilder extends Delta {
2161
2251
  * @template {Extract<keyof DeltaConfGetAllowedAttrs<Conf, FixedConf>,string|number>} Key
2162
2252
  * @param {Key} key
2163
2253
  * @param {Attribution?} [attribution] provenance for the deletion (data: object or `null`/none)
2164
- * @param {any} [prevValue]
2165
2254
  * @return {DeltaBuilder<DeltaConfOverwrite<Conf, {
2166
2255
  * attrs: AddToAttrs<DeltaConfGetAttrs<Conf>,Key,never>
2167
2256
  * }>, FixedConf>}
2168
2257
  */
2169
- deleteAttr (key, attribution, prevValue) {
2258
+ deleteAttr (key, attribution) {
2170
2259
  modDeltaCheck(this)
2171
2260
  // dropping a delta-valued attribute drops its subtree's marks; the conservative `maybeHasMarks` flag
2172
2261
  // is left as-is (never decremented - marksToPositions self-corrects it)
2173
2262
  // @ts-ignore
2174
2263
  this.attrs[key] /** @type {any} */ =
2175
- (new DeleteAttrOp(/** @type {any} */ (key), prevValue, combineData(this.usedAttribution, attribution, true)))
2264
+ (new DeleteAttrOp(/** @type {any} */ (key), combineData(this._getUsedAttributionData(), attribution, true)))
2176
2265
  return /** @type {any} */ (this)
2177
2266
  }
2178
2267
 
@@ -2240,16 +2329,11 @@ export class DeltaBuilder extends Delta {
2240
2329
  this.maybeHasMarks ||= op.value.maybeHasMarks // flag any marks the new modify value carries
2241
2330
  }
2242
2331
  } else if ($setAttrOp.check(op)) {
2243
- const prev = c?.value
2244
- // @ts-ignore
2245
- op.prevValue = prev
2246
2332
  // a delta-valued attribute carries marks in its subtree - flag conservatively (never decremented)
2247
2333
  if ($deltaAny.check(op.value)) this.maybeHasMarks ||= op.value.maybeHasMarks
2248
2334
  // @ts-ignore
2249
2335
  this.attrs[op.key] = move ? op : op.clone()
2250
2336
  } else if ($deleteAttrOp.check(op)) {
2251
- const prev = c?.value
2252
- op.prevValue = prev
2253
2337
  // removing a delta-valued attribute drops its subtree's marks; the conservative `maybeHasMarks`
2254
2338
  // flag is left as-is (never decremented - marksToPositions self-corrects it)
2255
2339
  if (final) {
@@ -2374,7 +2458,7 @@ export class DeltaBuilder extends Delta {
2374
2458
  let remainingLen = op.delete
2375
2459
  while (remainingLen > 0) {
2376
2460
  if (opsI == null) {
2377
- _insertChild(this, null, scheduleForMerge(new DeleteOp(remainingLen, null)))
2461
+ _insertChild(this, null, scheduleForMerge(new DeleteOp(remainingLen)))
2378
2462
  break
2379
2463
  } else if ($retainOp.check(opsI)) { // retain ⇒ splice retain, insert delete
2380
2464
  const delLen = math.min(opsI.length - offset, remainingLen)
@@ -2388,7 +2472,7 @@ export class DeltaBuilder extends Delta {
2388
2472
  opsI = opNextUndeleted(opsI)
2389
2473
  offset = 0
2390
2474
  }
2391
- insertClonedOp(new DeleteOp(delLen, null))
2475
+ insertClonedOp(new DeleteOp(delLen))
2392
2476
  remainingLen -= delLen
2393
2477
  } else if ($modifyOp.check(opsI)) { // modify ⇒ delete the source position it stands for
2394
2478
  // a modify op addresses an underlying source position (retain + change), so deleting it
@@ -2398,7 +2482,7 @@ export class DeltaBuilder extends Delta {
2398
2482
  _removeChild(this, opsI)
2399
2483
  opsI = opNextUndeleted(opsI)
2400
2484
  offset = 0
2401
- insertClonedOp(new DeleteOp(1, null))
2485
+ insertClonedOp(new DeleteOp(1))
2402
2486
  remainingLen -= 1
2403
2487
  } else if (!$deleteOp.check(opsI)) { // insert / embed ⇒ replace
2404
2488
  // case1: delete o fully
@@ -2686,10 +2770,8 @@ export class DeltaBuilder extends Delta {
2686
2770
  otherOffset += maxCommonLen
2687
2771
  } else { // insert/text.check(otherChild)
2688
2772
  if (currOffset > 0) {
2689
- // clone the LEFT (prefix) and shrink currChild from the front, so the original op survives as
2690
- // the [currOffset..] suffix — keeping a DeleteOp's prevValue in sync (DeleteOp.clone drops it).
2691
- _insertChild(this, currChild, currChild.clone(0, currOffset))
2692
- _shrinkChild(this, currChild, 0, currOffset)
2773
+ // split currChild so the cursor sits on the boundary; continue on the [currOffset..] suffix
2774
+ currChild = _splitChildAt(this, currChild, currOffset)
2693
2775
  currOffset = 0
2694
2776
  }
2695
2777
  _insertChild(this, currChild, new RetainOp(otherChild.length, undefined, undefined))
@@ -4002,6 +4084,118 @@ export const diff = (d1, d2, options = {}) => {
4002
4084
  return d.done(false)
4003
4085
  }
4004
4086
 
4087
+ /**
4088
+ * The tri-state update that undoes a `format`/`attribution` instruction `update` against the stored
4089
+ * value `stored` (object or `null`/none) it was applied to: applying the result after `update` restores
4090
+ * `stored`. Merge the instruction onto `stored` the way an apply resolves it ({@link applyDim}), then
4091
+ * diff the merged result back to `stored` ({@link diffDim}). `undefined` (skip) inverts to `undefined`.
4092
+ *
4093
+ * @param {{[k:string]:any}|null|undefined} stored
4094
+ * @param {{[k:string]:any}|null|undefined} update
4095
+ * @param {boolean} deep `true` for the `attribution` dimension (its `format` key merges per inner key —
4096
+ * see {@link mergeAttr}); `false` for `format` (shallow)
4097
+ * @return {{[k:string]:any}|null|undefined}
4098
+ */
4099
+ const inverseDim = (stored, update, deep) => {
4100
+ if (update === undefined) return undefined
4101
+ const merged = update === null ? null : (deep ? mergeAttr(stored, update, true) : mergeShallow(stored, update, true))
4102
+ return diffDim(object.isEmpty(merged) ? null : merged, stored ?? null, deep)
4103
+ }
4104
+
4105
+ /**
4106
+ * Compute a delta that undoes `d` — a change that was applied to the settled document state `base`
4107
+ * (inserts and set attributes only, e.g. an RDT's final state): `base.apply(d).apply(inverse(d, base))`
4108
+ * equals `base`. Inspired by Quill's `Delta#invert`, `base` supplies what the change alone doesn't
4109
+ * carry: deleted content is re-inserted from `base` (with its stored format/attribution), an
4110
+ * overwritten or deleted attribute is restored to its `base` value, and an inverted `retain`/`modify`
4111
+ * instruction is diffed against the `base` op's stored format/attribution.
4112
+ *
4113
+ * Content only: marks are local/ephemeral cursor state and are not inverted (mirroring {@link diff}).
4114
+ * Like {@link diff}, the result *shares* re-inserted children and restored attribute values with `base`.
4115
+ *
4116
+ * The round-trip holds for changes whose content-consuming ops (`retain`/`delete`/`modify`) stay within
4117
+ * `base`'s bounds. Beyond-base ops degrade gracefully: following ops stay positioned, but there is no
4118
+ * stored content to restore — and what a final apply *materializes* beyond content (e.g. a format set
4119
+ * kept as a trailing formatted retain, or an appended modify) cannot be removed by any change, so such
4120
+ * a change is not fully undone.
4121
+ *
4122
+ * @template {DeltaConf} Conf
4123
+ * @param {DeltaAny} d
4124
+ * @param {Delta<Conf>} base
4125
+ * @return {Delta<Conf>}
4126
+ */
4127
+ export const inverse = (d, base) => {
4128
+ const inv = create(d.name === base.name ? d.name : null, $deltaAny)
4129
+ for (const op of d.attrs) {
4130
+ const baseOp = /** @type {AttrOpAny|undefined} */ ((/** @type {any} */ (base.attrs))[op.key])
4131
+ if ($modifyAttrOp.check(op) && $setAttrOp.check(baseOp) && $deltaAny.check(baseOp.value)) {
4132
+ // the modify edited base's nested delta in place: undo it there, and undo the attribution
4133
+ // instruction it merged onto the attr op
4134
+ inv.modifyAttr(op.key, inverse(op.value, baseOp.value), /** @type {Attribution?} */ (inverseDim(baseOp.attribution, op.attribution, true)))
4135
+ } else if ($setAttrOp.check(baseOp)) {
4136
+ // a set/delete (or a modify that replaced a non-delta value) overwrote a settled attr:
4137
+ // restore value + attribution from base
4138
+ inv.setAttr(op.key, baseOp.value, baseOp.attribution)
4139
+ } else if (!$deleteAttrOp.check(op)) {
4140
+ // a set/modify introduced an attr base didn't have: undo by deleting it
4141
+ inv.deleteAttr(op.key)
4142
+ } // else: deleted an attr base didn't have — nothing to restore
4143
+ }
4144
+ // walk base's children alongside d's ops: an insert consumes no base positions; retain/delete/modify
4145
+ // consume their length in base coordinates
4146
+ let baseOp = /** @type {ChildrenOpAny?} */ (base.children.start)
4147
+ let baseOffset = 0
4148
+ /**
4149
+ * Advance the base cursor by `len`, reporting each traversed chunk `(op, start, end)` (stored
4150
+ * format/attribution are uniform per op, so per-op chunks are exact); returns the length that
4151
+ * extends beyond base's content.
4152
+ *
4153
+ * @param {number} len
4154
+ * @param {(op: TextOp|InsertOp<any>, start: number, end: number) => void} f
4155
+ */
4156
+ const forBase = (len, f) => {
4157
+ while (len > 0 && baseOp != null) {
4158
+ const chunk = math.min(len, baseOp.length - baseOffset)
4159
+ f(/** @type {TextOp|InsertOp<any>} */ (baseOp), baseOffset, baseOffset + chunk)
4160
+ len -= chunk
4161
+ baseOffset += chunk
4162
+ if (baseOffset === baseOp.length) {
4163
+ baseOp = baseOp.next
4164
+ baseOffset = 0
4165
+ }
4166
+ }
4167
+ return len
4168
+ }
4169
+ for (const op of d.children) {
4170
+ if ($textOp.check(op) || $insertOp.check(op)) {
4171
+ inv.delete(op.length)
4172
+ } else if ($retainOp.check(op)) {
4173
+ const rest = forBase(op.retain, (bop, start, end) => {
4174
+ inv.retain(end - start, inverseDim(bop.format, op.format, false), /** @type {Attribution?} */ (inverseDim(bop.attribution, op.attribution, true)))
4175
+ })
4176
+ // beyond base there is no stored content to restore — keep following ops positioned (see the
4177
+ // beyond-base note in the fn docs)
4178
+ rest > 0 && inv.retain(rest)
4179
+ } else if ($modifyOp.check(op)) {
4180
+ if (baseOp == null) {
4181
+ // a modify beyond base edited no stored content — keep the position (mirrors retain-beyond)
4182
+ inv.retain(1)
4183
+ } else {
4184
+ const bop = /** @type {InsertOp<any>} */ (baseOp)
4185
+ const node = /** @type {DeltaAny} */ (bop.insert[baseOffset])
4186
+ inv.modify(inverse(/** @type {DeltaAny} */ (op.value), node), inverseDim(bop.format, op.format, false), /** @type {Attribution?} */ (inverseDim(bop.attribution, op.attribution, true)))
4187
+ forBase(1, () => {})
4188
+ }
4189
+ } else if ($deleteOp.check(op)) {
4190
+ // re-insert the deleted base content with its stored format/attribution
4191
+ forBase(op.delete, (bop, start, end) => {
4192
+ inv.insert(bop.insert.slice(start, end), bop.format, bop.attribution)
4193
+ })
4194
+ }
4195
+ }
4196
+ return inv.done(false)
4197
+ }
4198
+
4005
4199
  /**
4006
4200
  * Default pairing predicate for {@link diff}: two delta nodes are paired (and recursed into via
4007
4201
  * `modify`) when their names match.
package/src/delta/rdt.js CHANGED
@@ -155,6 +155,13 @@ const propagate = (binding, ta, tb) => {
155
155
  }
156
156
  }
157
157
 
158
+ /**
159
+ * @typedef {object} BindOptions
160
+ * @property {delta.DiffOptions['compare']} [diffCompare] **Experimental** (may be changed or removed
161
+ * in a future release). Forwarded as `compare` to {@link delta.diff} when the initial-state sync diffs
162
+ * `a`'s projection against `b`'s current state, controlling how nodes are paired into `modify` ops.
163
+ */
164
+
158
165
  /**
159
166
  * Connects two RDTs so that changes on either side are transformed and reflected on the other.
160
167
  *
@@ -174,8 +181,9 @@ export class Binding {
174
181
  * @param {RDT<B>} b
175
182
  * @param {dt.TemplateFactory<A,B>} [template] a `$d => Template` factory; defaults to the
176
183
  * {@link dt.id identity} transformer. `bind` injects `a`'s schema, then materializes via `.init()`.
184
+ * @param {BindOptions} [options]
177
185
  */
178
- constructor (a, b, template = /** @type {dt.TemplateFactory<A,B>} */ (dt.id)) {
186
+ constructor (a, b, template = /** @type {dt.TemplateFactory<A,B>} */ (dt.id), options = {}) {
179
187
  /**
180
188
  * @type {dt.Transformer<A,B>}
181
189
  */
@@ -210,7 +218,7 @@ export class Binding {
210
218
  // `diff` shares nested children with `tres.b` by default, and `tres.b` is a (possibly stateful)
211
219
  // transformer's output that may still alias its internal state — so `clone` keeps the diff
212
220
  // 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
221
+ const diffB = tres.b ? delta.diff(this.b.delta, tres.b, { clone: true, compare: options.diffCompare }) : null
214
222
  const fb = diffB ? this.b.applyDelta(diffB, this) : null
215
223
  propagate(this, fa, fb)
216
224
  })
@@ -241,6 +249,7 @@ export class Binding {
241
249
  * @param {RDT<B>} b
242
250
  * @param {dt.TemplateFactory<A,B>} [template] a `$d => Template` factory (e.g. `dt.id` or
243
251
  * `$d => dt.renameAttrs($d, {a:'b'})`)
252
+ * @param {BindOptions} [options] `diffCompare` is **experimental** — see {@link BindOptions}.
244
253
  * @return {Binding<A,B>}
245
254
  */
246
- export const bind = (a, b, template) => new Binding(a, b, template)
255
+ export const bind = (a, b, template, options = {}) => new Binding(a, b, template, options)
@@ -173,9 +173,9 @@ const stripAttrAttributions = (out) => {
173
173
  if (delta.$modifyAttrOp.check(op)) {
174
174
  if (op.attribution !== undefined) out.modifyAttr(op.key, op.value)
175
175
  } else if (delta.$setAttrOp.check(op)) {
176
- if (op.attribution != null) out.setAttr(op.key, op.value, undefined, op.prevValue)
176
+ if (op.attribution != null) out.setAttr(op.key, op.value)
177
177
  } else if (op.attribution != null) { // DeleteAttrOp
178
- out.deleteAttr(op.key, undefined, op.prevValue)
178
+ out.deleteAttr(op.key)
179
179
  }
180
180
  }
181
181
  }
@@ -246,7 +246,7 @@ export class ConformTransformer extends Transformer {
246
246
  if (t != null) { out.modifyAttr(key, t.applyA(op.value).b); keptAttrKeys.add(key) }
247
247
  } else { // deleteAttr (the only remaining attr-op kind)
248
248
  const dop = /** @type {delta.DeleteAttrOp<any>} */ (op)
249
- out.deleteAttr(key, dop.attribution, dop.prevValue)
249
+ out.deleteAttr(key, dop.attribution)
250
250
  this.transformAttrs.delete(key)
251
251
  keptAttrKeys.add(key)
252
252
  }
@@ -276,7 +276,7 @@ export class ConformTransformer extends Transformer {
276
276
  /** @param {number} n */
277
277
  const addRetain = n => addUniform(delta.$retainOp, n, () => new delta.RetainOp(n, null, null)) // n pass-through positions
278
278
  /** @param {number} n */
279
- const addDrop = n => addUniform(delta.$deleteOp, n, () => new delta.DeleteOp(n, null)) // n dropped positions
279
+ const addDrop = n => addUniform(delta.$deleteOp, n, () => new delta.DeleteOp(n)) // n dropped positions
280
280
  /** @param {ConformTransformer} t */
281
281
  const addTransformed = t => { // one child routed through nested conform `t`
282
282
  if (src != null && delta.$insertOp.check(src)) { delta._spliceInsert(cmap, src, off, [t]); off += 1 } else { splitCursor(); delta._mergeChildWithPrev(cmap, delta._insertChild(cmap, src, new delta.InsertOp([t], null, null))) }
@@ -181,9 +181,9 @@ const buildFull = (d, overlay) => {
181
181
  const childOverlay = (oa != null && delta.$modifyAttrOp.check(oa) && delta.$deltaAny.check(oa.value)) ? oa.value : null
182
182
  full.modifyAttr(aop.key, delta.$deltaAny.check(aop.value) ? buildFull(aop.value, childOverlay) : aop.value, applyFull(oa == null ? undefined : oa.attribution, aop.attribution))
183
183
  } else if (delta.$setAttrOp.check(aop)) {
184
- full.setAttr(aop.key, aop.value, aop.attribution, aop.prevValue)
184
+ full.setAttr(aop.key, aop.value, aop.attribution)
185
185
  } else if (delta.$deleteAttrOp.check(aop)) {
186
- full.deleteAttr(aop.key, aop.attribution, aop.prevValue)
186
+ full.deleteAttr(aop.key, aop.attribution)
187
187
  }
188
188
  }
189
189
  full.done(false)