@syncular/client 0.0.6-96 → 0.1.1

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