@spooky-sync/core 0.0.1-canary.31 → 0.0.1-canary.310

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.
Files changed (257) hide show
  1. package/AGENTS.md +81 -0
  2. package/README.md +56 -16
  3. package/dist/index.d.ts +2026 -137
  4. package/dist/index.js +14956 -2434
  5. package/dist/live.d.ts +141 -0
  6. package/dist/live.js +709 -0
  7. package/dist/otel/index.d.ts +1 -1
  8. package/dist/otel/index.js +3 -3
  9. package/dist/pure.d.ts +329 -0
  10. package/dist/pure.js +24 -0
  11. package/dist/sqlite-open.js +358 -0
  12. package/dist/sqlite-worker.d.ts +1 -0
  13. package/dist/sqlite-worker.js +439 -0
  14. package/dist/sw.d.ts +207 -0
  15. package/dist/sw.js +393 -0
  16. package/dist/tabs-broker-worker.d.ts +8 -0
  17. package/dist/tabs-broker-worker.js +472 -0
  18. package/dist/types.d.ts +1765 -38
  19. package/dist/worker-edges.js +423 -0
  20. package/dist/worker-impersonation.js +60 -0
  21. package/dist/worker-ref-tables.d.ts +46 -0
  22. package/dist/worker-types.d.ts +112 -0
  23. package/dist/worker-types.js +73 -0
  24. package/package.json +27 -7
  25. package/scripts/check-broker-bundle.mjs +33 -0
  26. package/scripts/check-worker-entries.mjs +172 -0
  27. package/skills/sp00ky-core/SKILL.md +31 -6
  28. package/skills/sp00ky-core/references/config.md +6 -0
  29. package/src/boot/auth-flip.saga.test.ts +135 -0
  30. package/src/boot/auth-flip.saga.ts +77 -0
  31. package/src/boot/boot.saga.test.ts +158 -0
  32. package/src/boot/boot.saga.ts +131 -0
  33. package/src/boot/bucket-switch.saga.test.ts +187 -0
  34. package/src/boot/bucket-switch.saga.ts +133 -0
  35. package/src/boot/preload.saga.test.ts +51 -0
  36. package/src/boot/preload.saga.ts +31 -0
  37. package/src/bucket-blurhash.test.ts +148 -0
  38. package/src/build-globals.d.ts +12 -0
  39. package/src/client/bucket-handle.test.ts +117 -0
  40. package/src/client/bucket-handle.ts +227 -0
  41. package/src/client/router.test.ts +58 -0
  42. package/src/client/router.ts +100 -0
  43. package/src/client/runtime.test.ts +195 -0
  44. package/src/client/runtime.ts +296 -0
  45. package/src/client/services.ts +370 -0
  46. package/src/client/sp00ky-client.test.ts +303 -0
  47. package/src/client/sp00ky-client.ts +586 -0
  48. package/src/events/events.test.ts +2 -1
  49. package/src/events/index.ts +3 -0
  50. package/src/index.ts +69 -1
  51. package/src/kernel/constants.test.ts +19 -0
  52. package/src/kernel/constants.ts +80 -0
  53. package/src/kernel/effects.test.ts +40 -0
  54. package/src/kernel/effects.ts +161 -0
  55. package/src/kernel/events.ts +57 -0
  56. package/src/kernel/interpreter.test.ts +116 -0
  57. package/src/kernel/interpreter.ts +148 -0
  58. package/src/kernel/saga.test.ts +89 -0
  59. package/src/kernel/saga.ts +73 -0
  60. package/src/live/index.test.ts +478 -0
  61. package/src/live/index.ts +1013 -0
  62. package/src/modules/app-release/index.test.ts +125 -0
  63. package/src/modules/app-release/index.ts +209 -0
  64. package/src/modules/auth/auth.local-first.test.ts +141 -0
  65. package/src/modules/auth/events/index.ts +9 -2
  66. package/src/modules/auth/impersonation-banner.ts +443 -0
  67. package/src/modules/auth/impersonation.test.ts +355 -0
  68. package/src/modules/auth/impersonation.ts +179 -0
  69. package/src/modules/auth/index.ts +386 -17
  70. package/src/modules/crdt/crdt-field.ts +294 -0
  71. package/src/modules/crdt/crdt-hydration.test.ts +210 -0
  72. package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
  73. package/src/modules/crdt/index.ts +463 -0
  74. package/src/modules/crdt/loro-loader.ts +25 -0
  75. package/src/modules/devtools/flags.ts +349 -0
  76. package/src/modules/devtools/index.ts +746 -95
  77. package/src/modules/devtools/mutations.test.ts +92 -0
  78. package/src/modules/devtools/mutations.ts +160 -0
  79. package/src/modules/devtools/notify-throttle.test.ts +166 -0
  80. package/src/modules/devtools/state-shape.test.ts +263 -0
  81. package/src/modules/devtools/storage-info.test.ts +79 -0
  82. package/src/modules/devtools/storage-info.ts +168 -0
  83. package/src/modules/devtools/versions.test.ts +74 -0
  84. package/src/modules/devtools/versions.ts +110 -0
  85. package/src/modules/feature-flag/index.test.ts +251 -0
  86. package/src/modules/feature-flag/index.ts +316 -0
  87. package/src/modules/ref-tables.test.ts +91 -0
  88. package/src/modules/ref-tables.ts +88 -0
  89. package/src/modules/web-push/index.test.ts +824 -0
  90. package/src/modules/web-push/index.ts +1027 -0
  91. package/src/mutation/jobs.test.ts +25 -0
  92. package/src/mutation/jobs.ts +41 -0
  93. package/src/mutation/mutation-id.test.ts +37 -0
  94. package/src/mutation/mutation-id.ts +35 -0
  95. package/src/mutation/pending-writes.integration.test.ts +175 -0
  96. package/src/mutation/push.saga.test.ts +285 -0
  97. package/src/mutation/push.saga.ts +217 -0
  98. package/src/mutation/rows.test.ts +231 -0
  99. package/src/mutation/rows.ts +427 -0
  100. package/src/mutation/tray.saga.test.ts +63 -0
  101. package/src/mutation/tray.saga.ts +48 -0
  102. package/src/mutation/write.saga.test.ts +316 -0
  103. package/src/mutation/write.saga.ts +268 -0
  104. package/src/otel/index.ts +10 -7
  105. package/src/pure.ts +65 -0
  106. package/src/push/types.ts +174 -0
  107. package/src/query/env.test.ts +23 -0
  108. package/src/query/env.ts +56 -0
  109. package/src/query/fetch.saga.test.ts +199 -0
  110. package/src/query/fetch.saga.ts +120 -0
  111. package/src/query/hash.test.ts +14 -0
  112. package/src/query/hash.ts +18 -0
  113. package/src/query/id-set-plan.test.ts +132 -0
  114. package/src/query/lifecycle.saga.test.ts +274 -0
  115. package/src/query/lifecycle.saga.ts +159 -0
  116. package/src/query/materialize.saga.test.ts +115 -0
  117. package/src/query/materialize.saga.ts +84 -0
  118. package/src/query/materialize.test.ts +58 -0
  119. package/src/query/materialize.ts +57 -0
  120. package/src/query/membership.saga.test.ts +350 -0
  121. package/src/query/membership.saga.ts +278 -0
  122. package/src/query/membership.test.ts +118 -0
  123. package/src/query/membership.ts +201 -0
  124. package/src/query/register.saga.test.ts +293 -0
  125. package/src/query/register.saga.ts +226 -0
  126. package/src/query/render-set.test.ts +47 -0
  127. package/src/query/render-set.ts +71 -0
  128. package/src/query/sql.test.ts +63 -0
  129. package/src/query/sql.ts +93 -0
  130. package/src/query/window-query.test.ts +70 -0
  131. package/src/query/window-query.ts +195 -0
  132. package/src/services/blobs/blob-cache.test.ts +359 -0
  133. package/src/services/blobs/blob-cache.ts +603 -0
  134. package/src/services/blobs/blob-manifest.ts +227 -0
  135. package/src/services/blobs/blob-store.test.ts +77 -0
  136. package/src/services/blobs/blob-store.ts +359 -0
  137. package/src/services/blobs/blob.fixture.ts +90 -0
  138. package/src/services/blobs/index.ts +70 -0
  139. package/src/services/database/cache-engine.ts +199 -0
  140. package/src/services/database/connection-supervisor.test.ts +318 -0
  141. package/src/services/database/connection-supervisor.ts +422 -0
  142. package/src/services/database/database.query-timeout.test.ts +140 -0
  143. package/src/services/database/database.responses.test.ts +43 -0
  144. package/src/services/database/database.ts +146 -61
  145. package/src/services/database/engine-factory.ts +33 -0
  146. package/src/services/database/errors.ts +34 -0
  147. package/src/services/database/events/index.ts +2 -1
  148. package/src/services/database/index.ts +7 -0
  149. package/src/services/database/landed-body-clear.integration.test.ts +236 -0
  150. package/src/services/database/local-migrator.integration.test.ts +110 -0
  151. package/src/services/database/local-migrator.ts +59 -16
  152. package/src/services/database/local.test.ts +64 -0
  153. package/src/services/database/local.ts +487 -63
  154. package/src/services/database/plan-render.test.ts +159 -0
  155. package/src/services/database/plan-render.ts +108 -0
  156. package/src/services/database/relation-resolver.test.ts +467 -0
  157. package/src/services/database/relation-resolver.ts +0 -0
  158. package/src/services/database/remote.ts +213 -6
  159. package/src/services/database/sqlite-cache-engine.test.ts +617 -0
  160. package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
  161. package/src/services/database/sqlite-cache-engine.ts +1372 -0
  162. package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
  163. package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
  164. package/src/services/database/sqlite-lock-verify.test.ts +33 -0
  165. package/src/services/database/sqlite-lock-verify.ts +45 -0
  166. package/src/services/database/sqlite-open.test.ts +150 -0
  167. package/src/services/database/sqlite-open.ts +164 -0
  168. package/src/services/database/sqlite-plan-sql.test.ts +139 -0
  169. package/src/services/database/sqlite-plan-sql.ts +179 -0
  170. package/src/services/database/sqlite-projection.test.ts +121 -0
  171. package/src/services/database/sqlite-select.integration.test.ts +185 -0
  172. package/src/services/database/sqlite-select.test.ts +246 -0
  173. package/src/services/database/sqlite-select.ts +131 -0
  174. package/src/services/database/sqlite-transport.fixture.ts +30 -0
  175. package/src/services/database/sqlite-transport.ts +224 -0
  176. package/src/services/database/sqlite-worker.ts +437 -0
  177. package/src/services/database/surql-translate.ts +416 -0
  178. package/src/services/database/surreal-cache-engine.ts +161 -0
  179. package/src/services/logger/index.test.ts +132 -0
  180. package/src/services/logger/index.ts +189 -7
  181. package/src/services/persistence/localstorage.ts +2 -2
  182. package/src/services/persistence/resilient.ts +10 -3
  183. package/src/services/persistence/surrealdb.ts +4 -4
  184. package/src/services/stream-processor/index.ts +779 -67
  185. package/src/services/stream-processor/permissions.test.ts +47 -0
  186. package/src/services/stream-processor/permissions.ts +53 -0
  187. package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
  188. package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
  189. package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
  190. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  191. package/src/services/stream-processor/wasm-types.ts +58 -2
  192. package/src/services/tabs/broker-client.ts +283 -0
  193. package/src/services/tabs/broker.test.ts +327 -0
  194. package/src/services/tabs/coordinator.test.ts +386 -0
  195. package/src/services/tabs/coordinator.ts +638 -0
  196. package/src/services/tabs/fake-ports.fixture.ts +112 -0
  197. package/src/services/tabs/leader-locks.ts +75 -0
  198. package/src/services/tabs/protocol.ts +262 -0
  199. package/src/services/tabs/support.ts +36 -0
  200. package/src/services/tabs/tabs-broker-worker.ts +640 -0
  201. package/src/sp00ky.ts +5 -395
  202. package/src/state/client-state.ts +200 -0
  203. package/src/state/lifecycle.test.ts +118 -0
  204. package/src/state/lifecycle.ts +86 -0
  205. package/src/state/reducers.test.ts +304 -0
  206. package/src/state/reducers.ts +526 -0
  207. package/src/state/selectors.test.ts +167 -0
  208. package/src/state/selectors.ts +216 -0
  209. package/src/sw/index.test.ts +582 -0
  210. package/src/sw/index.ts +607 -0
  211. package/src/sw/store.ts +96 -0
  212. package/src/sync/connection.saga.test.ts +86 -0
  213. package/src/sync/connection.saga.ts +85 -0
  214. package/src/sync/live.saga.test.ts +178 -0
  215. package/src/sync/live.saga.ts +107 -0
  216. package/src/sync/policy.health.test.ts +56 -0
  217. package/src/sync/policy.test.ts +659 -0
  218. package/src/sync/policy.ts +502 -0
  219. package/src/sync/poll.saga.test.ts +51 -0
  220. package/src/sync/poll.saga.ts +36 -0
  221. package/src/sync/tabs.saga.test.ts +78 -0
  222. package/src/sync/tabs.saga.ts +96 -0
  223. package/src/testing/adapters.ts +133 -0
  224. package/src/testing/build.ts +63 -0
  225. package/src/testing/fake-services.ts +48 -0
  226. package/src/testing/run-pure.test.ts +92 -0
  227. package/src/testing/run-pure.ts +128 -0
  228. package/src/testing/services.ts +17 -0
  229. package/src/types.ts +624 -10
  230. package/src/utils/blurhash.ts +90 -0
  231. package/src/utils/edges.ts +39 -0
  232. package/src/utils/error-classification.test.ts +54 -0
  233. package/src/utils/error-classification.ts +25 -0
  234. package/src/utils/index.ts +78 -12
  235. package/src/utils/parser.test.ts +67 -102
  236. package/src/utils/parser.ts +64 -2
  237. package/src/utils/semver.test.ts +32 -0
  238. package/src/utils/semver.ts +30 -0
  239. package/src/utils/sha256.ts +15 -0
  240. package/src/utils/surql.ts +30 -18
  241. package/src/utils/utils.test.ts +3 -2
  242. package/tsdown.config.ts +117 -3
  243. package/vitest.config.ts +48 -0
  244. package/src/modules/cache/index.ts +0 -241
  245. package/src/modules/cache/types.ts +0 -19
  246. package/src/modules/data/data.test.ts +0 -58
  247. package/src/modules/data/index.ts +0 -879
  248. package/src/modules/sync/engine.ts +0 -173
  249. package/src/modules/sync/events/index.ts +0 -77
  250. package/src/modules/sync/index.ts +0 -3
  251. package/src/modules/sync/queue/index.ts +0 -2
  252. package/src/modules/sync/queue/queue-down.ts +0 -89
  253. package/src/modules/sync/queue/queue-up.ts +0 -223
  254. package/src/modules/sync/scheduler.ts +0 -84
  255. package/src/modules/sync/sync.ts +0 -421
  256. package/src/modules/sync/utils.test.ts +0 -311
  257. package/src/modules/sync/utils.ts +0 -167
