@abraca/dabra 2.4.0 → 2.6.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,60 @@ 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
+ /**
11095
+ * List `service`-role users (runners, demo seeders, automation
11096
+ * identities). Requires Service role. Admins can see service users via
11097
+ * `/admin/users` too but cannot mint or rotate them.
11098
+ */
11099
+ async adminListServiceAccounts() {
11100
+ return this.request("GET", "/admin/service-accounts");
11101
+ }
11102
+ /**
11103
+ * Create a new `service`-role user. When `public_key` is omitted the
11104
+ * server generates a keypair and returns the private half in the
11105
+ * response — show it to the operator **once** and discard; the server
11106
+ * never persists it. Requires Service role.
11107
+ */
11108
+ async adminCreateServiceAccount(body) {
11109
+ return this.request("POST", "/admin/service-accounts", { body });
11110
+ }
11111
+ /**
11112
+ * Rotate the active keypair on a service account. Old JWTs are
11113
+ * invalidated; old device keys are marked revoked; the canonical
11114
+ * `users.public_key` swaps to the new value. `users.id` stays put so
11115
+ * existing permission rows keep matching. Returns the new pubkey (and
11116
+ * private half when the server generated it). Requires Service role.
11117
+ */
11118
+ async adminRotateServiceAccountKey(userId, body = {}) {
11119
+ return this.request("POST", `/admin/service-accounts/${encodeURIComponent(userId)}/rotate-key`, { body });
11120
+ }
11121
+ /**
11122
+ * Lock a service account and revoke all of its device keys. Idempotent.
11123
+ * Refuses targets whose `users.role` isn't `"service"`. Requires
11124
+ * Service role.
11125
+ */
11126
+ async adminRevokeServiceAccount(userId) {
11127
+ await this.request("DELETE", `/admin/service-accounts/${encodeURIComponent(userId)}`);
11128
+ }
11129
+ /**
11130
+ * Revoke a single device key on a user (any role). Bumps
11131
+ * `tokens_invalid_before` so open WS sessions tied to the key must
11132
+ * re-auth. Requires elevated role (Service or Admin@root).
11133
+ */
11134
+ async adminRevokeDeviceKey(userId, keyId) {
11135
+ await this.request("POST", `/admin/users/${encodeURIComponent(userId)}/device-keys/${encodeURIComponent(keyId)}/revoke`);
11136
+ }
11137
+ /**
11032
11138
  * Page through the audit log. Filters AND-combine; `limit` defaults to
11033
11139
  * 100 server-side. Requires elevated role.
11034
11140
  */
