@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.
- package/README.md +50 -0
- package/dist/abracadabra-provider.cjs +491 -142
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +486 -153
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +192 -2
- package/package.json +2 -2
- package/src/AbracadabraClient.ts +195 -13
- package/src/AbracadabraProvider.ts +25 -16
- package/src/ContentManager.ts +19 -11
- package/src/DocUtils.ts +62 -0
- package/src/DocumentManager.ts +18 -0
- package/src/FileBlobStore.ts +43 -6
- package/src/MetaManager.ts +4 -7
- package/src/TreeManager.ts +343 -47
- package/src/TreeTimestamps.ts +6 -2
- package/src/index.ts +9 -1
|
@@ -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
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
update,
|
|
3291
|
-
|
|
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
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
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
|
|
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
|
-
|
|
14772
|
+
let db = await this.getDb();
|
|
14682
14773
|
if (db) {
|
|
14683
|
-
|
|
14684
|
-
|
|
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
|
-
|
|
15523
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
18556
|
-
|
|
18557
|
-
|
|
18558
|
-
|
|
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
|
-
|
|
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
|
|
20284
|
+
const fragment = (await this.dm.getChildProvider(docId)).document.getXmlFragment("default");
|
|
19941
20285
|
const treeMap = this.dm.getTreeMap();
|
|
19942
|
-
let label =
|
|
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 ||
|
|
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
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|