@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
@@ -1,855 +0,0 @@
1
- /**
2
- * @syncular/client - Sync pull engine
3
- */
4
-
5
- import type {
6
- SyncBootstrapState,
7
- SyncPullRequest,
8
- SyncPullResponse,
9
- SyncPullSubscriptionResponse,
10
- SyncSnapshot,
11
- SyncSubscriptionRequest,
12
- SyncTransport,
13
- } from '@syncular/core';
14
- import { decodeSnapshotRows } from '@syncular/core';
15
- import { type Kysely, sql, type Transaction } from 'kysely';
16
- import {
17
- type ClientHandlerCollection,
18
- getClientHandlerOrThrow,
19
- } from './handlers/collection';
20
- import type { ClientTableHandler } from './handlers/types';
21
- import type {
22
- SyncClientPlugin,
23
- SyncClientPluginContext,
24
- } from './plugins/types';
25
- import type { SyncClientDb, SyncSubscriptionStateTable } from './schema';
26
-
27
- // Simple JSON serialization cache to avoid repeated stringification
28
- // of the same objects during pull operations
29
- const jsonCache = new WeakMap<object, string>();
30
- const jsonCacheStats = { hits: 0, misses: 0 };
31
- const SNAPSHOT_CHUNK_CONCURRENCY = 8;
32
- const SNAPSHOT_APPLY_BATCH_ROWS = 500;
33
- const SNAPSHOT_ROW_FRAME_MAGIC = new Uint8Array([0x53, 0x52, 0x46, 0x31]); // "SRF1"
34
- const FRAME_LENGTH_BYTES = 4;
35
-
36
- function serializeJsonCached(obj: object): string {
37
- if (obj === null || typeof obj !== 'object') {
38
- return JSON.stringify(obj);
39
- }
40
- const cached = jsonCache.get(obj);
41
- if (cached !== undefined) {
42
- jsonCacheStats.hits++;
43
- return cached;
44
- }
45
- jsonCacheStats.misses++;
46
- const serialized = JSON.stringify(obj);
47
- // Only cache objects that are likely to be reused (not one-off empty objects)
48
- if (Object.keys(obj).length > 0) {
49
- jsonCache.set(obj, serialized);
50
- }
51
- return serialized;
52
- }
53
-
54
- function isGzipBytes(bytes: Uint8Array): boolean {
55
- return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
56
- }
57
-
58
- function bytesToReadableStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
59
- return new ReadableStream<Uint8Array>({
60
- start(controller) {
61
- controller.enqueue(bytes);
62
- controller.close();
63
- },
64
- });
65
- }
66
-
67
- function concatBytes(chunks: readonly Uint8Array[]): Uint8Array {
68
- if (chunks.length === 1) {
69
- return chunks[0] ?? new Uint8Array();
70
- }
71
- let total = 0;
72
- for (const chunk of chunks) total += chunk.length;
73
- const out = new Uint8Array(total);
74
- let offset = 0;
75
- for (const chunk of chunks) {
76
- out.set(chunk, offset);
77
- offset += chunk.length;
78
- }
79
- return out;
80
- }
81
-
82
- function appendBytes(base: Uint8Array, next: Uint8Array): Uint8Array {
83
- if (base.length === 0) return next;
84
- if (next.length === 0) return base;
85
- const out = new Uint8Array(base.length + next.length);
86
- out.set(base, 0);
87
- out.set(next, base.length);
88
- return out;
89
- }
90
-
91
- function toOwnedUint8Array(chunk: Uint8Array): Uint8Array<ArrayBuffer> {
92
- const out = new ArrayBuffer(chunk.byteLength);
93
- const bytes = new Uint8Array(out);
94
- bytes.set(chunk);
95
- return bytes;
96
- }
97
-
98
- async function streamToBytes(
99
- stream: ReadableStream<Uint8Array>
100
- ): Promise<Uint8Array> {
101
- const reader = stream.getReader();
102
- try {
103
- const chunks: Uint8Array[] = [];
104
- while (true) {
105
- const { done, value } = await reader.read();
106
- if (done) break;
107
- if (!value || value.length === 0) continue;
108
- chunks.push(value);
109
- }
110
- return concatBytes(chunks);
111
- } finally {
112
- reader.releaseLock();
113
- }
114
- }
115
-
116
- async function maybeGunzipStream(
117
- stream: ReadableStream<Uint8Array>
118
- ): Promise<ReadableStream<Uint8Array>> {
119
- const reader = stream.getReader();
120
- const prefetched: Uint8Array[] = [];
121
- let prefetchedBytes = 0;
122
-
123
- while (prefetchedBytes < 2) {
124
- const { done, value } = await reader.read();
125
- if (done) break;
126
- if (!value || value.length === 0) continue;
127
- prefetched.push(value);
128
- prefetchedBytes += value.length;
129
- }
130
-
131
- const prefetchedCombined = concatBytes(prefetched);
132
- const gzip = isGzipBytes(prefetchedCombined);
133
-
134
- const replayStream = new ReadableStream<Uint8Array<ArrayBuffer>>({
135
- start(controller) {
136
- if (prefetchedCombined.length > 0) {
137
- controller.enqueue(toOwnedUint8Array(prefetchedCombined));
138
- }
139
- },
140
- async pull(controller) {
141
- const { done, value } = await reader.read();
142
- if (done) {
143
- controller.close();
144
- reader.releaseLock();
145
- return;
146
- }
147
- if (!value || value.length === 0) return;
148
- controller.enqueue(toOwnedUint8Array(value));
149
- },
150
- async cancel(reason) {
151
- await reader.cancel(reason);
152
- reader.releaseLock();
153
- },
154
- });
155
-
156
- if (!gzip) return replayStream;
157
-
158
- if (typeof DecompressionStream !== 'undefined') {
159
- return replayStream.pipeThrough(new DecompressionStream('gzip'));
160
- }
161
-
162
- throw new Error(
163
- 'Snapshot chunk appears gzip-compressed but gzip decompression is not available in this runtime'
164
- );
165
- }
166
-
167
- async function maybeGunzip(bytes: Uint8Array): Promise<Uint8Array> {
168
- if (!isGzipBytes(bytes)) return bytes;
169
- const decompressedStream = await maybeGunzipStream(
170
- bytesToReadableStream(bytes)
171
- );
172
- return streamToBytes(decompressedStream);
173
- }
174
-
175
- async function* decodeSnapshotRowStreamBatches(
176
- stream: ReadableStream<Uint8Array>,
177
- batchSize: number
178
- ): AsyncGenerator<unknown[]> {
179
- const reader = stream.getReader();
180
- const decoder = new TextDecoder();
181
- let pending: Uint8Array = new Uint8Array(0);
182
- let headerValidated = false;
183
- let rows: unknown[] = [];
184
-
185
- try {
186
- while (true) {
187
- const { done, value } = await reader.read();
188
- if (done) break;
189
- if (!value || value.length === 0) continue;
190
- pending = appendBytes(pending, value);
191
-
192
- if (!headerValidated) {
193
- if (pending.length < SNAPSHOT_ROW_FRAME_MAGIC.length) {
194
- continue;
195
- }
196
- for (let index = 0; index < SNAPSHOT_ROW_FRAME_MAGIC.length; index++) {
197
- if (pending[index] !== SNAPSHOT_ROW_FRAME_MAGIC[index]) {
198
- throw new Error('Unexpected snapshot chunk format');
199
- }
200
- }
201
- pending = pending.subarray(SNAPSHOT_ROW_FRAME_MAGIC.length);
202
- headerValidated = true;
203
- }
204
-
205
- while (pending.length >= FRAME_LENGTH_BYTES) {
206
- const view = new DataView(
207
- pending.buffer,
208
- pending.byteOffset,
209
- pending.byteLength
210
- );
211
- const payloadLength = view.getUint32(0, false);
212
- if (pending.length < FRAME_LENGTH_BYTES + payloadLength) {
213
- break;
214
- }
215
-
216
- const payload = pending.subarray(
217
- FRAME_LENGTH_BYTES,
218
- FRAME_LENGTH_BYTES + payloadLength
219
- );
220
- rows.push(JSON.parse(decoder.decode(payload)));
221
- pending = pending.subarray(FRAME_LENGTH_BYTES + payloadLength);
222
-
223
- if (rows.length >= batchSize) {
224
- yield rows;
225
- rows = [];
226
- }
227
- }
228
- }
229
-
230
- if (!headerValidated) {
231
- throw new Error('Snapshot chunk payload is too small');
232
- }
233
-
234
- if (pending.length > 0) {
235
- if (pending.length < FRAME_LENGTH_BYTES) {
236
- throw new Error('Snapshot chunk payload ended mid-frame header');
237
- }
238
- const view = new DataView(
239
- pending.buffer,
240
- pending.byteOffset,
241
- pending.byteLength
242
- );
243
- const payloadLength = view.getUint32(0, false);
244
- if (pending.length < FRAME_LENGTH_BYTES + payloadLength) {
245
- throw new Error('Snapshot chunk payload ended mid-frame body');
246
- }
247
- while (pending.length >= FRAME_LENGTH_BYTES) {
248
- const nextView = new DataView(
249
- pending.buffer,
250
- pending.byteOffset,
251
- pending.byteLength
252
- );
253
- const nextLength = nextView.getUint32(0, false);
254
- if (pending.length < FRAME_LENGTH_BYTES + nextLength) {
255
- break;
256
- }
257
- const payload = pending.subarray(
258
- FRAME_LENGTH_BYTES,
259
- FRAME_LENGTH_BYTES + nextLength
260
- );
261
- rows.push(JSON.parse(decoder.decode(payload)));
262
- pending = pending.subarray(FRAME_LENGTH_BYTES + nextLength);
263
- if (rows.length >= batchSize) {
264
- yield rows;
265
- rows = [];
266
- }
267
- }
268
- if (pending.length > 0) {
269
- throw new Error('Snapshot chunk payload ended mid-frame body');
270
- }
271
- }
272
-
273
- if (rows.length > 0) {
274
- yield rows;
275
- }
276
- } finally {
277
- reader.releaseLock();
278
- }
279
- }
280
-
281
- async function computeSha256Hex(
282
- bytes: Uint8Array,
283
- sha256Override?: (bytes: Uint8Array) => Promise<string>
284
- ): Promise<string> {
285
- // Use injected implementation if provided (e.g. expo-crypto on React Native)
286
- if (sha256Override) {
287
- return sha256Override(bytes);
288
- }
289
-
290
- // Use crypto.subtle if available (browsers, modern Node/Bun)
291
- if (typeof crypto !== 'undefined' && crypto.subtle) {
292
- // Create a fresh ArrayBuffer to satisfy crypto.subtle's type requirements
293
- const buffer = new ArrayBuffer(bytes.length);
294
- new Uint8Array(buffer).set(bytes);
295
- const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
296
- const hashArray = new Uint8Array(hashBuffer);
297
- return Array.from(hashArray, (b) => b.toString(16).padStart(2, '0')).join(
298
- ''
299
- );
300
- }
301
-
302
- throw new Error(
303
- 'No crypto implementation available for SHA-256. ' +
304
- 'Provide a sha256 function via options or ensure crypto.subtle is available.'
305
- );
306
- }
307
-
308
- async function fetchSnapshotChunkStream(
309
- transport: SyncTransport,
310
- chunkId: string
311
- ): Promise<ReadableStream<Uint8Array>> {
312
- if (transport.fetchSnapshotChunkStream) {
313
- return transport.fetchSnapshotChunkStream({ chunkId });
314
- }
315
- const bytes = await transport.fetchSnapshotChunk({ chunkId });
316
- return bytesToReadableStream(bytes);
317
- }
318
-
319
- async function mapWithConcurrency<T, U>(
320
- items: readonly T[],
321
- concurrency: number,
322
- mapper: (item: T, index: number) => Promise<U>
323
- ): Promise<U[]> {
324
- if (items.length === 0) return [];
325
-
326
- const workerCount = Math.max(1, Math.min(concurrency, items.length));
327
- const results = new Array<U>(items.length);
328
- let nextIndex = 0;
329
-
330
- async function worker(): Promise<void> {
331
- while (nextIndex < items.length) {
332
- const index = nextIndex;
333
- nextIndex += 1;
334
- const item = items[index];
335
- if (item === undefined) continue;
336
- results[index] = await mapper(item, index);
337
- }
338
- }
339
-
340
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
341
- return results;
342
- }
343
-
344
- async function materializeChunkedSnapshots(
345
- transport: SyncTransport,
346
- response: SyncPullResponse,
347
- sha256Override?: (bytes: Uint8Array) => Promise<string>
348
- ): Promise<SyncPullResponse> {
349
- const chunkCache = new Map<string, Promise<Uint8Array>>();
350
-
351
- const subscriptions = await Promise.all(
352
- response.subscriptions.map(async (sub) => {
353
- if (!sub.bootstrap) return sub;
354
- if (!sub.snapshots || sub.snapshots.length === 0) return sub;
355
-
356
- const snapshots = await mapWithConcurrency(
357
- sub.snapshots,
358
- SNAPSHOT_CHUNK_CONCURRENCY,
359
- async (snapshot) => {
360
- const chunks = snapshot.chunks ?? [];
361
- if (chunks.length === 0) {
362
- return snapshot;
363
- }
364
-
365
- const parsedRowsByChunk = await mapWithConcurrency(
366
- chunks,
367
- SNAPSHOT_CHUNK_CONCURRENCY,
368
- async (chunk) => {
369
- const promise =
370
- chunkCache.get(chunk.id) ??
371
- transport.fetchSnapshotChunk({ chunkId: chunk.id });
372
- chunkCache.set(chunk.id, promise);
373
-
374
- const raw = await promise;
375
- const bytes = await maybeGunzip(raw);
376
-
377
- // Verify chunk integrity using sha256 hash
378
- if (chunk.sha256) {
379
- const actualHash = await computeSha256Hex(
380
- bytes,
381
- sha256Override
382
- );
383
- if (actualHash !== chunk.sha256) {
384
- throw new Error(
385
- `Snapshot chunk integrity check failed: expected sha256 ${chunk.sha256}, got ${actualHash}`
386
- );
387
- }
388
- }
389
-
390
- return decodeSnapshotRows(bytes);
391
- }
392
- );
393
-
394
- const rows: unknown[] = [];
395
- for (const parsedRows of parsedRowsByChunk) {
396
- rows.push(...parsedRows);
397
- }
398
-
399
- return { ...snapshot, rows, chunks: undefined };
400
- }
401
- );
402
-
403
- return { ...sub, snapshots };
404
- })
405
- );
406
-
407
- // Clear chunk cache after processing to prevent memory accumulation
408
- chunkCache.clear();
409
-
410
- return { ...response, subscriptions };
411
- }
412
-
413
- async function applyChunkedSnapshot<DB extends SyncClientDb>(
414
- transport: SyncTransport,
415
- handler: Pick<ClientTableHandler<DB>, 'applySnapshot'>,
416
- trx: Transaction<DB>,
417
- snapshot: SyncSnapshot
418
- ): Promise<void> {
419
- const chunks = snapshot.chunks ?? [];
420
- if (chunks.length === 0) {
421
- await handler.applySnapshot({ trx }, snapshot);
422
- return;
423
- }
424
-
425
- let nextIsFirstPage = snapshot.isFirstPage;
426
-
427
- for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
428
- const chunk = chunks[chunkIndex];
429
- if (!chunk) continue;
430
-
431
- const rawStream = await fetchSnapshotChunkStream(transport, chunk.id);
432
- const decodedStream = await maybeGunzipStream(rawStream);
433
- const rowBatchIterator = decodeSnapshotRowStreamBatches(
434
- decodedStream,
435
- SNAPSHOT_APPLY_BATCH_ROWS
436
- );
437
-
438
- let pendingBatch: unknown[] | null = null;
439
- // eslint-disable-next-line no-await-in-loop
440
- for await (const batch of rowBatchIterator) {
441
- if (pendingBatch) {
442
- // eslint-disable-next-line no-await-in-loop
443
- await handler.applySnapshot(
444
- { trx },
445
- {
446
- ...snapshot,
447
- rows: pendingBatch,
448
- chunks: undefined,
449
- isFirstPage: nextIsFirstPage,
450
- isLastPage: false,
451
- }
452
- );
453
- nextIsFirstPage = false;
454
- }
455
- pendingBatch = batch;
456
- }
457
-
458
- if (pendingBatch) {
459
- const isLastChunk = chunkIndex === chunks.length - 1;
460
- // eslint-disable-next-line no-await-in-loop
461
- await handler.applySnapshot(
462
- { trx },
463
- {
464
- ...snapshot,
465
- rows: pendingBatch,
466
- chunks: undefined,
467
- isFirstPage: nextIsFirstPage,
468
- isLastPage: isLastChunk ? snapshot.isLastPage : false,
469
- }
470
- );
471
- nextIsFirstPage = false;
472
- }
473
- }
474
- }
475
-
476
- function parseBootstrapState(
477
- value: string | object | null | undefined
478
- ): SyncBootstrapState | null {
479
- if (!value) return null;
480
- try {
481
- // Handle both string (raw JSON) and object (already deserialized by a DB plugin/driver)
482
- const parsed: SyncBootstrapState =
483
- typeof value === 'string'
484
- ? (JSON.parse(value) as SyncBootstrapState)
485
- : (value as SyncBootstrapState);
486
- if (!parsed || typeof parsed !== 'object') return null;
487
- if (typeof parsed.asOfCommitSeq !== 'number') return null;
488
- if (!Array.isArray(parsed.tables)) return null;
489
- if (typeof parsed.tableIndex !== 'number') return null;
490
- if (parsed.rowCursor !== null && typeof parsed.rowCursor !== 'string')
491
- return null;
492
- return parsed;
493
- } catch {
494
- return null;
495
- }
496
- }
497
-
498
- export interface SyncPullOnceOptions {
499
- clientId: string;
500
- actorId?: string;
501
- plugins?: SyncClientPlugin[];
502
- /**
503
- * Desired subscriptions (client-chosen ids).
504
- * Cursors are persisted in `sync_subscription_state`.
505
- */
506
- subscriptions: Array<Omit<SyncSubscriptionRequest, 'cursor'>>;
507
- limitCommits?: number;
508
- limitSnapshotRows?: number;
509
- maxSnapshotPages?: number;
510
- dedupeRows?: boolean;
511
- stateId?: string;
512
- /**
513
- * Custom SHA-256 hash function for snapshot chunk integrity verification.
514
- * Provide this on platforms where `crypto.subtle` is unavailable (e.g. React Native).
515
- * Must return the hex-encoded hash string.
516
- */
517
- sha256?: (bytes: Uint8Array) => Promise<string>;
518
- }
519
-
520
- export interface SyncPullRequestState {
521
- request: SyncPullRequest;
522
- existing: SyncSubscriptionStateTable[];
523
- existingById: Map<string, SyncSubscriptionStateTable>;
524
- stateId: string;
525
- }
526
-
527
- /**
528
- * Build a pull request from subscription state. Exported for use
529
- * by the combined sync path in sync-loop.ts.
530
- */
531
- export async function buildPullRequest<DB extends SyncClientDb>(
532
- db: Kysely<DB>,
533
- options: SyncPullOnceOptions
534
- ): Promise<SyncPullRequestState> {
535
- const stateId = options.stateId ?? 'default';
536
-
537
- const existingResult = await sql<SyncSubscriptionStateTable>`
538
- select
539
- ${sql.ref('state_id')},
540
- ${sql.ref('subscription_id')},
541
- ${sql.ref('table')},
542
- ${sql.ref('scopes_json')},
543
- ${sql.ref('params_json')},
544
- ${sql.ref('cursor')},
545
- ${sql.ref('bootstrap_state_json')},
546
- ${sql.ref('status')},
547
- ${sql.ref('created_at')},
548
- ${sql.ref('updated_at')}
549
- from ${sql.table('sync_subscription_state')}
550
- where ${sql.ref('state_id')} = ${sql.val(stateId)}
551
- `.execute(db);
552
- const existing = existingResult.rows;
553
-
554
- const existingById = new Map<string, SyncSubscriptionStateTable>();
555
- for (const row of existing) existingById.set(row.subscription_id, row);
556
-
557
- const request: SyncPullRequest = {
558
- clientId: options.clientId,
559
- limitCommits: options.limitCommits ?? 50,
560
- limitSnapshotRows: options.limitSnapshotRows ?? 1000,
561
- maxSnapshotPages: options.maxSnapshotPages ?? 4,
562
- dedupeRows: options.dedupeRows,
563
- subscriptions: (options.subscriptions ?? []).map((sub) => ({
564
- ...sub,
565
- cursor: Math.max(-1, existingById.get(sub.id)?.cursor ?? -1),
566
- bootstrapState: parseBootstrapState(
567
- existingById.get(sub.id)?.bootstrap_state_json
568
- ),
569
- })),
570
- };
571
-
572
- return { request, existing, existingById, stateId };
573
- }
574
-
575
- export function createFollowupPullState(
576
- pullState: SyncPullRequestState,
577
- response: SyncPullResponse
578
- ): SyncPullRequestState {
579
- const responseById = new Map<string, SyncPullSubscriptionResponse>();
580
- for (const sub of response.subscriptions ?? []) {
581
- responseById.set(sub.id, sub);
582
- }
583
-
584
- const now = Date.now();
585
- const nextExisting: SyncSubscriptionStateTable[] = [];
586
- const nextExistingById = new Map<string, SyncSubscriptionStateTable>();
587
-
588
- for (const sub of pullState.request.subscriptions ?? []) {
589
- const res = responseById.get(sub.id);
590
- if (res?.status === 'revoked') {
591
- continue;
592
- }
593
-
594
- const nextCursor = res ? Math.max(-1, res.nextCursor) : (sub.cursor ?? -1);
595
- const nextBootstrapState = res
596
- ? res.bootstrap
597
- ? (res.bootstrapState ?? null)
598
- : null
599
- : (sub.bootstrapState ?? null);
600
- const prev = pullState.existingById.get(sub.id);
601
- const nextRow: SyncSubscriptionStateTable = {
602
- state_id: pullState.stateId,
603
- subscription_id: sub.id,
604
- table: sub.table,
605
- scopes_json: serializeJsonCached(sub.scopes ?? {}),
606
- params_json: serializeJsonCached(sub.params ?? {}),
607
- cursor: nextCursor,
608
- bootstrap_state_json: nextBootstrapState
609
- ? serializeJsonCached(nextBootstrapState)
610
- : null,
611
- status: 'active',
612
- created_at: prev?.created_at ?? now,
613
- updated_at: now,
614
- };
615
- nextExisting.push(nextRow);
616
- nextExistingById.set(nextRow.subscription_id, nextRow);
617
- }
618
-
619
- const nextRequest: SyncPullRequest = {
620
- ...pullState.request,
621
- subscriptions: (pullState.request.subscriptions ?? []).map((sub) => {
622
- const row = nextExistingById.get(sub.id);
623
- return {
624
- ...sub,
625
- cursor: Math.max(-1, row?.cursor ?? -1),
626
- bootstrapState: parseBootstrapState(row?.bootstrap_state_json),
627
- };
628
- }),
629
- };
630
-
631
- return {
632
- request: nextRequest,
633
- existing: nextExisting,
634
- existingById: nextExistingById,
635
- stateId: pullState.stateId,
636
- };
637
- }
638
-
639
- /**
640
- * Apply a pull response (run plugins + write to local DB).
641
- * Exported for use by the combined sync path in sync-loop.ts.
642
- */
643
- export async function applyPullResponse<DB extends SyncClientDb>(
644
- db: Kysely<DB>,
645
- transport: SyncTransport,
646
- handlers: ClientHandlerCollection<DB>,
647
- options: SyncPullOnceOptions,
648
- pullState: SyncPullRequestState,
649
- rawResponse: SyncPullResponse
650
- ): Promise<SyncPullResponse> {
651
- const { request, existing, existingById, stateId } = pullState;
652
-
653
- const ctx: SyncClientPluginContext = {
654
- actorId: options.actorId ?? 'unknown',
655
- clientId: options.clientId,
656
- };
657
- const plugins = options.plugins ?? [];
658
- const requiresMaterializedSnapshots = plugins.some(
659
- (plugin) => !!plugin.afterPull
660
- );
661
-
662
- let responseToApply = requiresMaterializedSnapshots
663
- ? await materializeChunkedSnapshots(transport, rawResponse, options.sha256)
664
- : rawResponse;
665
- for (const plugin of plugins) {
666
- if (!plugin.afterPull) continue;
667
- responseToApply = await plugin.afterPull(ctx, {
668
- request,
669
- response: responseToApply,
670
- });
671
- }
672
-
673
- await db.transaction().execute(async (trx) => {
674
- const desiredIds = new Set((options.subscriptions ?? []).map((s) => s.id));
675
-
676
- // Remove local data for subscriptions that are no longer desired.
677
- for (const row of existing) {
678
- if (desiredIds.has(row.subscription_id)) continue;
679
-
680
- // Clear data for this table matching the subscription's scopes
681
- if (row.table) {
682
- try {
683
- const scopes = row.scopes_json
684
- ? typeof row.scopes_json === 'string'
685
- ? JSON.parse(row.scopes_json)
686
- : row.scopes_json
687
- : {};
688
- await getClientHandlerOrThrow(handlers, row.table).clearAll({
689
- trx,
690
- scopes,
691
- });
692
- } catch {
693
- // ignore missing table handler
694
- }
695
- }
696
-
697
- await sql`
698
- delete from ${sql.table('sync_subscription_state')}
699
- where ${sql.ref('state_id')} = ${sql.val(stateId)}
700
- and ${sql.ref('subscription_id')} = ${sql.val(row.subscription_id)}
701
- `.execute(trx);
702
- }
703
-
704
- const subsById = new Map<string, (typeof options.subscriptions)[number]>();
705
- for (const s of options.subscriptions ?? []) subsById.set(s.id, s);
706
-
707
- for (const sub of responseToApply.subscriptions) {
708
- const def = subsById.get(sub.id);
709
- const prev = existingById.get(sub.id);
710
-
711
- // Revoked: clear data and drop the subscription row.
712
- if (sub.status === 'revoked') {
713
- if (prev?.table) {
714
- try {
715
- const scopes = prev.scopes_json
716
- ? typeof prev.scopes_json === 'string'
717
- ? JSON.parse(prev.scopes_json)
718
- : prev.scopes_json
719
- : {};
720
- await getClientHandlerOrThrow(handlers, prev.table).clearAll({
721
- trx,
722
- scopes,
723
- });
724
- } catch {
725
- // ignore missing handler
726
- }
727
- }
728
-
729
- await sql`
730
- delete from ${sql.table('sync_subscription_state')}
731
- where ${sql.ref('state_id')} = ${sql.val(stateId)}
732
- and ${sql.ref('subscription_id')} = ${sql.val(sub.id)}
733
- `.execute(trx);
734
- continue;
735
- }
736
-
737
- // Apply snapshots (bootstrap mode)
738
- if (sub.bootstrap) {
739
- for (const snapshot of sub.snapshots ?? []) {
740
- const handler = getClientHandlerOrThrow(handlers, snapshot.table);
741
- const hasChunkRefs =
742
- Array.isArray(snapshot.chunks) && snapshot.chunks.length > 0;
743
-
744
- // Call onSnapshotStart hook when starting a new snapshot
745
- if (snapshot.isFirstPage && handler.onSnapshotStart) {
746
- await handler.onSnapshotStart({
747
- trx,
748
- table: snapshot.table,
749
- scopes: sub.scopes,
750
- });
751
- }
752
-
753
- if (hasChunkRefs) {
754
- await applyChunkedSnapshot(transport, handler, trx, snapshot);
755
- } else {
756
- await handler.applySnapshot({ trx }, snapshot);
757
- }
758
-
759
- // Call onSnapshotEnd hook when snapshot is complete
760
- if (snapshot.isLastPage && handler.onSnapshotEnd) {
761
- await handler.onSnapshotEnd({
762
- trx,
763
- table: snapshot.table,
764
- scopes: sub.scopes,
765
- });
766
- }
767
- }
768
- } else {
769
- // Apply incremental changes
770
- for (const commit of sub.commits) {
771
- for (const change of commit.changes) {
772
- const handler = getClientHandlerOrThrow(handlers, change.table);
773
- await handler.applyChange({ trx }, change);
774
- }
775
- }
776
- }
777
-
778
- // Persist subscription cursor + metadata.
779
- // Use cached JSON serialization to avoid repeated stringification
780
- const now = Date.now();
781
- const paramsJson = serializeJsonCached(def?.params ?? {});
782
- const scopesJson = serializeJsonCached(def?.scopes ?? {});
783
- const bootstrapStateJson = sub.bootstrap
784
- ? sub.bootstrapState
785
- ? serializeJsonCached(sub.bootstrapState)
786
- : null
787
- : null;
788
-
789
- const table = def?.table ?? 'unknown';
790
- await sql`
791
- insert into ${sql.table('sync_subscription_state')} (
792
- ${sql.join([
793
- sql.ref('state_id'),
794
- sql.ref('subscription_id'),
795
- sql.ref('table'),
796
- sql.ref('scopes_json'),
797
- sql.ref('params_json'),
798
- sql.ref('cursor'),
799
- sql.ref('bootstrap_state_json'),
800
- sql.ref('status'),
801
- sql.ref('created_at'),
802
- sql.ref('updated_at'),
803
- ])}
804
- ) values (
805
- ${sql.join([
806
- sql.val(stateId),
807
- sql.val(sub.id),
808
- sql.val(table),
809
- sql.val(scopesJson),
810
- sql.val(paramsJson),
811
- sql.val(sub.nextCursor),
812
- sql.val(bootstrapStateJson),
813
- sql.val('active'),
814
- sql.val(now),
815
- sql.val(now),
816
- ])}
817
- )
818
- on conflict (${sql.join([sql.ref('state_id'), sql.ref('subscription_id')])})
819
- do update set
820
- ${sql.ref('table')} = ${sql.val(table)},
821
- ${sql.ref('scopes_json')} = ${sql.val(scopesJson)},
822
- ${sql.ref('params_json')} = ${sql.val(paramsJson)},
823
- ${sql.ref('cursor')} = ${sql.val(sub.nextCursor)},
824
- ${sql.ref('bootstrap_state_json')} = ${sql.val(bootstrapStateJson)},
825
- ${sql.ref('status')} = ${sql.val('active')},
826
- ${sql.ref('updated_at')} = ${sql.val(now)}
827
- `.execute(trx);
828
- }
829
- });
830
-
831
- return responseToApply;
832
- }
833
-
834
- export async function syncPullOnce<DB extends SyncClientDb>(
835
- db: Kysely<DB>,
836
- transport: SyncTransport,
837
- handlers: ClientHandlerCollection<DB>,
838
- options: SyncPullOnceOptions,
839
- pullStateOverride?: SyncPullRequestState
840
- ): Promise<SyncPullResponse> {
841
- const pullState = pullStateOverride ?? (await buildPullRequest(db, options));
842
- const { clientId, ...pullBody } = pullState.request;
843
- const combined = await transport.sync({ clientId, pull: pullBody });
844
- if (!combined.pull) {
845
- return { ok: true, subscriptions: [] };
846
- }
847
- return applyPullResponse(
848
- db,
849
- transport,
850
- handlers,
851
- options,
852
- pullState,
853
- combined.pull
854
- );
855
- }