@@ -11149,6 +11255,30 @@ var AbracadabraClient = class {
11149
11255
  async adminConfigEnvSnapshot() {
11150
11256
  return this.request("GET", "/admin/config/env-snapshot");
11151
11257
  }
11258
+ /**
11259
+ * List every route pattern that currently has at least one per-route
11260
+ * config override. Use {@link adminConfigGetRoute} to read individual
11261
+ * fields. Requires elevated role.
11262
+ */
11263
+ async adminConfigListRoutes() {
11264
+ return (await this.request("GET", "/admin/config/routes")).routes;
11265
+ }
11266
+ /**
11267
+ * Read a field's effective value scoped to `route`, falling back to
11268
+ * the global value when no per-route override exists. `origin_kind`
11269
+ * is `"route_override"` only when an override is actually set.
11270
+ */
11271
+ async adminConfigGetRoute(route, path) {
11272
+ return this.request("GET", `/admin/config/routes/${encodeURIComponent(route)}/fields/${encodeURIComponent(path)}`);
11273
+ }
11274
+ /** Set or replace a per-route override. Mirrors {@link adminConfigSet}. */
11275
+ async adminConfigSetRoute(route, path, value) {
11276
+ return this.request("PUT", `/admin/config/routes/${encodeURIComponent(route)}/fields/${encodeURIComponent(path)}`, { body: { value } });
11277
+ }
11278
+ /** Clear a per-route override (falls back to global). True if one existed. */
11279
+ async adminConfigUnsetRoute(route, path) {
11280
+ return (await this.request("DELETE", `/admin/config/routes/${encodeURIComponent(route)}/fields/${encodeURIComponent(path)}`)).existed;
11281
+ }
11152
11282
  /** List snapshot metadata for a document. */
11153
11283
  async listSnapshots(docId, opts) {
11154
11284
  const params = new URLSearchParams();
@@ -14660,6 +14790,10 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
14660
14790
  if (!idbAvailable$1()) return Promise.resolve(null);
14661
14791
  if (!this.dbPromise) this.dbPromise = openDb$1(this.origin).catch(() => null).then((db) => {
14662
14792
  this.db = db;
14793
+ if (db) db.onclose = () => {
14794
+ if (this.db === db) this.db = null;
14795
+ this.dbPromise = null;
14796
+ };
14663
14797
  return db;
14664
14798
  });
14665
14799
  return this.dbPromise;
@@ -14678,10 +14812,23 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
14678
14812
  const key = this.blobKey(docId, uploadId);
14679
14813
  const existing = this.objectUrls.get(key);
14680
14814
  if (existing) return existing;
14681
- const db = await this.getDb();
14815
+ let db = await this.getDb();
14682
14816
  if (db) {
14683
- const tx = db.transaction("blobs", "readonly");
14684
- const entry = await txPromise(tx.objectStore("blobs"), tx.objectStore("blobs").get(key));
14817
+ let entry;
14818
+ try {
14819
+ const tx = db.transaction("blobs", "readonly");
14820
+ entry = await txPromise(tx.objectStore("blobs"), tx.objectStore("blobs").get(key));
14821
+ } catch (err) {
14822
+ if (err?.name === "InvalidStateError") {
14823
+ if (this.db === db) this.db = null;
14824
+ this.dbPromise = null;
14825
+ db = await this.getDb();
14826
+ if (db) {
14827
+ const tx = db.transaction("blobs", "readonly");
14828
+ entry = await txPromise(tx.objectStore("blobs"), tx.objectStore("blobs").get(key));
14829
+ }
14830
+ } else throw err;
14831
+ }
14685
14832
  if (entry) {
14686
14833
  const url = URL.createObjectURL(entry.blob);
14687
14834
  this.objectUrls.set(key, url);
@@ -14932,6 +15079,7 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
14932
15079
  this.objectUrls.clear();
14933
15080
  this.db?.close();
14934
15081
  this.db = null;
15082
+ this.dbPromise = null;
14935
15083
  this.removeAllListeners();
14936
15084
  }
14937
15085
  };
@@ -15482,6 +15630,112 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
15482
15630
  }
15483
15631
  };
15484
15632
 
