lib0 1.0.0-rc.13 → 1.0.0-rc.14

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.
@@ -42,6 +42,22 @@ export const $attribution: s.Schema<Attribution>;
42
42
  * @type {s.Schema<DeltaAttrOpJSON>}
43
43
  */
44
44
  export const $deltaMapChangeJson: s.Schema<DeltaAttrOpJSON>;
45
+ /**
46
+ * Invariants shared by all op classes below (TextOp, InsertOp, DeleteOp,
47
+ * RetainOp, ModifyOp, SetAttrOp, DeleteAttrOp, ModifyAttrOp):
48
+ *
49
+ * - **Only code inside `delta.js` may mutate op fields.** External consumers
50
+ * treat ops as immutable; structural fields are JSDoc-annotated `@readonly`
51
+ * to reinforce this. Mutation is permitted only while the owning Delta is
52
+ * not `done` — every builder entry point routes through `modDeltaCheck`
53
+ * to enforce this at runtime.
54
+ * - **Any mutation of a fingerprinted field MUST null `_fingerprint`.** The
55
+ * fingerprint is a lazy cache; if it has already been computed and the
56
+ * underlying data changes without invalidating it, every subsequent
57
+ * fingerprint read (and any `diff` / equality check that relies on it) is
58
+ * wrong. Fields covered: insert, delete, retain, format, attribution,
59
+ * value, key.
60
+ */
45
61
  export class TextOp extends list.ListNode {
46
62
  /**
47
63
  * @param {string} insert
@@ -125,9 +141,9 @@ export class InsertOp<ArrayContent extends unknown> extends list.ListNode {
125
141
  */
126
142
  _fingerprint: string | null;
127
143
  /**
128
- * @param {ArrayContent} newVal
144
+ * @param {ArrayContent} _newVal
129
145
  */
130
- _updateInsert(newVal: ArrayContent): void;
146
+ _updateInsert(_newVal: ArrayContent): void;
131
147
  /**
132
148
  * @return {'insert'}
133
149
  */
@@ -184,10 +200,10 @@ export class DeleteOp<Conf extends DeltaConf = {}> extends list.ListNode {
184
200
  /**
185
201
  * Remove a part of the operation (similar to Array.splice)
186
202
  *
187
- * @param {number} _offset
203
+ * @param {number} offset
188
204
  * @param {number} len
189
205
  */
190
- _splice(_offset: number, len: number): this;
206
+ _splice(offset: number, len: number): this;
191
207
  /**
192
208
  * @return {DeltaListOpJSON}
193
209
  */
@@ -666,10 +682,12 @@ export class DeltaBuilder<Conf extends DeltaConf = {}, FixedConf extends boolean
666
682
  *
667
683
  * a.apply(b).apply(c)
668
684
  *
669
- * @todo fuzz test the above property
685
+ * If `final = true`, we consider this delta the final state and drop deleteAttrOps from
686
+ * attributes. (E.g. if `otherOp` deletes an attribute, this op will simply not have the
687
+ * attribute). Any kind of `delete` op might be considered a bug. A final delta is not idempotent.
670
688
  *
671
689
  * @param {Delta<Conf>?} other
672
- * @param {{ final?: boolean }} opts -- experimental
690
+ * @param {{ final?: boolean }} opts -- (experimental)
673
691
  * @return {DeltaBuilder<Conf>}
674
692
  */
675
693
  apply(other: Delta<Conf> | null, { final }?: {
@@ -215,10 +215,10 @@ export class ProjectionTransformer<A extends delta.DeltaConf, B extends delta.De
215
215
  /**
216
216
  * @template {delta.DeltaConf} A
217
217
  * @template {delta.DeltaConf} B
218
- * @template {Pipe<Template[]>} PipeTemplate
218
+ * @template {Pipe<any>} PipeTemplate
219
219
  * @extends {Transformer<A,B>}
220
220
  */
221
- export class PipeTransformer<A extends delta.DeltaConf, B extends delta.DeltaConf, PipeTemplate extends Pipe<Template[]>> extends Transformer<A, B> {
221
+ export class PipeTransformer<A extends delta.DeltaConf, B extends delta.DeltaConf, PipeTemplate extends Pipe<any>> extends Transformer<A, B> {
222
222
  /**
223
223
  * @param {PipeTemplate} tpipe
224
224
  */
@@ -244,7 +244,102 @@ export type ApplyAttrRename<Renames extends {
244
244
  attrs: import("../ts.js").PropsRename<delta.DeltaConfGetAttrs<IN>, Renames>;
245
245
  }>;
246
246
  export type ApplyExpectType<DConf extends delta.DeltaConf, IN extends delta.DeltaConf> = { [K in keyof DConf & keyof IN]: K extends "attrs" ? import("../ts.js").PropsPickShared<DConf[K], IN[K]> : (DConf[K] & IN[K]); } & {};
247
- export type ApplyPipe<TS extends Array<Template>, IN extends delta.DeltaConf> = TS extends [infer FirstT extends Template, ...infer RestT extends Template[]] ? ApplyPipe<RestT, ApplyTemplate<FirstT, IN>> : IN;
247
+ /**
248
+ * Marker for absent props in a NormalizedDeltaConf.
249
+ */
250
+ export type NotSet = {
251
+ "lib0:notset": true;
252
+ };
253
+ /**
254
+ * DeltaConf in normalized form: all props defined, absent props are set to NotSet.
255
+ *
256
+ * ApplyPipe iterates over this form (see ApplyPipeNorm).
257
+ */
258
+ export type NormalizedDeltaConf<Name, Attrs, Children, Text, RecursiveChildren, RecursiveAttrs> = {
259
+ name: Name;
260
+ attrs: Attrs;
261
+ children: Children;
262
+ text: Text;
263
+ recursiveChildren: RecursiveChildren;
264
+ recursiveAttrs: RecursiveAttrs;
265
+ };
266
+ export type NormalizeDeltaConf<C extends delta.DeltaConf> = NormalizedDeltaConf<C extends {
267
+ name: infer Name extends string;
268
+ } ? Name : NotSet, C extends {
269
+ attrs: infer Attrs extends {
270
+ [K: string | number]: any;
271
+ };
272
+ } ? Attrs : NotSet, C extends {
273
+ children: infer Children;
274
+ } ? Children : NotSet, C extends {
275
+ text: infer Text extends boolean;
276
+ } ? Text : NotSet, C extends {
277
+ recursiveChildren: infer RecursiveChildren extends boolean;
278
+ } ? RecursiveChildren : NotSet, C extends {
279
+ recursiveAttrs: infer RecursiveAttrs extends boolean;
280
+ } ? RecursiveAttrs : NotSet>;
281
+ /**
282
+ * Strip NotSet props from a NormalizedDeltaConf, producing a regular DeltaConf again.
283
+ */
284
+ export type DenormalizeDeltaConf<NC> = { [K in keyof NC as NC[K] extends NotSet ? never : K]: NC[K]; } & {};
285
+ /**
286
+ * Intersect a prop of a Filter conf with the corresponding pipe conf prop. The prop is only kept
287
+ * if it is defined on both sides (mirrors ApplyExpectType).
288
+ */
289
+ export type FilterConfProp<FilterProp, PipeProp> = FilterProp extends NotSet ? NotSet : PipeProp extends NotSet ? NotSet : FilterProp & PipeProp;
290
+ /**
291
+ * Apply each Template to a NormalizedDeltaConf - must mirror the semantics of ApplyAttrRename /
292
+ * ApplyExpectType.
293
+ *
294
+ * This shape is tuned to stay below typescript's instantiation-depth limit (TS2589) for long
295
+ * pipes (~85 templates via pipe().init(), measured). What we learned:
296
+ *
297
+ * - The per-step destructure of NC is the load-bearing part: typescript resolves types lazily,
298
+ * and member inference out of the literal that was passed as a type argument in the previous
299
+ * step is what forces resolution of the accumulated conf. Without it (e.g. carrying the conf
300
+ * props as individual type params), the attrs accumulate as a deferred PropsRename chain and
301
+ * the limit hits at ~45 templates. Local annotations do NOT force resolution: `X & {}`,
302
+ * `X extends infer N ? ...`, and an inline `{ attrs: X } extends { attrs: infer A } ? ...`
303
+ * roundtrip were all measured to have no effect.
304
+ * - The recursion must carry a plain object literal. Wrapping the accumulator in a helper alias
305
+ * (even a trivial one like NormalizedDeltaConf) defers per step and rebuilds the chain.
306
+ * - The outer check must be on TS alone. Coupling NC into the check type (e.g.
307
+ * `[TS, NC] extends [[...], {...}]`) makes the conditional generic-deferred whenever the conf
308
+ * is generic, which sends constraint comparisons (e.g. Pipe<TS> vs Pipe<any>) into infinite
309
+ * recursion.
310
+ */
311
+ export type ApplyPipeNorm<TS extends Array<Template>, NC> = TS extends [infer FirstT extends Template, ...infer RestT extends Template[]] ? (NC extends {
312
+ name: infer Name;
313
+ attrs: infer Attrs extends {
314
+ [K: string | number]: any;
315
+ };
316
+ children: infer Children;
317
+ text: infer Text;
318
+ recursiveChildren: infer RecursiveChildren;
319
+ recursiveAttrs: infer RecursiveAttrs;
320
+ } ? ApplyPipeNorm<RestT, FirstT extends AttrRename<infer Renames> ? {
321
+ name: Name;
322
+ attrs: import("../ts.js").PropsRename<Attrs extends NotSet ? {} : Attrs, Renames>;
323
+ children: Children;
324
+ text: Text;
325
+ recursiveChildren: RecursiveChildren;
326
+ recursiveAttrs: RecursiveAttrs;
327
+ } : FirstT extends Filter<infer DConf extends delta.DeltaConf> ? (NormalizeDeltaConf<DConf> extends {
328
+ name: infer FilterName;
329
+ attrs: infer FilterAttrs;
330
+ children: infer FilterChildren;
331
+ text: infer FilterText;
332
+ recursiveChildren: infer FilterRecursiveChildren;
333
+ recursiveAttrs: infer FilterRecursiveAttrs;
334
+ } ? {
335
+ name: FilterConfProp<FilterName, Name>;
336
+ attrs: FilterAttrs extends NotSet ? NotSet : Attrs extends NotSet ? NotSet : import("../ts.js").PropsPickShared<FilterAttrs, Attrs>;
337
+ children: FilterConfProp<FilterChildren, Children>;
338
+ text: FilterConfProp<FilterText, Text>;
339
+ recursiveChildren: FilterConfProp<FilterRecursiveChildren, RecursiveChildren>;
340
+ recursiveAttrs: FilterConfProp<FilterRecursiveAttrs, RecursiveAttrs>;
341
+ } : never) : NC> : NC) : NC;
342
+ export type ApplyPipe<TS extends Array<Template>, IN extends delta.DeltaConf> = DenormalizeDeltaConf<ApplyPipeNorm<TS, NormalizeDeltaConf<IN>>>;
248
343
  export type ApplyQueryAttr<AttrName extends string, IN extends delta.DeltaConf> = {
249
344
  name: "lib0:value";
250
345
  attrs: {
@@ -253,14 +348,13 @@ export type ApplyQueryAttr<AttrName extends string, IN extends delta.DeltaConf>
253
348
  } ? V : never;
254
349
  };
255
350
  };
256
- export type EnsureDeltaConf<IN> = IN extends infer OUT extends delta.DeltaConf ? OUT : never;
257
- export type ApplyTemplate<T extends Template, IN extends delta.DeltaConf> = EnsureDeltaConf<T extends AttrRename<infer Renames> ? ApplyAttrRename<Renames, IN> : T extends Filter<infer DConf extends delta.DeltaConf> ? ApplyExpectType<DConf, IN> : IN>;
258
351
  /**
259
352
  * Flattens nested Pipe instances into a single flat Template array.
260
353
  * Since pipe() always produces flat Pipes, Inner is already flat and
261
354
  * only one level of unwrapping is needed per Pipe element.
355
+ * Tail-recursive with an accumulator so the instantiation depth stays constant.
262
356
  */
263
- export type FlattenTemplates<TS extends Array<Template>> = TS extends [infer F extends Template, ...infer R extends Template[]] ? F extends Pipe<infer Inner extends Template[]> ? [...Inner, ...FlattenTemplates<R>] : [F, ...FlattenTemplates<R>] : [];
357
+ export type FlattenTemplates<TS extends Array<Template>, Acc extends Array<Template> = []> = TS extends [infer F extends Template, ...infer R extends Template[]] ? FlattenTemplates<R, F extends Pipe<infer Inner extends Template[]> ? [...Acc, ...Inner] : [...Acc, F]> : Acc;
264
358
  import * as delta from './delta.js';
265
359
  import * as s from '../schema.js';
266
360
  /**
@@ -0,0 +1,11 @@
1
+ /**
2
+ * FNV-1a offset basis (32bit).
3
+ */
4
+ export const offsetBasis: 2166136261;
5
+ /**
6
+ * FNV-1a prime (32bit).
7
+ */
8
+ export const prime: 16777619;
9
+ export function digest(data: Uint8Array, hash?: number): number;
10
+ export function digestString(str: string, hash?: number): number;
11
+ //# sourceMappingURL=fnv1a.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lib0",
3
- "version": "1.0.0-rc.13",
3
+ "version": "1.0.0-rc.14",
4
4
  "description": "",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -69,6 +69,10 @@
69
69
  "types": "./dist/crypto/rsa-oaep.d.ts",
70
70
  "default": "./src/crypto/rsa-oaep.js"
71
71
  },
72
+ "./hash/fnv1a": {
73
+ "types": "./dist/hash/fnv1a.d.ts",
74
+ "default": "./src/hash/fnv1a.js"
75
+ },
72
76
  "./hash/rabin": {
73
77
  "types": "./dist/hash/rabin.d.ts",
74
78
  "default": "./src/hash/rabin.js"
package/src/bin/0serve.js CHANGED
@@ -89,9 +89,17 @@ const server = http.createServer((req, res) => {
89
89
  server.listen(port, host, () => {
90
90
  logging.print(logging.BOLD, logging.ORANGE, `Server is running on http://${host}:${port}`)
91
91
  if (paramOpenFile) {
92
- const start = debugBrowser || (process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open')
92
+ const url = `http://${host}:${port}/${paramOpenFile}`
93
93
  import('child_process').then(cp => {
94
- cp.exec(`${start} http://${host}:${port}/${paramOpenFile}`)
94
+ if (debugBrowser) {
95
+ cp.execFile(debugBrowser, [url])
96
+ } else if (process.platform === 'darwin') {
97
+ cp.execFile('open', [url])
98
+ } else if (process.platform === 'win32') {
99
+ cp.execFile('cmd', ['/c', 'start', '', url])
100
+ } else {
101
+ cp.execFile('xdg-open', [url])
102
+ }
95
103
  })
96
104
  }
97
105
  })
@@ -101,6 +101,22 @@ const _cloneAttrs = attrs => attrs == null ? attrs : { ...attrs }
101
101
  */
102
102
  const _markMaybeDeltaAsDone = maybeDelta => $deltaAny.check(maybeDelta) ? /** @type {MaybeDelta} */ (maybeDelta.done()) : maybeDelta
103
103
 
104
+ /**
105
+ * Invariants shared by all op classes below (TextOp, InsertOp, DeleteOp,
106
+ * RetainOp, ModifyOp, SetAttrOp, DeleteAttrOp, ModifyAttrOp):
107
+ *
108
+ * - **Only code inside `delta.js` may mutate op fields.** External consumers
109
+ * treat ops as immutable; structural fields are JSDoc-annotated `@readonly`
110
+ * to reinforce this. Mutation is permitted only while the owning Delta is
111
+ * not `done` — every builder entry point routes through `modDeltaCheck`
112
+ * to enforce this at runtime.
113
+ * - **Any mutation of a fingerprinted field MUST null `_fingerprint`.** The
114
+ * fingerprint is a lazy cache; if it has already been computed and the
115
+ * underlying data changes without invalidating it, every subsequent
116
+ * fingerprint read (and any `diff` / equality check that relies on it) is
117
+ * wrong. Fields covered: insert, delete, retain, format, attribution,
118
+ * value, key.
119
+ */
104
120
  export class TextOp extends list.ListNode {
105
121
  /**
106
122
  * @param {string} insert
@@ -228,14 +244,17 @@ export class InsertOp extends list.ListNode {
228
244
  this._fingerprint = null
229
245
  }
230
246
 
247
+ /* c8 ignore start */
231
248
  /**
232
- * @param {ArrayContent} newVal
249
+ * @param {ArrayContent} _newVal
233
250
  */
234
- _updateInsert (newVal) {
235
- // @ts-ignore
236
- this.insert = newVal
237
- this._fingerprint = null
251
+ _updateInsert (_newVal) {
252
+ // Mirror of TextOp._updateInsert; not currently called on InsertOp because
253
+ // adjacent inserts are merged in-place via `end.insert.push(...)`. Kept for
254
+ // parity with TextOp's API.
255
+ error.unexpectedCase() // throw if called
238
256
  }
257
+ /* c8 ignore stop */
239
258
 
240
259
  /**
241
260
  * @return {'insert'}
@@ -357,11 +376,13 @@ export class DeleteOp extends list.ListNode {
357
376
  /**
358
377
  * Remove a part of the operation (similar to Array.splice)
359
378
  *
360
- * @param {number} _offset
379
+ * @param {number} offset
361
380
  * @param {number} len
362
381
  */
363
- _splice (_offset, len) {
364
- this.prevValue = /** @type {any} */ (this.prevValue ? slice(this.prevValue, _offset, len) : null)
382
+ _splice (offset, len) {
383
+ if (this.prevValue) {
384
+ /** @type {DeltaBuilder<any>} */ (this.prevValue).apply(create().retain(offset).delete(len))
385
+ }
365
386
  this._fingerprint = null
366
387
  this.delete -= len
367
388
  return this
@@ -547,6 +568,9 @@ export class ModifyOp extends list.ListNode {
547
568
  })))
548
569
  }
549
570
 
571
+ /* c8 ignore start */
572
+ // ModifyOp has length 1, so callers never pass offset>0 or len>0 — splitHere
573
+ // is a no-op for length-1 ops. Kept for the structural _splice contract.
550
574
  /**
551
575
  * Remove a part of the operation (similar to Array.splice)
552
576
  *
@@ -556,6 +580,7 @@ export class ModifyOp extends list.ListNode {
556
580
  _splice (_offset, _len) {
557
581
  return this
558
582
  }
583
+ /* c8 ignore stop */
559
584
 
560
585
  /**
561
586
  * @return {DeltaListOpJSON}
@@ -851,7 +876,7 @@ export const $setAttrOpWith = $content => s.$custom(o => $setAttrOp.check(o) &&
851
876
  * @param {s.Schema<Content>} $content
852
877
  * @return {s.Schema<InsertOp<Content>>}
853
878
  */
854
- export const $insertOpWith = $content => s.$custom(o => $insertOp.check(o) && $content.check(o.insert.every(ins => $content.check(ins))))
879
+ export const $insertOpWith = $content => s.$custom(o => $insertOp.check(o) && o.insert.every(ins => $content.check(ins)))
855
880
 
856
881
  /**
857
882
  * @template {DeltaAny} Modify
@@ -1231,9 +1256,13 @@ const tryMergeWithPrev = (parent, op) => {
1231
1256
  /** @type {DeleteOp<any>} */ (prevOp).delete += op.delete
1232
1257
  } else if ($textOp.check(op)) {
1233
1258
  /** @type {TextOp} */ (prevOp)._updateInsert(/** @type {TextOp} */ (prevOp).insert + op.insert)
1259
+ /* c8 ignore start */
1234
1260
  } else {
1261
+ // unreachable: the constructor check at the top of the function already
1262
+ // limits `op` to one of the four kinds tested above
1235
1263
  error.unexpectedCase()
1236
1264
  }
1265
+ /* c8 ignore stop */
1237
1266
  list.remove(parent, op)
1238
1267
  }
1239
1268
 
@@ -1501,10 +1530,12 @@ export class DeltaBuilder extends Delta {
1501
1530
  *
1502
1531
  * a.apply(b).apply(c)
1503
1532
  *
1504
- * @todo fuzz test the above property
1533
+ * If `final = true`, we consider this delta the final state and drop deleteAttrOps from
1534
+ * attributes. (E.g. if `otherOp` deletes an attribute, this op will simply not have the
1535
+ * attribute). Any kind of `delete` op might be considered a bug. A final delta is not idempotent.
1505
1536
  *
1506
1537
  * @param {Delta<Conf>?} other
1507
- * @param {{ final?: boolean }} opts -- experimental
1538
+ * @param {{ final?: boolean }} opts -- (experimental)
1508
1539
  * @return {DeltaBuilder<Conf>}
1509
1540
  */
1510
1541
  apply (other, { final = this.isFinal } = {}) {
@@ -1517,7 +1548,7 @@ export class DeltaBuilder extends Delta {
1517
1548
  const c = /** @type {SetAttrOp<any,any>|DeleteAttrOp<any>|ModifyAttrOp<any,any>} */ (this.attrs[op.key])
1518
1549
  if ($modifyAttrOp.check(op)) {
1519
1550
  if ($deltaAny.check(c?.value)) {
1520
- c._modValue.apply(op.value)
1551
+ c._modValue.apply(op.value, { final })
1521
1552
  } else {
1522
1553
  // then this is a simple modify
1523
1554
  // @ts-ignore
@@ -1588,10 +1619,14 @@ export class DeltaBuilder extends Delta {
1588
1619
  this.childCnt += op.length
1589
1620
  }
1590
1621
  for (const op of other.children) {
1622
+ // defensive: the per-branch logic below resets opsI/offset whenever it
1623
+ // consumes an op exactly. This guard catches any path that forgets to.
1624
+ /* c8 ignore start */
1591
1625
  if (opsI?.length === offset) {
1592
1626
  opsI = opNextUndeleted(opsI)
1593
1627
  offset = 0
1594
1628
  }
1629
+ /* c8 ignore stop */
1595
1630
  if ($textOp.check(op) || $insertOp.check(op)) {
1596
1631
  insertClonedOp(op)
1597
1632
  } else if ($retainOp.check(op)) {
@@ -1611,7 +1646,11 @@ export class DeltaBuilder extends Delta {
1611
1646
  }
1612
1647
  if (opsI != null) {
1613
1648
  if (op.format != null && retainLen > 0) {
1614
- offset = retainLen
1649
+ // accumulate onto the existing offset the else-branch below uses
1650
+ // `offset += retainLen`, and we must agree with it when prior
1651
+ // iterations have advanced offset into opsI without splitting (e.g.
1652
+ // a format-less retain followed by a same-format retain).
1653
+ offset += retainLen
1615
1654
  splitHere()
1616
1655
  updateOpFormat(/** @type {ChildrenOpAny} */ (opsI.prev), op.format)
1617
1656
  scheduleForMerge(opsI.prev)
@@ -1670,9 +1709,12 @@ export class DeltaBuilder extends Delta {
1670
1709
  opsI._splice(offset, delLen)
1671
1710
  }
1672
1711
  remainingLen -= delLen
1712
+ /* c8 ignore start */
1673
1713
  } else {
1714
+ // unreachable: opsI was already typed as retain | non-delete-content | delete above
1674
1715
  error.unexpectedCase()
1675
1716
  }
1717
+ /* c8 ignore stop */
1676
1718
  }
1677
1719
  } else if ($modifyOp.check(op)) {
1678
1720
  if (opsI != null && op.format != null && (!$deleteOp.check(opsI) && !$retainOp.check(opsI))) { // retain handles splitting seperately, without copying attrs
@@ -1709,14 +1751,20 @@ export class DeltaBuilder extends Delta {
1709
1751
  opsI._splice(0, 1)
1710
1752
  scheduleForMerge(opsI)
1711
1753
  }
1712
- } else if ($deleteOp.check(opsI)) {
1713
- // nop
1754
+ /* c8 ignore start */
1714
1755
  } else {
1756
+ // remaining branches: opsI is deleteOp or something unknown
1757
+ // both branches are unreachable today: opNextUndeleted skips
1758
+ // delete ops, so opsI is never a delete during iteration; and the four
1759
+ // branches above exhaust the other op kinds. The deleteOp branch is
1760
+ // kept as a defensive no-op (drops a modify that lands in a deleted
1761
+ // region) rather than a throw.
1715
1762
  error.unexpectedCase()
1716
1763
  }
1717
1764
  } else {
1718
1765
  error.unexpectedCase()
1719
1766
  }
1767
+ /* c8 ignore stop */
1720
1768
  }
1721
1769
  // iterate backwards, to ensure that we merge all content
1722
1770
  for (let i = maybeMergeable.length - 1; i >= 0; i--) {
@@ -1772,9 +1820,12 @@ export class DeltaBuilder extends Delta {
1772
1820
  // @ts-ignore
1773
1821
  delete this.attrs[otherOp.key]
1774
1822
  }
1823
+ /* c8 ignore start */
1775
1824
  } else {
1825
+ // unreachable: attr ops are exhaustively setAttr | deleteAttr | modifyAttr
1776
1826
  error.unexpectedCase()
1777
1827
  }
1828
+ /* c8 ignore stop */
1778
1829
  }
1779
1830
  /**
1780
1831
  * Rebase children.
@@ -1831,7 +1882,10 @@ export class DeltaBuilder extends Delta {
1831
1882
  otherOffset = otherChild.length
1832
1883
  } else {
1833
1884
  if ($modifyOp.check(otherChild)) {
1834
- /** @type {any} */ (currChild.value).rebase(otherChild, priority)
1885
+ // _modValue (not .value) — ModifyOp.clone() marks its inner delta
1886
+ // as `done`, so a cloned ModifyOp can only be rebased after the
1887
+ // _modValue getter lazy-clones it back to mutable.
1888
+ currChild._modValue.rebase(otherChild.value, priority)
1835
1889
  } else if ($deleteOp.check(otherChild)) {
1836
1890
  list.remove(this.children, currChild)
1837
1891
  this.childCnt -= 1
@@ -1848,21 +1902,70 @@ export class DeltaBuilder extends Delta {
1848
1902
  * - insert: split curr op and insert retain
1849
1903
  */
1850
1904
  if ($retainOp.check(otherChild) || $modifyOp.check(otherChild)) {
1905
+ // Format reconciliation. priority=true is a no-op (currChild's format
1906
+ // wins). For !priority, currChild concedes any format key that
1907
+ // otherChild also writes — but only over the [currOffset..currOffset+
1908
+ // maxCommonLen] overlap. Split currChild around the overlap so the
1909
+ // prefix/suffix keep their original format and only the middle piece
1910
+ // carries the stripped format.
1911
+ if (
1912
+ !priority &&
1913
+ $retainOp.check(currChild) &&
1914
+ currChild.format != null &&
1915
+ otherChild.format != null
1916
+ ) {
1917
+ /** @type {FormattingAttributes} */
1918
+ const stripped = {}
1919
+ let strippedAny = false
1920
+ for (const k in currChild.format) {
1921
+ if (k in otherChild.format) {
1922
+ strippedAny = true
1923
+ } else {
1924
+ stripped[k] = currChild.format[k]
1925
+ }
1926
+ }
1927
+ if (strippedAny) {
1928
+ // split off the suffix [currOffset+maxCommonLen..length] if any
1929
+ if (currOffset + maxCommonLen < currChild.length) {
1930
+ const suffix = currChild.clone(currOffset + maxCommonLen, currChild.length)
1931
+ list.insertBetween(this.children, currChild, currChild.next, suffix)
1932
+ currChild._splice(currOffset + maxCommonLen, currChild.length - (currOffset + maxCommonLen))
1933
+ }
1934
+ // split off the prefix [0..currOffset] if any
1935
+ if (currOffset > 0) {
1936
+ const prefix = currChild.clone(0, currOffset)
1937
+ list.insertBetween(this.children, currChild.prev, currChild, prefix)
1938
+ currChild._splice(0, currOffset)
1939
+ currOffset = 0
1940
+ }
1941
+ // currChild now spans exactly the overlap. Replace its format.
1942
+ /** @type {any} */ (currChild).format = object.isEmpty(stripped) ? null : stripped
1943
+ currChild._fingerprint = null
1944
+ }
1945
+ }
1851
1946
  currOffset += maxCommonLen
1852
1947
  otherOffset += maxCommonLen
1853
1948
  } else if ($deleteOp.check(otherChild)) {
1854
1949
  if ($retainOp.check(currChild)) {
1855
1950
  // @ts-ignore
1856
1951
  currChild.retain -= maxCommonLen
1952
+ currChild._fingerprint = null
1857
1953
  } else if ($deleteOp.check(currChild)) {
1858
1954
  currChild.delete -= maxCommonLen
1955
+ currChild._fingerprint = null
1859
1956
  }
1860
1957
  this.childCnt -= maxCommonLen
1861
- } else { // insert/text.check(currOp)
1958
+ // advance other so subsequent currChild ops see what comes AFTER this
1959
+ // delete; without this we'd loop against the same delete forever and
1960
+ // never reach other's later inserts.
1961
+ otherOffset += maxCommonLen
1962
+ } else { // insert/text.check(otherChild)
1862
1963
  if (currOffset > 0) {
1863
- const leftPart = currChild.clone(currOffset)
1964
+ const leftPart = currChild.clone(0, currOffset)
1864
1965
  list.insertBetween(this.children, currChild.prev, currChild, leftPart)
1865
- currChild._splice(currOffset, currChild.length - currOffset)
1966
+ // leftPart is the prefix; currChild becomes the suffix. Remove the
1967
+ // prefix portion from currChild so it represents [currOffset..length].
1968
+ currChild._splice(0, currOffset)
1866
1969
  currOffset = 0
1867
1970
  }
1868
1971
  list.insertBetween(this.children, currChild.prev, currChild, new RetainOp(otherChild.length, null, null))
@@ -2000,8 +2103,10 @@ export class $Delta extends s.Schema {
2000
2103
  check (o, err = undefined) {
2001
2104
  const { $name, $attrs, $children, hasText, $formats } = this.shape
2002
2105
  if (!$deltaAny.check(o, err)) {
2106
+ /* c8 ignore next */
2003
2107
  err?.extend(null, 'Delta', o?.constructor.name, 'Constructor match failed')
2004
2108
  } else if (o.name != null && !$name.check(o.name, err)) {
2109
+ /* c8 ignore next */
2005
2110
  err?.extend('Delta.name', $name.toString(), o.name, 'Unexpected node name')
2006
2111
  } else if (list.toArray(o.children).some(c => (!hasText && $textOp.check(c)) || (hasText && $textOp.check(c) && c.format != null && !$formats.check(c.format)) || ($insertOp.check(c) && !c.insert.every(ins => $children.check(ins))))) {
2007
2112
  err?.extend('Delta.children', '', '', 'Children don\'t match the schema')
@@ -2097,7 +2202,7 @@ export const mergeDeltas = (a, b) => {
2097
2202
  c.apply(b)
2098
2203
  return /** @type {any} */ (c)
2099
2204
  }
2100
- return a == null ? b : (a || null)
2205
+ return /** @type {D} */ (a || b || null)
2101
2206
  }
2102
2207
 
2103
2208
  /**
@@ -2325,6 +2430,16 @@ class _DiffStringWrapper {
2325
2430
  */
2326
2431
 
2327
2432
  /**
2433
+ * Compute a delta that, when applied to `d1`, produces `d2`. Only the children and attributes of
2434
+ * `d1` and `d2` are compared; the top-level node names of `d1` and `d2` are *not*. Diffing
2435
+ * `<div>a</div>` against `<span>a</span>` is valid and yields an empty diff — they have the same
2436
+ * children and attributes, so as far as `diff` is concerned they are equal at the level it cares
2437
+ * about. The top-level name is treated as a document-type marker, not as diffable content.
2438
+ *
2439
+ * Names *are* compared on children: a child node whose name changes between `d1` and `d2` is
2440
+ * replaced wholesale (delete + insert), not converted into a `modify` op. Same-name child nodes
2441
+ * at aligned positions are paired and recursed into via `modify`.
2442
+ *
2328
2443
  * @template {DeltaConf} Conf
2329
2444
  * @param {Delta<Conf>} d1
2330
2445
  * @param {NoInfer<Delta<Conf>>} d2
@@ -2395,9 +2510,14 @@ export const diff = (d1, d2) => {
2395
2510
  cs2.push(left2.insert)
2396
2511
  } else if ($insertOp.check(left2)) {
2397
2512
  cs2.push(...left2.insert.map(ins => typeof ins === 'string' ? new _DiffStringWrapper(ins) : ins))
2513
+ /* c8 ignore start */
2398
2514
  } else {
2515
+ // unreachable for valid diff inputs (delete on the rhs would already
2516
+ // have been rejected via the `[lib0/delta] diffing deletes unsupported`
2517
+ // path above)
2399
2518
  error.unexpectedCase()
2400
2519
  }
2520
+ /* c8 ignore stop */
2401
2521
  formattingNeedsDiff ||= left2.format != null
2402
2522
  left2 = left2.next
2403
2523
  }
@@ -2459,9 +2579,14 @@ export const diff = (d1, d2) => {
2459
2579
  a = a.next
2460
2580
  aOffset = 0
2461
2581
  }
2582
+ /* c8 ignore start */
2462
2583
  } else {
2584
+ // unreachable: by this point both a and b are insert/text (deletes
2585
+ // were rejected upstream and `originalUpdated` is the result of an
2586
+ // apply, which keeps inserts only).
2463
2587
  error.unexpectedCase()
2464
2588
  }
2589
+ /* c8 ignore stop */
2465
2590
  }
2466
2591
  // @todo instead of applying, we want to first exec d, then formattingDiff - we need a merge
2467
2592
  // function!
@@ -2481,10 +2606,11 @@ export const diff = (d1, d2) => {
2481
2606
  } else {
2482
2607
  d.setAttr(key, nextVal)
2483
2608
  }
2609
+ /* c8 ignore start */
2484
2610
  } else {
2485
- /* c8 ignore next 2 */
2486
2611
  error.unexpectedCase()
2487
2612
  }
2613
+ /* c8 ignore stop */
2488
2614
  }
2489
2615
  }
2490
2616
  for (const { key } of d1.attrs) {
@@ -164,43 +164,116 @@ export const $transformer = transformerWith(s.$any, s.$any)
164
164
  */
165
165
 
166
166
  /**
167
- * @template {Array<Template>} TS
168
- * @template {delta.DeltaConf} IN
169
- * @typedef {TS extends [infer FirstT extends Template, ...infer RestT extends Template[]] ? ApplyPipe<RestT,ApplyTemplate<FirstT,IN>> : IN } ApplyPipe
167
+ * Marker for absent props in a NormalizedDeltaConf.
168
+ *
169
+ * @typedef {{ 'lib0:notset': true }} NotSet
170
170
  */
171
171
 
172
172
  /**
173
- * @template {string} AttrName
174
- * @template {delta.DeltaConf} IN
175
- * @typedef {{ name: 'lib0:value', attrs: { value: IN extends { attrs: { [K in AttrName]: infer V } } ? V : never }}} ApplyQueryAttr
173
+ * DeltaConf in normalized form: all props defined, absent props are set to NotSet.
174
+ *
175
+ * ApplyPipe iterates over this form (see ApplyPipeNorm).
176
+ *
177
+ * @template Name
178
+ * @template Attrs
179
+ * @template Children
180
+ * @template Text
181
+ * @template RecursiveChildren
182
+ * @template RecursiveAttrs
183
+ * @typedef {{ name: Name, attrs: Attrs, children: Children, text: Text, recursiveChildren: RecursiveChildren, recursiveAttrs: RecursiveAttrs }} NormalizedDeltaConf
184
+ */
185
+
186
+ /**
187
+ * @template {delta.DeltaConf} C
188
+ * @typedef {NormalizedDeltaConf<
189
+ * C extends { name: infer Name extends string } ? Name : NotSet,
190
+ * C extends { attrs: infer Attrs extends {[K:string|number]:any} } ? Attrs : NotSet,
191
+ * C extends { children: infer Children } ? Children : NotSet,
192
+ * C extends { text: infer Text extends boolean } ? Text : NotSet,
193
+ * C extends { recursiveChildren: infer RecursiveChildren extends boolean } ? RecursiveChildren : NotSet,
194
+ * C extends { recursiveAttrs: infer RecursiveAttrs extends boolean } ? RecursiveAttrs : NotSet
195
+ * >} NormalizeDeltaConf
196
+ */
197
+
198
+ /**
199
+ * Strip NotSet props from a NormalizedDeltaConf, producing a regular DeltaConf again.
200
+ *
201
+ * @template NC
202
+ * @typedef {{ [K in keyof NC as NC[K] extends NotSet ? never : K]: NC[K] } & {}} DenormalizeDeltaConf
176
203
  */
177
204
 
178
205
  /**
179
- * @template IN
180
- * @typedef {IN extends infer OUT extends delta.DeltaConf ? OUT : never} EnsureDeltaConf
206
+ * Intersect a prop of a Filter conf with the corresponding pipe conf prop. The prop is only kept
207
+ * if it is defined on both sides (mirrors ApplyExpectType).
208
+ *
209
+ * @template FilterProp
210
+ * @template PipeProp
211
+ * @typedef {FilterProp extends NotSet ? NotSet : PipeProp extends NotSet ? NotSet : FilterProp & PipeProp} FilterConfProp
212
+ */
213
+
214
+ /**
215
+ * Apply each Template to a NormalizedDeltaConf - must mirror the semantics of ApplyAttrRename /
216
+ * ApplyExpectType.
217
+ *
218
+ * This shape is tuned to stay below typescript's instantiation-depth limit (TS2589) for long
219
+ * pipes (~85 templates via pipe().init(), measured). What we learned:
220
+ *
221
+ * - The per-step destructure of NC is the load-bearing part: typescript resolves types lazily,
222
+ * and member inference out of the literal that was passed as a type argument in the previous
223
+ * step is what forces resolution of the accumulated conf. Without it (e.g. carrying the conf
224
+ * props as individual type params), the attrs accumulate as a deferred PropsRename chain and
225
+ * the limit hits at ~45 templates. Local annotations do NOT force resolution: `X & {}`,
226
+ * `X extends infer N ? ...`, and an inline `{ attrs: X } extends { attrs: infer A } ? ...`
227
+ * roundtrip were all measured to have no effect.
228
+ * - The recursion must carry a plain object literal. Wrapping the accumulator in a helper alias
229
+ * (even a trivial one like NormalizedDeltaConf) defers per step and rebuilds the chain.
230
+ * - The outer check must be on TS alone. Coupling NC into the check type (e.g.
231
+ * `[TS, NC] extends [[...], {...}]`) makes the conditional generic-deferred whenever the conf
232
+ * is generic, which sends constraint comparisons (e.g. Pipe<TS> vs Pipe<any>) into infinite
233
+ * recursion.
234
+ *
235
+ * @template {Array<Template>} TS
236
+ * @template NC
237
+ * @typedef {TS extends [infer FirstT extends Template, ...infer RestT extends Template[]]
238
+ * ? (NC extends { name: infer Name, attrs: infer Attrs extends {[K:string|number]:any}, children: infer Children, text: infer Text, recursiveChildren: infer RecursiveChildren, recursiveAttrs: infer RecursiveAttrs }
239
+ * ? ApplyPipeNorm<RestT,
240
+ * FirstT extends AttrRename<infer Renames> ? { name: Name, attrs: import('../ts.js').PropsRename<Attrs extends NotSet ? {} : Attrs, Renames>, children: Children, text: Text, recursiveChildren: RecursiveChildren, recursiveAttrs: RecursiveAttrs } :
241
+ * FirstT extends Filter<infer DConf extends delta.DeltaConf> ? (NormalizeDeltaConf<DConf> extends { name: infer FilterName, attrs: infer FilterAttrs, children: infer FilterChildren, text: infer FilterText, recursiveChildren: infer FilterRecursiveChildren, recursiveAttrs: infer FilterRecursiveAttrs } ? {
242
+ * name: FilterConfProp<FilterName, Name>,
243
+ * attrs: FilterAttrs extends NotSet ? NotSet : Attrs extends NotSet ? NotSet : import('../ts.js').PropsPickShared<FilterAttrs, Attrs>,
244
+ * children: FilterConfProp<FilterChildren, Children>,
245
+ * text: FilterConfProp<FilterText, Text>,
246
+ * recursiveChildren: FilterConfProp<FilterRecursiveChildren, RecursiveChildren>,
247
+ * recursiveAttrs: FilterConfProp<FilterRecursiveAttrs, RecursiveAttrs>
248
+ * } : never) :
249
+ * NC>
250
+ * : NC)
251
+ * : NC} ApplyPipeNorm
181
252
  */
182
253
 
183
254
  /**
184
- * @template {Template} T
255
+ * @template {Array<Template>} TS
185
256
  * @template {delta.DeltaConf} IN
186
- * @typedef {EnsureDeltaConf<
187
- * T extends AttrRename<infer Renames> ? ApplyAttrRename<Renames,IN> :
188
- * T extends Filter<infer DConf extends delta.DeltaConf> ? ApplyExpectType<DConf,IN> :
189
- * IN
190
- * >} ApplyTemplate
257
+ * @typedef {DenormalizeDeltaConf<ApplyPipeNorm<TS, NormalizeDeltaConf<IN>>>} ApplyPipe
258
+ */
259
+
260
+ /**
261
+ * @template {string} AttrName
262
+ * @template {delta.DeltaConf} IN
263
+ * @typedef {{ name: 'lib0:value', attrs: { value: IN extends { attrs: { [K in AttrName]: infer V } } ? V : never }}} ApplyQueryAttr
191
264
  */
192
265
 
193
266
  /**
194
267
  * Flattens nested Pipe instances into a single flat Template array.
195
268
  * Since pipe() always produces flat Pipes, Inner is already flat and
196
269
  * only one level of unwrapping is needed per Pipe element.
270
+ * Tail-recursive with an accumulator so the instantiation depth stays constant.
197
271
  *
198
272
  * @template {Array<Template>} TS
273
+ * @template {Array<Template>} [Acc=[]]
199
274
  * @typedef {TS extends [infer F extends Template, ...infer R extends Template[]]
200
- * ? F extends Pipe<infer Inner extends Template[]>
201
- * ? [...Inner, ...FlattenTemplates<R>]
202
- * : [F, ...FlattenTemplates<R>]
203
- * : []} FlattenTemplates
275
+ * ? FlattenTemplates<R, F extends Pipe<infer Inner extends Template[]> ? [...Acc, ...Inner] : [...Acc, F]>
276
+ * : Acc} FlattenTemplates
204
277
  */
205
278
 
206
279
  /**
@@ -563,7 +636,7 @@ export class ProjectionTransformer extends Transformer {
563
636
  /**
564
637
  * @template {delta.DeltaConf} A
565
638
  * @template {delta.DeltaConf} B
566
- * @template {Pipe<Template[]>} PipeTemplate
639
+ * @template {Pipe<any>} PipeTemplate
567
640
  * @extends {Transformer<A,B>}
568
641
  */
569
642
  export class PipeTransformer extends Transformer {
@@ -576,7 +649,7 @@ export class PipeTransformer extends Transformer {
576
649
  /**
577
650
  * @type {Transformer<any,any>[]}
578
651
  */
579
- this.ts = tpipe.templates.map(t => t.init(delta.$deltaAny))
652
+ this.ts = tpipe.templates.map((/** @type {Template} */ t) => t.init(delta.$deltaAny))
580
653
  }
581
654
 
582
655
  /**
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @module fnv1a
3
+ * FNV-1a (32bit) - a fast, non-cryptographic hash function.
4
+ * Spec: http://www.isthe.com/chongo/tech/comp/fnv/
5
+ */
6
+
7
+ import * as math from '../math.js'
8
+
9
+ /**
10
+ * FNV-1a offset basis (32bit).
11
+ */
12
+ export const offsetBasis = 0x811c9dc5
13
+
14
+ /**
15
+ * FNV-1a prime (32bit).
16
+ */
17
+ export const prime = 0x01000193
18
+
19
+ /**
20
+ * Compute the FNV-1a 32bit hash of a byte sequence.
21
+ *
22
+ * Pass the result of a previous call as `hash` to digest a message in chunks:
23
+ * `digest(concat(a, b)) === digest(b, digest(a))`.
24
+ *
25
+ * @param {Uint8Array} data
26
+ * @param {number} hash - continue hashing from a previous result (defaults to the offset basis)
27
+ * @return {number} unsigned 32bit hash
28
+ */
29
+ /* @__NO_SIDE_EFFECTS__ */
30
+ export const digest = (data, hash = offsetBasis) => {
31
+ for (let i = 0; i < data.length; i++) {
32
+ hash = math.imul(hash ^ data[i], prime)
33
+ }
34
+ return hash >>> 0
35
+ }
36
+
37
+ /**
38
+ * Compute the FNV-1a 32bit hash of a string - without allocating the utf8 encoding.
39
+ *
40
+ * Equivalent to `digest(string.encodeUtf8(str))`: the string is hashed as utf8 bytes; lone
41
+ * surrogates are hashed as the replacement character (like TextEncoder encodes them).
42
+ *
43
+ * Prefer this over `digest(string.encodeUtf8(str))` when hashing strings - it skips the
44
+ * TextEncoder call and the Uint8Array allocation, making it ~5x faster for small strings
45
+ * (<=20 chars, e.g. object keys) and ~2x faster for larger ones.
46
+ *
47
+ * @param {string} str
48
+ * @param {number} hash - continue hashing from a previous result (defaults to the offset basis)
49
+ * @return {number} unsigned 32bit hash
50
+ */
51
+ /* @__NO_SIDE_EFFECTS__ */
52
+ export const digestString = (str, hash = offsetBasis) => {
53
+ for (let i = 0; i < str.length; i++) {
54
+ let c = str.charCodeAt(i)
55
+ if (c < 0x80) {
56
+ hash = math.imul(hash ^ c, prime)
57
+ } else {
58
+ if (c >= 0xd800 && c < 0xe000) {
59
+ const lo = str.charCodeAt(i + 1) // NaN when out of bounds ⇒ NaN & x === 0
60
+ if (c < 0xdc00 && (lo & 0xfc00) === 0xdc00) {
61
+ c = 0x10000 + ((c & 0x3ff) << 10) + (lo & 0x3ff)
62
+ i++
63
+ } else {
64
+ c = 0xfffd // lone surrogate ⇒ replacement character
65
+ }
66
+ }
67
+ if (c < 0x800) {
68
+ hash = math.imul(hash ^ (0xc0 | (c >> 6)), prime)
69
+ } else {
70
+ if (c < 0x10000) {
71
+ hash = math.imul(hash ^ (0xe0 | (c >> 12)), prime)
72
+ } else {
73
+ hash = math.imul(hash ^ (0xf0 | (c >> 18)), prime)
74
+ hash = math.imul(hash ^ (0x80 | ((c >> 12) & 0x3f)), prime)
75
+ }
76
+ hash = math.imul(hash ^ (0x80 | ((c >> 6) & 0x3f)), prime)
77
+ }
78
+ hash = math.imul(hash ^ (0x80 | (c & 0x3f)), prime)
79
+ }
80
+ }
81
+ return hash >>> 0
82
+ }