@spooky-sync/core 0.0.1-canary.208 → 0.0.1-canary.209
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/index.d.ts +69 -6
- package/dist/index.js +206 -57
- package/dist/sqlite-worker.js +22 -7
- package/dist/types.d.ts +10 -0
- package/package.json +3 -3
- package/src/index.ts +1 -0
- package/src/modules/data/data.pending-ids.test.ts +74 -0
- package/src/modules/data/data.rematerialize.test.ts +114 -0
- package/src/modules/data/index.ts +151 -61
- package/src/modules/devtools/index.ts +65 -18
- package/src/modules/devtools/notify-throttle.test.ts +6 -1
- package/src/modules/devtools/state-shape.test.ts +146 -0
- package/src/modules/sync/sync.live-removal.test.ts +1 -0
- package/src/modules/sync/sync.ts +10 -0
- package/src/services/database/database.ts +14 -5
- package/src/services/database/errors.ts +34 -0
- package/src/services/database/local.ts +6 -0
- package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
- package/src/services/database/sqlite-cache-engine.ts +27 -1
- package/src/services/database/sqlite-lock-verify.test.ts +33 -0
- package/src/services/database/sqlite-lock-verify.ts +45 -0
- package/src/services/database/sqlite-transport.ts +5 -2
- package/src/services/database/sqlite-worker.ts +11 -11
- package/src/services/stream-processor/index.ts +8 -0
- package/src/types.ts +10 -0
- package/src/utils/index.ts +8 -2
package/dist/index.d.ts
CHANGED
|
@@ -12,12 +12,18 @@ declare abstract class AbstractDatabaseService {
|
|
|
12
12
|
protected logger: Logger$1;
|
|
13
13
|
protected events: DatabaseEventSystem;
|
|
14
14
|
/**
|
|
15
|
-
* Per-query deadline in ms; `0` disables.
|
|
16
|
-
* (see `RemoteDatabaseService`)
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Per-query deadline in ms; `0` disables. The remote service sets it from
|
|
16
|
+
* `queryTimeoutMs` (see `RemoteDatabaseService`), the local one from
|
|
17
|
+
* `localOpTimeoutMs` (see `LocalDatabaseService`): a local query can be
|
|
18
|
+
* legitimately slow, but it must never be endless - every query waits on the
|
|
19
|
+
* previous link of {@link query}'s chain, and one that never settled wedged
|
|
20
|
+
* every later local op behind it.
|
|
19
21
|
*/
|
|
20
22
|
protected queryTimeoutMs: number;
|
|
23
|
+
/** The error a deadline expiry rejects with; the local service substitutes
|
|
24
|
+
* its typed `LocalOpTimeoutError`. "timed out" in the message is
|
|
25
|
+
* load-bearing either way: `classifySyncError` keys off it. */
|
|
26
|
+
protected timeoutError(_query: string): Error;
|
|
21
27
|
protected abstract eventType: typeof DatabaseEventTypes.LocalQuery | typeof DatabaseEventTypes.RemoteQuery;
|
|
22
28
|
constructor(client: Surreal$1, logger: Logger$1, events: DatabaseEventSystem);
|
|
23
29
|
abstract connect(): Promise<void>;
|
|
@@ -256,6 +262,14 @@ interface StreamUpdate {
|
|
|
256
262
|
queryHash: string;
|
|
257
263
|
localArray: RecordVersionArray;
|
|
258
264
|
op?: 'CREATE' | 'UPDATE' | 'DELETE';
|
|
265
|
+
/**
|
|
266
|
+
* Client-internal: not from the circuit. A membership-only change that
|
|
267
|
+
* needed no fetch is re-materialized through this same path so it cannot
|
|
268
|
+
* race a real update (DataModule.scheduleRematerialize). Carries the last
|
|
269
|
+
* known `localArray`; consumers that describe an INGEST (persist, metrics,
|
|
270
|
+
* devtools events) skip it.
|
|
271
|
+
*/
|
|
272
|
+
synthetic?: boolean;
|
|
259
273
|
/**
|
|
260
274
|
* End-to-end ingest latency for the WASM call that produced this update,
|
|
261
275
|
* in milliseconds. Populated by StreamProcessorService.ingest. Undefined
|
|
@@ -734,6 +748,22 @@ declare class DataModule<S extends SchemaStructure> {
|
|
|
734
748
|
* Handle stream updates from DBSP (via CacheModule)
|
|
735
749
|
*/
|
|
736
750
|
onStreamUpdate(update: StreamUpdate): Promise<void>;
|
|
751
|
+
/** Coalesce `update` onto the query's trailing timer (see onStreamUpdate). */
|
|
752
|
+
private queueStreamUpdate;
|
|
753
|
+
/**
|
|
754
|
+
* Re-materialize + notify a query whose MEMBERSHIP changed without any row
|
|
755
|
+
* needing to be fetched, i.e. without the SSP stream update that normally
|
|
756
|
+
* carries the notify. That is every row this client wrote itself: the local
|
|
757
|
+
* CREATE memoized it at `_00_rv = 1`, the server publishes it at 1, so the
|
|
758
|
+
* sync engine rightly fetches nothing - and then nobody told the subscribers
|
|
759
|
+
* that `remoteArray` now holds the id. The row appeared on reload only.
|
|
760
|
+
*
|
|
761
|
+
* Routed through the same per-query debounce as a real stream update, so it
|
|
762
|
+
* cannot race one: a pending real update already materializes against the
|
|
763
|
+
* current `remoteArray` and wins. The synthetic update re-uses the circuit's
|
|
764
|
+
* last `localArray` and skips the persist/metrics that describe an ingest.
|
|
765
|
+
*/
|
|
766
|
+
scheduleRematerialize(queryHash: string): void;
|
|
737
767
|
/**
|
|
738
768
|
* Process a query's pending (debounced) stream update NOW instead of on the
|
|
739
769
|
* trailing edge. Called by the sync engine before it flips a query back to
|
|
@@ -794,6 +824,8 @@ declare class DataModule<S extends SchemaStructure> {
|
|
|
794
824
|
private pendingIdsAt;
|
|
795
825
|
private pendingIdsInflight;
|
|
796
826
|
private static readonly PENDING_IDS_TTL_MS;
|
|
827
|
+
private pendingIdsGen;
|
|
828
|
+
private static readonly PENDING_IDS_MAX_REREADS;
|
|
797
829
|
/** Drop the cached outbox ids. Cheap; call it on anything that could change
|
|
798
830
|
* `_00_pending_mutations`. */
|
|
799
831
|
private invalidatePendingIds;
|
|
@@ -957,7 +989,8 @@ declare class DataModule<S extends SchemaStructure> {
|
|
|
957
989
|
deletes: Set<string>;
|
|
958
990
|
}>;
|
|
959
991
|
/** The uncached read. Also the reload path after an invalidation, so the ids
|
|
960
|
-
* still survive a reload exactly as before.
|
|
992
|
+
* still survive a reload exactly as before. `gen` is the generation the read
|
|
993
|
+
* was issued under; the result is cached only if it is still current. */
|
|
961
994
|
private readPendingRecordIds;
|
|
962
995
|
/** True while ≥1 live subscriber is watching this query (refcount guard). */
|
|
963
996
|
hasSubscribers(hash: string): boolean;
|
|
@@ -2640,6 +2673,36 @@ declare class Sp00kyClient<S extends SchemaStructure> {
|
|
|
2640
2673
|
/** True when `a` is a valid version strictly greater than valid version `b`. */
|
|
2641
2674
|
declare function semverGt(a: unknown, b: unknown): boolean;
|
|
2642
2675
|
//#endregion
|
|
2676
|
+
//#region src/services/database/errors.d.ts
|
|
2677
|
+
/**
|
|
2678
|
+
* A local-store operation that did not answer within its deadline.
|
|
2679
|
+
*
|
|
2680
|
+
* The local write path (`db.create` / `db.update` / `db.delete`, every local
|
|
2681
|
+
* query behind them) used to have no deadline anywhere: the SQLite worker
|
|
2682
|
+
* transport parks a call until the worker replies, the surrealdb engine's
|
|
2683
|
+
* query chain waits on the previous link, and `withRetry` retries without a
|
|
2684
|
+
* clock. One op that never settled (a worker starved behind a long select, a
|
|
2685
|
+
* lock verification awaiting `navigator.locks.query()` forever) left the
|
|
2686
|
+
* caller's promise pending for the tab's lifetime - a chat composer that never
|
|
2687
|
+
* re-enabled, a call that never got past "Connecting".
|
|
2688
|
+
*
|
|
2689
|
+
* The message says "timed out" on purpose: `classifySyncError` keys off it and
|
|
2690
|
+
* treats the failure as transient (re-queue), never as an application error
|
|
2691
|
+
* that rolls the mutation back. `retryable: false` keeps `withRetry` from
|
|
2692
|
+
* spinning on it: the op is still running in the engine, retrying queues a
|
|
2693
|
+
* second copy behind it.
|
|
2694
|
+
*/
|
|
2695
|
+
declare class LocalOpTimeoutError extends Error {
|
|
2696
|
+
readonly name = "LocalOpTimeoutError";
|
|
2697
|
+
readonly retryable = false;
|
|
2698
|
+
readonly op: string;
|
|
2699
|
+
readonly timeoutMs: number;
|
|
2700
|
+
constructor(op: string, timeoutMs: number);
|
|
2701
|
+
}
|
|
2702
|
+
/** Default deadline for one local-store operation. Generous: a cold 4k-row
|
|
2703
|
+
* select on a throttled tab is seconds, not tens of seconds. */
|
|
2704
|
+
declare const DEFAULT_LOCAL_OP_TIMEOUT_MS = 30000;
|
|
2705
|
+
//#endregion
|
|
2643
2706
|
//#region src/utils/index.d.ts
|
|
2644
2707
|
declare function fileToUint8Array(file: File | Blob): Promise<Uint8Array>;
|
|
2645
2708
|
/**
|
|
@@ -2652,4 +2715,4 @@ declare function textToHtml(text: string): string;
|
|
|
2652
2715
|
*/
|
|
2653
2716
|
|
|
2654
2717
|
//#endregion
|
|
2655
|
-
export { AppReleaseHandle, AppReleaseModule, type AppReleaseOptions, type AppReleaseSnapshot, AuthEventSystem, AuthEventTypeMap, AuthEventTypes, AuthService, BLURHASH_IMAGE_EXTENSIONS, type BlobCacheStats, type BlobEntry, type BlobKey, type BlobReadOptions, type BlobUrlLease, type BlurhashEncodeOptions, type BlurhashSetting, BucketHandle, BucketPutOptions, BucketPutResult, CURSOR_COLORS, ConnectionState, CrdtField, CrdtManager, DebounceOptions, EventSubscriptionOptions, FeatureFlagHandle, FeatureFlagModule, type FeatureFlagOptions, type FeatureFlagOverride, type FeatureFlagSnapshot, Level, MATERIALIZATION_SAMPLE_WINDOW, MutationCallback, MutationEvent, MutationEventType, PersistenceClient, PhaseStat, PinoTransmit, PreloadOptions, PreloadRefresh, QueryConfig, QueryConfigRecord, QueryHash, QueryState, QueryStatus, QueryStatusCallback, QueryTimeToLive, QueryTimings, QueryUpdateCallback, ReconnectConfig, RecordVersionArray, RecordVersionDiff, RegistrationTimings, RunOptions, Sp00kyClient, Sp00kyConfig, Sp00kyQueryResult, Sp00kyQueryResultPromise, StorageHealth, StorageHealthStatus, StoreType, SyncHealth, SyncHealthConfig, SyncHealthStatus, TimingPhase, UpdateOptions, blurhashSidecarPath, bucketContentToBlob, createAuthEventSystem, cursorColorFromName, decode as decodeBlurhash, encode as encodeBlurhash, encodeImageToBlurhash, fileToUint8Array, isBlurhashValid, isImagePath, semverGt, textToHtml };
|
|
2718
|
+
export { AppReleaseHandle, AppReleaseModule, type AppReleaseOptions, type AppReleaseSnapshot, AuthEventSystem, AuthEventTypeMap, AuthEventTypes, AuthService, BLURHASH_IMAGE_EXTENSIONS, type BlobCacheStats, type BlobEntry, type BlobKey, type BlobReadOptions, type BlobUrlLease, type BlurhashEncodeOptions, type BlurhashSetting, BucketHandle, BucketPutOptions, BucketPutResult, CURSOR_COLORS, ConnectionState, CrdtField, CrdtManager, DEFAULT_LOCAL_OP_TIMEOUT_MS, DebounceOptions, EventSubscriptionOptions, FeatureFlagHandle, FeatureFlagModule, type FeatureFlagOptions, type FeatureFlagOverride, type FeatureFlagSnapshot, Level, LocalOpTimeoutError, MATERIALIZATION_SAMPLE_WINDOW, MutationCallback, MutationEvent, MutationEventType, PersistenceClient, PhaseStat, PinoTransmit, PreloadOptions, PreloadRefresh, QueryConfig, QueryConfigRecord, QueryHash, QueryState, QueryStatus, QueryStatusCallback, QueryTimeToLive, QueryTimings, QueryUpdateCallback, ReconnectConfig, RecordVersionArray, RecordVersionDiff, RegistrationTimings, RunOptions, Sp00kyClient, Sp00kyConfig, Sp00kyQueryResult, Sp00kyQueryResultPromise, StorageHealth, StorageHealthStatus, StoreType, SyncHealth, SyncHealthConfig, SyncHealthStatus, TimingPhase, UpdateOptions, blurhashSidecarPath, bucketContentToBlob, createAuthEventSystem, cursorColorFromName, decode as decodeBlurhash, encode as encodeBlurhash, encodeImageToBlurhash, fileToUint8Array, isBlurhashValid, isImagePath, semverGt, textToHtml };
|
package/dist/index.js
CHANGED
|
@@ -229,6 +229,7 @@ async function withRetry(logger, operation, retries = 3, delayMs = 100) {
|
|
|
229
229
|
return await operation();
|
|
230
230
|
} catch (err) {
|
|
231
231
|
lastError = err;
|
|
232
|
+
if (err?.retryable === false) throw err;
|
|
232
233
|
if (err?.message?.includes("Can not open transaction") || err?.message?.includes("transaction") || err?.message?.includes("Database is busy")) {
|
|
233
234
|
const msg = err instanceof Error ? err.message : String(err);
|
|
234
235
|
logger.warn({
|
|
@@ -264,7 +265,7 @@ async function withRetry(logger, operation, retries = 3, delayMs = 100) {
|
|
|
264
265
|
function withTimeout(promise, timeoutMs, message) {
|
|
265
266
|
if (!(timeoutMs > 0)) return promise;
|
|
266
267
|
return new Promise((resolve, reject) => {
|
|
267
|
-
const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
268
|
+
const timer = setTimeout(() => reject(typeof message === "function" ? message() : new Error(message)), timeoutMs);
|
|
268
269
|
promise.then((value) => {
|
|
269
270
|
clearTimeout(timer);
|
|
270
271
|
resolve(value);
|
|
@@ -282,12 +283,20 @@ var AbstractDatabaseService = class {
|
|
|
282
283
|
logger;
|
|
283
284
|
events;
|
|
284
285
|
/**
|
|
285
|
-
* Per-query deadline in ms; `0` disables.
|
|
286
|
-
* (see `RemoteDatabaseService`)
|
|
287
|
-
*
|
|
288
|
-
*
|
|
286
|
+
* Per-query deadline in ms; `0` disables. The remote service sets it from
|
|
287
|
+
* `queryTimeoutMs` (see `RemoteDatabaseService`), the local one from
|
|
288
|
+
* `localOpTimeoutMs` (see `LocalDatabaseService`): a local query can be
|
|
289
|
+
* legitimately slow, but it must never be endless - every query waits on the
|
|
290
|
+
* previous link of {@link query}'s chain, and one that never settled wedged
|
|
291
|
+
* every later local op behind it.
|
|
289
292
|
*/
|
|
290
293
|
queryTimeoutMs = 0;
|
|
294
|
+
/** The error a deadline expiry rejects with; the local service substitutes
|
|
295
|
+
* its typed `LocalOpTimeoutError`. "timed out" in the message is
|
|
296
|
+
* load-bearing either way: `classifySyncError` keys off it. */
|
|
297
|
+
timeoutError(_query) {
|
|
298
|
+
return /* @__PURE__ */ new Error(`Remote query timed out after ${this.queryTimeoutMs}ms`);
|
|
299
|
+
}
|
|
291
300
|
constructor(client, logger, events) {
|
|
292
301
|
this.client = client;
|
|
293
302
|
this.logger = logger.child({ service: "Database" });
|
|
@@ -321,7 +330,7 @@ var AbstractDatabaseService = class {
|
|
|
321
330
|
vars,
|
|
322
331
|
Category: "sp00ky-client::Database::query"
|
|
323
332
|
}, "Executing query");
|
|
324
|
-
const result = await withTimeout(this.client.query(query, vars), this.queryTimeoutMs,
|
|
333
|
+
const result = await withTimeout(this.client.query(query, vars), this.queryTimeoutMs, () => this.timeoutError(query));
|
|
325
334
|
const duration = performance.now() - startTime;
|
|
326
335
|
this.events.emit(this.eventType, {
|
|
327
336
|
query,
|
|
@@ -367,6 +376,41 @@ var AbstractDatabaseService = class {
|
|
|
367
376
|
}
|
|
368
377
|
};
|
|
369
378
|
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/services/database/errors.ts
|
|
381
|
+
/**
|
|
382
|
+
* A local-store operation that did not answer within its deadline.
|
|
383
|
+
*
|
|
384
|
+
* The local write path (`db.create` / `db.update` / `db.delete`, every local
|
|
385
|
+
* query behind them) used to have no deadline anywhere: the SQLite worker
|
|
386
|
+
* transport parks a call until the worker replies, the surrealdb engine's
|
|
387
|
+
* query chain waits on the previous link, and `withRetry` retries without a
|
|
388
|
+
* clock. One op that never settled (a worker starved behind a long select, a
|
|
389
|
+
* lock verification awaiting `navigator.locks.query()` forever) left the
|
|
390
|
+
* caller's promise pending for the tab's lifetime - a chat composer that never
|
|
391
|
+
* re-enabled, a call that never got past "Connecting".
|
|
392
|
+
*
|
|
393
|
+
* The message says "timed out" on purpose: `classifySyncError` keys off it and
|
|
394
|
+
* treats the failure as transient (re-queue), never as an application error
|
|
395
|
+
* that rolls the mutation back. `retryable: false` keeps `withRetry` from
|
|
396
|
+
* spinning on it: the op is still running in the engine, retrying queues a
|
|
397
|
+
* second copy behind it.
|
|
398
|
+
*/
|
|
399
|
+
var LocalOpTimeoutError = class extends Error {
|
|
400
|
+
name = "LocalOpTimeoutError";
|
|
401
|
+
retryable = false;
|
|
402
|
+
op;
|
|
403
|
+
timeoutMs;
|
|
404
|
+
constructor(op, timeoutMs) {
|
|
405
|
+
super(`Local database operation timed out after ${timeoutMs}ms (${op})`);
|
|
406
|
+
this.op = op;
|
|
407
|
+
this.timeoutMs = timeoutMs;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
/** Default deadline for one local-store operation. Generous: a cold 4k-row
|
|
411
|
+
* select on a throttled tab is seconds, not tens of seconds. */
|
|
412
|
+
const DEFAULT_LOCAL_OP_TIMEOUT_MS = 3e4;
|
|
413
|
+
|
|
370
414
|
//#endregion
|
|
371
415
|
//#region src/events/index.ts
|
|
372
416
|
/**
|
|
@@ -663,6 +707,10 @@ var LocalDatabaseService = class extends AbstractDatabaseService {
|
|
|
663
707
|
const events = createDatabaseEventSystem();
|
|
664
708
|
super(createBareSurrealClient(), logger, events);
|
|
665
709
|
this.config = config;
|
|
710
|
+
this.queryTimeoutMs = Math.max(0, config.localOpTimeoutMs ?? DEFAULT_LOCAL_OP_TIMEOUT_MS);
|
|
711
|
+
}
|
|
712
|
+
timeoutError(query) {
|
|
713
|
+
return new LocalOpTimeoutError(query.slice(0, 80), this.queryTimeoutMs);
|
|
666
714
|
}
|
|
667
715
|
getConfig() {
|
|
668
716
|
return this.config;
|
|
@@ -2076,7 +2124,7 @@ var WorkerSqliteTransport = class extends BaseTransport {
|
|
|
2076
2124
|
err: this.makeError(msg),
|
|
2077
2125
|
Category: "sp00ky-client::SqliteCacheEngine::worker"
|
|
2078
2126
|
}, "Worker error");
|
|
2079
|
-
this.
|
|
2127
|
+
this.close(msg);
|
|
2080
2128
|
};
|
|
2081
2129
|
this.worker.onerror = (e) => crash(e.message || "onerror");
|
|
2082
2130
|
this.worker.onmessageerror = () => crash("messageerror");
|
|
@@ -2229,6 +2277,8 @@ var SqliteCacheEngine = class {
|
|
|
2229
2277
|
workerSelectConfigured;
|
|
2230
2278
|
events = createDatabaseEventSystem();
|
|
2231
2279
|
bucketId = "anon";
|
|
2280
|
+
/** Deadline for one worker round trip; see `localOpTimeoutMs`. */
|
|
2281
|
+
localOpTimeoutMs;
|
|
2232
2282
|
/** Durability of the local store, set on every open. A plain Set of callbacks
|
|
2233
2283
|
* rather than a `DatabaseEventSystem` event: this changes at most once per
|
|
2234
2284
|
* open, and the typed event map is about query traffic. */
|
|
@@ -2250,6 +2300,7 @@ var SqliteCacheEngine = class {
|
|
|
2250
2300
|
constructor(config, logger, opts = {}) {
|
|
2251
2301
|
this.config = config;
|
|
2252
2302
|
this.logger = logger;
|
|
2303
|
+
this.localOpTimeoutMs = Math.max(0, config.localOpTimeoutMs ?? DEFAULT_LOCAL_OP_TIMEOUT_MS);
|
|
2253
2304
|
this.useOpfs = opts.useOpfs ?? true;
|
|
2254
2305
|
this.workerSelect = opts.workerSelect ?? config.workerSelect ?? true;
|
|
2255
2306
|
this.workerSelectConfigured = this.workerSelect;
|
|
@@ -2390,7 +2441,8 @@ var SqliteCacheEngine = class {
|
|
|
2390
2441
|
this.inFlightCalls.delete(tracked);
|
|
2391
2442
|
settle();
|
|
2392
2443
|
};
|
|
2393
|
-
|
|
2444
|
+
const timeoutMs = this.localOpTimeoutMs;
|
|
2445
|
+
return withTimeout(this.transport.call(type, payload), timeoutMs, () => new LocalOpTimeoutError(type, timeoutMs)).then((v) => {
|
|
2394
2446
|
finish();
|
|
2395
2447
|
s.inFlight--;
|
|
2396
2448
|
const wt = v?.wt;
|
|
@@ -2402,6 +2454,14 @@ var SqliteCacheEngine = class {
|
|
|
2402
2454
|
}, (e) => {
|
|
2403
2455
|
finish();
|
|
2404
2456
|
s.inFlight--;
|
|
2457
|
+
if (e instanceof LocalOpTimeoutError) {
|
|
2458
|
+
s.timeouts = (s.timeouts ?? 0) + 1;
|
|
2459
|
+
this.logger.error({
|
|
2460
|
+
type,
|
|
2461
|
+
timeoutMs,
|
|
2462
|
+
Category: "sp00ky-client::SqliteCacheEngine::rawCall"
|
|
2463
|
+
}, "Local store op did not answer within its deadline; rejecting the caller");
|
|
2464
|
+
}
|
|
2405
2465
|
throw e;
|
|
2406
2466
|
});
|
|
2407
2467
|
}
|
|
@@ -3565,6 +3625,11 @@ var DataModule = class DataModule {
|
|
|
3565
3625
|
await this.processStreamUpdate(update);
|
|
3566
3626
|
return;
|
|
3567
3627
|
}
|
|
3628
|
+
this.queueStreamUpdate(update);
|
|
3629
|
+
}
|
|
3630
|
+
/** Coalesce `update` onto the query's trailing timer (see onStreamUpdate). */
|
|
3631
|
+
queueStreamUpdate(update) {
|
|
3632
|
+
const { queryHash } = update;
|
|
3568
3633
|
if (this.debounceTimers.has(queryHash)) clearTimeout(this.debounceTimers.get(queryHash));
|
|
3569
3634
|
this.pendingStreamUpdates.set(queryHash, update);
|
|
3570
3635
|
const timer = setTimeout(async () => {
|
|
@@ -3575,6 +3640,30 @@ var DataModule = class DataModule {
|
|
|
3575
3640
|
this.debounceTimers.set(queryHash, timer);
|
|
3576
3641
|
}
|
|
3577
3642
|
/**
|
|
3643
|
+
* Re-materialize + notify a query whose MEMBERSHIP changed without any row
|
|
3644
|
+
* needing to be fetched, i.e. without the SSP stream update that normally
|
|
3645
|
+
* carries the notify. That is every row this client wrote itself: the local
|
|
3646
|
+
* CREATE memoized it at `_00_rv = 1`, the server publishes it at 1, so the
|
|
3647
|
+
* sync engine rightly fetches nothing - and then nobody told the subscribers
|
|
3648
|
+
* that `remoteArray` now holds the id. The row appeared on reload only.
|
|
3649
|
+
*
|
|
3650
|
+
* Routed through the same per-query debounce as a real stream update, so it
|
|
3651
|
+
* cannot race one: a pending real update already materializes against the
|
|
3652
|
+
* current `remoteArray` and wins. The synthetic update re-uses the circuit's
|
|
3653
|
+
* last `localArray` and skips the persist/metrics that describe an ingest.
|
|
3654
|
+
*/
|
|
3655
|
+
scheduleRematerialize(queryHash) {
|
|
3656
|
+
if (this.debounceTimers.has(queryHash)) return;
|
|
3657
|
+
const queryState = this.activeQueries.get(queryHash);
|
|
3658
|
+
if (!queryState) return;
|
|
3659
|
+
this.queueStreamUpdate({
|
|
3660
|
+
queryHash,
|
|
3661
|
+
localArray: queryState.config.localArray ?? [],
|
|
3662
|
+
op: "UPDATE",
|
|
3663
|
+
synthetic: true
|
|
3664
|
+
});
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3578
3667
|
* Process a query's pending (debounced) stream update NOW instead of on the
|
|
3579
3668
|
* trailing edge. Called by the sync engine before it flips a query back to
|
|
3580
3669
|
* `idle`, so the status change never races ahead of the rows it fetched.
|
|
@@ -3679,10 +3768,14 @@ var DataModule = class DataModule {
|
|
|
3679
3768
|
pendingIdsAt = 0;
|
|
3680
3769
|
pendingIdsInflight = null;
|
|
3681
3770
|
static PENDING_IDS_TTL_MS = 250;
|
|
3771
|
+
pendingIdsGen = 0;
|
|
3772
|
+
static PENDING_IDS_MAX_REREADS = 3;
|
|
3682
3773
|
/** Drop the cached outbox ids. Cheap; call it on anything that could change
|
|
3683
3774
|
* `_00_pending_mutations`. */
|
|
3684
3775
|
invalidatePendingIds() {
|
|
3685
3776
|
this.pendingIds = null;
|
|
3777
|
+
this.pendingIdsGen++;
|
|
3778
|
+
this.pendingIdsInflight = null;
|
|
3686
3779
|
}
|
|
3687
3780
|
/**
|
|
3688
3781
|
* Grace period for a settled write. Long enough to cover an SSP round trip
|
|
@@ -3768,7 +3861,7 @@ var DataModule = class DataModule {
|
|
|
3768
3861
|
try {
|
|
3769
3862
|
const newRecords = await this.materializeRecords(queryState, localArray);
|
|
3770
3863
|
if (epoch !== this.local.epoch) return;
|
|
3771
|
-
queryState.config.localArray = localArray;
|
|
3864
|
+
if (!update.synthetic) queryState.config.localArray = localArray;
|
|
3772
3865
|
const prevJson = JSON.stringify(queryState.records);
|
|
3773
3866
|
const newJson = JSON.stringify(newRecords);
|
|
3774
3867
|
queryState.records = newRecords;
|
|
@@ -3777,7 +3870,7 @@ var DataModule = class DataModule {
|
|
|
3777
3870
|
queryState.updateCount++;
|
|
3778
3871
|
queryState.lastUpdatedAt = Date.now();
|
|
3779
3872
|
}
|
|
3780
|
-
await this.local.query(surql.seal(surql.updateSet("id", [
|
|
3873
|
+
if (!update.synthetic) await this.local.query(surql.seal(surql.updateSet("id", [
|
|
3781
3874
|
"localArray",
|
|
3782
3875
|
"rowCount",
|
|
3783
3876
|
"updateCount",
|
|
@@ -4113,18 +4206,27 @@ var DataModule = class DataModule {
|
|
|
4113
4206
|
writes: new Set(cached.writes),
|
|
4114
4207
|
deletes: new Set(cached.deletes)
|
|
4115
4208
|
};
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4209
|
+
let fresh = null;
|
|
4210
|
+
for (let attempt = 0; attempt < DataModule.PENDING_IDS_MAX_REREADS; attempt++) {
|
|
4211
|
+
const gen = this.pendingIdsGen;
|
|
4212
|
+
if (!this.pendingIdsInflight) {
|
|
4213
|
+
const read = this.readPendingRecordIds(gen).finally(() => {
|
|
4214
|
+
if (this.pendingIdsInflight === read) this.pendingIdsInflight = null;
|
|
4215
|
+
});
|
|
4216
|
+
this.pendingIdsInflight = read;
|
|
4217
|
+
}
|
|
4218
|
+
fresh = await this.pendingIdsInflight;
|
|
4219
|
+
if (gen === this.pendingIdsGen) break;
|
|
4220
|
+
}
|
|
4120
4221
|
return {
|
|
4121
4222
|
writes: new Set(fresh.writes),
|
|
4122
4223
|
deletes: new Set(fresh.deletes)
|
|
4123
4224
|
};
|
|
4124
4225
|
}
|
|
4125
4226
|
/** The uncached read. Also the reload path after an invalidation, so the ids
|
|
4126
|
-
* still survive a reload exactly as before.
|
|
4127
|
-
|
|
4227
|
+
* still survive a reload exactly as before. `gen` is the generation the read
|
|
4228
|
+
* was issued under; the result is cached only if it is still current. */
|
|
4229
|
+
async readPendingRecordIds(gen) {
|
|
4128
4230
|
const writes = /* @__PURE__ */ new Set();
|
|
4129
4231
|
const deletes = /* @__PURE__ */ new Set();
|
|
4130
4232
|
try {
|
|
@@ -4145,11 +4247,13 @@ var DataModule = class DataModule {
|
|
|
4145
4247
|
deletes
|
|
4146
4248
|
};
|
|
4147
4249
|
}
|
|
4148
|
-
this.
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4250
|
+
if (gen === this.pendingIdsGen) {
|
|
4251
|
+
this.pendingIds = {
|
|
4252
|
+
writes,
|
|
4253
|
+
deletes
|
|
4254
|
+
};
|
|
4255
|
+
this.pendingIdsAt = Date.now();
|
|
4256
|
+
}
|
|
4153
4257
|
return {
|
|
4154
4258
|
writes,
|
|
4155
4259
|
deletes
|
|
@@ -4454,13 +4558,22 @@ var DataModule = class DataModule {
|
|
|
4454
4558
|
data: params,
|
|
4455
4559
|
...prefixedParams
|
|
4456
4560
|
}));
|
|
4561
|
+
this.invalidatePendingIds();
|
|
4457
4562
|
const parsedRecord = parseParams(tableSchema.columns, target);
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4563
|
+
try {
|
|
4564
|
+
await this.cache.save({
|
|
4565
|
+
table: tableName,
|
|
4566
|
+
op: "CREATE",
|
|
4567
|
+
record: parsedRecord,
|
|
4568
|
+
version: 1
|
|
4569
|
+
}, true);
|
|
4570
|
+
} catch (err) {
|
|
4571
|
+
this.logger.error({
|
|
4572
|
+
err,
|
|
4573
|
+
id,
|
|
4574
|
+
Category: "sp00ky-client::DataModule::create"
|
|
4575
|
+
}, "SSP create-ingest failed; the row is written and queued, but views only see it once membership arrives");
|
|
4576
|
+
}
|
|
4464
4577
|
const mutationEvent = {
|
|
4465
4578
|
type: "create",
|
|
4466
4579
|
mutation_id: mutationId,
|
|
@@ -4469,7 +4582,6 @@ var DataModule = class DataModule {
|
|
|
4469
4582
|
record: target,
|
|
4470
4583
|
tableName
|
|
4471
4584
|
};
|
|
4472
|
-
this.invalidatePendingIds();
|
|
4473
4585
|
for (const callback of this.mutationCallbacks) callback([mutationEvent]);
|
|
4474
4586
|
this.logger.debug({
|
|
4475
4587
|
id,
|
|
@@ -4506,13 +4618,22 @@ var DataModule = class DataModule {
|
|
|
4506
4618
|
for (const key of Object.keys(data)) if (key in target) updatedFields[key] = target[key];
|
|
4507
4619
|
if ("_00_rv" in target) updatedFields._00_rv = target._00_rv;
|
|
4508
4620
|
this.replaceRecordInQueries(updatedFields);
|
|
4621
|
+
this.invalidatePendingIds();
|
|
4509
4622
|
const parsedRecord = parseParams(tableSchema.columns, target);
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4623
|
+
try {
|
|
4624
|
+
await this.cache.save({
|
|
4625
|
+
table,
|
|
4626
|
+
op: "UPDATE",
|
|
4627
|
+
record: parsedRecord,
|
|
4628
|
+
version: target._00_rv
|
|
4629
|
+
}, true);
|
|
4630
|
+
} catch (err) {
|
|
4631
|
+
this.logger.error({
|
|
4632
|
+
err,
|
|
4633
|
+
id,
|
|
4634
|
+
Category: "sp00ky-client::DataModule::update"
|
|
4635
|
+
}, "SSP update-ingest failed; the row is written and queued");
|
|
4636
|
+
}
|
|
4516
4637
|
const pushEventOptions = parseUpdateOptions(id, data, options);
|
|
4517
4638
|
const mutationEvent = {
|
|
4518
4639
|
type: "update",
|
|
@@ -4523,7 +4644,6 @@ var DataModule = class DataModule {
|
|
|
4523
4644
|
beforeRecord: beforeRecord || void 0,
|
|
4524
4645
|
options: pushEventOptions
|
|
4525
4646
|
};
|
|
4526
|
-
this.invalidatePendingIds();
|
|
4527
4647
|
for (const callback of this.mutationCallbacks) callback([mutationEvent]);
|
|
4528
4648
|
this.logger.debug({
|
|
4529
4649
|
id,
|
|
@@ -4546,6 +4666,7 @@ var DataModule = class DataModule {
|
|
|
4546
4666
|
id: rid,
|
|
4547
4667
|
mid: mutationId
|
|
4548
4668
|
}));
|
|
4669
|
+
this.invalidatePendingIds();
|
|
4549
4670
|
try {
|
|
4550
4671
|
await this.cache.delete(table, id, true, beforeRecord);
|
|
4551
4672
|
} catch (err) {
|
|
@@ -4561,7 +4682,6 @@ var DataModule = class DataModule {
|
|
|
4561
4682
|
mutation_id: mutationId,
|
|
4562
4683
|
record_id: rid
|
|
4563
4684
|
};
|
|
4564
|
-
this.invalidatePendingIds();
|
|
4565
4685
|
for (const callback of this.mutationCallbacks) callback([mutationEvent]);
|
|
4566
4686
|
this.logger.debug({
|
|
4567
4687
|
id,
|
|
@@ -6505,6 +6625,7 @@ var Sp00kySync = class Sp00kySync {
|
|
|
6505
6625
|
Category: "sp00ky-client::Sp00kySync::refetchListRefForQuery"
|
|
6506
6626
|
}, "syncQuery failed during poll");
|
|
6507
6627
|
}
|
|
6628
|
+
if (changed) this.dataModule.scheduleRematerialize(queryHash);
|
|
6508
6629
|
await this.syncSubqueryChildren(queryHash).catch((err) => {
|
|
6509
6630
|
this.logger.info({
|
|
6510
6631
|
err: err?.message ?? err,
|
|
@@ -6701,6 +6822,7 @@ var Sp00kySync = class Sp00kySync {
|
|
|
6701
6822
|
}
|
|
6702
6823
|
await this.runSyncForQuery(hash, diff);
|
|
6703
6824
|
if (diff.removed.length > 0 && diff.added.length === 0 && diff.updated.length === 0) await this.dataModule.notifyQuerySynced(hash);
|
|
6825
|
+
else if (diff.added.length === 0 && diff.updated.length === 0) this.dataModule.scheduleRematerialize(hash);
|
|
6704
6826
|
}
|
|
6705
6827
|
/**
|
|
6706
6828
|
* Handle a LIVE change to a SUBQUERY child edge (a `_00_list_ref` row with
|
|
@@ -7438,8 +7560,8 @@ function selfAllowlistedVariant(flag, userId) {
|
|
|
7438
7560
|
|
|
7439
7561
|
//#endregion
|
|
7440
7562
|
//#region src/modules/devtools/index.ts
|
|
7441
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
7442
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
7563
|
+
const CORE_VERSION = "0.0.1-canary.209";
|
|
7564
|
+
const WASM_VERSION = "0.0.1-canary.209";
|
|
7443
7565
|
const SURREAL_VERSION = "3.0.3";
|
|
7444
7566
|
var DevToolsService = class DevToolsService {
|
|
7445
7567
|
eventsHistory = [];
|
|
@@ -7450,6 +7572,10 @@ var DevToolsService = class DevToolsService {
|
|
|
7450
7572
|
static NOTIFY_MIN_INTERVAL_MS = 250;
|
|
7451
7573
|
notifyTimer = null;
|
|
7452
7574
|
lastNotifyAt = 0;
|
|
7575
|
+
/** How many ids a pushed state carries per view; the rest is on demand. */
|
|
7576
|
+
static STATE_IDS_CAP = 200;
|
|
7577
|
+
/** devtools numeric hash -> the query's `_00_query` id, for on-demand rows. */
|
|
7578
|
+
hashToQuery = /* @__PURE__ */ new Map();
|
|
7453
7579
|
/** Shared-tabs snapshot for the panel, wired by Sp00kyClient whenever the
|
|
7454
7580
|
* feature was REQUESTED (so an inactive/degraded tab still reports why). */
|
|
7455
7581
|
tabsInfoProvider = null;
|
|
@@ -7490,6 +7616,7 @@ var DevToolsService = class DevToolsService {
|
|
|
7490
7616
|
const type = e.data?.type;
|
|
7491
7617
|
if (type === "SP00KY_DEVTOOLS_CONNECT") {
|
|
7492
7618
|
this.enabled = true;
|
|
7619
|
+
this.refreshLocalTables();
|
|
7493
7620
|
this.notifyDevTools();
|
|
7494
7621
|
} else if (type === "SP00KY_DEVTOOLS_DISCONNECT") this.enabled = false;
|
|
7495
7622
|
});
|
|
@@ -7525,6 +7652,9 @@ var DevToolsService = class DevToolsService {
|
|
|
7525
7652
|
this.dataManager.getActiveQueries().forEach((q) => {
|
|
7526
7653
|
const queryHash = this.hashString(encodeRecordId(q.config.id));
|
|
7527
7654
|
const createdAt = q.config.lastActiveAt instanceof Date ? q.config.lastActiveAt.getTime() : new Date(q.config.lastActiveAt || Date.now()).getTime();
|
|
7655
|
+
this.hashToQuery.set(queryHash, q.config.id);
|
|
7656
|
+
const localArray = q.config.localArray ?? [];
|
|
7657
|
+
const remoteArray = q.config.remoteArray ?? [];
|
|
7528
7658
|
result.set(queryHash, {
|
|
7529
7659
|
queryHash,
|
|
7530
7660
|
status: "active",
|
|
@@ -7537,9 +7667,11 @@ var DevToolsService = class DevToolsService {
|
|
|
7537
7667
|
query: q.config.surql,
|
|
7538
7668
|
variables: q.config.params || {},
|
|
7539
7669
|
dataSize: q.records?.length || 0,
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7670
|
+
localCount: localArray.length,
|
|
7671
|
+
remoteCount: remoteArray.length,
|
|
7672
|
+
localIds: localArray.slice(0, DevToolsService.STATE_IDS_CAP).map(([id]) => id),
|
|
7673
|
+
remoteIds: remoteArray.slice(0, DevToolsService.STATE_IDS_CAP).map(([id]) => id),
|
|
7674
|
+
idsTruncated: localArray.length > DevToolsService.STATE_IDS_CAP || remoteArray.length > DevToolsService.STATE_IDS_CAP,
|
|
7543
7675
|
membershipKnown: q.config.membershipKnown === true,
|
|
7544
7676
|
remoteSeen: q.config.remoteSeen === true,
|
|
7545
7677
|
emptyReads: q.config.emptyReads ?? 0,
|
|
@@ -7569,23 +7701,32 @@ var DevToolsService = class DevToolsService {
|
|
|
7569
7701
|
const queryHash = this.hashString(payload.queryId.toString());
|
|
7570
7702
|
this.addEvent("QUERY_UPDATED", {
|
|
7571
7703
|
queryHash,
|
|
7572
|
-
|
|
7704
|
+
recordCount: Array.isArray(payload.records) ? payload.records.length : 0
|
|
7573
7705
|
});
|
|
7574
7706
|
this.notifyDevTools();
|
|
7575
7707
|
}
|
|
7576
7708
|
onStreamUpdate(update) {
|
|
7709
|
+
if (update.synthetic) return;
|
|
7577
7710
|
this.logger.debug({
|
|
7578
|
-
update,
|
|
7711
|
+
queryHash: update.queryHash,
|
|
7579
7712
|
Category: "sp00ky-client::DevToolsService::onStreamUpdate"
|
|
7580
7713
|
}, "StreamUpdate");
|
|
7581
|
-
this.addEvent("STREAM_UPDATE", {
|
|
7714
|
+
this.addEvent("STREAM_UPDATE", {
|
|
7715
|
+
queryHash: update.queryHash,
|
|
7716
|
+
op: update.op,
|
|
7717
|
+
localCount: update.localArray?.length ?? 0,
|
|
7718
|
+
materializationTimeMs: update.materializationTimeMs,
|
|
7719
|
+
storeApplyMs: update.storeApplyMs,
|
|
7720
|
+
circuitStepMs: update.circuitStepMs,
|
|
7721
|
+
transformMs: update.transformMs
|
|
7722
|
+
});
|
|
7582
7723
|
this.notifyDevTools();
|
|
7583
7724
|
}
|
|
7584
7725
|
onMutation(payload) {
|
|
7585
7726
|
payload.forEach((p) => {
|
|
7586
7727
|
this.addEvent("MUTATION_REQUEST_EXECUTION", { mutation: {
|
|
7587
|
-
type: "create",
|
|
7588
|
-
|
|
7728
|
+
type: p.type ?? "create",
|
|
7729
|
+
fields: "data" in p && p.data && typeof p.data === "object" ? Object.keys(p.data) : [],
|
|
7589
7730
|
selector: encodeRecordId(p.record_id)
|
|
7590
7731
|
} });
|
|
7591
7732
|
});
|
|
@@ -7630,7 +7771,7 @@ var DevToolsService = class DevToolsService {
|
|
|
7630
7771
|
*/
|
|
7631
7772
|
refreshLocalTables() {
|
|
7632
7773
|
if (this.localTablesFetching) return;
|
|
7633
|
-
if (Date.now() - this.localTablesAt <
|
|
7774
|
+
if (Date.now() - this.localTablesAt < 3e4) return;
|
|
7634
7775
|
this.localTablesFetching = true;
|
|
7635
7776
|
this.databaseService.query("INFO FOR DB").then((res) => {
|
|
7636
7777
|
const info = this.unwrapInfo(res);
|
|
@@ -7645,8 +7786,8 @@ var DevToolsService = class DevToolsService {
|
|
|
7645
7786
|
this.localTablesFetching = false;
|
|
7646
7787
|
});
|
|
7647
7788
|
}
|
|
7648
|
-
getState() {
|
|
7649
|
-
this.refreshLocalTables();
|
|
7789
|
+
getState(opts = {}) {
|
|
7790
|
+
if (opts.refreshTables) this.refreshLocalTables();
|
|
7650
7791
|
return this.serializeForDevTools({
|
|
7651
7792
|
eventsHistory: [...this.eventsHistory],
|
|
7652
7793
|
activeQueries: Object.fromEntries(this.getActiveQueries()),
|
|
@@ -7760,14 +7901,11 @@ var DevToolsService = class DevToolsService {
|
|
|
7760
7901
|
if (typeof window === "undefined") return;
|
|
7761
7902
|
if (this.notifyTimer !== null) return;
|
|
7762
7903
|
const waited = Date.now() - this.lastNotifyAt;
|
|
7763
|
-
|
|
7764
|
-
this.flushNotify();
|
|
7765
|
-
return;
|
|
7766
|
-
}
|
|
7904
|
+
const delay = Math.max(0, DevToolsService.NOTIFY_MIN_INTERVAL_MS - waited);
|
|
7767
7905
|
this.notifyTimer = setTimeout(() => {
|
|
7768
7906
|
this.notifyTimer = null;
|
|
7769
7907
|
if (this.enabled) this.flushNotify();
|
|
7770
|
-
},
|
|
7908
|
+
}, delay);
|
|
7771
7909
|
}
|
|
7772
7910
|
flushNotify() {
|
|
7773
7911
|
this.lastNotifyAt = Date.now();
|
|
@@ -7812,7 +7950,18 @@ var DevToolsService = class DevToolsService {
|
|
|
7812
7950
|
if (typeof window !== "undefined") {
|
|
7813
7951
|
window.__00__ = {
|
|
7814
7952
|
version: this.version,
|
|
7815
|
-
getState: () => this.getState(),
|
|
7953
|
+
getState: () => this.getState({ refreshTables: true }),
|
|
7954
|
+
getQueryRows: (queryHash) => {
|
|
7955
|
+
const id = this.hashToQuery.get(Number(queryHash));
|
|
7956
|
+
const q = id !== void 0 ? this.dataManager?.getQueryById(id) : void 0;
|
|
7957
|
+
if (!q) return null;
|
|
7958
|
+
return this.serializeForDevTools({
|
|
7959
|
+
queryHash: Number(queryHash),
|
|
7960
|
+
data: q.records,
|
|
7961
|
+
localArray: q.config.localArray,
|
|
7962
|
+
remoteArray: q.config.remoteArray
|
|
7963
|
+
});
|
|
7964
|
+
},
|
|
7816
7965
|
getFlags: () => this.flagsAdmin.getFlags(),
|
|
7817
7966
|
setFlagEnabled: (key, enabled) => this.flagsAdmin.setFlagEnabled(key, enabled),
|
|
7818
7967
|
setFlagUserVariant: (key, variant, remove, userId) => this.flagsAdmin.setFlagUserVariant(key, variant, remove, userId),
|
|
@@ -12423,7 +12572,7 @@ var Sp00kyClient = class {
|
|
|
12423
12572
|
return new TabsCoordinator({
|
|
12424
12573
|
tabId,
|
|
12425
12574
|
fingerprint: computeTabsFingerprint({
|
|
12426
|
-
coreVersion: "0.0.1-canary.
|
|
12575
|
+
coreVersion: "0.0.1-canary.209",
|
|
12427
12576
|
schemaHash: hash53(this.config.schemaSurql),
|
|
12428
12577
|
endpoint: this.config.database.endpoint ?? "",
|
|
12429
12578
|
namespace: this.config.database.namespace,
|
|
@@ -13109,4 +13258,4 @@ var Sp00kyClient = class {
|
|
|
13109
13258
|
};
|
|
13110
13259
|
|
|
13111
13260
|
//#endregion
|
|
13112
|
-
export { AppReleaseHandle, AppReleaseModule, AuthEventTypes, AuthService, BLURHASH_IMAGE_EXTENSIONS, BucketHandle, CURSOR_COLORS, CrdtField, CrdtManager, FeatureFlagHandle, FeatureFlagModule, MATERIALIZATION_SAMPLE_WINDOW, Sp00kyClient, blurhashSidecarPath, bucketContentToBlob, createAuthEventSystem, cursorColorFromName, decode as decodeBlurhash, encode as encodeBlurhash, encodeImageToBlurhash, fileToUint8Array, isBlurhashValid, isImagePath, semverGt, textToHtml };
|
|
13261
|
+
export { AppReleaseHandle, AppReleaseModule, AuthEventTypes, AuthService, BLURHASH_IMAGE_EXTENSIONS, BucketHandle, CURSOR_COLORS, CrdtField, CrdtManager, DEFAULT_LOCAL_OP_TIMEOUT_MS, FeatureFlagHandle, FeatureFlagModule, LocalOpTimeoutError, MATERIALIZATION_SAMPLE_WINDOW, Sp00kyClient, blurhashSidecarPath, bucketContentToBlob, createAuthEventSystem, cursorColorFromName, decode as decodeBlurhash, encode as encodeBlurhash, encodeImageToBlurhash, fileToUint8Array, isBlurhashValid, isImagePath, semverGt, textToHtml };
|