@syncular/client 0.0.6-95 → 0.1.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 (356) hide show
  1. package/README.md +713 -11
  2. package/dist/auth-leases.d.ts +11 -0
  3. package/dist/auth-leases.d.ts.map +1 -0
  4. package/dist/auth-leases.js +140 -0
  5. package/dist/auth-leases.js.map +1 -0
  6. package/dist/blob-limits.d.ts +15 -0
  7. package/dist/blob-limits.d.ts.map +1 -0
  8. package/dist/blob-limits.js +66 -0
  9. package/dist/blob-limits.js.map +1 -0
  10. package/dist/bridge-client.d.ts +69 -0
  11. package/dist/bridge-client.d.ts.map +1 -0
  12. package/dist/bridge-client.js +282 -0
  13. package/dist/bridge-client.js.map +1 -0
  14. package/dist/client-config.d.ts +6 -0
  15. package/dist/client-config.d.ts.map +1 -0
  16. package/dist/client-config.js +18 -0
  17. package/dist/client-config.js.map +1 -0
  18. package/dist/client.d.ts +65 -397
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +150 -960
  21. package/dist/client.js.map +1 -1
  22. package/dist/command-history.d.ts +35 -0
  23. package/dist/command-history.d.ts.map +1 -0
  24. package/dist/command-history.js +378 -0
  25. package/dist/command-history.js.map +1 -0
  26. package/dist/console-diagnostics.d.ts +33 -0
  27. package/dist/console-diagnostics.d.ts.map +1 -0
  28. package/dist/console-diagnostics.js +496 -0
  29. package/dist/console-diagnostics.js.map +1 -0
  30. package/dist/crdt-yjs/index.d.ts +4 -0
  31. package/dist/crdt-yjs/index.d.ts.map +1 -0
  32. package/dist/crdt-yjs/index.js +4 -0
  33. package/dist/crdt-yjs/index.js.map +1 -0
  34. package/dist/crdt-yjs/webview-host-facade.d.ts +126 -0
  35. package/dist/crdt-yjs/webview-host-facade.d.ts.map +1 -0
  36. package/dist/crdt-yjs/webview-host-facade.js +284 -0
  37. package/dist/crdt-yjs/webview-host-facade.js.map +1 -0
  38. package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +153 -0
  39. package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +1 -0
  40. package/dist/crdt-yjs/yjs-document-field-adapter.js +406 -0
  41. package/dist/crdt-yjs/yjs-document-field-adapter.js.map +1 -0
  42. package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +73 -0
  43. package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +1 -0
  44. package/dist/crdt-yjs/yjs-prosemirror-bridge.js +169 -0
  45. package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +1 -0
  46. package/dist/database.d.ts +83 -0
  47. package/dist/database.d.ts.map +1 -0
  48. package/dist/database.js +916 -0
  49. package/dist/database.js.map +1 -0
  50. package/dist/diagnostics.d.ts +10 -0
  51. package/dist/diagnostics.d.ts.map +1 -0
  52. package/dist/diagnostics.js +83 -0
  53. package/dist/diagnostics.js.map +1 -0
  54. package/dist/errors.d.ts +24 -0
  55. package/dist/errors.d.ts.map +1 -0
  56. package/dist/errors.js +211 -0
  57. package/dist/errors.js.map +1 -0
  58. package/dist/generated-bridge.d.ts +365 -0
  59. package/dist/generated-bridge.d.ts.map +1 -0
  60. package/dist/generated-bridge.js +250 -0
  61. package/dist/generated-bridge.js.map +1 -0
  62. package/dist/index.d.ts +13 -23
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +14 -25
  65. package/dist/index.js.map +1 -1
  66. package/dist/mutations.d.ts +4 -72
  67. package/dist/mutations.d.ts.map +1 -1
  68. package/dist/mutations.js +2 -568
  69. package/dist/mutations.js.map +1 -1
  70. package/dist/network.d.ts +3 -0
  71. package/dist/network.d.ts.map +1 -0
  72. package/dist/network.js +17 -0
  73. package/dist/network.js.map +1 -0
  74. package/dist/react/index.d.ts +169 -0
  75. package/dist/react/index.d.ts.map +1 -0
  76. package/dist/react/index.js +628 -0
  77. package/dist/react/index.js.map +1 -0
  78. package/dist/react-native/index.d.ts +35 -0
  79. package/dist/react-native/index.d.ts.map +1 -0
  80. package/dist/react-native/index.js +49 -0
  81. package/dist/react-native/index.js.map +1 -0
  82. package/dist/runtime-contract.d.ts +13 -0
  83. package/dist/runtime-contract.d.ts.map +1 -0
  84. package/dist/runtime-contract.js +24 -0
  85. package/dist/runtime-contract.js.map +1 -0
  86. package/dist/rust-client.d.ts +106 -0
  87. package/dist/rust-client.d.ts.map +1 -0
  88. package/dist/rust-client.js +936 -0
  89. package/dist/rust-client.js.map +1 -0
  90. package/dist/sentry.d.ts +35 -0
  91. package/dist/sentry.d.ts.map +1 -0
  92. package/dist/sentry.js +155 -0
  93. package/dist/sentry.js.map +1 -0
  94. package/dist/sql-safety.d.ts +3 -0
  95. package/dist/sql-safety.d.ts.map +1 -0
  96. package/dist/sql-safety.js +62 -0
  97. package/dist/sql-safety.js.map +1 -0
  98. package/dist/syncular-runtime-artifacts.json +61 -0
  99. package/dist/tauri/index.d.ts +35 -0
  100. package/dist/tauri/index.d.ts.map +1 -0
  101. package/dist/tauri/index.js +114 -0
  102. package/dist/tauri/index.js.map +1 -0
  103. package/dist/types.d.ts +1059 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +2 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/wasm/.syncular-wasm-profile +1 -0
  108. package/dist/wasm/syncular-runtime-artifact.json +21 -0
  109. package/dist/wasm/syncular.d.ts +207 -0
  110. package/dist/wasm/syncular.js +2341 -0
  111. package/dist/wasm/syncular_bg.wasm +0 -0
  112. package/dist/wasm/syncular_bg.wasm.d.ts +97 -0
  113. package/dist/wasm-bindings/runtime-contract.d.ts +22 -0
  114. package/dist/wasm-bindings/runtime-contract.d.ts.map +1 -0
  115. package/dist/wasm-bindings/runtime-contract.js +112 -0
  116. package/dist/wasm-bindings/runtime-contract.js.map +1 -0
  117. package/dist/wasm-core/.syncular-wasm-profile +1 -0
  118. package/dist/wasm-core/syncular-runtime-artifact.json +17 -0
  119. package/dist/wasm-core/syncular.d.ts +162 -0
  120. package/dist/wasm-core/syncular.js +1847 -0
  121. package/dist/wasm-core/syncular_bg.wasm +0 -0
  122. package/dist/wasm-core/syncular_bg.wasm.d.ts +77 -0
  123. package/dist/wasm-perf/.syncular-wasm-profile +1 -0
  124. package/dist/wasm-perf/syncular-runtime-artifact.json +21 -0
  125. package/dist/wasm-perf/syncular.d.ts +207 -0
  126. package/dist/wasm-perf/syncular.js +2341 -0
  127. package/dist/wasm-perf/syncular_bg.wasm +0 -0
  128. package/dist/wasm-perf/syncular_bg.wasm.d.ts +97 -0
  129. package/dist/wasm-runtime.d.ts +18 -0
  130. package/dist/wasm-runtime.d.ts.map +1 -0
  131. package/dist/wasm-runtime.js +28 -0
  132. package/dist/wasm-runtime.js.map +1 -0
  133. package/dist/worker-client.d.ts +123 -0
  134. package/dist/worker-client.d.ts.map +1 -0
  135. package/dist/worker-client.js +1686 -0
  136. package/dist/worker-client.js.map +1 -0
  137. package/dist/worker-entry.d.ts +2 -0
  138. package/dist/worker-entry.d.ts.map +1 -0
  139. package/dist/worker-entry.js +479 -0
  140. package/dist/worker-entry.js.map +1 -0
  141. package/dist/worker-protocol.d.ts +64 -0
  142. package/dist/worker-protocol.d.ts.map +1 -0
  143. package/dist/worker-protocol.js +16 -0
  144. package/dist/worker-protocol.js.map +1 -0
  145. package/dist/worker-realtime.d.ts +39 -0
  146. package/dist/worker-realtime.d.ts.map +1 -0
  147. package/dist/worker-realtime.js +677 -0
  148. package/dist/worker-realtime.js.map +1 -0
  149. package/package.json +124 -17
  150. package/src/auth-leases.ts +251 -0
  151. package/src/blob-limits.ts +98 -0
  152. package/src/bridge-client.ts +512 -0
  153. package/src/client-config.ts +29 -0
  154. package/src/client.ts +256 -1414
  155. package/src/command-history.ts +623 -0
  156. package/src/console-diagnostics.ts +617 -0
  157. package/src/crdt-yjs/index.ts +3 -0
  158. package/src/crdt-yjs/webview-host-facade.ts +477 -0
  159. package/src/crdt-yjs/yjs-document-field-adapter.ts +733 -0
  160. package/src/crdt-yjs/yjs-prosemirror-bridge.ts +272 -0
  161. package/src/database.ts +1342 -0
  162. package/src/diagnostics.ts +116 -0
  163. package/src/errors.ts +269 -0
  164. package/src/generated-bridge.ts +741 -0
  165. package/src/index.ts +21 -25
  166. package/src/mutations.ts +12 -821
  167. package/src/network.ts +32 -0
  168. package/src/react/index.ts +1036 -0
  169. package/src/react-native/index.ts +152 -0
  170. package/src/runtime-contract.ts +48 -0
  171. package/src/rust-client.ts +1488 -0
  172. package/src/sentry.ts +215 -0
  173. package/src/sql-safety.ts +61 -0
  174. package/src/tauri/index.ts +211 -0
  175. package/src/types.ts +1356 -0
  176. package/src/wasm-bindings/generated-wasm-bindings.d.ts +70 -0
  177. package/src/wasm-bindings/runtime-contract.ts +158 -0
  178. package/src/wasm-runtime.ts +76 -0
  179. package/src/worker-client.ts +2215 -0
  180. package/src/worker-entry.ts +584 -0
  181. package/src/worker-protocol.ts +109 -0
  182. package/src/worker-realtime.ts +843 -0
  183. package/dist/blobs/index.d.ts +0 -7
  184. package/dist/blobs/index.d.ts.map +0 -1
  185. package/dist/blobs/index.js +0 -7
  186. package/dist/blobs/index.js.map +0 -1
  187. package/dist/blobs/manager.d.ts +0 -345
  188. package/dist/blobs/manager.d.ts.map +0 -1
  189. package/dist/blobs/manager.js +0 -749
  190. package/dist/blobs/manager.js.map +0 -1
  191. package/dist/blobs/migrate.d.ts +0 -14
  192. package/dist/blobs/migrate.d.ts.map +0 -1
  193. package/dist/blobs/migrate.js +0 -59
  194. package/dist/blobs/migrate.js.map +0 -1
  195. package/dist/blobs/types.d.ts +0 -62
  196. package/dist/blobs/types.d.ts.map +0 -1
  197. package/dist/blobs/types.js +0 -5
  198. package/dist/blobs/types.js.map +0 -1
  199. package/dist/conflicts.d.ts +0 -31
  200. package/dist/conflicts.d.ts.map +0 -1
  201. package/dist/conflicts.js +0 -112
  202. package/dist/conflicts.js.map +0 -1
  203. package/dist/create-client.d.ts +0 -118
  204. package/dist/create-client.d.ts.map +0 -1
  205. package/dist/create-client.js +0 -178
  206. package/dist/create-client.js.map +0 -1
  207. package/dist/engine/SyncEngine.d.ts +0 -272
  208. package/dist/engine/SyncEngine.d.ts.map +0 -1
  209. package/dist/engine/SyncEngine.js +0 -1860
  210. package/dist/engine/SyncEngine.js.map +0 -1
  211. package/dist/engine/index.d.ts +0 -6
  212. package/dist/engine/index.d.ts.map +0 -1
  213. package/dist/engine/index.js +0 -6
  214. package/dist/engine/index.js.map +0 -1
  215. package/dist/engine/types.d.ts +0 -345
  216. package/dist/engine/types.d.ts.map +0 -1
  217. package/dist/engine/types.js +0 -7
  218. package/dist/engine/types.js.map +0 -1
  219. package/dist/handlers/collection.d.ts +0 -6
  220. package/dist/handlers/collection.d.ts.map +0 -1
  221. package/dist/handlers/collection.js +0 -21
  222. package/dist/handlers/collection.js.map +0 -1
  223. package/dist/handlers/create-handler.d.ts +0 -120
  224. package/dist/handlers/create-handler.d.ts.map +0 -1
  225. package/dist/handlers/create-handler.js +0 -145
  226. package/dist/handlers/create-handler.js.map +0 -1
  227. package/dist/handlers/types.d.ts +0 -83
  228. package/dist/handlers/types.d.ts.map +0 -1
  229. package/dist/handlers/types.js +0 -5
  230. package/dist/handlers/types.js.map +0 -1
  231. package/dist/migrate.d.ts +0 -19
  232. package/dist/migrate.d.ts.map +0 -1
  233. package/dist/migrate.js +0 -238
  234. package/dist/migrate.js.map +0 -1
  235. package/dist/outbox.d.ts +0 -112
  236. package/dist/outbox.d.ts.map +0 -1
  237. package/dist/outbox.js +0 -294
  238. package/dist/outbox.js.map +0 -1
  239. package/dist/plugins/incrementing-version.d.ts +0 -34
  240. package/dist/plugins/incrementing-version.d.ts.map +0 -1
  241. package/dist/plugins/incrementing-version.js +0 -83
  242. package/dist/plugins/incrementing-version.js.map +0 -1
  243. package/dist/plugins/index.d.ts +0 -3
  244. package/dist/plugins/index.d.ts.map +0 -1
  245. package/dist/plugins/index.js +0 -3
  246. package/dist/plugins/index.js.map +0 -1
  247. package/dist/plugins/types.d.ts +0 -49
  248. package/dist/plugins/types.d.ts.map +0 -1
  249. package/dist/plugins/types.js +0 -15
  250. package/dist/plugins/types.js.map +0 -1
  251. package/dist/proxy/connection.d.ts +0 -33
  252. package/dist/proxy/connection.d.ts.map +0 -1
  253. package/dist/proxy/connection.js +0 -153
  254. package/dist/proxy/connection.js.map +0 -1
  255. package/dist/proxy/dialect.d.ts +0 -46
  256. package/dist/proxy/dialect.d.ts.map +0 -1
  257. package/dist/proxy/dialect.js +0 -58
  258. package/dist/proxy/dialect.js.map +0 -1
  259. package/dist/proxy/driver.d.ts +0 -42
  260. package/dist/proxy/driver.d.ts.map +0 -1
  261. package/dist/proxy/driver.js +0 -78
  262. package/dist/proxy/driver.js.map +0 -1
  263. package/dist/proxy/index.d.ts +0 -10
  264. package/dist/proxy/index.d.ts.map +0 -1
  265. package/dist/proxy/index.js +0 -10
  266. package/dist/proxy/index.js.map +0 -1
  267. package/dist/proxy/mutations.d.ts +0 -9
  268. package/dist/proxy/mutations.d.ts.map +0 -1
  269. package/dist/proxy/mutations.js +0 -11
  270. package/dist/proxy/mutations.js.map +0 -1
  271. package/dist/pull-engine.d.ts +0 -48
  272. package/dist/pull-engine.d.ts.map +0 -1
  273. package/dist/pull-engine.js +0 -648
  274. package/dist/pull-engine.js.map +0 -1
  275. package/dist/push-engine.d.ts +0 -18
  276. package/dist/push-engine.d.ts.map +0 -1
  277. package/dist/push-engine.js +0 -167
  278. package/dist/push-engine.js.map +0 -1
  279. package/dist/query/FingerprintCollector.d.ts +0 -18
  280. package/dist/query/FingerprintCollector.d.ts.map +0 -1
  281. package/dist/query/FingerprintCollector.js +0 -28
  282. package/dist/query/FingerprintCollector.js.map +0 -1
  283. package/dist/query/QueryContext.d.ts +0 -33
  284. package/dist/query/QueryContext.d.ts.map +0 -1
  285. package/dist/query/QueryContext.js +0 -16
  286. package/dist/query/QueryContext.js.map +0 -1
  287. package/dist/query/fingerprint.d.ts +0 -61
  288. package/dist/query/fingerprint.d.ts.map +0 -1
  289. package/dist/query/fingerprint.js +0 -91
  290. package/dist/query/fingerprint.js.map +0 -1
  291. package/dist/query/index.d.ts +0 -7
  292. package/dist/query/index.d.ts.map +0 -1
  293. package/dist/query/index.js +0 -7
  294. package/dist/query/index.js.map +0 -1
  295. package/dist/query/tracked-select.d.ts +0 -18
  296. package/dist/query/tracked-select.d.ts.map +0 -1
  297. package/dist/query/tracked-select.js +0 -90
  298. package/dist/query/tracked-select.js.map +0 -1
  299. package/dist/schema.d.ts +0 -83
  300. package/dist/schema.d.ts.map +0 -1
  301. package/dist/schema.js +0 -7
  302. package/dist/schema.js.map +0 -1
  303. package/dist/subscription-state.d.ts +0 -46
  304. package/dist/subscription-state.d.ts.map +0 -1
  305. package/dist/subscription-state.js +0 -185
  306. package/dist/subscription-state.js.map +0 -1
  307. package/dist/sync-loop.d.ts +0 -34
  308. package/dist/sync-loop.d.ts.map +0 -1
  309. package/dist/sync-loop.js +0 -253
  310. package/dist/sync-loop.js.map +0 -1
  311. package/dist/sync.d.ts +0 -32
  312. package/dist/sync.d.ts.map +0 -1
  313. package/dist/sync.js +0 -55
  314. package/dist/sync.js.map +0 -1
  315. package/dist/utils/id.d.ts +0 -17
  316. package/dist/utils/id.d.ts.map +0 -1
  317. package/dist/utils/id.js +0 -46
  318. package/dist/utils/id.js.map +0 -1
  319. package/src/blobs/index.ts +0 -7
  320. package/src/blobs/manager.ts +0 -1027
  321. package/src/blobs/migrate.ts +0 -67
  322. package/src/blobs/types.ts +0 -84
  323. package/src/client.test.ts +0 -417
  324. package/src/conflicts.ts +0 -171
  325. package/src/create-client.test.ts +0 -83
  326. package/src/create-client.ts +0 -330
  327. package/src/engine/SyncEngine.test.ts +0 -254
  328. package/src/engine/SyncEngine.ts +0 -2375
  329. package/src/engine/index.ts +0 -6
  330. package/src/engine/types.ts +0 -419
  331. package/src/handlers/collection.ts +0 -36
  332. package/src/handlers/create-handler.ts +0 -330
  333. package/src/handlers/types.ts +0 -108
  334. package/src/migrate.ts +0 -318
  335. package/src/outbox.ts +0 -383
  336. package/src/plugins/incrementing-version.ts +0 -133
  337. package/src/plugins/index.ts +0 -2
  338. package/src/plugins/types.ts +0 -63
  339. package/src/proxy/connection.ts +0 -191
  340. package/src/proxy/dialect.ts +0 -76
  341. package/src/proxy/driver.ts +0 -126
  342. package/src/proxy/index.ts +0 -10
  343. package/src/proxy/mutations.ts +0 -18
  344. package/src/pull-engine.test.ts +0 -151
  345. package/src/pull-engine.ts +0 -855
  346. package/src/push-engine.ts +0 -216
  347. package/src/query/FingerprintCollector.ts +0 -29
  348. package/src/query/QueryContext.ts +0 -54
  349. package/src/query/fingerprint.ts +0 -109
  350. package/src/query/index.ts +0 -10
  351. package/src/query/tracked-select.ts +0 -139
  352. package/src/schema.ts +0 -94
  353. package/src/subscription-state.ts +0 -259
  354. package/src/sync-loop.ts +0 -379
  355. package/src/sync.ts +0 -170
  356. package/src/utils/id.ts +0 -55
