@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.
- package/README.md +50 -0
- package/dist/abracadabra-provider.cjs +534 -142
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +529 -153
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +248 -2
- package/package.json +2 -2
- package/src/AbracadabraClient.ts +273 -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,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
|
-
|
|
14815
|
+
let db = await this.getDb();
|
|
14682
14816
|
if (db) {
|
|
14683
|
-
|
|
14684
|
-
|
|
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
|
-
|
|
15523
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
18556
|
-
|
|
18557
|
-
|
|
18558
|
-
|
|
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
|
-
|
|
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
|
|
20327
|
+
const fragment = (await this.dm.getChildProvider(docId)).document.getXmlFragment("default");
|
|
19941
20328
|
const treeMap = this.dm.getTreeMap();
|
|
19942
|
-
let label =
|
|
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 ||
|
|
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
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|