15633
+ //#endregion
15634
+ //#region packages/provider/src/DocUtils.ts
15635
+ /**
15636
+ * Shared utilities for the DocumentManager ORM layer.
15637
+ *
15638
+ * These functions were previously duplicated across `mcp/src/utils.ts`,
15639
+ * `mcp/src/server.ts`, `cli/src/connection.ts`, and `mcp/src/tools/tree.ts`.
15640
+ */
15641
+ /**
15642
+ * Wait for a provider's `synced` event with a timeout.
15643
+ * Resolves immediately if the provider is already synced.
15644
+ */
15645
+ function waitForSync(provider, timeoutMs = 15e3) {
15646
+ if (provider.isSynced) return Promise.resolve();
15647
+ return new Promise((resolve, reject) => {
15648
+ const timer = setTimeout(() => {
15649
+ provider.off("synced", handler);
15650
+ reject(/* @__PURE__ */ new Error(`Sync timed out after ${timeoutMs}ms`));
15651
+ }, timeoutMs);
15652
+ function handler() {
15653
+ clearTimeout(timer);
15654
+ resolve();
15655
+ }
15656
+ provider.on("synced", handler);
15657
+ });
15658
+ }
15659
+ /**
15660
+ * Wrap a promise with a timeout.
15661
+ */
15662
+ function withTimeout(promise, timeoutMs, message) {
15663
+ return new Promise((resolve, reject) => {
15664
+ const timer = setTimeout(() => reject(new Error(message ?? `Operation timed out after ${timeoutMs}ms`)), timeoutMs);
15665
+ promise.then((val) => {
15666
+ clearTimeout(timer);
15667
+ resolve(val);
15668
+ }, (err) => {
15669
+ clearTimeout(timer);
15670
+ reject(err);
15671
+ });
15672
+ });
15673
+ }
15674
+ /**
15675
+ * Normalize a document ID so the hub/root doc ID is treated as the tree root
15676
+ * (null). This lets callers pass the hub doc_id from list_spaces as
15677
+ * parentId/rootId and get the expected root-level results instead of an empty
15678
+ * set.
15679
+ */
15680
+ function normalizeRootId(id, rootDocId) {
15681
+ if (id == null) return null;
15682
+ return id === rootDocId ? null : id;
15683
+ }
15684
+ /**
15685
+ * Safely read a tree map value, converting Y.Map to plain object if needed.
15686
+ */
15687
+ function toPlain(val) {
15688
+ return val instanceof yjs.Map ? val.toJSON() : val;
15689
+ }
15690
+ /**
15691
+ * Build a tree/trash entry as a nested `Y.Map`. Use for a brand-new or
15692
+ * re-created key (create / duplicate / restore) where no concurrent
15693
+ * writer exists, so a whole-value write is safe. `undefined` fields are
15694
+ * omitted; `null` is kept (a real value, e.g. top-level `parentId`).
15695
+ */
15696
+ function makeEntryMap(fields) {
15697
+ const m = new yjs.Map();
15698
+ for (const [k, v] of Object.entries(fields)) if (v !== void 0) m.set(k, v);
15699
+ return m;
15700
+ }
15701
+ /**
15702
+ * Patch an EXISTING entry's fields per-key on its nested `Y.Map`, so a
15703
+ * concurrent edit to a *different* field by a peer is preserved instead
15704
+ * of being clobbered by a whole-entry write — the whole-entry-LWW fix
15705
+ * (audit ⑦), the mirror of the Rust provider's `with_entry_mut`.
15706
+ *
15707
+ * - nested `Y.Map` entry → set/delete only the touched keys in place;
15708
+ * - legacy opaque (plain-object) entry → migrated once to a `Y.Map`;
15709
+ * - missing entry → created from the patch (lenient; matches the prior
15710
+ * call-site behaviour of spreading `undefined`).
15711
+ *
15712
+ * A patch value of `undefined` deletes the key; `null` is written.
15713
+ * Self-transacting: it batches its writes in one `Y.Doc` transaction
15714
+ * (a safe reentrant no-op join when already inside one), so callers
15715
+ * don't need to pass or own a transaction.
15716
+ */
15717
+ function patchEntry(treeMap, id, patch, removeKeys = []) {
15718
+ const apply = () => {
15719
+ const raw = treeMap.get(id);
15720
+ if (raw instanceof yjs.Map) {
15721
+ for (const [k, v] of Object.entries(patch)) if (v === void 0) raw.delete(k);
15722
+ else raw.set(k, v);
15723
+ for (const k of removeKeys) raw.delete(k);
15724
+ return;
15725
+ }
15726
+ const merged = {
15727
+ ...raw == null ? {} : toPlain(raw),
15728
+ ...patch
15729
+ };
15730
+ for (const [k, v] of Object.entries(patch)) if (v === void 0) delete merged[k];
15731
+ for (const k of removeKeys) delete merged[k];
15732
+ treeMap.set(id, makeEntryMap(merged));
15733
+ };
15734
+ const doc = treeMap.doc;
15735
+ if (doc) doc.transact(apply);
15736
+ else apply();
15737
+ }
15738
+
15485
15739
  //#endregion
15486
15740
  //#region packages/provider/src/TreeTimestamps.ts
15487
15741
  /**
@@ -15519,13 +15773,8 @@ function attachUpdatedAtObserver(treeMap, childDocId, childDoc, offlineStore, op
15519
15773
  let pendingTs = 0;
15520
15774
  let timer = null;
15521
15775
  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
- });
15776
+ if (!treeMap.get(childDocId)) return;
15777
+ patchEntry(treeMap, childDocId, { updatedAt: ts });
15529
15778
  lastFlushedAt = ts;
15530
15779
  }
15531
15780
  function flushPending() {
@@ -18152,64 +18401,6 @@ function resolvePageType(key) {
18152
18401
  return PAGE_TYPES[TYPE_ALIASES[key] ?? key];
18153
18402
  }
18154
18403
 
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
18404
  //#endregion
18214
18405
  //#region packages/provider/src/SchemaTypes.ts
18215
18406
  /**
@@ -18255,9 +18446,112 @@ function projectTreeEntry(entry, expectedType) {
18255
18446
  * Extracted from `mcp/tools/tree.ts` and `cli/commands/documents.ts` logic.
18256
18447
  * All tree CRUD operations go through this class.
18257
18448
  */
