@abraca/dabra 2.25.0 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -3189,7 +3189,7 @@ declare function makeEncryptedYText(ydoc: Y.Doc, fieldName: string, docKey: Cryp
3189
3189
  //#region packages/provider/src/TreeTimestamps.d.ts
3190
3190
  /**
3191
3191
  * Attach an observer that writes `updatedAt` to the root doc-tree entry for
3192
- * `childDocId` whenever the child doc receives a non-offline update.
3192
+ * `childDocId` whenever the child doc receives a local edit.
3193
3193
  *
3194
3194
  * @param treeMap The root doc's "doc-tree" Y.Map.
3195
3195
  * @param childDocId The child document's UUID (key in treeMap).
@@ -5280,13 +5280,16 @@ interface DocumentManagerConfig {
5280
5280
  */
5281
5281
  rootDocId?: string;
5282
5282
  /**
5283
- * PROTOTYPE (Path-1 doctree scaling): opt into TreeManager's in-memory
5284
- * parent→children index. When `false` (default), every tree walk
5285
- * re-scans the whole `doc-tree` Y.Map (O(n) per call, O() recursive
5286
- * traversal) the historical behaviour, byte-for-byte. When `true`,
5287
- * walks resolve against a lazily-rebuilt adjacency index (O(k) per
5288
- * lookup, O(result) traversal) and `tree.childrenOfPage()` becomes
5289
- * usable. Behind a flag so it ships dark until benchmarked.
5283
+ * TreeManager's in-memory parent→children index. When `true` (the
5284
+ * default), tree WALKS resolve against a lazily-rebuilt adjacency index
5285
+ * (O(k) per lookup, O(result) traversal), kept fresh by an `observeDeep`
5286
+ * dirty bit and rebuilt at most once per mutation batch; `childrenOfPage()`
5287
+ * becomes usable with stable (order,id) sibling ordering. Set `false` to
5288
+ * force the legacy whole-map scan (O(n) per call, O() recursive) — only
5289
+ * needed to reproduce the exact historical order-only sibling ordering.
5290
+ * Benchmarked at ~700× on deep walks. Index affects WALK reads only;
5291
+ * `readEntries`/`get` keep raw parentId either way. Lazy: no index or
5292
+ * observer is bound until the first walk read.
5290
5293
  */
5291
5294
  treeIndex?: boolean;
5292
5295
  }
