@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
|
@@ -3251,22 +3251,23 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3251
3251
|
* errors across async await boundaries.
|
|
3252
3252
|
*/
|
|
3253
3253
|
async flushPendingUpdates() {
|
|
3254
|
-
if (!this.canWrite) return;
|
|
3255
3254
|
const store = this.offlineStore;
|
|
3256
3255
|
if (!store) return;
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
update,
|
|
3261
|
-
|
|
3256
|
+
if (this.canWrite) {
|
|
3257
|
+
const updates = await store.getPendingUpdates();
|
|
3258
|
+
if (updates.length > 0) {
|
|
3259
|
+
for (const update of updates) this.send(UpdateMessage, {
|
|
3260
|
+
update,
|
|
3261
|
+
documentName: this.configuration.name
|
|
3262
|
+
});
|
|
3263
|
+
await store.clearPendingUpdates();
|
|
3264
|
+
}
|
|
3265
|
+
const pendingSubdocs = await store.getPendingSubdocs();
|
|
3266
|
+
for (const { childId } of pendingSubdocs) this.send(SubdocMessage, {
|
|
3267
|
+
documentName: this.configuration.name,
|
|
3268
|
+
childDocumentName: childId
|
|
3262
3269
|
});
|
|
3263
|
-
await store.clearPendingUpdates();
|
|
3264
3270
|
}
|
|
3265
|
-
const pendingSubdocs = await store.getPendingSubdocs();
|
|
3266
|
-
for (const { childId } of pendingSubdocs) this.send(SubdocMessage, {
|
|
3267
|
-
documentName: this.configuration.name,
|
|
3268
|
-
childDocumentName: childId
|
|
3269
|
-
});
|
|
3270
3271
|
const snapshot = Y.encodeStateAsUpdate(this.document);
|
|
3271
3272
|
await store.saveDocSnapshot(snapshot).catch(() => null);
|
|
3272
3273
|
}
|
|
@@ -10336,6 +10337,7 @@ function fromBase64$3(b64) {
|
|
|
10336
10337
|
}
|
|
10337
10338
|
var AbracadabraClient = class {
|
|
10338
10339
|
constructor(config) {
|
|
10340
|
+
this.rootListInflight = /* @__PURE__ */ new Map();
|
|
10339
10341
|
this.baseUrl = config.url.replace(/\/+$/, "");
|
|
10340
10342
|
this.persistAuth = config.persistAuth ?? typeof localStorage !== "undefined";
|
|
10341
10343
|
this.storageKey = config.storageKey ?? "abracadabra:auth";
|
|
@@ -10739,11 +10741,21 @@ var AbracadabraClient = class {
|
|
|
10739
10741
|
* recursive tree walks; callers that need it can read `meta.id` from
|
|
10740
10742
|
* the returned metas.
|
|
10741
10743
|
*/
|
|
10742
|
-
async listChildren(parentId) {
|
|
10743
|
-
const
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10744
|
+
async listChildren(parentId, opts) {
|
|
10745
|
+
const kind = opts?.kind;
|
|
10746
|
+
if (parentId) {
|
|
10747
|
+
const res = await this.request("GET", `/docs/${encodeURIComponent(parentId)}/children`);
|
|
10748
|
+
if (this.cache && res.children) await this.cache.setChildren(parentId, res.children).catch(() => null);
|
|
10749
|
+
return kind ? res.documents.filter((d) => d.kind === kind) : res.documents;
|
|
10750
|
+
}
|
|
10751
|
+
const key = kind ?? "";
|
|
10752
|
+
const existing = this.rootListInflight.get(key);
|
|
10753
|
+
const docs = existing ? await existing : await (() => {
|
|
10754
|
+
const p = this.request("GET", kind ? `/docs?root=true&kind=${encodeURIComponent(kind)}` : "/docs?root=true").then((res) => res.documents).finally(() => this.rootListInflight.delete(key));
|
|
10755
|
+
this.rootListInflight.set(key, p);
|
|
10756
|
+
return p;
|
|
10757
|
+
})();
|
|
10758
|
+
return kind ? docs.filter((d) => d.kind === kind) : docs;
|
|
10747
10759
|
}
|
|
10748
10760
|
/**
|
|
10749
10761
|
* Create a child document under a parent (requires write permission).
|
|
@@ -10872,7 +10884,7 @@ var AbracadabraClient = class {
|
|
|
10872
10884
|
* spaces resolving to any role; anonymous users see public ones.
|
|
10873
10885
|
*/
|
|
10874
10886
|
async listSpaces() {
|
|
10875
|
-
return
|
|
10887
|
+
return this.listChildren(void 0, { kind: Kind.Space });
|
|
10876
10888
|
}
|
|
10877
10889
|
/**
|
|
10878
10890
|
* Create a new top-level Space. Equivalent to a `POST /docs` with
|
|
@@ -10990,6 +11002,46 @@ var AbracadabraClient = class {
|
|
|
10990
11002
|
return this.request("POST", "/admin/storage/repair");
|
|
10991
11003
|
}
|
|
10992
11004
|
/**
|
|
11005
|
+
* Admin one-shot: populate the `snapshot_files` table for snapshots
|
|
11006
|
+
* created before that migration ran, so `SnapshotMeta.file_count` and
|
|
11007
|
+
* upload-ref tracking become accurate. Idempotent (insert-or-ignore).
|
|
11008
|
+
* Requires elevated role.
|
|
11009
|
+
*/
|
|
11010
|
+
async adminSnapshotsBackfillRefs() {
|
|
11011
|
+
return this.request("POST", "/admin/snapshots/backfill-refs");
|
|
11012
|
+
}
|
|
11013
|
+
/**
|
|
11014
|
+
* Admin one-shot: migrate pre-dedup inline snapshot data into the
|
|
11015
|
+
* content-addressed `snapshot_blobs` store. Idempotent (only migrates
|
|
11016
|
+
* rows with `data_hash IS NULL`). Requires elevated role.
|
|
11017
|
+
*/
|
|
11018
|
+
async adminSnapshotsBackfillBlobs() {
|
|
11019
|
+
return this.request("POST", "/admin/snapshots/backfill-blobs");
|
|
11020
|
+
}
|
|
11021
|
+
/**
|
|
11022
|
+
* Admin: server-wide upload listing, joined with the owning document
|
|
11023
|
+
* (label is best-effort — labels live in the CRDT, not the SQL row)
|
|
11024
|
+
* and the content-addressed blob (`ref_count` exposes dedup).
|
|
11025
|
+
* Server-side paginated + filtered. Requires elevated role.
|
|
11026
|
+
*/
|
|
11027
|
+
async adminListUploads(opts = {}) {
|
|
11028
|
+
const p = new URLSearchParams();
|
|
11029
|
+
if (opts.q) p.set("q", opts.q);
|
|
11030
|
+
if (opts.docId) p.set("doc_id", opts.docId);
|
|
11031
|
+
if (opts.limit != null) p.set("limit", String(opts.limit));
|
|
11032
|
+
if (opts.offset != null) p.set("offset", String(opts.offset));
|
|
11033
|
+
const qs = p.toString();
|
|
11034
|
+
return this.request("GET", `/admin/uploads${qs ? `?${qs}` : ""}`);
|
|
11035
|
+
}
|
|
11036
|
+
/**
|
|
11037
|
+
* Admin: aggregate storage figures. `logicalBytes` is what users
|
|
11038
|
+
* uploaded; `physicalBytes` is on-disk after content-addressed dedup;
|
|
11039
|
+
* `dedupSaved` is the difference. Requires elevated role.
|
|
11040
|
+
*/
|
|
11041
|
+
async adminStorageStats() {
|
|
11042
|
+
return this.request("GET", "/admin/storage/stats");
|
|
11043
|
+
}
|
|
11044
|
+
/**
|
|
10993
11045
|
* Clear the lockout state on a user account: zeroes the failed-login
|
|
10994
11046
|
* counter and `locked_until`. Requires elevated role (Admin or
|
|
10995
11047
|
* Service). The action is recorded in the audit log under
|
|
@@ -10999,6 +11051,60 @@ var AbracadabraClient = class {
|
|
|
10999
11051
|
await this.request("POST", `/admin/users/${encodeURIComponent(userId)}/unlock`);
|
|
11000
11052
|
}
|
|
11001
11053
|
/**
|
|
11054
|
+
* Admin: every non-deleted document the user owns (`source: "owner"`)
|
|
11055
|
+
* or has an explicit permission grant on (`source: "grant"`, with
|
|
11056
|
+
* `role`). Answers "what does this identity touch" without an N+1
|
|
11057
|
+
* client tree walk. Labels are best-effort (they live in the CRDT).
|
|
11058
|
+
* Requires elevated role.
|
|
11059
|
+
*/
|
|
11060
|
+
async adminUserDocs(userId, opts = {}) {
|
|
11061
|
+
const qs = opts.limit != null ? `?limit=${opts.limit}` : "";
|
|
11062
|
+
return this.request("GET", `/admin/users/${encodeURIComponent(userId)}/docs${qs}`);
|
|
11063
|
+
}
|
|
11064
|
+
/**
|
|
11065
|
+
* List `service`-role users (runners, demo seeders, automation
|
|
11066
|
+
* identities). Requires Service role. Admins can see service users via
|
|
11067
|
+
* `/admin/users` too but cannot mint or rotate them.
|
|
11068
|
+
*/
|
|
11069
|
+
async adminListServiceAccounts() {
|
|
11070
|
+
return this.request("GET", "/admin/service-accounts");
|
|
11071
|
+
}
|
|
11072
|
+
/**
|
|
11073
|
+
* Create a new `service`-role user. When `public_key` is omitted the
|
|
11074
|
+
* server generates a keypair and returns the private half in the
|
|
11075
|
+
* response — show it to the operator **once** and discard; the server
|
|
11076
|
+
* never persists it. Requires Service role.
|
|
11077
|
+
*/
|
|
11078
|
+
async adminCreateServiceAccount(body) {
|
|
11079
|
+
return this.request("POST", "/admin/service-accounts", { body });
|
|
11080
|
+
}
|
|
11081
|
+
/**
|
|
11082
|
+
* Rotate the active keypair on a service account. Old JWTs are
|
|
11083
|
+
* invalidated; old device keys are marked revoked; the canonical
|
|
11084
|
+
* `users.public_key` swaps to the new value. `users.id` stays put so
|
|
11085
|
+
* existing permission rows keep matching. Returns the new pubkey (and
|
|
11086
|
+
* private half when the server generated it). Requires Service role.
|
|
11087
|
+
*/
|
|
11088
|
+
async adminRotateServiceAccountKey(userId, body = {}) {
|
|
11089
|
+
return this.request("POST", `/admin/service-accounts/${encodeURIComponent(userId)}/rotate-key`, { body });
|
|
11090
|
+
}
|
|
11091
|
+
/**
|
|
11092
|
+
* Lock a service account and revoke all of its device keys. Idempotent.
|
|
11093
|
+
* Refuses targets whose `users.role` isn't `"service"`. Requires
|
|
11094
|
+
* Service role.
|
|
11095
|
+
*/
|
|
11096
|
+
async adminRevokeServiceAccount(userId) {
|
|
11097
|
+
await this.request("DELETE", `/admin/service-accounts/${encodeURIComponent(userId)}`);
|
|
11098
|
+
}
|
|
11099
|
+
/**
|
|
11100
|
+
* Revoke a single device key on a user (any role). Bumps
|
|
11101
|
+
* `tokens_invalid_before` so open WS sessions tied to the key must
|
|
11102
|
+
* re-auth. Requires elevated role (Service or Admin@root).
|
|
11103
|
+
*/
|
|
11104
|
+
async adminRevokeDeviceKey(userId, keyId) {
|
|
11105
|
+
await this.request("POST", `/admin/users/${encodeURIComponent(userId)}/device-keys/${encodeURIComponent(keyId)}/revoke`);
|
|
11106
|
+
}
|
|
11107
|
+
/**
|
|
11002
11108
|
* Page through the audit log. Filters AND-combine; `limit` defaults to
|
|
11003
11109
|
* 100 server-side. Requires elevated role.
|
|
11004
11110
|
*/
|
|
@@ -11119,6 +11225,30 @@ var AbracadabraClient = class {
|
|
|
11119
11225
|
async adminConfigEnvSnapshot() {
|
|
11120
11226
|
return this.request("GET", "/admin/config/env-snapshot");
|
|
11121
11227
|
}
|
|
11228
|
+
/**
|
|
11229
|
+
* List every route pattern that currently has at least one per-route
|
|
11230
|
+
* config override. Use {@link adminConfigGetRoute} to read individual
|
|
11231
|
+
* fields. Requires elevated role.
|
|
11232
|
+
*/
|
|
11233
|
+
async adminConfigListRoutes() {
|
|
11234
|
+
return (await this.request("GET", "/admin/config/routes")).routes;
|
|
11235
|
+
}
|
|
11236
|
+
/**
|
|
11237
|
+
* Read a field's effective value scoped to `route`, falling back to
|
|
11238
|
+
* the global value when no per-route override exists. `origin_kind`
|
|
11239
|
+
* is `"route_override"` only when an override is actually set.
|
|
11240
|
+
*/
|
|
11241
|
+
async adminConfigGetRoute(route, path) {
|
|
11242
|
+
return this.request("GET", `/admin/config/routes/${encodeURIComponent(route)}/fields/${encodeURIComponent(path)}`);
|
|
11243
|
+
}
|
|
11244
|
+
/** Set or replace a per-route override. Mirrors {@link adminConfigSet}. */
|
|
11245
|
+
async adminConfigSetRoute(route, path, value) {
|
|
11246
|
+
return this.request("PUT", `/admin/config/routes/${encodeURIComponent(route)}/fields/${encodeURIComponent(path)}`, { body: { value } });
|
|
11247
|
+
}
|
|
11248
|
+
/** Clear a per-route override (falls back to global). True if one existed. */
|
|
11249
|
+
async adminConfigUnsetRoute(route, path) {
|
|
11250
|
+
return (await this.request("DELETE", `/admin/config/routes/${encodeURIComponent(route)}/fields/${encodeURIComponent(path)}`)).existed;
|
|
11251
|
+
}
|
|
11122
11252
|
/** List snapshot metadata for a document. */
|
|
11123
11253
|
async listSnapshots(docId, opts) {
|
|
11124
11254
|
const params = new URLSearchParams();
|
|
@@ -14630,6 +14760,10 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
14630
14760
|
if (!idbAvailable$1()) return Promise.resolve(null);
|
|
14631
14761
|
if (!this.dbPromise) this.dbPromise = openDb$1(this.origin).catch(() => null).then((db) => {
|
|
14632
14762
|
this.db = db;
|
|
14763
|
+
if (db) db.onclose = () => {
|
|
14764
|
+
if (this.db === db) this.db = null;
|
|
14765
|
+
this.dbPromise = null;
|
|
14766
|
+
};
|
|
14633
14767
|
return db;
|
|
14634
14768
|
});
|
|
14635
14769
|
return this.dbPromise;
|
|
@@ -14648,10 +14782,23 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
14648
14782
|
const key = this.blobKey(docId, uploadId);
|
|
14649
14783
|
const existing = this.objectUrls.get(key);
|
|
14650
14784
|
if (existing) return existing;
|
|
14651
|
-
|
|
14785
|
+
let db = await this.getDb();
|
|
14652
14786
|
if (db) {
|
|
14653
|
-
|
|
14654
|
-
|
|
14787
|
+
let entry;
|
|
14788
|
+
try {
|
|
14789
|
+
const tx = db.transaction("blobs", "readonly");
|
|
14790
|
+
entry = await txPromise(tx.objectStore("blobs"), tx.objectStore("blobs").get(key));
|
|
14791
|
+
} catch (err) {
|
|
14792
|
+
if (err?.name === "InvalidStateError") {
|
|
14793
|
+
if (this.db === db) this.db = null;
|
|
14794
|
+
this.dbPromise = null;
|
|
14795
|
+
db = await this.getDb();
|
|
14796
|
+
if (db) {
|
|
14797
|
+
const tx = db.transaction("blobs", "readonly");
|
|
14798
|
+
entry = await txPromise(tx.objectStore("blobs"), tx.objectStore("blobs").get(key));
|
|
14799
|
+
}
|
|
14800
|
+
} else throw err;
|
|
14801
|
+
}
|
|
14655
14802
|
if (entry) {
|
|
14656
14803
|
const url = URL.createObjectURL(entry.blob);
|
|
14657
14804
|
this.objectUrls.set(key, url);
|
|
@@ -14902,6 +15049,7 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
|
|
|
14902
15049
|
this.objectUrls.clear();
|
|
14903
15050
|
this.db?.close();
|
|
14904
15051
|
this.db = null;
|
|
15052
|
+
this.dbPromise = null;
|
|
14905
15053
|
this.removeAllListeners();
|
|
14906
15054
|
}
|
|
14907
15055
|
};
|
|
@@ -15444,21 +15592,113 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
|
|
|
15444
15592
|
};
|
|
15445
15593
|
|
|
15446
15594
|
//#endregion
|
|
15447
|
-
//#region packages/provider/src/
|
|
15595
|
+
//#region packages/provider/src/DocUtils.ts
|
|
15448
15596
|
/**
|
|
15449
|
-
*
|
|
15597
|
+
* Shared utilities for the DocumentManager ORM layer.
|
|
15450
15598
|
*
|
|
15451
|
-
*
|
|
15452
|
-
*
|
|
15453
|
-
|
|
15599
|
+
* These functions were previously duplicated across `mcp/src/utils.ts`,
|
|
15600
|
+
* `mcp/src/server.ts`, `cli/src/connection.ts`, and `mcp/src/tools/tree.ts`.
|
|
15601
|
+
*/
|
|
15602
|
+
/**
|
|
15603
|
+
* Wait for a provider's `synced` event with a timeout.
|
|
15604
|
+
* Resolves immediately if the provider is already synced.
|
|
15605
|
+
*/
|
|
15606
|
+
function waitForSync(provider, timeoutMs = 15e3) {
|
|
15607
|
+
if (provider.isSynced) return Promise.resolve();
|
|
15608
|
+
return new Promise((resolve, reject) => {
|
|
15609
|
+
const timer = setTimeout(() => {
|
|
15610
|
+
provider.off("synced", handler);
|
|
15611
|
+
reject(/* @__PURE__ */ new Error(`Sync timed out after ${timeoutMs}ms`));
|
|
15612
|
+
}, timeoutMs);
|
|
15613
|
+
function handler() {
|
|
15614
|
+
clearTimeout(timer);
|
|
15615
|
+
resolve();
|
|
15616
|
+
}
|
|
15617
|
+
provider.on("synced", handler);
|
|
15618
|
+
});
|
|
15619
|
+
}
|
|
15620
|
+
/**
|
|
15621
|
+
* Wrap a promise with a timeout.
|
|
15622
|
+
*/
|
|
15623
|
+
function withTimeout(promise, timeoutMs, message) {
|
|
15624
|
+
return new Promise((resolve, reject) => {
|
|
15625
|
+
const timer = setTimeout(() => reject(new Error(message ?? `Operation timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
15626
|
+
promise.then((val) => {
|
|
15627
|
+
clearTimeout(timer);
|
|
15628
|
+
resolve(val);
|
|
15629
|
+
}, (err) => {
|
|
15630
|
+
clearTimeout(timer);
|
|
15631
|
+
reject(err);
|
|
15632
|
+
});
|
|
15633
|
+
});
|
|
15634
|
+
}
|
|
15635
|
+
/**
|
|
15636
|
+
* Normalize a document ID so the hub/root doc ID is treated as the tree root
|
|
15637
|
+
* (null). This lets callers pass the hub doc_id from list_spaces as
|
|
15638
|
+
* parentId/rootId and get the expected root-level results instead of an empty
|
|
15639
|
+
* set.
|
|
15640
|
+
*/
|
|
15641
|
+
function normalizeRootId(id, rootDocId) {
|
|
15642
|
+
if (id == null) return null;
|
|
15643
|
+
return id === rootDocId ? null : id;
|
|
15644
|
+
}
|
|
15645
|
+
/**
|
|
15646
|
+
* Safely read a tree map value, converting Y.Map to plain object if needed.
|
|
15647
|
+
*/
|
|
15648
|
+
function toPlain(val) {
|
|
15649
|
+
return val instanceof Y.Map ? val.toJSON() : val;
|
|
15650
|
+
}
|
|
15651
|
+
/**
|
|
15652
|
+
* Build a tree/trash entry as a nested `Y.Map`. Use for a brand-new or
|
|
15653
|
+
* re-created key (create / duplicate / restore) where no concurrent
|
|
15654
|
+
* writer exists, so a whole-value write is safe. `undefined` fields are
|
|
15655
|
+
* omitted; `null` is kept (a real value, e.g. top-level `parentId`).
|
|
15656
|
+
*/
|
|
15657
|
+
function makeEntryMap(fields) {
|
|
15658
|
+
const m = new Y.Map();
|
|
15659
|
+
for (const [k, v] of Object.entries(fields)) if (v !== void 0) m.set(k, v);
|
|
15660
|
+
return m;
|
|
15661
|
+
}
|
|
15662
|
+
/**
|
|
15663
|
+
* Patch an EXISTING entry's fields per-key on its nested `Y.Map`, so a
|
|
15664
|
+
* concurrent edit to a *different* field by a peer is preserved instead
|
|
15665
|
+
* of being clobbered by a whole-entry write — the whole-entry-LWW fix
|
|
15666
|
+
* (audit ⑦), the mirror of the Rust provider's `with_entry_mut`.
|
|
15454
15667
|
*
|
|
15455
|
-
*
|
|
15456
|
-
*
|
|
15668
|
+
* - nested `Y.Map` entry → set/delete only the touched keys in place;
|
|
15669
|
+
* - legacy opaque (plain-object) entry → migrated once to a `Y.Map`;
|
|
15670
|
+
* - missing entry → created from the patch (lenient; matches the prior
|
|
15671
|
+
* call-site behaviour of spreading `undefined`).
|
|
15457
15672
|
*
|
|
15458
|
-
*
|
|
15459
|
-
*
|
|
15460
|
-
*
|
|
15673
|
+
* A patch value of `undefined` deletes the key; `null` is written.
|
|
15674
|
+
* Self-transacting: it batches its writes in one `Y.Doc` transaction
|
|
15675
|
+
* (a safe reentrant no-op join when already inside one), so callers
|
|
15676
|
+
* don't need to pass or own a transaction.
|
|
15461
15677
|
*/
|
|
15678
|
+
function patchEntry(treeMap, id, patch, removeKeys = []) {
|
|
15679
|
+
const apply = () => {
|
|
15680
|
+
const raw = treeMap.get(id);
|
|
15681
|
+
if (raw instanceof Y.Map) {
|
|
15682
|
+
for (const [k, v] of Object.entries(patch)) if (v === void 0) raw.delete(k);
|
|
15683
|
+
else raw.set(k, v);
|
|
15684
|
+
for (const k of removeKeys) raw.delete(k);
|
|
15685
|
+
return;
|
|
15686
|
+
}
|
|
15687
|
+
const merged = {
|
|
15688
|
+
...raw == null ? {} : toPlain(raw),
|
|
15689
|
+
...patch
|
|
15690
|
+
};
|
|
15691
|
+
for (const [k, v] of Object.entries(patch)) if (v === void 0) delete merged[k];
|
|
15692
|
+
for (const k of removeKeys) delete merged[k];
|
|
15693
|
+
treeMap.set(id, makeEntryMap(merged));
|
|
15694
|
+
};
|
|
15695
|
+
const doc = treeMap.doc;
|
|
15696
|
+
if (doc) doc.transact(apply);
|
|
15697
|
+
else apply();
|
|
15698
|
+
}
|
|
15699
|
+
|
|
15700
|
+
//#endregion
|
|
15701
|
+
//#region packages/provider/src/TreeTimestamps.ts
|
|
15462
15702
|
/**
|
|
15463
15703
|
* Attach an observer that writes `updatedAt` to the root doc-tree entry for
|
|
15464
15704
|
* `childDocId` whenever the child doc receives a non-offline update.
|
|
@@ -15480,13 +15720,8 @@ function attachUpdatedAtObserver(treeMap, childDocId, childDoc, offlineStore, op
|
|
|
15480
15720
|
let pendingTs = 0;
|
|
15481
15721
|
let timer = null;
|
|
15482
15722
|
function writeTs(ts) {
|
|
15483
|
-
|
|
15484
|
-
|
|
15485
|
-
const entry = raw instanceof Y.Map ? raw.toJSON() : raw;
|
|
15486
|
-
treeMap.set(childDocId, {
|
|
15487
|
-
...entry,
|
|
15488
|
-
updatedAt: ts
|
|
15489
|
-
});
|
|
15723
|
+
if (!treeMap.get(childDocId)) return;
|
|
15724
|
+
patchEntry(treeMap, childDocId, { updatedAt: ts });
|
|
15490
15725
|
lastFlushedAt = ts;
|
|
15491
15726
|
}
|
|
15492
15727
|
function flushPending() {
|
|
@@ -18113,64 +18348,6 @@ function resolvePageType(key) {
|
|
|
18113
18348
|
return PAGE_TYPES[TYPE_ALIASES[key] ?? key];
|
|
18114
18349
|
}
|
|
18115
18350
|
|
|
18116
|
-
//#endregion
|
|
18117
|
-
//#region packages/provider/src/DocUtils.ts
|
|
18118
|
-
/**
|
|
18119
|
-
* Shared utilities for the DocumentManager ORM layer.
|
|
18120
|
-
*
|
|
18121
|
-
* These functions were previously duplicated across `mcp/src/utils.ts`,
|
|
18122
|
-
* `mcp/src/server.ts`, `cli/src/connection.ts`, and `mcp/src/tools/tree.ts`.
|
|
18123
|
-
*/
|
|
18124
|
-
/**
|
|
18125
|
-
* Wait for a provider's `synced` event with a timeout.
|
|
18126
|
-
* Resolves immediately if the provider is already synced.
|
|
18127
|
-
*/
|
|
18128
|
-
function waitForSync(provider, timeoutMs = 15e3) {
|
|
18129
|
-
if (provider.isSynced) return Promise.resolve();
|
|
18130
|
-
return new Promise((resolve, reject) => {
|
|
18131
|
-
const timer = setTimeout(() => {
|
|
18132
|
-
provider.off("synced", handler);
|
|
18133
|
-
reject(/* @__PURE__ */ new Error(`Sync timed out after ${timeoutMs}ms`));
|
|
18134
|
-
}, timeoutMs);
|
|
18135
|
-
function handler() {
|
|
18136
|
-
clearTimeout(timer);
|
|
18137
|
-
resolve();
|
|
18138
|
-
}
|
|
18139
|
-
provider.on("synced", handler);
|
|
18140
|
-
});
|
|
18141
|
-
}
|
|
18142
|
-
/**
|
|
18143
|
-
* Wrap a promise with a timeout.
|
|
18144
|
-
*/
|
|
18145
|
-
function withTimeout(promise, timeoutMs, message) {
|
|
18146
|
-
return new Promise((resolve, reject) => {
|
|
18147
|
-
const timer = setTimeout(() => reject(new Error(message ?? `Operation timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
18148
|
-
promise.then((val) => {
|
|
18149
|
-
clearTimeout(timer);
|
|
18150
|
-
resolve(val);
|
|
18151
|
-
}, (err) => {
|
|
18152
|
-
clearTimeout(timer);
|
|
18153
|
-
reject(err);
|
|
18154
|
-
});
|
|
18155
|
-
});
|
|
18156
|
-
}
|
|
18157
|
-
/**
|
|
18158
|
-
* Normalize a document ID so the hub/root doc ID is treated as the tree root
|
|
18159
|
-
* (null). This lets callers pass the hub doc_id from list_spaces as
|
|
18160
|
-
* parentId/rootId and get the expected root-level results instead of an empty
|
|
18161
|
-
* set.
|
|
18162
|
-
*/
|
|
18163
|
-
function normalizeRootId(id, rootDocId) {
|
|
18164
|
-
if (id == null) return null;
|
|
18165
|
-
return id === rootDocId ? null : id;
|
|
18166
|
-
}
|
|
18167
|
-
/**
|
|
18168
|
-
* Safely read a tree map value, converting Y.Map to plain object if needed.
|
|
18169
|
-
*/
|
|
18170
|
-
function toPlain(val) {
|
|
18171
|
-
return val instanceof Y.Map ? val.toJSON() : val;
|
|
18172
|
-
}
|
|
18173
|
-
|
|
18174
18351
|
//#endregion
|
|
18175
18352
|
//#region packages/provider/src/SchemaTypes.ts
|
|
18176
18353
|
/**
|
|
@@ -18209,9 +18386,112 @@ function projectTreeEntry(entry, expectedType) {
|
|
|
18209
18386
|
|
|
18210
18387
|
//#endregion
|
|
18211
18388
|
//#region packages/provider/src/TreeManager.ts
|
|
18389
|
+
/**
|
|
18390
|
+
* Stable total order over tree siblings: `order` ascending, then `id`
|
|
18391
|
+
* ascending as a deterministic tiebreak. The legacy scan sorted by
|
|
18392
|
+
* `order` alone and left ties to insertion/iteration order — a superset
|
|
18393
|
+
* change that makes cursor pagination well-defined.
|
|
18394
|
+
*/
|
|
18395
|
+
function cmpKey(oa, ia, ob, ib) {
|
|
18396
|
+
if (oa !== ob) return oa < ob ? -1 : 1;
|
|
18397
|
+
return ia < ib ? -1 : ia > ib ? 1 : 0;
|
|
18398
|
+
}
|
|
18399
|
+
function cmpEntry(a, b) {
|
|
18400
|
+
return cmpKey(a.order ?? 0, a.id, b.order ?? 0, b.id);
|
|
18401
|
+
}
|
|
18402
|
+
/** Opaque, dependency-free cursor over the (order,id) sibling order. */
|
|
18403
|
+
function encodeCursor(order, id) {
|
|
18404
|
+
return encodeURIComponent(JSON.stringify([order ?? 0, id]));
|
|
18405
|
+
}
|
|
18406
|
+
function decodeCursor(c) {
|
|
18407
|
+
try {
|
|
18408
|
+
const v = JSON.parse(decodeURIComponent(c));
|
|
18409
|
+
if (Array.isArray(v) && typeof v[0] === "number" && typeof v[1] === "string") return {
|
|
18410
|
+
order: v[0],
|
|
18411
|
+
id: v[1]
|
|
18412
|
+
};
|
|
18413
|
+
} catch {}
|
|
18414
|
+
return null;
|
|
18415
|
+
}
|
|
18212
18416
|
var TreeManager = class {
|
|
18213
18417
|
constructor(dm) {
|
|
18214
18418
|
this.dm = dm;
|
|
18419
|
+
this._idxMap = null;
|
|
18420
|
+
this._idxObserver = null;
|
|
18421
|
+
this._idxDirty = true;
|
|
18422
|
+
this._byId = /* @__PURE__ */ new Map();
|
|
18423
|
+
this._childrenByParent = /* @__PURE__ */ new Map();
|
|
18424
|
+
}
|
|
18425
|
+
/**
|
|
18426
|
+
* Ensure the index is enabled, bound to the current root doc's tree
|
|
18427
|
+
* map, and fresh. Returns `false` when the index is disabled or there
|
|
18428
|
+
* is no tree map yet — callers then use the legacy scan path.
|
|
18429
|
+
*/
|
|
18430
|
+
ensureIndex() {
|
|
18431
|
+
if (!this.dm.treeIndexEnabled) return false;
|
|
18432
|
+
const treeMap = this.dm.getTreeMap();
|
|
18433
|
+
if (!treeMap) {
|
|
18434
|
+
this.unbindIndex();
|
|
18435
|
+
return false;
|
|
18436
|
+
}
|
|
18437
|
+
if (treeMap !== this._idxMap) {
|
|
18438
|
+
this.unbindIndex();
|
|
18439
|
+
const obs = () => {
|
|
18440
|
+
this._idxDirty = true;
|
|
18441
|
+
};
|
|
18442
|
+
treeMap.observeDeep(obs);
|
|
18443
|
+
this._idxMap = treeMap;
|
|
18444
|
+
this._idxObserver = obs;
|
|
18445
|
+
this._idxDirty = true;
|
|
18446
|
+
}
|
|
18447
|
+
if (this._idxDirty) this.rebuildIndex(treeMap);
|
|
18448
|
+
return true;
|
|
18449
|
+
}
|
|
18450
|
+
unbindIndex() {
|
|
18451
|
+
if (this._idxMap && this._idxObserver) this._idxMap.unobserveDeep(this._idxObserver);
|
|
18452
|
+
this._idxMap = null;
|
|
18453
|
+
this._idxObserver = null;
|
|
18454
|
+
this._byId = /* @__PURE__ */ new Map();
|
|
18455
|
+
this._childrenByParent = /* @__PURE__ */ new Map();
|
|
18456
|
+
this._idxDirty = true;
|
|
18457
|
+
}
|
|
18458
|
+
rebuildIndex(treeMap) {
|
|
18459
|
+
const root = this.dm.rootDocId;
|
|
18460
|
+
const byId = /* @__PURE__ */ new Map();
|
|
18461
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
18462
|
+
treeMap.forEach((raw, id) => {
|
|
18463
|
+
const value = toPlain(raw);
|
|
18464
|
+
if (typeof value !== "object" || value === null) return;
|
|
18465
|
+
const entry = {
|
|
18466
|
+
id,
|
|
18467
|
+
label: value.label || "Untitled",
|
|
18468
|
+
parentId: normalizeRootId(value.parentId ?? null, root),
|
|
18469
|
+
order: value.order ?? 0,
|
|
18470
|
+
type: value.type,
|
|
18471
|
+
meta: value.meta,
|
|
18472
|
+
createdAt: value.createdAt,
|
|
18473
|
+
updatedAt: value.updatedAt
|
|
18474
|
+
};
|
|
18475
|
+
byId.set(id, entry);
|
|
18476
|
+
let bucket = childrenByParent.get(entry.parentId);
|
|
18477
|
+
if (!bucket) {
|
|
18478
|
+
bucket = [];
|
|
18479
|
+
childrenByParent.set(entry.parentId, bucket);
|
|
18480
|
+
}
|
|
18481
|
+
bucket.push(entry);
|
|
18482
|
+
});
|
|
18483
|
+
for (const bucket of childrenByParent.values()) bucket.sort(cmpEntry);
|
|
18484
|
+
this._byId = byId;
|
|
18485
|
+
this._childrenByParent = childrenByParent;
|
|
18486
|
+
this._idxDirty = false;
|
|
18487
|
+
}
|
|
18488
|
+
/**
|
|
18489
|
+
* Release the deep observer. Optional — the observer is auto-rebound
|
|
18490
|
+
* on space switch and becomes moot when the root Y.Doc is GC'd — but
|
|
18491
|
+
* available for consumers that want deterministic teardown.
|
|
18492
|
+
*/
|
|
18493
|
+
dispose() {
|
|
18494
|
+
this.unbindIndex();
|
|
18215
18495
|
}
|
|
18216
18496
|
/** Read all tree entries as plain objects. */
|
|
18217
18497
|
readEntries() {
|
|
@@ -18234,15 +18514,83 @@ var TreeManager = class {
|
|
|
18234
18514
|
});
|
|
18235
18515
|
return entries;
|
|
18236
18516
|
}
|
|
18517
|
+
/**
|
|
18518
|
+
* Like {@link readEntries} but with every entry's *stored* parentId
|
|
18519
|
+
* run through {@link normalizeRootId} (parentId === rootDocId → null),
|
|
18520
|
+
* so a cou-sh / orphan-rescue top-level doc (parentId === spaceRoot)
|
|
18521
|
+
* resolves to top-level identically to a provider-created one
|
|
18522
|
+
* (parentId: null). Without this, the raw `parentId === spaceRoot`
|
|
18523
|
+
* form never matches the normalized `null` query and such docs are
|
|
18524
|
+
* silently invisible cross-client. Mirrors the Rust provider's
|
|
18525
|
+
* `normalized_entries`. readEntries/get keep raw values for
|
|
18526
|
+
* round-trip consumers; only tree-walk reads use this.
|
|
18527
|
+
*/
|
|
18528
|
+
normalizedEntries() {
|
|
18529
|
+
if (this.ensureIndex()) return Array.from(this._byId.values());
|
|
18530
|
+
const root = this.dm.rootDocId;
|
|
18531
|
+
return this.readEntries().map((e) => ({
|
|
18532
|
+
...e,
|
|
18533
|
+
parentId: normalizeRootId(e.parentId, root)
|
|
18534
|
+
}));
|
|
18535
|
+
}
|
|
18237
18536
|
/** Get immediate children of a parent (sorted by order). */
|
|
18238
18537
|
childrenOf(parentId) {
|
|
18239
18538
|
const normalized = normalizeRootId(parentId, this.dm.rootDocId);
|
|
18240
|
-
|
|
18539
|
+
if (this.ensureIndex()) {
|
|
18540
|
+
const bucket = this._childrenByParent.get(normalized);
|
|
18541
|
+
return bucket ? bucket.slice() : [];
|
|
18542
|
+
}
|
|
18543
|
+
return this.normalizedEntries().filter((e) => e.parentId === normalized).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
18544
|
+
}
|
|
18545
|
+
/**
|
|
18546
|
+
* Paginated immediate children — the Path-1 surface for large fan-out
|
|
18547
|
+
* parents. Walks the same stable (order,id) sibling order as
|
|
18548
|
+
* {@link childrenOf}; `cursor` is opaque (round-trip `nextCursor`).
|
|
18549
|
+
* `limit` defaults to 100. A stale/garbage cursor restarts from the
|
|
18550
|
+
* head rather than throwing. Cursor stability is exact when the index
|
|
18551
|
+
* is enabled; on the legacy scan path siblings with equal `order`
|
|
18552
|
+
* may shift between calls.
|
|
18553
|
+
*/
|
|
18554
|
+
childrenOfPage(parentId, opts = {}) {
|
|
18555
|
+
const all = this.childrenOf(parentId);
|
|
18556
|
+
const limit = opts.limit != null && opts.limit > 0 ? Math.floor(opts.limit) : 100;
|
|
18557
|
+
let start = 0;
|
|
18558
|
+
if (opts.cursor) {
|
|
18559
|
+
const dec = decodeCursor(opts.cursor);
|
|
18560
|
+
if (dec) {
|
|
18561
|
+
const at = all.findIndex((e) => cmpKey(e.order ?? 0, e.id, dec.order, dec.id) > 0);
|
|
18562
|
+
start = at < 0 ? all.length : at;
|
|
18563
|
+
}
|
|
18564
|
+
}
|
|
18565
|
+
const entries = all.slice(start, start + limit);
|
|
18566
|
+
const last = entries[entries.length - 1];
|
|
18567
|
+
return {
|
|
18568
|
+
entries,
|
|
18569
|
+
nextCursor: last && start + limit < all.length ? encodeCursor(last.order ?? 0, last.id) : null
|
|
18570
|
+
};
|
|
18241
18571
|
}
|
|
18242
18572
|
/** Get all descendants recursively. */
|
|
18243
18573
|
descendantsOf(parentId) {
|
|
18244
18574
|
const normalized = normalizeRootId(parentId, this.dm.rootDocId);
|
|
18245
|
-
|
|
18575
|
+
if (this.ensureIndex()) {
|
|
18576
|
+
const result = [];
|
|
18577
|
+
const visited = /* @__PURE__ */ new Set();
|
|
18578
|
+
const walk = (pid) => {
|
|
18579
|
+
if (pid !== null) {
|
|
18580
|
+
if (visited.has(pid)) return;
|
|
18581
|
+
visited.add(pid);
|
|
18582
|
+
}
|
|
18583
|
+
const bucket = this._childrenByParent.get(pid);
|
|
18584
|
+
if (!bucket) return;
|
|
18585
|
+
for (const child of bucket) {
|
|
18586
|
+
result.push(child);
|
|
18587
|
+
walk(child.id);
|
|
18588
|
+
}
|
|
18589
|
+
};
|
|
18590
|
+
walk(normalized);
|
|
18591
|
+
return result;
|
|
18592
|
+
}
|
|
18593
|
+
const entries = this.normalizedEntries();
|
|
18246
18594
|
const result = [];
|
|
18247
18595
|
const visited = /* @__PURE__ */ new Set();
|
|
18248
18596
|
const collect = (pid) => {
|
|
@@ -18259,9 +18607,25 @@ var TreeManager = class {
|
|
|
18259
18607
|
/** Build nested tree JSON. */
|
|
18260
18608
|
buildTree(rootId, maxDepth = 3) {
|
|
18261
18609
|
const normalized = normalizeRootId(rootId ?? null, this.dm.rootDocId);
|
|
18262
|
-
|
|
18610
|
+
if (this.ensureIndex()) return this._buildTreeIndexed(normalized, maxDepth, 0, /* @__PURE__ */ new Set());
|
|
18611
|
+
const entries = this.normalizedEntries();
|
|
18263
18612
|
return this._buildTree(entries, normalized, maxDepth, 0, /* @__PURE__ */ new Set());
|
|
18264
18613
|
}
|
|
18614
|
+
_buildTreeIndexed(rootId, maxDepth, currentDepth, visited) {
|
|
18615
|
+
if (maxDepth >= 0 && currentDepth >= maxDepth) return [];
|
|
18616
|
+
return (this._childrenByParent.get(rootId) ?? []).filter((e) => !visited.has(e.id)).map((entry) => {
|
|
18617
|
+
const next = new Set(visited);
|
|
18618
|
+
next.add(entry.id);
|
|
18619
|
+
return {
|
|
18620
|
+
id: entry.id,
|
|
18621
|
+
label: entry.label,
|
|
18622
|
+
type: entry.type,
|
|
18623
|
+
meta: entry.meta,
|
|
18624
|
+
order: entry.order,
|
|
18625
|
+
children: this._buildTreeIndexed(entry.id, maxDepth, currentDepth + 1, next)
|
|
18626
|
+
};
|
|
18627
|
+
});
|
|
18628
|
+
}
|
|
18265
18629
|
_buildTree(entries, rootId, maxDepth, currentDepth, visited) {
|
|
18266
18630
|
if (maxDepth >= 0 && currentDepth >= maxDepth) return [];
|
|
18267
18631
|
return entries.filter((e) => e.parentId === rootId).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).filter((e) => !visited.has(e.id)).map((entry) => {
|
|
@@ -18312,7 +18676,7 @@ var TreeManager = class {
|
|
|
18312
18676
|
}
|
|
18313
18677
|
/** Search by label (case-insensitive substring match). */
|
|
18314
18678
|
find(query, rootId) {
|
|
18315
|
-
const entries = this.
|
|
18679
|
+
const entries = this.normalizedEntries();
|
|
18316
18680
|
const lowerQuery = query.toLowerCase();
|
|
18317
18681
|
const normalized = normalizeRootId(rootId ?? null, this.dm.rootDocId);
|
|
18318
18682
|
const matches = (normalized ? this.descendantsOf(normalized) : entries).filter((e) => e.label.toLowerCase().includes(lowerQuery));
|
|
@@ -18346,7 +18710,7 @@ var TreeManager = class {
|
|
|
18346
18710
|
const normalizedParent = normalizeRootId(opts.parentId ?? null, this.dm.rootDocId);
|
|
18347
18711
|
const now = Date.now();
|
|
18348
18712
|
rootDoc.transact(() => {
|
|
18349
|
-
treeMap.set(id, {
|
|
18713
|
+
treeMap.set(id, makeEntryMap({
|
|
18350
18714
|
label: opts.label,
|
|
18351
18715
|
parentId: normalizedParent,
|
|
18352
18716
|
order: now,
|
|
@@ -18354,7 +18718,7 @@ var TreeManager = class {
|
|
|
18354
18718
|
meta: opts.meta,
|
|
18355
18719
|
createdAt: now,
|
|
18356
18720
|
updatedAt: now
|
|
18357
|
-
});
|
|
18721
|
+
}));
|
|
18358
18722
|
});
|
|
18359
18723
|
return {
|
|
18360
18724
|
id,
|
|
@@ -18393,12 +18757,9 @@ var TreeManager = class {
|
|
|
18393
18757
|
const treeMap = this.dm.getTreeMap();
|
|
18394
18758
|
const rootDoc = this.dm.rootDocument;
|
|
18395
18759
|
if (!treeMap || !rootDoc) throw new Error("Not connected");
|
|
18396
|
-
|
|
18397
|
-
if (!raw) throw new Error(`Document ${docId} not found`);
|
|
18398
|
-
const entry = toPlain(raw);
|
|
18760
|
+
if (!treeMap.get(docId)) throw new Error(`Document ${docId} not found`);
|
|
18399
18761
|
rootDoc.transact(() => {
|
|
18400
|
-
treeMap
|
|
18401
|
-
...entry,
|
|
18762
|
+
patchEntry(treeMap, docId, {
|
|
18402
18763
|
label,
|
|
18403
18764
|
updatedAt: Date.now()
|
|
18404
18765
|
});
|
|
@@ -18409,12 +18770,9 @@ var TreeManager = class {
|
|
|
18409
18770
|
const treeMap = this.dm.getTreeMap();
|
|
18410
18771
|
const rootDoc = this.dm.rootDocument;
|
|
18411
18772
|
if (!treeMap || !rootDoc) throw new Error("Not connected");
|
|
18412
|
-
|
|
18413
|
-
if (!raw) throw new Error(`Document ${docId} not found`);
|
|
18414
|
-
const entry = toPlain(raw);
|
|
18773
|
+
if (!treeMap.get(docId)) throw new Error(`Document ${docId} not found`);
|
|
18415
18774
|
rootDoc.transact(() => {
|
|
18416
|
-
treeMap
|
|
18417
|
-
...entry,
|
|
18775
|
+
patchEntry(treeMap, docId, {
|
|
18418
18776
|
parentId: normalizeRootId(newParentId ?? null, this.dm.rootDocId),
|
|
18419
18777
|
order: order ?? Date.now(),
|
|
18420
18778
|
updatedAt: Date.now()
|
|
@@ -18426,12 +18784,9 @@ var TreeManager = class {
|
|
|
18426
18784
|
const treeMap = this.dm.getTreeMap();
|
|
18427
18785
|
const rootDoc = this.dm.rootDocument;
|
|
18428
18786
|
if (!treeMap || !rootDoc) throw new Error("Not connected");
|
|
18429
|
-
|
|
18430
|
-
if (!raw) throw new Error(`Document ${docId} not found`);
|
|
18431
|
-
const entry = toPlain(raw);
|
|
18787
|
+
if (!treeMap.get(docId)) throw new Error(`Document ${docId} not found`);
|
|
18432
18788
|
rootDoc.transact(() => {
|
|
18433
|
-
treeMap
|
|
18434
|
-
...entry,
|
|
18789
|
+
patchEntry(treeMap, docId, {
|
|
18435
18790
|
type,
|
|
18436
18791
|
updatedAt: Date.now()
|
|
18437
18792
|
});
|
|
@@ -18446,10 +18801,12 @@ var TreeManager = class {
|
|
|
18446
18801
|
const trashMap = this.dm.getTrashMap();
|
|
18447
18802
|
const rootDoc = this.dm.rootDocument;
|
|
18448
18803
|
if (!treeMap || !trashMap || !rootDoc) throw new Error("Not connected");
|
|
18449
|
-
const entries = this.readEntries();
|
|
18450
|
-
const toDelete = [docId, ...this._descendantIds(entries, docId)];
|
|
18451
18804
|
const now = Date.now();
|
|
18805
|
+
let deletedCount = 0;
|
|
18452
18806
|
rootDoc.transact(() => {
|
|
18807
|
+
const entries = this.readEntries();
|
|
18808
|
+
const toDelete = [docId, ...this._descendantIds(entries, docId)];
|
|
18809
|
+
deletedCount = toDelete.length;
|
|
18453
18810
|
for (const nid of toDelete) {
|
|
18454
18811
|
const raw = treeMap.get(nid);
|
|
18455
18812
|
if (!raw) continue;
|
|
@@ -18465,7 +18822,7 @@ var TreeManager = class {
|
|
|
18465
18822
|
treeMap.delete(nid);
|
|
18466
18823
|
}
|
|
18467
18824
|
});
|
|
18468
|
-
return
|
|
18825
|
+
return deletedCount;
|
|
18469
18826
|
}
|
|
18470
18827
|
/** Duplicate a document (shallow clone). Returns the new entry. */
|
|
18471
18828
|
duplicate(docId) {
|
|
@@ -18477,13 +18834,13 @@ var TreeManager = class {
|
|
|
18477
18834
|
const newId = crypto.randomUUID();
|
|
18478
18835
|
const now = Date.now();
|
|
18479
18836
|
const newLabel = (entry.label || "Untitled") + " (copy)";
|
|
18480
|
-
treeMap.set(newId, {
|
|
18837
|
+
treeMap.set(newId, makeEntryMap({
|
|
18481
18838
|
...entry,
|
|
18482
18839
|
label: newLabel,
|
|
18483
18840
|
order: now,
|
|
18484
18841
|
createdAt: now,
|
|
18485
18842
|
updatedAt: now
|
|
18486
|
-
});
|
|
18843
|
+
}));
|
|
18487
18844
|
return {
|
|
18488
18845
|
id: newId,
|
|
18489
18846
|
label: newLabel,
|
|
@@ -18501,21 +18858,37 @@ var TreeManager = class {
|
|
|
18501
18858
|
const trashMap = this.dm.getTrashMap();
|
|
18502
18859
|
const rootDoc = this.dm.rootDocument;
|
|
18503
18860
|
if (!treeMap || !trashMap || !rootDoc) throw new Error("Not connected");
|
|
18504
|
-
|
|
18505
|
-
if (!raw) throw new Error(`Document ${docId} not found in trash`);
|
|
18506
|
-
const entry = toPlain(raw);
|
|
18861
|
+
if (!trashMap.get(docId)) throw new Error(`Document ${docId} not found in trash`);
|
|
18507
18862
|
const now = Date.now();
|
|
18508
18863
|
rootDoc.transact(() => {
|
|
18509
|
-
|
|
18510
|
-
|
|
18511
|
-
|
|
18512
|
-
|
|
18513
|
-
type: entry.type,
|
|
18514
|
-
meta: entry.meta,
|
|
18515
|
-
createdAt: entry.createdAt ?? now,
|
|
18516
|
-
updatedAt: now
|
|
18864
|
+
const trashed = /* @__PURE__ */ new Map();
|
|
18865
|
+
trashMap.forEach((raw, id) => {
|
|
18866
|
+
const v = toPlain(raw);
|
|
18867
|
+
if (typeof v === "object" && v !== null) trashed.set(id, v);
|
|
18517
18868
|
});
|
|
18518
|
-
|
|
18869
|
+
const toRestore = [];
|
|
18870
|
+
const visited = /* @__PURE__ */ new Set();
|
|
18871
|
+
const collect = (id) => {
|
|
18872
|
+
if (visited.has(id)) return;
|
|
18873
|
+
visited.add(id);
|
|
18874
|
+
if (!trashed.has(id)) return;
|
|
18875
|
+
toRestore.push(id);
|
|
18876
|
+
for (const [cid, v] of trashed) if ((v.parentId ?? null) === id) collect(cid);
|
|
18877
|
+
};
|
|
18878
|
+
collect(docId);
|
|
18879
|
+
for (const id of toRestore) {
|
|
18880
|
+
const entry = trashed.get(id);
|
|
18881
|
+
treeMap.set(id, makeEntryMap({
|
|
18882
|
+
label: entry.label || "Untitled",
|
|
18883
|
+
parentId: entry.parentId ?? null,
|
|
18884
|
+
order: entry.order ?? now,
|
|
18885
|
+
type: entry.type,
|
|
18886
|
+
meta: entry.meta,
|
|
18887
|
+
createdAt: entry.createdAt ?? now,
|
|
18888
|
+
updatedAt: now
|
|
18889
|
+
}));
|
|
18890
|
+
trashMap.delete(id);
|
|
18891
|
+
}
|
|
18519
18892
|
});
|
|
18520
18893
|
}
|
|
18521
18894
|
/** List trashed documents. */
|
|
@@ -19886,9 +20259,9 @@ var ContentManager = class {
|
|
|
19886
20259
|
* body, tree metadata, and immediate children.
|
|
19887
20260
|
*/
|
|
19888
20261
|
async read(docId) {
|
|
19889
|
-
const
|
|
20262
|
+
const fragment = (await this.dm.getChildProvider(docId)).document.getXmlFragment("default");
|
|
19890
20263
|
const treeMap = this.dm.getTreeMap();
|
|
19891
|
-
let label =
|
|
20264
|
+
let label = "Untitled";
|
|
19892
20265
|
let type;
|
|
19893
20266
|
let meta;
|
|
19894
20267
|
const childrenWithOrder = [];
|
|
@@ -19896,7 +20269,7 @@ var ContentManager = class {
|
|
|
19896
20269
|
const raw = treeMap.get(docId);
|
|
19897
20270
|
if (raw) {
|
|
19898
20271
|
const entry = toPlain(raw);
|
|
19899
|
-
label = entry.label ||
|
|
20272
|
+
label = entry.label || label;
|
|
19900
20273
|
type = entry.type;
|
|
19901
20274
|
meta = entry.meta;
|
|
19902
20275
|
}
|
|
@@ -19918,11 +20291,12 @@ var ContentManager = class {
|
|
|
19918
20291
|
type,
|
|
19919
20292
|
meta
|
|
19920
20293
|
}));
|
|
20294
|
+
const markdown = yjsToMarkdown(fragment, label, meta, type);
|
|
19921
20295
|
return {
|
|
19922
20296
|
label,
|
|
19923
20297
|
type,
|
|
19924
20298
|
meta,
|
|
19925
|
-
title,
|
|
20299
|
+
title: label,
|
|
19926
20300
|
markdown,
|
|
19927
20301
|
children
|
|
19928
20302
|
};
|
|
@@ -19944,16 +20318,14 @@ var ContentManager = class {
|
|
|
19944
20318
|
if (treeMap && rootDoc) {
|
|
19945
20319
|
const entry = treeMap.get(docId);
|
|
19946
20320
|
if (entry) rootDoc.transact(() => {
|
|
19947
|
-
const
|
|
19948
|
-
|
|
19949
|
-
|
|
19950
|
-
|
|
19951
|
-
|
|
19952
|
-
if (Object.keys(meta).length > 0) updates.meta = {
|
|
19953
|
-
...entry.meta ?? {},
|
|
20321
|
+
const cur = toPlain(entry);
|
|
20322
|
+
const patch = { updatedAt: Date.now() };
|
|
20323
|
+
if (title) patch.label = title;
|
|
20324
|
+
if (Object.keys(meta).length > 0) patch.meta = {
|
|
20325
|
+
...cur.meta ?? {},
|
|
19954
20326
|
...meta
|
|
19955
20327
|
};
|
|
19956
|
-
treeMap
|
|
20328
|
+
patchEntry(treeMap, docId, patch);
|
|
19957
20329
|
});
|
|
19958
20330
|
}
|
|
19959
20331
|
}
|
|
@@ -20072,8 +20444,7 @@ var MetaManager = class {
|
|
|
20072
20444
|
...meta
|
|
20073
20445
|
};
|
|
20074
20446
|
this.validateOrThrow(docId, entry, mergedMeta);
|
|
20075
|
-
treeMap
|
|
20076
|
-
...entry,
|
|
20447
|
+
patchEntry(treeMap, docId, {
|
|
20077
20448
|
meta: mergedMeta,
|
|
20078
20449
|
updatedAt: Date.now()
|
|
20079
20450
|
});
|
|
@@ -20092,8 +20463,7 @@ var MetaManager = class {
|
|
|
20092
20463
|
if (!raw) throw new Error(`Document ${docId} not found`);
|
|
20093
20464
|
const entry = toPlain(raw);
|
|
20094
20465
|
this.validateOrThrow(docId, entry, meta);
|
|
20095
|
-
treeMap
|
|
20096
|
-
...entry,
|
|
20466
|
+
patchEntry(treeMap, docId, {
|
|
20097
20467
|
meta,
|
|
20098
20468
|
updatedAt: Date.now()
|
|
20099
20469
|
});
|
|
@@ -20110,8 +20480,7 @@ var MetaManager = class {
|
|
|
20110
20480
|
const updated = { ...entry.meta ?? {} };
|
|
20111
20481
|
for (const key of keys) delete updated[key];
|
|
20112
20482
|
this.validateOrThrow(docId, entry, updated);
|
|
20113
|
-
treeMap
|
|
20114
|
-
...entry,
|
|
20483
|
+
patchEntry(treeMap, docId, {
|
|
20115
20484
|
meta: updated,
|
|
20116
20485
|
updatedAt: Date.now()
|
|
20117
20486
|
});
|
|
@@ -20218,6 +20587,13 @@ var DocumentManager = class {
|
|
|
20218
20587
|
get rootDocId() {
|
|
20219
20588
|
return this._rootDocId;
|
|
20220
20589
|
}
|
|
20590
|
+
/**
|
|
20591
|
+
* Whether the TreeManager in-memory index is enabled (Path-1 prototype).
|
|
20592
|
+
* Off by default — see {@link DocumentManagerConfig.treeIndex}.
|
|
20593
|
+
*/
|
|
20594
|
+
get treeIndexEnabled() {
|
|
20595
|
+
return this._config.treeIndex ?? false;
|
|
20596
|
+
}
|
|
20221
20597
|
get rootDocument() {
|
|
20222
20598
|
return this._rootDoc;
|
|
20223
20599
|
}
|
|
@@ -20347,5 +20723,5 @@ var DocumentManager = class {
|
|
|
20347
20723
|
};
|
|
20348
20724
|
|
|
20349
20725
|
//#endregion
|
|
20350
|
-
export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ChannelKeyResolver, ChatClient, ConnectionTimeout, ContentManager, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DevicePairingChannel, DeviceRegistrationService, DocKeyManager, DocumentCache, DocumentManager, E2EAbracadabraProvider, E2EEChannel, E2EOfflineStore, EncryptedChatClient, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, Forbidden, GEO_TYPE_META_SCHEMAS, HocuspocusProvider, HocuspocusProviderWebsocket, IdentityDocProvider, KEY_EXCHANGE_CHANNEL, Kind, LocalStorageDeviceSessionStorage, ManualSignaling, MessageTooBig, MessageType, MetaManager, MetaValidationError, NotificationsClient, OfflineStore, PAGE_TYPES, PeerConnection, QUERY_PREFIX, QueryClient, QueryError, RPC_PREFIX, ResetConnection, RpcClient, RpcError, SERVER_ROOT_ID, SearchIndex, SignalingSocket, SubdocMessage, TYPE_ALIASES, TokenManager, TreeManager, TypedDocTypeMismatchError, Unauthorized, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, wordlist as bip39Wordlist, buildBlockquoteElement, buildBlocksFromMarkdown, buildBulletListElement, buildCodeBlockElement, buildHeadingElement, buildHorizontalRuleElement, buildOrderedListElement, buildParagraphElement, buildTaskListElement, decryptChatContent, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptChatContent, encryptField, filenameToLabel, foldRecords, generateMnemonic, isEncryptedContent, makeEncryptedYMap, makeEncryptedYText, mnemonicToEd25519Seed, mnemonicToKeyPair, normalizeRootId, parseFrontmatter, populateYDocFromMarkdown, readAuthMessage, readBlocksFromFragment, recordFromYAny, resolvePageType, toPlain, unwrapSeed, validateMnemonic, waitForSync, withTimeout, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest, yjsToMarkdown };
|
|
20726
|
+
export { AbracadabraBaseProvider, AbracadabraClient, AbracadabraProvider, AbracadabraWS, AbracadabraWebRTC, AuthMessageType, AwarenessError, BackgroundSyncManager, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ChannelKeyResolver, ChatClient, ConnectionTimeout, ContentManager, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, DevicePairingChannel, DeviceRegistrationService, DocKeyManager, DocumentCache, DocumentManager, E2EAbracadabraProvider, E2EEChannel, E2EOfflineStore, EncryptedChatClient, EncryptedYMap, EncryptedYText, FileBlobStore, FileTransferChannel, FileTransferHandle, Forbidden, GEO_TYPE_META_SCHEMAS, HocuspocusProvider, HocuspocusProviderWebsocket, IdentityDocProvider, KEY_EXCHANGE_CHANNEL, Kind, LocalStorageDeviceSessionStorage, ManualSignaling, MessageTooBig, MessageType, MetaManager, MetaValidationError, NotificationsClient, OfflineStore, PAGE_TYPES, PeerConnection, QUERY_PREFIX, QueryClient, QueryError, RPC_PREFIX, ResetConnection, RpcClient, RpcError, SERVER_ROOT_ID, SearchIndex, SignalingSocket, SubdocMessage, TYPE_ALIASES, TokenManager, TreeManager, TypedDocTypeMismatchError, Unauthorized, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, wordlist as bip39Wordlist, buildBlockquoteElement, buildBlocksFromMarkdown, buildBulletListElement, buildCodeBlockElement, buildHeadingElement, buildHorizontalRuleElement, buildOrderedListElement, buildParagraphElement, buildTaskListElement, decryptChatContent, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptChatContent, encryptField, filenameToLabel, foldRecords, generateMnemonic, isEncryptedContent, makeEncryptedYMap, makeEncryptedYText, makeEntryMap, mnemonicToEd25519Seed, mnemonicToKeyPair, normalizeRootId, parseFrontmatter, patchEntry, populateYDocFromMarkdown, readAuthMessage, readBlocksFromFragment, recordFromYAny, resolvePageType, toPlain, unwrapSeed, validateMnemonic, waitForSync, withTimeout, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest, yjsToMarkdown };
|
|
20351
20727
|
//# sourceMappingURL=abracadabra-provider.esm.js.map
|