lib0 1.0.0-rc.7 → 1.0.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lib0",
3
- "version": "1.0.0-rc.7",
3
+ "version": "1.0.0-rc.9",
4
4
  "description": "",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -272,6 +272,7 @@
272
272
  "devDependencies": {
273
273
  "@types/node": "^24.11.0",
274
274
  "c8": "^10.1.3",
275
+ "dpdm": "^4.0.1",
275
276
  "standard": "^17.1.0",
276
277
  "typescript": "^5.9.3"
277
278
  },
@@ -280,15 +281,15 @@
280
281
  "types": "rm -rf types && tsc -p tsconfig.build.json",
281
282
  "dist": "npm run clean && npm run types",
282
283
  "debug": "npm run gentesthtml && node ./src/bin/0serve.js -o test.html",
283
- "test": "c8 --check-coverage --lines 100 --branches 100 --functions 100 --statements 100 node --unhandled-rejections=strict ./src/test.js --repetition-time 50",
284
+ "test": "c8 --check-coverage --lines 97 --branches 96 --functions 94 --statements 97 node --unhandled-rejections=strict ./src/test.js",
285
+ "test:deno": "deno run -A src/test.js",
284
286
  "test-inspect": "node --inspect-brk --unhandled-rejections=strict ./src/test.js --repetition-time 50",
285
- "test-extensive": "c8 --check-coverage --lines 100 --branches 100 --functions 100 --statements 100 node ./src/test.js --repetition-time 30000 --extensive",
287
+ "test-extensive": "node ./src/test.js --repetition-time 1000 --extensive",
286
288
  "test:bundle": "esbuild bundle.test.js --bundle --outfile=dist/bundle.test.js --analyze --minify && wc -c dist/bundle.test.js",
287
289
  "trace-deopt": "clear && node --trace-deopt src/test.js",
288
290
  "trace-opt": "clear && node --trace-opt src/test.js",
289
- "lint": "tsc && standard",
291
+ "lint": "tsc && standard && npx dpdm --exit-code circular:1 --warning false --tree false src/test.js",
290
292
  "gendocs": "node ./bin/gendocs.js",
291
- "release": "npm run dist && np --test-script dist",
292
293
  "version": "npm run clean && npm run dist && git add README.md",
293
294
  "postpublish": "npm run clean",
294
295
  "gentesthtml": "node ./src/bin/gentesthtml.js --script src/test.js > test.html"
@@ -44,6 +44,10 @@ class LocalStoragePolyfill {
44
44
  * @type {null|function({data:Uint8Array}):void}
45
45
  */
46
46
  this.onmessage = null
47
+ /**
48
+ * @type {null|((this: BroadcastChannel, ev: MessageEvent) => any)}
49
+ */
50
+ this.onmessageerror = null
47
51
  /**
48
52
  * @param {any} e
49
53
  */
@@ -79,8 +83,9 @@ const getChannel = room =>
79
83
  /**
80
84
  * @param {{data:ArrayBuffer}} e
81
85
  */
82
- /* c8 ignore next */
86
+ /* c8 ignore next 3 */
83
87
  bc.onmessage = e => subs.forEach(sub => sub(e.data, 'broadcastchannel'))
88
+ bc.onmessageerror = e => console.error('broadcastchannel error', e)
84
89
  return {
85
90
  bc, subs
86
91
  }
@@ -972,6 +972,9 @@ class DeltaData {
972
972
  * >}
973
973
  */
974
974
  this.children = /** @type {any} */ (list.create())
975
+ /**
976
+ * All child-ops sizes combined. Note that delete-ops also have a length
977
+ */
975
978
  this.childCnt = 0
976
979
  /**
977
980
  * @type {any}
@@ -982,6 +985,10 @@ class DeltaData {
982
985
  */
983
986
  this._fingerprint = null
984
987
  this.isDone = false
988
+ /**
989
+ * Is the final document and does not contain delete, modify, or retain ops.
990
+ */
991
+ this.isFinal = false
985
992
  }
