@spooky-sync/core 0.0.1-canary.33 → 0.0.1-canary.330
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/AGENTS.md +84 -0
- package/README.md +56 -16
- package/dist/index.d.ts +2071 -141
- package/dist/index.js +15917 -2499
- package/dist/live.d.ts +149 -0
- package/dist/live.js +739 -0
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.js +3 -3
- package/dist/pure.d.ts +373 -0
- package/dist/pure.js +24 -0
- package/dist/sqlite-open.js +392 -0
- package/dist/sqlite-worker.d.ts +1 -0
- package/dist/sqlite-worker.js +439 -0
- package/dist/sw.d.ts +207 -0
- package/dist/sw.js +393 -0
- package/dist/tabs-broker-worker.d.ts +8 -0
- package/dist/tabs-broker-worker.js +477 -0
- package/dist/types.d.ts +1844 -38
- package/dist/worker-edges.js +480 -0
- package/dist/worker-impersonation.js +60 -0
- package/dist/worker-ref-tables.d.ts +48 -0
- package/dist/worker-types.d.ts +112 -0
- package/dist/worker-types.js +73 -0
- package/package.json +28 -7
- package/scripts/check-broker-bundle.mjs +33 -0
- package/scripts/check-worker-entries.mjs +173 -0
- package/skills/sp00ky-core/SKILL.md +32 -7
- package/skills/sp00ky-core/references/config.md +7 -1
- package/src/boot/auth-flip.saga.test.ts +135 -0
- package/src/boot/auth-flip.saga.ts +77 -0
- package/src/boot/boot.saga.test.ts +203 -0
- package/src/boot/boot.saga.ts +146 -0
- package/src/boot/bucket-switch.saga.test.ts +187 -0
- package/src/boot/bucket-switch.saga.ts +133 -0
- package/src/boot/preload.saga.test.ts +80 -0
- package/src/boot/preload.saga.ts +40 -0
- package/src/bucket-blurhash.test.ts +148 -0
- package/src/build-globals.d.ts +12 -0
- package/src/client/bucket-handle.test.ts +117 -0
- package/src/client/bucket-handle.ts +227 -0
- package/src/client/router.test.ts +58 -0
- package/src/client/router.ts +100 -0
- package/src/client/runtime.test.ts +195 -0
- package/src/client/runtime.ts +296 -0
- package/src/client/services.test.ts +74 -0
- package/src/client/services.ts +434 -0
- package/src/client/sp00ky-client.test.ts +341 -0
- package/src/client/sp00ky-client.ts +625 -0
- package/src/events/events.test.ts +2 -1
- package/src/events/index.ts +3 -0
- package/src/index.ts +69 -1
- package/src/kernel/constants.test.ts +19 -0
- package/src/kernel/constants.ts +80 -0
- package/src/kernel/effects.test.ts +40 -0
- package/src/kernel/effects.ts +167 -0
- package/src/kernel/events.ts +57 -0
- package/src/kernel/interpreter.test.ts +116 -0
- package/src/kernel/interpreter.ts +148 -0
- package/src/kernel/saga.test.ts +89 -0
- package/src/kernel/saga.ts +73 -0
- package/src/live/index.test.ts +512 -0
- package/src/live/index.ts +1059 -0
- package/src/modules/app-release/index.test.ts +125 -0
- package/src/modules/app-release/index.ts +209 -0
- package/src/modules/auth/auth.local-first.test.ts +228 -0
- package/src/modules/auth/events/index.ts +9 -2
- package/src/modules/auth/impersonation-banner.ts +443 -0
- package/src/modules/auth/impersonation.integration.test.ts +205 -0
- package/src/modules/auth/impersonation.test.ts +377 -0
- package/src/modules/auth/impersonation.ts +179 -0
- package/src/modules/auth/index.ts +476 -22
- package/src/modules/crdt/acceptance.ts +75 -0
- package/src/modules/crdt/bytes.ts +50 -0
- package/src/modules/crdt/crdt-field.ts +125 -0
- package/src/modules/crdt/crdt.test.ts +469 -0
- package/src/modules/crdt/hash.ts +103 -0
- package/src/modules/crdt/index.ts +757 -0
- package/src/modules/crdt/loro-loader.ts +25 -0
- package/src/modules/crdt/mirror.ts +37 -0
- package/src/modules/devtools/flags.ts +349 -0
- package/src/modules/devtools/index.ts +746 -95
- package/src/modules/devtools/mutations.test.ts +92 -0
- package/src/modules/devtools/mutations.ts +160 -0
- package/src/modules/devtools/notify-throttle.test.ts +166 -0
- package/src/modules/devtools/state-shape.test.ts +263 -0
- package/src/modules/devtools/storage-info.test.ts +79 -0
- package/src/modules/devtools/storage-info.ts +168 -0
- package/src/modules/devtools/versions.test.ts +74 -0
- package/src/modules/devtools/versions.ts +110 -0
- package/src/modules/feature-flag/index.test.ts +298 -0
- package/src/modules/feature-flag/index.ts +316 -0
- package/src/modules/ref-tables.test.ts +93 -0
- package/src/modules/ref-tables.ts +91 -0
- package/src/modules/web-push/index.test.ts +824 -0
- package/src/modules/web-push/index.ts +1027 -0
- package/src/mutation/jobs.test.ts +25 -0
- package/src/mutation/jobs.ts +41 -0
- package/src/mutation/mutation-id.test.ts +37 -0
- package/src/mutation/mutation-id.ts +35 -0
- package/src/mutation/pending-writes.integration.test.ts +175 -0
- package/src/mutation/push.saga.test.ts +301 -0
- package/src/mutation/push.saga.ts +225 -0
- package/src/mutation/rows.test.ts +231 -0
- package/src/mutation/rows.ts +427 -0
- package/src/mutation/tray.saga.test.ts +63 -0
- package/src/mutation/tray.saga.ts +48 -0
- package/src/mutation/write.saga.test.ts +316 -0
- package/src/mutation/write.saga.ts +268 -0
- package/src/otel/index.ts +10 -7
- package/src/pure.ts +66 -0
- package/src/push/types.ts +174 -0
- package/src/query/anon-query-isolation.integration.test.ts +527 -0
- package/src/query/env.test.ts +23 -0
- package/src/query/env.ts +65 -0
- package/src/query/fetch.saga.test.ts +199 -0
- package/src/query/fetch.saga.ts +120 -0
- package/src/query/hash.test.ts +14 -0
- package/src/query/hash.ts +18 -0
- package/src/query/id-set-plan.test.ts +132 -0
- package/src/query/lifecycle.saga.test.ts +274 -0
- package/src/query/lifecycle.saga.ts +186 -0
- package/src/query/list-ref-live.integration.test.ts +203 -0
- package/src/query/materialize.saga.test.ts +115 -0
- package/src/query/materialize.saga.ts +84 -0
- package/src/query/materialize.test.ts +58 -0
- package/src/query/materialize.ts +57 -0
- package/src/query/membership.saga.test.ts +407 -0
- package/src/query/membership.saga.ts +282 -0
- package/src/query/membership.test.ts +118 -0
- package/src/query/membership.ts +201 -0
- package/src/query/register.saga.test.ts +311 -0
- package/src/query/register.saga.ts +235 -0
- package/src/query/render-set.test.ts +47 -0
- package/src/query/render-set.ts +71 -0
- package/src/query/sql.test.ts +118 -0
- package/src/query/sql.ts +157 -0
- package/src/query/window-query.test.ts +70 -0
- package/src/query/window-query.ts +208 -0
- package/src/services/blobs/blob-cache.test.ts +359 -0
- package/src/services/blobs/blob-cache.ts +603 -0
- package/src/services/blobs/blob-manifest.ts +227 -0
- package/src/services/blobs/blob-store.test.ts +77 -0
- package/src/services/blobs/blob-store.ts +359 -0
- package/src/services/blobs/blob.fixture.ts +90 -0
- package/src/services/blobs/index.ts +70 -0
- package/src/services/database/cache-engine.ts +209 -0
- package/src/services/database/connection-supervisor.test.ts +318 -0
- package/src/services/database/connection-supervisor.ts +422 -0
- package/src/services/database/database.query-timeout.test.ts +140 -0
- package/src/services/database/database.responses.test.ts +43 -0
- package/src/services/database/database.ts +156 -61
- package/src/services/database/engine-factory.ts +33 -0
- package/src/services/database/errors.ts +34 -0
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/index.ts +7 -0
- package/src/services/database/landed-body-clear.integration.test.ts +236 -0
- package/src/services/database/local-migrator.integration.test.ts +110 -0
- package/src/services/database/local-migrator.ts +61 -16
- package/src/services/database/local.test.ts +150 -0
- package/src/services/database/local.ts +508 -63
- package/src/services/database/plan-render.test.ts +159 -0
- package/src/services/database/plan-render.ts +108 -0
- package/src/services/database/relation-resolver.test.ts +484 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/remote.ts +279 -6
- package/src/services/database/sqlite-cache-engine.test.ts +628 -0
- package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
- package/src/services/database/sqlite-cache-engine.ts +1401 -0
- package/src/services/database/sqlite-column-reset.integration.test.ts +100 -0
- package/src/services/database/sqlite-column-reset.ts +91 -0
- package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
- package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
- 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-open.test.ts +150 -0
- package/src/services/database/sqlite-open.ts +164 -0
- package/src/services/database/sqlite-plan-sql.test.ts +164 -0
- package/src/services/database/sqlite-plan-sql.ts +196 -0
- package/src/services/database/sqlite-projection.test.ts +121 -0
- package/src/services/database/sqlite-select.integration.test.ts +185 -0
- package/src/services/database/sqlite-select.test.ts +261 -0
- package/src/services/database/sqlite-select.ts +131 -0
- package/src/services/database/sqlite-transport.fixture.ts +30 -0
- package/src/services/database/sqlite-transport.ts +224 -0
- package/src/services/database/sqlite-worker.ts +437 -0
- package/src/services/database/surql-translate.ts +417 -0
- package/src/services/database/surreal-cache-engine.ts +163 -0
- package/src/services/logger/index.test.ts +132 -0
- package/src/services/logger/index.ts +189 -7
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +10 -3
- package/src/services/persistence/surrealdb.ts +4 -4
- package/src/services/stream-processor/index.ts +806 -67
- package/src/services/stream-processor/permissions.test.ts +47 -0
- package/src/services/stream-processor/permissions.ts +53 -0
- package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
- package/src/services/stream-processor/stream-processor.prime.test.ts +234 -0
- package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +58 -2
- package/src/services/tabs/broker-client.ts +284 -0
- package/src/services/tabs/broker.test.ts +381 -0
- package/src/services/tabs/coordinator.test.ts +386 -0
- package/src/services/tabs/coordinator.ts +646 -0
- package/src/services/tabs/fake-ports.fixture.ts +112 -0
- package/src/services/tabs/leader-locks.ts +75 -0
- package/src/services/tabs/protocol.ts +262 -0
- package/src/services/tabs/support.ts +36 -0
- package/src/services/tabs/tabs-broker-worker.ts +652 -0
- package/src/sp00ky.ts +5 -395
- package/src/state/client-state.ts +208 -0
- package/src/state/lifecycle.test.ts +141 -0
- package/src/state/lifecycle.ts +99 -0
- package/src/state/reducers.test.ts +306 -0
- package/src/state/reducers.ts +531 -0
- package/src/state/selectors.test.ts +188 -0
- package/src/state/selectors.ts +258 -0
- package/src/sw/index.test.ts +582 -0
- package/src/sw/index.ts +607 -0
- package/src/sw/store.ts +96 -0
- package/src/sync/connection.saga.test.ts +97 -0
- package/src/sync/connection.saga.ts +89 -0
- package/src/sync/live.saga.test.ts +178 -0
- package/src/sync/live.saga.ts +107 -0
- package/src/sync/policy.health.test.ts +56 -0
- package/src/sync/policy.test.ts +663 -0
- package/src/sync/policy.ts +515 -0
- package/src/sync/poll.saga.test.ts +51 -0
- package/src/sync/poll.saga.ts +36 -0
- package/src/sync/tabs.saga.test.ts +78 -0
- package/src/sync/tabs.saga.ts +96 -0
- package/src/testing/adapters.ts +137 -0
- package/src/testing/build.ts +63 -0
- package/src/testing/fake-services.ts +63 -0
- package/src/testing/run-pure.test.ts +92 -0
- package/src/testing/run-pure.ts +128 -0
- package/src/testing/services.ts +17 -0
- package/src/types.ts +625 -10
- package/src/utils/blurhash.ts +90 -0
- package/src/utils/edges.ts +39 -0
- package/src/utils/error-classification.test.ts +54 -0
- package/src/utils/error-classification.ts +25 -0
- package/src/utils/index.ts +89 -12
- package/src/utils/parser.test.ts +67 -102
- package/src/utils/parser.ts +64 -2
- package/src/utils/semver.test.ts +32 -0
- package/src/utils/semver.ts +30 -0
- package/src/utils/sha256.ts +15 -0
- package/src/utils/surql.ts +29 -18
- package/src/utils/utils.test.ts +3 -2
- package/tsdown.config.ts +117 -3
- package/vitest.config.ts +48 -0
- package/src/modules/cache/index.ts +0 -241
- package/src/modules/cache/types.ts +0 -19
- package/src/modules/data/data.test.ts +0 -58
- package/src/modules/data/index.ts +0 -879
- package/src/modules/sync/engine.ts +0 -173
- package/src/modules/sync/events/index.ts +0 -77
- package/src/modules/sync/index.ts +0 -3
- package/src/modules/sync/queue/index.ts +0 -2
- package/src/modules/sync/queue/queue-down.ts +0 -89
- package/src/modules/sync/queue/queue-up.ts +0 -223
- package/src/modules/sync/scheduler.ts +0 -84
- package/src/modules/sync/sync.ts +0 -421
- package/src/modules/sync/utils.test.ts +0 -311
- package/src/modules/sync/utils.ts +0 -167
package/dist/index.d.ts
CHANGED
|
@@ -1,74 +1,218 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as WebPushSubscribeOptions, A as RunOptions, At as Logger$1, B as SyncHealthStatus, Bt as SESSION_POLL_MS, C as QueryTimeToLive, Ct as SignOutHook, D as RecordVersionArray, Dt as createAuthEventSystem, E as ReconnectConfig, Et as AuthEventTypes, F as StorageHealth, Ft as IMPERSONATION_ACCESS, G as WebPushConfig, Gt as sameImpersonation, H as UpdateOptions, Ht as decodeTokenClaims, I as StorageHealthStatus, It as ImpersonationInfo, J as WebPushErrorCode, K as WebPushDevice, L as StoreType, Lt as ImpersonationTimerHost, M as Sp00kyConfig, Mt as SealedQuery, N as Sp00kyQueryResult, Nt as EventDefinition, O as RecordVersionDiff, Ot as RemoteDatabaseService, P as Sp00kyQueryResultPromise, Pt as EventSystem, Q as WebPushModule, R as SyncHealth, Rt as ImpersonationTimers, S as QueryStatusCallback, St as SignOutContext, T as QueryUpdateCallback, Tt as AuthEventTypeMap, U as PUSH_ENDPOINT_KEY, Ut as impersonationFromToken, V as TimingPhase, Vt as TokenClaims, W as PushDeviceKind, Wt as renewDelayMs, X as WebPushMessage, Y as WebPushInfo, Z as WebPushMessageInput, _ as QueryConfig, _t as isPushPayload, a as Level, at as WebPushUpdateOptions, b as QueryState, bt as ImpersonationCandidate, c as MutationCallback, ct as BridgeTokenMessage, d as PersistenceClient, dt as PushNotificationAction, et as WebPushSupport, f as PhaseStat, ft as PushOp, g as QueryAuthorityCallback, gt as WebPushClientMessage, h as PreloadRefresh, ht as PushUrgency, i as InlineRow, it as WebPushUnsupportedReason, j as ServerViewMeta, jt as LocalStore, k as RegistrationTimings, kt as LogTap, l as MutationEvent, lt as PUSH_PAYLOAD_VERSION, m as PreloadOptions, mt as PushPayloadKind, n as DebounceOptions, nt as WebPushSyncStatus, o as MATERIALIZATION_SAMPLE_WINDOW, ot as detectWebPushSupport, p as PinoTransmit, pt as PushPayload, q as WebPushError, r as EventSubscriptionOptions, rt as WebPushUnsubscribeOptions, s as MembershipOutcome, st as BridgeSignOutMessage, t as ConnectionState, tt as WebPushSyncOptions, u as MutationEventType, ut as PushNotification, v as QueryConfigRecord, vt as ActiveImpersonation, w as QueryTimings, wt as AuthEventSystem, x as QueryStatus, xt as SIGN_OUT_HOOK_TIMEOUT_MS, y as QueryHash, yt as AuthService, z as SyncHealthConfig, zt as ORIGIN_TOKEN_KEY } from "./types.js";
|
|
2
|
+
import { BackendNames, BackendRoutes, BucketNames, FinalQuery, GetTable, QueryBuilder, QueryOptions, QueryPlan, RecordId, RoutePayload, SchemaStructure, TableModel, TableNames } from "@spooky-sync/query-builder";
|
|
2
3
|
import * as surrealdb0 from "surrealdb";
|
|
3
|
-
import { Duration, RecordId, Surreal
|
|
4
|
-
import { AccessDefinition, BackendNames, BackendRoutes, BucketNames, ColumnSchema, GetTable, QueryBuilder, QueryOptions, RoutePayload, SchemaStructure, TableModel, TableNames, TypeNameToTypeMap } from "@spooky-sync/query-builder";
|
|
4
|
+
import { Duration, RecordId as RecordId$1, Surreal } from "surrealdb";
|
|
5
5
|
import { Logger } from "pino";
|
|
6
|
+
import { decode, encode, isBlurhashValid } from "blurhash";
|
|
7
|
+
import { LoroDoc } from "loro-crdt";
|
|
6
8
|
|
|
7
|
-
//#region src/services/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
//#region src/services/tabs/protocol.d.ts
|
|
10
|
+
|
|
11
|
+
type TabId = string;
|
|
12
|
+
type TabRole$1 = 'solo' | 'leader' | 'follower';
|
|
13
|
+
/** Broker pings every tab at this cadence. */
|
|
14
|
+
|
|
15
|
+
/** Matches `CacheIngestTuple` (modules/cache): exactly what `ingestMany`
|
|
16
|
+
* consumes, so relayed batches feed follower circuits without reshaping. */
|
|
17
|
+
interface IngestTuple {
|
|
18
|
+
table: string;
|
|
19
|
+
op: 'CREATE' | 'UPDATE' | 'DELETE';
|
|
20
|
+
id: string;
|
|
21
|
+
record: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
type FollowerToLeaderMessage = {
|
|
24
|
+
type: 'sync-hello';
|
|
25
|
+
tabId: TabId;
|
|
26
|
+
}
|
|
27
|
+
/** The follower committed an outbox row (through the shared store) and the
|
|
28
|
+
* leader should drain it. Idempotent; a new leader's loadFromDatabase is
|
|
29
|
+
* the backstop for a notify lost in a failover window. */ | {
|
|
30
|
+
type: 'mutation-enqueued';
|
|
31
|
+
mutationId: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'request-poll';
|
|
34
|
+
}
|
|
35
|
+
/** An optimistic write this follower committed to the SHARED store and
|
|
36
|
+
* ingested into its own circuit. The leader ingests it (no DB write, the
|
|
37
|
+
* row is already there) and fans it out to every OTHER follower as
|
|
38
|
+
* `ingest-relay`, so a follower's write lands in every tab in one hop
|
|
39
|
+
* instead of after the server round-trip. */ | {
|
|
40
|
+
type: 'ingest';
|
|
41
|
+
tuples: IngestTuple[];
|
|
23
42
|
};
|
|
24
|
-
type
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
readonly extract: (results: unknown[]) => T;
|
|
43
|
+
type LeaderToFollowerMessage = {
|
|
44
|
+
type: 'db-ready';
|
|
45
|
+
leadershipId: number;
|
|
46
|
+
bucketId: string;
|
|
47
|
+
storageHealth: StorageHealth;
|
|
30
48
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*/
|
|
47
|
-
query<T extends unknown[]>(query: string, vars?: Record<string, unknown>): Promise<T>;
|
|
48
|
-
execute<T>(query: SealedQuery<T>, vars?: Record<string, unknown>): Promise<T>;
|
|
49
|
-
close(): Promise<void>;
|
|
49
|
+
/** Every ingest the leader's CacheModule committed, so follower circuits
|
|
50
|
+
* stay live without their own fetch. seq detects gaps. */ | {
|
|
51
|
+
type: 'ingest-relay';
|
|
52
|
+
tuples: IngestTuple[];
|
|
53
|
+
leadershipId: number;
|
|
54
|
+
seq: number;
|
|
55
|
+
}
|
|
56
|
+
/** A `_00_list_ref` LIVE event, relayed verbatim. Each follower resolves the
|
|
57
|
+
* queryId against its own DataModule and ignores foreign queries. */ | {
|
|
58
|
+
type: 'list-ref-change';
|
|
59
|
+
action: 'CREATE' | 'UPDATE' | 'DELETE';
|
|
60
|
+
queryId: string;
|
|
61
|
+
recordId: string;
|
|
62
|
+
version: number;
|
|
63
|
+
parent: boolean;
|
|
50
64
|
}
|
|
65
|
+
/** The leader's drain rolled back a mutation owned by this tab. */ | {
|
|
66
|
+
type: 'mutation-rolled-back';
|
|
67
|
+
mutationId: string;
|
|
68
|
+
recordId: string;
|
|
69
|
+
eventType: 'create' | 'update' | 'delete';
|
|
70
|
+
error: string;
|
|
71
|
+
}
|
|
72
|
+
/** The leader's drain pushed a mutation and deleted its outbox row from the
|
|
73
|
+
* SHARED store. Every follower keeps the row in its overlay until its
|
|
74
|
+
* `_00_list_ref` membership arrives. */ | {
|
|
75
|
+
type: 'mutation-settled';
|
|
76
|
+
mutationId: string;
|
|
77
|
+
recordId: string;
|
|
78
|
+
eventType: 'create' | 'update' | 'delete';
|
|
79
|
+
}
|
|
80
|
+
/** The leader's LIVE saw edges of these queries change; followers re-read
|
|
81
|
+
* membership for the ones they hold. */ | {
|
|
82
|
+
type: 'membership-dirty';
|
|
83
|
+
hashes: string[];
|
|
84
|
+
}
|
|
85
|
+
/** The failed-writes tray changed size on the shared store. */ | {
|
|
86
|
+
type: 'failed-mutations-changed';
|
|
87
|
+
count: number;
|
|
88
|
+
};
|
|
51
89
|
//#endregion
|
|
52
|
-
//#region src/services/database/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
90
|
+
//#region src/services/database/connection-supervisor.d.ts
|
|
91
|
+
/**
|
|
92
|
+
* Keeps the remote WebSocket alive for the whole life of the page.
|
|
93
|
+
*
|
|
94
|
+
* The SurrealDB SDK reconnects on its own after a socket `close`, but that
|
|
95
|
+
* covers only one of three ways the connection dies:
|
|
96
|
+
*
|
|
97
|
+
* 1. **Socket closes, SDK recovers.** Handled entirely by the SDK. This
|
|
98
|
+
* supervisor only observes it (to report `reconnecting` upward).
|
|
99
|
+
* 2. **Socket closes, SDK gives up.** With `attempts: -1` this shouldn't happen
|
|
100
|
+
* from exhaustion — but the SDK also terminates the engine permanently when
|
|
101
|
+
* its post-reconnect handshake throws (it re-runs `version()`, `use()`,
|
|
102
|
+
* `authenticate()` on every reconnect and closes the engine on any error).
|
|
103
|
+
* One transient hiccup there would otherwise kill the page's connection for
|
|
104
|
+
* good. The revive loop re-opens from scratch.
|
|
105
|
+
* 3. **Socket never closes at all.** A half-open connection: the peer is gone
|
|
106
|
+
* (NAT timeout, wifi switch, laptop sleep) but no FIN ever arrives, so
|
|
107
|
+
* `readyState` stays OPEN and the SDK's own 30s ping — fire-and-forget, no
|
|
108
|
+
* response deadline — never notices. Nothing ever fires a `close` event, so
|
|
109
|
+
* nothing ever triggers a reconnect. The heartbeat detects this and forces
|
|
110
|
+
* the teardown that case 2's loop then repairs.
|
|
111
|
+
*
|
|
112
|
+
* Plus wake triggers: coming back `online` or un-hiding the tab probes
|
|
113
|
+
* immediately rather than waiting out a backoff that was scheduled while the
|
|
114
|
+
* network was known-down.
|
|
115
|
+
*/
|
|
116
|
+
declare class ConnectionSupervisor {
|
|
117
|
+
private readonly remote;
|
|
118
|
+
private readonly logger;
|
|
119
|
+
private readonly config;
|
|
120
|
+
private state;
|
|
121
|
+
private subscribers;
|
|
122
|
+
private started;
|
|
123
|
+
private disposed;
|
|
124
|
+
private heartbeatTimer;
|
|
125
|
+
private heartbeatInFlight;
|
|
126
|
+
/** Consecutive failed probes. See {@link FAILURES_BEFORE_TEARDOWN}. */
|
|
127
|
+
private heartbeatFailures;
|
|
128
|
+
private reviveTimer;
|
|
129
|
+
private reviveAttempts;
|
|
130
|
+
/** Timestamp of the last wake-triggered probe, for rate limiting. */
|
|
131
|
+
private lastWakeProbeAt;
|
|
132
|
+
private reviving;
|
|
133
|
+
/**
|
|
134
|
+
* Set while the browser reports itself offline. Retrying a socket against a
|
|
135
|
+
* down interface only burns backoff, so the loop parks until `online` fires.
|
|
136
|
+
*/
|
|
137
|
+
private suspended;
|
|
138
|
+
private teardown;
|
|
139
|
+
private static readonly REVIVE_BASE_MS;
|
|
140
|
+
/**
|
|
141
|
+
* How many consecutive heartbeat failures it takes to tear the socket down.
|
|
142
|
+
*
|
|
143
|
+
* The probe rides the same serialized queue as every other RPC (deliberately
|
|
144
|
+
* — see {@link beat}), which means it cannot distinguish a WEDGED queue from
|
|
145
|
+
* a merely BUSY one. A single slow window (a large sync burst, one heavy
|
|
146
|
+
* app query) used to be enough to force-close a perfectly healthy socket,
|
|
147
|
+
* and the resulting reconnect re-registered every active query about a
|
|
148
|
+
* second later. That self-inflicted teardown manufactured the very reconnect
|
|
149
|
+
* storms this class exists to survive. A genuinely dead socket still fails
|
|
150
|
+
* every probe, so it is torn down one interval later than before.
|
|
151
|
+
*/
|
|
152
|
+
private static readonly FAILURES_BEFORE_TEARDOWN;
|
|
153
|
+
/** Retry delay after an inconclusive (first) heartbeat failure. */
|
|
154
|
+
private static readonly HEARTBEAT_RETRY_MS;
|
|
155
|
+
/** Floor between probes triggered by wake events (tab focus, pageshow). */
|
|
156
|
+
private static readonly WAKE_PROBE_MIN_INTERVAL_MS;
|
|
157
|
+
constructor(remote: RemoteDatabaseService, logger: Logger$1, config?: Required<ReconnectConfig>);
|
|
158
|
+
/** Latest observed transport state. */
|
|
159
|
+
get connection(): ConnectionState;
|
|
160
|
+
/**
|
|
161
|
+
* Observe transport state. Fires immediately with the current value and again
|
|
162
|
+
* on every change. Returns an unsubscribe.
|
|
163
|
+
*/
|
|
164
|
+
subscribe(cb: (state: ConnectionState) => void): () => void;
|
|
165
|
+
/**
|
|
166
|
+
* Begin supervising. Call once, after the initial {@link
|
|
167
|
+
* RemoteDatabaseService.connect}. Idempotent.
|
|
168
|
+
*/
|
|
169
|
+
start(): void;
|
|
170
|
+
/** Stop all timers and listeners. Safe to call more than once. */
|
|
171
|
+
dispose(): void;
|
|
172
|
+
private setState;
|
|
173
|
+
private clearReviveTimer;
|
|
174
|
+
/**
|
|
175
|
+
* Queue the next `connect()` attempt on exponential backoff, capped at
|
|
176
|
+
* `superviseRetryDelayMaxMs`. Never gives up — the page is expected to
|
|
177
|
+
* outlive any outage.
|
|
178
|
+
*/
|
|
179
|
+
private scheduleRevive;
|
|
180
|
+
private revive;
|
|
181
|
+
private stopHeartbeat;
|
|
182
|
+
private startHeartbeat;
|
|
183
|
+
/**
|
|
184
|
+
* Probe the server end-to-end. Deliberately goes through
|
|
185
|
+
* `remote.query` — the same serialized queue every other remote call uses —
|
|
186
|
+
* so a queue wedged behind a stuck RPC also fails the heartbeat instead of
|
|
187
|
+
* being invisible to it.
|
|
188
|
+
*/
|
|
189
|
+
private beat;
|
|
190
|
+
/**
|
|
191
|
+
* A restored network or an un-hidden tab is the strongest available hint that
|
|
192
|
+
* a reconnect will now succeed, so probe immediately instead of waiting out a
|
|
193
|
+
* backoff scheduled under worse conditions.
|
|
194
|
+
*/
|
|
195
|
+
private installWakeTriggers;
|
|
196
|
+
/**
|
|
197
|
+
* Reset the backoff and act on whichever problem is present: reconnect if the
|
|
198
|
+
* socket is gone, otherwise probe it (it may be half-open — which is exactly
|
|
199
|
+
* what a sleep/wake cycle produces).
|
|
200
|
+
*/
|
|
201
|
+
private wake;
|
|
59
202
|
}
|
|
60
203
|
//#endregion
|
|
61
|
-
//#region src/services/database/
|
|
62
|
-
declare class
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
constructor(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
204
|
+
//#region src/services/database/local-migrator.d.ts
|
|
205
|
+
declare class LocalMigrator {
|
|
206
|
+
private localDb;
|
|
207
|
+
private logger;
|
|
208
|
+
constructor(localDb: LocalStore, logger: Logger$1);
|
|
209
|
+
provision(schemaSurql: string): Promise<void>;
|
|
210
|
+
private readKeptRows;
|
|
211
|
+
private restoreKeptRows;
|
|
212
|
+
private isSchemaUpToDate;
|
|
213
|
+
private recreateDatabase;
|
|
214
|
+
private splitStatements;
|
|
215
|
+
private createHashRecord;
|
|
72
216
|
}
|
|
73
217
|
//#endregion
|
|
74
218
|
//#region src/services/stream-processor/wasm-types.d.ts
|
|
@@ -76,6 +220,12 @@ interface WasmStreamUpdate {
|
|
|
76
220
|
query_id: string;
|
|
77
221
|
result_hash: string;
|
|
78
222
|
result_data: RecordVersionArray;
|
|
223
|
+
timing_store_apply_ms?: number;
|
|
224
|
+
timing_circuit_step_ms?: number;
|
|
225
|
+
timing_transform_ms?: number;
|
|
226
|
+
timing_parse_ms?: number;
|
|
227
|
+
timing_plan_ms?: number;
|
|
228
|
+
timing_snapshot_ms?: number;
|
|
79
229
|
}
|
|
80
230
|
//#endregion
|
|
81
231
|
//#region src/services/stream-processor/index.d.ts
|
|
@@ -96,6 +246,33 @@ interface StreamUpdate {
|
|
|
96
246
|
queryHash: string;
|
|
97
247
|
localArray: RecordVersionArray;
|
|
98
248
|
op?: 'CREATE' | 'UPDATE' | 'DELETE';
|
|
249
|
+
/**
|
|
250
|
+
* Client-internal: not from the circuit. A membership-only change that
|
|
251
|
+
* needed no fetch is re-materialized through this same path so it cannot
|
|
252
|
+
* race a real update (DataModule.scheduleRematerialize). Carries the last
|
|
253
|
+
* known `localArray`; consumers that describe an INGEST (persist, metrics,
|
|
254
|
+
* devtools events) skip it.
|
|
255
|
+
*/
|
|
256
|
+
synthetic?: boolean;
|
|
257
|
+
/**
|
|
258
|
+
* End-to-end ingest latency for the WASM call that produced this update,
|
|
259
|
+
* in milliseconds. Populated by StreamProcessorService.ingest. Undefined
|
|
260
|
+
* for the initial register_view snapshot.
|
|
261
|
+
*/
|
|
262
|
+
materializationTimeMs?: number;
|
|
263
|
+
/** SSP internal sub-phase timings (ms) for this ingest, from the WASM binding. */
|
|
264
|
+
storeApplyMs?: number;
|
|
265
|
+
circuitStepMs?: number;
|
|
266
|
+
transformMs?: number;
|
|
267
|
+
/**
|
|
268
|
+
* One-shot registration timings (ms). Only set on the StreamUpdate returned
|
|
269
|
+
* by `registerQueryPlan` (the register_view snapshot), not on ingest updates.
|
|
270
|
+
*/
|
|
271
|
+
registration?: {
|
|
272
|
+
parseMs: number;
|
|
273
|
+
planMs: number;
|
|
274
|
+
snapshotMs: number;
|
|
275
|
+
};
|
|
99
276
|
}
|
|
100
277
|
type StreamProcessorEvents = {
|
|
101
278
|
stream_update: EventDefinition<'stream_update', StreamUpdate[]>;
|
|
@@ -107,34 +284,208 @@ type StreamProcessorEvents = {
|
|
|
107
284
|
interface StreamUpdateReceiver {
|
|
108
285
|
onStreamUpdate(update: StreamUpdate): void;
|
|
109
286
|
}
|
|
287
|
+
/** One row change in the shape `ingestMany` consumes. */
|
|
288
|
+
interface IngestRecord {
|
|
289
|
+
table: string;
|
|
290
|
+
/** `MERGE` overlays the given fields on the stored row (projection widening). */
|
|
291
|
+
op: 'CREATE' | 'UPDATE' | 'DELETE' | 'MERGE';
|
|
292
|
+
id: string;
|
|
293
|
+
record: any;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* What the boot-time prime needs from the client: which tables to walk, how
|
|
297
|
+
* to recognise a snapshot written under a different schema, and which rows'
|
|
298
|
+
* local `_00_rv` must not be reported as the server's.
|
|
299
|
+
*/
|
|
300
|
+
interface CircuitPrimeContext {
|
|
301
|
+
tables: string[];
|
|
302
|
+
schemaHash: string;
|
|
303
|
+
/** Encoded ids with an unsettled local mutation (their `_00_rv` was bumped
|
|
304
|
+
* locally and may exceed the server's next version). */
|
|
305
|
+
pendingIds: Set<string>;
|
|
306
|
+
/** Each table's client column set (`columnFingerprints`). An engine that
|
|
307
|
+
* keeps bodies across schema changes resets the versions of a table whose
|
|
308
|
+
* set changed before they are read, so its rows are fetched again whole. */
|
|
309
|
+
columnFingerprints?: Record<string, string>;
|
|
310
|
+
/** Receives every `(id, rv)` the prime put into the circuit, per table, so
|
|
311
|
+
* the sync layer can skip re-downloading bodies it already has. */
|
|
312
|
+
onVersions?: (table: string, entries: [string, number][]) => void;
|
|
313
|
+
}
|
|
314
|
+
/** Storage key of the circuit snapshot inside the local store. */
|
|
315
|
+
|
|
110
316
|
declare class StreamProcessorService {
|
|
111
317
|
events: EventSystem<StreamProcessorEvents>;
|
|
112
318
|
private db;
|
|
113
|
-
private persistenceClient;
|
|
114
319
|
private logger;
|
|
115
320
|
private processor;
|
|
116
321
|
private isInitialized;
|
|
117
322
|
private receivers;
|
|
118
|
-
|
|
323
|
+
private batching;
|
|
324
|
+
private batchBuffer;
|
|
325
|
+
private sessionAuth;
|
|
326
|
+
private stateGeneration;
|
|
327
|
+
private persistState;
|
|
328
|
+
private persistCircuit;
|
|
329
|
+
private checkpointMs;
|
|
330
|
+
private checkpointTimer;
|
|
331
|
+
private snapshotDirty;
|
|
332
|
+
private dirtyRows;
|
|
333
|
+
private hideHandler;
|
|
334
|
+
private checkpointInFlight;
|
|
335
|
+
private projection;
|
|
336
|
+
private primed;
|
|
337
|
+
private schemaHash;
|
|
338
|
+
private widenQueue;
|
|
339
|
+
private widenPending;
|
|
340
|
+
constructor(events: EventSystem<StreamProcessorEvents>, db: LocalStore, logger: Logger);
|
|
119
341
|
/**
|
|
120
342
|
* Add a receiver for stream updates.
|
|
121
343
|
* Multiple receivers can be registered (DataManager, DevTools, etc.)
|
|
122
344
|
*/
|
|
123
345
|
addReceiver(receiver: StreamUpdateReceiver): void;
|
|
124
346
|
private notifyUpdates;
|
|
347
|
+
private dispatchUpdates;
|
|
348
|
+
/**
|
|
349
|
+
* Ingest a batch of record changes, firing one coalesced `StreamUpdate` per
|
|
350
|
+
* affected query once every record has been ingested. Use this whenever
|
|
351
|
+
* multiple records land at once (sync fetching N rows, the boot prime).
|
|
352
|
+
*
|
|
353
|
+
* The batch is fed to the wasm side in chunks of {@link INGEST_CHUNK}: one
|
|
354
|
+
* circuit step per chunk (a step walks every registered view, so per-record
|
|
355
|
+
* ingest paid that fixed cost N times), but never the whole batch at once,
|
|
356
|
+
* because the wasm side has to hold every parsed row of a call at the same
|
|
357
|
+
* time and wasm32 dlmalloc never returns that peak.
|
|
358
|
+
*
|
|
359
|
+
* Returns the records that were ingested. A chunk that fails is reported
|
|
360
|
+
* and skipped, not retried (a retry would double-apply whatever the failed
|
|
361
|
+
* step already committed), and the remaining chunks still run.
|
|
362
|
+
*/
|
|
363
|
+
ingestMany(records: IngestRecord[]): IngestRecord[];
|
|
364
|
+
/**
|
|
365
|
+
* Open a coalescing window. While open, the per-record stream updates
|
|
366
|
+
* emitted by `ingest` are buffered (one entry per queryHash) instead of
|
|
367
|
+
* dispatched. Always paired with `flushCoalescing()` in a try/finally by
|
|
368
|
+
* `ingestMany` so the window always closes — otherwise the processor stays
|
|
369
|
+
* stuck buffering forever.
|
|
370
|
+
*
|
|
371
|
+
* No-op if a window is already open (nested batches aren't expected here).
|
|
372
|
+
*/
|
|
373
|
+
private beginCoalescing;
|
|
374
|
+
/**
|
|
375
|
+
* Close the coalescing window and flush: dispatch one coalesced
|
|
376
|
+
* `StreamUpdate` per buffered queryHash, then persist processor state once
|
|
377
|
+
* for the whole batch (instead of once per ingest).
|
|
378
|
+
*/
|
|
379
|
+
private flushCoalescing;
|
|
125
380
|
/**
|
|
126
381
|
* Initialize the WASM module and processor.
|
|
127
382
|
* This must be called before using other methods.
|
|
128
383
|
*/
|
|
129
384
|
init(): Promise<void>;
|
|
130
|
-
|
|
131
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Drop the current WASM processor and start a fresh, empty circuit. Used on
|
|
387
|
+
* local-bucket switches: the old circuit holds the previous user's rows AND
|
|
388
|
+
* views registered with the previous `$auth` context, so neither may survive.
|
|
389
|
+
* Deliberately loads nothing: the snapshot in the store being swapped away
|
|
390
|
+
* from belongs to the previous bucket; the caller primes the new bucket's
|
|
391
|
+
* circuit (`primeFromLocal`) once its store is open, and the DataModule
|
|
392
|
+
* rebind re-registers every live view against this fresh processor. Caller
|
|
393
|
+
* must re-seed `setPermissions` afterwards (a fresh circuit default-denies
|
|
394
|
+
* every table).
|
|
395
|
+
*/
|
|
396
|
+
reset(): Promise<void>;
|
|
397
|
+
/**
|
|
398
|
+
* Release the wasm circuit and stop checkpointing. Call when the client is
|
|
399
|
+
* torn down; a recreated client (provider remount, HMR) would otherwise stack
|
|
400
|
+
* one full circuit per instance.
|
|
401
|
+
*/
|
|
402
|
+
dispose(): void;
|
|
403
|
+
/**
|
|
404
|
+
* Explicitly run the wasm-bindgen destructor. Guarded: stale wasm builds may
|
|
405
|
+
* not expose `free`, and a double free must not take the app down.
|
|
406
|
+
*/
|
|
407
|
+
private freeProcessor;
|
|
408
|
+
/** Toggle circuit-state persistence (shared-tabs follower/leader role). */
|
|
409
|
+
setPersistenceEnabled(enabled: boolean): void;
|
|
410
|
+
/**
|
|
411
|
+
* Snapshot persistence (`persistCircuit`). When on, the circuit's store is
|
|
412
|
+
* written to the local store on a checkpoint interval and when the page
|
|
413
|
+
* goes hidden, and restored by {@link primeFromLocal} on the next boot.
|
|
414
|
+
*/
|
|
415
|
+
configureCircuitPersistence(enabled: boolean, checkpointMs?: number): void;
|
|
416
|
+
/**
|
|
417
|
+
* Field projection (`circuitProjection`, default on). Takes effect on the
|
|
418
|
+
* next processor (`init`/`reset`) and on rows written after that.
|
|
419
|
+
*/
|
|
420
|
+
configureProjection(enabled: boolean): void;
|
|
421
|
+
private applyProjection;
|
|
422
|
+
/** Resolves once the boot-time prime has finished (or was skipped). */
|
|
423
|
+
whenPrimed(): Promise<void>;
|
|
424
|
+
/**
|
|
425
|
+
* Fill the circuit from the LOCAL store, in the background.
|
|
426
|
+
*
|
|
427
|
+
* With a usable snapshot: install it under whatever views have registered
|
|
428
|
+
* meanwhile (`load_store_state` re-primes them), then `reconcile` each table
|
|
429
|
+
* against the store's `(id, rv)` list so rows deleted since the checkpoint
|
|
430
|
+
* are stepped out and only rows added or changed since are read back and
|
|
431
|
+
* ingested. Without one: read every row and ingest it, chunked.
|
|
432
|
+
*
|
|
433
|
+
* Either way the circuit ends up equal to the local store without touching
|
|
434
|
+
* the network, so the first sync diff is a real delta rather than "fetch
|
|
435
|
+
* everything". The returned promise never rejects; `whenPrimed` gates on it.
|
|
436
|
+
*/
|
|
437
|
+
primeFromLocal(ctx: CircuitPrimeContext): Promise<void>;
|
|
438
|
+
private runPrime;
|
|
439
|
+
/** Publish wasm updates produced outside an ingest (restore, reconcile). */
|
|
440
|
+
private dispatchWasmUpdates;
|
|
441
|
+
/**
|
|
442
|
+
* Record that the circuit changed by `rows` rows. Cheap; the snapshot is
|
|
443
|
+
* deferred to the checkpoint timer and skipped entirely when `persistCircuit`
|
|
444
|
+
* is off.
|
|
445
|
+
*/
|
|
446
|
+
private markSnapshotDirty;
|
|
447
|
+
private startCheckpoints;
|
|
448
|
+
/** Stop checkpointing and drop the visibility listeners. */
|
|
449
|
+
stopCheckpoints(): void;
|
|
450
|
+
/**
|
|
451
|
+
* Write the circuit's store to the local store as a snapshot. Compacts the
|
|
452
|
+
* row arena first when dead bytes outweigh live ones. Serialised: a second
|
|
453
|
+
* call while one is in flight joins it. No-op unless persistence is on, this
|
|
454
|
+
* tab owns the store, and the engine can hold a snapshot.
|
|
455
|
+
*/
|
|
456
|
+
checkpoint(reason?: string): Promise<void>;
|
|
457
|
+
private runCheckpoint;
|
|
458
|
+
/**
|
|
459
|
+
* Projection widening: a newly registered view evaluates fields that rows
|
|
460
|
+
* already in the circuit were stored without. Merge just those fields in,
|
|
461
|
+
* table by table, from the local store. The view registered against what
|
|
462
|
+
* was present and converges as the merges step through.
|
|
463
|
+
*/
|
|
464
|
+
private scheduleWiden;
|
|
465
|
+
private runWiden;
|
|
466
|
+
/**
|
|
467
|
+
* Seed per-table `select` permission predicates ({ [table]: whereText }).
|
|
468
|
+
* Must run after the processor exists and before any `register_view`, else
|
|
469
|
+
* non-`_00_` tables are default-denied and registration fails.
|
|
470
|
+
*/
|
|
471
|
+
setPermissions(permissions: Record<string, string>): void;
|
|
472
|
+
/**
|
|
473
|
+
* Set the current session's auth identity for permission injection,
|
|
474
|
+
* mirroring the server's `fn::query::register`
|
|
475
|
+
* (`object::extend(params, { auth: { id: $auth.id }, access: $access })`).
|
|
476
|
+
* Stored as strings (empty when logged out) and applied to every
|
|
477
|
+
* `register_view` in {@link registerQueryPlan}. Must be set before a
|
|
478
|
+
* `$auth`-gated query registers (and re-set on auth state changes), or the
|
|
479
|
+
* in-browser SSP's `permission_inject` rejects it with
|
|
480
|
+
* "requires $auth but registration params lack it".
|
|
481
|
+
*/
|
|
482
|
+
setSessionAuth(authId: string | null, access: string | null): void;
|
|
132
483
|
/**
|
|
133
484
|
* Ingest a record change into the processor.
|
|
134
485
|
* Emits 'stream_update' event if materialized views are affected.
|
|
135
486
|
* @param isOptimistic true = local mutation (increment versions), false = remote sync (keep versions)
|
|
136
487
|
*/
|
|
137
|
-
ingest(table: string, op: '
|
|
488
|
+
ingest(table: string, op: IngestRecord['op'], id: string, record: any): WasmStreamUpdate[];
|
|
138
489
|
/**
|
|
139
490
|
* Register a new query plan.
|
|
140
491
|
* Emits 'stream_update' with the initial result.
|
|
@@ -147,60 +498,1533 @@ declare class StreamProcessorService {
|
|
|
147
498
|
private normalizeValue;
|
|
148
499
|
}
|
|
149
500
|
//#endregion
|
|
150
|
-
//#region src/
|
|
151
|
-
declare
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
501
|
+
//#region src/utils/index.d.ts
|
|
502
|
+
declare function fileToUint8Array(file: File | Blob): Promise<Uint8Array>;
|
|
503
|
+
/**
|
|
504
|
+
* Convert plain text to simple HTML paragraphs.
|
|
505
|
+
* Useful for seeding a rich-text editor (e.g. TipTap/ProseMirror) with fallback content.
|
|
506
|
+
*/
|
|
507
|
+
declare function textToHtml(text: string): string;
|
|
508
|
+
/**
|
|
509
|
+
* Helper for retrying DB operations with exponential backoff
|
|
510
|
+
*/
|
|
159
511
|
//#endregion
|
|
160
|
-
//#region src/modules/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
512
|
+
//#region src/modules/crdt/mirror.d.ts
|
|
513
|
+
/**
|
|
514
|
+
* The plain text a `@crdt` field shows as its column value, rendered the same
|
|
515
|
+
* way the SSP fold renders it (`packages/ssp-node/src/crdt/mirror.rs`).
|
|
516
|
+
*/
|
|
517
|
+
type CrdtKind = 'text' | 'prosemirror';
|
|
518
|
+
//#endregion
|
|
519
|
+
//#region src/modules/crdt/crdt-field.d.ts
|
|
520
|
+
declare const CURSOR_COLORS: string[];
|
|
521
|
+
declare function cursorColorFromName(name: string): string;
|
|
522
|
+
/** What a field handle needs from the manager that owns its session. */
|
|
523
|
+
interface CrdtFieldHost {
|
|
524
|
+
pushPresence(state: Uint8Array): void;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* An open collaborative field: the Loro document an editor binds to. Edits
|
|
528
|
+
* made on it are sent as the client's own edit-log rows; other clients' rows
|
|
529
|
+
* are imported into it as they arrive. Every open of the same field in one tab
|
|
530
|
+
* shares this document.
|
|
531
|
+
*/
|
|
532
|
+
declare class CrdtField {
|
|
533
|
+
readonly table: string;
|
|
534
|
+
readonly recordId: string;
|
|
535
|
+
readonly fieldName: string;
|
|
536
|
+
readonly kind: CrdtKind;
|
|
537
|
+
readonly cursorsEnabled: boolean;
|
|
538
|
+
private readonly doc;
|
|
539
|
+
private readonly host;
|
|
540
|
+
private _onCursorUpdate;
|
|
541
|
+
private pendingCursorUpdate;
|
|
542
|
+
private loaded;
|
|
543
|
+
private loadedListeners;
|
|
544
|
+
constructor(table: string, recordId: string, fieldName: string, kind: CrdtKind, cursorsEnabled: boolean, doc: LoroDoc, host: CrdtFieldHost);
|
|
545
|
+
getDoc(): LoroDoc;
|
|
546
|
+
/** Whether the document holds anything yet (local state or edits). */
|
|
547
|
+
hasContent(): boolean;
|
|
548
|
+
/**
|
|
549
|
+
* Whether the document is known: this device had it, it was handed a draft,
|
|
550
|
+
* or the server answered (with content, or that it has none yet). Until then
|
|
551
|
+
* an empty document means "not loaded", not "empty": show the column's text
|
|
552
|
+
* instead, and do not bind an editor (an editor binding an empty document
|
|
553
|
+
* creates structure that competes with the real one).
|
|
554
|
+
*/
|
|
555
|
+
isLoaded(): boolean;
|
|
556
|
+
/** Runs `cb` once the field is loaded (at once if it already is). Returns
|
|
557
|
+
* an unsubscribe. */
|
|
558
|
+
onLoaded(cb: () => void): () => void;
|
|
559
|
+
/** @internal The manager calls this when the document became known. */
|
|
560
|
+
markLoaded(): void;
|
|
561
|
+
/** Publish this session's presence (an `EphemeralStore` encoding, e.g. the
|
|
562
|
+
* editor's cursor). No-op unless the field is `@cursor`. */
|
|
563
|
+
pushCursorState(encoded: Uint8Array): void;
|
|
564
|
+
/** Called with every other session's presence blob. A blob that arrives
|
|
565
|
+
* before the editor sets this is replayed when it does. */
|
|
566
|
+
set onCursorUpdate(cb: ((data: Uint8Array) => void) | null);
|
|
567
|
+
get onCursorUpdate(): ((data: Uint8Array) => void) | null;
|
|
568
|
+
/** @internal Deliver one remote presence blob. */
|
|
569
|
+
deliverPresence(data: Uint8Array): void;
|
|
570
|
+
}
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/modules/crdt/index.d.ts
|
|
573
|
+
/**
|
|
574
|
+
* Collaborative (`@crdt`) fields.
|
|
575
|
+
*
|
|
576
|
+
* Each open field is a Loro document. The client never sends the document,
|
|
577
|
+
* only its own edits: every batch of local edits becomes one row of the
|
|
578
|
+
* generated `<table>__crdt_log` table, written through the ordinary outbox
|
|
579
|
+
* (so it survives offline, reloads and retries), and every client receives
|
|
580
|
+
* the other rows through an ordinary query and merges them. Nobody writes the
|
|
581
|
+
* canonical state but the SSP, which folds the log into `_00_crdt_doc` and the
|
|
582
|
+
* parent field's plain text (see `packages/ssp-node/src/crdt`).
|
|
583
|
+
*
|
|
584
|
+
* A row is imported only if it passes the acceptance rule the fold applies
|
|
585
|
+
* (`./acceptance`), so this tab and the server always hold the same ops.
|
|
586
|
+
*
|
|
587
|
+
* Edits carry this tab's Loro peer id, claimed for the signed-in user in
|
|
588
|
+
* `_00_crdt_peer` before the first row is sent; the database refuses a row
|
|
589
|
+
* whose peer its author does not own. A tab picks a fresh peer per session.
|
|
590
|
+
* Offline, edits stay local (and in the local snapshot) until the claim and
|
|
591
|
+
* the outbox can go through.
|
|
592
|
+
*/
|
|
593
|
+
/** A followed query: stop following it, and know when the server answered. */
|
|
594
|
+
interface Watch {
|
|
595
|
+
stop: () => void;
|
|
596
|
+
/** Resolves once the server's membership for the query is known and its
|
|
597
|
+
* rows have landed locally (never rejects; may never resolve offline). */
|
|
598
|
+
settled: Promise<void>;
|
|
599
|
+
}
|
|
600
|
+
/** The runtime pieces the manager needs from the client facade. */
|
|
601
|
+
interface CrdtPorts {
|
|
602
|
+
/** Register a query on `table` filtered by `where` and follow its rows. */
|
|
603
|
+
watch(table: string, where: Record<string, unknown>, onRows: (rows: Record<string, unknown>[]) => void): Promise<Watch>;
|
|
604
|
+
/** Optimistic create through the outbox. */
|
|
605
|
+
create(id: string, data: Record<string, unknown>): Promise<unknown>;
|
|
606
|
+
}
|
|
607
|
+
declare class CrdtManager {
|
|
165
608
|
private schema;
|
|
609
|
+
private local;
|
|
166
610
|
private remote;
|
|
167
|
-
private
|
|
611
|
+
private debounceMs;
|
|
612
|
+
private sessions;
|
|
613
|
+
private opening;
|
|
614
|
+
/** Closes still saving their field's local state, by session key. */
|
|
615
|
+
private saving;
|
|
616
|
+
private ports;
|
|
168
617
|
private logger;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
private
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
618
|
+
private peer;
|
|
619
|
+
/** The current peer's claim: settled once the database holds it for us. */
|
|
620
|
+
private claim;
|
|
621
|
+
private claimRetry;
|
|
622
|
+
private connectionUnsubscribes;
|
|
623
|
+
constructor(schema: SchemaStructure, local: LocalStore, remote: RemoteDatabaseService, logger: Logger$1, debounceMs?: number);
|
|
624
|
+
/** Hand over the runtime ports. Called once by the client facade. */
|
|
625
|
+
attach(ports: CrdtPorts): void;
|
|
626
|
+
/**
|
|
627
|
+
* A new signed-in identity (boot, sign-in, sign-out): edits from here on
|
|
628
|
+
* belong to it, under a peer it claims. Edits already made stay with the
|
|
629
|
+
* peer they were made under.
|
|
630
|
+
*/
|
|
631
|
+
setSessionId(_salt: string): void;
|
|
632
|
+
dispose(): void;
|
|
633
|
+
/**
|
|
634
|
+
* Open a collaborative field. Every open of the same field shares one
|
|
635
|
+
* document; close each open with {@link close}.
|
|
636
|
+
*
|
|
637
|
+
* `draft` (from {@link createDraft}) hands over edits made before the record
|
|
638
|
+
* existed: they are merged in and sent like this tab's own.
|
|
639
|
+
*/
|
|
640
|
+
open(table: string, recordId: string, field: string, draft?: CrdtField): Promise<CrdtField>;
|
|
178
641
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
642
|
+
* A detached document for a field of a record that does not exist yet (a
|
|
643
|
+
* "new thread" form). Nothing is sent; pass it to {@link open} once the
|
|
644
|
+
* record is created and its edits become the record's first ones.
|
|
181
645
|
*/
|
|
182
|
-
|
|
183
|
-
private
|
|
646
|
+
createDraft(table: string, field: string): Promise<CrdtField>;
|
|
647
|
+
private adopt;
|
|
648
|
+
/** Close one open. The last close of a field resolves once its unsent
|
|
649
|
+
* edits are in the outbox, or once they are saved on the device when the
|
|
650
|
+
* server does not answer in time (offline: at once). */
|
|
651
|
+
close(table: string, recordId: string, field: string): Promise<void>;
|
|
652
|
+
/** Close every field. `flush: false` (a bucket switch) drops unsent edits
|
|
653
|
+
* from this session's memory; they stay in the local snapshot of the
|
|
654
|
+
* bucket they belong to. */
|
|
655
|
+
closeAll(options?: {
|
|
656
|
+
flush?: boolean;
|
|
657
|
+
}): void;
|
|
658
|
+
private fieldSpec;
|
|
659
|
+
private start;
|
|
660
|
+
private stop;
|
|
184
661
|
/**
|
|
185
|
-
*
|
|
662
|
+
* Wait for server-bound `work`, at most {@link SERVER_WAIT_MS}, and not at
|
|
663
|
+
* all while disconnected: a remote call made then queues behind the
|
|
664
|
+
* reconnect for up to the query timeout, and an offline "new thread" form
|
|
665
|
+
* must not hang on it. The work itself goes on either way.
|
|
186
666
|
*/
|
|
187
|
-
|
|
667
|
+
private serverWait;
|
|
668
|
+
private ingest;
|
|
669
|
+
/** Mark the field loaded once its content arrived, or the server answered
|
|
670
|
+
* both reads (the published document and the log) with nothing. */
|
|
671
|
+
private checkLoaded;
|
|
672
|
+
private tryImport;
|
|
673
|
+
private refetchCanonical;
|
|
674
|
+
private scheduleVerify;
|
|
675
|
+
/**
|
|
676
|
+
* After a fold: compare this device's document with the one the server
|
|
677
|
+
* published, at the version the server published. Missing ops (rows this
|
|
678
|
+
* device never got before they were folded away) are repaired by merging
|
|
679
|
+
* the published snapshot in. The same ops giving a different state cannot
|
|
680
|
+
* be merged away; that is logged.
|
|
681
|
+
*/
|
|
682
|
+
private verify;
|
|
683
|
+
private scheduleFlush;
|
|
684
|
+
private flush;
|
|
685
|
+
/** Send every own peer's ops that are not in the outbox yet, one row per
|
|
686
|
+
* peer. Only the peer's own ops travel (`updates-in-range`): a plain
|
|
687
|
+
* `from: version` export would carry ops imported from others too, and
|
|
688
|
+
* the acceptance rule refuses such a row. */
|
|
689
|
+
private sendOwnEdits;
|
|
690
|
+
private retryLater;
|
|
691
|
+
/** Claim this session's peer once; a failure (offline, signed out) is retried. */
|
|
692
|
+
private ensureClaimed;
|
|
693
|
+
private claimPeer;
|
|
694
|
+
private localId;
|
|
695
|
+
private readLocal;
|
|
696
|
+
private schedulePersist;
|
|
697
|
+
/** Save the merged document. Another tab may have saved too: merge with
|
|
698
|
+
* its snapshot rather than overwrite it, and keep the further of the two
|
|
699
|
+
* outbox marks per peer. */
|
|
700
|
+
private persist;
|
|
701
|
+
private pushPresence;
|
|
702
|
+
/** Presence is the latest state only: no outbox, no offline queue. */
|
|
703
|
+
private sendPresence;
|
|
704
|
+
/** Remove this session's cursor when the field closes; the server's TTL
|
|
705
|
+
* sweep covers a tab that never gets here. */
|
|
706
|
+
private dropPresence;
|
|
707
|
+
private ingestPresence;
|
|
708
|
+
}
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/modules/feature-flag/index.d.ts
|
|
711
|
+
interface FeatureFlagSnapshot {
|
|
712
|
+
variant: string | undefined;
|
|
713
|
+
payload: unknown | undefined;
|
|
714
|
+
}
|
|
715
|
+
interface FeatureFlagOptions {
|
|
716
|
+
fallback?: string;
|
|
717
|
+
ttl?: QueryTimeToLive;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* A locally forced variant. Applies to THIS browser only and is never sent to
|
|
721
|
+
* the server — the assignment in `_00_user_feature` is untouched, so clearing
|
|
722
|
+
* the override restores whatever the server says.
|
|
723
|
+
*/
|
|
724
|
+
interface FeatureFlagOverride {
|
|
725
|
+
variant: string;
|
|
726
|
+
payload?: unknown;
|
|
727
|
+
}
|
|
728
|
+
declare class FeatureFlagHandle {
|
|
729
|
+
readonly key: string;
|
|
730
|
+
readonly fallback: string | undefined;
|
|
731
|
+
private latest;
|
|
732
|
+
private listeners;
|
|
733
|
+
private unsubscribeFn;
|
|
734
|
+
private onCloseFn;
|
|
735
|
+
private closed;
|
|
736
|
+
constructor(key: string, fallback: string | undefined);
|
|
737
|
+
attach(unsubscribe: () => void): void;
|
|
738
|
+
detach(): void;
|
|
739
|
+
set(snapshot: FeatureFlagSnapshot): void;
|
|
740
|
+
variant(): string | undefined;
|
|
741
|
+
payload<T = unknown>(): T | undefined;
|
|
742
|
+
enabled(): boolean;
|
|
743
|
+
subscribe(cb: (s: FeatureFlagSnapshot) => void): () => void;
|
|
744
|
+
onClose(cb: () => void): void;
|
|
745
|
+
close(): void;
|
|
746
|
+
}
|
|
747
|
+
/** The slice of the query engine this module drives: register a live query
|
|
748
|
+
* locally, ask for its remote registration, subscribe to its rows. */
|
|
749
|
+
interface LiveQueryPort$1 {
|
|
750
|
+
query(table: string, surql: string, params: Record<string, unknown>, ttl: QueryTimeToLive): Promise<string>;
|
|
751
|
+
subscribe(hash: string, cb: (records: Record<string, any>[]) => void, options?: {
|
|
752
|
+
immediate?: boolean;
|
|
753
|
+
}): () => void;
|
|
754
|
+
}
|
|
755
|
+
interface RemoteRegisterPort$1 {
|
|
756
|
+
enqueueDownEvent(event: {
|
|
757
|
+
type: 'register';
|
|
758
|
+
payload: {
|
|
759
|
+
hash: string;
|
|
760
|
+
};
|
|
761
|
+
}): void;
|
|
762
|
+
}
|
|
763
|
+
interface FeatureFlagModuleDeps<S extends SchemaStructure> {
|
|
764
|
+
dataModule: LiveQueryPort$1;
|
|
765
|
+
sync: RemoteRegisterPort$1;
|
|
766
|
+
auth: Pick<AuthService<S>, 'subscribe'>;
|
|
767
|
+
logger: Logger$1;
|
|
768
|
+
}
|
|
769
|
+
declare class FeatureFlagModule<S extends SchemaStructure> {
|
|
770
|
+
private deps;
|
|
771
|
+
private logger;
|
|
772
|
+
private handles;
|
|
773
|
+
private authUnsubscribe;
|
|
774
|
+
private lastUserId;
|
|
775
|
+
private querySubscription;
|
|
776
|
+
private starting;
|
|
777
|
+
private ttl;
|
|
778
|
+
private snapshots;
|
|
779
|
+
private loaded;
|
|
780
|
+
private overrides;
|
|
781
|
+
constructor(deps: FeatureFlagModuleDeps<S>);
|
|
782
|
+
init(): void;
|
|
783
|
+
feature(key: string, options?: FeatureFlagOptions): FeatureFlagHandle;
|
|
784
|
+
closeAll(): Promise<void>;
|
|
785
|
+
/** Auth changed: drop the old user's query/snapshots and re-observe. */
|
|
786
|
+
private refresh;
|
|
787
|
+
private teardownQuery;
|
|
788
|
+
/** Start the single shared live query (idempotent; no-op with no handles). */
|
|
789
|
+
private ensureStarted;
|
|
790
|
+
/** Live query result → per-key snapshots → push to every active handle. */
|
|
791
|
+
private applyRecords;
|
|
188
792
|
/**
|
|
189
|
-
*
|
|
793
|
+
* Force `key` to `variant` in THIS browser. Pass `null` to clear.
|
|
794
|
+
*
|
|
795
|
+
* Nothing is written to the server: the `_00_user_feature` assignment is
|
|
796
|
+
* untouched, so clearing restores whatever the server says. Persisted to
|
|
797
|
+
* localStorage on the page origin, so it survives a reload.
|
|
190
798
|
*/
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
799
|
+
setLocalOverride(key: string, variant: string | null, payload?: unknown): void;
|
|
800
|
+
clearLocalOverrides(): void;
|
|
801
|
+
getLocalOverrides(): Record<string, FeatureFlagOverride>;
|
|
802
|
+
/** The assignment for `key`, with any local override taking precedence. */
|
|
803
|
+
private resolve;
|
|
804
|
+
private pushAll;
|
|
805
|
+
private loadOverrides;
|
|
806
|
+
private persistOverrides;
|
|
807
|
+
}
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region src/modules/app-release/index.d.ts
|
|
810
|
+
interface AppReleaseSnapshot {
|
|
811
|
+
/** Latest announced version for the app, or undefined when no row exists. */
|
|
812
|
+
version: string | undefined;
|
|
813
|
+
/** Clients should clear SW/caches when reloading onto this version. */
|
|
814
|
+
cacheBust: boolean;
|
|
815
|
+
/** Clients should reload/update immediately instead of asking. */
|
|
816
|
+
mandatory: boolean;
|
|
817
|
+
releasedAt: string | undefined;
|
|
818
|
+
}
|
|
819
|
+
interface AppReleaseOptions {
|
|
820
|
+
ttl?: QueryTimeToLive;
|
|
821
|
+
}
|
|
822
|
+
declare class AppReleaseHandle {
|
|
823
|
+
readonly app: string;
|
|
824
|
+
private latest;
|
|
825
|
+
private listeners;
|
|
826
|
+
private onCloseFn;
|
|
827
|
+
private closed;
|
|
828
|
+
constructor(app: string);
|
|
829
|
+
set(snapshot: AppReleaseSnapshot): void;
|
|
830
|
+
snapshot(): AppReleaseSnapshot;
|
|
831
|
+
version(): string | undefined;
|
|
832
|
+
/** True when the announced version is semver-newer than `currentVersion`. */
|
|
833
|
+
updateAvailable(currentVersion: string): boolean;
|
|
834
|
+
subscribe(cb: (s: AppReleaseSnapshot) => void): () => void;
|
|
835
|
+
onClose(cb: () => void): void;
|
|
836
|
+
close(): void;
|
|
837
|
+
}
|
|
838
|
+
/** The slice of the query engine this module drives: register a live query
|
|
839
|
+
* locally, ask for its remote registration, subscribe to its rows. */
|
|
840
|
+
interface LiveQueryPort {
|
|
841
|
+
query(table: string, surql: string, params: Record<string, unknown>, ttl: QueryTimeToLive): Promise<string>;
|
|
842
|
+
subscribe(hash: string, cb: (records: Record<string, any>[]) => void, options?: {
|
|
843
|
+
immediate?: boolean;
|
|
844
|
+
}): () => void;
|
|
845
|
+
}
|
|
846
|
+
interface RemoteRegisterPort {
|
|
847
|
+
enqueueDownEvent(event: {
|
|
848
|
+
type: 'register';
|
|
849
|
+
payload: {
|
|
850
|
+
hash: string;
|
|
851
|
+
};
|
|
852
|
+
}): void;
|
|
853
|
+
}
|
|
854
|
+
interface AppReleaseModuleDeps<S extends SchemaStructure> {
|
|
855
|
+
dataModule: LiveQueryPort;
|
|
856
|
+
sync: RemoteRegisterPort;
|
|
857
|
+
auth: Pick<AuthService<S>, 'subscribe'>;
|
|
858
|
+
logger: Logger$1;
|
|
859
|
+
}
|
|
860
|
+
declare class AppReleaseModule<S extends SchemaStructure> {
|
|
861
|
+
private deps;
|
|
862
|
+
private logger;
|
|
863
|
+
private handles;
|
|
864
|
+
private authUnsubscribe;
|
|
865
|
+
private lastUserId;
|
|
866
|
+
private querySubscription;
|
|
867
|
+
private starting;
|
|
868
|
+
private ttl;
|
|
869
|
+
private snapshots;
|
|
870
|
+
private loaded;
|
|
871
|
+
constructor(deps: AppReleaseModuleDeps<S>);
|
|
872
|
+
init(): void;
|
|
873
|
+
release(app: string, options?: AppReleaseOptions): AppReleaseHandle;
|
|
874
|
+
closeAll(): Promise<void>;
|
|
875
|
+
private refresh;
|
|
876
|
+
private teardownQuery;
|
|
877
|
+
private ensureStarted;
|
|
878
|
+
private applyRecords;
|
|
879
|
+
}
|
|
880
|
+
//#endregion
|
|
881
|
+
//#region src/kernel/events.d.ts
|
|
882
|
+
/**
|
|
883
|
+
* Inbound events. Everything that can start a saga arrives as one of these
|
|
884
|
+
* through the runtime router: public API calls, adapter callbacks, timers,
|
|
885
|
+
* and `dispatch` effects from other sagas.
|
|
886
|
+
*/
|
|
887
|
+
type RuntimeEvent = {
|
|
888
|
+
type: 'EnsureRegistered';
|
|
889
|
+
requireAuth?: boolean;
|
|
890
|
+
attempt?: number;
|
|
891
|
+
} | {
|
|
892
|
+
type: 'RegisterRemote';
|
|
893
|
+
hash: QueryHash;
|
|
894
|
+
retry?: boolean;
|
|
895
|
+
} | {
|
|
896
|
+
type: 'SyncOutcome';
|
|
897
|
+
ok: boolean;
|
|
898
|
+
error?: unknown;
|
|
899
|
+
} | {
|
|
900
|
+
type: 'AckPrune';
|
|
901
|
+
} | {
|
|
902
|
+
type: 'ReadDirtyMembership';
|
|
903
|
+
} | {
|
|
904
|
+
type: 'RecoverLostView';
|
|
905
|
+
hash: QueryHash;
|
|
906
|
+
} | {
|
|
907
|
+
type: 'ReadMembership';
|
|
908
|
+
hashes: QueryHash[];
|
|
909
|
+
} | {
|
|
910
|
+
type: 'FetchRows';
|
|
911
|
+
} | {
|
|
912
|
+
type: 'Materialize';
|
|
913
|
+
hash: QueryHash;
|
|
914
|
+
} | {
|
|
915
|
+
type: 'MaterializeDirty';
|
|
916
|
+
} | {
|
|
917
|
+
type: 'LifecycleTick';
|
|
918
|
+
} | {
|
|
919
|
+
type: 'GcTick';
|
|
920
|
+
} | {
|
|
921
|
+
type: 'Drain';
|
|
922
|
+
} | {
|
|
923
|
+
type: 'FlushWrite';
|
|
924
|
+
key: string;
|
|
925
|
+
} | {
|
|
926
|
+
type: 'FlushPendingWrites';
|
|
927
|
+
} | {
|
|
928
|
+
type: 'AdoptPendingWrites';
|
|
929
|
+
} | {
|
|
930
|
+
type: 'PollTick';
|
|
931
|
+
} | {
|
|
932
|
+
type: 'SelfHealTick';
|
|
933
|
+
} | {
|
|
934
|
+
type: 'HeartbeatNow';
|
|
935
|
+
} | {
|
|
936
|
+
type: 'StartRemote';
|
|
937
|
+
} | {
|
|
938
|
+
type: 'PrimeCircuit';
|
|
939
|
+
} | {
|
|
940
|
+
type: 'VersionsPrimed';
|
|
941
|
+
entries: Array<readonly [string, number]>;
|
|
942
|
+
} | {
|
|
943
|
+
type: 'WarmBlobs';
|
|
944
|
+
} | {
|
|
945
|
+
type: 'AuthFlip';
|
|
946
|
+
userId: string | null;
|
|
947
|
+
} | {
|
|
948
|
+
type: 'BucketSwitch';
|
|
949
|
+
target: string;
|
|
950
|
+
} | {
|
|
951
|
+
type: 'PageHide';
|
|
952
|
+
} | {
|
|
953
|
+
type: 'ConnectionChanged';
|
|
954
|
+
state: ConnectionState;
|
|
955
|
+
} | {
|
|
956
|
+
type: 'StreamUpdate';
|
|
957
|
+
update: StreamUpdate;
|
|
958
|
+
} | {
|
|
959
|
+
type: 'LiveStart';
|
|
960
|
+
} | {
|
|
961
|
+
type: 'LiveChange';
|
|
962
|
+
hashes: QueryHash[];
|
|
963
|
+
rows?: InlineRow[];
|
|
964
|
+
} | {
|
|
965
|
+
type: 'TabRole';
|
|
966
|
+
role: 'solo' | 'leader' | 'follower';
|
|
967
|
+
} | {
|
|
968
|
+
type: 'TabMessage';
|
|
969
|
+
message: unknown;
|
|
970
|
+
};
|
|
971
|
+
type OutEvent = {
|
|
972
|
+
type: 'query:records';
|
|
973
|
+
hash: QueryHash;
|
|
974
|
+
records: ReadonlyArray<Record<string, unknown>>;
|
|
975
|
+
} | {
|
|
976
|
+
type: 'query:status';
|
|
977
|
+
hash: QueryHash;
|
|
978
|
+
status: QueryStatus;
|
|
979
|
+
} | {
|
|
980
|
+
type: 'query:authority';
|
|
981
|
+
hash: QueryHash;
|
|
982
|
+
known: boolean;
|
|
983
|
+
} | {
|
|
984
|
+
type: 'query:view-lost';
|
|
985
|
+
hash: QueryHash;
|
|
986
|
+
} | {
|
|
987
|
+
type: 'query:evicted';
|
|
988
|
+
hash: QueryHash;
|
|
989
|
+
} | {
|
|
990
|
+
type: 'mutation:event';
|
|
991
|
+
event: unknown;
|
|
992
|
+
} | {
|
|
993
|
+
type: 'mutation:settled';
|
|
994
|
+
mutationId: string;
|
|
995
|
+
recordId: string;
|
|
996
|
+
eventType: string;
|
|
997
|
+
} | {
|
|
998
|
+
type: 'mutation:rolled-back';
|
|
999
|
+
mutationId: string;
|
|
1000
|
+
recordId: string;
|
|
1001
|
+
eventType: string;
|
|
1002
|
+
error: string;
|
|
1003
|
+
} | {
|
|
1004
|
+
type: 'tray:changed';
|
|
1005
|
+
count: number;
|
|
1006
|
+
} | {
|
|
1007
|
+
type: 'health:changed';
|
|
1008
|
+
health: SyncHealth;
|
|
1009
|
+
} | {
|
|
1010
|
+
type: 'activity:changed';
|
|
1011
|
+
fetching: number;
|
|
1012
|
+
pending: number;
|
|
1013
|
+
} | {
|
|
1014
|
+
type: 'tabs:broadcast';
|
|
1015
|
+
message: unknown;
|
|
1016
|
+
} | {
|
|
1017
|
+
type: 'tabs:sendTo';
|
|
1018
|
+
tabId: string;
|
|
1019
|
+
message: unknown;
|
|
1020
|
+
} | {
|
|
1021
|
+
type: 'log';
|
|
1022
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
1023
|
+
message: string;
|
|
1024
|
+
data?: unknown;
|
|
1025
|
+
};
|
|
1026
|
+
//#endregion
|
|
1027
|
+
//#region src/state/lifecycle.d.ts
|
|
1028
|
+
/**
|
|
1029
|
+
* The query lifecycle as one explicit machine.
|
|
1030
|
+
*
|
|
1031
|
+
* `phase` answers "where do this query's rows come from":
|
|
1032
|
+
* - `cold` never resolved on this device: rows come from a local
|
|
1033
|
+
* predicate scan (or the SSP local window), bindings show a loader.
|
|
1034
|
+
* - `cached` a durable `_00_view` row was found: rows come from that id-set.
|
|
1035
|
+
* - `live` a server membership set was accepted this session.
|
|
1036
|
+
* - `view-lost` the server's `_00_query` row vanished while we held
|
|
1037
|
+
* membership; rows are kept, a re-registration is under way.
|
|
1038
|
+
*
|
|
1039
|
+
* `remote` tracks the server-side registration, `fetchDepth` is the refcount
|
|
1040
|
+
* behind `status: 'fetching'`, `notified` says whether subscribers received
|
|
1041
|
+
* at least one materialization this registration.
|
|
1042
|
+
*
|
|
1043
|
+
* `backgroundDepth` is the part of `fetchDepth` that re-verifies rows the
|
|
1044
|
+
* query already shows: a registration of a query that is not `cold`. It still
|
|
1045
|
+
* holds `status: 'fetching'` (results are not yet confirmed), but it is not
|
|
1046
|
+
* traffic the user waits on, so the activity indicator leaves it out.
|
|
1047
|
+
*/
|
|
1048
|
+
type QueryPhase = 'cold' | 'cached' | 'live' | 'view-lost';
|
|
1049
|
+
type RemotePhase = 'unregistered' | 'registering' | 'registered' | 'failed';
|
|
1050
|
+
interface QueryLifecycle {
|
|
1051
|
+
readonly phase: QueryPhase;
|
|
1052
|
+
readonly remote: RemotePhase;
|
|
1053
|
+
readonly fetchDepth: number;
|
|
1054
|
+
readonly backgroundDepth: number;
|
|
1055
|
+
readonly notified: boolean;
|
|
1056
|
+
}
|
|
1057
|
+
//#endregion
|
|
1058
|
+
//#region src/kernel/effects.d.ts
|
|
1059
|
+
type Vars = Record<string, unknown>;
|
|
1060
|
+
/** What the in-browser SSP needs to build a local view. */
|
|
1061
|
+
interface RegisterPlan {
|
|
1062
|
+
queryHash: string;
|
|
1063
|
+
surql: string;
|
|
1064
|
+
params: Vars;
|
|
1065
|
+
ttl: string;
|
|
1066
|
+
tableName: string;
|
|
1067
|
+
involvedTables?: string[];
|
|
1068
|
+
}
|
|
1069
|
+
/** One statement's outcome of a multi-statement remote request. */
|
|
1070
|
+
type StatementResult = {
|
|
1071
|
+
status: 'OK';
|
|
1072
|
+
result: unknown;
|
|
1073
|
+
} | {
|
|
1074
|
+
status: 'ERR';
|
|
1075
|
+
error: string;
|
|
1076
|
+
};
|
|
1077
|
+
/** One entry of an `all` fan-out result (allSettled semantics). */
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Effects are data. A saga yields one and gets its result back on the next
|
|
1081
|
+
* `next()`; the interpreter (client/runtime) is the only place they execute.
|
|
1082
|
+
*/
|
|
1083
|
+
type Effect = {
|
|
1084
|
+
kind: 'local.query';
|
|
1085
|
+
sql: string;
|
|
1086
|
+
vars?: Vars;
|
|
1087
|
+
epoch?: number;
|
|
1088
|
+
} | {
|
|
1089
|
+
kind: 'local.select';
|
|
1090
|
+
plan: QueryPlan;
|
|
1091
|
+
params?: Vars;
|
|
1092
|
+
} | {
|
|
1093
|
+
kind: 'local.execute';
|
|
1094
|
+
query: SealedQuery<unknown>;
|
|
1095
|
+
vars?: Vars;
|
|
1096
|
+
epoch?: number;
|
|
1097
|
+
} | {
|
|
1098
|
+
kind: 'local.getById';
|
|
1099
|
+
table: string;
|
|
1100
|
+
id: unknown;
|
|
1101
|
+
} | {
|
|
1102
|
+
kind: 'local.upsert';
|
|
1103
|
+
table: string;
|
|
1104
|
+
id: unknown;
|
|
1105
|
+
data: Record<string, unknown>;
|
|
1106
|
+
mode: 'replace' | 'merge';
|
|
1107
|
+
} | {
|
|
1108
|
+
kind: 'local.delete';
|
|
1109
|
+
table: string;
|
|
1110
|
+
id: unknown;
|
|
1111
|
+
} | {
|
|
1112
|
+
kind: 'local.epoch';
|
|
1113
|
+
} | {
|
|
1114
|
+
kind: 'remote.query';
|
|
1115
|
+
sql: string;
|
|
1116
|
+
vars?: Vars;
|
|
1117
|
+
timeoutMs?: number;
|
|
1118
|
+
} | {
|
|
1119
|
+
kind: 'remote.live';
|
|
1120
|
+
table: string;
|
|
1121
|
+
} | {
|
|
1122
|
+
kind: 'remote.kill';
|
|
1123
|
+
uuid: string;
|
|
1124
|
+
} | {
|
|
1125
|
+
kind: 'ssp.register';
|
|
1126
|
+
plan: RegisterPlan;
|
|
1127
|
+
} | {
|
|
1128
|
+
kind: 'ssp.unregister';
|
|
1129
|
+
hash: string;
|
|
1130
|
+
} | {
|
|
1131
|
+
kind: 'ssp.ingest';
|
|
1132
|
+
records: IngestRecord[];
|
|
1133
|
+
} | {
|
|
1134
|
+
kind: 'timer.set';
|
|
1135
|
+
key: string;
|
|
1136
|
+
ms: number;
|
|
1137
|
+
event: RuntimeEvent;
|
|
1138
|
+
} | {
|
|
1139
|
+
kind: 'timer.clear';
|
|
1140
|
+
key: string;
|
|
1141
|
+
} | {
|
|
1142
|
+
kind: 'state.read';
|
|
1143
|
+
select: (s: ClientState) => unknown;
|
|
1144
|
+
} | {
|
|
1145
|
+
kind: 'state.update';
|
|
1146
|
+
fn: (s: ClientState) => ClientState;
|
|
1147
|
+
} | {
|
|
1148
|
+
kind: 'state.wait';
|
|
1149
|
+
until: (s: ClientState) => boolean;
|
|
1150
|
+
} | {
|
|
1151
|
+
kind: 'now';
|
|
1152
|
+
} | {
|
|
1153
|
+
kind: 'id';
|
|
1154
|
+
scope: IdScope;
|
|
1155
|
+
} | {
|
|
1156
|
+
kind: 'hash';
|
|
1157
|
+
input: string;
|
|
1158
|
+
} | {
|
|
1159
|
+
kind: 'emit';
|
|
1160
|
+
event: OutEvent;
|
|
1161
|
+
} | {
|
|
1162
|
+
kind: 'dispatch';
|
|
1163
|
+
event: RuntimeEvent;
|
|
1164
|
+
} | {
|
|
1165
|
+
kind: 'all';
|
|
1166
|
+
effects: Effect[];
|
|
1167
|
+
} | {
|
|
1168
|
+
kind: 'service';
|
|
1169
|
+
name: ServiceName;
|
|
1170
|
+
args: unknown[];
|
|
1171
|
+
};
|
|
1172
|
+
/** `mutation`: an outbox row id. `salt`: a query-key salt. `owner`: a page's anonymous-view owner secret. */
|
|
1173
|
+
type IdScope = 'mutation' | 'salt' | 'owner';
|
|
1174
|
+
/**
|
|
1175
|
+
* Calls into the legacy services that are adapters, not state: auth, tabs,
|
|
1176
|
+
* blobs, migrator, crdt, persistence, the SSP lifecycle, the remote socket.
|
|
1177
|
+
* A saga names the call; the runtime binds it to the real service.
|
|
1178
|
+
*/
|
|
1179
|
+
interface ServiceCalls {
|
|
1180
|
+
'hint.read': () => string | null;
|
|
1181
|
+
'hint.write': (bucketId: string) => void;
|
|
1182
|
+
/** The salt (and owner secret) this tab's previous page left, if minted for `authId`. */
|
|
1183
|
+
'salt.take': (authId: string | null) => {
|
|
1184
|
+
salt: string;
|
|
1185
|
+
owner: string | null;
|
|
1186
|
+
} | null;
|
|
1187
|
+
'salt.leave': (salt: string, authId: string | null, owner: string | null) => void;
|
|
1188
|
+
'local.connect': (bucketId: string) => Promise<void>;
|
|
1189
|
+
'local.switchStore': (bucketId: string) => Promise<void>;
|
|
1190
|
+
'local.beginSwitch': () => () => void;
|
|
1191
|
+
'local.currentBucketId': () => string;
|
|
1192
|
+
'local.usesSurqlSchema': () => boolean;
|
|
1193
|
+
'local.dropBucket': (bucketId: string) => Promise<void>;
|
|
1194
|
+
'migrator.provision': () => Promise<void>;
|
|
1195
|
+
'blobs.start': (bucketId: string) => Promise<void>;
|
|
1196
|
+
'blobs.setNamespace': (bucketId: string) => Promise<void>;
|
|
1197
|
+
'blobs.clear': () => Promise<void>;
|
|
1198
|
+
'ssp.init': () => Promise<void>;
|
|
1199
|
+
'ssp.setPermissions': () => void;
|
|
1200
|
+
'ssp.setSessionAuth': (authId: string | null, access: string | null) => void;
|
|
1201
|
+
'ssp.prime': (pendingIds: string[]) => Promise<void>;
|
|
1202
|
+
'ssp.reset': () => Promise<void>;
|
|
1203
|
+
'ssp.setPersistence': (enabled: boolean) => void;
|
|
1204
|
+
'auth.restoreSession': () => Promise<string | null>;
|
|
1205
|
+
'auth.init': () => Promise<void>;
|
|
1206
|
+
'auth.sessionAuthId': () => string | null;
|
|
1207
|
+
'auth.access': () => string | null;
|
|
1208
|
+
'auth.token': () => string | null;
|
|
1209
|
+
'auth.currentUser': () => Record<string, unknown> | null;
|
|
1210
|
+
'auth.consumeEndedImpersonation': () => string | null;
|
|
1211
|
+
'remote.connect': () => Promise<void>;
|
|
1212
|
+
'remote.releaseViews': (ids: unknown[]) => void;
|
|
1213
|
+
'supervisor.start': () => void;
|
|
1214
|
+
'tabs.start': (bucketId: string) => Promise<'solo' | 'leader' | 'follower'>;
|
|
1215
|
+
'tabs.moveToBucket': (bucketId: string) => Promise<'solo' | 'leader' | 'follower'>;
|
|
1216
|
+
'crdt.setSessionId': (sessionId: string) => void;
|
|
1217
|
+
'crdt.closeAll': (flush: boolean) => void;
|
|
1218
|
+
'persistence.set': (key: string, value: unknown) => Promise<void>;
|
|
1219
|
+
'features.init': () => void;
|
|
1220
|
+
'releases.init': () => void;
|
|
1221
|
+
'window.attach': () => void;
|
|
195
1222
|
}
|
|
1223
|
+
type ServiceName = keyof ServiceCalls;
|
|
1224
|
+
/** Typed constructors. Each is one line so the saga reads like the plan. */
|
|
196
1225
|
//#endregion
|
|
197
|
-
//#region src/
|
|
1226
|
+
//#region src/kernel/saga.d.ts
|
|
1227
|
+
/**
|
|
1228
|
+
* A saga is a generator that yields effects and receives their results. It
|
|
1229
|
+
* holds no references to adapters or state, so it can be driven by the real
|
|
1230
|
+
* interpreter or by `testing/run-pure` with canned results.
|
|
1231
|
+
*/
|
|
1232
|
+
type Saga<R> = Generator<Effect, R, any>;
|
|
1233
|
+
/**
|
|
1234
|
+
* Lanes bound concurrency without a queue object per lane:
|
|
1235
|
+
* - `serial`: runs strictly one at a time per key, in arrival order.
|
|
1236
|
+
* - `dedupe`: a request while one run is active joins that run instead of
|
|
1237
|
+
* starting another.
|
|
1238
|
+
* The bookkeeping is a pure value so the policy is unit-testable; the runtime
|
|
1239
|
+
* owns the promises.
|
|
1240
|
+
*/
|
|
1241
|
+
type Lane = {
|
|
1242
|
+
kind: 'serial';
|
|
1243
|
+
key: string;
|
|
1244
|
+
} | {
|
|
1245
|
+
kind: 'dedupe';
|
|
1246
|
+
key: string;
|
|
1247
|
+
};
|
|
1248
|
+
interface LaneState {
|
|
1249
|
+
readonly running: ReadonlySet<string>;
|
|
1250
|
+
readonly waiting: ReadonlyMap<string, number>;
|
|
1251
|
+
}
|
|
1252
|
+
//#endregion
|
|
1253
|
+
//#region src/state/client-state.d.ts
|
|
1254
|
+
type Row = Record<string, unknown>;
|
|
1255
|
+
/** Everything a query is, minus anything that changes after registration. */
|
|
1256
|
+
interface QueryDefinition {
|
|
1257
|
+
/** Session-salted `_00_query:<hash>` record id: the remote view id. */
|
|
1258
|
+
readonly id: RecordId<string>;
|
|
1259
|
+
readonly hash: QueryHash;
|
|
1260
|
+
/** Unsalted `sha256({surql, params})`: key of the durable `_00_view` row. */
|
|
1261
|
+
readonly viewKey: string;
|
|
1262
|
+
readonly surql: string;
|
|
1263
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
1264
|
+
readonly plan?: QueryPlan;
|
|
1265
|
+
readonly ttl: QueryTimeToLive;
|
|
1266
|
+
readonly ttlMs: number;
|
|
1267
|
+
readonly tableName: string;
|
|
1268
|
+
readonly createdAt: number;
|
|
1269
|
+
}
|
|
1270
|
+
type ServerViewState = 'materializing' | 'ready' | null;
|
|
1271
|
+
interface QueryTelemetry {
|
|
1272
|
+
readonly updateCount: number;
|
|
1273
|
+
readonly errorCount: number;
|
|
1274
|
+
readonly lastUpdatedAt: number | null;
|
|
1275
|
+
readonly materializationSamples: ReadonlyArray<number>;
|
|
1276
|
+
readonly lastIngestLatencyMs: number | null;
|
|
1277
|
+
readonly phaseSamples: Readonly<Record<string, ReadonlyArray<number>>>;
|
|
1278
|
+
readonly phaseLast: Readonly<Record<string, number | null>>;
|
|
1279
|
+
readonly registrationTimings: RegistrationTimings;
|
|
1280
|
+
}
|
|
1281
|
+
interface QueryEntry {
|
|
1282
|
+
readonly def: QueryDefinition;
|
|
1283
|
+
readonly lifecycle: QueryLifecycle;
|
|
1284
|
+
/** Server membership (ids + versions) or the durable seed. */
|
|
1285
|
+
readonly remoteArray: RecordVersionArray;
|
|
1286
|
+
/** The in-browser SSP view's id-set: local truth for "does this row match". */
|
|
1287
|
+
readonly localArray: RecordVersionArray;
|
|
1288
|
+
readonly subqueryRemoteArray: RecordVersionArray;
|
|
1289
|
+
readonly records: ReadonlyArray<Row>;
|
|
1290
|
+
readonly serverState: ServerViewState;
|
|
1291
|
+
readonly subscribers: number;
|
|
1292
|
+
readonly lastSubscriberLeftAt: number | null;
|
|
1293
|
+
readonly lastHeartbeatAt: number | null;
|
|
1294
|
+
readonly lastPolledAt: number | null;
|
|
1295
|
+
readonly registerAttempts: number;
|
|
1296
|
+
/**
|
|
1297
|
+
* Re-registrations issued because the `_00_query` row read as gone, since
|
|
1298
|
+
* the row was last seen. Paces the next one (see `applyMembership`).
|
|
1299
|
+
*/
|
|
1300
|
+
readonly viewLostCount: number;
|
|
1301
|
+
/** When the paced re-registration is due; null when none is pending. */
|
|
1302
|
+
readonly viewLostRetryAt: number | null;
|
|
1303
|
+
readonly telemetry: QueryTelemetry;
|
|
1304
|
+
}
|
|
1305
|
+
type OutboxStatus = 'pending' | 'acked';
|
|
1306
|
+
/** In-memory mirror of one `_00_pending_mutations` row plus its push progress. */
|
|
1307
|
+
interface OutboxItem {
|
|
1308
|
+
readonly id: string;
|
|
1309
|
+
readonly type: MutationEventType;
|
|
1310
|
+
readonly recordId: string;
|
|
1311
|
+
readonly table: string;
|
|
1312
|
+
readonly status: OutboxStatus;
|
|
1313
|
+
readonly ackedAt: number | null;
|
|
1314
|
+
readonly attempts: number;
|
|
1315
|
+
}
|
|
1316
|
+
type TabRole = 'solo' | 'leader' | 'follower';
|
|
1317
|
+
/** A debounced update accumulating locally until its flush writes the outbox row. */
|
|
1318
|
+
interface PendingWrite {
|
|
1319
|
+
readonly key: string;
|
|
1320
|
+
readonly table: string;
|
|
1321
|
+
readonly recordId: string;
|
|
1322
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
1323
|
+
readonly before: Readonly<Record<string, unknown>> | null;
|
|
1324
|
+
readonly firstAt: number;
|
|
1325
|
+
/** Its durable copy in `_00_pending_writes`; null when `flushOnHide` is off. */
|
|
1326
|
+
readonly mirrorId: string | null;
|
|
1327
|
+
}
|
|
1328
|
+
interface SyncSlice {
|
|
1329
|
+
readonly health: SyncHealth;
|
|
1330
|
+
readonly consecutiveFailures: number;
|
|
1331
|
+
readonly hasSyncedOnce: boolean;
|
|
1332
|
+
readonly selfHealAttempts: number;
|
|
1333
|
+
readonly pollIdleStreak: number;
|
|
1334
|
+
readonly lastReconnectRefetchAt: number | null;
|
|
1335
|
+
readonly needsResubscribe: boolean;
|
|
1336
|
+
readonly fetchAttempts: number;
|
|
1337
|
+
readonly liveUuid: string | null;
|
|
1338
|
+
readonly liveTable: string | null;
|
|
1339
|
+
readonly lanes: LaneState;
|
|
1340
|
+
}
|
|
1341
|
+
interface ClientState {
|
|
1342
|
+
readonly sessionId: string | null;
|
|
1343
|
+
readonly userId: string | null;
|
|
1344
|
+
/** The principal the current session salt was minted for. */
|
|
1345
|
+
readonly saltUserId: string | null;
|
|
1346
|
+
/**
|
|
1347
|
+
* This page's secret, sent with every registration (`$config.owner`). An
|
|
1348
|
+
* anonymous view answers only to the secret it was registered with, so it
|
|
1349
|
+
* lives as long as the page's views: a reconnect keeps it, and a reload
|
|
1350
|
+
* that keeps the salt keeps it too.
|
|
1351
|
+
*/
|
|
1352
|
+
readonly ownerSecret: string | null;
|
|
1353
|
+
/** Latest bucket-switch target; an older switch that wakes up to a newer target skips. */
|
|
1354
|
+
readonly pendingBucket: string | null;
|
|
1355
|
+
readonly tabId: string;
|
|
1356
|
+
readonly tabRole: TabRole;
|
|
1357
|
+
readonly bucketId: string | null;
|
|
1358
|
+
readonly localReady: boolean;
|
|
1359
|
+
readonly primed: boolean;
|
|
1360
|
+
readonly queries: ReadonlyMap<QueryHash, QueryEntry>;
|
|
1361
|
+
/** Hashes whose local registration is in flight (dedupes concurrent `query()` calls). */
|
|
1362
|
+
readonly registering: ReadonlySet<QueryHash>;
|
|
1363
|
+
/** Re-read attempts per hash while the server reports a view as `materializing`. */
|
|
1364
|
+
readonly membershipReread: ReadonlyMap<QueryHash, number>;
|
|
1365
|
+
/** Local body versions (`_00_rv`) by encoded record id. */
|
|
1366
|
+
readonly versions: ReadonlyMap<string, number>;
|
|
1367
|
+
/**
|
|
1368
|
+
* What each durable `_00_view` row vouches for, by view key: its members
|
|
1369
|
+
* and subquery children. A body no row names is an orphan: it never paints
|
|
1370
|
+
* before the server answers and the GC deletes it.
|
|
1371
|
+
*/
|
|
1372
|
+
readonly views: ReadonlyMap<string, ReadonlyArray<string>>;
|
|
1373
|
+
readonly outbox: ReadonlyArray<OutboxItem>;
|
|
1374
|
+
readonly pendingWrites: ReadonlyMap<string, PendingWrite>;
|
|
1375
|
+
readonly failedCount: number;
|
|
1376
|
+
/** Queries whose render inputs changed since their last materialization. */
|
|
1377
|
+
readonly dirty: ReadonlySet<QueryHash>;
|
|
1378
|
+
/** Queries whose server membership must be re-read. */
|
|
1379
|
+
readonly membershipDirty: ReadonlySet<QueryHash>;
|
|
1380
|
+
readonly sync: SyncSlice;
|
|
1381
|
+
}
|
|
1382
|
+
//#endregion
|
|
1383
|
+
//#region src/modules/ref-tables.d.ts
|
|
1384
|
+
type RefMode = 'single' | 'dedicated';
|
|
1385
|
+
/**
|
|
1386
|
+
* Sentinel user id for unauthenticated clients when anonymous live queries are
|
|
1387
|
+
* enabled. Mirrors `ssp_protocol::ANON_AUTH_ID`. It carries no `user:` prefix
|
|
1388
|
+
* so it can never collide with a real user id (those arrive as `user:<id>`);
|
|
1389
|
+
* both sides resolve it to the shared `_00_list_ref_anon` table.
|
|
1390
|
+
*/
|
|
1391
|
+
//#endregion
|
|
1392
|
+
//#region src/query/env.d.ts
|
|
1393
|
+
/**
|
|
1394
|
+
* Static configuration the sagas read. Plain data, built once by the runtime
|
|
1395
|
+
* from `Sp00kyConfig`; sagas never see the config object itself.
|
|
1396
|
+
*/
|
|
1397
|
+
interface SagaEnv {
|
|
1398
|
+
readonly schema: SchemaStructure;
|
|
1399
|
+
readonly refMode: RefMode;
|
|
1400
|
+
readonly anonLive: boolean;
|
|
1401
|
+
readonly remoteTimeoutMs: number;
|
|
1402
|
+
readonly pushTimeoutMs: number;
|
|
1403
|
+
readonly outboxBatchSize: number;
|
|
1404
|
+
readonly degradeAfter: number;
|
|
1405
|
+
readonly materializeDebounceMs: number;
|
|
1406
|
+
readonly pollBaseMs: number;
|
|
1407
|
+
readonly defaultTtlMs: number;
|
|
1408
|
+
}
|
|
1409
|
+
//#endregion
|
|
1410
|
+
//#region src/mutation/rows.d.ts
|
|
1411
|
+
interface FailedMutationRow {
|
|
1412
|
+
id: string;
|
|
1413
|
+
mutationType: MutationEventType;
|
|
1414
|
+
recordId: string;
|
|
1415
|
+
tableName: string;
|
|
1416
|
+
data?: Record<string, unknown>;
|
|
1417
|
+
beforeRecord?: Record<string, unknown> | null;
|
|
1418
|
+
error: {
|
|
1419
|
+
message: string;
|
|
1420
|
+
kind: 'application' | 'unreplayable';
|
|
1421
|
+
};
|
|
1422
|
+
attempts: number;
|
|
1423
|
+
createdAt: number;
|
|
1424
|
+
failedAt: number;
|
|
1425
|
+
revert: 'full' | 'partial' | 'none';
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Parse a record id READ BACK FROM THE STORE, stripping SurrealDB's `⟨⟩`
|
|
1429
|
+
* escaping (outbox ids contain `_`, so they come back escaped).
|
|
1430
|
+
*/
|
|
1431
|
+
//#endregion
|
|
1432
|
+
//#region src/kernel/interpreter.d.ts
|
|
1433
|
+
/** The five adapters plus the pure ports, as the interpreter sees them. */
|
|
1434
|
+
interface Adapters {
|
|
1435
|
+
local: Pick<LocalStore, 'query' | 'execute' | 'select' | 'getById' | 'upsert' | 'delete'> & {
|
|
1436
|
+
readonly epoch: number;
|
|
1437
|
+
};
|
|
1438
|
+
remote: {
|
|
1439
|
+
queryResponses(sql: string, vars?: Record<string, unknown>): Promise<StatementResult[]>;
|
|
1440
|
+
/**
|
|
1441
|
+
* Subscribe to `LIVE SELECT * FROM <table>`; the callback receives the
|
|
1442
|
+
* changed query hashes, plus the changed rows themselves when the
|
|
1443
|
+
* subscription was opened with the body joined on (`liveInlineBodies`).
|
|
1444
|
+
*/
|
|
1445
|
+
live(table: string, onChange: (hashes: string[], rows?: InlineRow[]) => void): Promise<string>;
|
|
1446
|
+
kill(uuid: string): Promise<void>;
|
|
1447
|
+
};
|
|
1448
|
+
ssp: {
|
|
1449
|
+
registerQueryPlan(plan: {
|
|
1450
|
+
queryHash: string;
|
|
1451
|
+
surql: string;
|
|
1452
|
+
params: Record<string, unknown>;
|
|
1453
|
+
ttl: Duration;
|
|
1454
|
+
lastActiveAt: Date;
|
|
1455
|
+
localArray: [];
|
|
1456
|
+
remoteArray: [];
|
|
1457
|
+
meta: {
|
|
1458
|
+
tableName: string;
|
|
1459
|
+
involvedTables?: string[];
|
|
1460
|
+
};
|
|
1461
|
+
}): StreamUpdate | undefined;
|
|
1462
|
+
unregisterQueryPlan(hash: string): void;
|
|
1463
|
+
ingestMany(records: IngestRecord[]): unknown;
|
|
1464
|
+
};
|
|
1465
|
+
timers: {
|
|
1466
|
+
set(key: string, ms: number, fire: () => void): void;
|
|
1467
|
+
clear(key: string): void;
|
|
1468
|
+
};
|
|
1469
|
+
clock: {
|
|
1470
|
+
now(): number;
|
|
1471
|
+
};
|
|
1472
|
+
ids: Record<IdScope, () => string>;
|
|
1473
|
+
hash(input: string): Promise<string>;
|
|
1474
|
+
services: ServiceCalls;
|
|
1475
|
+
}
|
|
1476
|
+
/** What the interpreter needs from the runtime: state, waiting, emitting, dispatching. */
|
|
1477
|
+
//#endregion
|
|
1478
|
+
//#region src/client/runtime.d.ts
|
|
1479
|
+
interface RuntimeOptions {
|
|
1480
|
+
env: SagaEnv;
|
|
1481
|
+
adapters: Adapters;
|
|
1482
|
+
logger: Logger$1;
|
|
1483
|
+
tabId: string;
|
|
1484
|
+
initialState?: ClientState;
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* The one effectful object of the engine: holds the state, runs sagas on
|
|
1488
|
+
* lanes, fires timers, fans events out to subscribers and schedules
|
|
1489
|
+
* materialization for dirty queries. No decision logic lives here.
|
|
1490
|
+
*/
|
|
1491
|
+
declare class Runtime {
|
|
1492
|
+
private stateValue;
|
|
1493
|
+
private readonly interpret;
|
|
1494
|
+
private readonly env;
|
|
1495
|
+
private readonly adapters;
|
|
1496
|
+
private readonly logger;
|
|
1497
|
+
private readonly serialLanes;
|
|
1498
|
+
private readonly dedupeLanes;
|
|
1499
|
+
private readonly waiters;
|
|
1500
|
+
private readonly recordSubs;
|
|
1501
|
+
private readonly statusSubs;
|
|
1502
|
+
private readonly authoritySubs;
|
|
1503
|
+
private readonly lastStatus;
|
|
1504
|
+
private readonly listeners;
|
|
1505
|
+
private readonly scheduledMaterialize;
|
|
1506
|
+
private readonly timerKeys;
|
|
1507
|
+
private lastActivity;
|
|
1508
|
+
private lastHealth;
|
|
1509
|
+
private disposed;
|
|
1510
|
+
constructor(opts: RuntimeOptions);
|
|
1511
|
+
get state(): ClientState;
|
|
1512
|
+
/** Run a saga, optionally on a lane. Rejections propagate to the caller. */
|
|
1513
|
+
run<R>(saga: Saga<R>, opts?: {
|
|
1514
|
+
lane?: Lane;
|
|
1515
|
+
signal?: AbortSignal;
|
|
1516
|
+
}): Promise<R>;
|
|
1517
|
+
/** Route an event to its saga. Never rejects: failures are logged. */
|
|
1518
|
+
dispatch(event: RuntimeEvent): Promise<void>;
|
|
1519
|
+
private setState;
|
|
1520
|
+
waitFor(until: (s: ClientState) => boolean, signal?: AbortSignal): Promise<void>;
|
|
1521
|
+
/** Deliver an outbound event to its subscribers and listeners. */
|
|
1522
|
+
emit(event: OutEvent): void;
|
|
1523
|
+
private notify;
|
|
1524
|
+
private safely;
|
|
1525
|
+
/** Observe outbound events by type (`'*'` for all). */
|
|
1526
|
+
on(type: OutEvent['type'] | '*', cb: (event: OutEvent) => void): () => void;
|
|
1527
|
+
subscribe(hash: QueryHash, cb: QueryUpdateCallback, options?: {
|
|
1528
|
+
immediate?: boolean;
|
|
1529
|
+
}): () => void;
|
|
1530
|
+
subscribeStatus(hash: QueryHash, cb: QueryStatusCallback, options?: {
|
|
1531
|
+
immediate?: boolean;
|
|
1532
|
+
}): () => void;
|
|
1533
|
+
subscribeAuthority(hash: QueryHash, cb: QueryAuthorityCallback, options?: {
|
|
1534
|
+
immediate?: boolean;
|
|
1535
|
+
}): () => void;
|
|
1536
|
+
/** Apply a reducer outside a saga (facade conveniences such as timing reports). */
|
|
1537
|
+
update(reducer: (s: ClientState) => ClientState): void;
|
|
1538
|
+
dispose(): void;
|
|
1539
|
+
}
|
|
1540
|
+
//#endregion
|
|
1541
|
+
//#region src/services/tabs/coordinator.d.ts
|
|
1542
|
+
interface CoordinatorHooks {
|
|
1543
|
+
/** Become the store owner: spawn the worker, open the pool under the given
|
|
1544
|
+
* per-leadership lock. Returns the resulting health for db-ready relays.
|
|
1545
|
+
* `resumeHeld` = keep the existing live worker (broker-restart path). */
|
|
1546
|
+
adoptOwner(bucketId: string, opts: {
|
|
1547
|
+
workerLockName: string;
|
|
1548
|
+
allowMemoryFallback: boolean;
|
|
1549
|
+
forceTakeover: boolean;
|
|
1550
|
+
resumeHeld: boolean;
|
|
1551
|
+
}): Promise<StorageHealth>;
|
|
1552
|
+
/** Attach to a leader's worker through `dbPort`. */
|
|
1553
|
+
adoptAttached(dbPort: MessagePort, snapshot: {
|
|
1554
|
+
bucketId: string;
|
|
1555
|
+
storageHealth: StorageHealth;
|
|
1556
|
+
leadershipId: number;
|
|
1557
|
+
}): Promise<void>;
|
|
1558
|
+
/** We were leader and got demoted (zombie thaw / stale promotion): tear the
|
|
1559
|
+
* owned worker down so its SAH handles free up. */
|
|
1560
|
+
releaseOwnership(): Promise<void>;
|
|
1561
|
+
/** The leader (or its port) is gone; queue/park ops until a new role lands. */
|
|
1562
|
+
onLeaderLost(reason: string): void;
|
|
1563
|
+
/** Leader side: forward a follower's dbPort into the owned worker. */
|
|
1564
|
+
exposeClientPort(clientId: string, port: MessagePort): Promise<void>;
|
|
1565
|
+
removeClientPort(clientId: string): Promise<void>;
|
|
1566
|
+
/** Sync layer role changes (implemented in the sync module). */
|
|
1567
|
+
/** Synchronous leader WIRING. Must not block: the role is published and
|
|
1568
|
+
* `leader-ready` is sent right after it returns. */
|
|
1569
|
+
becomeSyncLeader(hub: LeaderSyncHub): void;
|
|
1570
|
+
/** The blocking half of the leader handover (outbox drain, LIVE restart),
|
|
1571
|
+
* run in the background once the tab is already a working leader. */
|
|
1572
|
+
resumeSyncLeaderDuties(): Promise<void>;
|
|
1573
|
+
becomeSyncFollower(forwarder: SyncForwarder): void;
|
|
1574
|
+
becomeSyncSolo(): void;
|
|
1575
|
+
/** Current storage health, for db-ready sent to late-joining followers. */
|
|
1576
|
+
currentStorageHealth(): StorageHealth;
|
|
1577
|
+
}
|
|
1578
|
+
/** Leader-side fan-out surface handed to the sync layer. The sync router
|
|
1579
|
+
* (modules/sync/tab-router.ts) registers itself as the message handler. */
|
|
1580
|
+
declare class LeaderSyncHub {
|
|
1581
|
+
readonly leadershipId: number;
|
|
1582
|
+
private logger;
|
|
1583
|
+
private followers;
|
|
1584
|
+
private seq;
|
|
1585
|
+
onFollowerMessage: ((tabId: TabId, msg: FollowerToLeaderMessage) => void) | null;
|
|
1586
|
+
onFollowerDetached: ((tabId: TabId) => void) | null;
|
|
1587
|
+
constructor(leadershipId: number, logger: Logger$1);
|
|
1588
|
+
attach(tabId: TabId, port: MessagePort): void;
|
|
1589
|
+
detach(tabId: TabId): void;
|
|
1590
|
+
detachAll(): void;
|
|
1591
|
+
sendTo(tabId: TabId, msg: LeaderToFollowerMessage): void;
|
|
1592
|
+
broadcast(msg: LeaderToFollowerMessage, exceptTabId?: TabId): void;
|
|
1593
|
+
/** Stamped ingest relay; seq lets followers detect gaps. */
|
|
1594
|
+
relayIngest(tuples: IngestTuple[], exceptTabId?: TabId): void;
|
|
1595
|
+
get followerCount(): number;
|
|
1596
|
+
get relayedBatches(): number;
|
|
1597
|
+
}
|
|
1598
|
+
/** Follower half of the syncPort. Queues while detached (leaderless window)
|
|
1599
|
+
* and flushes on rebind; a lost-in-flight mutation notify is additionally
|
|
1600
|
+
* backstopped by the new leader reloading the shared outbox from the store. */
|
|
1601
|
+
declare class SyncForwarder {
|
|
1602
|
+
private tabId;
|
|
1603
|
+
private port;
|
|
1604
|
+
private queued;
|
|
1605
|
+
onLeaderMessage: ((msg: LeaderToFollowerMessage) => void) | null;
|
|
1606
|
+
constructor(tabId: TabId);
|
|
1607
|
+
rebind(port: MessagePort): void;
|
|
1608
|
+
unbind(): void;
|
|
1609
|
+
private post;
|
|
1610
|
+
mutationEnqueued(mutationId: string): void;
|
|
1611
|
+
/** An optimistic write this tab just ingested. Deliberately NOT queued while
|
|
1612
|
+
* detached: a new leader primes its circuit from the shared store, which
|
|
1613
|
+
* already holds the row, and replaying a stale tuple at it later would put
|
|
1614
|
+
* an older `_00_rv` in its version memo. */
|
|
1615
|
+
ingest(tuples: IngestTuple[]): void;
|
|
1616
|
+
requestPoll(): void;
|
|
1617
|
+
}
|
|
1618
|
+
declare class TabsCoordinator {
|
|
1619
|
+
private deps;
|
|
1620
|
+
role: TabRole$1;
|
|
1621
|
+
leadershipId: number;
|
|
1622
|
+
leaderTabId: TabId | null;
|
|
1623
|
+
brokerRole: 'pending' | TabRole$1;
|
|
1624
|
+
/** Wall time from `become-leader` to `leader-ready` on the last promotion.
|
|
1625
|
+
* Reported in the DevTools role-change event: a slow store adopt (OPFS pool
|
|
1626
|
+
* contention after the previous leader died) shows up here as seconds. */
|
|
1627
|
+
lastPromotionMs: number | null;
|
|
1628
|
+
private fingerprint;
|
|
1629
|
+
private bucketId;
|
|
1630
|
+
private broker;
|
|
1631
|
+
private hub;
|
|
1632
|
+
private forwarder;
|
|
1633
|
+
private roleListeners;
|
|
1634
|
+
private startResolve;
|
|
1635
|
+
private startReject;
|
|
1636
|
+
/** Ports received before start() resolves or between roles. */
|
|
1637
|
+
private promotionChain;
|
|
1638
|
+
private closed;
|
|
1639
|
+
constructor(deps: {
|
|
1640
|
+
tabId: TabId;
|
|
1641
|
+
fingerprint: string;
|
|
1642
|
+
hooks: CoordinatorHooks;
|
|
1643
|
+
logger: Logger$1;
|
|
1644
|
+
/** Fired on pagehide while this tab leads: last-chance OPFS release. */
|
|
1645
|
+
onLeaderPageHide?: () => void;
|
|
1646
|
+
});
|
|
1647
|
+
/** Serialize role transitions; each is small but async (worker opens). */
|
|
1648
|
+
private enqueue;
|
|
1649
|
+
onRoleChange(cb: (role: TabRole$1) => void): () => void;
|
|
1650
|
+
private setRole;
|
|
1651
|
+
/** Connect the broker and resolve once this tab has a usable store role.
|
|
1652
|
+
* Rejects when no role lands in time; the caller then boots solo. */
|
|
1653
|
+
start(bucketId: string): Promise<TabRole$1>;
|
|
1654
|
+
/** Bucket switch: leave the old namespace, join the new one. Resolves when
|
|
1655
|
+
* a role lands in the new namespace. */
|
|
1656
|
+
moveToBucket(bucketId: string): Promise<TabRole$1>;
|
|
1657
|
+
private promote;
|
|
1658
|
+
private demote;
|
|
1659
|
+
private tabLock;
|
|
1660
|
+
private teardownLeader;
|
|
1661
|
+
private teardownFollower;
|
|
1662
|
+
private serveFollower;
|
|
1663
|
+
private attachToLeader;
|
|
1664
|
+
private handleLeaderGone;
|
|
1665
|
+
private fallbackToSolo;
|
|
1666
|
+
get syncHub(): LeaderSyncHub | null;
|
|
1667
|
+
get syncForwarder(): SyncForwarder | null;
|
|
1668
|
+
get tabId(): TabId;
|
|
1669
|
+
stop(): Promise<void>;
|
|
1670
|
+
}
|
|
1671
|
+
//#endregion
|
|
1672
|
+
//#region src/services/blobs/blob-store.d.ts
|
|
1673
|
+
/**
|
|
1674
|
+
* Byte storage for cached bucket files.
|
|
1675
|
+
*
|
|
1676
|
+
* The default implementation is OPFS. Bucket files never arrive over HTTP in
|
|
1677
|
+
* this client — `BucketHandle.get()` is a SurrealQL RPC on the sync socket — so
|
|
1678
|
+
* neither the browser's HTTP cache nor the Cache API can hold them. We persist
|
|
1679
|
+
* the bytes ourselves, and OPFS is the cheapest place to put them: a read is
|
|
1680
|
+
* `getFile()` → a disk-backed lazy `File` that `URL.createObjectURL` can serve
|
|
1681
|
+
* without ever moving the bytes through the JS heap.
|
|
1682
|
+
*
|
|
1683
|
+
* Layout is real nested directories rather than one hashed filename:
|
|
1684
|
+
*
|
|
1685
|
+
* sp00ky-blobs/<namespace>/<bucket>/<...path segments>
|
|
1686
|
+
*
|
|
1687
|
+
* That costs a `getDirectoryHandle` per segment on write, and buys the property
|
|
1688
|
+
* the whole orphan story rests on: the full `(bucket, path)` key is recoverable
|
|
1689
|
+
* from a directory walk alone. The `_00_blob` manifest can therefore be wiped
|
|
1690
|
+
* (memory fallback, SQLite pool wipe, IndexedDB corruption recovery) and be
|
|
1691
|
+
* rebuilt from disk instead of taking the cached bytes down with it.
|
|
1692
|
+
*/
|
|
1693
|
+
/** Identifies one cached file: the bucket it lives in and its path within. */
|
|
1694
|
+
interface BlobKey {
|
|
1695
|
+
bucket: string;
|
|
1696
|
+
path: string;
|
|
1697
|
+
}
|
|
1698
|
+
/** What a directory walk can tell us about a stored file, with no manifest. */
|
|
1699
|
+
interface BlobStat {
|
|
1700
|
+
key: BlobKey;
|
|
1701
|
+
size: number;
|
|
1702
|
+
/** File mtime. Seeds `lastAccess` when a manifest row has to be rebuilt. */
|
|
1703
|
+
mtime: number;
|
|
1704
|
+
}
|
|
1705
|
+
interface BlobStore {
|
|
1706
|
+
/** False for {@link MemoryBlobStore} and for OPFS-less environments: the
|
|
1707
|
+
* cache still dedupes and serves within a tab, but nothing survives reload. */
|
|
1708
|
+
readonly persistent: boolean;
|
|
1709
|
+
/** Namespace (the local bucketId) all keys are resolved under. */
|
|
1710
|
+
readonly namespace: string;
|
|
1711
|
+
read(key: BlobKey): Promise<Blob | null>;
|
|
1712
|
+
/** Returns the number of bytes written. Throws on quota exhaustion. */
|
|
1713
|
+
write(key: BlobKey, bytes: Blob): Promise<number>;
|
|
1714
|
+
remove(key: BlobKey): Promise<void>;
|
|
1715
|
+
/** Every committed file under the current namespace. Sweeps torn writes. */
|
|
1716
|
+
list(): Promise<BlobStat[]>;
|
|
1717
|
+
/** Drop the whole namespace (sign-out with `clearOnSignOut`, or a reset). */
|
|
1718
|
+
clear(): Promise<void>;
|
|
1719
|
+
/** Point at another namespace. Does not touch the bytes of the old one. */
|
|
1720
|
+
setNamespace(namespace: string): void;
|
|
1721
|
+
}
|
|
1722
|
+
//#endregion
|
|
1723
|
+
//#region src/services/blobs/blob-manifest.d.ts
|
|
1724
|
+
interface BlobEntry {
|
|
1725
|
+
/** `${bucket}/${path}` — also the `_00_blob` row id. */
|
|
1726
|
+
id: string;
|
|
1727
|
+
bucket: string;
|
|
1728
|
+
path: string;
|
|
1729
|
+
size: number;
|
|
1730
|
+
contentType: string;
|
|
1731
|
+
createdAt: number;
|
|
1732
|
+
lastAccess: number;
|
|
1733
|
+
hits: number;
|
|
1734
|
+
/** Exempt from pressure eviction. Never expires on its own. */
|
|
1735
|
+
pinned: boolean;
|
|
1736
|
+
}
|
|
1737
|
+
declare class BlobManifest {
|
|
1738
|
+
private local;
|
|
1739
|
+
private entries;
|
|
1740
|
+
/** Ids whose in-memory state has not been written back yet. */
|
|
1741
|
+
private dirty;
|
|
1742
|
+
private removed;
|
|
1743
|
+
private flushing;
|
|
1744
|
+
constructor(local: LocalStore);
|
|
1745
|
+
/**
|
|
1746
|
+
* Hydrate from the rows matching `keys`. Ids come from the OPFS listing, so
|
|
1747
|
+
* this never needs a full-table scan (and therefore never needs a QueryPlan).
|
|
1748
|
+
* Any read failure yields an empty manifest: reconcile then rebuilds every
|
|
1749
|
+
* row from disk, which is exactly the desired degradation.
|
|
1750
|
+
*/
|
|
1751
|
+
load(ids: string[]): Promise<void>;
|
|
1752
|
+
get(key: BlobKey): BlobEntry | undefined;
|
|
1753
|
+
getById(id: string): BlobEntry | undefined;
|
|
1754
|
+
all(): BlobEntry[];
|
|
1755
|
+
totalBytes(): number;
|
|
1756
|
+
pinnedBytes(): number;
|
|
1757
|
+
put(entry: BlobEntry): void;
|
|
1758
|
+
touch(id: string, now: number): void;
|
|
1759
|
+
setPinned(id: string, pinned: boolean): boolean;
|
|
1760
|
+
remove(id: string): void;
|
|
1761
|
+
/** Forget everything without scheduling deletes — for a bucket switch, where
|
|
1762
|
+
* the rows belong to the store we are leaving and must stay put. */
|
|
1763
|
+
reset(): void;
|
|
1764
|
+
hasPendingWrites(): boolean;
|
|
1765
|
+
/**
|
|
1766
|
+
* Write back pending changes. Serialized: a second concurrent flush awaits
|
|
1767
|
+
* the first rather than racing it into the same rows. Failures are swallowed
|
|
1768
|
+
* on purpose — a lost metadata write costs an LRU timestamp, and the entry is
|
|
1769
|
+
* rebuilt from disk on the next reconcile.
|
|
1770
|
+
*/
|
|
1771
|
+
flush(): Promise<void>;
|
|
1772
|
+
private doFlush;
|
|
1773
|
+
}
|
|
1774
|
+
//#endregion
|
|
1775
|
+
//#region src/services/blobs/blob-cache.d.ts
|
|
1776
|
+
interface BlobUrlLease {
|
|
1777
|
+
url: string;
|
|
1778
|
+
release(): void;
|
|
1779
|
+
}
|
|
1780
|
+
interface BlobReadOptions {
|
|
1781
|
+
/** Write through to L1 on a miss. Default true. */
|
|
1782
|
+
persist?: boolean;
|
|
1783
|
+
/** Mark the entry exempt from pressure eviction. */
|
|
1784
|
+
pin?: boolean;
|
|
1785
|
+
/**
|
|
1786
|
+
* Default `'never'`: a bucket path is treated as immutable, which is how the
|
|
1787
|
+
* client writes them (`crypto.randomUUID() + ext`). `'head'` spends a remote
|
|
1788
|
+
* `head()` to compare sizes before trusting L1.
|
|
1789
|
+
*/
|
|
1790
|
+
revalidate?: 'never' | 'head';
|
|
1791
|
+
/** Skip L0/L1 entirely and refill from remote. Backs `refetch()`. */
|
|
1792
|
+
reload?: boolean;
|
|
1793
|
+
}
|
|
1794
|
+
interface BlobCacheStats {
|
|
1795
|
+
entries: number;
|
|
1796
|
+
totalBytes: number;
|
|
1797
|
+
budgetBytes: number;
|
|
1798
|
+
pinnedBytes: number;
|
|
1799
|
+
evictedEntries: number;
|
|
1800
|
+
evictedBytes: number;
|
|
1801
|
+
reconciledEntries: number;
|
|
1802
|
+
hits: number;
|
|
1803
|
+
misses: number;
|
|
1804
|
+
persistent: boolean;
|
|
1805
|
+
/** True when pinned bytes alone exceed the budget: new entries stop being
|
|
1806
|
+
* written rather than pinned ones being thrown away. */
|
|
1807
|
+
persistPaused: boolean;
|
|
1808
|
+
}
|
|
1809
|
+
interface BlobCacheOptions {
|
|
1810
|
+
store: BlobStore;
|
|
1811
|
+
manifest: BlobManifest;
|
|
1812
|
+
/** L2 read. Resolves to null when the file does not exist remotely. */
|
|
1813
|
+
fetchRemote(key: BlobKey): Promise<Blob | null>;
|
|
1814
|
+
/** L2 metadata, for `revalidate: 'head'`. */
|
|
1815
|
+
headRemote?(key: BlobKey): Promise<Record<string, unknown> | null>;
|
|
1816
|
+
logger: Logger$1;
|
|
1817
|
+
maxBytes: number;
|
|
1818
|
+
now?: () => number;
|
|
1819
|
+
/** Injected so the URL layer is exercisable off a DOM (node tests). */
|
|
1820
|
+
urls?: {
|
|
1821
|
+
create(blob: Blob): string;
|
|
1822
|
+
revoke(url: string): void;
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
declare class BlobCache {
|
|
1826
|
+
private readonly store;
|
|
1827
|
+
private readonly manifest;
|
|
1828
|
+
private readonly fetchRemote;
|
|
1829
|
+
private readonly headRemote?;
|
|
1830
|
+
private readonly logger;
|
|
1831
|
+
private readonly now;
|
|
1832
|
+
private readonly urlFactory;
|
|
1833
|
+
private maxBytes;
|
|
1834
|
+
private persistPaused;
|
|
1835
|
+
/** Set after a quota failure survives one forced eviction. */
|
|
1836
|
+
private persistDisabled;
|
|
1837
|
+
private readonly urls;
|
|
1838
|
+
/** Ids at zero references, oldest first — the hot-URL window. */
|
|
1839
|
+
private idleUrls;
|
|
1840
|
+
private readonly inflight;
|
|
1841
|
+
private flushTimer;
|
|
1842
|
+
private readonly onPageHide;
|
|
1843
|
+
/**
|
|
1844
|
+
* Resolves once the manifest has been reconciled against disk. Reads await
|
|
1845
|
+
* it, so `start()` does NOT have to be awaited on the boot path — blocking
|
|
1846
|
+
* boot on an OPFS directory walk delayed the WebSocket connect (and with it
|
|
1847
|
+
* the connection supervisor) for no benefit.
|
|
1848
|
+
*/
|
|
1849
|
+
private ready;
|
|
1850
|
+
private hits;
|
|
1851
|
+
private misses;
|
|
1852
|
+
private evictedEntries;
|
|
1853
|
+
private evictedBytes;
|
|
1854
|
+
private reconciledEntries;
|
|
1855
|
+
constructor(opts: BlobCacheOptions);
|
|
1856
|
+
/** Coalesce manifest write-back. Metadata only, so losing the tail costs an
|
|
1857
|
+
* LRU timestamp that reconcile reseeds from the file mtime. */
|
|
1858
|
+
private scheduleFlush;
|
|
1859
|
+
/**
|
|
1860
|
+
* Resolve the bytes for `key`, filling L1 on the way when `persist` is on.
|
|
1861
|
+
* Returns null when the file does not exist remotely and is not cached.
|
|
1862
|
+
*/
|
|
1863
|
+
read(key: BlobKey, options?: BlobReadOptions): Promise<Blob | null>;
|
|
1864
|
+
/**
|
|
1865
|
+
* An object URL for `key`, refcounted. Callers MUST `release()`; the URL is
|
|
1866
|
+
* revoked once the last holder lets go and it falls out of the hot window.
|
|
1867
|
+
*/
|
|
1868
|
+
acquireUrl(key: BlobKey, options?: BlobReadOptions): Promise<BlobUrlLease | null>;
|
|
1869
|
+
private lease;
|
|
1870
|
+
private releaseUrl;
|
|
1871
|
+
private revokeUrl;
|
|
1872
|
+
/** L1 lookup with the size check that catches torn and cross-tab writes. */
|
|
1873
|
+
private readLocal;
|
|
1874
|
+
/** True when the remote agrees with the cached size, or cannot be reached. */
|
|
1875
|
+
private headMatches;
|
|
1876
|
+
private fetchDeduped;
|
|
1877
|
+
private persist;
|
|
1878
|
+
private writeThrough;
|
|
1879
|
+
/** Forget one path everywhere. Called on `bucket.put()`/`bucket.delete()`. */
|
|
1880
|
+
invalidate(key: BlobKey): Promise<void>;
|
|
1881
|
+
private dropLocal;
|
|
1882
|
+
setPinned(key: BlobKey, pinned: boolean): void;
|
|
1883
|
+
/**
|
|
1884
|
+
* Bring total bytes under budget by dropping the least recently used
|
|
1885
|
+
* entries. Pinned entries and anything with a live object URL are skipped —
|
|
1886
|
+
* evicting bytes that a mounted `<img>` is displaying would blank it.
|
|
1887
|
+
*/
|
|
1888
|
+
private enforceBudget;
|
|
1889
|
+
/** Evict LRU-first until at or below `target`. Returns the resulting total. */
|
|
1890
|
+
private evictTo;
|
|
1891
|
+
/**
|
|
1892
|
+
* Rebuild the manifest from what is actually on disk. OPFS wins on existence
|
|
1893
|
+
* in both directions: files with no row get a row (seeded from mtime), rows
|
|
1894
|
+
* are only loaded for files that exist, and torn `.part-` writes are swept by
|
|
1895
|
+
* the walk itself.
|
|
1896
|
+
*
|
|
1897
|
+
* Rows whose file vanished outside our control (a browser origin eviction)
|
|
1898
|
+
* are left in `_00_blob`. They are inert — `load()` only ever asks for ids it
|
|
1899
|
+
* found on disk — and are overwritten if that path is cached again.
|
|
1900
|
+
*/
|
|
1901
|
+
reconcile(): Promise<void>;
|
|
1902
|
+
/** Warm the cache for offline use. Skips anything already cached. */
|
|
1903
|
+
prefetch(keys: BlobKey[]): Promise<void>;
|
|
1904
|
+
/** Bind to the boot bucket and hydrate the manifest from disk. Separate from
|
|
1905
|
+
* {@link setNamespace} because boot must reconcile even when the namespace
|
|
1906
|
+
* it lands on is the one the store was constructed with. */
|
|
1907
|
+
start(namespace: string): Promise<void>;
|
|
1908
|
+
/** Repoint at another local bucket. The bytes of the old one stay on disk so
|
|
1909
|
+
* switching back (or signing back in) is still warm. */
|
|
1910
|
+
setNamespace(namespace: string): Promise<void>;
|
|
1911
|
+
setMaxBytes(maxBytes: number): void;
|
|
1912
|
+
/** Delete every cached byte in the current namespace. */
|
|
1913
|
+
clear(): Promise<void>;
|
|
1914
|
+
flush(): Promise<void>;
|
|
1915
|
+
/** Flush metadata and drop every object URL. Must run before the local store
|
|
1916
|
+
* closes — the flush writes through it. */
|
|
1917
|
+
close(): Promise<void>;
|
|
1918
|
+
stats(): BlobCacheStats;
|
|
1919
|
+
}
|
|
1920
|
+
//#endregion
|
|
1921
|
+
//#region src/client/services.d.ts
|
|
1922
|
+
/** Everything the facade talks to besides the runtime. */
|
|
1923
|
+
interface Services<S extends SchemaStructure> {
|
|
1924
|
+
logger: Logger$1;
|
|
1925
|
+
/** What the logger wrote, for the DevTools Logs tab. Absent in test fakes. */
|
|
1926
|
+
logTap?: LogTap;
|
|
1927
|
+
local: LocalStore;
|
|
1928
|
+
remote: RemoteDatabaseService;
|
|
1929
|
+
connectionSupervisor: ConnectionSupervisor;
|
|
1930
|
+
blobs: BlobCache;
|
|
1931
|
+
persistence: PersistenceClient;
|
|
1932
|
+
streamProcessor: StreamProcessorService;
|
|
1933
|
+
migrator: LocalMigrator;
|
|
1934
|
+
crdt: CrdtManager;
|
|
1935
|
+
auth: AuthService<S>;
|
|
1936
|
+
tabs: TabsCoordinator | null;
|
|
1937
|
+
tabsUnsupportedReason: string | undefined;
|
|
1938
|
+
tabId: string;
|
|
1939
|
+
}
|
|
1940
|
+
/** What the services need back from the runtime once it exists. */
|
|
1941
|
+
//#endregion
|
|
1942
|
+
//#region src/utils/blurhash.d.ts
|
|
1943
|
+
/**
|
|
1944
|
+
* Blurhash generation settings. `true` enables with the defaults below, `false`
|
|
1945
|
+
* disables. Resolution order for a put: per-call option > client config >
|
|
1946
|
+
* default ON. See {@link Sp00kyConfig.blurhash}.
|
|
1947
|
+
*/
|
|
1948
|
+
type BlurhashSetting = boolean | BlurhashEncodeOptions;
|
|
1949
|
+
interface BlurhashEncodeOptions {
|
|
1950
|
+
/** Horizontal detail components, 1-9. Defaults to 4. */
|
|
1951
|
+
componentX?: number;
|
|
1952
|
+
/** Vertical detail components, 1-9. Defaults to 3. */
|
|
1953
|
+
componentY?: number;
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Where an image's blurhash lives: a tiny sidecar object in the same bucket.
|
|
1957
|
+
* Buckets have no per-object metadata channel (`put` is just `.put($content)`),
|
|
1958
|
+
* so the hash for `covers/x_t.webp` is the text object `covers/x_t.webp.bh`.
|
|
1959
|
+
*/
|
|
1960
|
+
declare function blurhashSidecarPath(path: string): string;
|
|
1961
|
+
/** Extensions `bucket.put` treats as images worth hashing. */
|
|
1962
|
+
declare const BLURHASH_IMAGE_EXTENSIONS: readonly ["webp", "png", "jpg", "jpeg", "gif", "avif", "bmp"];
|
|
1963
|
+
declare function isImagePath(path: string): boolean;
|
|
1964
|
+
/**
|
|
1965
|
+
* Decode `content` as an image and compute its blurhash. Browser-only: returns
|
|
1966
|
+
* null (never throws) when image decoding is unavailable (node, workers without
|
|
1967
|
+
* canvas), when the bytes are not a decodable image, or on any other failure —
|
|
1968
|
+
* a missing hash must never break the upload that triggered it.
|
|
1969
|
+
*/
|
|
1970
|
+
declare function encodeImageToBlurhash(content: string | Uint8Array | Blob, options?: BlurhashEncodeOptions): Promise<string | null>;
|
|
1971
|
+
//#endregion
|
|
1972
|
+
//#region src/client/bucket-handle.d.ts
|
|
1973
|
+
/** Coerce whatever the `.get()` RPC hands back into a Blob. */
|
|
1974
|
+
declare function bucketContentToBlob(content: unknown): Blob | null;
|
|
1975
|
+
interface BucketPutOptions {
|
|
1976
|
+
/** Override the client-level {@link Sp00kyConfig.blurhash} setting for this put. */
|
|
1977
|
+
blurhash?: BlurhashSetting;
|
|
1978
|
+
}
|
|
1979
|
+
interface BucketPutResult {
|
|
1980
|
+
/** The computed blurhash when the content was a hashable image; else null. */
|
|
1981
|
+
blurhash: string | null;
|
|
1982
|
+
}
|
|
1983
|
+
interface BucketHandleSettings {
|
|
1984
|
+
blurhash?: BlurhashSetting;
|
|
1985
|
+
logger?: {
|
|
1986
|
+
warn: (obj: unknown, msg?: string) => void;
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
198
1989
|
declare class BucketHandle {
|
|
199
1990
|
private bucketName;
|
|
200
1991
|
private remote;
|
|
201
|
-
|
|
202
|
-
|
|
1992
|
+
/** Absent on the raw handle the cache itself reads through. */
|
|
1993
|
+
private blobs?;
|
|
1994
|
+
private settings?;
|
|
1995
|
+
constructor(bucketName: string, remote: RemoteDatabaseService, /** Absent on the raw handle the cache itself reads through. */
|
|
1996
|
+
blobs?: (BlobCache | null) | undefined, settings?: BucketHandleSettings | undefined);
|
|
1997
|
+
/** Effective blurhash setting: per-call option > client config > default ON. */
|
|
1998
|
+
private resolveBlurhash;
|
|
1999
|
+
put(path: string, content: string | Uint8Array | Blob, options?: BucketPutOptions): Promise<BucketPutResult>;
|
|
2000
|
+
/**
|
|
2001
|
+
* The blurhash stored alongside an uploaded image (see
|
|
2002
|
+
* {@link blurhashSidecarPath}), or null when there is none. Reads through the
|
|
2003
|
+
* blob cache, so a warm client answers from OPFS without a network hop, and
|
|
2004
|
+
* misses are remembered per tab so a hashless image costs at most one
|
|
2005
|
+
* serialized remote read per session.
|
|
2006
|
+
*/
|
|
2007
|
+
blurhash(path: string): Promise<string | null>;
|
|
203
2008
|
get(path: string): Promise<unknown>;
|
|
2009
|
+
/**
|
|
2010
|
+
* Read through the local blob cache: OPFS first, the bucket second. Unlike
|
|
2011
|
+
* {@link get} this survives a reload and works offline. Returns null when the
|
|
2012
|
+
* file exists in neither place.
|
|
2013
|
+
*/
|
|
2014
|
+
read(path: string, options?: BlobReadOptions): Promise<Blob | null>;
|
|
2015
|
+
/**
|
|
2016
|
+
* A refcounted object URL for `path`, suitable for `<img src>`. The caller
|
|
2017
|
+
* MUST call `release()` when the URL goes off screen. Returns null when the
|
|
2018
|
+
* file does not exist, or when object URLs are unavailable (non-browser).
|
|
2019
|
+
*/
|
|
2020
|
+
url(path: string, options?: BlobReadOptions): Promise<BlobUrlLease | null>;
|
|
2021
|
+
/** Exempt `path` from pressure eviction. Pinned bytes never expire. */
|
|
2022
|
+
pin(path: string): void;
|
|
2023
|
+
unpin(path: string): void;
|
|
2024
|
+
/** Drop `path` from the local cache without touching the remote file. */
|
|
2025
|
+
evict(path: string): Promise<void>;
|
|
2026
|
+
/** Warm the cache for offline use. Already-cached paths are skipped. */
|
|
2027
|
+
prefetch(paths: string[]): Promise<void>;
|
|
204
2028
|
delete(path: string): Promise<void>;
|
|
205
2029
|
exists(path: string): Promise<boolean>;
|
|
206
2030
|
head(path: string): Promise<Record<string, unknown>>;
|
|
@@ -208,55 +2032,161 @@ declare class BucketHandle {
|
|
|
208
2032
|
rename(sourcePath: string, targetPath: string): Promise<void>;
|
|
209
2033
|
list(prefix?: string): Promise<string[]>;
|
|
210
2034
|
}
|
|
2035
|
+
//#endregion
|
|
2036
|
+
//#region src/client/sp00ky-client.d.ts
|
|
2037
|
+
interface Sp00kyClientDeps<S extends SchemaStructure> {
|
|
2038
|
+
services?: Services<S>;
|
|
2039
|
+
runtime?: Runtime;
|
|
2040
|
+
env?: Partial<SagaEnv>;
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* The public client. Every method is a thin translation into a saga run, a
|
|
2044
|
+
* state read, or a subscription on the runtime; the engine's behaviour lives
|
|
2045
|
+
* in the sagas.
|
|
2046
|
+
*/
|
|
211
2047
|
declare class Sp00kyClient<S extends SchemaStructure> {
|
|
212
|
-
private config;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
private
|
|
218
|
-
private
|
|
219
|
-
private
|
|
220
|
-
private
|
|
221
|
-
private
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
2048
|
+
private readonly config;
|
|
2049
|
+
readonly auth: AuthService<S>;
|
|
2050
|
+
readonly streamProcessor: StreamProcessorService;
|
|
2051
|
+
/** Web Push for this browser: subscribe, devices, self-pushes, service worker bridge. */
|
|
2052
|
+
readonly webPush: WebPushModule;
|
|
2053
|
+
private readonly services;
|
|
2054
|
+
private readonly env;
|
|
2055
|
+
private readonly runtime;
|
|
2056
|
+
private readonly featureFlags;
|
|
2057
|
+
private readonly appReleases;
|
|
2058
|
+
/** Teardown for subscriptions the client itself owns. */
|
|
2059
|
+
private readonly disposers;
|
|
2060
|
+
/** Assigned in `wireAdapters`, which the constructor always runs. */
|
|
2061
|
+
private impersonationBanner;
|
|
2062
|
+
private readonly devTools;
|
|
2063
|
+
private hub;
|
|
2064
|
+
private forwarder;
|
|
2065
|
+
private detachWindow;
|
|
2066
|
+
private sharedActive;
|
|
2067
|
+
constructor(config: Sp00kyConfig<S>, deps?: Sp00kyClientDeps<S>);
|
|
2068
|
+
/** Adapter callbacks become runtime events; runtime events reach the legacy services. */
|
|
2069
|
+
private wireAdapters;
|
|
2070
|
+
/** Runtime events the DevTools panel follows; query transitions also go to the log. */
|
|
2071
|
+
private forwardToDevTools;
|
|
233
2072
|
init(): Promise<void>;
|
|
234
2073
|
close(): Promise<void>;
|
|
235
|
-
|
|
236
|
-
deauthenticate(): Promise<void>;
|
|
2074
|
+
isLocalReady(): boolean;
|
|
237
2075
|
query<Table extends TableNames<S>>(table: Table, options: QueryOptions<TableModel<GetTable<S, Table>>, false>, ttl?: QueryTimeToLive): QueryBuilder<S, Table, Sp00kyQueryResultPromise>;
|
|
2076
|
+
private registerInput;
|
|
238
2077
|
private initQuery;
|
|
2078
|
+
/**
|
|
2079
|
+
* The raw remote escape hatches bypass the query builder, so their shapes
|
|
2080
|
+
* can never be on the server's query allowlist. When the generated schema
|
|
2081
|
+
* says the allowlist is on, refuse them unless the app opted in explicitly.
|
|
2082
|
+
*/
|
|
2083
|
+
private assertRawRemoteAllowed;
|
|
239
2084
|
queryRaw(sql: string, params: Record<string, any>, ttl: QueryTimeToLive): Promise<string>;
|
|
2085
|
+
/**
|
|
2086
|
+
* Preload = register a query nobody subscribes to. Resolved before on this
|
|
2087
|
+
* device: returns at once. Never resolved: resolves when the server's
|
|
2088
|
+
* membership and every body are local. `signal` aborts the wait.
|
|
2089
|
+
*/
|
|
2090
|
+
preload(finalQuery: FinalQuery<S, any, any, any, any, any>, options?: PreloadOptions): Promise<void>;
|
|
240
2091
|
subscribe(queryHash: string, callback: (records: Record<string, any>[]) => void, options?: {
|
|
241
2092
|
immediate?: boolean;
|
|
242
2093
|
}): Promise<() => void>;
|
|
2094
|
+
deregisterQuery(queryHash: string): void;
|
|
2095
|
+
subscribeQueryStatus(queryHash: string, callback: QueryStatusCallback, options?: {
|
|
2096
|
+
immediate?: boolean;
|
|
2097
|
+
}): () => void;
|
|
2098
|
+
subscribeQueryAuthority(queryHash: string, callback: QueryAuthorityCallback, options?: {
|
|
2099
|
+
immediate?: boolean;
|
|
2100
|
+
}): () => void;
|
|
2101
|
+
reportFrontendTiming(queryHash: string, ms: number): void;
|
|
2102
|
+
create<T extends Record<string, unknown>>(id: string, data: T): Promise<T>;
|
|
2103
|
+
update<T extends Record<string, unknown>>(_table: string, id: string, data: Partial<T>, options?: UpdateOptions): Promise<T>;
|
|
2104
|
+
delete(_table: string, id: string): Promise<void>;
|
|
243
2105
|
run<B extends BackendNames<S>, R extends BackendRoutes<S, B>>(backend: B, path: R, payload: RoutePayload<S, B, R>, options?: RunOptions): Promise<void>;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
2106
|
+
get failedMutationCount(): number;
|
|
2107
|
+
subscribeToFailedMutations(cb: (count: number) => void): () => void;
|
|
2108
|
+
listFailedMutations(): Promise<FailedMutationRow[]>;
|
|
2109
|
+
retryFailedMutation(mutationId: string): Promise<boolean>;
|
|
2110
|
+
discardFailedMutation(mutationId: string): Promise<boolean>;
|
|
2111
|
+
get tabRole(): TabRole$1 | null;
|
|
2112
|
+
get remoteClient(): Surreal;
|
|
2113
|
+
get localClient(): unknown;
|
|
2114
|
+
get pendingMutationCount(): number;
|
|
2115
|
+
/** Kept for the e2e suite; the saga core has no LIVE retry ladder. */
|
|
2116
|
+
get liveRetryCount(): number;
|
|
2117
|
+
subscribeToPendingMutations(cb: (count: number) => void): () => void;
|
|
2118
|
+
get fetchingQueryCount(): number;
|
|
2119
|
+
subscribeToFetchActivity(cb: (fetching: number) => void): () => void;
|
|
2120
|
+
get syncHealth(): SyncHealth;
|
|
2121
|
+
subscribeToSyncHealth(cb: (health: SyncHealth) => void): () => void;
|
|
2122
|
+
get storageHealth(): StorageHealth;
|
|
2123
|
+
subscribeToStorageHealth(cb: (health: StorageHealth) => void): () => void;
|
|
2124
|
+
/** The engine state, read-only. For tests and DevTools. */
|
|
2125
|
+
get state(): ClientState;
|
|
2126
|
+
/** Feed an event into the engine. For adapters, tests and DevTools. */
|
|
2127
|
+
dispatch(event: RuntimeEvent): Promise<void>;
|
|
2128
|
+
authenticate(token: string): Promise<surrealdb0.Tokens>;
|
|
2129
|
+
deauthenticate(): Promise<void>;
|
|
250
2130
|
useRemote<T>(fn: (client: Surreal) => Promise<T> | T): Promise<T>;
|
|
251
|
-
|
|
252
|
-
|
|
2131
|
+
remoteQuery<T extends unknown[]>(sql: string, vars?: Record<string, unknown>): Promise<T>;
|
|
2132
|
+
/**
|
|
2133
|
+
* Tell the client that the app's own impersonation banner is on screen.
|
|
2134
|
+
* Only meaningful with `impersonationBanner: { mode: 'custom' }`, where it
|
|
2135
|
+
* cancels the fallback that would otherwise show the built-in banner. Call
|
|
2136
|
+
* it while the custom banner is rendered (the Solid `useImpersonation`
|
|
2137
|
+
* hook does it for you with `rendersBanner: true`).
|
|
2138
|
+
*/
|
|
2139
|
+
acknowledgeImpersonationBanner(): void;
|
|
2140
|
+
feature(key: string, options?: FeatureFlagOptions): FeatureFlagHandle;
|
|
2141
|
+
setFeatureOverride(key: string, variant: string | null, payload?: unknown): void;
|
|
2142
|
+
clearFeatureOverrides(): void;
|
|
2143
|
+
getFeatureOverrides(): Record<string, FeatureFlagOverride>;
|
|
2144
|
+
appRelease(app: string, options?: AppReleaseOptions): AppReleaseHandle;
|
|
2145
|
+
/** Open a `@crdt` field for editing. `draft` (from {@link createCrdtDraft})
|
|
2146
|
+
* hands over edits made before the record existed. */
|
|
2147
|
+
openCrdtField(table: string, recordId: string, field: string, draft?: CrdtField): Promise<CrdtField>;
|
|
2148
|
+
/** A detached `@crdt` document for a record not created yet; see
|
|
2149
|
+
* {@link openCrdtField}. */
|
|
2150
|
+
createCrdtDraft(table: string, field: string): Promise<CrdtField>;
|
|
2151
|
+
/** Close a `@crdt` field. The last close resolves once its unsent edits
|
|
2152
|
+
* are queued in the outbox; offline, once they are saved on the device. */
|
|
2153
|
+
closeCrdtField(table: string, recordId: string, field: string): Promise<void>;
|
|
2154
|
+
bucket<B extends BucketNames<S>>(name: B): BucketHandle;
|
|
2155
|
+
getBlobCacheStats(): BlobCacheStats;
|
|
253
2156
|
}
|
|
254
2157
|
//#endregion
|
|
255
|
-
//#region src/utils/
|
|
256
|
-
|
|
2158
|
+
//#region src/utils/semver.d.ts
|
|
2159
|
+
/** True when `a` is a valid version strictly greater than valid version `b`. */
|
|
2160
|
+
declare function semverGt(a: unknown, b: unknown): boolean;
|
|
2161
|
+
//#endregion
|
|
2162
|
+
//#region src/services/database/errors.d.ts
|
|
257
2163
|
/**
|
|
258
|
-
*
|
|
2164
|
+
* A local-store operation that did not answer within its deadline.
|
|
2165
|
+
*
|
|
2166
|
+
* The local write path (`db.create` / `db.update` / `db.delete`, every local
|
|
2167
|
+
* query behind them) used to have no deadline anywhere: the SQLite worker
|
|
2168
|
+
* transport parks a call until the worker replies, the surrealdb engine's
|
|
2169
|
+
* query chain waits on the previous link, and `withRetry` retries without a
|
|
2170
|
+
* clock. One op that never settled (a worker starved behind a long select, a
|
|
2171
|
+
* lock verification awaiting `navigator.locks.query()` forever) left the
|
|
2172
|
+
* caller's promise pending for the tab's lifetime - a chat composer that never
|
|
2173
|
+
* re-enabled, a call that never got past "Connecting".
|
|
2174
|
+
*
|
|
2175
|
+
* The message says "timed out" on purpose: `classifySyncError` keys off it and
|
|
2176
|
+
* treats the failure as transient (re-queue), never as an application error
|
|
2177
|
+
* that rolls the mutation back. `retryable: false` keeps `withRetry` from
|
|
2178
|
+
* spinning on it: the op is still running in the engine, retrying queues a
|
|
2179
|
+
* second copy behind it.
|
|
259
2180
|
*/
|
|
260
|
-
|
|
2181
|
+
declare class LocalOpTimeoutError extends Error {
|
|
2182
|
+
readonly name = "LocalOpTimeoutError";
|
|
2183
|
+
readonly retryable = false;
|
|
2184
|
+
readonly op: string;
|
|
2185
|
+
readonly timeoutMs: number;
|
|
2186
|
+
constructor(op: string, timeoutMs: number);
|
|
2187
|
+
}
|
|
2188
|
+
/** Default deadline for one local-store operation. Generous: a cold 4k-row
|
|
2189
|
+
* select on a throttled tab is seconds, not tens of seconds. */
|
|
2190
|
+
declare const DEFAULT_LOCAL_OP_TIMEOUT_MS = 30000;
|
|
261
2191
|
//#endregion
|
|
262
|
-
export { AuthEventSystem, AuthEventTypeMap, AuthEventTypes, AuthService, BucketHandle, DebounceOptions, EventSubscriptionOptions, Level, MutationCallback, MutationEvent, MutationEventType, PersistenceClient, PinoTransmit, QueryConfig, QueryConfigRecord, QueryHash, QueryState, QueryTimeToLive, QueryUpdateCallback, RecordVersionArray, RecordVersionDiff, RunOptions, Sp00kyClient, Sp00kyConfig, Sp00kyQueryResult, Sp00kyQueryResultPromise, StoreType, UpdateOptions, createAuthEventSystem, fileToUint8Array };
|
|
2192
|
+
export { ActiveImpersonation, 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, type BridgeSignOutMessage, type BridgeTokenMessage, BucketHandle, BucketPutOptions, BucketPutResult, CURSOR_COLORS, ConnectionState, CrdtField, CrdtManager, DEFAULT_LOCAL_OP_TIMEOUT_MS, DebounceOptions, EventSubscriptionOptions, type FailedMutationRow as FailedMutation, FeatureFlagHandle, FeatureFlagModule, type FeatureFlagOptions, type FeatureFlagOverride, type FeatureFlagSnapshot, IMPERSONATION_ACCESS, ImpersonationCandidate, ImpersonationInfo, ImpersonationTimerHost, ImpersonationTimers, InlineRow, Level, LocalOpTimeoutError, MATERIALIZATION_SAMPLE_WINDOW, MembershipOutcome, MutationCallback, MutationEvent, MutationEventType, ORIGIN_TOKEN_KEY, PUSH_ENDPOINT_KEY, PUSH_PAYLOAD_VERSION, PersistenceClient, PhaseStat, PinoTransmit, PreloadOptions, PreloadRefresh, type PushDeviceKind, type PushNotification, type PushNotificationAction, type PushOp, type PushPayload, type PushPayloadKind, type PushUrgency, QueryAuthorityCallback, QueryConfig, QueryConfigRecord, QueryHash, QueryState, QueryStatus, QueryStatusCallback, QueryTimeToLive, QueryTimings, QueryUpdateCallback, ReconnectConfig, RecordVersionArray, RecordVersionDiff, RegistrationTimings, RunOptions, SESSION_POLL_MS, SIGN_OUT_HOOK_TIMEOUT_MS, ServerViewMeta, SignOutContext, SignOutHook, Sp00kyClient, Sp00kyClientDeps, Sp00kyConfig, Sp00kyQueryResult, Sp00kyQueryResultPromise, StorageHealth, StorageHealthStatus, StoreType, SyncHealth, SyncHealthConfig, SyncHealthStatus, TimingPhase, TokenClaims, UpdateOptions, type WebPushClientMessage, type WebPushConfig, type WebPushDevice, WebPushError, type WebPushErrorCode, type WebPushInfo, type WebPushMessage, type WebPushMessageInput, WebPushModule, type WebPushSubscribeOptions, type WebPushSupport, type WebPushSyncOptions, type WebPushSyncStatus, type WebPushUnsubscribeOptions, type WebPushUnsupportedReason, type WebPushUpdateOptions, blurhashSidecarPath, bucketContentToBlob, createAuthEventSystem, cursorColorFromName, decode as decodeBlurhash, decodeTokenClaims, detectWebPushSupport, encode as encodeBlurhash, encodeImageToBlurhash, fileToUint8Array, impersonationFromToken, isBlurhashValid, isImagePath, isPushPayload, renewDelayMs, sameImpersonation, semverGt, textToHtml };
|