@@ -5325,8 +5328,8 @@ declare class DocumentManager {
5325
5328
  get serverInfo(): ServerInfo | null;
5326
5329
  get rootDocId(): string | null;
5327
5330
  /**
5328
- * Whether the TreeManager in-memory index is enabled (Path-1 prototype).
5329
- * Off by default — see {@link DocumentManagerConfig.treeIndex}.
5331
+ * Whether the TreeManager in-memory index is enabled.
5332
+ * On by default — see {@link DocumentManagerConfig.treeIndex}.
5330
5333
  */
5331
5334
  get treeIndexEnabled(): boolean;
5332
5335
  get rootDocument(): Y.Doc | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/dabra",
3
- "version": "2.25.0",
3
+ "version": "2.27.0",
4
4
  "description": "abracadabra provider",
5
5
  "keywords": [
6
6
  "abracadabra",
@@ -41,7 +41,7 @@
41
41
  "yjs": "^13.6.8"
42
42
  },
43
43
  "devDependencies": {
44
- "@abraca/schema": "2.25.0"
44
+ "@abraca/schema": "2.27.0"
45
45
  },
46
46
  "scripts": {
47
47
  "test": "node --no-warnings --conditions=source --experimental-transform-types --test 'tests/*.test.ts'"
@@ -46,8 +46,7 @@ function xmlTextToMarkdown(xmlText: Y.XmlText): string {
46
46
 
47
47
  function elementTextContent(el: Y.XmlElement | Y.XmlFragment): string {
48
48
  const parts: string[] = [];
49
- for (let i = 0; i < el.length; i++) {
50
- const child = el.get(i);
49
+ for (const child of el.toArray()) {
51
50
  if (child instanceof Y.XmlText) {
52
51
  parts.push(xmlTextToMarkdown(child));
53
52
  } else if (child instanceof Y.XmlElement) {
@@ -242,8 +241,9 @@ function serializeList(
242
241
  indent: string,
243
242
  ): string {
244
243
  const lines: string[] = [];
245
- for (let i = 0; i < el.length; i++) {
246
- const item = el.get(i);
244
+ const items = el.toArray();
245
+ for (let i = 0; i < items.length; i++) {
246
+ const item = items[i];
247
247
  if (
248
248
  item instanceof Y.XmlElement &&
249
249
  item.nodeName === "listItem"
@@ -258,8 +258,7 @@ function serializeList(
258
258
 
259
259
  function serializeTaskList(el: Y.XmlElement, indent: string): string {
260
260
  const lines: string[] = [];
261
- for (let i = 0; i < el.length; i++) {
262
- const item = el.get(i);
261
+ for (const item of el.toArray()) {
263
262
  if (
264
263
  item instanceof Y.XmlElement &&
265
264
  item.nodeName === "taskItem"
@@ -277,8 +276,7 @@ function serializeTaskList(el: Y.XmlElement, indent: string): string {
277
276
  function serializeTable(el: Y.XmlElement): string {
278
277
  const rows: string[][] = [];
279
278
 
280
- for (let i = 0; i < el.length; i++) {
281
- const row = el.get(i);
279
+ for (const row of el.toArray()) {
282
280
  if (
283
281
  !(row instanceof Y.XmlElement) ||
284
282
  row.nodeName !== "tableRow"
@@ -286,8 +284,7 @@ function serializeTable(el: Y.XmlElement): string {
286
284
  continue;
287
285
 
288
286
  const cells: string[] = [];
289
- for (let j = 0; j < row.length; j++) {
290
- const cell = row.get(j);
287
+ for (const cell of row.toArray()) {
291
288
  if (cell instanceof Y.XmlElement) {
292
289
  cells.push(elementTextContent(cell));
293
290
  }
@@ -346,8 +343,7 @@ function serializeChildren(
346
343
  indent: string,
347
344
  ): string {
348
345
  const parts: string[] = [];
349
- for (let i = 0; i < el.length; i++) {
350
- const child = el.get(i);
346
+ for (const child of el.toArray()) {
351
347
  if (child instanceof Y.XmlElement) {
352
348
  const serialized = serializeElement(child, indent);
353
349
  if (serialized) parts.push(serialized);
@@ -373,8 +369,9 @@ export function yjsToMarkdown(
373
369
  let title = "Untitled";
374
370
  const bodyParts: string[] = [];
375
371
 
376
- for (let i = 0; i < fragment.length; i++) {
377
- const child = fragment.get(i);
372
+ // Iterate via toArray() (O(n)). Indexed fragment.get(i) is an O(i)
373
+ // linked-list walk, which made this top-level loop O() on large docs.
374
+ for (const child of fragment.toArray()) {
378
375
  if (!(child instanceof Y.XmlElement)) continue;
379
376
 
380
377
  if (child.nodeName === "documentHeader") {
@@ -1805,8 +1802,8 @@ export interface DocumentBlock {
1805
1802
 
1806
1803
  export function readBlocksFromFragment(fragment: Y.XmlFragment): DocumentBlock[] {
1807
1804
  const result: DocumentBlock[] = [];
1808
- for (let i = 0; i < fragment.length; i++) {
1809
- const child = fragment.get(i);
1805
+ // toArray() is O(n); indexed fragment.get(i) would make this O(n²).
1806
+ for (const child of fragment.toArray()) {
1810
1807
  if (!(child instanceof Y.XmlElement)) continue;
1811
1808
  const name = child.nodeName;
1812
1809
  if (name === "documentHeader" || name === "documentMeta") continue;
@@ -75,13 +75,16 @@ export interface DocumentManagerConfig {
75
75
  */
76
76
  rootDocId?: string;
77
77
  /**
78
- * PROTOTYPE (Path-1 doctree scaling): opt into TreeManager's in-memory
79
- * parent→children index. When `false` (default), every tree walk
80
- * re-scans the whole `doc-tree` Y.Map (O(n) per call, O() recursive
81
- * traversal) the historical behaviour, byte-for-byte. When `true`,
82
- * walks resolve against a lazily-rebuilt adjacency index (O(k) per
83
- * lookup, O(result) traversal) and `tree.childrenOfPage()` becomes
84
- * usable. Behind a flag so it ships dark until benchmarked.
78
+ * TreeManager's in-memory parent→children index. When `true` (the
79
+ * default), tree WALKS resolve against a lazily-rebuilt adjacency index
80
+ * (O(k) per lookup, O(result) traversal), kept fresh by an `observeDeep`
81
+ * dirty bit and rebuilt at most once per mutation batch; `childrenOfPage()`
82
+ * becomes usable with stable (order,id) sibling ordering. Set `false` to
83
+ * force the legacy whole-map scan (O(n) per call, O() recursive) — only
84
+ * needed to reproduce the exact historical order-only sibling ordering.
85
+ * Benchmarked at ~700× on deep walks. Index affects WALK reads only;
86
+ * `readEntries`/`get` keep raw parentId either way. Lazy: no index or
87
+ * observer is bound until the first walk read.
85
88
  */
86
89
  treeIndex?: boolean;
87
90
  }
@@ -206,11 +209,11 @@ export class DocumentManager {
206
209
  }
207
210
 
208
211
  /**
209
- * Whether the TreeManager in-memory index is enabled (Path-1 prototype).
210
- * Off by default — see {@link DocumentManagerConfig.treeIndex}.
212
+ * Whether the TreeManager in-memory index is enabled.
213
+ * On by default — see {@link DocumentManagerConfig.treeIndex}.
211
214
  */
212
215
  get treeIndexEnabled(): boolean {
213
- return this._config.treeIndex ?? false;
216
+ return this._config.treeIndex ?? true;
214
217
  }
215
218
 
216
219
  get rootDocument(): Y.Doc | null {
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * TreeTimestamps
3
3
  *
4
- * Attaches an afterUpdate observer on a child Y.Doc so that whenever a
5
- * non-offline update is applied, the `updatedAt` timestamp on the
6
- * corresponding entry in the root doc's `doc-tree` map is written.
4
+ * Attaches an afterTransaction observer on a child Y.Doc so that whenever a
5
+ * LOCAL edit is made, the `updatedAt` timestamp on the corresponding entry
6
+ * in the root doc's `doc-tree` map is written.
7
7
  *
8
8
  * This propagates "last edited" timestamps to all peers via the root CRDT,
9
9
  * without requiring any server-side changes.
@@ -20,7 +20,7 @@ import type { OfflineStore } from "./OfflineStore.ts";
20
20
 
21
21
  /**
22
22
  * Attach an observer that writes `updatedAt` to the root doc-tree entry for
23
- * `childDocId` whenever the child doc receives a non-offline update.
23
+ * `childDocId` whenever the child doc receives a local edit.
24
24
  *
25
25
  * @param treeMap The root doc's "doc-tree" Y.Map.
26
26
  * @param childDocId The child document's UUID (key in treeMap).
@@ -64,8 +64,17 @@ export function attachUpdatedAtObserver(
64
64
  writeTs(ts);
65
65
  }
66
66
 
67
- function handler(_update: Uint8Array, origin: unknown): void {
68
- if (offlineStore !== null && origin === offlineStore) return;
67
+ function handler(tr: Y.Transaction): void {
68
+ // Only LOCAL edits stamp updatedAt. Remote updates (tr.local === false:
69
+ // initial sync replay, other peers' edits, offline-store replay,
70
+ // cross-tab broadcast) are stamped by the client that made them and the
71
+ // timestamp propagates through the root CRDT — counting them here
72
+ // turned "last edited" into "last synced on this client": merely
73
+ // opening a doc bumped its updatedAt to now.
74
+ if (!tr.local) return;
75
+ if (offlineStore !== null && tr.origin === offlineStore) return;
76
+ // No-op transactions (nothing actually changed) don't count as edits.
77
+ if (tr.changed.size === 0) return;
69
78
 
70
79
  const now = Date.now();
71
80
  if (now - lastFlushedAt >= throttleMs) {
@@ -80,10 +89,10 @@ export function attachUpdatedAtObserver(
80
89
  }
81
90
  }
82
91
 
83
- childDoc.on("update", handler);
92
+ childDoc.on("afterTransaction", handler);
84
93
 
85
94
  return () => {
86
- childDoc.off("update", handler);
95
+ childDoc.off("afterTransaction", handler);
87
96
  if (timer !== null) {
88
97
  clearTimeout(timer);
89
98
  flushPending();