18449
+ /**
18450
+ * Stable total order over tree siblings: `order` ascending, then `id`
18451
+ * ascending as a deterministic tiebreak. The legacy scan sorted by
18452
+ * `order` alone and left ties to insertion/iteration order — a superset
18453
+ * change that makes cursor pagination well-defined.
18454
+ */
18455
+ function cmpKey(oa, ia, ob, ib) {
18456
+ if (oa !== ob) return oa < ob ? -1 : 1;
18457
+ return ia < ib ? -1 : ia > ib ? 1 : 0;
18458
+ }
18459
+ function cmpEntry(a, b) {
18460
+ return cmpKey(a.order ?? 0, a.id, b.order ?? 0, b.id);
18461
+ }
18462
+ /** Opaque, dependency-free cursor over the (order,id) sibling order. */
18463
+ function encodeCursor(order, id) {
18464
+ return encodeURIComponent(JSON.stringify([order ?? 0, id]));
18465
+ }
18466
+ function decodeCursor(c) {
18467
+ try {
18468
+ const v = JSON.parse(decodeURIComponent(c));
18469
+ if (Array.isArray(v) && typeof v[0] === "number" && typeof v[1] === "string") return {
18470
+ order: v[0],
18471
+ id: v[1]
18472
+ };
18473
+ } catch {}
18474
+ return null;
18475
+ }
18258
18476
  var TreeManager = class {
18259
18477
  constructor(dm) {
18260
18478
  this.dm = dm;
18479
+ this._idxMap = null;
18480
+ this._idxObserver = null;
18481
+ this._idxDirty = true;
18482
+ this._byId = /* @__PURE__ */ new Map();
18483
+ this._childrenByParent = /* @__PURE__ */ new Map();
18484
+ }
18485
+ /**
18486
+ * Ensure the index is enabled, bound to the current root doc's tree
18487
+ * map, and fresh. Returns `false` when the index is disabled or there
18488
+ * is no tree map yet — callers then use the legacy scan path.
18489
+ */
18490
+ ensureIndex() {
18491
+ if (!this.dm.treeIndexEnabled) return false;
18492
+ const treeMap = this.dm.getTreeMap();
18493
+ if (!treeMap) {
18494
+ this.unbindIndex();
18495
+ return false;
18496
+ }
18497
+ if (treeMap !== this._idxMap) {
18498
+ this.unbindIndex();
18499
+ const obs = () => {
18500
+ this._idxDirty = true;
18501
+ };
18502
+ treeMap.observeDeep(obs);
18503
+ this._idxMap = treeMap;
18504
+ this._idxObserver = obs;
18505
+ this._idxDirty = true;
18506
+ }
18507
+ if (this._idxDirty) this.rebuildIndex(treeMap);
18508
+ return true;
18509
+ }
18510
+ unbindIndex() {
18511
+ if (this._idxMap && this._idxObserver) this._idxMap.unobserveDeep(this._idxObserver);
18512
+ this._idxMap = null;
18513
+ this._idxObserver = null;
18514
+ this._byId = /* @__PURE__ */ new Map();
18515
+ this._childrenByParent = /* @__PURE__ */ new Map();
18516
+ this._idxDirty = true;
18517
+ }
18518
+ rebuildIndex(treeMap) {
18519
+ const root = this.dm.rootDocId;
18520
+ const byId = /* @__PURE__ */ new Map();
18521
+ const childrenByParent = /* @__PURE__ */ new Map();
18522
+ treeMap.forEach((raw, id) => {
18523
+ const value = toPlain(raw);
18524
+ if (typeof value !== "object" || value === null) return;
18525
+ const entry = {
18526
+ id,
18527
+ label: value.label || "Untitled",
18528
+ parentId: normalizeRootId(value.parentId ?? null, root),
18529
+ order: value.order ?? 0,
18530
+ type: value.type,
18531
+ meta: value.meta,
18532
+ createdAt: value.createdAt,
18533
+ updatedAt: value.updatedAt
18534
+ };
18535
+ byId.set(id, entry);
18536
+ let bucket = childrenByParent.get(entry.parentId);
18537
+ if (!bucket) {
18538
+ bucket = [];
18539
+ childrenByParent.set(entry.parentId, bucket);
18540
+ }
18541
+ bucket.push(entry);
18542
+ });
18543
+ for (const bucket of childrenByParent.values()) bucket.sort(cmpEntry);
18544
+ this._byId = byId;
18545
+ this._childrenByParent = childrenByParent;
18546
+ this._idxDirty = false;
18547
+ }
18548
+ /**
18549
+ * Release the deep observer. Optional — the observer is auto-rebound
18550
+ * on space switch and becomes moot when the root Y.Doc is GC'd — but
18551
+ * available for consumers that want deterministic teardown.
18552
+ */
18553
+ dispose() {
18554
+ this.unbindIndex();
18261
18555
  }
18262
18556
  /** Read all tree entries as plain objects. */
18263
18557
  readEntries() {
@@ -18280,15 +18574,83 @@ var TreeManager = class {
18280
18574
  });
18281
18575
  return entries;
18282
18576
  }
