@syncular/client 0.1.3 → 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,479 +1,323 @@
1
- import { resolveSyncularClientConfig } from './client-config.js';
2
- import { syncularDiagnosticAttemptFields } from './diagnostics.js';
3
- import { classifySyncularError, SyncularClientError, syncularErrorDetails, syncularErrorMessage, syncularErrorStatus, } from './errors.js';
4
- import { dispatchGeneratedSyncularWorkerRequest, generatedSyncularWorkerRequestDiagnosticSource, isGeneratedSyncularAbortableWorkerRequestType, isGeneratedSyncularDiagnosedSuccessWorkerRequestType, } from './generated-bridge.js';
5
- import { createSyncularRuntimeInfo } from './runtime-contract.js';
6
- import { openSyncularRustClient } from './rust-client.js';
7
- import { getSyncularRustRuntimeInfo, getSyncularWasmGlueUrl, getSyncularWasmUrl, } from './wasm-runtime.js';
8
- import { createSyncularWorkerErrorPayload, SYNCULAR_WORKER_PROTOCOL_VERSION, } from './worker-protocol.js';
9
- import { SyncularWorkerRealtimeController } from './worker-realtime.js';
10
- let client;
11
- let openedConfig;
12
- let openedRuntime;
13
- let removeRowsChangedListener;
14
- const canceledRequests = new Set();
15
- const abortControllers = new Map();
16
- const realtime = new SyncularWorkerRealtimeController({
17
- getClient: requireClient,
18
- getConfig: () => openedConfig,
19
- getLocationOrigin: () => typeof self.location?.origin === 'string'
20
- ? self.location.origin
21
- : 'http://localhost',
22
- createWebSocket: (url) => new WebSocket(url),
23
- postEvent: (event) => self.postMessage(event),
24
- postDiagnostic,
25
- });
26
- self.onmessage = (event) => {
27
- void handleRequest(event.data);
28
- };
29
- async function handleRequest(request) {
30
- if (request.protocolVersion !== SYNCULAR_WORKER_PROTOCOL_VERSION) {
31
- post({
32
- id: request.id,
33
- protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
34
- ok: false,
35
- error: createSyncularWorkerErrorPayload('worker.protocol_mismatch', `Unsupported Syncular worker protocol ${request.protocolVersion}`, { details: { supported: SYNCULAR_WORKER_PROTOCOL_VERSION } }),
36
- });
37
- return;
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.js';
17
+ import { ClientSyncError } from './errors.js';
18
+ import { httpBlobTransport, httpSegmentDownloader, httpSyncTransport, webSocketRealtimeConnector, } from './http.js';
19
+ import { openPersistentWasmDatabase } from './wasm-database.js';
20
+ import { WORKER_FAILED_CODE, } from './worker-protocol.js';
21
+ function toErrorShape(error) {
22
+ if (error instanceof ClientSyncError) {
23
+ return {
24
+ code: error.code,
25
+ message: error.message,
26
+ retryable: error.retryable,
27
+ };
38
28
  }
39
- if (request.type === 'cancel') {
40
- canceledRequests.add(request.requestId);
41
- const controller = abortControllers.get(request.requestId);
42
- controller?.abort();
43
- if (controller) {
44
- postDiagnostic({
45
- level: 'warn',
46
- source: 'worker',
47
- code: 'worker.request_aborted',
48
- message: 'Syncular worker request aborted',
49
- details: { requestId: request.requestId },
50
- });
29
+ return {
30
+ code: WORKER_FAILED_CODE,
31
+ message: error instanceof Error ? error.message : String(error),
32
+ retryable: false,
33
+ };
34
+ }
35
+ /**
36
+ * ArrayBuffers safe to transfer out of a query result: whole-buffer
37
+ * views only (a view into a larger/pooled buffer is cloned instead —
38
+ * transferring would detach unrelated data), deduplicated because a
39
+ * buffer may only appear once in a transfer list.
40
+ */
41
+ function queryTransferables(rows) {
42
+ const buffers = new Set();
43
+ for (const row of rows) {
44
+ if (row === null || typeof row !== 'object')
45
+ continue;
46
+ for (const value of Object.values(row)) {
47
+ if (value instanceof Uint8Array &&
48
+ value.buffer instanceof ArrayBuffer &&
49
+ value.byteOffset === 0 &&
50
+ value.byteLength === value.buffer.byteLength) {
51
+ buffers.add(value.buffer);
52
+ }
51
53
  }
52
- post({
53
- id: request.id,
54
- protocolVersion: request.protocolVersion,
55
- ok: true,
56
- });
57
- return;
58
54
  }
59
- const startedAt = Date.now();
60
- const abortController = createAbortController(request.type);
61
- if (abortController) {
62
- abortControllers.set(request.id, abortController);
55
+ return [...buffers];
56
+ }
57
+ /**
58
+ * Register the RPC server on the current worker scope. Call this once
59
+ * from a worker entry script; the main-thread handle drives everything
60
+ * else through `init`/`call` messages.
61
+ */
62
+ export function startSyncWorker(overrides = {}) {
63
+ const scope = globalThis;
64
+ const post = (message, transfer) => {
65
+ if (transfer !== undefined && transfer.length > 0) {
66
+ scope.postMessage(message, transfer);
67
+ }
68
+ else {
69
+ scope.postMessage(message);
70
+ }
71
+ };
72
+ let client;
73
+ let database;
74
+ let offline = false;
75
+ let closed = false;
76
+ // -- one sync loop: RPC-driven and auto-driven rounds serialize ----------
77
+ let syncChain = Promise.resolve();
78
+ function serializedSync(fn) {
79
+ const next = syncChain.then(fn, fn);
80
+ syncChain = next.then(() => undefined, () => undefined);
81
+ return next;
63
82
  }
64
- try {
65
- if (abortController)
66
- client?.setAbortSignal(abortController.signal);
67
- const value = await dispatch(request);
68
- if (canceledRequests.delete(request.id))
83
+ // -- §8.4 host loop: coalesced, jittered, inside the worker --------------
84
+ let autoSync = true;
85
+ let wakeJitterMs = 200;
86
+ let autoSyncScheduled = false;
87
+ function scheduleAutoSync() {
88
+ if (!autoSync || autoSyncScheduled || closed || client === undefined) {
69
89
  return;
70
- post({
71
- id: request.id,
72
- protocolVersion: request.protocolVersion,
73
- ok: true,
74
- value,
90
+ }
91
+ autoSyncScheduled = true;
92
+ setTimeout(() => {
93
+ autoSyncScheduled = false;
94
+ if (closed || client === undefined)
95
+ return;
96
+ const running = client;
97
+ void serializedSync(() => running.syncUntilIdle())
98
+ .then((summary) => {
99
+ if (!closed)
100
+ post({ t: 'event', event: { kind: 'synced', summary } });
101
+ })
102
+ .catch((error) => {
103
+ if (!closed) {
104
+ post({
105
+ t: 'event',
106
+ event: { kind: 'synced', error: toErrorShape(error) },
107
+ });
108
+ }
109
+ });
110
+ }, Math.floor(Math.random() * wakeJitterMs));
111
+ }
112
+ function requireClient() {
113
+ if (client === undefined) {
114
+ throw new ClientSyncError(WORKER_FAILED_CODE, 'the worker received a call before init completed');
115
+ }
116
+ return client;
117
+ }
118
+ function assertOnline() {
119
+ if (offline) {
120
+ throw new ClientSyncError('sync.transport_failed', 'the worker transport is offline (setOffline(true))', true);
121
+ }
122
+ }
123
+ function gateOffline(inner) {
124
+ return (request) => {
125
+ assertOnline();
126
+ return inner(request);
127
+ };
128
+ }
129
+ /** Offline gate that preserves the §5.4 `fetchUrl` capability marker. */
130
+ function gateSegmentsOffline(inner) {
131
+ const fetchUrl = inner.fetchUrl;
132
+ return Object.assign(gateOffline(inner), {
133
+ ...(fetchUrl !== undefined
134
+ ? {
135
+ fetchUrl: (url) => {
136
+ assertOnline();
137
+ return fetchUrl(url);
138
+ },
139
+ }
140
+ : {}),
75
141
  });
76
- postRequestSuccessDiagnostic(request, value, Date.now() - startedAt);
77
142
  }
78
- catch (err) {
79
- if (canceledRequests.delete(request.id))
80
- return;
81
- const source = requestDiagnosticSource(request.type);
82
- const resyncRequired = errorRequiresFullRefresh(err);
83
- const encodedError = encodeWorkerError(err);
84
- const diagnosticCode = resyncRequired
85
- ? `${source}.resync_required`
86
- : encodedError.code.startsWith(`${source}.`)
87
- ? encodedError.code
88
- : `${source}.${request.type}.failed`;
89
- postDiagnostic({
90
- level: requestDiagnosticLevel(request.type, err),
91
- source,
92
- code: diagnosticCode,
93
- message: resyncRequired
94
- ? `Syncular worker request ${request.type} requires full resync`
95
- : `Syncular worker request ${request.type} failed`,
96
- details: {
97
- requestType: request.type,
98
- durationMs: Date.now() - startedAt,
99
- error: errorMessage(err),
100
- errorCode: encodedError.code,
101
- ...(encodedError.category ? { category: encodedError.category } : {}),
102
- ...(encodedError.retryable != null
103
- ? { retryable: encodedError.retryable }
104
- : {}),
105
- ...(encodedError.recommendedAction
106
- ? { recommendedAction: encodedError.recommendedAction }
107
- : {}),
108
- ...(resyncRequired ? { resyncRequired: true } : {}),
109
- ...diagnosticStatus(err),
143
+ async function init(config) {
144
+ if (client !== undefined) {
145
+ throw new ClientSyncError(WORKER_FAILED_CODE, 'the worker is already initialized');
146
+ }
147
+ autoSync = config.autoSync ?? true;
148
+ wakeJitterMs = config.wakeJitterMs ?? 200;
149
+ database =
150
+ overrides.openDatabase !== undefined
151
+ ? await overrides.openDatabase(config)
152
+ : await defaultOpenDatabase(config);
153
+ const transport = gateOffline(overrides.createTransport !== undefined
154
+ ? overrides.createTransport(config)
155
+ : httpSyncTransport(config.endpoints.syncUrl));
156
+ const segments = overrides.createSegments !== undefined
157
+ ? overrides.createSegments(config)
158
+ : config.endpoints.segmentsUrl !== undefined
159
+ ? httpSegmentDownloader(config.endpoints.segmentsUrl)
160
+ : undefined;
161
+ // §5.9 blob transport: only when a blobs URL is configured.
162
+ const blobs = config.endpoints.blobsUrl !== undefined
163
+ ? httpBlobTransport(config.endpoints.blobsUrl)
164
+ : undefined;
165
+ // Realtime needs the (possibly persisted) clientId for the
166
+ // `{clientId}` URL placeholder start the client first, then attach
167
+ // the connector through a mutable slot read at connect time.
168
+ let realtimeConnector;
169
+ const started = new SyncClient({
170
+ database,
171
+ schema: config.schema,
172
+ transport,
173
+ ...(segments !== undefined
174
+ ? { segments: gateSegmentsOffline(segments) }
175
+ : {}),
176
+ ...(blobs !== undefined ? { blobs } : {}),
177
+ realtime: (handlers) => {
178
+ if (offline) {
179
+ throw new ClientSyncError('sync.transport_failed', 'the worker realtime channel is offline (setOffline(true))', true);
180
+ }
181
+ if (realtimeConnector === undefined) {
182
+ throw new ClientSyncError('sync.invalid_request', 'no realtime connector configured');
183
+ }
184
+ return realtimeConnector(handlers);
185
+ },
186
+ ...(config.clientId !== undefined ? { clientId: config.clientId } : {}),
187
+ ...(config.limits !== undefined ? { limits: config.limits } : {}),
188
+ onSyncNeeded: (reason) => {
189
+ post({ t: 'event', event: { kind: 'sync-needed', reason } });
190
+ scheduleAutoSync();
191
+ },
192
+ onConflict: (conflict) => {
193
+ post({ t: 'event', event: { kind: 'conflict', conflict } });
194
+ },
195
+ onUpgrading: (upgrading) => {
196
+ // §7.4.5: surface the schema-bump reset + completion to the UI thread.
197
+ post({ t: 'event', event: { kind: 'upgrading', upgrading } });
198
+ },
199
+ onPresence: (scopeKey) => {
200
+ // §8.6: surface a presence change to the UI thread.
201
+ post({ t: 'event', event: { kind: 'presence', scopeKey } });
110
202
  },
111
- ...requestSyncAttemptDiagnosticFields(request),
112
203
  });
113
- post({
114
- id: request.id,
115
- protocolVersion: request.protocolVersion,
116
- ok: false,
117
- error: encodedError,
204
+ // TODO 3.1 / I1: forward every coalesced apply-batch invalidation to the
205
+ // UI thread so `SyncClientHandle.onInvalidate` fires with worker parity.
206
+ started.onInvalidate((event) => {
207
+ if (!closed)
208
+ post({ t: 'event', event: { kind: 'invalidate', event } });
118
209
  });
210
+ await started.start();
211
+ realtimeConnector =
212
+ overrides.createRealtime !== undefined
213
+ ? overrides.createRealtime(config, started.clientId)
214
+ : config.endpoints.realtimeUrl !== undefined
215
+ ? webSocketRealtimeConnector(config.endpoints.realtimeUrl.replace('{clientId}', encodeURIComponent(started.clientId)))
216
+ : undefined;
217
+ client = started;
218
+ return { clientId: started.clientId };
119
219
  }
120
- finally {
121
- abortControllers.delete(request.id);
122
- if (abortController)
123
- clearClientAbortSignal();
124
- }
125
- }
126
- async function dispatch(request) {
127
- return dispatchGeneratedSyncularWorkerRequest({
128
- requireClient,
129
- openClient,
130
- startRealtime: (options) => {
131
- realtime.start(options);
132
- return true;
220
+ const api = {
221
+ subscribe: (input) => requireClient().subscribe(input),
222
+ unsubscribe: (id) => requireClient().unsubscribe(id),
223
+ setWindow: async (base, units) => {
224
+ await requireClient().setWindow(base, units);
225
+ // A window change adds/removes value-sharded subscriptions; a widened
226
+ // unit needs a bootstrap pull to become visible. In autoSync mode the
227
+ // host loop owns rounds, so schedule one now — otherwise the new unit
228
+ // waits for an unrelated server wake-up (§8.4). A no-op change still
229
+ // schedules a harmless idempotent round.
230
+ scheduleAutoSync();
133
231
  },
134
- stopRealtime: () => {
135
- realtime.stop();
136
- return true;
232
+ windowState: (base) => requireClient().windowState(base),
233
+ mutate: (mutations) => {
234
+ const id = requireClient().mutate(mutations);
235
+ // In autoSync mode the host loop owns rounds (§8.4): a local write must
236
+ // push without the app orchestrating sync, so schedule a jittered round
237
+ // to drain the outbox promptly. `mutate` is synchronous — its outbox
238
+ // write has committed before this line — and the round itself serializes
239
+ // on SyncClient's operation chain, so no ordering hack is needed here;
240
+ // the §8.4 jitter in `scheduleAutoSync` is the only deferral, and it
241
+ // coalesces bursts. (Historically this used setTimeout to dodge a
242
+ // nested-transaction flake; that race is fixed at the source now — the
243
+ // real cause was cross-operation interleaving, serialized in the core.)
244
+ scheduleAutoSync();
245
+ return id;
137
246
  },
138
- sendPresence: (action, scopeKey, metadata) => {
139
- realtime.sendPresence(action, scopeKey, metadata);
140
- return true;
247
+ sync: () => {
248
+ const running = requireClient();
249
+ return serializedSync(() => running.sync());
141
250
  },
142
- runtimeInfo,
143
- closeClient,
144
- }, request);
145
- }
146
- async function openClient(request) {
147
- const config = resolveSyncularClientConfig(request.config);
148
- realtime.stop();
149
- detachRowsChangedListener();
150
- client?.close();
151
- client = undefined;
152
- openedConfig = undefined;
153
- openedRuntime = undefined;
154
- const runtime = request.runtime;
155
- client = await openSyncularRustClient({
156
- config,
157
- module: runtime?.wasmGlueUrl
158
- ? loadWorkerWasmGlue(runtime.wasmGlueUrl)
159
- : undefined,
160
- wasmGlueUrl: runtime?.wasmGlueUrl,
161
- wasmUrl: runtime?.wasmUrl,
162
- });
163
- attachRowsChangedListener(client);
164
- openedConfig = config;
165
- openedRuntime = runtime;
166
- return true;
167
- }
168
- function closeClient() {
169
- realtime.stop();
170
- detachRowsChangedListener();
171
- client?.close();
172
- client = undefined;
173
- openedConfig = undefined;
174
- openedRuntime = undefined;
175
- return true;
176
- }
177
- async function runtimeInfo() {
178
- const wasmUrl = getSyncularWasmUrl();
179
- const selectedWasmUrl = openedRuntime?.wasmUrl ?? wasmUrl;
180
- const selectedWasmGlueUrl = openedRuntime?.wasmGlueUrl ?? getSyncularWasmGlueUrl();
181
- return createSyncularRuntimeInfo({
182
- storage: openedConfig?.storage,
183
- workerUrl: typeof self.location?.href === 'string' ? self.location.href : '',
184
- wasmGlueUrl: selectedWasmGlueUrl,
185
- wasmUrl: selectedWasmUrl,
186
- rust: await getSyncularRustRuntimeInfo(openedRuntime?.wasmGlueUrl
187
- ? loadWorkerWasmGlue(openedRuntime.wasmGlueUrl)
188
- : undefined, selectedWasmUrl),
189
- });
190
- }
191
- function loadWorkerWasmGlue(wasmGlueUrl) {
192
- return import(/* @vite-ignore */ wasmGlueUrl);
193
- }
194
- function requireClient() {
195
- if (!client) {
196
- throw createSyncularWorkerErrorPayload('worker.not_open', 'Syncular worker client is not open');
197
- }
198
- return client;
199
- }
200
- function attachRowsChangedListener(nextClient) {
201
- removeRowsChangedListener = nextClient.addRowsChangedListener((event) => {
202
- post({
203
- protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
204
- type: 'rowsChanged',
205
- source: event.source,
206
- changedTables: event.changedTables,
207
- changedRows: event.changedRows,
208
- changedRowsTruncated: event.changedRowsTruncated,
209
- });
210
- });
211
- }
212
- function detachRowsChangedListener() {
213
- removeRowsChangedListener?.();
214
- removeRowsChangedListener = undefined;
215
- }
216
- function post(message) {
217
- self.postMessage(message);
218
- }
219
- function createAbortController(type) {
220
- if (!isAbortableRequest(type) || typeof AbortController === 'undefined') {
221
- return undefined;
222
- }
223
- return new AbortController();
224
- }
225
- function isAbortableRequest(type) {
226
- return isGeneratedSyncularAbortableWorkerRequestType(type);
227
- }
228
- function clearClientAbortSignal() {
229
- try {
230
- client?.setAbortSignal(null);
231
- }
232
- catch {
233
- // Best effort after a request completed, failed, or closed the client.
234
- }
235
- }
236
- function postDiagnostic(event) {
237
- self.postMessage({
238
- protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
239
- type: 'diagnostic',
240
- event: {
241
- at: event.at ?? Date.now(),
242
- level: event.level,
243
- source: event.source,
244
- code: event.code,
245
- message: event.message,
246
- ...(event.syncAttemptId ? { syncAttemptId: event.syncAttemptId } : {}),
247
- ...(event.traceId ? { traceId: event.traceId } : {}),
248
- ...(event.spanId ? { spanId: event.spanId } : {}),
249
- ...(event.clientId ? { clientId: event.clientId } : {}),
250
- ...(event.subscriptionId ? { subscriptionId: event.subscriptionId } : {}),
251
- ...(event.table ? { table: event.table } : {}),
252
- ...(event.rowId ? { rowId: event.rowId } : {}),
253
- ...(event.cursor !== undefined ? { cursor: event.cursor } : {}),
254
- ...(event.details ? { details: event.details } : {}),
251
+ syncUntilIdle: (maxRounds) => {
252
+ const running = requireClient();
253
+ return serializedSync(() => running.syncUntilIdle(maxRounds));
255
254
  },
256
- });
257
- }
258
- function postRequestSuccessDiagnostic(request, value, durationMs) {
259
- if (!isDiagnosedSuccessRequest(request.type))
260
- return;
261
- const syncRevocation = syncScopeRevocationDetails(value);
262
- postDiagnostic({
263
- level: 'info',
264
- source: requestDiagnosticSource(request.type),
265
- code: `${requestDiagnosticSource(request.type)}.${request.type}.completed`,
266
- message: `Syncular worker request ${request.type} completed`,
267
- ...requestSyncAttemptDiagnosticFields(request),
268
- details: {
269
- requestType: request.type,
270
- durationMs,
271
- ...requestSuccessDetails(request, value),
255
+ query: (sql, params) => requireClient().query(sql, params),
256
+ conflicts: () => requireClient().conflicts,
257
+ rejections: () => requireClient().rejections,
258
+ schemaFloor: () => requireClient().schemaFloor,
259
+ leaseState: () => requireClient().leaseState,
260
+ upgrading: () => requireClient().upgrading,
261
+ syncNeeded: () => requireClient().syncNeeded,
262
+ pendingCommits: () => requireClient().pendingCommits(),
263
+ subscriptions: () => requireClient().subscriptions(),
264
+ subscription: (id) => requireClient().subscription(id),
265
+ connectRealtime: () => requireClient().connectRealtime(),
266
+ disconnectRealtime: () => requireClient().disconnectRealtime(),
267
+ setPresence: (scopeKey, doc) => requireClient().setPresence(scopeKey, doc),
268
+ presence: (scopeKey) => requireClient().presence(scopeKey),
269
+ uploadBlob: (bytes, options) => requireClient().uploadBlob(bytes, options),
270
+ fetchBlob: (blobIdOrRef) => requireClient().fetchBlob(blobIdOrRef),
271
+ setOffline: (value) => {
272
+ offline = value;
273
+ if (offline)
274
+ client?.disconnectRealtime();
272
275
  },
273
- });
274
- if (syncRevocation &&
275
- (request.type === 'syncPull' ||
276
- request.type === 'syncPush' ||
277
- request.type === 'syncOnce')) {
278
- postDiagnostic({
279
- level: 'warn',
280
- source: 'sync',
281
- code: 'sync.scope_revoked',
282
- message: 'Syncular subscription scope revoked',
283
- ...requestSyncAttemptDiagnosticFields(request),
284
- details: {
285
- requestType: request.type,
286
- ...syncRevocation,
287
- },
288
- });
276
+ close: async () => {
277
+ closed = true;
278
+ await client?.close();
279
+ database?.close();
280
+ client = undefined;
281
+ database = undefined;
282
+ },
283
+ };
284
+ async function dispatch(message) {
285
+ const method = api[message.method];
286
+ return await method.apply(api, message.args);
289
287
  }
290
- }
291
- function requestSyncAttemptDiagnosticFields(request) {
292
- if (request.type !== 'syncPull' &&
293
- request.type !== 'syncPush' &&
294
- request.type !== 'syncOnce') {
295
- return {};
288
+ function run(id, fn, transfer) {
289
+ void (async () => {
290
+ try {
291
+ const value = await fn();
292
+ post({ t: 'result', id, value }, transfer !== undefined ? transfer(value) : undefined);
293
+ }
294
+ catch (error) {
295
+ post({ t: 'error', id, error: toErrorShape(error) });
296
+ }
297
+ })();
296
298
  }
297
- return syncularDiagnosticAttemptFields(request.syncAttempt);
298
- }
299
- function isDiagnosedSuccessRequest(type) {
300
- return isGeneratedSyncularDiagnosedSuccessWorkerRequestType(type);
301
- }
302
- function requestDiagnosticSource(type) {
303
- return generatedSyncularWorkerRequestDiagnosticSource(type);
304
- }
305
- function requestDiagnosticLevel(type, error) {
306
- if (type === 'setAuthHeaders' || syncularErrorStatus(error))
307
- return 'warn';
308
- return 'error';
309
- }
310
- function requestSuccessDetails(request, value) {
311
- switch (request.type) {
312
- case 'open':
313
- return {
314
- storage: openedConfig?.storage,
315
- clientId: openedConfig?.clientId,
316
- };
317
- case 'setAuthHeaders':
318
- return { headerCount: Object.keys(request.headers).length };
319
- case 'setFieldEncryption':
320
- return {
321
- enabled: request.config != null,
322
- ruleCount: request.config?.rules.length ?? 0,
323
- };
324
- case 'setEncryptedCrdt':
325
- return {
326
- enabled: request.config != null,
327
- keyCount: request.config ? Object.keys(request.config.keys).length : 0,
328
- };
329
- case 'setBlobEncryption':
330
- return {
331
- enabled: request.config != null,
332
- keyCount: request.config ? Object.keys(request.config.keys).length : 0,
333
- };
334
- case 'syncPull':
335
- case 'syncPush':
336
- case 'syncOnce':
337
- return syncResultDetails(value);
338
- case 'storeBlob':
339
- return { immediate: request.options?.immediate === true };
340
- case 'pruneBlobCache':
341
- return { maxBytes: request.maxBytes ?? null, prunedBytes: value };
342
- case 'compactStorage':
343
- return objectRecord(value);
344
- case 'localHealthCheck': {
345
- const report = objectRecord(value);
346
- const findings = Array.isArray(report.findings) ? report.findings : [];
347
- return {
348
- ok: report.ok === true,
349
- findingCount: findings.length,
350
- };
351
- }
352
- case 'exportLocalSupportBundle': {
353
- const bundle = objectRecord(value);
354
- const health = objectRecord(bundle.health);
355
- const findings = Array.isArray(health.findings) ? health.findings : [];
356
- return {
357
- redacted: bundle.redacted === true,
358
- source: bundle.source,
359
- findingCount: findings.length,
360
- };
299
+ scope.addEventListener('message', (event) => {
300
+ const message = event.data;
301
+ if (message.t === 'init') {
302
+ run(message.id, () => init(message.config));
303
+ return;
361
304
  }
362
- case 'importLocalSupportBundle':
363
- return objectRecord(value);
364
- case 'repairLocalHealth':
365
- return objectRecord(value);
366
- case 'resetLocalSyncState':
367
- return objectRecord(value);
368
- case 'processBlobUploadQueue':
369
- return objectRecord(value);
370
- default:
371
- return {};
372
- }
373
- }
374
- function syncResultDetails(value) {
375
- const result = objectRecord(value);
376
- const changedTables = Array.isArray(result.changedTables)
377
- ? result.changedTables
378
- : [];
379
- const changedRows = Array.isArray(result.changedRows)
380
- ? result.changedRows
381
- : [];
382
- return {
383
- changedTables,
384
- changedRows,
385
- changedRowsTruncated: result.changedRowsTruncated === true,
386
- changedTableCount: changedTables.length,
387
- changedRowCount: changedRows.length,
388
- bootstrap: objectRecord(result.bootstrap),
389
- pushedCommits: typeof result.pushedCommits === 'number' ? result.pushedCommits : 0,
390
- };
391
- }
392
- function syncScopeRevocationDetails(value) {
393
- const result = objectRecord(value);
394
- const subscriptions = Array.isArray(result.subscriptions)
395
- ? result.subscriptions
396
- : [];
397
- const revokedSubscriptionIds = subscriptions
398
- .filter((subscription) => {
399
- return (subscription !== null &&
400
- typeof subscription === 'object' &&
401
- subscription.status === 'revoked' &&
402
- typeof subscription.id === 'string');
403
- })
404
- .map((subscription) => subscription.id);
405
- return revokedSubscriptionIds.length > 0
406
- ? {
407
- revokedSubscriptionIds,
408
- revokedSubscriptionCount: revokedSubscriptionIds.length,
305
+ if (message.t === 'call') {
306
+ run(message.id, () => dispatch(message), message.method === 'query'
307
+ ? (value) => queryTransferables(value)
308
+ : undefined);
409
309
  }
410
- : null;
411
- }
412
- function diagnosticStatus(error) {
413
- const status = syncularErrorStatus(error);
414
- return status ? { status } : {};
415
- }
416
- function objectRecord(value) {
417
- return value && typeof value === 'object'
418
- ? value
419
- : {};
420
- }
421
- function errorMessage(error) {
422
- return syncularErrorMessage(error);
423
- }
424
- function errorRequiresFullRefresh(error) {
425
- return errorMessage(error).includes('full snapshot resync required');
310
+ });
311
+ post({ t: 'ready' });
426
312
  }
427
- function encodeWorkerError(error) {
428
- if (isWorkerErrorPayload(error) && !(error instanceof Error))
429
- return error;
430
- if (error instanceof SyncularClientError) {
431
- return {
432
- code: error.code,
433
- message: error.message,
434
- category: error.category,
435
- retryable: error.retryable,
436
- recommendedAction: error.recommendedAction,
437
- name: error.name,
438
- stack: error.stack,
439
- details: error.details,
440
- };
313
+ async function defaultOpenDatabase(config) {
314
+ if (config.database.mode !== 'persistent') {
315
+ throw new ClientSyncError(WORKER_FAILED_CODE, "database mode 'custom' requires an openDatabase override in the " +
316
+ 'worker bootstrap (startSyncWorker({ openDatabase }))');
441
317
  }
442
- if (error instanceof Error) {
443
- const details = syncularErrorDetails(error);
444
- const classification = classifySyncularError(error, error.message, details);
445
- if (!classification) {
446
- return createSyncularWorkerErrorPayload('worker.failed', error.message, {
447
- name: error.name,
448
- stack: error.stack,
449
- details,
450
- });
451
- }
452
- return {
453
- code: classification.code,
454
- message: error.message,
455
- category: classification.category,
456
- retryable: classification.retryable,
457
- recommendedAction: classification.recommendedAction,
458
- name: error.name,
459
- stack: error.stack,
460
- details,
461
- };
462
- }
463
- const message = String(error);
464
- const classification = classifySyncularError(error, message);
465
- if (!classification) {
466
- return createSyncularWorkerErrorPayload('worker.failed', message);
467
- }
468
- return {
469
- code: classification.code,
470
- message,
471
- category: classification.category,
472
- retryable: classification.retryable,
473
- recommendedAction: classification.recommendedAction,
474
- };
475
- }
476
- function isWorkerErrorPayload(value) {
477
- return Boolean(value && typeof value === 'object' && 'code' in value && 'message' in value);
318
+ return openPersistentWasmDatabase(config.database.name, {
319
+ ...(config.database.directory !== undefined
320
+ ? { directory: config.database.directory }
321
+ : {}),
322
+ });
478
323
  }
479
- //# sourceMappingURL=worker-entry.js.map