@syncular/client 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +227 -709
  2. package/dist/apply.d.ts +80 -0
  3. package/dist/apply.js +301 -0
  4. package/dist/blob.d.ts +127 -0
  5. package/dist/blob.js +201 -0
  6. package/dist/bun-database.d.ts +22 -0
  7. package/dist/bun-database.js +61 -0
  8. package/dist/client.d.ts +320 -69
  9. package/dist/client.js +1627 -163
  10. package/dist/content-type.d.ts +2 -0
  11. package/dist/content-type.js +2 -0
  12. package/dist/database.d.ts +35 -80
  13. package/dist/database.js +32 -947
  14. package/dist/encryption.d.ts +47 -0
  15. package/dist/encryption.js +75 -0
  16. package/dist/errors.d.ts +8 -22
  17. package/dist/errors.js +10 -207
  18. package/dist/http.d.ts +32 -0
  19. package/dist/http.js +196 -0
  20. package/dist/index.d.ts +28 -16
  21. package/dist/index.js +25 -12
  22. package/dist/invalidation.d.ts +69 -0
  23. package/dist/invalidation.js +84 -0
  24. package/dist/leader-lock.d.ts +28 -0
  25. package/dist/leader-lock.js +38 -0
  26. package/dist/multi-tab.d.ts +134 -0
  27. package/dist/multi-tab.js +399 -0
  28. package/dist/node-database.d.ts +41 -0
  29. package/dist/node-database.js +133 -0
  30. package/dist/outbox.d.ts +56 -0
  31. package/dist/outbox.js +140 -0
  32. package/dist/schema.d.ts +107 -0
  33. package/dist/schema.js +267 -0
  34. package/dist/state.d.ts +40 -0
  35. package/dist/state.js +77 -0
  36. package/dist/transport.d.ts +44 -0
  37. package/dist/transport.js +7 -0
  38. package/dist/wasm-database.d.ts +30 -0
  39. package/dist/wasm-database.js +170 -0
  40. package/dist/window.d.ts +64 -0
  41. package/dist/window.js +0 -0
  42. package/dist/worker-entry.d.ts +16 -2
  43. package/dist/worker-entry.js +300 -456
  44. package/dist/worker-host.d.ts +174 -0
  45. package/dist/worker-host.js +536 -0
  46. package/dist/worker-protocol.d.ts +173 -61
  47. package/dist/worker-protocol.js +7 -16
  48. package/package.json +45 -112
  49. package/src/apply.ts +435 -0
  50. package/src/blob.ts +333 -0
  51. package/src/bun-database.ts +75 -0
  52. package/src/client.ts +2247 -277
  53. package/src/content-type.ts +2 -0
  54. package/src/database.ts +63 -1389
  55. package/src/encryption.ts +123 -0
  56. package/src/errors.ts +11 -265
  57. package/src/http.ts +254 -0
  58. package/src/index.ts +25 -20
  59. package/src/invalidation.ts +128 -0
  60. package/src/leader-lock.ts +68 -0
  61. package/src/multi-tab.ts +550 -0
  62. package/src/node-database.ts +180 -0
  63. package/src/outbox.ts +206 -0
  64. package/src/schema.ts +397 -0
  65. package/src/state.ts +126 -0
  66. package/src/transport.ts +52 -0
  67. package/src/wasm-database.ts +279 -0
  68. package/src/window.ts +0 -0
  69. package/src/worker-entry.ts +391 -545
  70. package/src/worker-host.ts +800 -0
  71. package/src/worker-protocol.ts +204 -99
  72. package/dist/auth-leases.d.ts +0 -11
  73. package/dist/auth-leases.d.ts.map +0 -1
  74. package/dist/auth-leases.js +0 -140
  75. package/dist/auth-leases.js.map +0 -1
  76. package/dist/blob-limits.d.ts +0 -15
  77. package/dist/blob-limits.d.ts.map +0 -1
  78. package/dist/blob-limits.js +0 -66
  79. package/dist/blob-limits.js.map +0 -1
  80. package/dist/bridge-client.d.ts +0 -69
  81. package/dist/bridge-client.d.ts.map +0 -1
  82. package/dist/bridge-client.js +0 -282
  83. package/dist/bridge-client.js.map +0 -1
  84. package/dist/client-config.d.ts +0 -6
  85. package/dist/client-config.d.ts.map +0 -1
  86. package/dist/client-config.js +0 -18
  87. package/dist/client-config.js.map +0 -1
  88. package/dist/client.d.ts.map +0 -1
  89. package/dist/client.js.map +0 -1
  90. package/dist/command-history.d.ts +0 -35
  91. package/dist/command-history.d.ts.map +0 -1
  92. package/dist/command-history.js +0 -378
  93. package/dist/command-history.js.map +0 -1
  94. package/dist/console-diagnostics.d.ts +0 -33
  95. package/dist/console-diagnostics.d.ts.map +0 -1
  96. package/dist/console-diagnostics.js +0 -496
  97. package/dist/console-diagnostics.js.map +0 -1
  98. package/dist/crdt-yjs/index.d.ts +0 -4
  99. package/dist/crdt-yjs/index.d.ts.map +0 -1
  100. package/dist/crdt-yjs/index.js +0 -4
  101. package/dist/crdt-yjs/index.js.map +0 -1
  102. package/dist/crdt-yjs/webview-host-facade.d.ts +0 -126
  103. package/dist/crdt-yjs/webview-host-facade.d.ts.map +0 -1
  104. package/dist/crdt-yjs/webview-host-facade.js +0 -284
  105. package/dist/crdt-yjs/webview-host-facade.js.map +0 -1
  106. package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +0 -153
  107. package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +0 -1
  108. package/dist/crdt-yjs/yjs-document-field-adapter.js +0 -406
  109. package/dist/crdt-yjs/yjs-document-field-adapter.js.map +0 -1
  110. package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +0 -73
  111. package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +0 -1
  112. package/dist/crdt-yjs/yjs-prosemirror-bridge.js +0 -169
  113. package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +0 -1
  114. package/dist/database.d.ts.map +0 -1
  115. package/dist/database.js.map +0 -1
  116. package/dist/diagnostics.d.ts +0 -10
  117. package/dist/diagnostics.d.ts.map +0 -1
  118. package/dist/diagnostics.js +0 -83
  119. package/dist/diagnostics.js.map +0 -1
  120. package/dist/errors.d.ts.map +0 -1
  121. package/dist/errors.js.map +0 -1
  122. package/dist/generated-bridge.d.ts +0 -365
  123. package/dist/generated-bridge.d.ts.map +0 -1
  124. package/dist/generated-bridge.js +0 -250
  125. package/dist/generated-bridge.js.map +0 -1
  126. package/dist/index.d.ts.map +0 -1
  127. package/dist/index.js.map +0 -1
  128. package/dist/mutations.d.ts +0 -72
  129. package/dist/mutations.d.ts.map +0 -1
  130. package/dist/mutations.js +0 -63
  131. package/dist/mutations.js.map +0 -1
  132. package/dist/network.d.ts +0 -3
  133. package/dist/network.d.ts.map +0 -1
  134. package/dist/network.js +0 -17
  135. package/dist/network.js.map +0 -1
  136. package/dist/react/index.d.ts +0 -169
  137. package/dist/react/index.d.ts.map +0 -1
  138. package/dist/react/index.js +0 -628
  139. package/dist/react/index.js.map +0 -1
  140. package/dist/react-native/index.d.ts +0 -35
  141. package/dist/react-native/index.d.ts.map +0 -1
  142. package/dist/react-native/index.js +0 -49
  143. package/dist/react-native/index.js.map +0 -1
  144. package/dist/runtime-contract.d.ts +0 -13
  145. package/dist/runtime-contract.d.ts.map +0 -1
  146. package/dist/runtime-contract.js +0 -24
  147. package/dist/runtime-contract.js.map +0 -1
  148. package/dist/rust-client.d.ts +0 -106
  149. package/dist/rust-client.d.ts.map +0 -1
  150. package/dist/rust-client.js +0 -938
  151. package/dist/rust-client.js.map +0 -1
  152. package/dist/sentry.d.ts +0 -35
  153. package/dist/sentry.d.ts.map +0 -1
  154. package/dist/sentry.js +0 -155
  155. package/dist/sentry.js.map +0 -1
  156. package/dist/sql-safety.d.ts +0 -3
  157. package/dist/sql-safety.d.ts.map +0 -1
  158. package/dist/sql-safety.js +0 -62
  159. package/dist/sql-safety.js.map +0 -1
  160. package/dist/syncular-runtime-artifacts.json +0 -61
  161. package/dist/tauri/index.d.ts +0 -35
  162. package/dist/tauri/index.d.ts.map +0 -1
  163. package/dist/tauri/index.js +0 -114
  164. package/dist/tauri/index.js.map +0 -1
  165. package/dist/types.d.ts +0 -1096
  166. package/dist/types.d.ts.map +0 -1
  167. package/dist/types.js +0 -2
  168. package/dist/types.js.map +0 -1
  169. package/dist/wasm/.syncular-wasm-profile +0 -1
  170. package/dist/wasm/syncular-runtime-artifact.json +0 -21
  171. package/dist/wasm/syncular.d.ts +0 -207
  172. package/dist/wasm/syncular.js +0 -2341
  173. package/dist/wasm/syncular_bg.wasm +0 -0
  174. package/dist/wasm/syncular_bg.wasm.d.ts +0 -97
  175. package/dist/wasm-bindings/runtime-contract.d.ts +0 -22
  176. package/dist/wasm-bindings/runtime-contract.d.ts.map +0 -1
  177. package/dist/wasm-bindings/runtime-contract.js +0 -112
  178. package/dist/wasm-bindings/runtime-contract.js.map +0 -1
  179. package/dist/wasm-core/.syncular-wasm-profile +0 -1
  180. package/dist/wasm-core/syncular-runtime-artifact.json +0 -17
  181. package/dist/wasm-core/syncular.d.ts +0 -162
  182. package/dist/wasm-core/syncular.js +0 -1847
  183. package/dist/wasm-core/syncular_bg.wasm +0 -0
  184. package/dist/wasm-core/syncular_bg.wasm.d.ts +0 -77
  185. package/dist/wasm-perf/.syncular-wasm-profile +0 -1
  186. package/dist/wasm-perf/syncular-runtime-artifact.json +0 -21
  187. package/dist/wasm-perf/syncular.d.ts +0 -207
  188. package/dist/wasm-perf/syncular.js +0 -2341
  189. package/dist/wasm-perf/syncular_bg.wasm +0 -0
  190. package/dist/wasm-perf/syncular_bg.wasm.d.ts +0 -97
  191. package/dist/wasm-runtime.d.ts +0 -23
  192. package/dist/wasm-runtime.d.ts.map +0 -1
  193. package/dist/wasm-runtime.js +0 -69
  194. package/dist/wasm-runtime.js.map +0 -1
  195. package/dist/worker-client.d.ts +0 -123
  196. package/dist/worker-client.d.ts.map +0 -1
  197. package/dist/worker-client.js +0 -1735
  198. package/dist/worker-client.js.map +0 -1
  199. package/dist/worker-entry.d.ts.map +0 -1
  200. package/dist/worker-entry.js.map +0 -1
  201. package/dist/worker-protocol.d.ts.map +0 -1
  202. package/dist/worker-protocol.js.map +0 -1
  203. package/dist/worker-realtime.d.ts +0 -39
  204. package/dist/worker-realtime.d.ts.map +0 -1
  205. package/dist/worker-realtime.js +0 -677
  206. package/dist/worker-realtime.js.map +0 -1
  207. package/src/auth-leases.ts +0 -251
  208. package/src/blob-limits.ts +0 -98
  209. package/src/bridge-client.ts +0 -512
  210. package/src/client-config.ts +0 -29
  211. package/src/command-history.ts +0 -623
  212. package/src/console-diagnostics.ts +0 -617
  213. package/src/crdt-yjs/index.ts +0 -3
  214. package/src/crdt-yjs/webview-host-facade.ts +0 -477
  215. package/src/crdt-yjs/yjs-document-field-adapter.ts +0 -733
  216. package/src/crdt-yjs/yjs-prosemirror-bridge.ts +0 -272
  217. package/src/diagnostics.ts +0 -116
  218. package/src/generated-bridge.ts +0 -741
  219. package/src/mutations.ts +0 -168
  220. package/src/network.ts +0 -32
  221. package/src/react/index.ts +0 -1036
  222. package/src/react-native/index.ts +0 -152
  223. package/src/runtime-contract.ts +0 -48
  224. package/src/rust-client.ts +0 -1491
  225. package/src/sentry.ts +0 -215
  226. package/src/sql-safety.ts +0 -61
  227. package/src/tauri/index.ts +0 -211
  228. package/src/types.ts +0 -1397
  229. package/src/wasm-bindings/generated-wasm-bindings.d.ts +0 -70
  230. package/src/wasm-bindings/runtime-contract.ts +0 -158
  231. package/src/wasm-runtime.ts +0 -145
  232. package/src/worker-client.ts +0 -2289
  233. package/src/worker-realtime.ts +0 -843