18577
+ /**
18578
+ * Like {@link readEntries} but with every entry's *stored* parentId
18579
+ * run through {@link normalizeRootId} (parentId === rootDocId → null),
18580
+ * so a cou-sh / orphan-rescue top-level doc (parentId === spaceRoot)
18581
+ * resolves to top-level identically to a provider-created one
18582
+ * (parentId: null). Without this, the raw `parentId === spaceRoot`
18583
+ * form never matches the normalized `null` query and such docs are
18584
+ * silently invisible cross-client. Mirrors the Rust provider's
18585
+ * `normalized_entries`. readEntries/get keep raw values for
18586
+ * round-trip consumers; only tree-walk reads use this.
18587
+ */
18588
+ normalizedEntries() {
18589
+ if (this.ensureIndex()) return Array.from(this._byId.values());
18590
+ const root = this.dm.rootDocId;
18591
+ return this.readEntries().map((e) => ({
18592
+ ...e,
18593
+ parentId: normalizeRootId(e.parentId, root)
18594
+ }));
18595
+ }
18283
18596
  /** Get immediate children of a parent (sorted by order). */
18284
18597
  childrenOf(parentId) {
18285
18598
  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));
18599
+ if (this.ensureIndex()) {
18600
+ const bucket = this._childrenByParent.get(normalized);
18601
+ return bucket ? bucket.slice() : [];
18602
+ }
18603
+ return this.normalizedEntries().filter((e) => e.parentId === normalized).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
18604
+ }
18605
+ /**
18606
+ * Paginated immediate children — the Path-1 surface for large fan-out
18607
+ * parents. Walks the same stable (order,id) sibling order as
18608
+ * {@link childrenOf}; `cursor` is opaque (round-trip `nextCursor`).
18609
+ * `limit` defaults to 100. A stale/garbage cursor restarts from the
18610
+ * head rather than throwing. Cursor stability is exact when the index
18611
+ * is enabled; on the legacy scan path siblings with equal `order`
18612
+ * may shift between calls.
18613
+ */
18614
+ childrenOfPage(parentId, opts = {}) {
18615
+ const all = this.childrenOf(parentId);
18616
+ const limit = opts.limit != null && opts.limit > 0 ? Math.floor(opts.limit) : 100;
18617
+ let start = 0;
18618
+ if (opts.cursor) {
18619
+ const dec = decodeCursor(opts.cursor);
18620
+ if (dec) {
18621
+ const at = all.findIndex((e) => cmpKey(e.order ?? 0, e.id, dec.order, dec.id) > 0);
18622
+ start = at < 0 ? all.length : at;
18623
+ }
18624
+ }
18625
+ const entries = all.slice(start, start + limit);
18626
+ const last = entries[entries.length - 1];
18627
+ return {
18628
+ entries,
18629
+ nextCursor: last && start + limit < all.length ? encodeCursor(last.order ?? 0, last.id) : null
18630
+ };
18287
18631
  }
18288
18632
  /** Get all descendants recursively. */