@@ -0,0 +1,1686 @@
1
+ import { issueSyncularAuthLease } from './auth-leases';
2
+ import { assertSyncularBlobPayloadLimit } from './blob-limits';
3
+ import { isSyncularRemoteMode, resolveSyncularClientConfig, SYNCULAR_DEFAULT_STORAGE, } from './client-config';
4
+ import { appendSyncularDiagnosticEvent, appendSyncularSyncTimings, createSyncularSyncAttempt, summarizeSyncularDiagnosticSubscriptions, } from './diagnostics';
5
+ import { isGeneratedSyncularOperationalStateWorkerRequestType, } from './generated-bridge';
6
+ import { browserSyncularNetworkStatusSource } from './network';
7
+ import { assertSyncularReadonlySql } from './sql-safety';
8
+ import { getSyncularRuntimeArtifact, selectSyncularRuntimeArtifact, } from './wasm-runtime';
9
+ import { createSyncularWorkerErrorPayload, SYNCULAR_WORKER_PROTOCOL_VERSION, } from './worker-protocol';
10
+ const DEFAULT_SYNCULAR_WORKER_REQUEST_TIMEOUT_MS = 30_000;
11
+ export async function createSyncularWorkerClient(options) {
12
+ const config = resolveSyncularClientConfig(options.config);
13
+ const runtime = typeof options.runtime === 'string'
14
+ ? getSyncularRuntimeArtifact(options.runtime)
15
+ : (options.runtime ??
16
+ selectSyncularRuntimeArtifact(options.requiredRuntimeFeatures, options.runtimeArtifacts));
17
+ const worker = typeof options.worker === 'function'
18
+ ? options.worker()
19
+ : (options.worker ?? createDefaultSyncularWorker());
20
+ const client = new SyncularWorkerClient(worker, {
21
+ ownsWorker: options.worker == null,
22
+ requestTimeoutMs: options.requestTimeoutMs,
23
+ getHeaders: options.getHeaders,
24
+ authLifecycle: options.authLifecycle,
25
+ diagnostics: options.diagnostics,
26
+ rowsChangedDebounceMs: options.sync?.rowsChangedDebounceMs,
27
+ network: options.sync?.network === false
28
+ ? undefined
29
+ : (options.sync?.network ?? browserSyncularNetworkStatusSource()),
30
+ blobLimits: options.blobLimits,
31
+ });
32
+ try {
33
+ await client.open(config, runtime);
34
+ }
35
+ catch (err) {
36
+ if (options.config.storage == null &&
37
+ config.storage === SYNCULAR_DEFAULT_STORAGE &&
38
+ isOpfsOpenFailure(err)) {
39
+ const fallbackConfig = { ...config, storage: 'indexedDb' };
40
+ client.setStorageFallback({
41
+ from: SYNCULAR_DEFAULT_STORAGE,
42
+ to: fallbackConfig.storage,
43
+ reason: errorMessage(err),
44
+ });
45
+ await client.open(fallbackConfig, runtime);
46
+ }
47
+ else {
48
+ throw err;
49
+ }
50
+ }
51
+ return client;
52
+ }
53
+ export function getDefaultSyncularWorkerUrl() {
54
+ const runtimeUrl = new URL(import.meta.url);
55
+ const sourceRuntime = runtimeUrl.pathname.endsWith('/src/worker-client.ts');
56
+ return new URL(sourceRuntime ? './worker-entry.ts' : './worker-entry.js', runtimeUrl);
57
+ }
58
+ export function createDefaultSyncularWorker() {
59
+ return new Worker(getDefaultSyncularWorkerUrl(), {
60
+ type: 'module',
61
+ credentials: 'same-origin',
62
+ });
63
+ }
64
+ export class SyncularWorkerError extends Error {
65
+ code;
66
+ category;
67
+ retryable;
68
+ recommendedAction;
69
+ details;
70
+ constructor(payload) {
71
+ super(payload.message);
72
+ this.name = payload.name ?? 'SyncularWorkerError';
73
+ this.code = payload.code;
74
+ this.category = payload.category;
75
+ this.retryable = payload.retryable === true;
76
+ this.recommendedAction = payload.recommendedAction;
77
+ this.details = payload.details;
78
+ if (payload.stack)
79
+ this.stack = payload.stack;
80
+ }
81
+ }
82
+ export class SyncularWorkerClient {
83
+ worker;
84
+ #nextId = 1;
85
+ #pending = new Map();
86
+ #closed = false;
87
+ #requestTimeoutMs;
88
+ #getHeaders;
89
+ #authHeaders = {};
90
+ #authLifecycle;
91
+ #authRefreshInFlight;
92
+ #config;
93
+ #realtimeOptions;
94
+ #realtimeState = 'disconnected';
95
+ #storageFallback;
96
+ #lastDiagnostic;
97
+ #lastError;
98
+ #lastLifecycleState;
99
+ #recoveryRequired = false;
100
+ #authRequired = false;
101
+ #subscriptions = [];
102
+ #lastBootstrap;
103
+ #recentDiagnostics = [];
104
+ #recentSyncTimings = [];
105
+ #rowsChangedDebounceMs;
106
+ #rowsChangedDebounceTimer;
107
+ #pendingRowsChanged;
108
+ #lastOutboxStats;
109
+ #lastConflictStats;
110
+ #lastBlobUploadStats;
111
+ #blobLimits;
112
+ #network;
113
+ #networkOnline;
114
+ #unsubscribeNetwork;
115
+ #diagnosticListeners = new Set();
116
+ #rowsChangedListeners = new Set();
117
+ #eventListeners = new Map();
118
+ #presenceByScopeKey = new Map();
119
+ #joinedPresence = new Map();
120
+ #liveListeners = new Map();
121
+ constructor(worker, options) {
122
+ this.worker = worker;
123
+ this.#requestTimeoutMs =
124
+ options.requestTimeoutMs ?? DEFAULT_SYNCULAR_WORKER_REQUEST_TIMEOUT_MS;
125
+ this.#getHeaders = options.getHeaders;
126
+ this.#authLifecycle = options.authLifecycle;
127
+ this.#blobLimits = options.blobLimits;
128
+ this.#network = options.network;
129
+ this.#networkOnline = this.#network?.isOnline();
130
+ this.#unsubscribeNetwork = this.#subscribeNetworkEvents();
131
+ this.#rowsChangedDebounceMs =
132
+ options.rowsChangedDebounceMs === false
133
+ ? false
134
+ : Math.max(0, options.rowsChangedDebounceMs ?? 0);
135
+ if (options.diagnostics) {
136
+ this.#diagnosticListeners.add(options.diagnostics);
137
+ }
138
+ this.worker.onmessage = (event) => {
139
+ this.#handleWorkerMessage(event.data);
140
+ };
141
+ this.worker.onerror = (event) => {
142
+ const payload = createSyncularWorkerErrorPayload('worker.failed', event.message || 'Syncular worker failed');
143
+ this.#emitDiagnostic({
144
+ level: 'error',
145
+ source: 'worker',
146
+ code: 'worker.failed',
147
+ message: payload.message,
148
+ });
149
+ this.#rejectAll(payload);
150
+ };
151
+ this.worker.onmessageerror = () => {
152
+ const payload = createSyncularWorkerErrorPayload('worker.message_unreadable', 'Syncular worker sent an unreadable message');
153
+ this.#emitDiagnostic({
154
+ level: 'error',
155
+ source: 'worker',
156
+ code: 'worker.message_unreadable',
157
+ message: payload.message,
158
+ });
159
+ this.#rejectAll(payload);
160
+ };
161
+ this.ownsWorker = options.ownsWorker;
162
+ }
163
+ ownsWorker;
164
+ async open(config, runtime) {
165
+ const resolvedConfig = resolveSyncularClientConfig(config);
166
+ const resolvedRuntime = typeof runtime === 'string'
167
+ ? getSyncularRuntimeArtifact(runtime)
168
+ : runtime;
169
+ await this.#request({
170
+ type: 'open',
171
+ config: resolvedConfig,
172
+ runtime: serializeRuntimeArtifact(resolvedRuntime),
173
+ });
174
+ this.#config = resolvedConfig;
175
+ this.#realtimeOptions = undefined;
176
+ this.#subscriptions = [];
177
+ this.#lastBootstrap = undefined;
178
+ await this.#refreshAuthHeaders();
179
+ }
180
+ async setAuthHeaders(headers) {
181
+ await this.#setAuthHeaders(headers, { restartRealtime: true });
182
+ }
183
+ async issueAuthLease(request) {
184
+ await this.#refreshAuthHeaders();
185
+ try {
186
+ return await this.#issueAuthLeaseWithCurrentHeaders(request);
187
+ }
188
+ catch (error) {
189
+ const shouldRetry = await this.#resolveAuthRetry(error, 'authLeaseIssue');
190
+ if (!shouldRetry)
191
+ throw error;
192
+ await this.#refreshAuthHeaders();
193
+ return this.#issueAuthLeaseWithCurrentHeaders(request);
194
+ }
195
+ }
196
+ async upsertAuthLease(lease) {
197
+ await this.#requestAndDrain({
198
+ type: 'upsertAuthLease',
199
+ lease,
200
+ });
201
+ }
202
+ async authLease(leaseId) {
203
+ return this.#request({
204
+ type: 'authLease',
205
+ leaseId,
206
+ });
207
+ }
208
+ async activeAuthLeases(actorId, nowMs = Date.now()) {
209
+ return this.#request({
210
+ type: 'activeAuthLeases',
211
+ actorId: actorId ?? null,
212
+ nowMs: Math.trunc(nowMs),
213
+ });
214
+ }
215
+ async setFieldEncryption(config) {
216
+ await this.#request({
217
+ type: 'setFieldEncryption',
218
+ config: config == null ? null : normalizeFieldEncryptionConfig(config),
219
+ });
220
+ }
221
+ async setEncryptedCrdt(config) {
222
+ await this.#request({
223
+ type: 'setEncryptedCrdt',
224
+ config: config == null ? null : normalizeEncryptedCrdtConfig(config),
225
+ });
226
+ }
227
+ async setBlobEncryption(config) {
228
+ await this.#request({
229
+ type: 'setBlobEncryption',
230
+ config: config == null ? null : normalizeBlobEncryptionConfig(config),
231
+ });
232
+ }
233
+ async #setAuthHeaders(headers, options) {
234
+ this.#authHeaders = cloneAuthHeaders(headers);
235
+ await this.#request({
236
+ type: 'setAuthHeaders',
237
+ headers: cloneAuthHeaders(this.#authHeaders),
238
+ });
239
+ if (options.restartRealtime)
240
+ await this.#restartRealtime();
241
+ }
242
+ async #issueAuthLeaseWithCurrentHeaders(request) {
243
+ const config = this.#config;
244
+ if (!config) {
245
+ throw new Error('Syncular worker client must be opened before auth lease issue');
246
+ }
247
+ assertSyncularRemoteMode(config, 'auth lease issue');
248
+ const lease = await issueSyncularAuthLease({
249
+ baseUrl: config.baseUrl,
250
+ headers: this.#authHeaders,
251
+ request,
252
+ appSchema: config.appSchema,
253
+ });
254
+ await this.upsertAuthLease(lease);
255
+ this.#emitDiagnostic({
256
+ level: 'info',
257
+ source: 'auth',
258
+ code: 'auth_lease.issued',
259
+ message: 'Syncular auth lease issued and stored',
260
+ details: {
261
+ leaseId: lease.leaseId,
262
+ expiresAtMs: lease.expiresAtMs,
263
+ schemaVersion: lease.schemaVersion,
264
+ },
265
+ });
266
+ return lease;
267
+ }
268
+ async startRealtime(options = {}) {
269
+ if (options === false) {
270
+ await this.stopRealtime();
271
+ return;
272
+ }
273
+ const config = this.#config;
274
+ if (!config) {
275
+ throw new Error('Syncular worker client must be opened before realtime');
276
+ }
277
+ assertSyncularRemoteMode(config, 'realtime');
278
+ const realtimeOptions = options === true ? {} : options;
279
+ if (realtimeOptions.enabled === false) {
280
+ await this.stopRealtime();
281
+ return;
282
+ }
283
+ this.#realtimeOptions = realtimeOptions;
284
+ await this.#refreshAuthHeaders({ restartRealtime: false });
285
+ await this.#request({
286
+ type: 'startRealtime',
287
+ options: await resolveRealtimeWorkerOptions(realtimeOptions, config),
288
+ });
289
+ }
290
+ async stopRealtime() {
291
+ this.#realtimeOptions = undefined;
292
+ await this.#request({ type: 'stopRealtime' });
293
+ }
294
+ async setSubscriptions(subscriptions) {
295
+ this.#subscriptions = [...subscriptions];
296
+ this.#lastBootstrap = undefined;
297
+ await this.#request({
298
+ type: 'setSubscriptions',
299
+ subscriptions: this.#subscriptions,
300
+ });
301
+ }
302
+ async forceSubscriptionsBootstrap(subscriptionIds = []) {
303
+ return this.#request({
304
+ type: 'forceSubscriptionsBootstrap',
305
+ subscriptionIds: [...subscriptionIds],
306
+ });
307
+ }
308
+ async executeSql(sql, params = []) {
309
+ assertSyncularReadonlySql(sql);
310
+ return this.#request({
311
+ type: 'executeSql',
312
+ sql,
313
+ params: [...params],
314
+ });
315
+ }
316
+ executeUnsafeSql(sql, params = []) {
317
+ return this.#request({
318
+ type: 'executeUnsafeSql',
319
+ sql,
320
+ params: [...params],
321
+ });
322
+ }
323
+ subscribeQuery(sql, params, tables, hints = []) {
324
+ return this.#request({
325
+ type: 'subscribeQuery',
326
+ sql,
327
+ params: [...params],
328
+ tables: [...tables],
329
+ hints: hints.map((hint) => ({
330
+ table: hint.table,
331
+ rowIds: [...(hint.rowIds ?? [])],
332
+ fields: [...(hint.fields ?? [])],
333
+ })),
334
+ });
335
+ }
336
+ async unsubscribeQuery(id) {
337
+ await this.#request({ type: 'unsubscribeQuery', queryId: id });
338
+ }
339
+ drainLiveQueryEvents() {
340
+ return this.#request({ type: 'drainLiveQueryEvents' });
341
+ }
342
+ liveQueryDiagnostics() {
343
+ return this.#request({ type: 'liveQueryDiagnostics' });
344
+ }
345
+ async applyMutation(operation, localRow) {
346
+ return this.#requestAndDrain({
347
+ type: 'applyMutation',
348
+ operation,
349
+ localRow,
350
+ });
351
+ }
352
+ async applyLeasedMutation(operation, localRow) {
353
+ return this.#requestAndDrain({
354
+ type: 'applyLeasedMutation',
355
+ operation,
356
+ localRow,
357
+ });
358
+ }
359
+ async applyMutationsBatch(operations) {
360
+ return this.#requestAndDrain({
361
+ type: 'applyMutationsBatch',
362
+ operations,
363
+ });
364
+ }
365
+ async applyMutationsCommit(operations) {
366
+ return this.#requestAndDrain({
367
+ type: 'applyMutationsCommit',
368
+ operations,
369
+ });
370
+ }
371
+ async applyLeasedMutationsCommit(operations) {
372
+ return this.#requestAndDrain({
373
+ type: 'applyLeasedMutationsCommit',
374
+ operations,
375
+ });
376
+ }
377
+ async syncPull(options = {}) {
378
+ return this.#syncWithAuthRetry({
379
+ type: 'syncPull',
380
+ syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
381
+ });
382
+ }
383
+ async syncPush(options = {}) {
384
+ return this.#syncWithAuthRetry({
385
+ type: 'syncPush',
386
+ syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
387
+ });
388
+ }
389
+ async syncOnce(options = {}) {
390
+ return this.#syncWithAuthRetry({
391
+ type: 'syncOnce',
392
+ syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
393
+ });
394
+ }
395
+ async resumeFromBackground(options = {}) {
396
+ this.#recoveryRequired = true;
397
+ this.#emitDiagnostic({
398
+ level: 'info',
399
+ source: 'client',
400
+ code: 'lifecycle.resume_from_background',
401
+ message: 'Syncular resumed from background',
402
+ });
403
+ await this.#refreshAuthHeaders({ restartRealtime: false });
404
+ await this.#restartRealtime();
405
+ return this.#syncWithAuthRetry({
406
+ type: 'syncOnce',
407
+ syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
408
+ }, { refreshAuthHeaders: false });
409
+ }
410
+ transportStats() {
411
+ return this.#request({ type: 'transportStats' });
412
+ }
413
+ async resetTransportStats() {
414
+ await this.#request({ type: 'resetTransportStats' });
415
+ }
416
+ conflictSummaries() {
417
+ return this.#request({ type: 'conflictSummaries' });
418
+ }
419
+ retryConflictKeepLocal(id) {
420
+ return this.#requestAndDrain({
421
+ type: 'retryConflictKeepLocal',
422
+ conflictId: id,
423
+ });
424
+ }
425
+ async resolveConflict(id, resolution) {
426
+ await this.#requestAndDrain({
427
+ type: 'resolveConflict',
428
+ conflictId: id,
429
+ resolution,
430
+ });
431
+ }
432
+ listTable(table) {
433
+ return this.#request({ type: 'listTable', table });
434
+ }
435
+ storeBlob(data, options) {
436
+ try {
437
+ assertSyncularBlobPayloadLimit({
438
+ operation: 'store',
439
+ size: data.byteLength,
440
+ limits: this.#blobLimits,
441
+ options,
442
+ diagnostics: (event) => this.#emitDiagnostic(event),
443
+ });
444
+ }
445
+ catch (error) {
446
+ return Promise.reject(error);
447
+ }
448
+ const request = { type: 'storeBlob', data, options };
449
+ const result = options?.immediate
450
+ ? this.#requestWithAuthRetry(request, 'blobInitiateUpload')
451
+ : this.#request(request);
452
+ return result.then((ref) => {
453
+ if (options?.immediate) {
454
+ this.#emitClientEvent('blobUploadCompleted', { ref });
455
+ }
456
+ void this.#emitOperationalState();
457
+ return ref;
458
+ });
459
+ }
460
+ async retrieveBlob(ref) {
461
+ assertSyncularBlobPayloadLimit({
462
+ operation: 'retrieve',
463
+ size: ref.size,
464
+ limits: this.#blobLimits,
465
+ refHash: ref.hash,
466
+ diagnostics: (event) => this.#emitDiagnostic(event),
467
+ });
468
+ const wasLocal = this.#hasDiagnosticListeners()
469
+ ? await this.#request({
470
+ type: 'isBlobLocal',
471
+ hash: ref.hash,
472
+ }).catch(() => undefined)
473
+ : undefined;
474
+ try {
475
+ const bytes = await this.#requestWithAuthRetry({ type: 'retrieveBlob', ref }, 'blobGetDownloadUrl');
476
+ if (wasLocal !== undefined) {
477
+ this.#emitDiagnostic({
478
+ level: 'info',
479
+ source: 'blob',
480
+ code: wasLocal ? 'blob.cache_hit' : 'blob.cache_miss',
481
+ message: wasLocal
482
+ ? 'Syncular blob served from local cache'
483
+ : 'Syncular blob fetched after local cache miss',
484
+ details: {
485
+ hash: ref.hash,
486
+ size: ref.size,
487
+ mimeType: ref.mimeType,
488
+ encrypted: ref.encrypted === true,
489
+ ...(ref.keyId ? { keyId: ref.keyId } : {}),
490
+ },
491
+ });
492
+ }
493
+ return bytes;
494
+ }
495
+ catch (error) {
496
+ if (this.#hasDiagnosticListeners()) {
497
+ this.#emitDiagnostic({
498
+ level: 'warn',
499
+ source: 'blob',
500
+ code: 'blob.download_failed',
501
+ message: `Syncular blob download failed: ${errorMessage(error)}`,
502
+ details: {
503
+ hash: ref.hash,
504
+ size: ref.size,
505
+ mimeType: ref.mimeType,
506
+ encrypted: ref.encrypted === true,
507
+ ...(ref.keyId ? { keyId: ref.keyId } : {}),
508
+ },
509
+ });
510
+ }
511
+ throw error;
512
+ }
513
+ }
514
+ async isBlobLocal(hash) {
515
+ const local = await this.#request({ type: 'isBlobLocal', hash });
516
+ if (this.#hasDiagnosticListeners()) {
517
+ this.#emitDiagnostic({
518
+ level: 'debug',
519
+ source: 'blob',
520
+ code: local ? 'blob.cache_hit' : 'blob.cache_miss',
521
+ message: local
522
+ ? 'Syncular blob is available in local cache'
523
+ : 'Syncular blob is not available in local cache',
524
+ details: { hash },
525
+ });
526
+ }
527
+ return local;
528
+ }
529
+ async processBlobUploadQueue(options) {
530
+ const observeBlobEvents = this.#hasClientEventListeners('blobUploadCompleted') ||
531
+ this.#hasClientEventListeners('blobUploadFailed') ||
532
+ this.#hasDiagnosticListeners();
533
+ const before = observeBlobEvents
534
+ ? await this.#readBlobOutboxRows().catch(() => [])
535
+ : [];
536
+ const result = await this.#requestWithAuthRetry({ type: 'processBlobUploadQueue', options }, 'blobInitiateUpload');
537
+ if (observeBlobEvents) {
538
+ const after = await this.#readBlobOutboxRows().catch(() => []);
539
+ this.#emitBlobUploadEvents(before, after);
540
+ }
541
+ if (this.#hasDiagnosticListeners()) {
542
+ this.#emitDiagnostic({
543
+ level: result.failed > 0 ? 'warn' : 'info',
544
+ source: 'blob',
545
+ code: 'blob.upload_queue_processed',
546
+ message: 'Syncular blob upload queue processed',
547
+ details: result,
548
+ });
549
+ }
550
+ void this.#emitOperationalState();
551
+ return result;
552
+ }
553
+ async blobUploadQueueStats() {
554
+ const stats = await this.#readBlobUploadStats();
555
+ this.#lastBlobUploadStats = stats;
556
+ return stats;
557
+ }
558
+ blobCacheStats() {
559
+ return this.#request({ type: 'blobCacheStats' });
560
+ }
561
+ async pruneBlobCache(maxBytes) {
562
+ const prunedBytes = await this.#request({
563
+ type: 'pruneBlobCache',
564
+ maxBytes,
565
+ });
566
+ if (this.#hasDiagnosticListeners()) {
567
+ this.#emitDiagnostic({
568
+ level: 'info',
569
+ source: 'blob',
570
+ code: 'blob.cache_pruned',
571
+ message: 'Syncular blob cache pruned',
572
+ details: {
573
+ prunedBytes,
574
+ ...(maxBytes !== undefined ? { maxBytes } : {}),
575
+ },
576
+ });
577
+ }
578
+ return prunedBytes;
579
+ }
580
+ async clearBlobCache() {
581
+ await this.#request({ type: 'clearBlobCache' });
582
+ if (this.#hasDiagnosticListeners()) {
583
+ this.#emitDiagnostic({
584
+ level: 'info',
585
+ source: 'blob',
586
+ code: 'blob.cache_cleared',
587
+ message: 'Syncular blob cache cleared',
588
+ });
589
+ }
590
+ }
591
+ compactStorage(options = {}) {
592
+ return this.#request({ type: 'compactStorage', options });
593
+ }
594
+ generatedSchemaState() {
595
+ return this.#request({ type: 'generatedSchemaState' });
596
+ }
597
+ localHealthCheck() {
598
+ return this.#request({ type: 'localHealthCheck' });
599
+ }
600
+ exportLocalSupportBundle() {
601
+ return this.#request({ type: 'exportLocalSupportBundle' });
602
+ }
603
+ importLocalSupportBundle(bundle) {
604
+ return this.#request({
605
+ type: 'importLocalSupportBundle',
606
+ bundleJson: typeof bundle === 'string' ? bundle : JSON.stringify(bundle),
607
+ });
608
+ }
609
+ async repairLocalHealth(request) {
610
+ const message = {
611
+ type: 'repairLocalHealth',
612
+ request: {
613
+ action: request.action,
614
+ subscriptionIds: [...(request.subscriptionIds ?? [])],
615
+ tables: [...(request.tables ?? [])],
616
+ },
617
+ };
618
+ const result = request.action === 'clearOrphanedSyncedRows'
619
+ ? await this.#requestAndDrain(message)
620
+ : await this.#request(message);
621
+ if (request.action === 'clearOrphanedSyncedRows') {
622
+ this.#emitLifecycleChanged();
623
+ }
624
+ return result;
625
+ }
626
+ async resetLocalSyncState(request = {}) {
627
+ const normalized = {
628
+ subscriptionIds: [...(request.subscriptionIds ?? [])],
629
+ clearSyncedRows: request.clearSyncedRows === true,
630
+ };
631
+ const result = await this.#requestAndDrain({
632
+ type: 'resetLocalSyncState',
633
+ request: normalized,
634
+ });
635
+ this.#lastBootstrap = undefined;
636
+ this.#emitLifecycleChanged();
637
+ return result;
638
+ }
639
+ buildYjsTextUpdate(args) {
640
+ return this.#request({ type: 'buildYjsTextUpdate', args });
641
+ }
642
+ applyYjsTextUpdates(args) {
643
+ return this.#request({ type: 'applyYjsTextUpdates', args });
644
+ }
645
+ applyYjsEnvelopeToPayload(args) {
646
+ return this.#request({ type: 'applyYjsEnvelopeToPayload', args });
647
+ }
648
+ openCrdtField(request) {
649
+ return this.#request({ type: 'openCrdtField', request });
650
+ }
651
+ applyCrdtFieldText(request) {
652
+ return this.#requestAndDrain({ type: 'applyCrdtFieldText', request });
653
+ }
654
+ applyCrdtFieldYjsUpdate(request) {
655
+ return this.#requestAndDrain({
656
+ type: 'applyCrdtFieldYjsUpdate',
657
+ request,
658
+ });
659
+ }
660
+ materializeCrdtField(request) {
661
+ return this.#request({ type: 'materializeCrdtField', request });
662
+ }
663
+ crdtDocumentSnapshot(request) {
664
+ return this.#request({ type: 'crdtDocumentSnapshot', request });
665
+ }
666
+ crdtUpdateLog(request) {
667
+ return this.#request({ type: 'crdtUpdateLog', request });
668
+ }
669
+ snapshotCrdtFieldStateVector(request) {
670
+ return this.#request({ type: 'snapshotCrdtFieldStateVector', request });
671
+ }
672
+ compactCrdtField(request) {
673
+ return this.#requestAndDrain({ type: 'compactCrdtField', request });
674
+ }
675
+ encryptionHelper(method, args = {}) {
676
+ return this.#request({ type: 'encryptionHelper', method, args });
677
+ }
678
+ runtimeInfo() {
679
+ return this.#request({ type: 'runtimeInfo' }).then((info) => ({
680
+ ...info,
681
+ storageFallback: this.#storageFallback,
682
+ }));
683
+ }
684
+ connectionState() {
685
+ return {
686
+ closed: this.#closed,
687
+ pendingRequests: this.#pending.size,
688
+ realtime: this.#realtimeState,
689
+ ...(this.#storageFallback
690
+ ? { storageFallback: this.#storageFallback }
691
+ : {}),
692
+ ...(this.#lastDiagnostic ? { lastDiagnostic: this.#lastDiagnostic } : {}),
693
+ ...(this.#lastError ? { lastError: this.#lastError } : {}),
694
+ };
695
+ }
696
+ lifecycleState() {
697
+ return this.#computeLifecycleState();
698
+ }
699
+ #computeLifecycleState() {
700
+ const outbox = this.#lastOutboxStats;
701
+ const conflicts = this.#lastConflictStats;
702
+ const blobUploads = this.#lastBlobUploadStats;
703
+ const bootstrap = this.#lastBootstrap
704
+ ? {
705
+ complete: this.#lastBootstrap.complete,
706
+ criticalReady: this.#lastBootstrap.criticalReady,
707
+ interactiveReady: this.#lastBootstrap.interactiveReady,
708
+ isBootstrapping: this.#lastBootstrap.isBootstrapping,
709
+ progressPercent: this.#lastBootstrap.progressPercent,
710
+ }
711
+ : undefined;
712
+ const hasFailedOutbox = (outbox?.failed ?? 0) > 0;
713
+ const hasConflicts = (conflicts?.unresolved ?? 0) > 0;
714
+ const hasFailedBlobUploads = (blobUploads?.failed ?? 0) > 0;
715
+ const authRequired = this.#authRequired || this.#lastError?.code === 'sync.auth_required';
716
+ const offline = this.#networkOnline === false;
717
+ const pendingSyncRequests = this.#pendingSyncRequestCount();
718
+ const phase = this.#closed
719
+ ? 'closed'
720
+ : authRequired
721
+ ? 'authRequired'
722
+ : offline
723
+ ? 'offline'
724
+ : this.#recoveryRequired || bootstrap?.isBootstrapping === true
725
+ ? 'recovering'
726
+ : pendingSyncRequests > 0
727
+ ? 'syncing'
728
+ : this.#realtimeState === 'connecting'
729
+ ? 'connecting'
730
+ : hasFailedOutbox || hasConflicts || hasFailedBlobUploads
731
+ ? 'degraded'
732
+ : bootstrap?.complete === true
733
+ ? 'complete'
734
+ : 'offline';
735
+ return {
736
+ phase,
737
+ realtime: this.#realtimeState,
738
+ online: this.#networkOnline ?? this.#realtimeState === 'connected',
739
+ requiresAction: authRequired || hasFailedOutbox || hasConflicts || hasFailedBlobUploads,
740
+ pendingRequests: this.#pending.size,
741
+ ...(bootstrap ? { bootstrap } : {}),
742
+ ...(outbox ? { outbox } : {}),
743
+ ...(conflicts ? { conflicts } : {}),
744
+ ...(blobUploads ? { blobUploads } : {}),
745
+ ...(this.#lastDiagnostic ? { lastDiagnostic: this.#lastDiagnostic } : {}),
746
+ ...(this.#lastError ? { lastError: this.#lastError } : {}),
747
+ };
748
+ }
749
+ async diagnosticSnapshot() {
750
+ const [runtime, transportStats] = await Promise.all([
751
+ this.runtimeInfo(),
752
+ this.transportStats().catch(() => undefined),
753
+ ]);
754
+ const outboxStats = this.#lastOutboxStats ??
755
+ (await this.#readOutboxStats().catch(() => undefined));
756
+ if (outboxStats)
757
+ this.#lastOutboxStats = outboxStats;
758
+ const conflictStats = this.#lastConflictStats ??
759
+ (await this.#readConflictStats().catch(() => undefined));
760
+ if (conflictStats)
761
+ this.#lastConflictStats = conflictStats;
762
+ const blobUploadStats = this.#lastBlobUploadStats ??
763
+ (await this.blobUploadQueueStats().catch(() => undefined));
764
+ return {
765
+ generatedAt: Date.now(),
766
+ runtime,
767
+ connection: this.connectionState(),
768
+ subscriptions: summarizeSyncularDiagnosticSubscriptions(this.#subscriptions, this.#lastBootstrap),
769
+ recentDiagnostics: [...this.#recentDiagnostics],
770
+ recentSyncTimings: [...this.#recentSyncTimings],
771
+ ...(this.#lastBootstrap ? { bootstrap: this.#lastBootstrap } : {}),
772
+ ...(transportStats ? { transportStats } : {}),
773
+ ...(outboxStats ? { outboxStats } : {}),
774
+ ...(conflictStats ? { conflictStats } : {}),
775
+ ...(blobUploadStats ? { blobUploadStats } : {}),
776
+ };
777
+ }
778
+ setStorageFallback(fallback) {
779
+ this.#storageFallback = fallback;
780
+ this.#emitDiagnostic({
781
+ level: 'warn',
782
+ source: 'storage',
783
+ code: 'storage.fallback',
784
+ message: `Syncular storage fell back from ${fallback.from} to ${fallback.to}`,
785
+ details: {
786
+ from: fallback.from,
787
+ to: fallback.to,
788
+ reason: fallback.reason,
789
+ },
790
+ });
791
+ }
792
+ addDiagnosticListener(listener) {
793
+ this.#diagnosticListeners.add(listener);
794
+ return () => {
795
+ this.#diagnosticListeners.delete(listener);
796
+ };
797
+ }
798
+ addEventListener(event, listener) {
799
+ const listeners = this.#eventListeners.get(event) ?? new Set();
800
+ listeners.add(listener);
801
+ this.#eventListeners.set(event, listeners);
802
+ return () => {
803
+ listeners.delete(listener);
804
+ if (listeners.size === 0)
805
+ this.#eventListeners.delete(event);
806
+ };
807
+ }
808
+ addRowsChangedListener(listener) {
809
+ this.#rowsChangedListeners.add(listener);
810
+ return () => {
811
+ this.#rowsChangedListeners.delete(listener);
812
+ };
813
+ }
814
+ getPresence(scopeKey) {
815
+ return (this.#presenceByScopeKey.get(scopeKey) ??
816
+ []);
817
+ }
818
+ joinPresence(scopeKey, metadata) {
819
+ this.#joinedPresence.set(scopeKey, metadata);
820
+ void this.#request({
821
+ type: 'sendPresence',
822
+ action: 'join',
823
+ scopeKey,
824
+ ...(metadata === undefined ? {} : { metadata }),
825
+ }).catch((error) => {
826
+ this.#emitDiagnostic({
827
+ level: 'warn',
828
+ source: 'realtime',
829
+ code: 'realtime.presence_join_failed',
830
+ message: `Syncular presence join failed: ${errorMessage(error)}`,
831
+ details: { scopeKey },
832
+ });
833
+ });
834
+ this.#applyPresenceEvent({
835
+ action: 'join',
836
+ scopeKey,
837
+ clientId: this.#config?.clientId ?? '',
838
+ actorId: this.#config?.actorId ?? '',
839
+ metadata,
840
+ });
841
+ }
842
+ leavePresence(scopeKey) {
843
+ this.#joinedPresence.delete(scopeKey);
844
+ void this.#request({
845
+ type: 'sendPresence',
846
+ action: 'leave',
847
+ scopeKey,
848
+ }).catch((error) => {
849
+ this.#emitDiagnostic({
850
+ level: 'warn',
851
+ source: 'realtime',
852
+ code: 'realtime.presence_leave_failed',
853
+ message: `Syncular presence leave failed: ${errorMessage(error)}`,
854
+ details: { scopeKey },
855
+ });
856
+ });
857
+ this.#applyPresenceEvent({
858
+ action: 'leave',
859
+ scopeKey,
860
+ clientId: this.#config?.clientId ?? '',
861
+ actorId: this.#config?.actorId ?? '',
862
+ });
863
+ }
864
+ updatePresenceMetadata(scopeKey, metadata) {
865
+ if (this.#joinedPresence.has(scopeKey)) {
866
+ this.#joinedPresence.set(scopeKey, metadata);
867
+ }
868
+ void this.#request({
869
+ type: 'sendPresence',
870
+ action: 'update',
871
+ scopeKey,
872
+ metadata,
873
+ }).catch((error) => {
874
+ this.#emitDiagnostic({
875
+ level: 'warn',
876
+ source: 'realtime',
877
+ code: 'realtime.presence_update_failed',
878
+ message: `Syncular presence update failed: ${errorMessage(error)}`,
879
+ details: { scopeKey },
880
+ });
881
+ });
882
+ this.#applyPresenceEvent({
883
+ action: 'update',
884
+ scopeKey,
885
+ clientId: this.#config?.clientId ?? '',
886
+ actorId: this.#config?.actorId ?? '',
887
+ metadata,
888
+ });
889
+ }
890
+ addPresenceListener(listener) {
891
+ return this.addEventListener('presenceChanged', listener);
892
+ }
893
+ addLiveQueryListener(queryId, listener) {
894
+ this.#liveListeners.set(queryId, listener);
895
+ }
896
+ removeLiveQueryListener(queryId) {
897
+ this.#liveListeners.delete(queryId);
898
+ }
899
+ async close() {
900
+ if (this.#closed)
901
+ return;
902
+ this.#closed = true;
903
+ this.#realtimeState = 'disconnected';
904
+ this.#unsubscribeNetwork?.();
905
+ this.#unsubscribeNetwork = undefined;
906
+ this.#emitLifecycleChanged();
907
+ try {
908
+ await this.#request({ type: 'close' });
909
+ }
910
+ finally {
911
+ this.#flushRowsChanged();
912
+ this.#rejectAll(createSyncularWorkerErrorPayload('worker.closed', 'Syncular worker client closed'));
913
+ if (this.ownsWorker)
914
+ this.worker.terminate();
915
+ }
916
+ }
917
+ async #requestAndDrain(request) {
918
+ const value = await this.#request(request);
919
+ await this.#emitLiveEvents();
920
+ if (shouldEmitOperationalState(request.type)) {
921
+ void this.#emitOperationalState();
922
+ }
923
+ return value;
924
+ }
925
+ async #syncWithAuthRetry(request, options = { refreshAuthHeaders: true }) {
926
+ const config = this.#config;
927
+ if (!config) {
928
+ throw new Error('Syncular worker client must be opened before sync');
929
+ }
930
+ assertSyncularRemoteMode(config, request.type);
931
+ if (options.refreshAuthHeaders) {
932
+ await this.#refreshAuthHeaders({ restartRealtime: false });
933
+ }
934
+ try {
935
+ const result = await this.#requestAndDrain(request);
936
+ this.#emitBootstrapChanged(result);
937
+ return result;
938
+ }
939
+ catch (error) {
940
+ const shouldRetry = await this.#resolveAuthRetry(error, 'sync');
941
+ if (!shouldRetry)
942
+ throw error;
943
+ await this.#refreshAuthHeaders({ restartRealtime: false });
944
+ const result = await this.#requestAndDrain(request);
945
+ this.#emitBootstrapChanged(result);
946
+ return result;
947
+ }
948
+ }
949
+ async #requestWithAuthRetry(request, operation) {
950
+ await this.#refreshAuthHeaders();
951
+ try {
952
+ return await this.#request(request);
953
+ }
954
+ catch (error) {
955
+ const shouldRetry = await this.#resolveAuthRetry(error, operation);
956
+ if (!shouldRetry)
957
+ throw error;
958
+ await this.#refreshAuthHeaders();
959
+ return this.#request(request);
960
+ }
961
+ }
962
+ async #refreshAuthHeaders(options = { restartRealtime: true }) {
963
+ if (!this.#getHeaders)
964
+ return;
965
+ const headers = await this.#getHeaders();
966
+ this.#emitDiagnostic({
967
+ level: 'debug',
968
+ source: 'auth',
969
+ code: 'auth.headers_refreshed',
970
+ message: 'Syncular auth headers refreshed',
971
+ details: { headerCount: Object.keys(headers).length },
972
+ });
973
+ await this.#setAuthHeaders(headers, options);
974
+ }
975
+ async #restartRealtime() {
976
+ const realtimeOptions = this.#realtimeOptions;
977
+ const config = this.#config;
978
+ if (!realtimeOptions || !config || this.#closed)
979
+ return;
980
+ await this.#request({
981
+ type: 'startRealtime',
982
+ options: await resolveRealtimeWorkerOptions(realtimeOptions, config),
983
+ });
984
+ }
985
+ async #resolveAuthRetry(error, operation) {
986
+ const status = authStatusFromError(error);
987
+ const lifecycle = this.#authLifecycle;
988
+ if (!status || !lifecycle)
989
+ return false;
990
+ const context = { operation, status };
991
+ this.#emitDiagnostic({
992
+ level: 'warn',
993
+ source: 'auth',
994
+ code: 'auth.expired',
995
+ message: `Syncular auth expired during ${operation}`,
996
+ details: context,
997
+ });
998
+ await lifecycle.onAuthExpired?.(context);
999
+ const refreshResult = await this.#refreshAuthSingleFlight(context);
1000
+ this.#emitDiagnostic({
1001
+ level: refreshResult ? 'info' : 'warn',
1002
+ source: 'auth',
1003
+ code: refreshResult ? 'auth.refresh_succeeded' : 'auth.refresh_failed',
1004
+ message: refreshResult
1005
+ ? 'Syncular auth refresh succeeded'
1006
+ : 'Syncular auth refresh did not produce fresh credentials',
1007
+ details: context,
1008
+ });
1009
+ if (lifecycle.retryWithFreshToken) {
1010
+ return Boolean(await lifecycle.retryWithFreshToken({ ...context, refreshResult }));
1011
+ }
1012
+ return refreshResult;
1013
+ }
1014
+ #refreshAuthSingleFlight(context) {
1015
+ const refreshToken = this.#authLifecycle?.refreshToken;
1016
+ if (!refreshToken)
1017
+ return Promise.resolve(false);
1018
+ this.#authRefreshInFlight ??= Promise.resolve(refreshToken(context))
1019
+ .then(Boolean)
1020
+ .finally(() => {
1021
+ this.#authRefreshInFlight = undefined;
1022
+ });
1023
+ return this.#authRefreshInFlight;
1024
+ }
1025
+ async #emitLiveEvents() {
1026
+ const events = await this.drainLiveQueryEvents();
1027
+ for (const event of events) {
1028
+ this.#liveListeners.get(event.queryId)?.(event);
1029
+ }
1030
+ }
1031
+ #request(request) {
1032
+ if (this.#closed && request.type !== 'close') {
1033
+ return Promise.reject(new SyncularWorkerError(createSyncularWorkerErrorPayload('worker.closed', 'Syncular worker client is closed')));
1034
+ }
1035
+ const id = this.#nextId++;
1036
+ const message = {
1037
+ ...request,
1038
+ id,
1039
+ protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
1040
+ };
1041
+ return new Promise((resolve, reject) => {
1042
+ const timeout = this.#requestTimeoutMs > 0
1043
+ ? setTimeout(() => {
1044
+ this.#pending.delete(id);
1045
+ this.#sendCancel(id);
1046
+ this.#emitDiagnostic({
1047
+ level: 'warn',
1048
+ source: 'worker',
1049
+ code: 'worker.request_timeout',
1050
+ message: `Syncular worker request ${request.type} timed out`,
1051
+ details: {
1052
+ requestId: id,
1053
+ requestType: request.type,
1054
+ timeoutMs: this.#requestTimeoutMs,
1055
+ },
1056
+ });
1057
+ const error = new SyncularWorkerError(createSyncularWorkerErrorPayload('worker.request_timeout', `Syncular worker request ${request.type} timed out after ${this.#requestTimeoutMs}ms`, { details: { requestId: id, requestType: request.type } }));
1058
+ this.#lastError = {
1059
+ message: error.message,
1060
+ code: error.code,
1061
+ };
1062
+ this.#emitLifecycleChanged();
1063
+ reject(error);
1064
+ }, this.#requestTimeoutMs)
1065
+ : undefined;
1066
+ this.#pending.set(id, {
1067
+ type: request.type,
1068
+ timeout,
1069
+ resolve: (value) => resolve(value),
1070
+ reject,
1071
+ });
1072
+ this.#emitLifecycleChanged();
1073
+ this.worker.postMessage(message);
1074
+ });
1075
+ }
1076
+ #handleWorkerMessage(message) {
1077
+ if (message.protocolVersion !== SYNCULAR_WORKER_PROTOCOL_VERSION) {
1078
+ this.#rejectAll(createSyncularWorkerErrorPayload('worker.protocol_mismatch', `Unsupported Syncular worker protocol ${message.protocolVersion}`, { details: { supported: SYNCULAR_WORKER_PROTOCOL_VERSION } }));
1079
+ return;
1080
+ }
1081
+ if (isWorkerEvent(message)) {
1082
+ this.#handleWorkerEvent(message);
1083
+ return;
1084
+ }
1085
+ this.#handleResponse(message);
1086
+ }
1087
+ #handleResponse(response) {
1088
+ const pending = this.#pending.get(response.id);
1089
+ if (!pending)
1090
+ return;
1091
+ this.#pending.delete(response.id);
1092
+ if (pending.timeout)
1093
+ clearTimeout(pending.timeout);
1094
+ if (response.ok) {
1095
+ pending.resolve(response.value);
1096
+ }
1097
+ else {
1098
+ const error = new SyncularWorkerError(response.error);
1099
+ this.#lastError = {
1100
+ message: error.message,
1101
+ code: error.code,
1102
+ };
1103
+ pending.reject(error);
1104
+ }
1105
+ this.#emitLifecycleChanged();
1106
+ }
1107
+ #handleWorkerEvent(event) {
1108
+ if (event.type === 'liveQueryEvents') {
1109
+ for (const liveEvent of event.events) {
1110
+ this.#liveListeners.get(liveEvent.queryId)?.(liveEvent);
1111
+ }
1112
+ return;
1113
+ }
1114
+ if (event.type === 'rowsChanged') {
1115
+ this.#emitRowsChanged({
1116
+ source: event.source,
1117
+ changedTables: event.changedTables,
1118
+ changedRows: event.changedRows,
1119
+ changedRowsTruncated: event.changedRowsTruncated,
1120
+ });
1121
+ return;
1122
+ }
1123
+ if (event.type === 'bootstrapChanged') {
1124
+ this.#lastBootstrap = event.bootstrap;
1125
+ if (event.bootstrap.complete) {
1126
+ this.#recoveryRequired = false;
1127
+ this.#authRequired = false;
1128
+ this.#lastError = undefined;
1129
+ }
1130
+ this.#emitClientEvent('bootstrapChanged', event.bootstrap);
1131
+ this.#emitLifecycleChanged();
1132
+ return;
1133
+ }
1134
+ if (event.type === 'realtimeState') {
1135
+ this.#realtimeState = event.state;
1136
+ this.#emitDiagnostic({
1137
+ level: 'info',
1138
+ source: 'realtime',
1139
+ code: 'realtime.state',
1140
+ message: `Syncular realtime is ${event.state}`,
1141
+ details: { state: event.state },
1142
+ });
1143
+ if (event.state === 'connected') {
1144
+ this.#rejoinPresence();
1145
+ }
1146
+ this.#emitLifecycleChanged();
1147
+ return;
1148
+ }
1149
+ if (event.type === 'presenceEvent') {
1150
+ this.#applyPresenceEvent(event);
1151
+ return;
1152
+ }
1153
+ this.#emitDiagnostic(event.event);
1154
+ }
1155
+ #emitClientEvent(event, payload) {
1156
+ const listeners = this.#eventListeners.get(event);
1157
+ if (!listeners || listeners.size === 0)
1158
+ return;
1159
+ for (const listener of listeners) {
1160
+ try {
1161
+ listener(payload);
1162
+ }
1163
+ catch {
1164
+ // Client event listeners must never break sync control flow.
1165
+ }
1166
+ }
1167
+ }
1168
+ #emitLifecycleChanged() {
1169
+ const state = this.#computeLifecycleState();
1170
+ if (this.#lastLifecycleState && sameJson(this.#lastLifecycleState, state)) {
1171
+ return;
1172
+ }
1173
+ this.#lastLifecycleState = state;
1174
+ this.#emitClientEvent('lifecycleChanged', state);
1175
+ }
1176
+ #pendingSyncRequestCount() {
1177
+ let count = 0;
1178
+ for (const pending of this.#pending.values()) {
1179
+ if (pending.type === 'syncOnce' ||
1180
+ pending.type === 'syncPull' ||
1181
+ pending.type === 'syncPush') {
1182
+ count += 1;
1183
+ }
1184
+ }
1185
+ return count;
1186
+ }
1187
+ #subscribeNetworkEvents() {
1188
+ const network = this.#network;
1189
+ if (!network?.addEventListener || !network.removeEventListener)
1190
+ return;
1191
+ const refresh = () => {
1192
+ const next = network.isOnline();
1193
+ if (next === this.#networkOnline)
1194
+ return;
1195
+ this.#networkOnline = next;
1196
+ this.#emitLifecycleChanged();
1197
+ };
1198
+ network.addEventListener('online', refresh);
1199
+ network.addEventListener('offline', refresh);
1200
+ return () => {
1201
+ network.removeEventListener?.('online', refresh);
1202
+ network.removeEventListener?.('offline', refresh);
1203
+ };
1204
+ }
1205
+ #hasClientEventListeners(event) {
1206
+ return (this.#eventListeners.get(event)?.size ?? 0) > 0;
1207
+ }
1208
+ #hasDiagnosticListeners() {
1209
+ return this.#diagnosticListeners.size > 0;
1210
+ }
1211
+ #emitRowsChanged(event) {
1212
+ if (this.#rowsChangedDebounceMs === false ||
1213
+ this.#rowsChangedDebounceMs <= 0) {
1214
+ this.#deliverRowsChanged(event);
1215
+ return;
1216
+ }
1217
+ if (!this.#pendingRowsChanged) {
1218
+ this.#pendingRowsChanged = {
1219
+ source: event.source,
1220
+ changedTables: new Set(event.changedTables),
1221
+ changedRows: [...event.changedRows],
1222
+ changedRowsTruncated: event.changedRowsTruncated === true,
1223
+ };
1224
+ }
1225
+ else {
1226
+ const pending = this.#pendingRowsChanged;
1227
+ pending.source =
1228
+ pending.source === event.source ? pending.source : 'mixed';
1229
+ for (const table of event.changedTables)
1230
+ pending.changedTables.add(table);
1231
+ pending.changedRows.push(...event.changedRows);
1232
+ pending.changedRowsTruncated ||= event.changedRowsTruncated === true;
1233
+ }
1234
+ if (this.#rowsChangedDebounceTimer)
1235
+ return;
1236
+ this.#rowsChangedDebounceTimer = setTimeout(() => {
1237
+ this.#rowsChangedDebounceTimer = undefined;
1238
+ this.#flushRowsChanged();
1239
+ }, this.#rowsChangedDebounceMs);
1240
+ }
1241
+ #flushRowsChanged() {
1242
+ if (this.#rowsChangedDebounceTimer) {
1243
+ clearTimeout(this.#rowsChangedDebounceTimer);
1244
+ this.#rowsChangedDebounceTimer = undefined;
1245
+ }
1246
+ const pending = this.#pendingRowsChanged;
1247
+ this.#pendingRowsChanged = undefined;
1248
+ if (!pending)
1249
+ return;
1250
+ this.#deliverRowsChanged({
1251
+ source: pending.source,
1252
+ changedTables: [...pending.changedTables],
1253
+ changedRows: pending.changedRows,
1254
+ changedRowsTruncated: pending.changedRowsTruncated,
1255
+ });
1256
+ }
1257
+ #deliverRowsChanged(event) {
1258
+ for (const listener of this.#rowsChangedListeners) {
1259
+ try {
1260
+ listener(event);
1261
+ }
1262
+ catch {
1263
+ // Row-change listeners must never break worker event handling.
1264
+ }
1265
+ }
1266
+ this.#emitClientEvent('rowsChanged', event);
1267
+ }
1268
+ #emitBootstrapChanged(result) {
1269
+ this.#lastBootstrap = result.bootstrap;
1270
+ this.#recoveryRequired = false;
1271
+ this.#authRequired = false;
1272
+ this.#lastError = undefined;
1273
+ appendSyncularSyncTimings(this.#recentSyncTimings, result.timings);
1274
+ this.#emitClientEvent('bootstrapChanged', result.bootstrap);
1275
+ this.#emitLifecycleChanged();
1276
+ }
1277
+ async #emitOperationalState() {
1278
+ const observeLifecycle = this.#hasClientEventListeners('lifecycleChanged');
1279
+ const observeOutbox = observeLifecycle || this.#hasClientEventListeners('outboxChanged');
1280
+ const observeConflicts = observeLifecycle || this.#hasClientEventListeners('conflictsChanged');
1281
+ const observeBlobUploads = observeLifecycle || this.#hasClientEventListeners('blobUploadsChanged');
1282
+ if (!observeOutbox && !observeConflicts && !observeBlobUploads) {
1283
+ return;
1284
+ }
1285
+ const [outboxStats, conflictStats, blobUploadStats] = await Promise.all([
1286
+ observeOutbox
1287
+ ? this.#readOutboxStats().catch(() => undefined)
1288
+ : Promise.resolve(undefined),
1289
+ observeConflicts
1290
+ ? this.#readConflictStats().catch(() => undefined)
1291
+ : Promise.resolve(undefined),
1292
+ observeBlobUploads
1293
+ ? this.#readBlobUploadStats().catch(() => undefined)
1294
+ : Promise.resolve(undefined),
1295
+ ]);
1296
+ if (outboxStats && !sameJson(this.#lastOutboxStats, outboxStats)) {
1297
+ this.#lastOutboxStats = outboxStats;
1298
+ this.#emitClientEvent('outboxChanged', outboxStats);
1299
+ }
1300
+ if (conflictStats && !sameJson(this.#lastConflictStats, conflictStats)) {
1301
+ this.#lastConflictStats = conflictStats;
1302
+ this.#emitClientEvent('conflictsChanged', conflictStats);
1303
+ }
1304
+ if (blobUploadStats &&
1305
+ !sameJson(this.#lastBlobUploadStats, blobUploadStats)) {
1306
+ this.#lastBlobUploadStats = blobUploadStats;
1307
+ this.#emitClientEvent('blobUploadsChanged', blobUploadStats);
1308
+ }
1309
+ this.#emitLifecycleChanged();
1310
+ }
1311
+ #readBlobUploadStats() {
1312
+ return this.#request({ type: 'blobUploadQueueStats' });
1313
+ }
1314
+ async #readOutboxStats() {
1315
+ const result = await this.#request({
1316
+ type: 'executeUnsafeSql',
1317
+ sql: 'select status, count(*) as count from sync_outbox_commits group by status',
1318
+ params: [],
1319
+ });
1320
+ const stats = {
1321
+ pending: 0,
1322
+ sending: 0,
1323
+ failed: 0,
1324
+ acked: 0,
1325
+ total: 0,
1326
+ };
1327
+ for (const row of result.rows) {
1328
+ const count = coerceCount(row.count);
1329
+ if (row.status === 'pending')
1330
+ stats.pending = count;
1331
+ if (row.status === 'sending')
1332
+ stats.sending = count;
1333
+ if (row.status === 'failed')
1334
+ stats.failed = count;
1335
+ if (row.status === 'acked')
1336
+ stats.acked = count;
1337
+ stats.total += count;
1338
+ }
1339
+ return stats;
1340
+ }
1341
+ async #readConflictStats() {
1342
+ const result = await this.#request({
1343
+ type: 'executeUnsafeSql',
1344
+ sql: 'select ' +
1345
+ 'coalesce(sum(case when resolved_at is null then 1 else 0 end), 0) as unresolved, ' +
1346
+ 'coalesce(sum(case when resolved_at is not null then 1 else 0 end), 0) as resolved, ' +
1347
+ 'count(*) as total from sync_conflicts',
1348
+ params: [],
1349
+ });
1350
+ const row = result.rows[0];
1351
+ return {
1352
+ unresolved: coerceCount(row?.unresolved),
1353
+ resolved: coerceCount(row?.resolved),
1354
+ total: coerceCount(row?.total),
1355
+ };
1356
+ }
1357
+ async #readBlobOutboxRows() {
1358
+ const result = await this.#request({
1359
+ type: 'executeUnsafeSql',
1360
+ sql: 'select hash, size, mime_type, encrypted, key_id, status, error ' +
1361
+ 'from sync_blob_outbox order by created_at asc',
1362
+ params: [],
1363
+ });
1364
+ return result.rows;
1365
+ }
1366
+ #emitBlobUploadEvents(beforeRows, afterRows) {
1367
+ const after = new Map(afterRows.map((row) => [row.hash, row]));
1368
+ for (const before of beforeRows) {
1369
+ const next = after.get(before.hash);
1370
+ if (!next) {
1371
+ const ref = {
1372
+ hash: before.hash,
1373
+ size: coerceCount(before.size),
1374
+ mimeType: before.mime_type,
1375
+ encrypted: before.encrypted === true || before.encrypted === 1,
1376
+ ...(before.key_id ? { keyId: before.key_id } : {}),
1377
+ };
1378
+ this.#emitClientEvent('blobUploadCompleted', {
1379
+ ref,
1380
+ });
1381
+ if (this.#hasDiagnosticListeners()) {
1382
+ this.#emitDiagnostic({
1383
+ level: 'info',
1384
+ source: 'blob',
1385
+ code: 'blob.upload_completed',
1386
+ message: 'Syncular blob upload completed',
1387
+ details: ref,
1388
+ });
1389
+ }
1390
+ continue;
1391
+ }
1392
+ if (before.status !== 'failed' && next.status === 'failed') {
1393
+ const ref = {
1394
+ hash: next.hash,
1395
+ size: coerceCount(next.size),
1396
+ mimeType: next.mime_type,
1397
+ encrypted: next.encrypted === true || next.encrypted === 1,
1398
+ ...(next.key_id ? { keyId: next.key_id } : {}),
1399
+ };
1400
+ this.#emitClientEvent('blobUploadFailed', {
1401
+ hash: next.hash,
1402
+ error: next.error ?? 'Blob upload failed',
1403
+ ref,
1404
+ });
1405
+ if (this.#hasDiagnosticListeners()) {
1406
+ this.#emitDiagnostic({
1407
+ level: 'warn',
1408
+ source: 'blob',
1409
+ code: 'blob.upload_failed',
1410
+ message: next.error ?? 'Syncular blob upload failed',
1411
+ details: {
1412
+ ...ref,
1413
+ error: next.error ?? 'Blob upload failed',
1414
+ },
1415
+ });
1416
+ }
1417
+ }
1418
+ }
1419
+ }
1420
+ #applyPresenceEvent(event) {
1421
+ const scopeKey = normalizePresenceScopeKey(event.scopeKey);
1422
+ if (!scopeKey)
1423
+ return;
1424
+ const current = this.#presenceByScopeKey.get(scopeKey) ?? [];
1425
+ let next;
1426
+ if (event.action === 'snapshot') {
1427
+ next = event.entries ?? [];
1428
+ }
1429
+ else {
1430
+ if (!event.clientId || !event.actorId)
1431
+ return;
1432
+ if (event.action === 'leave') {
1433
+ next = current.filter((entry) => entry.clientId !== event.clientId);
1434
+ }
1435
+ else if (event.action === 'update') {
1436
+ const existing = current.find((entry) => entry.clientId === event.clientId);
1437
+ if (!existing)
1438
+ return;
1439
+ next = current.map((entry) => entry.clientId === event.clientId
1440
+ ? { ...entry, metadata: event.metadata }
1441
+ : entry);
1442
+ }
1443
+ else {
1444
+ const existing = current.find((entry) => entry.clientId === event.clientId);
1445
+ next = [
1446
+ ...current.filter((entry) => entry.clientId !== event.clientId),
1447
+ {
1448
+ clientId: event.clientId,
1449
+ actorId: event.actorId,
1450
+ joinedAt: existing?.joinedAt ?? Date.now(),
1451
+ metadata: event.metadata,
1452
+ },
1453
+ ];
1454
+ }
1455
+ }
1456
+ if (arePresenceEntriesEqual(current, next))
1457
+ return;
1458
+ if (next.length === 0) {
1459
+ this.#presenceByScopeKey.delete(scopeKey);
1460
+ }
1461
+ else {
1462
+ this.#presenceByScopeKey.set(scopeKey, next);
1463
+ }
1464
+ this.#emitClientEvent('presenceChanged', {
1465
+ scopeKey,
1466
+ presence: next,
1467
+ });
1468
+ }
1469
+ #rejoinPresence() {
1470
+ if (this.#joinedPresence.size === 0)
1471
+ return;
1472
+ for (const [scopeKey, metadata] of this.#joinedPresence) {
1473
+ void this.#request({
1474
+ type: 'sendPresence',
1475
+ action: 'join',
1476
+ scopeKey,
1477
+ ...(metadata === undefined ? {} : { metadata }),
1478
+ }).catch((error) => {
1479
+ this.#emitDiagnostic({
1480
+ level: 'warn',
1481
+ source: 'realtime',
1482
+ code: 'realtime.presence_rejoin_failed',
1483
+ message: `Syncular presence rejoin failed: ${errorMessage(error)}`,
1484
+ details: { scopeKey },
1485
+ });
1486
+ });
1487
+ }
1488
+ }
1489
+ #rejectAll(reason) {
1490
+ const error = reason instanceof Error ? reason : new SyncularWorkerError(reason);
1491
+ this.#lastError = {
1492
+ message: error.message,
1493
+ ...(error instanceof SyncularWorkerError ? { code: error.code } : {}),
1494
+ };
1495
+ for (const pending of this.#pending.values()) {
1496
+ if (pending.timeout)
1497
+ clearTimeout(pending.timeout);
1498
+ pending.reject(error);
1499
+ }
1500
+ this.#pending.clear();
1501
+ }
1502
+ #sendCancel(requestId) {
1503
+ const id = this.#nextId++;
1504
+ this.worker.postMessage({
1505
+ id,
1506
+ protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
1507
+ type: 'cancel',
1508
+ requestId,
1509
+ });
1510
+ }
1511
+ #emitDiagnostic(event) {
1512
+ const diagnostic = {
1513
+ at: event.at ?? Date.now(),
1514
+ level: event.level,
1515
+ source: event.source,
1516
+ code: event.code,
1517
+ message: event.message,
1518
+ ...(event.syncAttemptId ? { syncAttemptId: event.syncAttemptId } : {}),
1519
+ ...(event.traceId ? { traceId: event.traceId } : {}),
1520
+ ...(event.spanId ? { spanId: event.spanId } : {}),
1521
+ ...(event.clientId ? { clientId: event.clientId } : {}),
1522
+ ...(event.subscriptionId ? { subscriptionId: event.subscriptionId } : {}),
1523
+ ...(event.table ? { table: event.table } : {}),
1524
+ ...(event.rowId ? { rowId: event.rowId } : {}),
1525
+ ...(event.cursor !== undefined ? { cursor: event.cursor } : {}),
1526
+ ...(event.details ? { details: event.details } : {}),
1527
+ };
1528
+ if (diagnostic.details?.resyncRequired === true) {
1529
+ this.#recoveryRequired = true;
1530
+ }
1531
+ if (diagnostic.code === 'auth.expired' ||
1532
+ diagnostic.code === 'auth.refresh_failed' ||
1533
+ diagnostic.code === 'sync.auth_required') {
1534
+ this.#authRequired = true;
1535
+ }
1536
+ if (diagnostic.code === 'auth.refresh_succeeded') {
1537
+ this.#authRequired = false;
1538
+ }
1539
+ this.#lastDiagnostic = diagnostic;
1540
+ appendSyncularDiagnosticEvent(this.#recentDiagnostics, diagnostic);
1541
+ this.#emitLifecycleChanged();
1542
+ if (this.#diagnosticListeners.size === 0)
1543
+ return;
1544
+ for (const listener of this.#diagnosticListeners) {
1545
+ try {
1546
+ listener(diagnostic);
1547
+ }
1548
+ catch {
1549
+ // Diagnostics must never break client control flow.
1550
+ }
1551
+ }
1552
+ }
1553
+ }
1554
+ function isOpfsOpenFailure(error) {
1555
+ if (!(error instanceof SyncularWorkerError))
1556
+ return false;
1557
+ const message = errorMessage(error).toLowerCase();
1558
+ return (message.includes('opfs') ||
1559
+ message.includes('sahpool') ||
1560
+ message.includes('sync access handle'));
1561
+ }
1562
+ function shouldEmitOperationalState(type) {
1563
+ return isGeneratedSyncularOperationalStateWorkerRequestType(type);
1564
+ }
1565
+ function errorMessage(error) {
1566
+ return error instanceof Error ? error.message : String(error);
1567
+ }
1568
+ function normalizePresenceScopeKey(scopeKey) {
1569
+ const separator = scopeKey.indexOf('::');
1570
+ return separator >= 0 ? scopeKey.slice(separator + 2) : scopeKey;
1571
+ }
1572
+ function coerceCount(value) {
1573
+ const numeric = typeof value === 'number' ? value : Number(value ?? 0);
1574
+ return Number.isFinite(numeric) && numeric > 0 ? Math.trunc(numeric) : 0;
1575
+ }
1576
+ function sameJson(left, right) {
1577
+ return JSON.stringify(left) === JSON.stringify(right);
1578
+ }
1579
+ function cloneAuthHeaders(headers) {
1580
+ return Object.fromEntries(Object.entries(headers));
1581
+ }
1582
+ function serializeRuntimeArtifact(runtime) {
1583
+ if (!runtime)
1584
+ return undefined;
1585
+ return {
1586
+ wasmGlueUrl: runtime.wasmGlueUrl == null
1587
+ ? undefined
1588
+ : runtimeUrlHref(runtime.wasmGlueUrl),
1589
+ wasmUrl: runtime.wasmUrl == null ? undefined : runtimeUrlHref(runtime.wasmUrl),
1590
+ };
1591
+ }
1592
+ function runtimeUrlHref(value) {
1593
+ if (value instanceof Request)
1594
+ return value.url;
1595
+ return value instanceof URL ? value.href : value;
1596
+ }
1597
+ function normalizeFieldEncryptionConfig(config) {
1598
+ const keys = {};
1599
+ for (const [kid, value] of Object.entries(config.keys)) {
1600
+ keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
1601
+ }
1602
+ return { ...config, keys };
1603
+ }
1604
+ function normalizeEncryptedCrdtConfig(config) {
1605
+ const keys = {};
1606
+ for (const [kid, value] of Object.entries(config.keys)) {
1607
+ keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
1608
+ }
1609
+ return { ...config, keys };
1610
+ }
1611
+ function normalizeBlobEncryptionConfig(config) {
1612
+ const keys = {};
1613
+ for (const [kid, value] of Object.entries(config.keys)) {
1614
+ keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
1615
+ }
1616
+ return { ...config, keys };
1617
+ }
1618
+ function arePresenceEntriesEqual(left, right) {
1619
+ if (left.length !== right.length)
1620
+ return false;
1621
+ for (let index = 0; index < left.length; index += 1) {
1622
+ const a = left[index];
1623
+ const b = right[index];
1624
+ if (a.clientId !== b.clientId ||
1625
+ a.actorId !== b.actorId ||
1626
+ a.joinedAt !== b.joinedAt ||
1627
+ JSON.stringify(a.metadata ?? {}) !== JSON.stringify(b.metadata ?? {})) {
1628
+ return false;
1629
+ }
1630
+ }
1631
+ return true;
1632
+ }
1633
+ function bytesToBase64Url(bytes) {
1634
+ let binary = '';
1635
+ for (const byte of bytes)
1636
+ binary += String.fromCharCode(byte);
1637
+ return btoa(binary)
1638
+ .replace(/\+/g, '-')
1639
+ .replace(/\//g, '_')
1640
+ .replace(/=+$/g, '');
1641
+ }
1642
+ async function resolveRealtimeWorkerOptions(options, config) {
1643
+ const params = {
1644
+ ...(options.params ?? {}),
1645
+ ...((await options.getParams?.({ clientId: config.clientId })) ?? {}),
1646
+ };
1647
+ return {
1648
+ ...(options.wsUrl ? { wsUrl: options.wsUrl } : {}),
1649
+ ...(Object.keys(params).length > 0 ? { params } : {}),
1650
+ ...(options.initialReconnectDelayMs != null
1651
+ ? { initialReconnectDelayMs: options.initialReconnectDelayMs }
1652
+ : {}),
1653
+ ...(options.maxReconnectDelayMs != null
1654
+ ? { maxReconnectDelayMs: options.maxReconnectDelayMs }
1655
+ : {}),
1656
+ ...(options.reconnectBackoffFactor != null
1657
+ ? { reconnectBackoffFactor: options.reconnectBackoffFactor }
1658
+ : {}),
1659
+ ...(options.heartbeatTimeoutMs != null
1660
+ ? { heartbeatTimeoutMs: options.heartbeatTimeoutMs }
1661
+ : {}),
1662
+ };
1663
+ }
1664
+ function assertSyncularRemoteMode(config, operation) {
1665
+ if (isSyncularRemoteMode(config))
1666
+ return;
1667
+ throw new Error(`Syncular ${operation} requires remote mode; current mode is ${config.mode}`);
1668
+ }
1669
+ function isWorkerEvent(message) {
1670
+ return 'type' in message && !('id' in message);
1671
+ }
1672
+ function authStatusFromError(error) {
1673
+ const details = error instanceof SyncularWorkerError ? error.details : undefined;
1674
+ if (details &&
1675
+ typeof details === 'object' &&
1676
+ 'status' in details &&
1677
+ (details.status === 401 || details.status === 403)) {
1678
+ return details.status;
1679
+ }
1680
+ const message = errorMessage(error);
1681
+ const match = /\bHTTP (401|403)\b/.exec(message);
1682
+ if (!match)
1683
+ return undefined;
1684
+ return match[1] === '401' ? 401 : 403;
1685
+ }
1686
+ //# sourceMappingURL=worker-client.js.map