@@ -1,584 +1,430 @@
1
- import { resolveSyncularClientConfig } from './client-config';
2
- import { syncularDiagnosticAttemptFields } from './diagnostics';
1
+ /**
2
+ * Worker-side bootstrap (Direction decision 2): constructs the WHOLE
3
+ * client core — SyncClient + transports + realtime + SQLite — inside the
4
+ * worker and serves the `worker-protocol` RPC over postMessage.
5
+ *
6
+ * Browsers get the defaults: opfs-sahpool persistence and the fetch/WS
7
+ * transports from `./http`, wired from `WorkerInitConfig.endpoints`.
8
+ * Tests inject a database factory (bun:sqlite) through `overrides` — the
9
+ * same bootstrap, the same RPC, a different SQLite.
10
+ *
11
+ * The §8.4 host loop lives HERE: wake-ups (hello/`sync` events, delta
12
+ * drops) coalesce into one jittered `syncUntilIdle` round in the worker;
13
+ * RPC-driven and auto-driven sync rounds serialize on one queue because
14
+ * the core owns exactly one loop.
15
+ */
16
+ import { SyncClient } from './client';
17
+ import type { ClientDatabase } from './database';
18
+ import { ClientSyncError } from './errors';
3
19
  import {
4
- classifySyncularError,
5
- SyncularClientError,
6
- syncularErrorDetails,
7
- syncularErrorMessage,
8
- syncularErrorStatus,
9
- } from './errors';
10
- import {
11
- dispatchGeneratedSyncularWorkerRequest,
12
- generatedSyncularWorkerRequestDiagnosticSource,
13
- isGeneratedSyncularAbortableWorkerRequestType,
14
- isGeneratedSyncularDiagnosedSuccessWorkerRequestType,
15
- } from './generated-bridge';
16
- import { createSyncularRuntimeInfo } from './runtime-contract';
17
- import { openSyncularRustClient, type SyncularRustClient } from './rust-client';
18
- import type {
19
- ResolvedSyncularClientConfig,
20
- SyncularDiagnosticEvent,
21
- } from './types';
22
- import {
23
- getSyncularRustRuntimeInfo,
24
- getSyncularWasmGlueUrl,
25
- getSyncularWasmUrl,
26
- type SyncularWasmGlue,
27
- } from './wasm-runtime';
20
+ httpBlobTransport,
21
+ httpSegmentDownloader,
22
+ httpSyncTransport,
23
+ webSocketRealtimeConnector,
24
+ } from './http';
28
25
  import type {
29
- SyncularWorkerErrorPayload,
30
- SyncularWorkerOutboundMessage,
31
- SyncularWorkerRequest,
32
- SyncularWorkerRuntimeArtifact,
33
- } from './worker-protocol';
26
+ RealtimeConnector,
27
+ SegmentDownloader,
28
+ SyncTransport,
29
+ } from './transport';
30
+ import { openPersistentWasmDatabase } from './wasm-database';
34
31
  import {
35
- createSyncularWorkerErrorPayload,
36
- SYNCULAR_WORKER_PROTOCOL_VERSION,
32
+ type MainToWorkerMessage,
33
+ WORKER_FAILED_CODE,
34
+ type WorkerApi,
35
+ type WorkerCallMessage,
36
+ type WorkerErrorShape,
37
+ type WorkerInitConfig,
38
+ type WorkerInitResult,
39
+ type WorkerToMainMessage,
37
40
  } from './worker-protocol';
