@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,1488 @@
1
+ import type {
2
+ BlobRef,
3
+ SyncAuthLeaseIssueRequest,
4
+ SyncOperation,
5
+ } from '@syncular/core';
6
+ import { issueSyncularAuthLease } from './auth-leases';
7
+ import { assertSyncularBlobPayloadLimit } from './blob-limits';
8
+ import { resolveSyncularClientConfig } from './client-config';
9
+ import {
10
+ appendSyncularDiagnosticEvent,
11
+ appendSyncularSyncTimings,
12
+ createSyncularSyncAttempt,
13
+ summarizeSyncularDiagnosticSubscriptions,
14
+ syncularDiagnosticAttemptFields,
15
+ syncularSyncAttemptHeaders,
16
+ } from './diagnostics';
17
+ import { SyncularClientError, toSyncularClientError } from './errors';
18
+ import { createSyncularRuntimeInfo } from './runtime-contract';
19
+ import { assertSyncularReadonlySql } from './sql-safety';
20
+ import type {
21
+ ResolvedSyncularClientConfig,
22
+ SyncularApplyYjsEnvelopeToPayloadArgs,
23
+ SyncularApplyYjsTextUpdatesArgs,
24
+ SyncularApplyYjsTextUpdatesResult,
25
+ SyncularAuthHeaders,
26
+ SyncularAuthLeaseRecord,
27
+ SyncularBlobCacheStats,
28
+ SyncularBlobEncryptionConfig,
29
+ SyncularBlobLimits,
30
+ SyncularBlobStoreOptions,
31
+ SyncularBlobUploadQueueProcessOptions,
32
+ SyncularBlobUploadQueueStats,
33
+ SyncularBootstrapState,
34
+ SyncularBootstrapStatus,
35
+ SyncularBootstrapSubscriptionPhase,
36
+ SyncularBuildYjsTextUpdateArgs,
37
+ SyncularBuildYjsTextUpdateResult,
38
+ SyncularChangedRow,
39
+ SyncularClientConfig,
40
+ SyncularConflictResolution,
41
+ SyncularConflictSummary,
42
+ SyncularConnectionState,
43
+ SyncularCrdtDocumentSnapshot,
44
+ SyncularCrdtFieldCompactionReceipt,
45
+ SyncularCrdtFieldCompactionRequest,
46
+ SyncularCrdtFieldDescriptor,
47
+ SyncularCrdtFieldMaterialization,
48
+ SyncularCrdtFieldRequest,
49
+ SyncularCrdtFieldTextRequest,
50
+ SyncularCrdtFieldWriteReceipt,
51
+ SyncularCrdtFieldYjsUpdateRequest,
52
+ SyncularCrdtUpdateLogEntry,
53
+ SyncularDiagnosticEvent,
54
+ SyncularDiagnosticSink,
55
+ SyncularDiagnosticSnapshot,
56
+ SyncularEncryptedCrdtConfig,
57
+ SyncularEncryptionHelperMethod,
58
+ SyncularFieldEncryptionConfig,
59
+ SyncularLifecycleState,
60
+ SyncularLiveQueryDependencyHint,
61
+ SyncularLiveQueryDiagnostics,
62
+ SyncularLiveQueryEvent,
63
+ SyncularLiveQuerySnapshot,
64
+ SyncularLocalHealthRepairReport,
65
+ SyncularLocalHealthRepairRequest,
66
+ SyncularLocalHealthReport,
67
+ SyncularLocalSupportBundle,
68
+ SyncularLocalSupportBundleImportReport,
69
+ SyncularLocalSyncResetReport,
70
+ SyncularLocalSyncResetRequest,
71
+ SyncularPullOptions,
72
+ SyncularRowsChangedEvent,
73
+ SyncularRowsChangedSink,
74
+ SyncularRuntimeArtifact,
75
+ SyncularRuntimeInfo,
76
+ SyncularSchemaState,
77
+ SyncularSqlResult,
78
+ SyncularStorageCompactionOptions,
79
+ SyncularStorageCompactionReport,
80
+ SyncularSubscriptionSpec,
81
+ SyncularSyncAttempt,
82
+ SyncularSyncRequestOptions,
83
+ SyncularSyncResult,
84
+ SyncularSyncTimings,
85
+ SyncularTransportStats,
86
+ } from './types';
87
+ import {
88
+ getSyncularWasmGlueUrl,
89
+ getSyncularWasmUrl,
90
+ loadSyncularWasmGlue,
91
+ readSyncularRustRuntimeInfo,
92
+ type SyncularRustOwnedSqliteClient,
93
+ type SyncularWasmGlue,
94
+ } from './wasm-runtime';
95
+
96
+ interface CreateSyncularRustClientOptions {
97
+ module?: SyncularWasmGlue | Promise<SyncularWasmGlue>;
98
+ wasmGlueUrl?: string | URL;
99
+ wasmUrl?: string | URL | Request;
100
+ runtime?: SyncularRuntimeArtifact;
101
+ config: SyncularClientConfig;
102
+ blobLimits?: SyncularBlobLimits;
103
+ }
104
+
105
+ type RawSyncResult = {
106
+ changed_tables?: string[];
107
+ changed_rows?: SyncularChangedRow[];
108
+ changed_rows_truncated?: boolean;
109
+ subscriptions?: Array<{
110
+ id: string;
111
+ table: string;
112
+ status: string;
113
+ scopes: Record<string, string | string[]>;
114
+ next_cursor: number;
115
+ bootstrap_phase?: number;
116
+ bootstrap_state?: RawBootstrapState | null;
117
+ ready?: boolean;
118
+ phase?: SyncularBootstrapSubscriptionPhase;
119
+ progress_percent?: number;
120
+ snapshot_rows?: unknown[];
121
+ commits?: unknown[];
122
+ }>;
123
+ bootstrap?: RawBootstrapStatus;
124
+ pushed_commits?: number;
125
+ timings?: {
126
+ total_ms?: number;
127
+ push_ms?: number;
128
+ pull_ms?: number;
129
+ pull_request_ms?: number;
130
+ sync_pack_decode_ms?: number;
131
+ pull_transform_ms?: number;
132
+ integrity_verify_ms?: number;
133
+ snapshot_fetch_ms?: number;
134
+ pull_apply_ms?: number;
135
+ scope_clear_ms?: number;
136
+ snapshot_row_apply_ms?: number;
137
+ snapshot_artifact_apply_ms?: number;
138
+ snapshot_artifact_checkpoint_ms?: number;
139
+ snapshot_artifact_checkpoint_count?: number;
140
+ snapshot_chunk_apply_ms?: number;
141
+ snapshot_chunk_materialize_ms?: number;
142
+ snapshot_chunk_reset_ms?: number;
143
+ snapshot_chunk_bind_ms?: number;
144
+ snapshot_chunk_step_ms?: number;
145
+ commit_apply_ms?: number;
146
+ subscription_state_ms?: number;
147
+ notify_ms?: number;
148
+ };
149
+ };
150
+
151
+ type RawBootstrapState = {
152
+ asOfCommitSeq?: number;
153
+ tables?: string[];
154
+ tableIndex?: number;
155
+ rowCursor?: string | null;
156
+ };
157
+
158
+ type RawBootstrapStatus = {
159
+ channel_phase?: string;
160
+ progress_percent?: number;
161
+ is_bootstrapping?: boolean;
162
+ critical_ready?: boolean;
163
+ interactive_ready?: boolean;
164
+ complete?: boolean;
165
+ active_phase?: number | null;
166
+ expected_subscription_ids?: string[];
167
+ ready_subscription_ids?: string[];
168
+ pending_subscription_ids?: string[];
169
+ subscriptions?: Array<{
170
+ id: string;
171
+ table: string;
172
+ expected?: boolean;
173
+ ready?: boolean;
174
+ status?: string | null;
175
+ phase?: SyncularBootstrapSubscriptionPhase;
176
+ progress_percent?: number;
177
+ cursor?: number | null;
178
+ bootstrap_state?: RawBootstrapState | null;
179
+ bootstrap_phase?: number;
180
+ }>;
181
+ phases?: Array<{
182
+ phase?: number;
183
+ expected_subscription_ids?: string[];
184
+ ready_subscription_ids?: string[];
185
+ pending_subscription_ids?: string[];
186
+ is_ready?: boolean;
187
+ progress_percent?: number;
188
+ }>;
189
+ };
190
+
191
+ type RawConflictSummary = {
192
+ id: string;
193
+ client_commit_id: string;
194
+ op_index: number;
195
+ result_status: string;
196
+ message: string;
197
+ code: string | null;
198
+ server_version: number | null;
199
+ resolved_at: number | null;
200
+ resolution: string | null;
201
+ };
202
+
203
+ type SyncularBootstrapSubscriptionStatusEntry = {
204
+ id: string;
205
+ table: string;
206
+ status: string | null;
207
+ ready: boolean;
208
+ phase: SyncularBootstrapSubscriptionPhase;
209
+ progressPercent: number;
210
+ cursor: number | null;
211
+ bootstrapState: SyncularBootstrapState | null;
212
+ bootstrapPhase: number;
213
+ };
214
+
215
+ export async function openSyncularRustClient(
216
+ options: CreateSyncularRustClientOptions
217
+ ): Promise<SyncularRustClient> {
218
+ const wasmGlueUrl =
219
+ options.runtime?.wasmGlueUrl ??
220
+ options.wasmGlueUrl ??
221
+ getSyncularWasmGlueUrl();
222
+ const mod = await (options.module ??
223
+ (options.runtime?.wasmGlueUrl || options.wasmGlueUrl
224
+ ? loadSyncularWasmGlueFromUrl(wasmGlueUrl)
225
+ : loadSyncularWasmGlue()));
226
+ const wasmUrl =
227
+ options.runtime?.wasmUrl ?? options.wasmUrl ?? getSyncularWasmUrl();
228
+ const config = resolveSyncularClientConfig(options.config);
229
+ await mod.default(wasmUrl);
230
+ const rustRuntimeInfo = readSyncularRustRuntimeInfo(mod);
231
+ return new SyncularRustClient(
232
+ await mod.openSyncularRustOwnedSqliteClient(config),
233
+ createSyncularRuntimeInfo({
234
+ storage: config.storage,
235
+ wasmGlueUrl,
236
+ wasmUrl,
237
+ rust: rustRuntimeInfo,
238
+ }),
239
+ config,
240
+ config.pull,
241
+ options.blobLimits
242
+ );
243
+ }
244
+
245
+ function loadSyncularWasmGlueFromUrl(
246
+ wasmGlueUrl: string | URL
247
+ ): Promise<SyncularWasmGlue> {
248
+ const href = wasmGlueUrl instanceof URL ? wasmGlueUrl.href : wasmGlueUrl;
249
+ return import(/* @vite-ignore */ href) as Promise<SyncularWasmGlue>;
250
+ }
251
+
252
+ export class SyncularRustClient {
253
+ #rowsChangedListeners = new Set<SyncularRowsChangedSink>();
254
+ #diagnosticListeners = new Set<SyncularDiagnosticSink>();
255
+ #recentDiagnostics: SyncularDiagnosticEvent[] = [];
256
+ #recentSyncTimings: SyncularSyncTimings[] = [];
257
+ #subscriptions: SyncularSubscriptionSpec[] = [];
258
+ #authHeaders: SyncularAuthHeaders = {};
259
+ #bootstrapById = new Map<string, SyncularBootstrapSubscriptionStatusEntry>();
260
+ #closed = false;
261
+
262
+ constructor(
263
+ private readonly raw: SyncularRustOwnedSqliteClient,
264
+ private readonly runtime: SyncularRuntimeInfo,
265
+ private readonly config: ResolvedSyncularClientConfig,
266
+ private readonly pullOptions: SyncularPullOptions | undefined,
267
+ private readonly blobLimits: SyncularBlobLimits | undefined
268
+ ) {}
269
+
270
+ #assertRuntimeFeature(feature: 'crdt-yjs' | 'e2ee', apiName: string): void {
271
+ if (this.runtime.rust?.features.includes(feature)) return;
272
+ throw new Error(
273
+ `Syncular runtime feature "${feature}" is required for ${apiName}. ` +
274
+ 'Select the full browser WASM runtime artifact.'
275
+ );
276
+ }
277
+
278
+ setSubscriptions(subscriptions: readonly SyncularSubscriptionSpec[]): void {
279
+ this.#subscriptions = [...subscriptions];
280
+ this.#bootstrapById.clear();
281
+ this.raw.setSubscriptionsJson(JSON.stringify(subscriptions));
282
+ this.#emitDiagnostic({
283
+ at: Date.now(),
284
+ level: 'info',
285
+ source: 'client',
286
+ code: 'client.subscriptions.updated',
287
+ message: 'Syncular subscriptions updated',
288
+ details: { subscriptionCount: subscriptions.length },
289
+ });
290
+ }
291
+
292
+ async forceSubscriptionsBootstrap(
293
+ subscriptionIds: readonly string[] = []
294
+ ): Promise<number> {
295
+ const count = parseJson<number>(
296
+ await this.raw.forceSubscriptionsBootstrapJson(
297
+ JSON.stringify(subscriptionIds)
298
+ )
299
+ );
300
+ for (const id of subscriptionIds.length > 0
301
+ ? subscriptionIds
302
+ : this.#subscriptions.map((subscription) => subscription.id)) {
303
+ this.#bootstrapById.delete(id);
304
+ }
305
+ return count;
306
+ }
307
+
308
+ setAuthHeaders(headers: SyncularAuthHeaders): void {
309
+ this.#authHeaders = { ...headers };
310
+ this.raw.setAuthHeadersJson(JSON.stringify(this.#authHeaders));
311
+ }
312
+
313
+ async issueAuthLease(
314
+ request: SyncAuthLeaseIssueRequest
315
+ ): Promise<SyncularAuthLeaseRecord> {
316
+ const lease = await issueSyncularAuthLease({
317
+ baseUrl: this.config.baseUrl,
318
+ headers: this.#authHeaders,
319
+ request,
320
+ appSchema: this.config.appSchema,
321
+ });
322
+ await this.upsertAuthLease(lease);
323
+ this.#emitDiagnostic({
324
+ at: Date.now(),
325
+ level: 'info',
326
+ source: 'auth',
327
+ code: 'auth_lease.issued',
328
+ message: 'Syncular auth lease issued and stored',
329
+ details: {
330
+ leaseId: lease.leaseId,
331
+ expiresAtMs: lease.expiresAtMs,
332
+ schemaVersion: lease.schemaVersion,
333
+ },
334
+ });
335
+ return lease;
336
+ }
337
+
338
+ async upsertAuthLease(lease: SyncularAuthLeaseRecord): Promise<void> {
339
+ this.raw.upsertAuthLeaseJson(JSON.stringify(lease));
340
+ }
341
+
342
+ async authLease(leaseId: string): Promise<SyncularAuthLeaseRecord | null> {
343
+ return parseJson<SyncularAuthLeaseRecord | null>(
344
+ this.raw.authLeaseJson(leaseId)
345
+ );
346
+ }
347
+
348
+ async activeAuthLeases(
349
+ actorId?: string | null,
350
+ nowMs = Date.now()
351
+ ): Promise<SyncularAuthLeaseRecord[]> {
352
+ return parseJson<SyncularAuthLeaseRecord[]>(
353
+ this.raw.activeAuthLeasesJson(actorId ?? null, BigInt(Math.trunc(nowMs)))
354
+ );
355
+ }
356
+
357
+ setFieldEncryption(config: SyncularFieldEncryptionConfig | null): void {
358
+ if (config == null && !this.runtime.rust?.features.includes('e2ee')) return;
359
+ this.#assertRuntimeFeature('e2ee', 'setFieldEncryption');
360
+ this.raw.setFieldEncryptionJson(
361
+ config == null
362
+ ? 'null'
363
+ : JSON.stringify(normalizeFieldEncryptionConfig(config))
364
+ );
365
+ }
366
+
367
+ setEncryptedCrdt(config: SyncularEncryptedCrdtConfig | null): void {
368
+ if (config == null && !this.runtime.rust?.features.includes('e2ee')) return;
369
+ this.#assertRuntimeFeature('e2ee', 'setEncryptedCrdt');
370
+ this.raw.setEncryptedCrdtJson(
371
+ config == null
372
+ ? 'null'
373
+ : JSON.stringify(normalizeEncryptedCrdtConfig(config))
374
+ );
375
+ }
376
+
377
+ setBlobEncryption(config: SyncularBlobEncryptionConfig | null): void {
378
+ if (config == null && !this.runtime.rust?.features.includes('e2ee')) return;
379
+ this.#assertRuntimeFeature('e2ee', 'setBlobEncryption');
380
+ this.raw.setBlobEncryptionJson(
381
+ config == null
382
+ ? 'null'
383
+ : JSON.stringify(normalizeBlobEncryptionConfig(config))
384
+ );
385
+ }
386
+
387
+ setAbortSignal(signal?: AbortSignal | null): void {
388
+ this.raw.setAbortSignal(signal ?? null);
389
+ }
390
+
391
+ async applyMutation(
392
+ operation: SyncOperation,
393
+ localRow?: unknown
394
+ ): Promise<string> {
395
+ const commitId = await this.raw.applyMutationJson(
396
+ JSON.stringify(operation),
397
+ localRow == null ? null : JSON.stringify(localRow)
398
+ );
399
+ this.#drainAndEmitRowsChanged();
400
+ return commitId;
401
+ }
402
+
403
+ async applyLeasedMutation(
404
+ operation: SyncOperation,
405
+ localRow?: unknown
406
+ ): Promise<string> {
407
+ const commitId = await this.raw.applyLeasedMutationJson(
408
+ JSON.stringify(operation),
409
+ localRow == null ? null : JSON.stringify(localRow)
410
+ );
411
+ this.#drainAndEmitRowsChanged();
412
+ return commitId;
413
+ }
414
+
415
+ async applyMutationsBatch(
416
+ operations: Array<{ operation: SyncOperation; localRow?: unknown | null }>
417
+ ): Promise<string[]> {
418
+ const commitIds = parseJson<string[]>(
419
+ await this.raw.applyMutationsBatchJson(JSON.stringify(operations))
420
+ );
421
+ this.#drainAndEmitRowsChanged();
422
+ return commitIds;
423
+ }
424
+
425
+ async applyMutationsCommit(
426
+ operations: Array<{ operation: SyncOperation; localRow?: unknown | null }>
427
+ ): Promise<string> {
428
+ const commitId = parseJson<string>(
429
+ await this.raw.applyMutationsCommitJson(JSON.stringify(operations))
430
+ );
431
+ this.#drainAndEmitRowsChanged();
432
+ return commitId;
433
+ }
434
+
435
+ async applyLeasedMutationsCommit(
436
+ operations: Array<{ operation: SyncOperation; localRow?: unknown | null }>
437
+ ): Promise<string> {
438
+ const commitId = parseJson<string>(
439
+ await this.raw.applyLeasedMutationsCommitJson(JSON.stringify(operations))
440
+ );
441
+ this.#drainAndEmitRowsChanged();
442
+ return commitId;
443
+ }
444
+
445
+ async syncPull(
446
+ options: SyncularSyncRequestOptions = {}
447
+ ): Promise<SyncularSyncResult> {
448
+ const result = await this.#runTracedSync(
449
+ 'syncPull',
450
+ options.syncAttempt,
451
+ async () =>
452
+ this.#decorateSyncResult(parseSyncResult(await this.raw.syncPullJson()))
453
+ );
454
+ this.#captureSyncTimings(result);
455
+ this.#emitRowsChanged('remotePull', result);
456
+ return result;
457
+ }
458
+
459
+ async applyRealtimeSyncPack(bytes: Uint8Array): Promise<SyncularSyncResult> {
460
+ const result = this.#decorateSyncResult(
461
+ parseSyncResult(await this.raw.applyRealtimeSyncPackBytes(bytes))
462
+ );
463
+ this.#captureSyncTimings(result);
464
+ this.#emitRowsChanged('remotePull', result);
465
+ return result;
466
+ }
467
+
468
+ async syncPush(
469
+ options: SyncularSyncRequestOptions = {}
470
+ ): Promise<SyncularSyncResult> {
471
+ try {
472
+ const result = await this.#runTracedSync(
473
+ 'syncPush',
474
+ options.syncAttempt,
475
+ async () =>
476
+ this.#decorateSyncResult(
477
+ parseSyncResult(await this.raw.syncPushJson())
478
+ )
479
+ );
480
+ this.#captureSyncTimings(result);
481
+ this.#emitRowsChanged('localWrite', result);
482
+ return result;
483
+ } catch (error) {
484
+ this.raw.recoverSyncPushErrorJson(String(error));
485
+ throw error;
486
+ }
487
+ }
488
+
489
+ async syncOnce(
490
+ options: SyncularSyncRequestOptions = {}
491
+ ): Promise<SyncularSyncResult> {
492
+ try {
493
+ const result = await this.#runTracedSync(
494
+ 'syncOnce',
495
+ options.syncAttempt,
496
+ async () =>
497
+ this.#decorateSyncResult(
498
+ parseSyncResult(await this.raw.syncOnceJson())
499
+ )
500
+ );
501
+ this.#captureSyncTimings(result);
502
+ this.#emitRowsChanged('remotePull', result);
503
+ return result;
504
+ } catch (error) {
505
+ this.raw.recoverSyncPushErrorJson(String(error));
506
+ throw error;
507
+ }
508
+ }
509
+
510
+ resumeFromBackground(
511
+ options: SyncularSyncRequestOptions = {}
512
+ ): Promise<SyncularSyncResult> {
513
+ return this.syncOnce(options);
514
+ }
515
+
516
+ transportStats(): SyncularTransportStats {
517
+ return parseJson(this.raw.transportStatsJson());
518
+ }
519
+
520
+ resetTransportStats(): void {
521
+ this.raw.resetTransportStats();
522
+ }
523
+
524
+ async conflictSummaries(): Promise<SyncularConflictSummary[]> {
525
+ return parseConflictSummaries(await this.raw.conflictSummariesJson());
526
+ }
527
+
528
+ retryConflictKeepLocal(id: string): Promise<string> {
529
+ return this.raw.retryConflictKeepLocal(id);
530
+ }
531
+
532
+ resolveConflict(
533
+ id: string,
534
+ resolution: SyncularConflictResolution
535
+ ): Promise<void> {
536
+ return this.raw.resolveConflict(id, resolution);
537
+ }
538
+
539
+ executeSql<Row extends Record<string, unknown> = Record<string, unknown>>(
540
+ sql: string,
541
+ params: readonly unknown[] = []
542
+ ): SyncularSqlResult<Row> {
543
+ assertSyncularReadonlySql(sql);
544
+ return this.raw.executeSqlValue(sql, params) as SyncularSqlResult<Row>;
545
+ }
546
+
547
+ executeUnsafeSql<
548
+ Row extends Record<string, unknown> = Record<string, unknown>,
549
+ >(sql: string, params: readonly unknown[] = []): SyncularSqlResult<Row> {
550
+ return this.raw.executeUnsafeSqlValue(
551
+ sql,
552
+ params
553
+ ) as SyncularSqlResult<Row>;
554
+ }
555
+
556
+ subscribeQuery<Row extends Record<string, unknown> = Record<string, unknown>>(
557
+ sql: string,
558
+ params: readonly unknown[],
559
+ tables: readonly string[],
560
+ hints: readonly SyncularLiveQueryDependencyHint[] = []
561
+ ): SyncularLiveQuerySnapshot<Row> {
562
+ return parseJson(
563
+ this.raw.subscribeQueryJson(
564
+ sql,
565
+ stringifyParams(params),
566
+ JSON.stringify(tables),
567
+ JSON.stringify(hints)
568
+ )
569
+ );
570
+ }
571
+
572
+ unsubscribeQuery(id: string): void {
573
+ this.raw.unsubscribeQuery(id);
574
+ }
575
+
576
+ drainLiveQueryEvents<
577
+ Row extends Record<string, unknown> = Record<string, unknown>,
578
+ >(): Array<SyncularLiveQueryEvent<Row>> {
579
+ return parseJson(this.raw.drainLiveQueryEventsJson());
580
+ }
581
+
582
+ liveQueryDiagnostics(): SyncularLiveQueryDiagnostics {
583
+ return parseJson(this.raw.liveQueryDiagnosticsJson());
584
+ }
585
+
586
+ addRowsChangedListener(listener: SyncularRowsChangedSink): () => void {
587
+ this.#rowsChangedListeners.add(listener);
588
+ return () => {
589
+ this.#rowsChangedListeners.delete(listener);
590
+ };
591
+ }
592
+
593
+ async listTable<
594
+ Row extends Record<string, unknown> = Record<string, unknown>,
595
+ >(table: string): Promise<Row[]> {
596
+ return parseJson(await this.raw.listTableJson(table));
597
+ }
598
+
599
+ async storeBlob(
600
+ data: Uint8Array,
601
+ options: SyncularBlobStoreOptions = {}
602
+ ): Promise<BlobRef> {
603
+ assertSyncularBlobPayloadLimit({
604
+ operation: 'store',
605
+ size: data.byteLength,
606
+ limits: this.blobLimits,
607
+ options,
608
+ diagnostics: (event) => this.#emitDiagnostic(event),
609
+ });
610
+ return parseJson(
611
+ await this.raw.storeBlobJson(data, JSON.stringify(options))
612
+ );
613
+ }
614
+
615
+ async retrieveBlob(ref: BlobRef): Promise<Uint8Array> {
616
+ assertSyncularBlobPayloadLimit({
617
+ operation: 'retrieve',
618
+ size: ref.size,
619
+ limits: this.blobLimits,
620
+ refHash: ref.hash,
621
+ diagnostics: (event) => this.#emitDiagnostic(event),
622
+ });
623
+ const wasLocal = this.#hasDiagnosticListeners()
624
+ ? this.raw.isBlobLocal(ref.hash)
625
+ : undefined;
626
+ try {
627
+ const bytes = await this.raw.retrieveBlob(JSON.stringify(ref));
628
+ if (wasLocal !== undefined) {
629
+ this.#emitDiagnostic({
630
+ at: Date.now(),
631
+ level: 'info',
632
+ source: 'blob',
633
+ code: wasLocal ? 'blob.cache_hit' : 'blob.cache_miss',
634
+ message: wasLocal
635
+ ? 'Syncular blob served from local cache'
636
+ : 'Syncular blob fetched after local cache miss',
637
+ details: {
638
+ hash: ref.hash,
639
+ size: ref.size,
640
+ mimeType: ref.mimeType,
641
+ encrypted: ref.encrypted === true,
642
+ ...(ref.keyId ? { keyId: ref.keyId } : {}),
643
+ },
644
+ });
645
+ }
646
+ return bytes;
647
+ } catch (error) {
648
+ if (this.#hasDiagnosticListeners()) {
649
+ this.#emitDiagnostic({
650
+ at: Date.now(),
651
+ level: 'warn',
652
+ source: 'blob',
653
+ code: 'blob.download_failed',
654
+ message: `Syncular blob download failed: ${String(error)}`,
655
+ details: {
656
+ hash: ref.hash,
657
+ size: ref.size,
658
+ mimeType: ref.mimeType,
659
+ encrypted: ref.encrypted === true,
660
+ ...(ref.keyId ? { keyId: ref.keyId } : {}),
661
+ },
662
+ });
663
+ }
664
+ throw error;
665
+ }
666
+ }
667
+
668
+ isBlobLocal(hash: string): boolean {
669
+ const local = this.raw.isBlobLocal(hash);
670
+ if (this.#hasDiagnosticListeners()) {
671
+ this.#emitDiagnostic({
672
+ at: Date.now(),
673
+ level: 'debug',
674
+ source: 'blob',
675
+ code: local ? 'blob.cache_hit' : 'blob.cache_miss',
676
+ message: local
677
+ ? 'Syncular blob is available in local cache'
678
+ : 'Syncular blob is not available in local cache',
679
+ details: { hash },
680
+ });
681
+ }
682
+ return local;
683
+ }
684
+
685
+ async processBlobUploadQueue(
686
+ options?: SyncularBlobUploadQueueProcessOptions
687
+ ): Promise<{
688
+ uploaded: number;
689
+ failed: number;
690
+ }> {
691
+ const result = parseJson<{ uploaded: number; failed: number }>(
692
+ await this.raw.processBlobUploadQueueJson(options?.retryNow === true)
693
+ );
694
+ if (this.#hasDiagnosticListeners()) {
695
+ this.#emitDiagnostic({
696
+ at: Date.now(),
697
+ level: result.failed > 0 ? 'warn' : 'info',
698
+ source: 'blob',
699
+ code: 'blob.upload_queue_processed',
700
+ message: 'Syncular blob upload queue processed',
701
+ details: result,
702
+ });
703
+ }
704
+ return result;
705
+ }
706
+
707
+ blobUploadQueueStats(): SyncularBlobUploadQueueStats {
708
+ return parseJson(this.raw.blobUploadQueueStatsJson());
709
+ }
710
+
711
+ blobCacheStats(): SyncularBlobCacheStats {
712
+ return parseJson(this.raw.blobCacheStatsJson());
713
+ }
714
+
715
+ pruneBlobCache(maxBytes = 0): number {
716
+ const prunedBytes = Number(this.raw.pruneBlobCache(BigInt(maxBytes)));
717
+ if (this.#hasDiagnosticListeners()) {
718
+ this.#emitDiagnostic({
719
+ at: Date.now(),
720
+ level: 'info',
721
+ source: 'blob',
722
+ code: 'blob.cache_pruned',
723
+ message: 'Syncular blob cache pruned',
724
+ details: { prunedBytes, maxBytes },
725
+ });
726
+ }
727
+ return prunedBytes;
728
+ }
729
+
730
+ clearBlobCache(): void {
731
+ this.raw.clearBlobCache();
732
+ if (this.#hasDiagnosticListeners()) {
733
+ this.#emitDiagnostic({
734
+ at: Date.now(),
735
+ level: 'info',
736
+ source: 'blob',
737
+ code: 'blob.cache_cleared',
738
+ message: 'Syncular blob cache cleared',
739
+ });
740
+ }
741
+ }
742
+
743
+ compactStorage(
744
+ options: SyncularStorageCompactionOptions = {}
745
+ ): SyncularStorageCompactionReport {
746
+ return parseJson(this.raw.compactStorageJson(JSON.stringify(options)));
747
+ }
748
+
749
+ generatedSchemaState(): SyncularSchemaState {
750
+ return parseJson(this.raw.generatedSchemaStateJson());
751
+ }
752
+
753
+ async localHealthCheck(): Promise<SyncularLocalHealthReport> {
754
+ return parseJson(await this.raw.localHealthCheckJson());
755
+ }
756
+
757
+ async exportLocalSupportBundle(): Promise<SyncularLocalSupportBundle> {
758
+ return parseJson(await this.raw.exportLocalSupportBundleJson());
759
+ }
760
+
761
+ async importLocalSupportBundle(
762
+ bundle: SyncularLocalSupportBundle | string
763
+ ): Promise<SyncularLocalSupportBundleImportReport> {
764
+ const bundleJson =
765
+ typeof bundle === 'string' ? bundle : JSON.stringify(bundle);
766
+ return parseJson(await this.raw.importLocalSupportBundleJson(bundleJson));
767
+ }
768
+
769
+ async repairLocalHealth(
770
+ request: SyncularLocalHealthRepairRequest
771
+ ): Promise<SyncularLocalHealthRepairReport> {
772
+ const normalized = {
773
+ action: request.action,
774
+ subscriptionIds: [...(request.subscriptionIds ?? [])],
775
+ tables: [...(request.tables ?? [])],
776
+ };
777
+ return parseJson(
778
+ await this.raw.repairLocalHealthJson(JSON.stringify(normalized))
779
+ );
780
+ }
781
+
782
+ async resetLocalSyncState(
783
+ request: SyncularLocalSyncResetRequest = {}
784
+ ): Promise<SyncularLocalSyncResetReport> {
785
+ const normalized = {
786
+ subscriptionIds: [...(request.subscriptionIds ?? [])],
787
+ clearSyncedRows: request.clearSyncedRows === true,
788
+ };
789
+ const result = parseJson<SyncularLocalSyncResetReport>(
790
+ await this.raw.resetLocalSyncStateJson(JSON.stringify(normalized))
791
+ );
792
+ for (const id of normalized.subscriptionIds.length > 0
793
+ ? normalized.subscriptionIds
794
+ : this.#subscriptions.map((subscription) => subscription.id)) {
795
+ this.#bootstrapById.delete(id);
796
+ }
797
+ return result;
798
+ }
799
+
800
+ buildYjsTextUpdate(
801
+ args: SyncularBuildYjsTextUpdateArgs
802
+ ): SyncularBuildYjsTextUpdateResult {
803
+ this.#assertRuntimeFeature('crdt-yjs', 'buildYjsTextUpdate');
804
+ return parseJson(this.raw.buildYjsTextUpdateJson(JSON.stringify(args)));
805
+ }
806
+
807
+ applyYjsTextUpdates(
808
+ args: SyncularApplyYjsTextUpdatesArgs
809
+ ): SyncularApplyYjsTextUpdatesResult {
810
+ this.#assertRuntimeFeature('crdt-yjs', 'applyYjsTextUpdates');
811
+ return parseJson(this.raw.applyYjsTextUpdatesJson(JSON.stringify(args)));
812
+ }
813
+
814
+ applyYjsEnvelopeToPayload(
815
+ args: SyncularApplyYjsEnvelopeToPayloadArgs
816
+ ): Record<string, unknown> {
817
+ this.#assertRuntimeFeature('crdt-yjs', 'applyYjsEnvelopeToPayload');
818
+ const result = parseJson<{ payload: Record<string, unknown> }>(
819
+ this.raw.applyYjsEnvelopeToPayloadJson(JSON.stringify(args))
820
+ );
821
+ return result.payload;
822
+ }
823
+
824
+ async openCrdtField(
825
+ request: SyncularCrdtFieldRequest
826
+ ): Promise<SyncularCrdtFieldDescriptor> {
827
+ this.#assertRuntimeFeature('crdt-yjs', 'openCrdtField');
828
+ return parseJson(this.raw.openCrdtFieldJson(JSON.stringify(request)));
829
+ }
830
+
831
+ async applyCrdtFieldText(
832
+ request: SyncularCrdtFieldTextRequest
833
+ ): Promise<SyncularCrdtFieldWriteReceipt> {
834
+ this.#assertRuntimeFeature('crdt-yjs', 'applyCrdtFieldText');
835
+ const receipt = parseJson<SyncularCrdtFieldWriteReceipt>(
836
+ this.raw.applyCrdtFieldTextJson(JSON.stringify(request))
837
+ );
838
+ this.#drainAndEmitRowsChanged();
839
+ return receipt;
840
+ }
841
+
842
+ async applyCrdtFieldYjsUpdate(
843
+ request: SyncularCrdtFieldYjsUpdateRequest
844
+ ): Promise<SyncularCrdtFieldWriteReceipt> {
845
+ this.#assertRuntimeFeature('crdt-yjs', 'applyCrdtFieldYjsUpdate');
846
+ const receipt = parseJson<SyncularCrdtFieldWriteReceipt>(
847
+ this.raw.applyCrdtFieldYjsUpdateJson(JSON.stringify(request))
848
+ );
849
+ this.#drainAndEmitRowsChanged();
850
+ return receipt;
851
+ }
852
+
853
+ async materializeCrdtField(
854
+ request: SyncularCrdtFieldRequest
855
+ ): Promise<SyncularCrdtFieldMaterialization> {
856
+ this.#assertRuntimeFeature('crdt-yjs', 'materializeCrdtField');
857
+ return parseJson(
858
+ this.raw.materializeCrdtFieldJson(JSON.stringify(request))
859
+ );
860
+ }
861
+
862
+ async crdtDocumentSnapshot(
863
+ request: SyncularCrdtFieldRequest
864
+ ): Promise<SyncularCrdtDocumentSnapshot> {
865
+ this.#assertRuntimeFeature('crdt-yjs', 'crdtDocumentSnapshot');
866
+ return parseJson(
867
+ this.raw.crdtDocumentSnapshotJson(JSON.stringify(request))
868
+ );
869
+ }
870
+
871
+ async crdtUpdateLog(
872
+ request: SyncularCrdtFieldRequest & { limit?: number }
873
+ ): Promise<SyncularCrdtUpdateLogEntry[]> {
874
+ this.#assertRuntimeFeature('crdt-yjs', 'crdtUpdateLog');
875
+ return parseJson(this.raw.crdtUpdateLogJson(JSON.stringify(request)));
876
+ }
877
+
878
+ async snapshotCrdtFieldStateVector(
879
+ request: SyncularCrdtFieldRequest
880
+ ): Promise<{ stateVectorBase64: string }> {
881
+ this.#assertRuntimeFeature('crdt-yjs', 'snapshotCrdtFieldStateVector');
882
+ return parseJson(
883
+ this.raw.snapshotCrdtFieldStateVectorJson(JSON.stringify(request))
884
+ );
885
+ }
886
+
887
+ async compactCrdtField(
888
+ request: SyncularCrdtFieldCompactionRequest
889
+ ): Promise<SyncularCrdtFieldCompactionReceipt> {
890
+ this.#assertRuntimeFeature('crdt-yjs', 'compactCrdtField');
891
+ const receipt = parseJson<SyncularCrdtFieldCompactionReceipt>(
892
+ this.raw.compactCrdtFieldJson(JSON.stringify(request))
893
+ );
894
+ this.#drainAndEmitRowsChanged();
895
+ return receipt;
896
+ }
897
+
898
+ encryptionHelper<T = unknown>(
899
+ method: SyncularEncryptionHelperMethod,
900
+ args: unknown = {}
901
+ ): T {
902
+ this.#assertRuntimeFeature('e2ee', 'encryptionHelper');
903
+ return parseJson(
904
+ this.raw.encryptionHelperJson(method, JSON.stringify(args))
905
+ );
906
+ }
907
+
908
+ async runtimeInfo(): Promise<SyncularRuntimeInfo> {
909
+ return this.runtime;
910
+ }
911
+
912
+ connectionState(): SyncularConnectionState {
913
+ const lastDiagnostic =
914
+ this.#recentDiagnostics[this.#recentDiagnostics.length - 1];
915
+ return {
916
+ closed: this.#closed,
917
+ pendingRequests: 0,
918
+ realtime: 'disconnected',
919
+ ...(lastDiagnostic ? { lastDiagnostic } : {}),
920
+ };
921
+ }
922
+
923
+ lifecycleState(): SyncularLifecycleState {
924
+ const bootstrap = buildBootstrapStatus(
925
+ this.#subscriptions,
926
+ this.#bootstrapById,
927
+ this.pullOptions
928
+ );
929
+ const lastDiagnostic =
930
+ this.#recentDiagnostics[this.#recentDiagnostics.length - 1];
931
+ return {
932
+ phase: this.#closed
933
+ ? 'closed'
934
+ : bootstrap.complete
935
+ ? 'complete'
936
+ : 'offline',
937
+ realtime: 'disconnected',
938
+ online: false,
939
+ requiresAction: false,
940
+ pendingRequests: 0,
941
+ bootstrap: {
942
+ complete: bootstrap.complete,
943
+ criticalReady: bootstrap.criticalReady,
944
+ interactiveReady: bootstrap.interactiveReady,
945
+ isBootstrapping: bootstrap.isBootstrapping,
946
+ progressPercent: bootstrap.progressPercent,
947
+ },
948
+ ...(lastDiagnostic ? { lastDiagnostic } : {}),
949
+ };
950
+ }
951
+
952
+ async diagnosticSnapshot(): Promise<SyncularDiagnosticSnapshot> {
953
+ const bootstrap = buildBootstrapStatus(
954
+ this.#subscriptions,
955
+ this.#bootstrapById,
956
+ this.pullOptions
957
+ );
958
+ return {
959
+ generatedAt: Date.now(),
960
+ runtime: this.runtime,
961
+ connection: this.connectionState(),
962
+ subscriptions: summarizeSyncularDiagnosticSubscriptions(
963
+ this.#subscriptions,
964
+ bootstrap
965
+ ),
966
+ recentDiagnostics: [...this.#recentDiagnostics],
967
+ recentSyncTimings: [...this.#recentSyncTimings],
968
+ bootstrap,
969
+ transportStats: this.transportStats(),
970
+ };
971
+ }
972
+
973
+ addDiagnosticListener(listener: SyncularDiagnosticSink): () => void {
974
+ this.#diagnosticListeners.add(listener);
975
+ return () => {
976
+ this.#diagnosticListeners.delete(listener);
977
+ };
978
+ }
979
+
980
+ #decorateSyncResult(result: SyncularSyncResult): SyncularSyncResult {
981
+ for (const subscription of result.subscriptions) {
982
+ this.#bootstrapById.set(subscription.id, {
983
+ id: subscription.id,
984
+ table: subscription.table,
985
+ status: subscription.status,
986
+ ready: subscription.ready,
987
+ phase: subscription.phase,
988
+ progressPercent: subscription.progressPercent,
989
+ cursor: subscription.nextCursor,
990
+ bootstrapState: subscription.bootstrapState,
991
+ bootstrapPhase: subscription.bootstrapPhase,
992
+ });
993
+ }
994
+ return {
995
+ ...result,
996
+ bootstrap: buildBootstrapStatus(
997
+ this.#subscriptions,
998
+ this.#bootstrapById,
999
+ this.pullOptions
1000
+ ),
1001
+ };
1002
+ }
1003
+
1004
+ #emitRowsChanged(
1005
+ source: 'localWrite' | 'remotePull',
1006
+ result: SyncularSyncResult
1007
+ ): void {
1008
+ if (
1009
+ this.#rowsChangedListeners.size === 0 ||
1010
+ (result.changedTables.length === 0 && result.changedRows.length === 0)
1011
+ ) {
1012
+ return;
1013
+ }
1014
+ for (const listener of this.#rowsChangedListeners) {
1015
+ try {
1016
+ listener({
1017
+ source,
1018
+ changedTables: result.changedTables,
1019
+ changedRows: result.changedRows,
1020
+ changedRowsTruncated: result.changedRowsTruncated,
1021
+ });
1022
+ } catch {
1023
+ // Row-change listeners must never break client control flow.
1024
+ }
1025
+ }
1026
+ }
1027
+
1028
+ #captureSyncTimings(result: SyncularSyncResult): void {
1029
+ appendSyncularSyncTimings(this.#recentSyncTimings, result.timings);
1030
+ }
1031
+
1032
+ async #runTracedSync<T>(
1033
+ requestType: 'syncPull' | 'syncPush' | 'syncOnce',
1034
+ providedAttempt: SyncularSyncAttempt | undefined,
1035
+ run: () => Promise<T>
1036
+ ): Promise<T> {
1037
+ const syncAttempt = providedAttempt ?? createSyncularSyncAttempt();
1038
+ const startedAt = Date.now();
1039
+ this.#emitDiagnostic({
1040
+ at: startedAt,
1041
+ level: 'debug',
1042
+ source: 'sync',
1043
+ code: `sync.${requestType}.started`,
1044
+ message: `Syncular ${requestType} started`,
1045
+ ...syncularDiagnosticAttemptFields(syncAttempt),
1046
+ });
1047
+ this.raw.setAuthHeadersJson(
1048
+ JSON.stringify({
1049
+ ...this.#authHeaders,
1050
+ ...syncularSyncAttemptHeaders(syncAttempt),
1051
+ })
1052
+ );
1053
+ try {
1054
+ const result = await run();
1055
+ this.#emitDiagnostic({
1056
+ at: Date.now(),
1057
+ level: 'info',
1058
+ source: 'sync',
1059
+ code: `sync.${requestType}.completed`,
1060
+ message: `Syncular ${requestType} completed`,
1061
+ ...syncularDiagnosticAttemptFields(syncAttempt),
1062
+ details: { durationMs: Date.now() - startedAt },
1063
+ });
1064
+ if (isSyncularSyncResult(result)) {
1065
+ this.#emitScopeRevokedDiagnostic(requestType, result, syncAttempt);
1066
+ }
1067
+ return result;
1068
+ } catch (error) {
1069
+ const classifiedError = toSyncularClientError(error);
1070
+ const classified =
1071
+ classifiedError instanceof SyncularClientError ? classifiedError : null;
1072
+ this.#emitDiagnostic({
1073
+ at: Date.now(),
1074
+ level: 'warn',
1075
+ source: 'sync',
1076
+ code: classified?.code ?? `sync.${requestType}.failed`,
1077
+ message: `Syncular ${requestType} failed`,
1078
+ ...syncularDiagnosticAttemptFields(syncAttempt),
1079
+ details: {
1080
+ durationMs: Date.now() - startedAt,
1081
+ error:
1082
+ classifiedError instanceof Error
1083
+ ? classifiedError.message
1084
+ : String(classifiedError),
1085
+ ...(classified
1086
+ ? {
1087
+ errorCode: classified.code,
1088
+ category: classified.category,
1089
+ retryable: classified.retryable,
1090
+ recommendedAction: classified.recommendedAction,
1091
+ ...(classified.details ?? {}),
1092
+ }
1093
+ : {}),
1094
+ },
1095
+ });
1096
+ throw classifiedError;
1097
+ } finally {
1098
+ this.raw.setAuthHeadersJson(JSON.stringify(this.#authHeaders));
1099
+ }
1100
+ }
1101
+
1102
+ #emitDiagnostic(event: SyncularDiagnosticEvent): void {
1103
+ appendSyncularDiagnosticEvent(this.#recentDiagnostics, event);
1104
+ if (this.#diagnosticListeners.size === 0) return;
1105
+ for (const listener of this.#diagnosticListeners) {
1106
+ try {
1107
+ listener(event);
1108
+ } catch {
1109
+ // Diagnostics must never break client control flow.
1110
+ }
1111
+ }
1112
+ }
1113
+
1114
+ #hasDiagnosticListeners(): boolean {
1115
+ return this.#diagnosticListeners.size > 0;
1116
+ }
1117
+
1118
+ #drainAndEmitRowsChanged(): void {
1119
+ let events: SyncularRowsChangedEvent[];
1120
+ try {
1121
+ events = parseJson(this.raw.drainRowsChangedEventsJson());
1122
+ } catch {
1123
+ return;
1124
+ }
1125
+ if (this.#rowsChangedListeners.size === 0) return;
1126
+ for (const event of events) {
1127
+ if (event.changedTables.length === 0 && event.changedRows.length === 0) {
1128
+ continue;
1129
+ }
1130
+ for (const listener of this.#rowsChangedListeners) {
1131
+ try {
1132
+ listener(event);
1133
+ } catch {
1134
+ // Row-change listeners must never break client control flow.
1135
+ }
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ close(): void {
1141
+ this.#closed = true;
1142
+ this.raw.close();
1143
+ }
1144
+
1145
+ #emitScopeRevokedDiagnostic(
1146
+ requestType: 'syncPull' | 'syncPush' | 'syncOnce',
1147
+ result: SyncularSyncResult,
1148
+ syncAttempt: SyncularSyncAttempt
1149
+ ): void {
1150
+ const revokedSubscriptionIds = result.subscriptions
1151
+ .filter((subscription) => subscription.status === 'revoked')
1152
+ .map((subscription) => subscription.id);
1153
+ if (revokedSubscriptionIds.length === 0) return;
1154
+
1155
+ this.#emitDiagnostic({
1156
+ at: Date.now(),
1157
+ level: 'warn',
1158
+ source: 'sync',
1159
+ code: 'sync.scope_revoked',
1160
+ message: 'Syncular subscription scope revoked',
1161
+ ...syncularDiagnosticAttemptFields(syncAttempt),
1162
+ details: {
1163
+ requestType,
1164
+ revokedSubscriptionIds,
1165
+ revokedSubscriptionCount: revokedSubscriptionIds.length,
1166
+ },
1167
+ });
1168
+ }
1169
+ }
1170
+
1171
+ function isSyncularSyncResult(value: unknown): value is SyncularSyncResult {
1172
+ return (
1173
+ value !== null &&
1174
+ typeof value === 'object' &&
1175
+ Array.isArray((value as { subscriptions?: unknown }).subscriptions)
1176
+ );
1177
+ }
1178
+
1179
+ function parseSyncResult(value: string): SyncularSyncResult {
1180
+ const raw = parseJson<RawSyncResult>(value);
1181
+ return {
1182
+ changedTables: raw.changed_tables ?? [],
1183
+ changedRows: raw.changed_rows ?? [],
1184
+ changedRowsTruncated: raw.changed_rows_truncated ?? false,
1185
+ subscriptions: (raw.subscriptions ?? []).map((subscription) => ({
1186
+ id: subscription.id,
1187
+ table: subscription.table,
1188
+ status: subscription.status,
1189
+ scopes: subscription.scopes,
1190
+ nextCursor: subscription.next_cursor,
1191
+ bootstrapPhase: subscription.bootstrap_phase ?? 0,
1192
+ bootstrapState: parseBootstrapState(subscription.bootstrap_state),
1193
+ ready: subscription.ready === true,
1194
+ phase: subscription.phase ?? 'pending',
1195
+ progressPercent: subscription.progress_percent ?? 0,
1196
+ snapshotRows: subscription.snapshot_rows ?? [],
1197
+ commits: subscription.commits ?? [],
1198
+ })),
1199
+ bootstrap: parseBootstrapStatus(raw.bootstrap),
1200
+ pushedCommits: raw.pushed_commits ?? 0,
1201
+ timings: {
1202
+ totalMs: raw.timings?.total_ms ?? 0,
1203
+ pushMs: raw.timings?.push_ms ?? 0,
1204
+ pullMs: raw.timings?.pull_ms ?? 0,
1205
+ pullRequestMs: raw.timings?.pull_request_ms ?? 0,
1206
+ syncPackDecodeMs: raw.timings?.sync_pack_decode_ms ?? 0,
1207
+ pullTransformMs: raw.timings?.pull_transform_ms ?? 0,
1208
+ integrityVerifyMs: raw.timings?.integrity_verify_ms ?? 0,
1209
+ snapshotFetchMs: raw.timings?.snapshot_fetch_ms ?? 0,
1210
+ pullApplyMs: raw.timings?.pull_apply_ms ?? 0,
1211
+ scopeClearMs: raw.timings?.scope_clear_ms ?? 0,
1212
+ snapshotRowApplyMs: raw.timings?.snapshot_row_apply_ms ?? 0,
1213
+ snapshotArtifactApplyMs: raw.timings?.snapshot_artifact_apply_ms ?? 0,
1214
+ snapshotArtifactCheckpointMs:
1215
+ raw.timings?.snapshot_artifact_checkpoint_ms ?? 0,
1216
+ snapshotArtifactCheckpointCount:
1217
+ raw.timings?.snapshot_artifact_checkpoint_count ?? 0,
1218
+ snapshotChunkApplyMs: raw.timings?.snapshot_chunk_apply_ms ?? 0,
1219
+ snapshotChunkMaterializeMs:
1220
+ raw.timings?.snapshot_chunk_materialize_ms ?? 0,
1221
+ snapshotChunkResetMs: raw.timings?.snapshot_chunk_reset_ms ?? 0,
1222
+ snapshotChunkBindMs: raw.timings?.snapshot_chunk_bind_ms ?? 0,
1223
+ snapshotChunkStepMs: raw.timings?.snapshot_chunk_step_ms ?? 0,
1224
+ commitApplyMs: raw.timings?.commit_apply_ms ?? 0,
1225
+ subscriptionStateMs: raw.timings?.subscription_state_ms ?? 0,
1226
+ notifyMs: raw.timings?.notify_ms ?? 0,
1227
+ },
1228
+ };
1229
+ }
1230
+
1231
+ function parseBootstrapState(
1232
+ state: RawBootstrapState | null | undefined
1233
+ ): SyncularBootstrapStatus['subscriptions'][number]['bootstrapState'] {
1234
+ if (!state) return null;
1235
+ return {
1236
+ asOfCommitSeq: state.asOfCommitSeq ?? 0,
1237
+ tables: state.tables ?? [],
1238
+ tableIndex: state.tableIndex ?? 0,
1239
+ rowCursor: state.rowCursor ?? null,
1240
+ };
1241
+ }
1242
+
1243
+ function parseBootstrapStatus(
1244
+ raw: RawBootstrapStatus | null | undefined
1245
+ ): SyncularBootstrapStatus {
1246
+ return {
1247
+ channelPhase: raw?.channel_phase ?? 'idle',
1248
+ progressPercent: raw?.progress_percent ?? 100,
1249
+ isBootstrapping: raw?.is_bootstrapping === true,
1250
+ criticalReady: raw?.critical_ready ?? true,
1251
+ interactiveReady: raw?.interactive_ready ?? true,
1252
+ complete: raw?.complete ?? true,
1253
+ activePhase: raw?.active_phase ?? null,
1254
+ expectedSubscriptionIds: raw?.expected_subscription_ids ?? [],
1255
+ readySubscriptionIds: raw?.ready_subscription_ids ?? [],
1256
+ pendingSubscriptionIds: raw?.pending_subscription_ids ?? [],
1257
+ subscriptions: (raw?.subscriptions ?? []).map((subscription) => ({
1258
+ id: subscription.id,
1259
+ table: subscription.table,
1260
+ expected: subscription.expected ?? true,
1261
+ ready: subscription.ready === true,
1262
+ status: subscription.status ?? null,
1263
+ phase: subscription.phase ?? 'pending',
1264
+ progressPercent: subscription.progress_percent ?? 0,
1265
+ cursor: subscription.cursor ?? null,
1266
+ bootstrapState: parseBootstrapState(subscription.bootstrap_state),
1267
+ bootstrapPhase: subscription.bootstrap_phase ?? 0,
1268
+ })),
1269
+ phases: (raw?.phases ?? []).map((phase) => ({
1270
+ phase: phase.phase ?? 0,
1271
+ expectedSubscriptionIds: phase.expected_subscription_ids ?? [],
1272
+ readySubscriptionIds: phase.ready_subscription_ids ?? [],
1273
+ pendingSubscriptionIds: phase.pending_subscription_ids ?? [],
1274
+ isReady: phase.is_ready === true,
1275
+ progressPercent: phase.progress_percent ?? 0,
1276
+ })),
1277
+ };
1278
+ }
1279
+
1280
+ function buildBootstrapStatus(
1281
+ configuredSubscriptions: readonly SyncularSubscriptionSpec[],
1282
+ cachedById: ReadonlyMap<string, SyncularBootstrapSubscriptionStatusEntry>,
1283
+ pullOptions: SyncularPullOptions | undefined
1284
+ ): SyncularBootstrapStatus {
1285
+ const criticalPhase = normalizeBootstrapPhase(
1286
+ pullOptions?.criticalBootstrapPhase
1287
+ );
1288
+ const interactivePhase = Math.max(
1289
+ criticalPhase,
1290
+ normalizeBootstrapPhase(pullOptions?.interactiveBootstrapPhase ?? 1)
1291
+ );
1292
+ const subscriptions = configuredSubscriptions.map((spec) => {
1293
+ const cached = cachedById.get(spec.id);
1294
+ const bootstrapPhase = normalizeBootstrapPhase(spec.bootstrapPhase);
1295
+ return {
1296
+ id: spec.id,
1297
+ table: spec.table,
1298
+ expected: true,
1299
+ ready: cached?.ready ?? false,
1300
+ status: cached?.status ?? null,
1301
+ phase: cached?.phase ?? 'pending',
1302
+ progressPercent: cached?.progressPercent ?? 0,
1303
+ cursor: cached?.cursor ?? null,
1304
+ bootstrapState: cached?.bootstrapState ?? null,
1305
+ bootstrapPhase,
1306
+ };
1307
+ });
1308
+ const expectedSubscriptionIds = subscriptions.map(
1309
+ (subscription) => subscription.id
1310
+ );
1311
+ const readySubscriptionIds = subscriptions
1312
+ .filter((subscription) => subscription.ready)
1313
+ .map((subscription) => subscription.id);
1314
+ const pendingSubscriptionIds = subscriptions
1315
+ .filter((subscription) => !subscription.ready)
1316
+ .map((subscription) => subscription.id);
1317
+ const complete = pendingSubscriptionIds.length === 0;
1318
+ const criticalReady = subscriptions.every(
1319
+ (subscription) =>
1320
+ subscription.bootstrapPhase > criticalPhase || subscription.ready
1321
+ );
1322
+ const interactiveReady = subscriptions.every(
1323
+ (subscription) =>
1324
+ subscription.bootstrapPhase > interactivePhase || subscription.ready
1325
+ );
1326
+ const activePhase =
1327
+ subscriptions
1328
+ .filter((subscription) => !subscription.ready)
1329
+ .reduce<number | null>(
1330
+ (lowest, subscription) =>
1331
+ lowest === null || subscription.bootstrapPhase < lowest
1332
+ ? subscription.bootstrapPhase
1333
+ : lowest,
1334
+ null
1335
+ ) ?? null;
1336
+ const hasError = subscriptions.some(
1337
+ (subscription) => subscription.phase === 'error'
1338
+ );
1339
+ const isBootstrapping = subscriptions.some(
1340
+ (subscription) => !subscription.ready && subscription.phase !== 'error'
1341
+ );
1342
+ const channelPhase = hasError
1343
+ ? 'error'
1344
+ : isBootstrapping
1345
+ ? 'bootstrapping'
1346
+ : complete && expectedSubscriptionIds.length > 0
1347
+ ? 'live'
1348
+ : 'idle';
1349
+ const progressPercent =
1350
+ subscriptions.length === 0
1351
+ ? 100
1352
+ : Math.round(
1353
+ subscriptions.reduce(
1354
+ (sum, subscription) => sum + subscription.progressPercent,
1355
+ 0
1356
+ ) / subscriptions.length
1357
+ );
1358
+ const phaseMap = new Map<
1359
+ number,
1360
+ {
1361
+ expectedSubscriptionIds: string[];
1362
+ readySubscriptionIds: string[];
1363
+ pendingSubscriptionIds: string[];
1364
+ progressPercent: number;
1365
+ }
1366
+ >();
1367
+ for (const subscription of subscriptions) {
1368
+ const phase = phaseMap.get(subscription.bootstrapPhase) ?? {
1369
+ expectedSubscriptionIds: [],
1370
+ readySubscriptionIds: [],
1371
+ pendingSubscriptionIds: [],
1372
+ progressPercent: 0,
1373
+ };
1374
+ phase.expectedSubscriptionIds.push(subscription.id);
1375
+ phase.progressPercent += subscription.progressPercent;
1376
+ if (subscription.ready) {
1377
+ phase.readySubscriptionIds.push(subscription.id);
1378
+ } else {
1379
+ phase.pendingSubscriptionIds.push(subscription.id);
1380
+ }
1381
+ phaseMap.set(subscription.bootstrapPhase, phase);
1382
+ }
1383
+ const phases = [...phaseMap.entries()]
1384
+ .sort(([left], [right]) => left - right)
1385
+ .map(([phase, summary]) => ({
1386
+ phase,
1387
+ expectedSubscriptionIds: summary.expectedSubscriptionIds,
1388
+ readySubscriptionIds: summary.readySubscriptionIds,
1389
+ pendingSubscriptionIds: summary.pendingSubscriptionIds,
1390
+ isReady: summary.pendingSubscriptionIds.length === 0,
1391
+ progressPercent:
1392
+ summary.expectedSubscriptionIds.length === 0
1393
+ ? 100
1394
+ : Math.round(
1395
+ summary.progressPercent / summary.expectedSubscriptionIds.length
1396
+ ),
1397
+ }));
1398
+
1399
+ return {
1400
+ channelPhase,
1401
+ progressPercent,
1402
+ isBootstrapping,
1403
+ criticalReady,
1404
+ interactiveReady,
1405
+ complete,
1406
+ activePhase,
1407
+ expectedSubscriptionIds,
1408
+ readySubscriptionIds,
1409
+ pendingSubscriptionIds,
1410
+ subscriptions,
1411
+ phases,
1412
+ };
1413
+ }
1414
+
1415
+ function normalizeBootstrapPhase(value: number | null | undefined): number {
1416
+ return Number.isFinite(value) ? Math.max(0, Math.trunc(value!)) : 0;
1417
+ }
1418
+
1419
+ function parseConflictSummaries(value: string): SyncularConflictSummary[] {
1420
+ return parseJson<RawConflictSummary[]>(value).map((conflict) => ({
1421
+ id: conflict.id,
1422
+ clientCommitId: conflict.client_commit_id,
1423
+ opIndex: conflict.op_index,
1424
+ resultStatus: conflict.result_status,
1425
+ message: conflict.message,
1426
+ code: conflict.code,
1427
+ serverVersion: conflict.server_version,
1428
+ resolvedAt: conflict.resolved_at,
1429
+ resolution: conflict.resolution,
1430
+ }));
1431
+ }
1432
+
1433
+ function parseJson<T>(value: string): T {
1434
+ return JSON.parse(value) as T;
1435
+ }
1436
+
1437
+ function stringifyParams(params: readonly unknown[]): string {
1438
+ return JSON.stringify(params, (_key, value) => {
1439
+ if (typeof value === 'bigint') return Number(value);
1440
+ if (value instanceof Uint8Array) return Array.from(value);
1441
+ return value;
1442
+ });
1443
+ }
1444
+
1445
+ function normalizeFieldEncryptionConfig(
1446
+ config: SyncularFieldEncryptionConfig
1447
+ ): Omit<SyncularFieldEncryptionConfig, 'keys'> & {
1448
+ keys: Record<string, string>;
1449
+ } {
1450
+ const keys: Record<string, string> = {};
1451
+ for (const [kid, value] of Object.entries(config.keys)) {
1452
+ keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
1453
+ }
1454
+ return { ...config, keys };
1455
+ }
1456
+
1457
+ function normalizeEncryptedCrdtConfig(
1458
+ config: SyncularEncryptedCrdtConfig
1459
+ ): Omit<SyncularEncryptedCrdtConfig, 'keys'> & {
1460
+ keys: Record<string, string>;
1461
+ } {
1462
+ const keys: Record<string, string> = {};
1463
+ for (const [kid, value] of Object.entries(config.keys)) {
1464
+ keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
1465
+ }
1466
+ return { ...config, keys };
1467
+ }
1468
+
1469
+ function normalizeBlobEncryptionConfig(
1470
+ config: SyncularBlobEncryptionConfig
1471
+ ): Omit<SyncularBlobEncryptionConfig, 'keys'> & {
1472
+ keys: Record<string, string>;
1473
+ } {
1474
+ const keys: Record<string, string> = {};
1475
+ for (const [kid, value] of Object.entries(config.keys)) {
1476
+ keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
1477
+ }
1478
+ return { ...config, keys };
1479
+ }
1480
+
1481
+ function bytesToBase64Url(bytes: Uint8Array): string {
1482
+ let binary = '';
1483
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1484
+ return btoa(binary)
1485
+ .replace(/\+/g, '-')
1486
+ .replace(/\//g, '_')
1487
+ .replace(/=+$/g, '');
1488
+ }