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