@abraca/dabra 2.0.9 → 2.3.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 +540 -9
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +534 -10
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +434 -2
- 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)) {
|
|
@@ -10265,7 +10390,8 @@ var AbracadabraClient = class {
|
|
|
10265
10390
|
deviceName: opts.deviceName,
|
|
10266
10391
|
displayName: opts.displayName,
|
|
10267
10392
|
email: opts.email,
|
|
10268
|
-
inviteCode: opts.inviteCode
|
|
10393
|
+
inviteCode: opts.inviteCode,
|
|
10394
|
+
x25519Key: opts.x25519Key
|
|
10269
10395
|
},
|
|
10270
10396
|
auth: false
|
|
10271
10397
|
});
|
|
@@ -10584,6 +10710,27 @@ var AbracadabraClient = class {
|
|
|
10584
10710
|
return (await this.request("GET", `/docs/search?${params.toString()}`)).results;
|
|
10585
10711
|
}
|
|
10586
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
|
+
/**
|
|
10587
10734
|
* List the direct children of a document, returning full metadata. Pass
|
|
10588
10735
|
* no argument to list the children of the server root — what the
|
|
10589
10736
|
* dashboard renders as the Spaces sidebar.
|
|
@@ -11119,6 +11266,234 @@ var AbracadabraClient = class {
|
|
|
11119
11266
|
}
|
|
11120
11267
|
};
|
|
11121
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
|
+
|
|
11122
11497
|
//#endregion
|
|
11123
11498
|
//#region packages/provider/src/CloseEvents.ts
|
|
11124
11499
|
/**
|
|
@@ -15244,10 +15619,22 @@ var Semaphore = class {
|
|
|
15244
15619
|
else this.slots++;
|
|
15245
15620
|
}
|
|
15246
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
|
+
}
|
|
15247
15633
|
var BackgroundSyncManager = class extends EventEmitter {
|
|
15248
15634
|
constructor(rootProvider, client, fileBlobStore, opts) {
|
|
15249
15635
|
super();
|
|
15250
15636
|
this.syncStates = /* @__PURE__ */ new Map();
|
|
15637
|
+
this._warnedInvalidIds = /* @__PURE__ */ new Set();
|
|
15251
15638
|
this._destroyed = false;
|
|
15252
15639
|
this._initPromise = null;
|
|
15253
15640
|
this.rootProvider = rootProvider;
|
|
@@ -15389,7 +15776,19 @@ var BackgroundSyncManager = class extends EventEmitter {
|
|
|
15389
15776
|
* 3. Errored docs last
|
|
15390
15777
|
*/
|
|
15391
15778
|
_buildQueue(entries) {
|
|
15392
|
-
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]) => {
|
|
15393
15792
|
const state = this.syncStates.get(docId);
|
|
15394
15793
|
const updatedAt = v?.updatedAt ?? v?.createdAt ?? 0;
|
|
15395
15794
|
let priority;
|
|
@@ -17772,6 +18171,42 @@ function toPlain(val) {
|
|
|
17772
18171
|
return val instanceof Y.Map ? val.toJSON() : val;
|
|
17773
18172
|
}
|
|
17774
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
|
+
|
|
17775
18210
|
//#endregion
|
|
17776
18211
|
//#region packages/provider/src/TreeManager.ts
|
|
17777
18212
|
var TreeManager = class {
|
|
@@ -17842,6 +18277,20 @@ var TreeManager = class {
|
|
|
17842
18277
|
};
|
|
17843
18278
|
});
|
|
17844
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
|
+
}
|
|
17845
18294
|
/** Find a single entry by ID. */
|
|
17846
18295
|
get(docId) {
|
|
17847
18296
|
const treeMap = this.dm.getTreeMap();
|
|
@@ -19567,9 +20016,29 @@ var ContentManager = class {
|
|
|
19567
20016
|
|
|
19568
20017
|
//#endregion
|
|
19569
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
|
+
};
|
|
19570
20029
|
var MetaManager = class {
|
|
19571
20030
|
constructor(dm) {
|
|
19572
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;
|
|
19573
20042
|
}
|
|
19574
20043
|
/** Read metadata for a document. Returns null if not found. */
|
|
19575
20044
|
get(docId) {
|
|
@@ -19588,6 +20057,9 @@ var MetaManager = class {
|
|
|
19588
20057
|
/**
|
|
19589
20058
|
* Merge fields into a document's metadata.
|
|
19590
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.
|
|
19591
20063
|
*/
|
|
19592
20064
|
update(docId, meta) {
|
|
19593
20065
|
const treeMap = this.dm.getTreeMap();
|
|
@@ -19595,18 +20067,23 @@ var MetaManager = class {
|
|
|
19595
20067
|
const raw = treeMap.get(docId);
|
|
19596
20068
|
if (!raw) throw new Error(`Document ${docId} not found`);
|
|
19597
20069
|
const entry = toPlain(raw);
|
|
20070
|
+
const mergedMeta = {
|
|
20071
|
+
...entry.meta ?? {},
|
|
20072
|
+
...meta
|
|
20073
|
+
};
|
|
20074
|
+
this.validateOrThrow(docId, entry, mergedMeta);
|
|
19598
20075
|
treeMap.set(docId, {
|
|
19599
20076
|
...entry,
|
|
19600
|
-
meta:
|
|
19601
|
-
...entry.meta ?? {},
|
|
19602
|
-
...meta
|
|
19603
|
-
},
|
|
20077
|
+
meta: mergedMeta,
|
|
19604
20078
|
updatedAt: Date.now()
|
|
19605
20079
|
});
|
|
19606
20080
|
}
|
|
19607
20081
|
/**
|
|
19608
20082
|
* Replace all metadata on a document.
|
|
19609
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.
|
|
19610
20087
|
*/
|
|
19611
20088
|
set(docId, meta) {
|
|
19612
20089
|
const treeMap = this.dm.getTreeMap();
|
|
@@ -19614,6 +20091,7 @@ var MetaManager = class {
|
|
|
19614
20091
|
const raw = treeMap.get(docId);
|
|
19615
20092
|
if (!raw) throw new Error(`Document ${docId} not found`);
|
|
19616
20093
|
const entry = toPlain(raw);
|
|
20094
|
+
this.validateOrThrow(docId, entry, meta);
|
|
19617
20095
|
treeMap.set(docId, {
|
|
19618
20096
|
...entry,
|
|
19619
20097
|
meta,
|
|
@@ -19631,12 +20109,20 @@ var MetaManager = class {
|
|
|
19631
20109
|
const entry = toPlain(raw);
|
|
19632
20110
|
const updated = { ...entry.meta ?? {} };
|
|
19633
20111
|
for (const key of keys) delete updated[key];
|
|
20112
|
+
this.validateOrThrow(docId, entry, updated);
|
|
19634
20113
|
treeMap.set(docId, {
|
|
19635
20114
|
...entry,
|
|
19636
20115
|
meta: updated,
|
|
19637
20116
|
updatedAt: Date.now()
|
|
19638
20117
|
});
|
|
19639
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
|
+
}
|
|
19640
20126
|
};
|
|
19641
20127
|
|
|
19642
20128
|
//#endregion
|
|
@@ -19682,6 +20168,44 @@ var DocumentManager = class {
|
|
|
19682
20168
|
this.content = new ContentManager(this);
|
|
19683
20169
|
this.meta = new MetaManager(this);
|
|
19684
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
|
+
}
|
|
19685
20209
|
get displayName() {
|
|
19686
20210
|
return this._config.name || "DocumentManager";
|
|
19687
20211
|
}
|
|
@@ -19823,5 +20347,5 @@ var DocumentManager = class {
|
|
|
19823
20347
|
};
|
|
19824
20348
|
|
|
19825
20349
|
//#endregion
|
|
19826
|
-
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 };
|
|
19827
20351
|
//# sourceMappingURL=abracadabra-provider.esm.js.map
|