38
- import { SyncularWorkerRealtimeController } from './worker-realtime';
39
-
40
- let client: SyncularRustClient | undefined;
41
- let openedConfig: ResolvedSyncularClientConfig | undefined;
42
- let openedRuntime: SyncularWorkerRuntimeArtifact | undefined;
43
- let removeRowsChangedListener: (() => void) | undefined;
44
- const canceledRequests = new Set<number>();
45
- const abortControllers = new Map<number, AbortController>();
46
- const realtime = new SyncularWorkerRealtimeController({
47
- getClient: requireClient,
48
- getConfig: () => openedConfig,
49
- getLocationOrigin: () =>
50
- typeof self.location?.origin === 'string'
51
- ? self.location.origin
52
- : 'http://localhost',
53
- createWebSocket: (url) => new WebSocket(url),
54
- postEvent: (event) => self.postMessage(event),
55
- postDiagnostic,
56
- });
57
-
58
- self.onmessage = (event: MessageEvent<SyncularWorkerRequest>) => {
59
- void handleRequest(event.data);
60
- };
61
-
62
- async function handleRequest(request: SyncularWorkerRequest): Promise<void> {
63
- if (request.protocolVersion !== SYNCULAR_WORKER_PROTOCOL_VERSION) {
64
- post({
65
- id: request.id,
66
- protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
67
- ok: false,
68
- error: createSyncularWorkerErrorPayload(
69
- 'worker.protocol_mismatch',
70
- `Unsupported Syncular worker protocol ${request.protocolVersion}`,
71
- { details: { supported: SYNCULAR_WORKER_PROTOCOL_VERSION } }
72
- ),
73
- });
74
- return;
75
- }
76
-
77
- if (request.type === 'cancel') {
78
- canceledRequests.add(request.requestId);
79
- const controller = abortControllers.get(request.requestId);
80
- controller?.abort();
81
- if (controller) {
82
- postDiagnostic({
83
- level: 'warn',
84
- source: 'worker',
85
- code: 'worker.request_aborted',
86
- message: 'Syncular worker request aborted',
87
- details: { requestId: request.requestId },
88
- });
89
- }
90
- post({
91
- id: request.id,
92
- protocolVersion: request.protocolVersion,
93
- ok: true,
94
- });
95
- return;
96
- }
97
-
98
- const startedAt = Date.now();
99
- const abortController = createAbortController(request.type);
100
- if (abortController) {
101
- abortControllers.set(request.id, abortController);
102
- }
103
- try {
104
- if (abortController) client?.setAbortSignal(abortController.signal);
105
- const value = await dispatch(request);
106
- if (canceledRequests.delete(request.id)) return;
107
- post({
108
- id: request.id,
109
- protocolVersion: request.protocolVersion,
110
- ok: true,
111
- value,
112
- });
113
- postRequestSuccessDiagnostic(request, value, Date.now() - startedAt);
114
- } catch (err) {
115
- if (canceledRequests.delete(request.id)) return;
116
- const source = requestDiagnosticSource(request.type);
117
- const resyncRequired = errorRequiresFullRefresh(err);
118
- const encodedError = encodeWorkerError(err);
119
- const diagnosticCode = resyncRequired
120
- ? `${source}.resync_required`
121
- : encodedError.code.startsWith(`${source}.`)
122
- ? encodedError.code
123
- : `${source}.${request.type}.failed`;
124
- postDiagnostic({
125
- level: requestDiagnosticLevel(request.type, err),
126
- source,
127
- code: diagnosticCode,
128
- message: resyncRequired
129
- ? `Syncular worker request ${request.type} requires full resync`
130
- : `Syncular worker request ${request.type} failed`,
131
- details: {
132
- requestType: request.type,
133
- durationMs: Date.now() - startedAt,
134
- error: errorMessage(err),
135
- errorCode: encodedError.code,
136
- ...(encodedError.category ? { category: encodedError.category } : {}),
137
- ...(encodedError.retryable != null
138
- ? { retryable: encodedError.retryable }
139
- : {}),
140
- ...(encodedError.recommendedAction
141
- ? { recommendedAction: encodedError.recommendedAction }
142
- : {}),
143
- ...(resyncRequired ? { resyncRequired: true } : {}),
144
- ...diagnosticStatus(err),
145
- },
146
- ...requestSyncAttemptDiagnosticFields(request),
147
- });
148
- post({
149
- id: request.id,
150
- protocolVersion: request.protocolVersion,
151
- ok: false,
152
- error: encodedError,
153
- });
154
- } finally {
155
- abortControllers.delete(request.id);
156
- if (abortController) clearClientAbortSignal();
157
- }
158
- }
159
-
160
- async function dispatch(request: SyncularWorkerRequest): Promise<unknown> {
161
- return dispatchGeneratedSyncularWorkerRequest(
162
- {
163
- requireClient,
164
- openClient,
165
- startRealtime: (options) => {
166
- realtime.start(options);
167
- return true;
168
- },
169
- stopRealtime: () => {
170
- realtime.stop();
171
- return true;
172
- },
173
- sendPresence: (action, scopeKey, metadata) => {
174
- realtime.sendPresence(action, scopeKey, metadata);
175
- return true;
176
- },
177
- runtimeInfo,
178
- closeClient,
179
- },
180
- request
181
- );
182
- }
183
-
184
- async function openClient(
185
- request: Extract<SyncularWorkerRequest, { type: 'open' }>
186
- ): Promise<boolean> {
187
- const config = resolveSyncularClientConfig(request.config);
188
- realtime.stop();
189
- detachRowsChangedListener();
190
- client?.close();
191
- client = undefined;
192
- openedConfig = undefined;
193
- openedRuntime = undefined;
194
- const runtime = request.runtime;
195
- client = await openSyncularRustClient({
196
- config,
197
- module: runtime?.wasmGlueUrl
198
- ? loadWorkerWasmGlue(runtime.wasmGlueUrl)
199
- : undefined,
200
- wasmGlueUrl: runtime?.wasmGlueUrl,
201
- wasmUrl: runtime?.wasmUrl,
202
- });
203
- attachRowsChangedListener(client);
204
- openedConfig = config;
205
- openedRuntime = runtime;
206
- return true;
207
- }
208
-
209
- function closeClient(): boolean {
210
- realtime.stop();
211
- detachRowsChangedListener();
212
- client?.close();
213
- client = undefined;
214
- openedConfig = undefined;
215
- openedRuntime = undefined;
216
- return true;
217
- }
218
41
 
