@abraca/dabra 2.0.10 → 2.4.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/dist/abracadabra-provider.cjs +538 -8
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +532 -9
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +432 -1
- package/package.json +8 -2
- package/src/AbracadabraBaseProvider.ts +28 -0
- package/src/AbracadabraClient.ts +32 -0
- package/src/BackgroundSyncManager.ts +34 -1
- package/src/DocumentManager.ts +70 -0
- package/src/MetaManager.ts +98 -1
- package/src/QueryClient.ts +209 -0
- package/src/SchemaTypes.ts +179 -0
- package/src/TokenManager.ts +329 -0
- package/src/TreeManager.ts +27 -0
- package/src/index.ts +32 -2
|
@@ -2185,6 +2185,119 @@ function serializeTarget(t) {
|
|
|
2185
2185
|
}
|
|
2186
2186
|
}
|
|
2187
2187
|
|
|
2188
|
+
//#endregion
|
|
2189
|
+
//#region packages/provider/src/QueryClient.ts
|
|
2190
|
+
/**
|
|
2191
|
+
* V2 query layer — live subscription client.
|
|
2192
|
+
*
|
|
2193
|
+
* Mirrors the server in `crates/abracadabra/src/query_stream.rs`. Frames
|
|
2194
|
+
* travel as `MSG_STATELESS` payloads with the literal `query:v1:` prefix
|
|
2195
|
+
* followed by a JSON envelope.
|
|
2196
|
+
*
|
|
2197
|
+
* Each call to `subscribeQuery(...)` opens a long-lived subscription:
|
|
2198
|
+
* 1. The client sends `req` with a fresh correlation id.
|
|
2199
|
+
* 2. The server replies `ack` with an initial snapshot
|
|
2200
|
+
* (`DocumentMeta[]`).
|
|
2201
|
+
* 3. The server emits `delta` frames as the doc-tree projection
|
|
2202
|
+
* drifts under the predicate.
|
|
2203
|
+
* 4. The client sends `cancel` to tear it down (or relies on the
|
|
2204
|
+
* WebSocket close path to free server state).
|
|
2205
|
+
*
|
|
2206
|
+
* The shape of the `query` field mirrors the REST `POST /docs/query`
|
|
2207
|
+
* body — `type`, `parentId`, `labelContains`, `where`, `limit`.
|
|
2208
|
+
*/
|
|
2209
|
+
const QUERY_PREFIX = "query:v1:";
|
|
2210
|
+
var QueryError = class extends Error {
|
|
2211
|
+
constructor(code, message) {
|
|
2212
|
+
super(message);
|
|
2213
|
+
this.name = "QueryError";
|
|
2214
|
+
this.code = code;
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
var QueryClient = class extends EventEmitter {
|
|
2218
|
+
constructor(transport) {
|
|
2219
|
+
super();
|
|
2220
|
+
this.subs = /* @__PURE__ */ new Map();
|
|
2221
|
+
this.idCounter = 0;
|
|
2222
|
+
this.transport = transport;
|
|
2223
|
+
this.onStatelessBound = (data) => this.receive(data.payload);
|
|
2224
|
+
this.transport.on("stateless", this.onStatelessBound);
|
|
2225
|
+
}
|
|
2226
|
+
destroy() {
|
|
2227
|
+
for (const sub of this.subs.values()) if (!sub.cancelled) {
|
|
2228
|
+
this.send({
|
|
2229
|
+
kind: "cancel",
|
|
2230
|
+
id: sub.id
|
|
2231
|
+
});
|
|
2232
|
+
sub.cancelled = true;
|
|
2233
|
+
}
|
|
2234
|
+
this.subs.clear();
|
|
2235
|
+
this.transport.off("stateless", this.onStatelessBound);
|
|
2236
|
+
}
|
|
2237
|
+
subscribeQuery(spec, handlers = {}) {
|
|
2238
|
+
const id = this.nextId();
|
|
2239
|
+
const sub = {
|
|
2240
|
+
id,
|
|
2241
|
+
handlers,
|
|
2242
|
+
cancelled: false
|
|
2243
|
+
};
|
|
2244
|
+
this.subs.set(id, sub);
|
|
2245
|
+
this.send({
|
|
2246
|
+
kind: "req",
|
|
2247
|
+
id,
|
|
2248
|
+
query: spec
|
|
2249
|
+
});
|
|
2250
|
+
const self = this;
|
|
2251
|
+
return {
|
|
2252
|
+
id,
|
|
2253
|
+
cancel() {
|
|
2254
|
+
if (sub.cancelled) return;
|
|
2255
|
+
sub.cancelled = true;
|
|
2256
|
+
self.subs.delete(id);
|
|
2257
|
+
self.send({
|
|
2258
|
+
kind: "cancel",
|
|
2259
|
+
id
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
nextId() {
|
|
2265
|
+
this.idCounter += 1;
|
|
2266
|
+
return `q-${Date.now()}-${this.idCounter}`;
|
|
2267
|
+
}
|
|
2268
|
+
send(frame) {
|
|
2269
|
+
this.transport.sendStateless(`${QUERY_PREFIX}${JSON.stringify(frame)}`);
|
|
2270
|
+
}
|
|
2271
|
+
receive(payload) {
|
|
2272
|
+
if (!payload.startsWith(QUERY_PREFIX)) return;
|
|
2273
|
+
const rest = payload.slice(9);
|
|
2274
|
+
let frame;
|
|
2275
|
+
try {
|
|
2276
|
+
frame = JSON.parse(rest);
|
|
2277
|
+
} catch {
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
const sub = this.subs.get(frame.id);
|
|
2281
|
+
if (!sub || sub.cancelled) return;
|
|
2282
|
+
switch (frame.kind) {
|
|
2283
|
+
case "ack":
|
|
2284
|
+
sub.handlers.onSnapshot?.(frame.initial ?? []);
|
|
2285
|
+
break;
|
|
2286
|
+
case "delta":
|
|
2287
|
+
sub.handlers.onDelta?.({
|
|
2288
|
+
added: frame.added ?? [],
|
|
2289
|
+
removed: frame.removed ?? []
|
|
2290
|
+
});
|
|
2291
|
+
break;
|
|
2292
|
+
case "err":
|
|
2293
|
+
if (frame.error) sub.handlers.onError?.(new QueryError(frame.error.code, frame.error.message));
|
|
2294
|
+
this.subs.delete(frame.id);
|
|
2295
|
+
break;
|
|
2296
|
+
default: break;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
};
|
|
2300
|
+
|
|
2188
2301
|
//#endregion
|
|
2189
2302
|
//#region packages/provider/src/OutgoingMessages/SyncStepOneMessage.ts
|
|
2190
2303
|
var SyncStepOneMessage = class extends OutgoingMessage {
|
|
@@ -2235,6 +2348,18 @@ var AbracadabraBaseProvider = class extends EventEmitter {
|
|
|
2235
2348
|
if (!this._rpc) this._rpc = new RpcClient(this);
|
|
2236
2349
|
return this._rpc;
|
|
2237
2350
|
}
|
|
2351
|
+
getQueryClient() {
|
|
2352
|
+
if (!this._queryClient) this._queryClient = new QueryClient(this);
|
|
2353
|
+
return this._queryClient;
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Open a live query subscription. Mirrors the v1 REST shape
|
|
2357
|
+
* (`POST /docs/query`) but pushes deltas as the server-side
|
|
2358
|
+
* projection drifts under the predicate.
|
|
2359
|
+
*/
|
|
2360
|
+
subscribeQuery(spec, handlers = {}) {
|
|
2361
|
+
return this.getQueryClient().subscribeQuery(spec, handlers);
|
|
2362
|
+
}
|
|
2238
2363
|
constructor(configuration) {
|
|
2239
2364
|
super();
|
|
2240
2365
|
this.configuration = {
|
|
@@ -2750,7 +2875,7 @@ var SubdocMessage = class extends OutgoingMessage {
|
|
|
2750
2875
|
//#endregion
|
|
2751
2876
|
//#region packages/provider/src/AbracadabraProvider.ts
|
|
2752
2877
|
/** Validate that a string is a UUID acceptable by the server's DocId parser. */
|
|
2753
|
-
function isValidDocId(id) {
|
|
2878
|
+
function isValidDocId$1(id) {
|
|
2754
2879
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
|
2755
2880
|
}
|
|
2756
2881
|
/**
|
|
@@ -2959,7 +3084,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
2959
3084
|
*/
|
|
2960
3085
|
handleYSubdocsChange({ added, removed }) {
|
|
2961
3086
|
for (const subdoc of added) {
|
|
2962
|
-
if (!isValidDocId(subdoc.guid)) continue;
|
|
3087
|
+
if (!isValidDocId$1(subdoc.guid)) continue;
|
|
2963
3088
|
this.registerSubdoc(subdoc);
|
|
2964
3089
|
}
|
|
2965
3090
|
for (const subdoc of removed) this.unloadChild(subdoc.guid);
|
|
@@ -3003,7 +3128,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
|
|
|
3003
3128
|
* explicit `unloadChild(id)` once they're done.
|
|
3004
3129
|
*/
|
|
3005
3130
|
loadChild(childId, options = {}) {
|
|
3006
|
-
if (!isValidDocId(childId)) return Promise.reject(/* @__PURE__ */ new Error(`loadChild: "${childId}" is not a valid document ID (must be a UUID). If this node was created with an older version of the app, delete it and recreate it.`));
|
|
3131
|
+
if (!isValidDocId$1(childId)) return Promise.reject(/* @__PURE__ */ new Error(`loadChild: "${childId}" is not a valid document ID (must be a UUID). If this node was created with an older version of the app, delete it and recreate it.`));
|
|
3007
3132
|
if (childId === this.configuration.name) return Promise.resolve(this);
|
|
3008
3133
|
const evictable = options.evictable !== false;
|
|
3009
3134
|
if (this.childProviders.has(childId)) {
|
|
@@ -10585,6 +10710,27 @@ var AbracadabraClient = class {
|
|
|
10585
10710
|
return (await this.request("GET", `/docs/search?${params.toString()}`)).results;
|
|
10586
10711
|
}
|
|
10587
10712
|
/**
|
|
10713
|
+
* Query for documents matching a structural predicate. v1 of the
|
|
10714
|
+
* indexed-query layer described in
|
|
10715
|
+
* `ARCHITECTURE/20-kickoff-phase3-queries.md`.
|
|
10716
|
+
*
|
|
10717
|
+
* The wire shape accepts a `where` field so v2 (meta-predicate
|
|
10718
|
+
* filtering on top of the `doc_meta_index` projection) can light up
|
|
10719
|
+
* without a compatibility break. Sending a non-empty `where` today
|
|
10720
|
+
* returns a 400 — callers should leave it absent.
|
|
10721
|
+
*
|
|
10722
|
+
* Returns docs the requester can read at viewer-or-above, filtered
|
|
10723
|
+
* through the cascading permission resolver server-side.
|
|
10724
|
+
*/
|
|
10725
|
+
async queryDocs(opts) {
|
|
10726
|
+
const body = {};
|
|
10727
|
+
if (opts.type != null) body.type = opts.type;
|
|
10728
|
+
if (opts.parentId != null) body.parent_id = opts.parentId;
|
|
10729
|
+
if (opts.labelContains != null) body.label_contains = opts.labelContains;
|
|
10730
|
+
if (opts.limit != null) body.limit = opts.limit;
|
|
10731
|
+
return (await this.request("POST", "/docs/query", { body })).documents;
|
|
10732
|
+
}
|
|
10733
|
+
/**
|
|
10588
10734
|
* List the direct children of a document, returning full metadata. Pass
|
|
10589
10735
|
* no argument to list the children of the server root — what the
|
|
10590
10736
|
* dashboard renders as the Spaces sidebar.
|
|
@@ -11120,6 +11266,234 @@ var AbracadabraClient = class {
|
|
|
11120
11266
|
}
|
|
11121
11267
|
};
|
|
11122
11268
|
|
|
11269
|
+
//#endregion
|
|
11270
|
+
//#region packages/provider/src/TokenManager.ts
|
|
11271
|
+
const DEFAULT_STORAGE_KEY = "abracadabra:device-session";
|
|
11272
|
+
/** localStorage-backed storage; safe in SSR/Node (no-ops without localStorage). */
|
|
11273
|
+
var LocalStorageDeviceSessionStorage = class {
|
|
11274
|
+
constructor(key = DEFAULT_STORAGE_KEY) {
|
|
11275
|
+
this.key = key;
|
|
11276
|
+
}
|
|
11277
|
+
load() {
|
|
11278
|
+
try {
|
|
11279
|
+
const raw = localStorage.getItem(this.key);
|
|
11280
|
+
if (!raw) return null;
|
|
11281
|
+
const parsed = JSON.parse(raw);
|
|
11282
|
+
if (typeof parsed?.sessionId === "string" && typeof parsed?.sessionToken === "string" && typeof parsed?.expiresAt === "number") return parsed;
|
|
11283
|
+
return null;
|
|
11284
|
+
} catch {
|
|
11285
|
+
return null;
|
|
11286
|
+
}
|
|
11287
|
+
}
|
|
11288
|
+
save(record) {
|
|
11289
|
+
try {
|
|
11290
|
+
localStorage.setItem(this.key, JSON.stringify(record));
|
|
11291
|
+
} catch {}
|
|
11292
|
+
}
|
|
11293
|
+
clear() {
|
|
11294
|
+
try {
|
|
11295
|
+
localStorage.removeItem(this.key);
|
|
11296
|
+
} catch {}
|
|
11297
|
+
}
|
|
11298
|
+
};
|
|
11299
|
+
/**
|
|
11300
|
+
* Manages the lifetime of the short-lived JWT against a long-lived device
|
|
11301
|
+
* session token.
|
|
11302
|
+
*
|
|
11303
|
+
* Flow:
|
|
11304
|
+
* - Bootstrap: if a stored device session exists, exchange it for a fresh
|
|
11305
|
+
* JWT immediately (`bootstrap()`). Otherwise the consumer must run a
|
|
11306
|
+
* full crypto-auth handshake and then call `attachSession(record)` once
|
|
11307
|
+
* the server returns a device-session token.
|
|
11308
|
+
* - Steady state: `getValidToken()` returns the JWT, refreshing if the
|
|
11309
|
+
* JWT is within `refreshLeadMs` of expiry. Concurrent callers share one
|
|
11310
|
+
* in-flight refresh.
|
|
11311
|
+
* - Background: a proactive timer fires `refreshLeadMs` before `exp`.
|
|
11312
|
+
* `visibilitychange` and `online` events trigger an opportunistic
|
|
11313
|
+
* refresh in case the timer was suppressed (hidden tab, sleeping
|
|
11314
|
+
* laptop). The combination is what restores the WS without a reload.
|
|
11315
|
+
* - Failure: refresh errors with status 401/403 mean the device session
|
|
11316
|
+
* itself is dead (revoked or 30 d expired). The record is cleared and
|
|
11317
|
+
* `session-expired` fires so the consumer can prompt re-auth. Other
|
|
11318
|
+
* errors (network) are transparent — the existing JWT is returned and
|
|
11319
|
+
* the caller's normal retry loop covers it.
|
|
11320
|
+
*/
|
|
11321
|
+
var TokenManager = class extends EventEmitter {
|
|
11322
|
+
constructor(options) {
|
|
11323
|
+
super();
|
|
11324
|
+
this.refreshing = null;
|
|
11325
|
+
this.proactiveTimer = null;
|
|
11326
|
+
this.resumeHandlersInstalled = false;
|
|
11327
|
+
this.boundOnResume = null;
|
|
11328
|
+
this.disposed = false;
|
|
11329
|
+
this.client = options.client;
|
|
11330
|
+
this.storage = options.storage ?? new LocalStorageDeviceSessionStorage();
|
|
11331
|
+
this.refreshLeadMs = options.refreshLeadMs ?? 300 * 1e3;
|
|
11332
|
+
this.session = this.storage.load();
|
|
11333
|
+
if (options.installResumeHandlers !== false) this.installResumeHandlers();
|
|
11334
|
+
this.scheduleProactiveRefresh();
|
|
11335
|
+
}
|
|
11336
|
+
get hasSession() {
|
|
11337
|
+
return this.session !== null;
|
|
11338
|
+
}
|
|
11339
|
+
get currentSession() {
|
|
11340
|
+
return this.session;
|
|
11341
|
+
}
|
|
11342
|
+
/**
|
|
11343
|
+
* Persist a freshly-issued device session and exchange it immediately
|
|
11344
|
+
* for a JWT. Call this once after a successful crypto-auth login when
|
|
11345
|
+
* the server returns a device-session token.
|
|
11346
|
+
*/
|
|
11347
|
+
async attachSession(record) {
|
|
11348
|
+
this.session = record;
|
|
11349
|
+
this.storage.save(record);
|
|
11350
|
+
return await this.runRefresh();
|
|
11351
|
+
}
|
|
11352
|
+
/**
|
|
11353
|
+
* Drop the local device session (no server call). Use after a logout
|
|
11354
|
+
* or when `session-expired` fires. To revoke server-side as well, call
|
|
11355
|
+
* `client.revokeDeviceSession(sessionId)` first.
|
|
11356
|
+
*/
|
|
11357
|
+
clearSession() {
|
|
11358
|
+
this.session = null;
|
|
11359
|
+
this.storage.clear();
|
|
11360
|
+
if (this.proactiveTimer) {
|
|
11361
|
+
clearTimeout(this.proactiveTimer);
|
|
11362
|
+
this.proactiveTimer = null;
|
|
11363
|
+
}
|
|
11364
|
+
}
|
|
11365
|
+
/**
|
|
11366
|
+
* Return a valid JWT, refreshing if it is missing or within
|
|
11367
|
+
* `refreshLeadMs` of expiry. Safe to call concurrently — one
|
|
11368
|
+
* refresh is in flight at a time.
|
|
11369
|
+
*
|
|
11370
|
+
* Transient refresh errors (network) fall back to the cached JWT so
|
|
11371
|
+
* the WS auth attempt can still proceed; the server will reject and
|
|
11372
|
+
* the next reconnect tries again. Only 401/403 from the refresh
|
|
11373
|
+
* endpoint surfaces as a thrown error here, since those mean the
|
|
11374
|
+
* device session itself is dead.
|
|
11375
|
+
*/
|
|
11376
|
+
async getValidToken() {
|
|
11377
|
+
if (this.refreshing) return this.refreshing;
|
|
11378
|
+
const current = this.client.token;
|
|
11379
|
+
if (current && this.tokenIsFresh(current)) return current;
|
|
11380
|
+
if (!this.session) return current ?? "";
|
|
11381
|
+
try {
|
|
11382
|
+
return await this.runRefresh();
|
|
11383
|
+
} catch (err) {
|
|
11384
|
+
const status = err?.status;
|
|
11385
|
+
if (status === 401 || status === 403) throw err;
|
|
11386
|
+
return current ?? "";
|
|
11387
|
+
}
|
|
11388
|
+
}
|
|
11389
|
+
/**
|
|
11390
|
+
* If a stored device session exists, exchange it for a fresh JWT now.
|
|
11391
|
+
* Returns the JWT (or null if there is no session to bootstrap from).
|
|
11392
|
+
* Throws on 401/403 — the stored session is invalid and the caller
|
|
11393
|
+
* should fall through to a full crypto-auth flow.
|
|
11394
|
+
*/
|
|
11395
|
+
async bootstrap() {
|
|
11396
|
+
if (!this.session) return null;
|
|
11397
|
+
try {
|
|
11398
|
+
return await this.runRefresh();
|
|
11399
|
+
} catch (err) {
|
|
11400
|
+
const status = err?.status;
|
|
11401
|
+
if (status === 401 || status === 403) throw err;
|
|
11402
|
+
return null;
|
|
11403
|
+
}
|
|
11404
|
+
}
|
|
11405
|
+
dispose() {
|
|
11406
|
+
if (this.disposed) return;
|
|
11407
|
+
this.disposed = true;
|
|
11408
|
+
if (this.proactiveTimer) {
|
|
11409
|
+
clearTimeout(this.proactiveTimer);
|
|
11410
|
+
this.proactiveTimer = null;
|
|
11411
|
+
}
|
|
11412
|
+
if (this.boundOnResume) {
|
|
11413
|
+
try {
|
|
11414
|
+
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", this.boundOnResume);
|
|
11415
|
+
if (typeof window !== "undefined") window.removeEventListener("online", this.boundOnResume);
|
|
11416
|
+
} catch {}
|
|
11417
|
+
this.boundOnResume = null;
|
|
11418
|
+
}
|
|
11419
|
+
this.removeAllListeners();
|
|
11420
|
+
}
|
|
11421
|
+
runRefresh() {
|
|
11422
|
+
if (this.refreshing) return this.refreshing;
|
|
11423
|
+
if (!this.session) return Promise.reject(/* @__PURE__ */ new Error("No device session"));
|
|
11424
|
+
const sessionToken = this.session.sessionToken;
|
|
11425
|
+
this.refreshing = (async () => {
|
|
11426
|
+
try {
|
|
11427
|
+
const jwt = await this.client.refreshWithDeviceSession(sessionToken);
|
|
11428
|
+
this.scheduleProactiveRefresh();
|
|
11429
|
+
this.emit("refresh", jwt);
|
|
11430
|
+
return jwt;
|
|
11431
|
+
} catch (err) {
|
|
11432
|
+
const status = err?.status;
|
|
11433
|
+
if (status === 401 || status === 403) {
|
|
11434
|
+
const message = err?.message ?? "device session rejected";
|
|
11435
|
+
this.clearSession();
|
|
11436
|
+
this.emit("session-expired", {
|
|
11437
|
+
status,
|
|
11438
|
+
message
|
|
11439
|
+
});
|
|
11440
|
+
}
|
|
11441
|
+
throw err;
|
|
11442
|
+
} finally {
|
|
11443
|
+
this.refreshing = null;
|
|
11444
|
+
}
|
|
11445
|
+
})();
|
|
11446
|
+
return this.refreshing;
|
|
11447
|
+
}
|
|
11448
|
+
tokenIsFresh(jwt) {
|
|
11449
|
+
const exp = this.parseExp(jwt);
|
|
11450
|
+
if (exp == null) return false;
|
|
11451
|
+
return exp * 1e3 - Date.now() > this.refreshLeadMs;
|
|
11452
|
+
}
|
|
11453
|
+
parseExp(jwt) {
|
|
11454
|
+
try {
|
|
11455
|
+
const [, payload] = jwt.split(".");
|
|
11456
|
+
if (!payload) return null;
|
|
11457
|
+
const { exp } = JSON.parse(atob(payload));
|
|
11458
|
+
return typeof exp === "number" ? exp : null;
|
|
11459
|
+
} catch {
|
|
11460
|
+
return null;
|
|
11461
|
+
}
|
|
11462
|
+
}
|
|
11463
|
+
scheduleProactiveRefresh() {
|
|
11464
|
+
if (this.proactiveTimer) {
|
|
11465
|
+
clearTimeout(this.proactiveTimer);
|
|
11466
|
+
this.proactiveTimer = null;
|
|
11467
|
+
}
|
|
11468
|
+
if (!this.session || this.disposed) return;
|
|
11469
|
+
const jwt = this.client.token;
|
|
11470
|
+
const exp = jwt ? this.parseExp(jwt) : null;
|
|
11471
|
+
if (exp == null) return;
|
|
11472
|
+
const delay = Math.max(3e4, exp * 1e3 - Date.now() - this.refreshLeadMs);
|
|
11473
|
+
this.proactiveTimer = setTimeout(() => {
|
|
11474
|
+
this.runRefresh().catch(() => {});
|
|
11475
|
+
}, delay);
|
|
11476
|
+
}
|
|
11477
|
+
installResumeHandlers() {
|
|
11478
|
+
if (this.resumeHandlersInstalled) return;
|
|
11479
|
+
if (typeof document === "undefined" && typeof window === "undefined") return;
|
|
11480
|
+
const onResume = () => {
|
|
11481
|
+
if (this.disposed) return;
|
|
11482
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
11483
|
+
if (!this.session) return;
|
|
11484
|
+
const jwt = this.client.token;
|
|
11485
|
+
if (jwt && this.tokenIsFresh(jwt)) return;
|
|
11486
|
+
this.runRefresh().catch(() => {});
|
|
11487
|
+
};
|
|
11488
|
+
this.boundOnResume = onResume;
|
|
11489
|
+
try {
|
|
11490
|
+
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onResume);
|
|
11491
|
+
if (typeof window !== "undefined") window.addEventListener("online", onResume);
|
|
11492
|
+
this.resumeHandlersInstalled = true;
|
|
11493
|
+
} catch {}
|
|
11494
|
+
}
|
|
11495
|
+
};
|
|
11496
|
+
|
|
11123
11497
|
//#endregion
|
|
11124
11498
|
//#region packages/provider/src/CloseEvents.ts
|
|
11125
11499
|
/**
|
|
@@ -15245,10 +15619,22 @@ var Semaphore = class {
|
|
|
15245
15619
|
else this.slots++;
|
|
15246
15620
|
}
|
|
15247
15621
|
};
|
|
15622
|
+
/**
|
|
15623
|
+
* Server-accepted doc-id format. The Rust server's `DocId::from_string`
|
|
15624
|
+
* is a strict UUID parse; any other string returns 422. Stale entries in
|
|
15625
|
+
* the doc-tree map (legacy slug ids from earlier demos, manual experiments)
|
|
15626
|
+
* would otherwise emit one 422 per `syncAll()` forever — see the warn
|
|
15627
|
+
* below in `_buildQueue`.
|
|
15628
|
+
*/
|
|
15629
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15630
|
+
function isValidDocId(id) {
|
|
15631
|
+
return UUID_RE.test(id);
|
|
15632
|
+
}
|
|
15248
15633
|
var BackgroundSyncManager = class extends EventEmitter {
|
|
15249
15634
|
constructor(rootProvider, client, fileBlobStore, opts) {
|
|
15250
15635
|
super();
|
|
15251
15636
|
this.syncStates = /* @__PURE__ */ new Map();
|
|
15637
|
+
this._warnedInvalidIds = /* @__PURE__ */ new Set();
|
|
15252
15638
|
this._destroyed = false;
|
|
15253
15639
|
this._initPromise = null;
|
|
15254
15640
|
this.rootProvider = rootProvider;
|
|
@@ -15390,7 +15776,19 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
15390
15776
|
* 3. Errored docs last
|
|
15391
15777
|
*/
|
|
15392
15778
|
_buildQueue(entries) {
|
|
15393
|
-
const
|
|
15779
|
+
const filtered = [];
|
|
15780
|
+
for (const entry of entries) {
|
|
15781
|
+
const [docId] = entry;
|
|
15782
|
+
if (isValidDocId(docId)) {
|
|
15783
|
+
filtered.push(entry);
|
|
15784
|
+
continue;
|
|
15785
|
+
}
|
|
15786
|
+
if (!this._warnedInvalidIds.has(docId)) {
|
|
15787
|
+
this._warnedInvalidIds.add(docId);
|
|
15788
|
+
console.warn(`[BackgroundSyncManager] skipping non-UUID doc id "${docId}" in doc-tree — server would reject it (422). Likely a stale entry from an earlier seeder; remove it from the tree to silence this.`);
|
|
15789
|
+
}
|
|
15790
|
+
}
|
|
15791
|
+
const items = filtered.map(([docId, v]) => {
|
|
15394
15792
|
const state = this.syncStates.get(docId);
|
|
15395
15793
|
const updatedAt = v?.updatedAt ?? v?.createdAt ?? 0;
|
|
15396
15794
|
let priority;
|
|
@@ -17773,6 +18171,42 @@ function toPlain(val) {
|
|
|
17773
18171
|
return val instanceof Y.Map ? val.toJSON() : val;
|
|
17774
18172
|
}
|
|
17775
18173
|
|
|
18174
|
+
//#endregion
|
|
18175
|
+
//#region packages/provider/src/SchemaTypes.ts
|
|
18176
|
+
/**
|
|
18177
|
+
* Thrown by typed write methods on `TypedDocsClient` when the stored
|
|
18178
|
+
* entry's `type` doesn't match the caller's `expectedType`. Distinct from
|
|
18179
|
+
* `MetaValidationError` (which signals schema-content mismatches).
|
|
18180
|
+
*/
|
|
18181
|
+
var TypedDocTypeMismatchError = class extends Error {
|
|
18182
|
+
constructor(docId, expectedType, actualType) {
|
|
18183
|
+
super(`Typed write on document ${docId} expected type "${expectedType}" but stored type is ${actualType === void 0 ? "(none)" : `"${actualType}"`}`);
|
|
18184
|
+
this.name = "TypedDocTypeMismatchError";
|
|
18185
|
+
this.docId = docId;
|
|
18186
|
+
this.expectedType = expectedType;
|
|
18187
|
+
this.actualType = actualType;
|
|
18188
|
+
}
|
|
18189
|
+
};
|
|
18190
|
+
/**
|
|
18191
|
+
* Project a raw `TreeEntry` to a `TypedTreeEntry<TMap, N>` if its `type`
|
|
18192
|
+
* matches; otherwise return `null`. Used by both `TreeManager.getTyped`
|
|
18193
|
+
* and `TypedDocsClient.{get,narrow}` to keep semantics consistent.
|
|
18194
|
+
*/
|
|
18195
|
+
function projectTreeEntry(entry, expectedType) {
|
|
18196
|
+
if (!entry) return null;
|
|
18197
|
+
if (entry.type !== expectedType) return null;
|
|
18198
|
+
return {
|
|
18199
|
+
id: entry.id,
|
|
18200
|
+
type: expectedType,
|
|
18201
|
+
label: entry.label,
|
|
18202
|
+
parentId: entry.parentId,
|
|
18203
|
+
order: entry.order,
|
|
18204
|
+
meta: entry.meta,
|
|
18205
|
+
createdAt: entry.createdAt,
|
|
18206
|
+
updatedAt: entry.updatedAt
|
|
18207
|
+
};
|
|
18208
|
+
}
|
|
18209
|
+
|
|
17776
18210
|
//#endregion
|
|
17777
18211
|
//#region packages/provider/src/TreeManager.ts
|
|
17778
18212
|
var TreeManager = class {
|
|
@@ -17843,6 +18277,20 @@ var TreeManager = class {
|
|
|
17843
18277
|
};
|
|
17844
18278
|
});
|
|
17845
18279
|
}
|
|
18280
|
+
/**
|
|
18281
|
+
* Schema-typed lookup. Returns a `TypedTreeEntry<TMap, N>` when the
|
|
18282
|
+
* entry's `type` matches `expectedType`, else `null`. Pure projection
|
|
18283
|
+
* over the existing untyped tree — no schema validation is performed
|
|
18284
|
+
* here (the entry's data is whatever was last synced; meta correctness
|
|
18285
|
+
* is the writer's responsibility, optionally enforced via
|
|
18286
|
+
* `MetaManager.setSchema`).
|
|
18287
|
+
*
|
|
18288
|
+
* Rule 4 alignment: when the entry's type doesn't match, returns null
|
|
18289
|
+
* rather than throwing — callers branch on the result.
|
|
18290
|
+
*/
|
|
18291
|
+
getTyped(_schema, expectedType, docId) {
|
|
18292
|
+
return projectTreeEntry(this.get(docId), expectedType);
|
|
18293
|
+
}
|
|
17846
18294
|
/** Find a single entry by ID. */
|
|
17847
18295
|
get(docId) {
|
|
17848
18296
|
const treeMap = this.dm.getTreeMap();
|
|
@@ -19568,9 +20016,29 @@ var ContentManager = class {
|
|
|
19568
20016
|
|
|
19569
20017
|
//#endregion
|
|
19570
20018
|
//#region packages/provider/src/MetaManager.ts
|
|
20019
|
+
var MetaValidationError = class extends Error {
|
|
20020
|
+
constructor(docId, docType, errors) {
|
|
20021
|
+
const detail = errors.map((e) => `${e.path.join(".") || "(root)"}: ${e.message}`).join("; ");
|
|
20022
|
+
super(`Meta validation failed for document ${docId} of type "${docType}": ${detail}`);
|
|
20023
|
+
this.name = "MetaValidationError";
|
|
20024
|
+
this.docId = docId;
|
|
20025
|
+
this.docType = docType;
|
|
20026
|
+
this.errors = errors;
|
|
20027
|
+
}
|
|
20028
|
+
};
|
|
19571
20029
|
var MetaManager = class {
|
|
19572
20030
|
constructor(dm) {
|
|
19573
20031
|
this.dm = dm;
|
|
20032
|
+
this.schema = null;
|
|
20033
|
+
}
|
|
20034
|
+
/**
|
|
20035
|
+
* Attach (or detach with `null`) a schema validator. When set, every
|
|
20036
|
+
* `update` / `set` validates the post-write meta against the entry's
|
|
20037
|
+
* declared `type` before writing to the doc-tree. Entries without a
|
|
20038
|
+
* `type` field pass through unconditionally.
|
|
20039
|
+
*/
|
|
20040
|
+
setSchema(schema) {
|
|
20041
|
+
this.schema = schema;
|
|
19574
20042
|
}
|
|
19575
20043
|
/** Read metadata for a document. Returns null if not found. */
|
|
19576
20044
|
get(docId) {
|
|
@@ -19589,6 +20057,9 @@ var MetaManager = class {
|
|
|
19589
20057
|
/**
|
|
19590
20058
|
* Merge fields into a document's metadata.
|
|
19591
20059
|
* Existing keys not in the update are preserved.
|
|
20060
|
+
*
|
|
20061
|
+
* @throws {MetaValidationError} when a schema is attached and the
|
|
20062
|
+
* merged meta fails validation for the entry's declared type.
|
|
19592
20063
|
*/
|
|
19593
20064
|
update(docId, meta) {
|
|
19594
20065
|
const treeMap = this.dm.getTreeMap();
|
|
@@ -19596,18 +20067,23 @@ var MetaManager = class {
|
|
|
19596
20067
|
const raw = treeMap.get(docId);
|
|
19597
20068
|
if (!raw) throw new Error(`Document ${docId} not found`);
|
|
19598
20069
|
const entry = toPlain(raw);
|
|
20070
|
+
const mergedMeta = {
|
|
20071
|
+
...entry.meta ?? {},
|
|
20072
|
+
...meta
|
|
20073
|
+
};
|
|
20074
|
+
this.validateOrThrow(docId, entry, mergedMeta);
|
|
19599
20075
|
treeMap.set(docId, {
|
|
19600
20076
|
...entry,
|
|
19601
|
-
meta:
|
|
19602
|
-
...entry.meta ?? {},
|
|
19603
|
-
...meta
|
|
19604
|
-
},
|
|
20077
|
+
meta: mergedMeta,
|
|
19605
20078
|
updatedAt: Date.now()
|
|
19606
20079
|
});
|
|
19607
20080
|
}
|
|
19608
20081
|
/**
|
|
19609
20082
|
* Replace all metadata on a document.
|
|
19610
20083
|
* This overwrites the entire meta object.
|
|
20084
|
+
*
|
|
20085
|
+
* @throws {MetaValidationError} when a schema is attached and the
|
|
20086
|
+
* replacement meta fails validation for the entry's declared type.
|
|
19611
20087
|
*/
|
|
19612
20088
|
set(docId, meta) {
|
|
19613
20089
|
const treeMap = this.dm.getTreeMap();
|
|
@@ -19615,6 +20091,7 @@ var MetaManager = class {
|
|
|
19615
20091
|
const raw = treeMap.get(docId);
|
|
19616
20092
|
if (!raw) throw new Error(`Document ${docId} not found`);
|
|
19617
20093
|
const entry = toPlain(raw);
|
|
20094
|
+
this.validateOrThrow(docId, entry, meta);
|
|
19618
20095
|
treeMap.set(docId, {
|
|
19619
20096
|
...entry,
|
|
19620
20097
|
meta,
|
|
@@ -19632,12 +20109,20 @@ var MetaManager = class {
|
|
|
19632
20109
|
const entry = toPlain(raw);
|
|
19633
20110
|
const updated = { ...entry.meta ?? {} };
|
|
19634
20111
|
for (const key of keys) delete updated[key];
|
|
20112
|
+
this.validateOrThrow(docId, entry, updated);
|
|
19635
20113
|
treeMap.set(docId, {
|
|
19636
20114
|
...entry,
|
|
19637
20115
|
meta: updated,
|
|
19638
20116
|
updatedAt: Date.now()
|
|
19639
20117
|
});
|
|
19640
20118
|
}
|
|
20119
|
+
validateOrThrow(docId, entry, meta) {
|
|
20120
|
+
if (!this.schema) return;
|
|
20121
|
+
const docType = entry.type;
|
|
20122
|
+
if (!docType) return;
|
|
20123
|
+
const result = this.schema.validateMeta(docType, meta);
|
|
20124
|
+
if (!result.ok) throw new MetaValidationError(docId, docType, result.errors);
|
|
20125
|
+
}
|
|
19641
20126
|
};
|
|
19642
20127
|
|
|
19643
20128
|
//#endregion
|
|
@@ -19683,6 +20168,44 @@ var DocumentManager = class {
|
|
|
19683
20168
|
this.content = new ContentManager(this);
|
|
19684
20169
|
this.meta = new MetaManager(this);
|
|
19685
20170
|
}
|
|
20171
|
+
/**
|
|
20172
|
+
* Bind a schema registry to a typed accessor surface. The returned
|
|
20173
|
+
* client offers `.get(type, id)` with the registry's meta types
|
|
20174
|
+
* inferred at the call site, removing the need to pass `schema`
|
|
20175
|
+
* to every lookup. The provider remains schema-free at runtime —
|
|
20176
|
+
* `schema` is only used to drive type inference (see
|
|
20177
|
+
* `SchemaRegistryLike` for the structural witness).
|
|
20178
|
+
*
|
|
20179
|
+
* @example
|
|
20180
|
+
* const docs = dm.docs(kanbanSchema);
|
|
20181
|
+
* const board = docs.get("kanban", id);
|
|
20182
|
+
* if (board) console.log(board.meta.kanbanColumnWidth); // typed
|
|
20183
|
+
*/
|
|
20184
|
+
docs(_schema) {
|
|
20185
|
+
const tree = this.tree;
|
|
20186
|
+
const meta = this.meta;
|
|
20187
|
+
const expectType = (expected, id) => {
|
|
20188
|
+
const actual = tree.get(id)?.type;
|
|
20189
|
+
if (actual !== expected) throw new TypedDocTypeMismatchError(id, expected, actual);
|
|
20190
|
+
};
|
|
20191
|
+
return {
|
|
20192
|
+
get: (expectedType, id) => projectTreeEntry(tree.get(id), expectedType),
|
|
20193
|
+
getEntry: (id) => tree.get(id),
|
|
20194
|
+
narrow: (expectedType, entry) => projectTreeEntry(entry ?? null, expectedType),
|
|
20195
|
+
update: (expectedType, id, patch) => {
|
|
20196
|
+
expectType(expectedType, id);
|
|
20197
|
+
meta.update(id, patch);
|
|
20198
|
+
},
|
|
20199
|
+
set: (expectedType, id, value) => {
|
|
20200
|
+
expectType(expectedType, id);
|
|
20201
|
+
meta.set(id, value);
|
|
20202
|
+
},
|
|
20203
|
+
clear: (expectedType, id, keys) => {
|
|
20204
|
+
expectType(expectedType, id);
|
|
20205
|
+
meta.clear(id, keys);
|
|
20206
|
+
}
|
|
20207
|
+
};
|
|
20208
|
+
}
|
|
19686
20209
|
get displayName() {
|
|
19687
20210
|
return this._config.name || "DocumentManager";
|
|
19688
20211
|
}
|
|
@@ -19824,5 +20347,5 @@ var DocumentManager = class {
|
|
|
19824
20347
|
};
|
|
19825
20348
|
|
|
19826
20349
|
//#endregion
|
|
19827
|
-
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, ManualSignaling, MessageTooBig, MessageType, MetaManager, NotificationsClient, OfflineStore, PAGE_TYPES, PeerConnection, RPC_PREFIX, ResetConnection, RpcClient, RpcError, SERVER_ROOT_ID, SearchIndex, SignalingSocket, SubdocMessage, TYPE_ALIASES, TreeManager, 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 };
|
|
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 };
|
|
19828
20351
|
//# sourceMappingURL=abracadabra-provider.esm.js.map
|