@spooky-sync/core 0.0.1-canary.32 → 0.0.1-canary.321
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 +81 -0
- package/README.md +56 -16
- package/dist/index.d.ts +2034 -137
- package/dist/index.js +15236 -2467
- package/dist/live.d.ts +141 -0
- package/dist/live.js +733 -0
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.js +3 -3
- package/dist/pure.d.ts +365 -0
- package/dist/pure.js +24 -0
- package/dist/sqlite-open.js +379 -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 +472 -0
- package/dist/types.d.ts +1821 -38
- package/dist/worker-edges.js +470 -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 +27 -7
- package/scripts/check-broker-bundle.mjs +33 -0
- package/scripts/check-worker-entries.mjs +172 -0
- package/skills/sp00ky-core/SKILL.md +31 -6
- package/skills/sp00ky-core/references/config.md +6 -0
- 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 +180 -0
- package/src/boot/boot.saga.ts +142 -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 +51 -0
- package/src/boot/preload.saga.ts +31 -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 +60 -0
- package/src/client/services.ts +418 -0
- package/src/client/sp00ky-client.test.ts +303 -0
- package/src/client/sp00ky-client.ts +586 -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 +163 -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 +501 -0
- package/src/live/index.ts +1039 -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 +169 -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 +466 -19
- package/src/modules/crdt/crdt-field.ts +294 -0
- package/src/modules/crdt/crdt-hydration.test.ts +210 -0
- package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
- package/src/modules/crdt/index.ts +463 -0
- package/src/modules/crdt/loro-loader.ts +25 -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 +285 -0
- package/src/mutation/push.saga.ts +217 -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 +337 -0
- package/src/query/env.test.ts +23 -0
- package/src/query/env.ts +56 -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 +159 -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 +350 -0
- package/src/query/membership.saga.ts +278 -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 +229 -0
- package/src/query/render-set.test.ts +47 -0
- package/src/query/render-set.ts +71 -0
- package/src/query/sql.test.ts +112 -0
- package/src/query/sql.ts +149 -0
- package/src/query/window-query.test.ts +70 -0
- package/src/query/window-query.ts +195 -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 +199 -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 +146 -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 +59 -16
- package/src/services/database/local.test.ts +64 -0
- package/src/services/database/local.ts +487 -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 +273 -6
- package/src/services/database/sqlite-cache-engine.test.ts +617 -0
- package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
- package/src/services/database/sqlite-cache-engine.ts +1372 -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 +145 -0
- package/src/services/database/sqlite-plan-sql.ts +182 -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 +416 -0
- package/src/services/database/surreal-cache-engine.ts +161 -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 +783 -68
- 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 +198 -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 +283 -0
- package/src/services/tabs/broker.test.ts +327 -0
- package/src/services/tabs/coordinator.test.ts +386 -0
- package/src/services/tabs/coordinator.ts +638 -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 +640 -0
- package/src/sp00ky.ts +5 -395
- package/src/state/client-state.ts +200 -0
- package/src/state/lifecycle.test.ts +141 -0
- package/src/state/lifecycle.ts +99 -0
- package/src/state/reducers.test.ts +304 -0
- package/src/state/reducers.ts +526 -0
- package/src/state/selectors.test.ts +175 -0
- package/src/state/selectors.ts +222 -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 +133 -0
- package/src/testing/build.ts +63 -0
- package/src/testing/fake-services.ts +48 -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 +624 -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 +78 -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 +30 -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
|
@@ -1,18 +1,97 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Diagnostic,
|
|
3
|
+
SurrealEvents} from 'surrealdb';
|
|
1
4
|
import {
|
|
2
5
|
applyDiagnostics,
|
|
3
6
|
createRemoteEngines,
|
|
4
|
-
Diagnostic,
|
|
5
7
|
Surreal,
|
|
6
|
-
SurrealTransaction,
|
|
7
8
|
} from 'surrealdb';
|
|
8
|
-
import { Sp00kyConfig } from '../../types';
|
|
9
|
-
import { Logger } from '../logger/index';
|
|
9
|
+
import type { ConnectionState, ReconnectConfig, Sp00kyConfig } from '../../types';
|
|
10
|
+
import type { Logger } from '../logger/index';
|
|
10
11
|
import { AbstractDatabaseService } from './database';
|
|
11
12
|
import { createDatabaseEventSystem, DatabaseEventTypes } from './events/index';
|
|
12
13
|
|
|
14
|
+
/** Defaults for {@link ReconnectConfig}. See that type for the rationale. */
|
|
15
|
+
export const RECONNECT_DEFAULTS = {
|
|
16
|
+
attempts: -1,
|
|
17
|
+
retryDelayMax: 15_000,
|
|
18
|
+
heartbeatIntervalMs: 20_000,
|
|
19
|
+
heartbeatTimeoutMs: 10_000,
|
|
20
|
+
superviseRetryDelayMaxMs: 15_000,
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
/** Fill in {@link RECONNECT_DEFAULTS} for every field the caller omitted. */
|
|
24
|
+
export function resolveReconnectConfig(
|
|
25
|
+
input?: ReconnectConfig
|
|
26
|
+
): Required<ReconnectConfig> {
|
|
27
|
+
return { ...RECONNECT_DEFAULTS, ...input };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Transport events the SDK publishes, mapped 1:1 to {@link ConnectionState}. */
|
|
31
|
+
export type RemoteConnectionEvent = ConnectionState | 'error';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Statements the remote service keeps in flight at once. Enough for the sync
|
|
35
|
+
* scheduler's `MAX_CONCURRENT_DOWN` registrations plus the list_ref poll and
|
|
36
|
+
* an app's own one-shot reads, small enough to stay polite to the server.
|
|
37
|
+
*/
|
|
38
|
+
const REMOTE_MAX_CONCURRENT_QUERIES = 6;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Session ids a page remembers for its pagehide release and its anonymous LIVE
|
|
42
|
+
* filter. The release only needs the sessions of the last ttl; this many
|
|
43
|
+
* reconnects inside one ttl is a broken network. The LIVE filter would like
|
|
44
|
+
* every session that stamped an edge still held, but the SSP restamps an edge
|
|
45
|
+
* whenever its row changes, so an id that falls off costs at most the LIVE
|
|
46
|
+
* removal of a row untouched since, which the membership poll still catches.
|
|
47
|
+
*/
|
|
48
|
+
const MAX_SESSION_IDS = 16;
|
|
49
|
+
|
|
13
50
|
export class RemoteDatabaseService extends AbstractDatabaseService {
|
|
14
51
|
private config: Sp00kyConfig<any>['database'];
|
|
15
52
|
protected eventType = DatabaseEventTypes.RemoteQuery;
|
|
53
|
+
private readonly reconnectConfig: Required<ReconnectConfig>;
|
|
54
|
+
/**
|
|
55
|
+
* In-flight `connect()`, so concurrent callers (boot + supervisor revive +
|
|
56
|
+
* an `online` event landing at the same moment) share one attempt instead of
|
|
57
|
+
* racing two sockets. Cleared on settle, so a later call always reconnects.
|
|
58
|
+
*/
|
|
59
|
+
private connecting: Promise<void> | null = null;
|
|
60
|
+
/**
|
|
61
|
+
* The token to re-authenticate a freshly opened socket with, kept current by
|
|
62
|
+
* {@link setAuthToken}.
|
|
63
|
+
*
|
|
64
|
+
* `config.token` is fixed at construction and most apps never set it: they
|
|
65
|
+
* sign in later, which authenticates the socket that happens to be open at
|
|
66
|
+
* the time. That is enough for the SDK's OWN reconnects (it replays
|
|
67
|
+
* `version`/`use`/`authenticate` itself), but not for the supervisor's revive
|
|
68
|
+
* loop, which builds a socket from scratch. Without this the revived socket
|
|
69
|
+
* came back UNAUTHENTICATED and stayed that way for the life of the page,
|
|
70
|
+
* while the client's `currentUserId` — restored from local storage — kept
|
|
71
|
+
* reporting the user as signed in.
|
|
72
|
+
*
|
|
73
|
+
* The visible damage is silent and total: `fn::query::register` sends
|
|
74
|
+
* `<string>($auth.id OR '')`, so every view registered afterwards is stamped
|
|
75
|
+
* with an empty identity, and every `$auth.id` predicate in it resolves
|
|
76
|
+
* false. Public tables keep returning rows while everything owned by the user
|
|
77
|
+
* returns nothing, on a page that still looks signed in.
|
|
78
|
+
*/
|
|
79
|
+
private authToken: string | null = null;
|
|
80
|
+
/**
|
|
81
|
+
* Every `session::id()` this page's sockets have had, oldest first.
|
|
82
|
+
*
|
|
83
|
+
* The server stamps a view's `subscribers` entry with the session that
|
|
84
|
+
* registered or heartbeated it, and every socket (an SDK reconnect, a
|
|
85
|
+
* supervisor revive) is a new session. The pagehide release goes over HTTP,
|
|
86
|
+
* whose own session id matches none of them, so it names these instead. Kept
|
|
87
|
+
* for the page, not the socket: a view registered before a reconnect still
|
|
88
|
+
* carries the old id until its next heartbeat.
|
|
89
|
+
*
|
|
90
|
+
* The anonymous LIVE filters on the same ids (see `listRefLive`): its edges
|
|
91
|
+
* name the socket that registered their view, which may be an earlier one.
|
|
92
|
+
* A reload hands its ids to the next page with the salt.
|
|
93
|
+
*/
|
|
94
|
+
private sessionIdsSeen: string[] = [];
|
|
16
95
|
|
|
17
96
|
constructor(config: Sp00kyConfig<any>['database'], logger: Logger) {
|
|
18
97
|
const events = createDatabaseEventSystem();
|
|
@@ -41,14 +120,188 @@ export class RemoteDatabaseService extends AbstractDatabaseService {
|
|
|
41
120
|
events
|
|
42
121
|
);
|
|
43
122
|
this.config = config;
|
|
123
|
+
this.reconnectConfig = resolveReconnectConfig(config.reconnect);
|
|
124
|
+
this.queryTimeoutMs = Math.max(0, config.queryTimeoutMs ?? 60_000);
|
|
125
|
+
this.maxConcurrentQueries = REMOTE_MAX_CONCURRENT_QUERIES;
|
|
126
|
+
this.client.subscribe('connected', () => void this.recordSessionId());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** See {@link sessionIdsSeen}. */
|
|
130
|
+
sessionIds(): readonly string[] {
|
|
131
|
+
return this.sessionIdsSeen;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Take on the session ids of the page this one reloaded from. It kept that
|
|
136
|
+
* page's salt, so the views it re-registers still carry edges stamped by
|
|
137
|
+
* those sockets. Older than anything this page has seen, so they go first.
|
|
138
|
+
*/
|
|
139
|
+
adoptSessionIds(ids: ReadonlyArray<string>): void {
|
|
140
|
+
const earlier = ids.filter((sid) => typeof sid === 'string' && sid !== '' && !this.sessionIdsSeen.includes(sid));
|
|
141
|
+
this.sessionIdsSeen = [...earlier, ...this.sessionIdsSeen].slice(-MAX_SESSION_IDS);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Ask the socket that just connected for its session id. Straight to the
|
|
146
|
+
* SDK: it needs no namespace or identity, so it need not wait behind an
|
|
147
|
+
* in-flight connect or take a slot from the query queue.
|
|
148
|
+
*/
|
|
149
|
+
private async recordSessionId(): Promise<void> {
|
|
150
|
+
try {
|
|
151
|
+
const [sid] = (await this.client.query('RETURN <string>session::id()')) as unknown[];
|
|
152
|
+
if (typeof sid !== 'string' || this.sessionIdsSeen.includes(sid)) return;
|
|
153
|
+
this.sessionIdsSeen = [...this.sessionIdsSeen, sid].slice(-MAX_SESSION_IDS);
|
|
154
|
+
} catch {
|
|
155
|
+
// Without it this socket's views fall back to the TTL sweep.
|
|
156
|
+
}
|
|
44
157
|
}
|
|
45
158
|
|
|
46
159
|
getConfig(): Sp00kyConfig<any>['database'] {
|
|
47
160
|
return this.config;
|
|
48
161
|
}
|
|
49
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Send one SurrealQL statement so that it survives the page going away.
|
|
165
|
+
*
|
|
166
|
+
* A WebSocket `send()` during `pagehide` is not guaranteed to flush — the
|
|
167
|
+
* browser may tear the socket down first, and the frame is simply lost.
|
|
168
|
+
* Measured: an unload-time release over the live socket reached the server
|
|
169
|
+
* zero times out of one. `fetch` with `keepalive` is the primitive the
|
|
170
|
+
* platform actually guarantees here, so this goes over SurrealDB's HTTP
|
|
171
|
+
* `/sql` endpoint instead of the RPC socket.
|
|
172
|
+
*
|
|
173
|
+
* Best-effort by design: no await, no retry, errors swallowed. Every caller
|
|
174
|
+
* must have a server-side fallback that makes a lost beacon a non-event.
|
|
175
|
+
*
|
|
176
|
+
* The statement runs in a fresh HTTP session: `session::id()` there is a new
|
|
177
|
+
* id, not this page's socket's, so anything keyed on it must be passed the
|
|
178
|
+
* ids from {@link sessionIds} instead.
|
|
179
|
+
*/
|
|
180
|
+
beaconSql(sql: string): void {
|
|
181
|
+
try {
|
|
182
|
+
const { endpoint, namespace, database } = this.getConfig();
|
|
183
|
+
if (!endpoint || typeof fetch !== 'function') return;
|
|
184
|
+
|
|
185
|
+
// `ws(s)://host/rpc` is the socket; `http(s)://host/sql` is the same
|
|
186
|
+
// server's statement endpoint.
|
|
187
|
+
const url = new URL(endpoint);
|
|
188
|
+
url.protocol = url.protocol === 'wss:' ? 'https:' : url.protocol === 'ws:' ? 'http:' : url.protocol;
|
|
189
|
+
url.pathname = url.pathname.replace(/\/rpc\/?$/, '') + '/sql';
|
|
190
|
+
|
|
191
|
+
const headers: Record<string, string> = {
|
|
192
|
+
Accept: 'application/json',
|
|
193
|
+
'Content-Type': 'text/plain',
|
|
194
|
+
};
|
|
195
|
+
if (namespace) headers['surreal-ns'] = namespace;
|
|
196
|
+
if (database) headers['surreal-db'] = database;
|
|
197
|
+
// Without this the statement runs unauthenticated and `$auth.id` is NONE,
|
|
198
|
+
// which for a per-user release means it matches nothing.
|
|
199
|
+
if (this.authToken) headers.Authorization = `Bearer ${this.authToken}`;
|
|
200
|
+
|
|
201
|
+
void fetch(url.toString(), {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers,
|
|
204
|
+
body: sql,
|
|
205
|
+
keepalive: true,
|
|
206
|
+
credentials: 'omit',
|
|
207
|
+
}).catch(() => {});
|
|
208
|
+
} catch {
|
|
209
|
+
// Malformed endpoint, no fetch, blocked by CSP: the caller's fallback owns it.
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Record the token every future connect should authenticate with, or `null`
|
|
215
|
+
* on sign-out. See {@link authToken}.
|
|
216
|
+
*
|
|
217
|
+
* Does not touch the CURRENT socket: callers authenticate that themselves
|
|
218
|
+
* (sign-in and session restore both already do). This only makes the next
|
|
219
|
+
* from-scratch connect reproduce that state.
|
|
220
|
+
*/
|
|
221
|
+
setAuthToken(token: string | null): void {
|
|
222
|
+
this.authToken = token;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Resolved reconnect tunables; the supervisor reads its own knobs here. */
|
|
226
|
+
getReconnectConfig(): Required<ReconnectConfig> {
|
|
227
|
+
return this.reconnectConfig;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Current transport state as reported by the SDK. */
|
|
231
|
+
getStatus(): ConnectionState {
|
|
232
|
+
return this.client.status as ConnectionState;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Observe transport events. Thin passthrough so callers (the supervisor,
|
|
237
|
+
* sync, CRDT) don't have to reach through `getClient()`.
|
|
238
|
+
*/
|
|
239
|
+
subscribeConnection<K extends RemoteConnectionEvent>(
|
|
240
|
+
event: K,
|
|
241
|
+
cb: (...payload: SurrealEvents[K]) => void
|
|
242
|
+
): () => void {
|
|
243
|
+
return this.client.subscribe(event, cb);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Tear the socket down on purpose. Used by the heartbeat watchdog when a
|
|
248
|
+
* socket stops answering but never closes: `close()` makes the SDK publish
|
|
249
|
+
* `disconnected`, which is what drives the supervisor's revive loop.
|
|
250
|
+
*/
|
|
251
|
+
async forceClose(): Promise<void> {
|
|
252
|
+
try {
|
|
253
|
+
await this.client.close();
|
|
254
|
+
} catch (err) {
|
|
255
|
+
this.logger.debug(
|
|
256
|
+
{ err, Category: 'sp00ky-client::RemoteDatabaseService::forceClose' },
|
|
257
|
+
'forceClose failed; treating the socket as gone anyway'
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Hold statements while a connect is in flight.
|
|
264
|
+
*
|
|
265
|
+
* `connect()` opens the socket, then selects the namespace/database, then
|
|
266
|
+
* authenticates - three round trips. The SDK considers the connection ready
|
|
267
|
+
* after the first, so a query queued during the other two ran on a socket
|
|
268
|
+
* with no namespace ("Specify a namespace to use") or, worse, no identity.
|
|
269
|
+
* Seen 2026-09-06 after the supervisor rebuilt a socket: a collection
|
|
270
|
+
* recount failed with exactly that error. Waiting on the in-flight connect
|
|
271
|
+
* costs nothing when no connect is running; a failed connect is not
|
|
272
|
+
* swallowed here - the query then fails on its own, as before.
|
|
273
|
+
*/
|
|
274
|
+
protected override async beforeQuery(): Promise<void> {
|
|
275
|
+
const inflight = this.connecting;
|
|
276
|
+
if (!inflight) return;
|
|
277
|
+
try {
|
|
278
|
+
await inflight;
|
|
279
|
+
} catch {
|
|
280
|
+
// The query that follows reports the failure itself.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Open (or re-open) the remote connection.
|
|
286
|
+
*
|
|
287
|
+
* Safe to call repeatedly: concurrent calls share the in-flight attempt, and
|
|
288
|
+
* a call after a `disconnected` builds a fresh socket. `use()` and
|
|
289
|
+
* `authenticate()` are re-applied here for the cold path; the SDK also
|
|
290
|
+
* replays them itself on its own internal reconnects.
|
|
291
|
+
*/
|
|
50
292
|
async connect(): Promise<void> {
|
|
51
|
-
|
|
293
|
+
if (this.connecting) return this.connecting;
|
|
294
|
+
this.connecting = this.doConnect().finally(() => {
|
|
295
|
+
this.connecting = null;
|
|
296
|
+
});
|
|
297
|
+
return this.connecting;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private async doConnect(): Promise<void> {
|
|
301
|
+
const { endpoint, namespace, database } = this.getConfig();
|
|
302
|
+
// The live token wins over the constructor-time one: this connect may be
|
|
303
|
+
// the supervisor rebuilding a socket long after sign-in. See `authToken`.
|
|
304
|
+
const token = this.authToken ?? this.getConfig().token;
|
|
52
305
|
if (endpoint) {
|
|
53
306
|
this.logger.info(
|
|
54
307
|
{
|
|
@@ -60,7 +313,21 @@ export class RemoteDatabaseService extends AbstractDatabaseService {
|
|
|
60
313
|
'Connecting to remote database'
|
|
61
314
|
);
|
|
62
315
|
try {
|
|
63
|
-
|
|
316
|
+
// Without explicit options the SDK caps itself at 5 attempts (~62s of
|
|
317
|
+
// outage) and then gives up permanently — nothing would re-open the
|
|
318
|
+
// socket after that. `attempts: -1` keeps it trying; the supervisor
|
|
319
|
+
// still covers the case where the SDK terminates the engine because its
|
|
320
|
+
// post-reconnect handshake threw.
|
|
321
|
+
await this.client.connect(endpoint, {
|
|
322
|
+
reconnect: {
|
|
323
|
+
enabled: true,
|
|
324
|
+
attempts: this.reconnectConfig.attempts,
|
|
325
|
+
retryDelay: 1_000,
|
|
326
|
+
retryDelayMax: this.reconnectConfig.retryDelayMax,
|
|
327
|
+
retryDelayMultiplier: 2,
|
|
328
|
+
retryDelayJitter: 0.1,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
64
331
|
await this.client.use({
|
|
65
332
|
namespace,
|
|
66
333
|
database,
|