18289
18633
  descendantsOf(parentId) {
18290
18634
  const normalized = normalizeRootId(parentId, this.dm.rootDocId);
18291
- const entries = this.readEntries();
18635
+ if (this.ensureIndex()) {
18636
+ const result = [];
18637
+ const visited = /* @__PURE__ */ new Set();
18638
+ const walk = (pid) => {
18639
+ if (pid !== null) {
18640
+ if (visited.has(pid)) return;
18641
+ visited.add(pid);
18642
+ }
18643
+ const bucket = this._childrenByParent.get(pid);
18644
+ if (!bucket) return;
18645
+ for (const child of bucket) {
18646
+ result.push(child);
18647
+ walk(child.id);
18648
+ }
18649
+ };
18650
+ walk(normalized);
18651
+ return result;
18652
+ }
18653
+ const entries = this.normalizedEntries();
18292
18654
  const result = [];
18293
18655
  const visited = /* @__PURE__ */ new Set();
18294
18656
  const collect = (pid) => {
@@ -18305,9 +18667,25 @@ var TreeManager = class {
18305
18667
  /** Build nested tree JSON. */
18306
18668
  buildTree(rootId, maxDepth = 3) {
18307
18669
  const normalized = normalizeRootId(rootId ?? null, this.dm.rootDocId);
18308
- const entries = this.readEntries();
18670
+ if (this.ensureIndex()) return this._buildTreeIndexed(normalized, maxDepth, 0, /* @__PURE__ */ new Set());
18671
+ const entries = this.normalizedEntries();
18309
18672
  return this._buildTree(entries, normalized, maxDepth, 0, /* @__PURE__ */ new Set());
18310
18673
  }
18674
+ _buildTreeIndexed(rootId, maxDepth, currentDepth, visited) {
18675
+ if (maxDepth >= 0 && currentDepth >= maxDepth) return [];
18676
+ return (this._childrenByParent.get(rootId) ?? []).filter((e) => !visited.has(e.id)).map((entry) => {
18677
+ const next = new Set(visited);
18678
+ next.add(entry.id);
18679
+ return {
18680
+ id: entry.id,
18681
+ label: entry.label,
18682
+ type: entry.type,
18683
+ meta: entry.meta,
18684
+ order: entry.order,
18685
+ children: this._buildTreeIndexed(entry.id, maxDepth, currentDepth + 1, next)
18686
+ };
18687
+ });
18688
+ }
18311
18689
  _buildTree(entries, rootId, maxDepth, currentDepth, visited) {
18312
18690
  if (maxDepth >= 0 && currentDepth >= maxDepth) return [];
18313
18691
  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 +18736,7 @@ var TreeManager = class {
18358
18736
  }
18359
18737
  /** Search by label (case-insensitive substring match). */
18360
18738
  find(query, rootId) {
18361
- const entries = this.readEntries();
18739
+ const entries = this.normalizedEntries();
18362
18740
  const lowerQuery = query.toLowerCase();
18363
18741
  const normalized = normalizeRootId(rootId ?? null, this.dm.rootDocId);
18364
18742
  const matches = (normalized ? this.descendantsOf(normalized) : entries).filter((e) => e.label.toLowerCase().includes(lowerQuery));
@@ -18392,7 +18770,7 @@ var TreeManager = class {
18392
18770
  const normalizedParent = normalizeRootId(opts.parentId ?? null, this.dm.rootDocId);
18393
18771
  const now = Date.now();
18394
18772
  rootDoc.transact(() => {
18395
- treeMap.set(id, {
18773
+ treeMap.set(id, makeEntryMap({
18396
18774
  label: opts.label,
18397
18775
  parentId: normalizedParent,
18398
18776
  order: now,
@@ -18400,7 +18778,7 @@ var TreeManager = class {
18400
18778
  meta: opts.meta,
18401
18779
  createdAt: now,
18402
18780
  updatedAt: now
18403
- });
18781
+ }));
18404
18782
  });
18405
18783
  return {
18406
18784
  id,
@@ -18439,12 +18817,9 @@ var TreeManager = class {
18439
18817
  const treeMap = this.dm.getTreeMap();
18440
18818
  const rootDoc = this.dm.rootDocument;
18441
18819
  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);
18820
+ if (!treeMap.get(docId)) throw new Error(`Document ${docId} not found`);
18445
18821
  rootDoc.transact(() => {
18446
- treeMap.set(docId, {
18447
- ...entry,
18822
+ patchEntry(treeMap, docId, {
18448
18823
  label,
18449
18824
  updatedAt: Date.now()
18450
18825
  });
@@ -18455,12 +18830,9 @@ var TreeManager = class {
18455
18830
  const treeMap = this.dm.getTreeMap();
18456
18831
  const rootDoc = this.dm.rootDocument;
18457
18832
  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);
18833
+ if (!treeMap.get(docId)) throw new Error(`Document ${docId} not found`);
18461
18834
  rootDoc.transact(() => {
18462
- treeMap.set(docId, {
18463
- ...entry,
18835
+ patchEntry(treeMap, docId, {
18464
18836
  parentId: normalizeRootId(newParentId ?? null, this.dm.rootDocId),
18465
18837
  order: order ?? Date.now(),
18466
18838
  updatedAt: Date.now()
@@ -18472,12 +18844,9 @@ var TreeManager = class {
18472
18844
  const treeMap = this.dm.getTreeMap();
18473
18845
  const rootDoc = this.dm.rootDocument;
18474
18846
  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);
18847
+ if (!treeMap.get(docId)) throw new Error(`Document ${docId} not found`);
18478
18848
  rootDoc.transact(() => {
18479
- treeMap.set(docId, {
18480
- ...entry,
18849
+ patchEntry(treeMap, docId, {
18481
18850
  type,
18482
18851
  updatedAt: Date.now()
18483
18852
  });
@@ -18492,10 +18861,12 @@ var TreeManager = class {
18492
18861
  const trashMap = this.dm.getTrashMap();
18493
18862
  const rootDoc = this.dm.rootDocument;
18494
18863
  if (!treeMap || !trashMap || !rootDoc) throw new Error("Not connected");
18495
- const entries = this.readEntries();
18496
- const toDelete = [docId, ...this._descendantIds(entries, docId)];
18497
18864
  const now = Date.now();
18865
+ let deletedCount = 0;
18498
18866
  rootDoc.transact(() => {
18867
+ const entries = this.readEntries();
18868
+ const toDelete = [docId, ...this._descendantIds(entries, docId)];
18869
+ deletedCount = toDelete.length;
18499
18870
  for (const nid of toDelete) {
18500
18871
  const raw = treeMap.get(nid);
18501
18872
  if (!raw) continue;
@@ -18511,7 +18882,7 @@ var TreeManager = class {
18511
18882
  treeMap.delete(nid);
18512
18883
  }
18513
18884
  });
18514
- return toDelete.length;
18885
+ return deletedCount;
18515
18886
  }
18516
18887
  /** Duplicate a document (shallow clone). Returns the new entry. */
18517
18888
  duplicate(docId) {
@@ -18523,13 +18894,13 @@ var TreeManager = class {
18523
18894
  const newId = crypto.randomUUID();
18524
18895
  const now = Date.now();
18525
18896
  const newLabel = (entry.label || "Untitled") + " (copy)";
18526
- treeMap.set(newId, {
18897
+ treeMap.set(newId, makeEntryMap({
18527
18898
  ...entry,
18528
18899
  label: newLabel,
18529
18900
  order: now,
18530
18901
  createdAt: now,
18531
18902
  updatedAt: now
18532
- });
18903
+ }));
18533
18904
  return {
18534
18905
  id: newId,
18535
18906
  label: newLabel,
@@ -18547,21 +18918,37 @@ var TreeManager = class {
18547
18918
  const trashMap = this.dm.getTrashMap();
18548
18919
  const rootDoc = this.dm.rootDocument;
18549
18920
  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);
18921
+ if (!trashMap.get(docId)) throw new Error(`Document ${docId} not found in trash`);
18553
18922
  const now = Date.now();
18554
18923
  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
18924
+ const trashed = /* @__PURE__ */ new Map();
18925
+ trashMap.forEach((raw, id) => {
18926
+ const v = toPlain(raw);
18927
+ if (typeof v === "object" && v !== null) trashed.set(id, v);
18563
18928
  });
18564
- trashMap.delete(docId);
18929
+ const toRestore = [];
18930
+ const visited = /* @__PURE__ */ new Set();
18931
+ const collect = (id) => {
18932
+ if (visited.has(id)) return;
18933
+ visited.add(id);
18934
+ if (!trashed.has(id)) return;
18935
+ toRestore.push(id);
18936
+ for (const [cid, v] of trashed) if ((v.parentId ?? null) === id) collect(cid);
18937
+ };
18938
+ collect(docId);
18939
+ for (const id of toRestore) {
18940
+ const entry = trashed.get(id);
18941
+ treeMap.set(id, makeEntryMap({
18942
+ label: entry.label || "Untitled",
18943
+ parentId: entry.parentId ?? null,
18944
+ order: entry.order ?? now,
18945
+ type: entry.type,
18946
+ meta: entry.meta,
18947
+ createdAt: entry.createdAt ?? now,
18948
+ updatedAt: now
18949
+ }));
18950
+ trashMap.delete(id);
18951
+ }
18565
18952
  });
18566
18953
  }
18567
18954
  /** List trashed documents. */
@@ -19937,9 +20324,9 @@ var ContentManager = class {
19937
20324
  * body, tree metadata, and immediate children.
19938
20325
  */
19939
20326
  async read(docId) {
19940
- const { title, markdown } = yjsToMarkdown((await this.dm.getChildProvider(docId)).document.getXmlFragment("default"));
20327
+ const fragment = (await this.dm.getChildProvider(docId)).document.getXmlFragment("default");
19941
20328
  const treeMap = this.dm.getTreeMap();
19942
- let label = title;
20329
+ let label = "Untitled";
19943
20330
  let type;
19944
20331
  let meta;
19945
20332
  const childrenWithOrder = [];
@@ -19947,7 +20334,7 @@ var ContentManager = class {
19947
20334
  const raw = treeMap.get(docId);
19948
20335
  if (raw) {
19949
20336
  const entry = toPlain(raw);
19950
- label = entry.label || title;
20337
+ label = entry.label || label;
19951
20338
  type = entry.type;
19952
20339
  meta = entry.meta;
19953
20340
  }
@@ -19969,11 +20356,12 @@ var ContentManager = class {
19969
20356
  type,
19970
20357
  meta
19971
20358
  }));
20359
+ const markdown = yjsToMarkdown(fragment, label, meta, type);
19972
20360
  return {
19973
20361
  label,
19974
20362
  type,
19975
20363
  meta,
19976
- title,
20364
+ title: label,
19977
20365
  markdown,
19978
20366
  children
19979
20367
  };
@@ -19995,16 +20383,14 @@ var ContentManager = class {
19995
20383
  if (treeMap && rootDoc) {
19996
20384
  const entry = treeMap.get(docId);
19997
20385
  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 ?? {},
20386
+ const cur = toPlain(entry);
20387
+ const patch = { updatedAt: Date.now() };
20388
+ if (title) patch.label = title;
20389
+ if (Object.keys(meta).length > 0) patch.meta = {
20390
+ ...cur.meta ?? {},
20005
20391
  ...meta
20006
20392
  };
20007
- treeMap.set(docId, updates);
20393
+ patchEntry(treeMap, docId, patch);
20008
20394
  });
20009
20395
  }
20010
20396
  }
@@ -20123,8 +20509,7 @@ var MetaManager = class {
20123
20509
  ...meta
20124
20510
  };
20125
20511
  this.validateOrThrow(docId, entry, mergedMeta);
20126
- treeMap.set(docId, {
20127
- ...entry,
20512
+ patchEntry(treeMap, docId, {
20128
20513
  meta: mergedMeta,
20129
20514
  updatedAt: Date.now()
20130
20515
  });
@@ -20143,8 +20528,7 @@ var MetaManager = class {
20143
20528
  if (!raw) throw new Error(`Document ${docId} not found`);
20144
20529
  const entry = toPlain(raw);
20145
20530
  this.validateOrThrow(docId, entry, meta);
20146
- treeMap.set(docId, {
20147
- ...entry,
20531
+ patchEntry(treeMap, docId, {
20148
20532
  meta,
20149
20533
  updatedAt: Date.now()
20150
20534
  });
@@ -20161,8 +20545,7 @@ var MetaManager = class {
20161
20545
  const updated = { ...entry.meta ?? {} };
20162
20546
  for (const key of keys) delete updated[key];
20163
20547
  this.validateOrThrow(docId, entry, updated);
20164
- treeMap.set(docId, {
20165
- ...entry,
20548
+ patchEntry(treeMap, docId, {
20166
20549
  meta: updated,
20167
20550
  updatedAt: Date.now()
20168
20551
  });
@@ -20269,6 +20652,13 @@ var DocumentManager = class {
20269
20652
  get rootDocId() {
20270
20653
  return this._rootDocId;
20271
20654
  }
20655
+ /**
20656
+ * Whether the TreeManager in-memory index is enabled (Path-1 prototype).
20657
+ * Off by default — see {@link DocumentManagerConfig.treeIndex}.
20658
+ */
20659
+ get treeIndexEnabled() {
20660
+ return this._config.treeIndex ?? false;
20661
+ }
20272
20662
  get rootDocument() {
20273
20663
  return this._rootDoc;
20274
20664
  }
@@ -20491,10 +20881,12 @@ exports.generateMnemonic = generateMnemonic;
20491
20881
  exports.isEncryptedContent = isEncryptedContent;
20492
20882
  exports.makeEncryptedYMap = makeEncryptedYMap;
20493
20883
  exports.makeEncryptedYText = makeEncryptedYText;
20884
+ exports.makeEntryMap = makeEntryMap;
20494
20885
  exports.mnemonicToEd25519Seed = mnemonicToEd25519Seed;
20495
20886
  exports.mnemonicToKeyPair = mnemonicToKeyPair;
20496
20887
  exports.normalizeRootId = normalizeRootId;
20497
20888
  exports.parseFrontmatter = parseFrontmatter;
20889
+ exports.patchEntry = patchEntry;
20498
20890
  exports.populateYDocFromMarkdown = populateYDocFromMarkdown;
20499
20891
  exports.readAuthMessage = readAuthMessage;
20500
20892
  exports.readBlocksFromFragment = readBlocksFromFragment;