219
- async function runtimeInfo(): Promise<
220
- ReturnType<typeof createSyncularRuntimeInfo>
221
- > {
222
- const wasmUrl = getSyncularWasmUrl();
223
- const selectedWasmUrl = openedRuntime?.wasmUrl ?? wasmUrl;
224
- const selectedWasmGlueUrl =
225
- openedRuntime?.wasmGlueUrl ?? getSyncularWasmGlueUrl();
226
- return createSyncularRuntimeInfo({
227
- storage: openedConfig?.storage,
228
- workerUrl:
229
- typeof self.location?.href === 'string' ? self.location.href : '',
230
- wasmGlueUrl: selectedWasmGlueUrl,
231
- wasmUrl: selectedWasmUrl,
232
- rust: await getSyncularRustRuntimeInfo(
233
- openedRuntime?.wasmGlueUrl
234
- ? loadWorkerWasmGlue(openedRuntime.wasmGlueUrl)
235
- : undefined,
236
- selectedWasmUrl
237
- ),
238
- });
42
+ export interface SyncWorkerOverrides {
43
+ /** Database factory indirection: tests inject bun:sqlite here. */
44
+ readonly openDatabase?: (
45
+ config: WorkerInitConfig,
46
+ ) => Promise<ClientDatabase> | ClientDatabase;
47
+ readonly createTransport?: (config: WorkerInitConfig) => SyncTransport;
48
+ readonly createSegments?: (
49
+ config: WorkerInitConfig,
50
+ ) => SegmentDownloader | undefined;
51
+ readonly createRealtime?: (
52
+ config: WorkerInitConfig,
53
+ clientId: string,
54
+ ) => RealtimeConnector | undefined;
239
55
  }
240
56
 
241
- function loadWorkerWasmGlue(wasmGlueUrl: string): Promise<SyncularWasmGlue> {
242
- return import(/* @vite-ignore */ wasmGlueUrl) as Promise<SyncularWasmGlue>;
57
+ /** Minimal structural view of the dedicated-worker global scope. */
58
+ interface WorkerScope {
59
+ postMessage(message: unknown, transfer?: Transferable[]): void;
60
+ addEventListener(
61
+ type: 'message',
62
+ listener: (event: MessageEvent) => void,
63
+ ): void;
243
64
  }
244
65
 
245
- function requireClient(): SyncularRustClient {
246
- if (!client) {
247
- throw createSyncularWorkerErrorPayload(
248
- 'worker.not_open',
249
- 'Syncular worker client is not open'
250
- );
66
+ function toErrorShape(error: unknown): WorkerErrorShape {
67
+ if (error instanceof ClientSyncError) {
68
+ return {
69
+ code: error.code,
70
+ message: error.message,
71
+ retryable: error.retryable,
72
+ };
251
73
  }
252
- return client;
253
- }
254
-
255
- function attachRowsChangedListener(nextClient: SyncularRustClient): void {
256
- removeRowsChangedListener = nextClient.addRowsChangedListener((event) => {
257
- post({
258
- protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
259
- type: 'rowsChanged',
260
- source: event.source,
261
- changedTables: event.changedTables,
262
- changedRows: event.changedRows,
263
- changedRowsTruncated: event.changedRowsTruncated,
264
- });
265
- });
266
- }
267
-
268
- function detachRowsChangedListener(): void {
269
- removeRowsChangedListener?.();
270
- removeRowsChangedListener = undefined;
271
- }
272
-
273
- function post(message: SyncularWorkerOutboundMessage): void {
274
- self.postMessage(message);
74
+ return {
75
+ code: WORKER_FAILED_CODE,
76
+ message: error instanceof Error ? error.message : String(error),
77
+ retryable: false,
78
+ };
275
79
  }
276
80
 
277
- function createAbortController(
278
- type: SyncularWorkerRequest['type']
279
- ): AbortController | undefined {
280
- if (!isAbortableRequest(type) || typeof AbortController === 'undefined') {
281
- return undefined;
81
+ /**
82
+ * ArrayBuffers safe to transfer out of a query result: whole-buffer
83
+ * views only (a view into a larger/pooled buffer is cloned instead —
84
+ * transferring would detach unrelated data), deduplicated because a
85
+ * buffer may only appear once in a transfer list.
86
+ */
87
+ function queryTransferables(rows: readonly unknown[]): Transferable[] {
88
+ const buffers = new Set<ArrayBuffer>();
89
+ for (const row of rows) {
90
+ if (row === null || typeof row !== 'object') continue;
91
+ for (const value of Object.values(row)) {
92
+ if (
93
+ value instanceof Uint8Array &&
94
+ value.buffer instanceof ArrayBuffer &&
95
+ value.byteOffset === 0 &&
96
+ value.byteLength === value.buffer.byteLength
97
+ ) {
98
+ buffers.add(value.buffer);
99
+ }
100
+ }
282
101
  }
283
- return new AbortController();
102
+ return [...buffers];
284
103
  }
285
104
 
286
- function isAbortableRequest(type: SyncularWorkerRequest['type']): boolean {
287
- return isGeneratedSyncularAbortableWorkerRequestType(type);
288
- }
105
+ /**
106
+ * Register the RPC server on the current worker scope. Call this once
107
+ * from a worker entry script; the main-thread handle drives everything
108
+ * else through `init`/`call` messages.
109
+ */
110
+ export function startSyncWorker(overrides: SyncWorkerOverrides = {}): void {
111
+ const scope = globalThis as unknown as WorkerScope;
112
+ const post = (message: WorkerToMainMessage, transfer?: Transferable[]) => {
113
+ if (transfer !== undefined && transfer.length > 0) {
114
+ scope.postMessage(message, transfer);
115
+ } else {
116
+ scope.postMessage(message);
117
+ }
118
+ };
289
119
 
290
- function clearClientAbortSignal(): void {
291
- try {
292
- client?.setAbortSignal(null);
293
- } catch {
294
- // Best effort after a request completed, failed, or closed the client.
120
+ let client: SyncClient | undefined;
121
+ let database: ClientDatabase | undefined;
122
+ let offline = false;
123
+ let closed = false;
124
+
125
+ // -- one sync loop: RPC-driven and auto-driven rounds serialize ----------
126
+ let syncChain: Promise<unknown> = Promise.resolve();
127
+ function serializedSync<T>(fn: () => Promise<T>): Promise<T> {
128
+ const next = syncChain.then(fn, fn);
129
+ syncChain = next.then(
130
+ () => undefined,
131
+ () => undefined,
132
+ );
133
+ return next;
295
134
  }
296
- }
297
135
 
298
- function postDiagnostic(
299
- event: Omit<SyncularDiagnosticEvent, 'at'> & { at?: number }
300
- ): void {
301
- self.postMessage({
302
- protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
303
- type: 'diagnostic',
304
- event: {
305
- at: event.at ?? Date.now(),
306
- level: event.level,
307
- source: event.source,
308
- code: event.code,
309
- message: event.message,
310
- ...(event.syncAttemptId ? { syncAttemptId: event.syncAttemptId } : {}),
311
- ...(event.traceId ? { traceId: event.traceId } : {}),
312
- ...(event.spanId ? { spanId: event.spanId } : {}),
313
- ...(event.clientId ? { clientId: event.clientId } : {}),
314
- ...(event.subscriptionId ? { subscriptionId: event.subscriptionId } : {}),
315
- ...(event.table ? { table: event.table } : {}),
316
- ...(event.rowId ? { rowId: event.rowId } : {}),
317
- ...(event.cursor !== undefined ? { cursor: event.cursor } : {}),
318
- ...(event.details ? { details: event.details } : {}),
319
- },
320
- });
321
- }
322
-
323
- function postRequestSuccessDiagnostic(
324
- request: SyncularWorkerRequest,
325
- value: unknown,
326
- durationMs: number
327
- ): void {
328
- if (!isDiagnosedSuccessRequest(request.type)) return;
329
- const syncRevocation = syncScopeRevocationDetails(value);
330
- postDiagnostic({
331
- level: 'info',
332
- source: requestDiagnosticSource(request.type),
333
- code: `${requestDiagnosticSource(request.type)}.${request.type}.completed`,
334
- message: `Syncular worker request ${request.type} completed`,
335
- ...requestSyncAttemptDiagnosticFields(request),
336
- details: {
337
- requestType: request.type,
338
- durationMs,
339
- ...requestSuccessDetails(request, value),
340
- },
341
- });
342
- if (
343
- syncRevocation &&
344
- (request.type === 'syncPull' ||
345
- request.type === 'syncPush' ||
346
- request.type === 'syncOnce')
347
- ) {
348
- postDiagnostic({
349
- level: 'warn',
350
- source: 'sync',
351
- code: 'sync.scope_revoked',
352
- message: 'Syncular subscription scope revoked',
353
- ...requestSyncAttemptDiagnosticFields(request),
354
- details: {
355
- requestType: request.type,
356
- ...syncRevocation,
136
+ // -- §8.4 host loop: coalesced, jittered, inside the worker --------------
137
+ let autoSync = true;
138
+ let wakeJitterMs = 200;
139
+ let autoSyncScheduled = false;
140
+ function scheduleAutoSync(): void {
141
+ if (!autoSync || autoSyncScheduled || closed || client === undefined) {
142
+ return;
143
+ }
144
+ autoSyncScheduled = true;
145
+ setTimeout(
146
+ () => {
147
+ autoSyncScheduled = false;
148
+ if (closed || client === undefined) return;
149
+ const running = client;
150
+ void serializedSync(() => running.syncUntilIdle())
151
+ .then((summary) => {
152
+ if (!closed)
153
+ post({ t: 'event', event: { kind: 'synced', summary } });
154
+ })
155
+ .catch((error: unknown) => {
156
+ if (!closed) {
157
+ post({
158
+ t: 'event',
159
+ event: { kind: 'synced', error: toErrorShape(error) },
160
+ });
161
+ }
162
+ });
357
163
  },
358
- });
164
+ Math.floor(Math.random() * wakeJitterMs),
165
+ );
359
166
  }
360
- }
361
167
 
362
- function requestSyncAttemptDiagnosticFields(
363
- request: SyncularWorkerRequest
364
- ): Pick<SyncularDiagnosticEvent, 'syncAttemptId' | 'traceId' | 'spanId'> {
365
- if (
366
- request.type !== 'syncPull' &&
367
- request.type !== 'syncPush' &&
368
- request.type !== 'syncOnce'
369
- ) {
370
- return {};
168
+ function requireClient(): SyncClient {
169
+ if (client === undefined) {
170
+ throw new ClientSyncError(
171
+ WORKER_FAILED_CODE,
172
+ 'the worker received a call before init completed',
173
+ );
174
+ }
175
+ return client;
371
176
  }
372
- return syncularDiagnosticAttemptFields(request.syncAttempt);
373
- }
374
177
 
375
- function isDiagnosedSuccessRequest(
376
- type: SyncularWorkerRequest['type']
377
- ): boolean {
378
- return isGeneratedSyncularDiagnosedSuccessWorkerRequestType(type);
379
- }
178
+ function assertOnline(): void {
179
+ if (offline) {
180
+ throw new ClientSyncError(
181
+ 'sync.transport_failed',
182
+ 'the worker transport is offline (setOffline(true))',
183
+ true,
184
+ );
185
+ }
186
+ }
380
187
 
381
- function requestDiagnosticSource(
382
- type: SyncularWorkerRequest['type']
383
- ): SyncularDiagnosticEvent['source'] {
384
- return generatedSyncularWorkerRequestDiagnosticSource(type);
385
- }
188
+ function gateOffline<T>(inner: (request: T) => Promise<Uint8Array>) {
189
+ return (request: T): Promise<Uint8Array> => {
190
+ assertOnline();
191
+ return inner(request);
192
+ };
193
+ }
386
194
 
387
- function requestDiagnosticLevel(
388
- type: SyncularWorkerRequest['type'],
389
- error: unknown
390
- ): SyncularDiagnosticEvent['level'] {
391
- if (type === 'setAuthHeaders' || syncularErrorStatus(error)) return 'warn';
392
- return 'error';
393
- }
195
+ /** Offline gate that preserves the §5.4 `fetchUrl` capability marker. */
196
+ function gateSegmentsOffline(inner: SegmentDownloader): SegmentDownloader {
197
+ const fetchUrl = inner.fetchUrl;
198
+ return Object.assign(gateOffline(inner), {
199
+ ...(fetchUrl !== undefined
200
+ ? {
201
+ fetchUrl: (url: string) => {
202
+ assertOnline();
203
+ return fetchUrl(url);
204
+ },
205
+ }
206
+ : {}),
207
+ });
208
+ }
394
209
 
395
- function requestSuccessDetails(
396
- request: SyncularWorkerRequest,
397
- value: unknown
398
- ): Record<string, unknown> {
399
- switch (request.type) {
400
- case 'open':
401
- return {
402
- storage: openedConfig?.storage,
403
- clientId: openedConfig?.clientId,
404
- };
405
- case 'setAuthHeaders':
406
- return { headerCount: Object.keys(request.headers).length };
407
- case 'setFieldEncryption':
408
- return {
409
- enabled: request.config != null,
410
- ruleCount: request.config?.rules.length ?? 0,
411
- };
412
- case 'setEncryptedCrdt':
413
- return {
414
- enabled: request.config != null,
415
- keyCount: request.config ? Object.keys(request.config.keys).length : 0,
416
- };
417
- case 'setBlobEncryption':
418
- return {
419
- enabled: request.config != null,
420
- keyCount: request.config ? Object.keys(request.config.keys).length : 0,
421
- };
422
- case 'syncPull':
423
- case 'syncPush':
424
- case 'syncOnce':
425
- return syncResultDetails(value);
426
- case 'storeBlob':
427
- return { immediate: request.options?.immediate === true };
428
- case 'pruneBlobCache':
429
- return { maxBytes: request.maxBytes ?? null, prunedBytes: value };
430
- case 'compactStorage':
431
- return objectRecord(value);
432
- case 'localHealthCheck': {
433
- const report = objectRecord(value);
434
- const findings = Array.isArray(report.findings) ? report.findings : [];
435
- return {
436
- ok: report.ok === true,
437
- findingCount: findings.length,
438
- };
439
- }
440
- case 'exportLocalSupportBundle': {
441
- const bundle = objectRecord(value);
442
- const health = objectRecord(bundle.health);
443
- const findings = Array.isArray(health.findings) ? health.findings : [];
444
- return {
445
- redacted: bundle.redacted === true,
446
- source: bundle.source,
447
- findingCount: findings.length,
448
- };
210
+ async function init(config: WorkerInitConfig): Promise<WorkerInitResult> {
211
+ if (client !== undefined) {
212
+ throw new ClientSyncError(
213
+ WORKER_FAILED_CODE,
214
+ 'the worker is already initialized',
215
+ );
449
216
  }
450
- case 'importLocalSupportBundle':
451
- return objectRecord(value);
452
- case 'repairLocalHealth':
453
- return objectRecord(value);
454
- case 'resetLocalSyncState':
455
- return objectRecord(value);
456
- case 'processBlobUploadQueue':
457
- return objectRecord(value);
458
- default:
459
- return {};
217
+ autoSync = config.autoSync ?? true;
218
+ wakeJitterMs = config.wakeJitterMs ?? 200;
219
+
220
+ database =
221
+ overrides.openDatabase !== undefined
222
+ ? await overrides.openDatabase(config)
223
+ : await defaultOpenDatabase(config);
224
+ const transport = gateOffline(
225
+ overrides.createTransport !== undefined
226
+ ? overrides.createTransport(config)
227
+ : httpSyncTransport(config.endpoints.syncUrl),
228
+ );
229
+ const segments =
230
+ overrides.createSegments !== undefined
231
+ ? overrides.createSegments(config)
232
+ : config.endpoints.segmentsUrl !== undefined
233
+ ? httpSegmentDownloader(config.endpoints.segmentsUrl)
234
+ : undefined;
235
+ // §5.9 blob transport: only when a blobs URL is configured.
236
+ const blobs =
237
+ config.endpoints.blobsUrl !== undefined
238
+ ? httpBlobTransport(config.endpoints.blobsUrl)
239
+ : undefined;
240
+
241
+ // Realtime needs the (possibly persisted) clientId for the
242
+ // `{clientId}` URL placeholder — start the client first, then attach
243
+ // the connector through a mutable slot read at connect time.
244
+ let realtimeConnector: RealtimeConnector | undefined;
245
+ const started = new SyncClient({
246
+ database,
247
+ schema: config.schema,
248
+ transport,
249
+ ...(segments !== undefined
250
+ ? { segments: gateSegmentsOffline(segments) }
251
+ : {}),
252
+ ...(blobs !== undefined ? { blobs } : {}),
253
+ realtime: (handlers) => {
254
+ if (offline) {
255
+ throw new ClientSyncError(
256
+ 'sync.transport_failed',
257
+ 'the worker realtime channel is offline (setOffline(true))',
258
+ true,
259
+ );
260
+ }
261
+ if (realtimeConnector === undefined) {
262
+ throw new ClientSyncError(
263
+ 'sync.invalid_request',
264
+ 'no realtime connector configured',
265
+ );
266
+ }
267
+ return realtimeConnector(handlers);
268
+ },
269
+ ...(config.clientId !== undefined ? { clientId: config.clientId } : {}),
270
+ ...(config.limits !== undefined ? { limits: config.limits } : {}),
271
+ onSyncNeeded: (reason) => {
272
+ post({ t: 'event', event: { kind: 'sync-needed', reason } });
273
+ scheduleAutoSync();
274
+ },
275
+ onConflict: (conflict) => {
276
+ post({ t: 'event', event: { kind: 'conflict', conflict } });
277
+ },
278
+ onUpgrading: (upgrading) => {
279
+ // §7.4.5: surface the schema-bump reset + completion to the UI thread.
280
+ post({ t: 'event', event: { kind: 'upgrading', upgrading } });
281
+ },
282
+ onPresence: (scopeKey) => {
283
+ // §8.6: surface a presence change to the UI thread.
284
+ post({ t: 'event', event: { kind: 'presence', scopeKey } });
285
+ },
286
+ });
287
+ // TODO 3.1 / I1: forward every coalesced apply-batch invalidation to the
288
+ // UI thread so `SyncClientHandle.onInvalidate` fires with worker parity.
289
+ started.onInvalidate((event) => {
290
+ if (!closed) post({ t: 'event', event: { kind: 'invalidate', event } });
291
+ });
292
+ await started.start();
293
+ realtimeConnector =
294
+ overrides.createRealtime !== undefined
295
+ ? overrides.createRealtime(config, started.clientId)
296
+ : config.endpoints.realtimeUrl !== undefined
297
+ ? webSocketRealtimeConnector(
298
+ config.endpoints.realtimeUrl.replace(
299
+ '{clientId}',
300
+ encodeURIComponent(started.clientId),
301
+ ),
302
+ )
303
+ : undefined;
304
+ client = started;
305
+ return { clientId: started.clientId };
460
306
  }
461
- }
462
307
 
463
- function syncResultDetails(value: unknown): Record<string, unknown> {
464
- const result = objectRecord(value);
465
- const changedTables = Array.isArray(result.changedTables)
466
- ? result.changedTables
467
- : [];
468
- const changedRows = Array.isArray(result.changedRows)
469
- ? result.changedRows
470
- : [];
471
- return {
472
- changedTables,
473
- changedRows,
474
- changedRowsTruncated: result.changedRowsTruncated === true,
475
- changedTableCount: changedTables.length,
476
- changedRowCount: changedRows.length,
477
- bootstrap: objectRecord(result.bootstrap),
478
- pushedCommits:
479
- typeof result.pushedCommits === 'number' ? result.pushedCommits : 0,
308
+ const api: WorkerApi = {
309
+ subscribe: (input) => requireClient().subscribe(input),
310
+ unsubscribe: (id) => requireClient().unsubscribe(id),
311
+ setWindow: async (base, units) => {
312
+ await requireClient().setWindow(base, units);
313
+ // A window change adds/removes value-sharded subscriptions; a widened
314
+ // unit needs a bootstrap pull to become visible. In autoSync mode the
315
+ // host loop owns rounds, so schedule one now — otherwise the new unit
316
+ // waits for an unrelated server wake-up (§8.4). A no-op change still
317
+ // schedules a harmless idempotent round.
318
+ scheduleAutoSync();
319
+ },
320
+ windowState: (base) => requireClient().windowState(base),
321
+ mutate: (mutations) => {
322
+ const id = requireClient().mutate(mutations);
323
+ // In autoSync mode the host loop owns rounds (§8.4): a local write must
324
+ // push without the app orchestrating sync, so schedule a jittered round
325
+ // to drain the outbox promptly. `mutate` is synchronous — its outbox
326
+ // write has committed before this line — and the round itself serializes
327
+ // on SyncClient's operation chain, so no ordering hack is needed here;
328
+ // the §8.4 jitter in `scheduleAutoSync` is the only deferral, and it
329
+ // coalesces bursts. (Historically this used setTimeout to dodge a
330
+ // nested-transaction flake; that race is fixed at the source now — the
331
+ // real cause was cross-operation interleaving, serialized in the core.)
332
+ scheduleAutoSync();
333
+ return id;
334
+ },
335
+ sync: () => {
336
+ const running = requireClient();
337
+ return serializedSync(() => running.sync());
338
+ },
339
+ syncUntilIdle: (maxRounds) => {
340
+ const running = requireClient();
341
+ return serializedSync(() => running.syncUntilIdle(maxRounds));
342
+ },
343
+ query: (sql, params) => requireClient().query(sql, params),
344
+ conflicts: () => requireClient().conflicts,
345
+ rejections: () => requireClient().rejections,
346
+ schemaFloor: () => requireClient().schemaFloor,
347
+ leaseState: () => requireClient().leaseState,
348
+ upgrading: () => requireClient().upgrading,
349
+ syncNeeded: () => requireClient().syncNeeded,
350
+ pendingCommits: () => requireClient().pendingCommits(),
351
+ subscriptions: () => requireClient().subscriptions(),
352
+ subscription: (id) => requireClient().subscription(id),
353
+ connectRealtime: () => requireClient().connectRealtime(),
354
+ disconnectRealtime: () => requireClient().disconnectRealtime(),
355
+ setPresence: (scopeKey, doc) => requireClient().setPresence(scopeKey, doc),
356
+ presence: (scopeKey) => requireClient().presence(scopeKey),
357
+ uploadBlob: (bytes, options) => requireClient().uploadBlob(bytes, options),
358
+ fetchBlob: (blobIdOrRef) => requireClient().fetchBlob(blobIdOrRef),
359
+ setOffline: (value) => {
360
+ offline = value;
361
+ if (offline) client?.disconnectRealtime();
362
+ },
363
+ close: async () => {
364
+ closed = true;
365
+ await client?.close();
366
+ database?.close();
367
+ client = undefined;
368
+ database = undefined;
369
+ },
480
370
  };
481
- }
482
371
 
483
- function syncScopeRevocationDetails(value: unknown): {
484
- revokedSubscriptionIds: string[];
485
- revokedSubscriptionCount: number;
486
- } | null {
487
- const result = objectRecord(value);
488
- const subscriptions = Array.isArray(result.subscriptions)
489
- ? result.subscriptions
490
- : [];
491
- const revokedSubscriptionIds = subscriptions
492
- .filter((subscription): subscription is { id: string; status: string } => {
493
- return (
494
- subscription !== null &&
495
- typeof subscription === 'object' &&
496
- (subscription as { status?: unknown }).status === 'revoked' &&
497
- typeof (subscription as { id?: unknown }).id === 'string'
498
- );
499
- })
500
- .map((subscription) => subscription.id);
372
+ async function dispatch(message: WorkerCallMessage): Promise<unknown> {
373
+ const method = api[message.method] as (...args: unknown[]) => unknown;
374
+ return await method.apply(api, message.args as unknown[]);
375
+ }
501
376
 
502
- return revokedSubscriptionIds.length > 0
503
- ? {
504
- revokedSubscriptionIds,
505
- revokedSubscriptionCount: revokedSubscriptionIds.length,
377
+ function run(
378
+ id: number,
379
+ fn: () => Promise<unknown>,
380
+ transfer?: (value: unknown) => Transferable[],
381
+ ): void {
382
+ void (async () => {
383
+ try {
384
+ const value = await fn();
385
+ post(
386
+ { t: 'result', id, value },
387
+ transfer !== undefined ? transfer(value) : undefined,
388
+ );
389
+ } catch (error) {
390
+ post({ t: 'error', id, error: toErrorShape(error) });
506
391
  }
507
- : null;
508
- }
509
-
510
- function diagnosticStatus(error: unknown): Record<string, unknown> {
511
- const status = syncularErrorStatus(error);
512
- return status ? { status } : {};
513
- }
514
-
515
- function objectRecord(value: unknown): Record<string, unknown> {
516
- return value && typeof value === 'object'
517
- ? (value as Record<string, unknown>)
518
- : {};
519
- }
392
+ })();
393
+ }
520
394
 
521
- function errorMessage(error: unknown): string {
522
- return syncularErrorMessage(error);
523
- }
395
+ scope.addEventListener('message', (event) => {
396
+ const message = event.data as MainToWorkerMessage;
397
+ if (message.t === 'init') {
398
+ run(message.id, () => init(message.config));
399
+ return;
400
+ }
401
+ if (message.t === 'call') {
402
+ run(
403
+ message.id,
404
+ () => dispatch(message),
405
+ message.method === 'query'
406
+ ? (value) => queryTransferables(value as readonly unknown[])
407
+ : undefined,
408
+ );
409
+ }
410
+ });
524
411
 
525
- function errorRequiresFullRefresh(error: unknown): boolean {
526
- return errorMessage(error).includes('full snapshot resync required');
412
+ post({ t: 'ready' });
527
413
  }
528
414
 
529
- function encodeWorkerError(error: unknown): SyncularWorkerErrorPayload {
530
- if (isWorkerErrorPayload(error) && !(error instanceof Error)) return error;
531
- if (error instanceof SyncularClientError) {
532
- return {
533
- code: error.code,
534
- message: error.message,
535
- category: error.category,
536
- retryable: error.retryable,
537
- recommendedAction: error.recommendedAction,
538
- name: error.name,
539
- stack: error.stack,
540
- details: error.details,
541
- };
542
- }
543
- if (error instanceof Error) {
544
- const details = syncularErrorDetails(error);
545
- const classification = classifySyncularError(error, error.message, details);
546
- if (!classification) {
547
- return createSyncularWorkerErrorPayload('worker.failed', error.message, {
548
- name: error.name,
549
- stack: error.stack,
550
- details,
551
- });
552
- }
553
- return {
554
- code: classification.code,
555
- message: error.message,
556
- category: classification.category,
557
- retryable: classification.retryable,
558
- recommendedAction: classification.recommendedAction,
559
- name: error.name,
560
- stack: error.stack,
561
- details,
562
- };
563
- }
564
- const message = String(error);
565
- const classification = classifySyncularError(error, message);
566
- if (!classification) {
567
- return createSyncularWorkerErrorPayload('worker.failed', message);
415
+ async function defaultOpenDatabase(
416
+ config: WorkerInitConfig,
417
+ ): Promise<ClientDatabase> {
418
+ if (config.database.mode !== 'persistent') {
419
+ throw new ClientSyncError(
420
+ WORKER_FAILED_CODE,
421
+ "database mode 'custom' requires an openDatabase override in the " +
422
+ 'worker bootstrap (startSyncWorker({ openDatabase }))',
423
+ );
568
424
  }
569
- return {
570
- code: classification.code,
571
- message,
572
- category: classification.category,
573
- retryable: classification.retryable,
574
- recommendedAction: classification.recommendedAction,
575
- };
576
- }
577
-
578
- function isWorkerErrorPayload(
579
- value: unknown
580
- ): value is SyncularWorkerErrorPayload {
581
- return Boolean(
582
- value && typeof value === 'object' && 'code' in value && 'message' in value
583
- );
425
+ return openPersistentWasmDatabase(config.database.name, {
426
+ ...(config.database.directory !== undefined
427
+ ? { directory: config.database.directory }
428
+ : {}),
429
+ });
584
430
  }