@abraca/dabra 2.4.0 → 2.5.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.
@@ -3281,22 +3281,23 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
3281
3281
  * errors across async await boundaries.
3282
3282
  */
3283
3283
  async flushPendingUpdates() {
3284
- if (!this.canWrite) return;
3285
3284
  const store = this.offlineStore;
3286
3285
  if (!store) return;
3287
- const updates = await store.getPendingUpdates();
3288
- if (updates.length > 0) {
3289
- for (const update of updates) this.send(UpdateMessage, {
3290
- update,
3291
- documentName: this.configuration.name
3286
+ if (this.canWrite) {
3287
+ const updates = await store.getPendingUpdates();
3288
+ if (updates.length > 0) {
3289
+ for (const update of updates) this.send(UpdateMessage, {
3290
+ update,
3291
+ documentName: this.configuration.name
3292
+ });
3293
+ await store.clearPendingUpdates();
3294
+ }
3295
+ const pendingSubdocs = await store.getPendingSubdocs();
3296
+ for (const { childId } of pendingSubdocs) this.send(SubdocMessage, {
3297
+ documentName: this.configuration.name,
3298
+ childDocumentName: childId
3292
3299
  });
3293
- await store.clearPendingUpdates();
3294
3300
  }
3295
- const pendingSubdocs = await store.getPendingSubdocs();
3296
- for (const { childId } of pendingSubdocs) this.send(SubdocMessage, {
3297
- documentName: this.configuration.name,
3298
- childDocumentName: childId
3299
- });
3300
3301
  const snapshot = yjs.encodeStateAsUpdate(this.document);
3301
3302
  await store.saveDocSnapshot(snapshot).catch(() => null);
3302
3303
  }
@@ -10366,6 +10367,7 @@ function fromBase64$3(b64) {
10366
10367
  }
10367
10368
  var AbracadabraClient = class {
10368
10369
  constructor(config) {
10370
+ this.rootListInflight = /* @__PURE__ */ new Map();
10369
10371
  this.baseUrl = config.url.replace(/\/+$/, "");
10370
10372
  this.persistAuth = config.persistAuth ?? typeof localStorage !== "undefined";
10371
10373
  this.storageKey = config.storageKey ?? "abracadabra:auth";
@@ -10769,11 +10771,21 @@ var AbracadabraClient = class {
10769
10771
  * recursive tree walks; callers that need it can read `meta.id` from
10770
10772
  * the returned metas.
10771
10773
  */
10772
- async listChildren(parentId) {
10773
- const path = parentId ? `/docs/${encodeURIComponent(parentId)}/children` : "/docs?root=true";
10774
- const res = await this.request("GET", path);
10775
- if (this.cache && parentId && res.children) await this.cache.setChildren(parentId, res.children).catch(() => null);
10776
- return res.documents;
10774
+ async listChildren(parentId, opts) {
10775
+ const kind = opts?.kind;
10776
+ if (parentId) {
10777
+ const res = await this.request("GET", `/docs/${encodeURIComponent(parentId)}/children`);
10778
+ if (this.cache && res.children) await this.cache.setChildren(parentId, res.children).catch(() => null);
10779
+ return kind ? res.documents.filter((d) => d.kind === kind) : res.documents;
10780
+ }
10781
+ const key = kind ?? "";
10782
+ const existing = this.rootListInflight.get(key);
10783
+ const docs = existing ? await existing : await (() => {
10784
+ const p = this.request("GET", kind ? `/docs?root=true&kind=${encodeURIComponent(kind)}` : "/docs?root=true").then((res) => res.documents).finally(() => this.rootListInflight.delete(key));
10785
+ this.rootListInflight.set(key, p);
10786
+ return p;
10787
+ })();
10788
+ return kind ? docs.filter((d) => d.kind === kind) : docs;
10777
10789
  }
10778
10790
  /**
10779
10791
  * Create a child document under a parent (requires write permission).
@@ -10902,7 +10914,7 @@ var AbracadabraClient = class {
10902
10914
  * spaces resolving to any role; anonymous users see public ones.
10903
10915
  */
10904
10916
  async listSpaces() {
10905
- return (await this.listChildren()).filter((d) => d.kind === Kind.Space);
10917
+ return this.listChildren(void 0, { kind: Kind.Space });
10906
10918
  }
10907
10919
  /**
10908
10920
  * Create a new top-level Space. Equivalent to a `POST /docs` with
@@ -11020,6 +11032,46 @@ var AbracadabraClient = class {
11020
11032
  return this.request("POST", "/admin/storage/repair");
11021
11033
  }
11022
11034
  /**
11035
+ * Admin one-shot: populate the `snapshot_files` table for snapshots
11036
+ * created before that migration ran, so `SnapshotMeta.file_count` and
11037
+ * upload-ref tracking become accurate. Idempotent (insert-or-ignore).
11038
+ * Requires elevated role.
11039
+ */
11040
+ async adminSnapshotsBackfillRefs() {
11041
+ return this.request("POST", "/admin/snapshots/backfill-refs");
11042
+ }
11043
+ /**
11044
+ * Admin one-shot: migrate pre-dedup inline snapshot data into the
11045
+ * content-addressed `snapshot_blobs` store. Idempotent (only migrates
11046
+ * rows with `data_hash IS NULL`). Requires elevated role.
11047
+ */
11048
+ async adminSnapshotsBackfillBlobs() {
11049
+ return this.request("POST", "/admin/snapshots/backfill-blobs");
11050
+ }
11051
+ /**
11052
+ * Admin: server-wide upload listing, joined with the owning document
11053
+ * (label is best-effort — labels live in the CRDT, not the SQL row)
11054
+ * and the content-addressed blob (`ref_count` exposes dedup).
11055
+ * Server-side paginated + filtered. Requires elevated role.
11056
+ */
11057
+ async adminListUploads(opts = {}) {
11058
+ const p = new URLSearchParams();
11059
+ if (opts.q) p.set("q", opts.q);
11060
+ if (opts.docId) p.set("doc_id", opts.docId);
11061
+ if (opts.limit != null) p.set("limit", String(opts.limit));
11062
+ if (opts.offset != null) p.set("offset", String(opts.offset));
11063
+ const qs = p.toString();
11064
+ return this.request("GET", `/admin/uploads${qs ? `?${qs}` : ""}`);
11065
+ }
11066
+ /**
11067
+ * Admin: aggregate storage figures. `logicalBytes` is what users
11068
+ * uploaded; `physicalBytes` is on-disk after content-addressed dedup;
11069
+ * `dedupSaved` is the difference. Requires elevated role.
11070
+ */
11071
+ async adminStorageStats() {
11072
+ return this.request("GET", "/admin/storage/stats");
11073
+ }
11074
+ /**
11023
11075
  * Clear the lockout state on a user account: zeroes the failed-login
11024
11076
  * counter and `locked_until`. Requires elevated role (Admin or
11025
11077
  * Service). The action is recorded in the audit log under
@@ -11029,6 +11081,17 @@ var AbracadabraClient = class {
11029
11081
  await this.request("POST", `/admin/users/${encodeURIComponent(userId)}/unlock`);
11030
11082
  }
11031
11083
  /**
11084
+ * Admin: every non-deleted document the user owns (`source: "owner"`)
11085
+ * or has an explicit permission grant on (`source: "grant"`, with
11086
+ * `role`). Answers "what does this identity touch" without an N+1
11087
+ * client tree walk. Labels are best-effort (they live in the CRDT).
11088
+ * Requires elevated role.
11089
+ */
11090
+ async adminUserDocs(userId, opts = {}) {
11091
+ const qs = opts.limit != null ? `?limit=${opts.limit}` : "";
11092
+ return this.request("GET", `/admin/users/${encodeURIComponent(userId)}/docs${qs}`);
11093
+ }
11094
+ /**
11032
11095
  * Page through the audit log. Filters AND-combine; `limit` defaults to
11033
11096
  * 100 server-side. Requires elevated role.
11034
11097
  */
@@ -11149,6 +11212,30 @@ var AbracadabraClient = class {
11149
11212
  async adminConfigEnvSnapshot() {
11150
11213
  return this.request("GET", "/admin/config/env-snapshot");
11151
11214
  }
11215
+ /**
11216
+ * List every route pattern that currently has at least one per-route
11217
+ * config override. Use {@link adminConfigGetRoute} to read individual
11218
+ * fields. Requires elevated role.
11219
+ */
11220
+ async adminConfigListRoutes() {
11221
+ return (await this.request("GET", "/admin/config/routes")).routes;
11222
+ }
11223
+ /**
11224
+ * Read a field's effective value scoped to `route`, falling back to
11225
+ * the global value when no per-route override exists. `origin_kind`
11226
+ * is `"route_override"` only when an override is actually set.
11227
+ */
11228
+ async adminConfigGetRoute(route, path) {
11229
+ return this.request("GET", `/admin/config/routes/${encodeURIComponent(route)}/fields/${encodeURIComponent(path)}`);
11230
+ }
11231
+ /** Set or replace a per-route override. Mirrors {@link adminConfigSet}. */
11232
+ async adminConfigSetRoute(route, path, value) {
11233
+ return this.request("PUT", `/admin/config/routes/${encodeURIComponent(route)}/fields/${encodeURIComponent(path)}`, { body: { value } });
11234
+ }
11235
+ /** Clear a per-route override (falls back to global). True if one existed. */
11236
+ async adminConfigUnsetRoute(route, path) {
11237
+ return (await this.request("DELETE", `/admin/config/routes/${encodeURIComponent(route)}/fields/${encodeURIComponent(path)}`)).existed;
11238
+ }
11152
11239
  /** List snapshot metadata for a document. */
11153
11240
  async listSnapshots(docId, opts) {
11154
11241
  const params = new URLSearchParams();
@@ -14660,6 +14747,10 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
14660
14747
  if (!idbAvailable$1()) return Promise.resolve(null);
14661
14748
  if (!this.dbPromise) this.dbPromise = openDb$1(this.origin).catch(() => null).then((db) => {
14662
14749
  this.db = db;
14750
+ if (db) db.onclose = () => {
14751
+ if (this.db === db) this.db = null;
14752
+ this.dbPromise = null;
14753
+ };
14663
14754
  return db;
14664
14755
  });
14665
14756
  return this.dbPromise;
@@ -14678,10 +14769,23 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
14678
14769
  const key = this.blobKey(docId, uploadId);
14679
14770
  const existing = this.objectUrls.get(key);
14680
14771
  if (existing) return existing;
14681
- const db = await this.getDb();
14772
+ let db = await this.getDb();
14682
14773
  if (db) {
14683
- const tx = db.transaction("blobs", "readonly");
14684
- const entry = await txPromise(tx.objectStore("blobs"), tx.objectStore("blobs").get(key));
14774
+ let entry;
14775
+ try {
14776
+ const tx = db.transaction("blobs", "readonly");
14777
+ entry = await txPromise(tx.objectStore("blobs"), tx.objectStore("blobs").get(key));
14778
+ } catch (err) {
14779
+ if (err?.name === "InvalidStateError") {
14780
+ if (this.db === db) this.db = null;
14781
+ this.dbPromise = null;
14782
+ db = await this.getDb();
14783
+ if (db) {
14784
+ const tx = db.transaction("blobs", "readonly");
14785
+ entry = await txPromise(tx.objectStore("blobs"), tx.objectStore("blobs").get(key));
14786
+ }
14787
+ } else throw err;
14788
+ }
14685
14789
  if (entry) {
14686
14790
  const url = URL.createObjectURL(entry.blob);
14687
14791
  this.objectUrls.set(key, url);
@@ -14932,6 +15036,7 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
14932
15036
  this.objectUrls.clear();
14933
15037
  this.db?.close();
14934
15038
  this.db = null;
15039
+ this.dbPromise = null;
14935
15040
  this.removeAllListeners();
14936
15041
  }
14937
15042
  };
@@ -15482,6 +15587,112 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
15482
15587
  }
15483
15588
  };
15484
15589
 
15590
+ //#endregion
15591
+ //#region packages/provider/src/DocUtils.ts
15592
+ /**
15593
+ * Shared utilities for the DocumentManager ORM layer.
15594
+ *
15595
+ * These functions were previously duplicated across `mcp/src/utils.ts`,
15596
+ * `mcp/src/server.ts`, `cli/src/connection.ts`, and `mcp/src/tools/tree.ts`.
15597
+ */
15598
+ /**
15599
+ * Wait for a provider's `synced` event with a timeout.
15600
+ * Resolves immediately if the provider is already synced.
15601
+ */
15602
+ function waitForSync(provider, timeoutMs = 15e3) {
15603
+ if (provider.isSynced) return Promise.resolve();
15604
+ return new Promise((resolve, reject) => {
15605
+ const timer = setTimeout(() => {
15606
+ provider.off("synced", handler);
15607
+ reject(/* @__PURE__ */ new Error(`Sync timed out after ${timeoutMs}ms`));
15608
+ }, timeoutMs);
15609
+ function handler() {
15610
+ clearTimeout(timer);
15611
+ resolve();
15612
+ }
15613
+ provider.on("synced", handler);
15614
+ });
15615
+ }
15616
+ /**
15617
+ * Wrap a promise with a timeout.
15618
+ */
15619
+ function withTimeout(promise, timeoutMs, message) {
15620
+ return new Promise((resolve, reject) => {
15621
+ const timer = setTimeout(() => reject(new Error(message ?? `Operation timed out after ${timeoutMs}ms`)), timeoutMs);
15622
+ promise.then((val) => {
15623
+ clearTimeout(timer);
15624
+ resolve(val);
15625
+ }, (err) => {
15626
+ clearTimeout(timer);
15627
+ reject(err);
15628
+ });
15629
+ });
15630
+ }
15631
+ /**
15632
+ * Normalize a document ID so the hub/root doc ID is treated as the tree root
15633
+ * (null). This lets callers pass the hub doc_id from list_spaces as
15634
+ * parentId/rootId and get the expected root-level results instead of an empty
15635
+ * set.
15636
+ */
15637
+ function normalizeRootId(id, rootDocId) {
15638
+ if (id == null) return null;
15639
+ return id === rootDocId ? null : id;
15640
+ }
15641
+ /**
15642
+ * Safely read a tree map value, converting Y.Map to plain object if needed.
15643
+ */
15644
+ function toPlain(val) {
15645
+ return val instanceof yjs.Map ? val.toJSON() : val;
15646
+ }
15647
+ /**
15648
+ * Build a tree/trash entry as a nested `Y.Map`. Use for a brand-new or
15649
+ * re-created key (create / duplicate / restore) where no concurrent
15650
+ * writer exists, so a whole-value write is safe. `undefined` fields are
15651
+ * omitted; `null` is kept (a real value, e.g. top-level `parentId`).
15652
+ */
15653
+ function makeEntryMap(fields) {
15654
+ const m = new yjs.Map();
15655
+ for (const [k, v] of Object.entries(fields)) if (v !== void 0) m.set(k, v);
15656
+ return m;
15657
+ }
15658
+ /**
15659
+ * Patch an EXISTING entry's fields per-key on its nested `Y.Map`, so a
15660
+ * concurrent edit to a *different* field by a peer is preserved instead
15661
+ * of being clobbered by a whole-entry write — the whole-entry-LWW fix
15662
+ * (audit ⑦), the mirror of the Rust provider's `with_entry_mut`.
15663
+ *
15664
+ * - nested `Y.Map` entry → set/delete only the touched keys in place;
15665
+ * - legacy opaque (plain-object) entry → migrated once to a `Y.Map`;
15666
+ * - missing entry → created from the patch (lenient; matches the prior
15667
+ * call-site behaviour of spreading `undefined`).
15668
+ *
15669
+ * A patch value of `undefined` deletes the key; `null` is written.
15670
+ * Self-transacting: it batches its writes in one `Y.Doc` transaction
15671
+ * (a safe reentrant no-op join when already inside one), so callers
15672
+ * don't need to pass or own a transaction.
15673
+ */
15674
+ function patchEntry(treeMap, id, patch, removeKeys = []) {
15675
+ const apply = () => {
15676
+ const raw = treeMap.get(id);
15677
+ if (raw instanceof yjs.Map) {
15678
+ for (const [k, v] of Object.entries(patch)) if (v === void 0) raw.delete(k);
15679
+ else raw.set(k, v);
15680
+ for (const k of removeKeys) raw.delete(k);
15681
+ return;
15682
+ }
15683
+ const merged = {
15684
+ ...raw == null ? {} : toPlain(raw),
15685
+ ...patch
15686
+ };
15687
+ for (const [k, v] of Object.entries(patch)) if (v === void 0) delete merged[k];
15688
+ for (const k of removeKeys) delete merged[k];
15689
+ treeMap.set(id, makeEntryMap(merged));
15690
+ };
15691
+ const doc = treeMap.doc;
15692
+ if (doc) doc.transact(apply);
15693
+ else apply();
15694
+ }
15695
+
15485
15696
  //#endregion
15486
15697
  //#region packages/provider/src/TreeTimestamps.ts
15487
15698
  /**
@@ -15519,13 +15730,8 @@ function attachUpdatedAtObserver(treeMap, childDocId, childDoc, offlineStore, op
15519
15730
  let pendingTs = 0;
15520
15731
  let timer = null;
15521
15732
  function writeTs(ts) {
15522
- const raw = treeMap.get(childDocId);
15523
- if (!raw) return;
15524
- const entry = raw instanceof yjs.Map ? raw.toJSON() : raw;
15525
- treeMap.set(childDocId, {
15526
- ...entry,
15527
- updatedAt: ts
15528
- });
15733
+ if (!treeMap.get(childDocId)) return;
15734
+ patchEntry(treeMap, childDocId, { updatedAt: ts });
15529
15735
  lastFlushedAt = ts;
15530
15736
  }
15531
15737
  function flushPending() {
@@ -18152,64 +18358,6 @@ function resolvePageType(key) {
18152
18358
  return PAGE_TYPES[TYPE_ALIASES[key] ?? key];
18153
18359
  }
18154
18360
 
18155
- //#endregion
18156
- //#region packages/provider/src/DocUtils.ts
18157
- /**
18158
- * Shared utilities for the DocumentManager ORM layer.
18159
- *
18160
- * These functions were previously duplicated across `mcp/src/utils.ts`,
18161
- * `mcp/src/server.ts`, `cli/src/connection.ts`, and `mcp/src/tools/tree.ts`.
18162
- */
18163
- /**
18164
- * Wait for a provider's `synced` event with a timeout.
18165
- * Resolves immediately if the provider is already synced.
18166
- */
18167
- function waitForSync(provider, timeoutMs = 15e3) {
18168
- if (provider.isSynced) return Promise.resolve();
18169
- return new Promise((resolve, reject) => {
18170
- const timer = setTimeout(() => {
18171
- provider.off("synced", handler);
18172
- reject(/* @__PURE__ */ new Error(`Sync timed out after ${timeoutMs}ms`));
18173
- }, timeoutMs);
18174
- function handler() {
18175
- clearTimeout(timer);
18176
- resolve();
18177
- }
18178
- provider.on("synced", handler);
18179
- });
18180
- }
18181
- /**
18182
- * Wrap a promise with a timeout.
18183
- */
18184
- function withTimeout(promise, timeoutMs, message) {
18185
- return new Promise((resolve, reject) => {
18186
- const timer = setTimeout(() => reject(new Error(message ?? `Operation timed out after ${timeoutMs}ms`)), timeoutMs);
18187
- promise.then((val) => {
18188
- clearTimeout(timer);
18189
- resolve(val);
18190
- }, (err) => {
18191
- clearTimeout(timer);
18192
- reject(err);
18193
- });
18194
- });
18195
- }
18196
- /**
18197
- * Normalize a document ID so the hub/root doc ID is treated as the tree root
18198
- * (null). This lets callers pass the hub doc_id from list_spaces as
18199
- * parentId/rootId and get the expected root-level results instead of an empty
18200
- * set.
18201
- */
18202
- function normalizeRootId(id, rootDocId) {
18203
- if (id == null) return null;
18204
- return id === rootDocId ? null : id;
18205
- }
18206
- /**
18207
- * Safely read a tree map value, converting Y.Map to plain object if needed.
18208
- */
18209
- function toPlain(val) {
18210
- return val instanceof yjs.Map ? val.toJSON() : val;
18211
- }
18212
-
18213
18361
  //#endregion
18214
18362
  //#region packages/provider/src/SchemaTypes.ts
18215
18363
  /**
@@ -18255,9 +18403,112 @@ function projectTreeEntry(entry, expectedType) {
18255
18403
  * Extracted from `mcp/tools/tree.ts` and `cli/commands/documents.ts` logic.
18256
18404
  * All tree CRUD operations go through this class.
18257
18405
  */
18406
+ /**
18407
+ * Stable total order over tree siblings: `order` ascending, then `id`
18408
+ * ascending as a deterministic tiebreak. The legacy scan sorted by
18409
+ * `order` alone and left ties to insertion/iteration order — a superset
18410
+ * change that makes cursor pagination well-defined.
18411
+ */
18412
+ function cmpKey(oa, ia, ob, ib) {
18413
+ if (oa !== ob) return oa < ob ? -1 : 1;
18414
+ return ia < ib ? -1 : ia > ib ? 1 : 0;
18415
+ }
18416
+ function cmpEntry(a, b) {
18417
+ return cmpKey(a.order ?? 0, a.id, b.order ?? 0, b.id);
18418
+ }
18419
+ /** Opaque, dependency-free cursor over the (order,id) sibling order. */
18420
+ function encodeCursor(order, id) {
18421
+ return encodeURIComponent(JSON.stringify([order ?? 0, id]));
18422
+ }
18423
+ function decodeCursor(c) {
18424
+ try {
18425
+ const v = JSON.parse(decodeURIComponent(c));
18426
+ if (Array.isArray(v) && typeof v[0] === "number" && typeof v[1] === "string") return {
18427
+ order: v[0],
18428
+ id: v[1]
18429
+ };
18430
+ } catch {}
18431
+ return null;
18432
+ }
18258
18433
  var TreeManager = class {
18259
18434
  constructor(dm) {
18260
18435
  this.dm = dm;
18436
+ this._idxMap = null;
18437
+ this._idxObserver = null;
18438
+ this._idxDirty = true;
18439
+ this._byId = /* @__PURE__ */ new Map();
18440
+ this._childrenByParent = /* @__PURE__ */ new Map();
18441
+ }
18442
+ /**
18443
+ * Ensure the index is enabled, bound to the current root doc's tree
18444
+ * map, and fresh. Returns `false` when the index is disabled or there
18445
+ * is no tree map yet — callers then use the legacy scan path.
18446
+ */
18447
+ ensureIndex() {
18448
+ if (!this.dm.treeIndexEnabled) return false;
18449
+ const treeMap = this.dm.getTreeMap();
18450
+ if (!treeMap) {
18451
+ this.unbindIndex();
18452
+ return false;
18453
+ }
18454
+ if (treeMap !== this._idxMap) {
18455
+ this.unbindIndex();
18456
+ const obs = () => {
18457
+ this._idxDirty = true;
18458
+ };
18459
+ treeMap.observeDeep(obs);
18460
+ this._idxMap = treeMap;
18461
+ this._idxObserver = obs;
18462
+ this._idxDirty = true;
18463
+ }
18464
+ if (this._idxDirty) this.rebuildIndex(treeMap);
18465
+ return true;
18466
+ }
18467
+ unbindIndex() {
18468
+ if (this._idxMap && this._idxObserver) this._idxMap.unobserveDeep(this._idxObserver);
18469
+ this._idxMap = null;
18470
+ this._idxObserver = null;
18471
+ this._byId = /* @__PURE__ */ new Map();
18472
+ this._childrenByParent = /* @__PURE__ */ new Map();
18473
+ this._idxDirty = true;
18474
+ }
18475
+ rebuildIndex(treeMap) {
18476
+ const root = this.dm.rootDocId;
18477
+ const byId = /* @__PURE__ */ new Map();
18478
+ const childrenByParent = /* @__PURE__ */ new Map();
18479
+ treeMap.forEach((raw, id) => {
18480
+ const value = toPlain(raw);
18481
+ if (typeof value !== "object" || value === null) return;
18482
+ const entry = {
18483
+ id,
18484
+ label: value.label || "Untitled",
18485
+ parentId: normalizeRootId(value.parentId ?? null, root),
18486
+ order: value.order ?? 0,
18487
+ type: value.type,
18488
+ meta: value.meta,
18489
+ createdAt: value.createdAt,
18490
+ updatedAt: value.updatedAt
18491
+ };
18492
+ byId.set(id, entry);
18493
+ let bucket = childrenByParent.get(entry.parentId);
18494
+ if (!bucket) {
18495
+ bucket = [];
18496
+ childrenByParent.set(entry.parentId, bucket);
18497
+ }
18498
+ bucket.push(entry);
18499
+ });
18500
+ for (const bucket of childrenByParent.values()) bucket.sort(cmpEntry);
18501
+ this._byId = byId;
18502
+ this._childrenByParent = childrenByParent;
18503
+ this._idxDirty = false;
18504
+ }
18505
+ /**
18506
+ * Release the deep observer. Optional — the observer is auto-rebound
18507
+ * on space switch and becomes moot when the root Y.Doc is GC'd — but
18508
+ * available for consumers that want deterministic teardown.
18509
+ */
18510
+ dispose() {
18511
+ this.unbindIndex();
18261
18512
  }
18262
18513
  /** Read all tree entries as plain objects. */
18263
18514
  readEntries() {
@@ -18280,15 +18531,83 @@ var TreeManager = class {
18280
18531
  });
18281
18532
  return entries;
18282
18533
  }
18534
+ /**
18535
+ * Like {@link readEntries} but with every entry's *stored* parentId
18536
+ * run through {@link normalizeRootId} (parentId === rootDocId → null),
18537
+ * so a cou-sh / orphan-rescue top-level doc (parentId === spaceRoot)
18538
+ * resolves to top-level identically to a provider-created one
18539
+ * (parentId: null). Without this, the raw `parentId === spaceRoot`
18540
+ * form never matches the normalized `null` query and such docs are
18541
+ * silently invisible cross-client. Mirrors the Rust provider's
18542
+ * `normalized_entries`. readEntries/get keep raw values for
18543
+ * round-trip consumers; only tree-walk reads use this.
18544
+ */
18545
+ normalizedEntries() {
18546
+ if (this.ensureIndex()) return Array.from(this._byId.values());
18547
+ const root = this.dm.rootDocId;
18548
+ return this.readEntries().map((e) => ({
18549
+ ...e,
18550
+ parentId: normalizeRootId(e.parentId, root)
18551
+ }));
18552
+ }
18283
18553
  /** Get immediate children of a parent (sorted by order). */
18284
18554
  childrenOf(parentId) {
18285
18555
  const normalized = normalizeRootId(parentId, this.dm.rootDocId);
18286
- return this.readEntries().filter((e) => e.parentId === normalized).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
18556
+ if (this.ensureIndex()) {
18557
+ const bucket = this._childrenByParent.get(normalized);
18558
+ return bucket ? bucket.slice() : [];
18559
+ }
18560
+ return this.normalizedEntries().filter((e) => e.parentId === normalized).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
18561
+ }
18562
+ /**
18563
+ * Paginated immediate children — the Path-1 surface for large fan-out
18564
+ * parents. Walks the same stable (order,id) sibling order as
18565
+ * {@link childrenOf}; `cursor` is opaque (round-trip `nextCursor`).
18566
+ * `limit` defaults to 100. A stale/garbage cursor restarts from the
18567
+ * head rather than throwing. Cursor stability is exact when the index
18568
+ * is enabled; on the legacy scan path siblings with equal `order`
18569
+ * may shift between calls.
18570
+ */
18571
+ childrenOfPage(parentId, opts = {}) {
18572
+ const all = this.childrenOf(parentId);
18573
+ const limit = opts.limit != null && opts.limit > 0 ? Math.floor(opts.limit) : 100;
18574
+ let start = 0;
18575
+ if (opts.cursor) {
18576
+ const dec = decodeCursor(opts.cursor);
18577
+ if (dec) {
18578
+ const at = all.findIndex((e) => cmpKey(e.order ?? 0, e.id, dec.order, dec.id) > 0);
18579
+ start = at < 0 ? all.length : at;
18580
+ }
18581
+ }
18582
+ const entries = all.slice(start, start + limit);
18583
+ const last = entries[entries.length - 1];
18584
+ return {
18585
+ entries,
18586
+ nextCursor: last && start + limit < all.length ? encodeCursor(last.order ?? 0, last.id) : null
18587
+ };
18287
18588
  }
18288
18589
  /** Get all descendants recursively. */
18289
18590
  descendantsOf(parentId) {
18290
18591
  const normalized = normalizeRootId(parentId, this.dm.rootDocId);
18291
- const entries = this.readEntries();
18592
+ if (this.ensureIndex()) {
18593
+ const result = [];
18594
+ const visited = /* @__PURE__ */ new Set();
18595
+ const walk = (pid) => {
18596
+ if (pid !== null) {
18597
+ if (visited.has(pid)) return;
18598
+ visited.add(pid);
18599
+ }
18600
+ const bucket = this._childrenByParent.get(pid);
18601
+ if (!bucket) return;
18602
+ for (const child of bucket) {
18603
+ result.push(child);
18604
+ walk(child.id);
18605
+ }
18606
+ };
18607
+ walk(normalized);
18608
+ return result;
18609
+ }
18610
+ const entries = this.normalizedEntries();
18292
18611
  const result = [];
18293
18612
  const visited = /* @__PURE__ */ new Set();
18294
18613
  const collect = (pid) => {
@@ -18305,9 +18624,25 @@ var TreeManager = class {
18305
18624
  /** Build nested tree JSON. */
18306
18625
  buildTree(rootId, maxDepth = 3) {
18307
18626
  const normalized = normalizeRootId(rootId ?? null, this.dm.rootDocId);
18308
- const entries = this.readEntries();
18627
+ if (this.ensureIndex()) return this._buildTreeIndexed(normalized, maxDepth, 0, /* @__PURE__ */ new Set());
18628
+ const entries = this.normalizedEntries();
18309
18629
  return this._buildTree(entries, normalized, maxDepth, 0, /* @__PURE__ */ new Set());
18310
18630
  }
18631
+ _buildTreeIndexed(rootId, maxDepth, currentDepth, visited) {
18632
+ if (maxDepth >= 0 && currentDepth >= maxDepth) return [];
18633
+ return (this._childrenByParent.get(rootId) ?? []).filter((e) => !visited.has(e.id)).map((entry) => {
18634
+ const next = new Set(visited);
18635
+ next.add(entry.id);
18636
+ return {
18637
+ id: entry.id,
18638
+ label: entry.label,
18639
+ type: entry.type,
18640
+ meta: entry.meta,
18641
+ order: entry.order,
18642
+ children: this._buildTreeIndexed(entry.id, maxDepth, currentDepth + 1, next)
18643
+ };
18644
+ });
18645
+ }
18311
18646
  _buildTree(entries, rootId, maxDepth, currentDepth, visited) {
18312
18647
  if (maxDepth >= 0 && currentDepth >= maxDepth) return [];
18313
18648
  return entries.filter((e) => e.parentId === rootId).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).filter((e) => !visited.has(e.id)).map((entry) => {
@@ -18358,7 +18693,7 @@ var TreeManager = class {
18358
18693
  }
18359
18694
  /** Search by label (case-insensitive substring match). */
18360
18695
  find(query, rootId) {
18361
- const entries = this.readEntries();
18696
+ const entries = this.normalizedEntries();
18362
18697
  const lowerQuery = query.toLowerCase();
18363
18698
  const normalized = normalizeRootId(rootId ?? null, this.dm.rootDocId);
18364
18699
  const matches = (normalized ? this.descendantsOf(normalized) : entries).filter((e) => e.label.toLowerCase().includes(lowerQuery));
@@ -18392,7 +18727,7 @@ var TreeManager = class {
18392
18727
  const normalizedParent = normalizeRootId(opts.parentId ?? null, this.dm.rootDocId);
18393
18728
  const now = Date.now();
18394
18729
  rootDoc.transact(() => {
18395
- treeMap.set(id, {
18730
+ treeMap.set(id, makeEntryMap({
18396
18731
  label: opts.label,
18397
18732
  parentId: normalizedParent,
18398
18733
  order: now,
@@ -18400,7 +18735,7 @@ var TreeManager = class {
18400
18735
  meta: opts.meta,
18401
18736
  createdAt: now,
18402
18737
  updatedAt: now
18403
- });
18738
+ }));
18404
18739
  });
18405
18740
  return {
18406
18741
  id,
@@ -18439,12 +18774,9 @@ var TreeManager = class {
18439
18774
  const treeMap = this.dm.getTreeMap();
18440
18775
  const rootDoc = this.dm.rootDocument;
18441
18776
  if (!treeMap || !rootDoc) throw new Error("Not connected");
18442
- const raw = treeMap.get(docId);
18443
- if (!raw) throw new Error(`Document ${docId} not found`);
18444
- const entry = toPlain(raw);
18777
+ if (!treeMap.get(docId)) throw new Error(`Document ${docId} not found`);
18445
18778
  rootDoc.transact(() => {
18446
- treeMap.set(docId, {
18447
- ...entry,
18779
+ patchEntry(treeMap, docId, {
18448
18780
  label,
18449
18781
  updatedAt: Date.now()
18450
18782
  });
@@ -18455,12 +18787,9 @@ var TreeManager = class {
18455
18787
  const treeMap = this.dm.getTreeMap();
18456
18788
  const rootDoc = this.dm.rootDocument;
18457
18789
  if (!treeMap || !rootDoc) throw new Error("Not connected");
18458
- const raw = treeMap.get(docId);
18459
- if (!raw) throw new Error(`Document ${docId} not found`);
18460
- const entry = toPlain(raw);
18790
+ if (!treeMap.get(docId)) throw new Error(`Document ${docId} not found`);
18461
18791
  rootDoc.transact(() => {
18462
- treeMap.set(docId, {
18463
- ...entry,
18792
+ patchEntry(treeMap, docId, {
18464
18793
  parentId: normalizeRootId(newParentId ?? null, this.dm.rootDocId),
18465
18794
  order: order ?? Date.now(),
18466
18795
  updatedAt: Date.now()
@@ -18472,12 +18801,9 @@ var TreeManager = class {
18472
18801
  const treeMap = this.dm.getTreeMap();
18473
18802
  const rootDoc = this.dm.rootDocument;
18474
18803
  if (!treeMap || !rootDoc) throw new Error("Not connected");
18475
- const raw = treeMap.get(docId);
18476
- if (!raw) throw new Error(`Document ${docId} not found`);
18477
- const entry = toPlain(raw);
18804
+ if (!treeMap.get(docId)) throw new Error(`Document ${docId} not found`);
18478
18805
  rootDoc.transact(() => {
18479
- treeMap.set(docId, {
18480
- ...entry,
18806
+ patchEntry(treeMap, docId, {
18481
18807
  type,
18482
18808
  updatedAt: Date.now()
18483
18809
  });
@@ -18492,10 +18818,12 @@ var TreeManager = class {
18492
18818
  const trashMap = this.dm.getTrashMap();
18493
18819
  const rootDoc = this.dm.rootDocument;
18494
18820
  if (!treeMap || !trashMap || !rootDoc) throw new Error("Not connected");
18495
- const entries = this.readEntries();
18496
- const toDelete = [docId, ...this._descendantIds(entries, docId)];
18497
18821
  const now = Date.now();
18822
+ let deletedCount = 0;
18498
18823
  rootDoc.transact(() => {
18824
+ const entries = this.readEntries();
18825
+ const toDelete = [docId, ...this._descendantIds(entries, docId)];
18826
+ deletedCount = toDelete.length;
18499
18827
  for (const nid of toDelete) {
18500
18828
  const raw = treeMap.get(nid);
18501
18829
  if (!raw) continue;
@@ -18511,7 +18839,7 @@ var TreeManager = class {
18511
18839
  treeMap.delete(nid);
18512
18840
  }
18513
18841
  });
18514
- return toDelete.length;
18842
+ return deletedCount;
18515
18843
  }
18516
18844
  /** Duplicate a document (shallow clone). Returns the new entry. */
18517
18845
  duplicate(docId) {
@@ -18523,13 +18851,13 @@ var TreeManager = class {
18523
18851
  const newId = crypto.randomUUID();
18524
18852
  const now = Date.now();
18525
18853
  const newLabel = (entry.label || "Untitled") + " (copy)";
18526
- treeMap.set(newId, {
18854
+ treeMap.set(newId, makeEntryMap({
18527
18855
  ...entry,
18528
18856
  label: newLabel,
18529
18857
  order: now,
18530
18858
  createdAt: now,
18531
18859
  updatedAt: now
18532
- });
18860
+ }));
18533
18861
  return {
18534
18862
  id: newId,
18535
18863
  label: newLabel,
@@ -18547,21 +18875,37 @@ var TreeManager = class {
18547
18875
  const trashMap = this.dm.getTrashMap();
18548
18876
  const rootDoc = this.dm.rootDocument;
18549
18877
  if (!treeMap || !trashMap || !rootDoc) throw new Error("Not connected");
18550
- const raw = trashMap.get(docId);
18551
- if (!raw) throw new Error(`Document ${docId} not found in trash`);
18552
- const entry = toPlain(raw);
18878
+ if (!trashMap.get(docId)) throw new Error(`Document ${docId} not found in trash`);
18553
18879
  const now = Date.now();
18554
18880
  rootDoc.transact(() => {
18555
- treeMap.set(docId, {
18556
- label: entry.label || "Untitled",
18557
- parentId: entry.parentId ?? null,
18558
- order: entry.order ?? now,
18559
- type: entry.type,
18560
- meta: entry.meta,
18561
- createdAt: entry.createdAt ?? now,
18562
- updatedAt: now
18881
+ const trashed = /* @__PURE__ */ new Map();
18882
+ trashMap.forEach((raw, id) => {
18883
+ const v = toPlain(raw);
18884
+ if (typeof v === "object" && v !== null) trashed.set(id, v);
18563
18885
  });
18564
- trashMap.delete(docId);
18886
+ const toRestore = [];
18887
+ const visited = /* @__PURE__ */ new Set();
18888
+ const collect = (id) => {
18889
+ if (visited.has(id)) return;
18890
+ visited.add(id);
18891
+ if (!trashed.has(id)) return;
18892
+ toRestore.push(id);
18893
+ for (const [cid, v] of trashed) if ((v.parentId ?? null) === id) collect(cid);
18894
+ };
18895
+ collect(docId);
18896
+ for (const id of toRestore) {
18897
+ const entry = trashed.get(id);
18898
+ treeMap.set(id, makeEntryMap({
18899
+ label: entry.label || "Untitled",
18900
+ parentId: entry.parentId ?? null,
18901
+ order: entry.order ?? now,
18902
+ type: entry.type,
18903
+ meta: entry.meta,
18904
+ createdAt: entry.createdAt ?? now,
18905
+ updatedAt: now
18906
+ }));
18907
+ trashMap.delete(id);
18908
+ }
18565
18909
  });
18566
18910
  }
18567
18911
  /** List trashed documents. */
@@ -19937,9 +20281,9 @@ var ContentManager = class {
19937
20281
  * body, tree metadata, and immediate children.
19938
20282
  */
19939
20283
  async read(docId) {
19940
- const { title, markdown } = yjsToMarkdown((await this.dm.getChildProvider(docId)).document.getXmlFragment("default"));
20284
+ const fragment = (await this.dm.getChildProvider(docId)).document.getXmlFragment("default");
19941
20285
  const treeMap = this.dm.getTreeMap();
19942
- let label = title;
20286
+ let label = "Untitled";
19943
20287
  let type;
19944
20288
  let meta;
19945
20289
  const childrenWithOrder = [];
@@ -19947,7 +20291,7 @@ var ContentManager = class {
19947
20291
  const raw = treeMap.get(docId);
19948
20292
  if (raw) {
19949
20293
  const entry = toPlain(raw);
19950
- label = entry.label || title;
20294
+ label = entry.label || label;
19951
20295
  type = entry.type;
19952
20296
  meta = entry.meta;
19953
20297
  }
@@ -19969,11 +20313,12 @@ var ContentManager = class {
19969
20313
  type,
19970
20314
  meta
19971
20315
  }));
20316
+ const markdown = yjsToMarkdown(fragment, label, meta, type);
19972
20317
  return {
19973
20318
  label,
19974
20319
  type,
19975
20320
  meta,
19976
- title,
20321
+ title: label,
19977
20322
  markdown,
19978
20323
  children
19979
20324
  };
@@ -19995,16 +20340,14 @@ var ContentManager = class {
19995
20340
  if (treeMap && rootDoc) {
19996
20341
  const entry = treeMap.get(docId);
19997
20342
  if (entry) rootDoc.transact(() => {
19998
- const updates = {
19999
- ...entry,
20000
- updatedAt: Date.now()
20001
- };
20002
- if (title) updates.label = title;
20003
- if (Object.keys(meta).length > 0) updates.meta = {
20004
- ...entry.meta ?? {},
20343
+ const cur = toPlain(entry);
20344
+ const patch = { updatedAt: Date.now() };
20345
+ if (title) patch.label = title;
20346
+ if (Object.keys(meta).length > 0) patch.meta = {
20347
+ ...cur.meta ?? {},
20005
20348
  ...meta
20006
20349
  };
20007
- treeMap.set(docId, updates);
20350
+ patchEntry(treeMap, docId, patch);
20008
20351
  });
20009
20352
  }
20010
20353
  }
@@ -20123,8 +20466,7 @@ var MetaManager = class {
20123
20466
  ...meta
20124
20467
  };
20125
20468
  this.validateOrThrow(docId, entry, mergedMeta);
20126
- treeMap.set(docId, {
20127
- ...entry,
20469
+ patchEntry(treeMap, docId, {
20128
20470
  meta: mergedMeta,
20129
20471
  updatedAt: Date.now()
20130
20472
  });
@@ -20143,8 +20485,7 @@ var MetaManager = class {
20143
20485
  if (!raw) throw new Error(`Document ${docId} not found`);
20144
20486
  const entry = toPlain(raw);
20145
20487
  this.validateOrThrow(docId, entry, meta);
20146
- treeMap.set(docId, {
20147
- ...entry,
20488
+ patchEntry(treeMap, docId, {
20148
20489
  meta,
20149
20490
  updatedAt: Date.now()
20150
20491
  });
@@ -20161,8 +20502,7 @@ var MetaManager = class {
20161
20502
  const updated = { ...entry.meta ?? {} };
20162
20503
  for (const key of keys) delete updated[key];
20163
20504
  this.validateOrThrow(docId, entry, updated);
20164
- treeMap.set(docId, {
20165
- ...entry,
20505
+ patchEntry(treeMap, docId, {
20166
20506
  meta: updated,
20167
20507
  updatedAt: Date.now()
20168
20508
  });
@@ -20269,6 +20609,13 @@ var DocumentManager = class {
20269
20609
  get rootDocId() {
20270
20610
  return this._rootDocId;
20271
20611
  }
20612
+ /**
20613
+ * Whether the TreeManager in-memory index is enabled (Path-1 prototype).
20614
+ * Off by default — see {@link DocumentManagerConfig.treeIndex}.
20615
+ */
20616
+ get treeIndexEnabled() {
20617
+ return this._config.treeIndex ?? false;
20618
+ }
20272
20619
  get rootDocument() {
20273
20620
  return this._rootDoc;
20274
20621
  }
@@ -20491,10 +20838,12 @@ exports.generateMnemonic = generateMnemonic;
20491
20838
  exports.isEncryptedContent = isEncryptedContent;
20492
20839
  exports.makeEncryptedYMap = makeEncryptedYMap;
20493
20840
  exports.makeEncryptedYText = makeEncryptedYText;
20841
+ exports.makeEntryMap = makeEntryMap;
20494
20842
  exports.mnemonicToEd25519Seed = mnemonicToEd25519Seed;
20495
20843
  exports.mnemonicToKeyPair = mnemonicToKeyPair;
20496
20844
  exports.normalizeRootId = normalizeRootId;
20497
20845
  exports.parseFrontmatter = parseFrontmatter;
20846
+ exports.patchEntry = patchEntry;
20498
20847
  exports.populateYDocFromMarkdown = populateYDocFromMarkdown;
20499
20848
  exports.readAuthMessage = readAuthMessage;
20500
20849
  exports.readBlocksFromFragment = readBlocksFromFragment;