986
993
  }
987
994
 
@@ -1461,9 +1468,21 @@ export class DeltaBuilder extends Delta {
1461
1468
  }
1462
1469
 
1463
1470
  /**
1471
+ * Apply other delta on this op. The result is the merged op of both of them.
1472
+ *
1473
+ * a.apply(b.apply(c))
1474
+ *
1475
+ * is equivalent to
1476
+ *
1477
+ * a.apply(b).apply(c)
1478
+ *
1479
+ * @todo fuzz test the above property
1480
+ *
1464
1481
  * @param {Delta<Conf>?} other
1482
+ * @param {{ final?: boolean }} opts -- experimental
1483
+ * @return {Delta<Conf>}
1465
1484
  */
1466
- apply (other) {
1485
+ apply (other, { final = this.isFinal } = {}) {
1467
1486
  if (other == null) return this
1468
1487
  modDeltaCheck(this)
1469
1488
  this.$schema?.expect(other)
@@ -1486,8 +1505,13 @@ export class DeltaBuilder extends Delta {
1486
1505
  this.attrs[op.key] = op.clone()
1487
1506
  } else if ($deleteAttrOp.check(op)) {
1488
1507
  op.prevValue = c?.value
1489
- // @ts-ignore
1490
- delete this.attrs[op.key]
1508
+ if (final) {
1509
+ // @ts-ignore
1510
+ delete this.attrs[op.key]
1511
+ } else {
1512
+ // @ts-ignore
1513
+ this.attrs[op.key] = op.clone()
1514
+ }
1491
1515
  }
1492
1516
  }
1493
1517
  // apply children
@@ -1495,6 +1519,9 @@ export class DeltaBuilder extends Delta {
1495
1519
  * @type {ChildrenOpAny?}
1496
1520
  */
1497
1521
  let opsI = this.children.start
1522
+ if ($deleteOp.check(opsI)) {
1523
+ opsI = opNextUndeleted(opsI)
1524
+ }
1498
1525
  let offset = 0
1499
1526
  /**
1500
1527
  * At the end, we will try to merge this op, and op.next op with their respective previous op.
@@ -1506,57 +1533,63 @@ export class DeltaBuilder extends Delta {
1506
1533
  */
1507
1534
  const maybeMergeable = []
1508
1535
  /**
1509
- * @template {InsertOp<any>|RetainOp|DeleteOp|TextOp|ModifyOp<any>|null} OP
1510
- * @param {OP} op
1511
- * @return {OP}
1536
+ * Schedule an op to be merged later
1537
+ * @param {ChildrenOpAny?} op
1538
+ * @return {ChildrenOpAny}
1512
1539
  */
1513
1540
  const scheduleForMerge = op => {
1514
1541
  op && maybeMergeable.push(op)
1515
- return op
1542
+ return /** @type {any} */ (op)
1543
+ }
1544
+
1545
+ /**
1546
+ * Split opsI at the current offset - ensuring that we can insert safely at the current position
1547
+ */
1548
+ const splitHere = () => {
1549
+ if (offset > 0 && opsI != null) {
1550
+ const rightPart = scheduleForMerge(opsI.clone(offset))
1551
+ opsI._splice(offset, opsI.length - offset)
1552
+ list.insertBetween(this.children, opsI, opsI.next || null, rightPart)
1553
+ offset = 0
1554
+ opsI = rightPart
1555
+ }
1556
+ }
1557
+ /**
1558
+ * @param {ChildrenOpAny} op
1559
+ */
1560
+ const insertClonedOp = op => {
1561
+ splitHere()
1562
+ list.insertBetween(this.children, opsI == null ? this.children.end : opsI.prev, opsI, scheduleForMerge(op.clone()))
1563
+ this.childCnt += op.length
1516
1564
  }
1517
- other.children.forEach(op => {
1565
+ for (const op of other.children) {
1566
+ if (opsI?.length === offset) {
1567
+ opsI = opNextUndeleted(opsI)
1568
+ offset = 0
1569
+ }
1518
1570
  if ($textOp.check(op) || $insertOp.check(op)) {
1519
- if (offset === 0) {
1520
- list.insertBetween(this.children, opsI == null ? this.children.end : opsI.prev, opsI, scheduleForMerge(op.clone()))
1521
- } else {
1522
- // @todo inmplement "splitHelper" and "insertHelper" - I'm splitting all the time and
1523
- // forget to update opsI
1524
- if (opsI == null) error.unexpectedCase()
1525
- const cpy = scheduleForMerge(opsI.clone(offset))
1526
- opsI._splice(offset, opsI.length - offset)
1527
- list.insertBetween(this.children, opsI, opsI.next || null, cpy)
1528
- list.insertBetween(this.children, opsI, cpy || null, scheduleForMerge(op.clone()))
1529
- opsI = cpy
1530
- offset = 0
1531
- }
1532
- this.childCnt += op.insert.length
1571
+ insertClonedOp(op)
1533
1572
  } else if ($retainOp.check(op)) {
1534
1573
  let retainLen = op.length
1535
-
1536
- if (offset > 0 && opsI != null && op.format != null && !$deleteOp.check(opsI) && !object.every(op.format, (v, k) => fun.equalityDeep(v, /** @type {InsertOp<any>|RetainOp|ModifyOp} */ (opsI).format?.[k] || null))) {
1574
+ if (offset > 0 && opsI != null && op.format != null && !$deleteOp.check(opsI) && (/** @type {InsertOp<any>|RetainOp|ModifyOp} */ (opsI).format == null || !fun.equalityDeep(opsI.format, op.format))) {
1537
1575
  // need to split current op
1538
- const cpy = scheduleForMerge(opsI.clone(offset))
1539
- opsI._splice(offset, opsI.length - offset)
1540
- list.insertBetween(this.children, opsI, opsI.next || null, cpy)
1541
- opsI = cpy
1542
- offset = 0
1576
+ splitHere()
1543
1577
  }
1544
-
1545
1578
  while (opsI != null && opsI.length - offset <= retainLen) {
1546
- op.format != null && updateOpFormat(opsI, op.format)
1579
+ if (op.format != null) {
1580
+ updateOpFormat(opsI, op.format)
1581
+ scheduleForMerge(opsI)
1582
+ }
1547
1583
  retainLen -= opsI.length - offset
1548
- opsI = opsI?.next || null
1584
+ opsI = opNextUndeleted(opsI)
1549
1585
  offset = 0
1550
1586
  }
1551
-
1552
1587
  if (opsI != null) {
1553
1588
  if (op.format != null && retainLen > 0) {
1554
- // split current op and apply format
1555
- const cpy = scheduleForMerge(opsI.clone(retainLen))
1556
- opsI._splice(retainLen, opsI.length - retainLen)
1557
- list.insertBetween(this.children, opsI, opsI.next || null, cpy)
1558
- updateOpFormat(opsI, op.format)
1559
- opsI = cpy
1589
+ offset = retainLen
1590
+ splitHere()
1591
+ updateOpFormat(/** @type {ChildrenOpAny} */ (opsI.prev), op.format)
1592
+ scheduleForMerge(opsI.prev)
1560
1593
  } else {
1561
1594
  offset += retainLen
1562
1595
  }
@@ -1571,17 +1604,22 @@ export class DeltaBuilder extends Delta {
1571
1604
  list.pushEnd(this.children, scheduleForMerge(new DeleteOp(remainingLen, null)))
1572
1605
  this.childCnt += remainingLen
1573
1606
  break
1574
- } else if ($deleteOp.check(opsI)) {
1575
- const delLen = opsI.length - offset
1576
- // the same content can't be deleted twice, remove duplicated deletes
1577
- if (delLen >= remainingLen) {
1607
+ } else if ($retainOp.check(opsI)) { // retain ⇒ splice retain, insert delete
1608
+ const delLen = math.min(opsI.length - offset, remainingLen)
1609
+ opsI._splice(offset, delLen)
1610
+ this.childCnt -= delLen
1611
+ if (opsI.length === 0) {
1612
+ list.remove(this.children, opsI)
1613
+ opsI = opNextUndeleted(opsI)
1614
+ offset = 0
1615
+ }
1616
+ if (opsI?.length === offset) {
1617
+ opsI = opNextUndeleted(opsI)
1578
1618
  offset = 0
1579
- opsI = opsI.next
1580
- } else {
1581
- offset += remainingLen
1582
1619
  }
1620
+ insertClonedOp(new DeleteOp(delLen, null))
1583
1621
  remainingLen -= delLen
1584
- } else { // insert / embed / retain / modify ⇒ replace
1622
+ } else if (!$deleteOp.check(opsI)) { // insert / embed / modify ⇒ replace
1585
1623
  // case1: delete o fully
1586
1624
  // case2: delete some part of beginning
1587
1625
  // case3: delete some part of end
@@ -1590,38 +1628,38 @@ export class DeltaBuilder extends Delta {
1590
1628
  this.childCnt -= delLen
1591
1629
  if (opsI.length === delLen) {
1592
1630
  // case 1
1593
- offset = 0
1594
1631
  list.remove(this.children, opsI)
1595
- scheduleForMerge(opsI = opsI.next)
1632
+ scheduleForMerge(opsI = opNextUndeleted(opsI))
1633
+ offset = 0
1596
1634
  } else if (offset === 0) {
1597
1635
  // case 2
1598
1636
  offset = 0
1599
- opsI._splice(0, delLen)
1637
+ opsI._splice(offset, delLen)
1600
1638
  } else if (offset + delLen === opsI.length) {
1601
1639
  // case 3
1602
1640
  opsI._splice(offset, delLen)
1641
+ opsI = opNextUndeleted(opsI)
1603
1642
  offset = 0
1604
- opsI = opsI.next
1605
1643
  } else {
1606
1644
  // case 4
1607
1645
  opsI._splice(offset, delLen)
1608
1646
  }
1609
1647
  remainingLen -= delLen
1648
+ } else {
1649
+ error.unexpectedCase()
1610
1650
  }
1611
1651
  }
1612
1652
  } else if ($modifyOp.check(op)) {
1613
1653
  if (opsI == null) {
1614
1654
  list.pushEnd(this.children, op.clone())
1615
1655
  this.childCnt += 1
1616
- return
1617
- }
1618
- if ($modifyAttrOp.check(opsI)) {
1619
- opsI._modValue.apply(/** @type {any} */ (op.value))
1620
- opsI = opsI.next
1656
+ } else if ($modifyOp.check(opsI)) {
1657
+ opsI._modValue.apply(/** @type {any} */ (op.value), { final })
1658
+ opsI = opNextUndeleted(opsI)
1621
1659
  } else if ($insertOp.check(opsI)) {
1622
- opsI._modValue(offset).apply(op.value)
1660
+ opsI._modValue(offset).apply(op.value, { final })
1623
1661
  if (opsI.length === ++offset) {
1624
- opsI = opsI.next
1662
+ opsI = opNextUndeleted(opsI)
1625
1663
  offset = 0
1626
1664
  }
1627
1665
  } else if ($retainOp.check(opsI)) {
@@ -1631,7 +1669,9 @@ export class DeltaBuilder extends Delta {
1631
1669
  list.insertBetween(this.children, opsI.prev, opsI, cpy) // insert skipped len
1632
1670
  offset = 0
1633
1671
  }
1634
- list.insertBetween(this.children, opsI.prev, opsI, scheduleForMerge(op.clone())) // insert skipped len
1672
+ const insertModOp = scheduleForMerge(op.clone())
1673
+ opsI.format && updateOpFormat(insertModOp, opsI.format)
1674
+ list.insertBetween(this.children, opsI.prev, opsI, insertModOp) // insert skipped len
1635
1675
  if (opsI.length === 1) {
1636
1676
  list.remove(this.children, opsI)
1637
1677
  } else {
@@ -1646,7 +1686,7 @@ export class DeltaBuilder extends Delta {
1646
1686
  } else {
1647
1687
  error.unexpectedCase()
1648
1688
  }
1649
- })
1689
+ }
1650
1690
  // iterate backwards, to ensure that we merge all content
1651
1691
  for (let i = maybeMergeable.length - 1; i >= 0; i--) {
1652
1692
  const op = maybeMergeable[i]
@@ -1660,6 +1700,9 @@ export class DeltaBuilder extends Delta {
1660
1700
  }
1661
1701
 
1662
1702
  /**
1703
+ * Rebase this op against a concurrent op. We can apply this op on a doc that already applied
1704
+ * `other` op.
1705
+ *
1663
1706
  * @param {DeltaAny} other
1664
1707
  * @param {boolean} priority
1665
1708
  */
@@ -1863,12 +1906,23 @@ const updateOpFormat = (op, formatUpdate) => {
1863
1906
  /** @type {any} */ (op).format = object.assign({}, op.format, { [k]: v })
1864
1907
  } else if (op.format != null) {
1865
1908
  const { [k]: _, ...rest } = op.format
1866
- ;/** @type {any} */ (op).format = rest
1909
+ ;/** @type {any} */ (op).format = object.isEmpty(rest) ? null : rest
1867
1910
  }
1868
1911
  }
1869
1912
  }
1870
1913
  }
1871
1914
 
1915
+ /**
1916
+ * @param {ChildrenOpAny?} op
1917
+ */
1918
+ const opNextUndeleted = op => {
1919
+ op = op?.next || null
1920
+ while (op != null && $deleteOp.check(op)) {
1921
+ op = op.next
1922
+ }
1923
+ return op
1924
+ }
1925
+
1872
1926
  /**
1873
1927
  * @template {DeltaConf} Conf
1874
1928
  * @extends {s.Schema<Delta<Conf>>}
@@ -2361,6 +2415,8 @@ export const diff = (d1, d2) => {
2361
2415
  error.unexpectedCase()
2362
2416
  }
2363
2417
  }
2418
+ // @todo instead of applying, we want to first exec d, then formattingDiff - we need a merge
2419
+ // function!
2364
2420
  d.apply(formattingDiff)
2365
2421
  }
2366
2422
  for (const attr2 of d2.attrs) {
@@ -13,6 +13,14 @@ import * as f from './function.js'
13
13
  /* c8 ignore next */
14
14
  export const isNode = /* @__PURE__ */(() => typeof process !== 'undefined' && process.release && /node|io\.js/.test(process.release.name) && Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]')()
15
15
 
16
+ /**
17
+ * True iff this script is running in deno
18
+ * @type {boolean}
19
+ */
20
+ /* c8 ignore next 2 */
21
+ // @ts-ignore
22
+ export const isDeno = /* @__PURE__ */(() => typeof Deno !== 'undefined')()
23
+
16
24
  /* c8 ignore next */
17
25
  export const isBrowser = /* @__PURE__ */(() => typeof window !== 'undefined' && typeof document !== 'undefined' && !isNode)()
18
26
  /* c8 ignore next */
package/src/schema.js CHANGED
@@ -487,7 +487,7 @@ export class $StringTemplate extends Schema {
487
487
  check (o, err) {
488
488
  const c = this._r.exec(o) != null
489
489
  /* c8 ignore next */
490
- !c && err?.extend(null, this._r.toString(), o+'', 'String doesn\'t match string template.')
490
+ !c && err?.extend(null, this._r.toString(), o + '', 'String doesn\'t match string template.')
491
491
  return c
492
492
  }
493
493
  }
@@ -1,4 +1,4 @@
1
- export const EqualityTraitSymbol = Symbol('Equality')
1
+ export const EqualityTraitSymbol = Symbol.for('0:equality')
2
2
 
3
3
  /**
4
4
  * @typedef {{ [EqualityTraitSymbol]:(other:EqualityTrait)=>boolean }} EqualityTrait
@@ -2,7 +2,7 @@ import * as encoding from '../encoding.js'
2
2
  import * as rabin from '../hash/rabin.js'
3
3
  import * as buffer from '../buffer.js'
4
4
 
5
- export const FingerprintTraitSymbol = Symbol('Fingerprint')
5
+ export const FingerprintTraitSymbol = Symbol.for('0:fingerprint')
6
6
 
7
7
  /**
8
8
  * When implementing this trait, it is recommended to write some sort of "magic number" first to
@@ -658,10 +658,27 @@ export class DeltaBuilder<Conf extends DeltaConf = {}, FixedConf extends boolean
658
658
  attrs: AddToAttrs<DeltaConfGetAttrs<Conf>, Key, D>;
659
659
  }>, FixedConf>;
660
660
  /**
661
+ * Apply other delta on this op. The result is the merged op of both of them.
662
+ *
663
+ * a.apply(b.apply(c))
664
+ *
665
+ * is equivalent to
666
+ *
667
+ * a.apply(b).apply(c)
668
+ *
669
+ * @todo fuzz test the above property
670
+ *
661
671
  * @param {Delta<Conf>?} other
672
+ * @param {{ final?: boolean }} opts -- experimental
673
+ * @return {Delta<Conf>}
662
674
  */
663
- apply(other: Delta<Conf> | null): this;
675
+ apply(other: Delta<Conf> | null, { final }?: {
676
+ final?: boolean;
677
+ }): Delta<Conf>;
664
678
  /**
679
+ * Rebase this op against a concurrent op. We can apply this op on a doc that already applied
680
+ * `other` op.
681
+ *
665
682
  * @param {DeltaAny} other
666
683
  * @param {boolean} priority
667
684
  */
@@ -1119,6 +1136,9 @@ declare class DeltaData<Name extends string, Attrs extends { [K in string | numb
1119
1136
  * >}
1120
1137
  */
1121
1138
  children: list.List<(Text extends true ? (RetainOp | TextOp | DeleteOp<any>) : never) | (RetainOp | InsertOp<Children> | DeleteOp<any> | (Delta extends Children ? ModifyOp<Extract<Children, Delta>> : never))>;
1139
+ /**
1140
+ * All child-ops sizes combined. Note that delete-ops also have a length
1141
+ */
1122
1142
  childCnt: number;
1123
1143
  /**
1124
1144
  * @type {any}
@@ -1129,6 +1149,10 @@ declare class DeltaData<Name extends string, Attrs extends { [K in string | numb
1129
1149
  */
1130
1150
  _fingerprint: string | null;
1131
1151
  isDone: boolean;
1152
+ /**
1153
+ * Is the final document and does not contain delete, modify, or retain ops.
1154
+ */
1155
+ isFinal: boolean;
1132
1156
  }
1133
1157
  import * as prng from '../prng.js';
1134
1158
  export {};
@@ -1,4 +1,9 @@
1
1
  export const isNode: boolean;
2
+ /**
3
+ * True iff this script is running in deno
4
+ * @type {boolean}
5
+ */
6
+ export const isDeno: boolean;
2
7
  export const isBrowser: boolean;
3
8
  export const isMac: boolean;
4
9
  export function hasParam(name: string): boolean;