@@ -1,18 +1,72 @@
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
+
13
40
  export class RemoteDatabaseService extends AbstractDatabaseService {
14
41
  private config: Sp00kyConfig<any>['database'];
15
42
  protected eventType = DatabaseEventTypes.RemoteQuery;
43
+ private readonly reconnectConfig: Required<ReconnectConfig>;
44
+ /**
45
+ * In-flight `connect()`, so concurrent callers (boot + supervisor revive +
46
+ * an `online` event landing at the same moment) share one attempt instead of
47
+ * racing two sockets. Cleared on settle, so a later call always reconnects.
48
+ */
49
+ private connecting: Promise<void> | null = null;
50
+ /**
51
+ * The token to re-authenticate a freshly opened socket with, kept current by
52
+ * {@link setAuthToken}.
53
+ *
54
+ * `config.token` is fixed at construction and most apps never set it: they
55
+ * sign in later, which authenticates the socket that happens to be open at
56
+ * the time. That is enough for the SDK's OWN reconnects (it replays
57
+ * `version`/`use`/`authenticate` itself), but not for the supervisor's revive
58
+ * loop, which builds a socket from scratch. Without this the revived socket
59
+ * came back UNAUTHENTICATED and stayed that way for the life of the page,
60
+ * while the client's `currentUserId` — restored from local storage — kept
61
+ * reporting the user as signed in.
62
+ *
63
+ * The visible damage is silent and total: `fn::query::register` sends
64
+ * `<string>($auth.id OR '')`, so every view registered afterwards is stamped
65
+ * with an empty identity, and every `$auth.id` predicate in it resolves
66
+ * false. Public tables keep returning rows while everything owned by the user
67
+ * returns nothing, on a page that still looks signed in.
68
+ */
69
+ private authToken: string | null = null;
16
70
 
17
71
  constructor(config: Sp00kyConfig<any>['database'], logger: Logger) {
18
72
  const events = createDatabaseEventSystem();
@@ -41,14 +95,153 @@ export class RemoteDatabaseService extends AbstractDatabaseService {
41
95
  events
42
96
  );
43
97
  this.config = config;
98
+ this.reconnectConfig = resolveReconnectConfig(config.reconnect);
99
+ this.queryTimeoutMs = Math.max(0, config.queryTimeoutMs ?? 60_000);
100
+ this.maxConcurrentQueries = REMOTE_MAX_CONCURRENT_QUERIES;
44
101
  }
45
102
 
46
103
  getConfig(): Sp00kyConfig<any>['database'] {
47
104
  return this.config;
48
105
  }
49
106
 
107
+ /**
108
+ * Send one SurrealQL statement so that it survives the page going away.
109
+ *
110
+ * A WebSocket `send()` during `pagehide` is not guaranteed to flush — the
111
+ * browser may tear the socket down first, and the frame is simply lost.
112
+ * Measured: an unload-time release over the live socket reached the server
113
+ * zero times out of one. `fetch` with `keepalive` is the primitive the
114
+ * platform actually guarantees here, so this goes over SurrealDB's HTTP
115
+ * `/sql` endpoint instead of the RPC socket.
116
+ *
117
+ * Best-effort by design: no await, no retry, errors swallowed. Every caller
118
+ * must have a server-side fallback that makes a lost beacon a non-event.
119
+ */
120
+ beaconSql(sql: string): void {
121
+ try {
122
+ const { endpoint, namespace, database } = this.getConfig();
123
+ if (!endpoint || typeof fetch !== 'function') return;
124
+
125
+ // `ws(s)://host/rpc` is the socket; `http(s)://host/sql` is the same
126
+ // server's statement endpoint.
127
+ const url = new URL(endpoint);
128
+ url.protocol = url.protocol === 'wss:' ? 'https:' : url.protocol === 'ws:' ? 'http:' : url.protocol;
129
+ url.pathname = url.pathname.replace(/\/rpc\/?$/, '') + '/sql';
130
+
131
+ const headers: Record<string, string> = {
132
+ Accept: 'application/json',
133
+ 'Content-Type': 'text/plain',
134
+ };
135
+ if (namespace) headers['surreal-ns'] = namespace;
136
+ if (database) headers['surreal-db'] = database;
137
+ // Without this the statement runs unauthenticated and `$auth.id` is NONE,
138
+ // which for a per-user release means it matches nothing.
139
+ if (this.authToken) headers.Authorization = `Bearer ${this.authToken}`;
140
+
141
+ void fetch(url.toString(), {
142
+ method: 'POST',
143
+ headers,
144
+ body: sql,
145
+ keepalive: true,
146
+ credentials: 'omit',
147
+ }).catch(() => {});
148
+ } catch {
149
+ // Malformed endpoint, no fetch, blocked by CSP: the caller's fallback owns it.
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Record the token every future connect should authenticate with, or `null`
155
+ * on sign-out. See {@link authToken}.
156
+ *
157
+ * Does not touch the CURRENT socket: callers authenticate that themselves
158
+ * (sign-in and session restore both already do). This only makes the next
159
+ * from-scratch connect reproduce that state.
160
+ */
161
+ setAuthToken(token: string | null): void {
162
+ this.authToken = token;
163
+ }
164
+
165
+ /** Resolved reconnect tunables; the supervisor reads its own knobs here. */
166
+ getReconnectConfig(): Required<ReconnectConfig> {
167
+ return this.reconnectConfig;
168
+ }
169
+
170
+ /** Current transport state as reported by the SDK. */
171
+ getStatus(): ConnectionState {
172
+ return this.client.status as ConnectionState;
173
+ }
174
+
175
+ /**
176
+ * Observe transport events. Thin passthrough so callers (the supervisor,
177
+ * sync, CRDT) don't have to reach through `getClient()`.
178
+ */
179
+ subscribeConnection<K extends RemoteConnectionEvent>(
180
+ event: K,
181
+ cb: (...payload: SurrealEvents[K]) => void
182
+ ): () => void {
183
+ return this.client.subscribe(event, cb);
184
+ }
185
+
186
+ /**
187
+ * Tear the socket down on purpose. Used by the heartbeat watchdog when a
188
+ * socket stops answering but never closes: `close()` makes the SDK publish
189
+ * `disconnected`, which is what drives the supervisor's revive loop.
190
+ */
191
+ async forceClose(): Promise<void> {
192
+ try {
193
+ await this.client.close();
194
+ } catch (err) {
195
+ this.logger.debug(
196
+ { err, Category: 'sp00ky-client::RemoteDatabaseService::forceClose' },
197
+ 'forceClose failed; treating the socket as gone anyway'
198
+ );
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Hold statements while a connect is in flight.
204
+ *
205
+ * `connect()` opens the socket, then selects the namespace/database, then
206
+ * authenticates - three round trips. The SDK considers the connection ready
207
+ * after the first, so a query queued during the other two ran on a socket
208
+ * with no namespace ("Specify a namespace to use") or, worse, no identity.
209
+ * Seen 2026-09-06 after the supervisor rebuilt a socket: a collection
210
+ * recount failed with exactly that error. Waiting on the in-flight connect
211
+ * costs nothing when no connect is running; a failed connect is not
212
+ * swallowed here - the query then fails on its own, as before.
213
+ */
214
+ protected override async beforeQuery(): Promise<void> {
215
+ const inflight = this.connecting;
216
+ if (!inflight) return;
217
+ try {
218
+ await inflight;
219
+ } catch {
220
+ // The query that follows reports the failure itself.
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Open (or re-open) the remote connection.
226
+ *
227
+ * Safe to call repeatedly: concurrent calls share the in-flight attempt, and
228
+ * a call after a `disconnected` builds a fresh socket. `use()` and
229
+ * `authenticate()` are re-applied here for the cold path; the SDK also
230
+ * replays them itself on its own internal reconnects.
231
+ */
50
232
  async connect(): Promise<void> {
51
- const { endpoint, token, namespace, database } = this.getConfig();
233
+ if (this.connecting) return this.connecting;
234
+ this.connecting = this.doConnect().finally(() => {
235
+ this.connecting = null;
236
+ });
237
+ return this.connecting;
238
+ }
239
+
240
+ private async doConnect(): Promise<void> {
241
+ const { endpoint, namespace, database } = this.getConfig();
242
+ // The live token wins over the constructor-time one: this connect may be
243
+ // the supervisor rebuilding a socket long after sign-in. See `authToken`.
244
+ const token = this.authToken ?? this.getConfig().token;
52
245
  if (endpoint) {
53
246
  this.logger.info(
54
247
  {
@@ -60,7 +253,21 @@ export class RemoteDatabaseService extends AbstractDatabaseService {
60
253
  'Connecting to remote database'
61
254
  );
62
255
  try {
63
- await this.client.connect(endpoint);
256
+ // Without explicit options the SDK caps itself at 5 attempts (~62s of
257
+ // outage) and then gives up permanently — nothing would re-open the
258
+ // socket after that. `attempts: -1` keeps it trying; the supervisor
259
+ // still covers the case where the SDK terminates the engine because its
260
+ // post-reconnect handshake threw.
261
+ await this.client.connect(endpoint, {
262
+ reconnect: {
263
+ enabled: true,
264
+ attempts: this.reconnectConfig.attempts,
265
+ retryDelay: 1_000,
266
+ retryDelayMax: this.reconnectConfig.retryDelayMax,
267
+ retryDelayMultiplier: 2,
268
+ retryDelayJitter: 0.1,
269
+ },
270
+ });
64
271
  await this.client.use({
65
272
  namespace,
66
273
  database,