@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
package/src/migrate.ts DELETED
@@ -1,318 +0,0 @@
1
- /**
2
- * @syncular/client - Sync migrations (SQLite reference)
3
- */
4
-
5
- import { type Kysely, sql } from 'kysely';
6
- import type { SyncClientDb } from './schema';
7
-
8
- type SyncInternalTable =
9
- | 'sync_subscription_state'
10
- | 'sync_outbox_commits'
11
- | 'sync_conflicts';
12
-
13
- function toErrorMessage(error: unknown): string {
14
- return error instanceof Error ? error.message : String(error);
15
- }
16
-
17
- function isMissingTableError(message: string): boolean {
18
- const normalized = message.toLowerCase();
19
- return (
20
- normalized.includes('no such table') ||
21
- (normalized.includes('relation') && normalized.includes('does not exist'))
22
- );
23
- }
24
-
25
- function isMissingColumnError(message: string): boolean {
26
- const normalized = message.toLowerCase();
27
- return (
28
- normalized.includes('no such column') ||
29
- (normalized.includes('column') && normalized.includes('does not exist'))
30
- );
31
- }
32
-
33
- function isDuplicateColumnError(message: string): boolean {
34
- const normalized = message.toLowerCase();
35
- return (
36
- normalized.includes('duplicate column name') ||
37
- (normalized.includes('column') && normalized.includes('already exists'))
38
- );
39
- }
40
-
41
- async function getColumnNames<DB extends SyncClientDb>(
42
- db: Kysely<DB>,
43
- tableName: SyncInternalTable
44
- ): Promise<Set<string> | null> {
45
- try {
46
- const sqlite = await sql<{ name: string }>`
47
- select name from pragma_table_info(${sql.val(tableName)})
48
- `.execute(db);
49
- return new Set(sqlite.rows.map((row) => String(row.name)));
50
- } catch {
51
- // Not SQLite or pragma unavailable.
52
- }
53
-
54
- try {
55
- const postgres = await sql<{ name: string }>`
56
- select column_name as name
57
- from information_schema.columns
58
- where table_name = ${sql.val(tableName)}
59
- `.execute(db);
60
- return new Set(postgres.rows.map((row) => String(row.name)));
61
- } catch {
62
- // Introspection unavailable; caller falls back to probing.
63
- }
64
-
65
- return null;
66
- }
67
-
68
- async function hasColumn<DB extends SyncClientDb>(
69
- db: Kysely<DB>,
70
- tableName: SyncInternalTable,
71
- columnName: string
72
- ): Promise<boolean> {
73
- const columns = await getColumnNames(db, tableName);
74
- if (columns) {
75
- return columns.has(columnName);
76
- }
77
-
78
- try {
79
- await sql`select ${sql.ref(columnName)} from ${sql.table(tableName)} limit 1`.execute(
80
- db
81
- );
82
- return true;
83
- } catch (error) {
84
- const message = toErrorMessage(error);
85
- if (isMissingTableError(message) || isMissingColumnError(message)) {
86
- return false;
87
- }
88
- throw error;
89
- }
90
- }
91
-
92
- async function addColumnIfMissing<DB extends SyncClientDb>(
93
- db: Kysely<DB>,
94
- tableName: SyncInternalTable,
95
- columnName: string,
96
- addColumn: () => Promise<void>
97
- ): Promise<void> {
98
- if (await hasColumn(db, tableName, columnName)) {
99
- return;
100
- }
101
- try {
102
- await addColumn();
103
- } catch (error) {
104
- const message = toErrorMessage(error);
105
- if (isDuplicateColumnError(message)) {
106
- return;
107
- }
108
- throw error;
109
- }
110
- }
111
-
112
- async function ensureClientSyncSchemaCompat<DB extends SyncClientDb>(
113
- db: Kysely<DB>
114
- ): Promise<void> {
115
- const hasTableColumn = await hasColumn(
116
- db,
117
- 'sync_subscription_state',
118
- 'table'
119
- );
120
- if (
121
- !hasTableColumn &&
122
- (await hasColumn(db, 'sync_subscription_state', 'shape'))
123
- ) {
124
- try {
125
- await sql`alter table ${sql.table('sync_subscription_state')} rename column ${sql.ref('shape')} to ${sql.ref('table')}`.execute(
126
- db
127
- );
128
- } catch {
129
- await addColumnIfMissing(
130
- db,
131
- 'sync_subscription_state',
132
- 'table',
133
- async () => {
134
- await db.schema
135
- .alterTable('sync_subscription_state')
136
- .addColumn('table', 'text', (col) => col.notNull().defaultTo(''))
137
- .execute();
138
- }
139
- );
140
- await sql`update ${sql.table('sync_subscription_state')}
141
- set ${sql.ref('table')} = ${sql.ref('shape')}
142
- where ${sql.ref('table')} = ${sql.val('')}`.execute(db);
143
- }
144
- }
145
-
146
- await addColumnIfMissing(
147
- db,
148
- 'sync_subscription_state',
149
- 'bootstrap_state_json',
150
- async () => {
151
- await db.schema
152
- .alterTable('sync_subscription_state')
153
- .addColumn('bootstrap_state_json', 'text')
154
- .execute();
155
- }
156
- );
157
-
158
- await addColumnIfMissing(
159
- db,
160
- 'sync_outbox_commits',
161
- 'schema_version',
162
- async () => {
163
- await db.schema
164
- .alterTable('sync_outbox_commits')
165
- .addColumn('schema_version', 'integer', (col) =>
166
- col.notNull().defaultTo(1)
167
- )
168
- .execute();
169
- }
170
- );
171
-
172
- await addColumnIfMissing(db, 'sync_conflicts', 'resolved_at', async () => {
173
- await db.schema
174
- .alterTable('sync_conflicts')
175
- .addColumn('resolved_at', 'bigint')
176
- .execute();
177
- });
178
-
179
- await addColumnIfMissing(db, 'sync_conflicts', 'resolution', async () => {
180
- await db.schema
181
- .alterTable('sync_conflicts')
182
- .addColumn('resolution', 'text')
183
- .execute();
184
- });
185
-
186
- await db.schema
187
- .createIndex('idx_sync_outbox_commits_status_updated_at')
188
- .ifNotExists()
189
- .on('sync_outbox_commits')
190
- .columns(['status', 'updated_at', 'created_at'])
191
- .execute();
192
- }
193
-
194
- /**
195
- * Ensures the client sync schema exists in the database.
196
- * Safe to call multiple times (idempotent).
197
- *
198
- * @typeParam DB - Your database type that extends SyncClientDb
199
- */
200
- export async function ensureClientSyncSchema<DB extends SyncClientDb>(
201
- db: Kysely<DB>
202
- ): Promise<void> {
203
- // Schema builder doesn't need typed access - operates on raw SQL.
204
- await db.schema
205
- .createTable('sync_subscription_state')
206
- .ifNotExists()
207
- .addColumn('state_id', 'text', (col) => col.notNull())
208
- .addColumn('subscription_id', 'text', (col) => col.notNull())
209
- .addColumn('table', 'text', (col) => col.notNull())
210
- .addColumn('scopes_json', 'text', (col) => col.notNull().defaultTo('{}'))
211
- .addColumn('params_json', 'text', (col) => col.notNull())
212
- .addColumn('cursor', 'bigint', (col) => col.notNull())
213
- .addColumn('bootstrap_state_json', 'text')
214
- .addColumn('status', 'text', (col) => col.notNull())
215
- .addColumn('created_at', 'bigint', (col) => col.notNull())
216
- .addColumn('updated_at', 'bigint', (col) => col.notNull())
217
- .execute();
218
-
219
- await db.schema
220
- .createTable('sync_outbox_commits')
221
- .ifNotExists()
222
- .addColumn('id', 'text', (col) => col.primaryKey())
223
- .addColumn('client_commit_id', 'text', (col) => col.notNull())
224
- .addColumn('status', 'text', (col) => col.notNull())
225
- .addColumn('operations_json', 'text', (col) => col.notNull())
226
- .addColumn('last_response_json', 'text')
227
- .addColumn('error', 'text')
228
- .addColumn('created_at', 'bigint', (col) => col.notNull())
229
- .addColumn('updated_at', 'bigint', (col) => col.notNull())
230
- .addColumn('attempt_count', 'integer', (col) => col.notNull().defaultTo(0))
231
- .addColumn('acked_commit_seq', 'bigint')
232
- .addColumn('schema_version', 'integer', (col) => col.notNull().defaultTo(1))
233
- .execute();
234
-
235
- await db.schema
236
- .createTable('sync_conflicts')
237
- .ifNotExists()
238
- .addColumn('id', 'text', (col) => col.primaryKey())
239
- .addColumn('outbox_commit_id', 'text', (col) => col.notNull())
240
- .addColumn('client_commit_id', 'text', (col) => col.notNull())
241
- .addColumn('op_index', 'integer', (col) => col.notNull())
242
- .addColumn('result_status', 'text', (col) => col.notNull())
243
- .addColumn('message', 'text', (col) => col.notNull())
244
- .addColumn('code', 'text')
245
- .addColumn('server_version', 'bigint')
246
- .addColumn('server_row_json', 'text')
247
- .addColumn('created_at', 'bigint', (col) => col.notNull())
248
- .addColumn('resolved_at', 'bigint')
249
- .addColumn('resolution', 'text')
250
- .execute();
251
-
252
- // Apply framework-managed compatibility upgrades for legacy sync tables.
253
- await ensureClientSyncSchemaCompat(db);
254
-
255
- await db.schema
256
- .createIndex('idx_sync_subscription_state_state_sub')
257
- .ifNotExists()
258
- .on('sync_subscription_state')
259
- .columns(['state_id', 'subscription_id'])
260
- .unique()
261
- .execute();
262
-
263
- await db.schema
264
- .createIndex('idx_sync_subscription_state_state')
265
- .ifNotExists()
266
- .on('sync_subscription_state')
267
- .columns(['state_id', 'updated_at'])
268
- .execute();
269
-
270
- await db.schema
271
- .createIndex('idx_sync_outbox_commits_client_commit_id')
272
- .ifNotExists()
273
- .on('sync_outbox_commits')
274
- .columns(['client_commit_id'])
275
- .unique()
276
- .execute();
277
-
278
- await db.schema
279
- .createIndex('idx_sync_outbox_commits_status_created_at')
280
- .ifNotExists()
281
- .on('sync_outbox_commits')
282
- .columns(['status', 'created_at'])
283
- .execute();
284
-
285
- await db.schema
286
- .createIndex('idx_sync_outbox_commits_status_updated_at')
287
- .ifNotExists()
288
- .on('sync_outbox_commits')
289
- .columns(['status', 'updated_at', 'created_at'])
290
- .execute();
291
-
292
- await db.schema
293
- .createIndex('idx_sync_conflicts_outbox_commit')
294
- .ifNotExists()
295
- .on('sync_conflicts')
296
- .columns(['outbox_commit_id'])
297
- .execute();
298
-
299
- await db.schema
300
- .createIndex('idx_sync_conflicts_resolved_at')
301
- .ifNotExists()
302
- .on('sync_conflicts')
303
- .columns(['resolved_at'])
304
- .execute();
305
- }
306
-
307
- /**
308
- * Drops the client sync schema from the database.
309
- *
310
- * @typeParam DB - Your database type that extends SyncClientDb
311
- */
312
- export async function dropClientSyncSchema<DB extends SyncClientDb>(
313
- db: Kysely<DB>
314
- ): Promise<void> {
315
- await db.schema.dropTable('sync_conflicts').ifExists().execute();
316
- await db.schema.dropTable('sync_outbox_commits').ifExists().execute();
317
- await db.schema.dropTable('sync_subscription_state').ifExists().execute();
318
- }
package/src/outbox.ts DELETED
@@ -1,383 +0,0 @@
1
- /**
2
- * @syncular/client - Sync outbox (commit-based)
3
- */
4
-
5
- import type { SyncOperation } from '@syncular/core';
6
- import { isRecord, randomId } from '@syncular/core';
7
- import type { Kysely } from 'kysely';
8
- import { sql } from 'kysely';
9
- import type { OutboxCommitStatus, SyncClientDb } from './schema';
10
-
11
- export interface OutboxCommit {
12
- id: string;
13
- client_commit_id: string;
14
- status: OutboxCommitStatus;
15
- operations: SyncOperation[];
16
- last_response_json: string | null;
17
- error: string | null;
18
- created_at: number;
19
- updated_at: number;
20
- attempt_count: number;
21
- acked_commit_seq: number | null;
22
- /** Client schema version when commit was created */
23
- schema_version: number;
24
- }
25
-
26
- function isSyncOperation(value: unknown): value is SyncOperation {
27
- if (!isRecord(value)) return false;
28
- if (typeof value.table !== 'string') return false;
29
- if (typeof value.row_id !== 'string') return false;
30
- if (value.op !== 'upsert' && value.op !== 'delete') return false;
31
- if (value.payload !== null && !isRecord(value.payload)) return false;
32
- if (!('base_version' in value)) return true;
33
- return (
34
- value.base_version === null ||
35
- value.base_version === undefined ||
36
- typeof value.base_version === 'number'
37
- );
38
- }
39
-
40
- function parseOperations(value: unknown): SyncOperation[] {
41
- let parsed: unknown = value;
42
- if (typeof value === 'string') {
43
- try {
44
- parsed = JSON.parse(value);
45
- } catch {
46
- return [];
47
- }
48
- }
49
-
50
- if (!Array.isArray(parsed)) return [];
51
-
52
- const out: SyncOperation[] = [];
53
- for (const item of parsed) {
54
- if (!isSyncOperation(item)) continue;
55
- out.push(item);
56
- }
57
- return out;
58
- }
59
-
60
- export async function enqueueOutboxCommit<DB extends SyncClientDb>(
61
- db: Kysely<DB>,
62
- args: {
63
- operations: SyncOperation[];
64
- clientCommitId?: string;
65
- nowMs?: number;
66
- /** Client schema version (default: 1) */
67
- schemaVersion?: number;
68
- }
69
- ): Promise<{ id: string; clientCommitId: string }> {
70
- const now = args.nowMs ?? Date.now();
71
- const id = randomId();
72
- const clientCommitId = args.clientCommitId ?? randomId();
73
- const schemaVersion = args.schemaVersion ?? 1;
74
-
75
- await sql`
76
- insert into ${sql.table('sync_outbox_commits')} (
77
- ${sql.join([
78
- sql.ref('id'),
79
- sql.ref('client_commit_id'),
80
- sql.ref('status'),
81
- sql.ref('operations_json'),
82
- sql.ref('last_response_json'),
83
- sql.ref('error'),
84
- sql.ref('created_at'),
85
- sql.ref('updated_at'),
86
- sql.ref('attempt_count'),
87
- sql.ref('acked_commit_seq'),
88
- sql.ref('schema_version'),
89
- ])}
90
- ) values (
91
- ${sql.join([
92
- sql.val(id),
93
- sql.val(clientCommitId),
94
- sql.val('pending'),
95
- sql.val(JSON.stringify(args.operations ?? [])),
96
- sql.val(null),
97
- sql.val(null),
98
- sql.val(now),
99
- sql.val(now),
100
- sql.val(0),
101
- sql.val(null),
102
- sql.val(schemaVersion),
103
- ])}
104
- )
105
- `.execute(db);
106
-
107
- return { id, clientCommitId };
108
- }
109
-
110
- /**
111
- * Atomically claim and return the next sendable outbox commit.
112
- *
113
- * Uses SELECT then UPDATE with status check to prevent race conditions.
114
- * If another tab claims the same commit, retries with a different candidate.
115
- */
116
- export async function getNextSendableOutboxCommit<DB extends SyncClientDb>(
117
- db: Kysely<DB>,
118
- options?: { staleTimeoutMs?: number; maxRetries?: number }
119
- ): Promise<OutboxCommit | null> {
120
- const staleTimeoutMs = options?.staleTimeoutMs ?? 30000;
121
- const maxRetries = options?.maxRetries ?? 3;
122
-
123
- // Track IDs we've already tried to claim (to avoid retrying the same one)
124
- const attemptedIds = new Set<string>();
125
-
126
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
127
- const staleThreshold = Date.now() - staleTimeoutMs;
128
- const now = Date.now();
129
-
130
- // Find a candidate to claim, excluding ones we've already tried
131
- const attempted = Array.from(attemptedIds);
132
- const attemptedFilter =
133
- attempted.length === 0
134
- ? sql``
135
- : sql`and ${sql.ref('id')} not in (${sql.join(
136
- attempted.map((id) => sql.val(id)),
137
- sql`, `
138
- )})`;
139
-
140
- const candidateResult = await sql<{ id: string }>`
141
- select ${sql.ref('id')}
142
- from ${sql.table('sync_outbox_commits')}
143
- where (
144
- ${sql.ref('status')} = ${sql.val('pending')}
145
- or (
146
- ${sql.ref('status')} = ${sql.val('sending')}
147
- and ${sql.ref('updated_at')} < ${sql.val(staleThreshold)}
148
- )
149
- )
150
- ${attemptedFilter}
151
- order by ${sql.ref('created_at')} asc
152
- limit ${sql.val(1)}
153
- `.execute(db);
154
- const candidate = candidateResult.rows[0];
155
- if (!candidate) return null;
156
-
157
- attemptedIds.add(candidate.id);
158
-
159
- // Atomically claim the commit using UPDATE...WHERE with status check
160
- const claimResult = await sql`
161
- update ${sql.table('sync_outbox_commits')}
162
- set
163
- ${sql.ref('status')} = ${sql.val('sending')},
164
- ${sql.ref('updated_at')} = ${sql.val(now)},
165
- ${sql.ref('attempt_count')} = ${sql.ref('attempt_count')} + ${sql.val(1)},
166
- ${sql.ref('error')} = ${sql.val(null)},
167
- ${sql.ref('last_response_json')} = ${sql.val(null)}
168
- where ${sql.ref('id')} = ${sql.val(candidate.id)}
169
- and (
170
- ${sql.ref('status')} = ${sql.val('pending')}
171
- or (
172
- ${sql.ref('status')} = ${sql.val('sending')}
173
- and ${sql.ref('updated_at')} < ${sql.val(staleThreshold)}
174
- )
175
- )
176
- `.execute(db);
177
-
178
- const claimed = Number(claimResult.numAffectedRows ?? 0) > 0;
179
- if (claimed) {
180
- const rowResult = await sql<{
181
- id: string;
182
- client_commit_id: string;
183
- status: OutboxCommitStatus;
184
- operations_json: string;
185
- last_response_json: string | null;
186
- error: string | null;
187
- created_at: number;
188
- updated_at: number;
189
- attempt_count: number;
190
- acked_commit_seq: number | null;
191
- schema_version: number | null;
192
- }>`
193
- select
194
- ${sql.ref('id')},
195
- ${sql.ref('client_commit_id')},
196
- ${sql.ref('status')},
197
- ${sql.ref('operations_json')},
198
- ${sql.ref('last_response_json')},
199
- ${sql.ref('error')},
200
- ${sql.ref('created_at')},
201
- ${sql.ref('updated_at')},
202
- ${sql.ref('attempt_count')},
203
- ${sql.ref('acked_commit_seq')},
204
- ${sql.ref('schema_version')}
205
- from ${sql.table('sync_outbox_commits')}
206
- where ${sql.ref('id')} = ${sql.val(candidate.id)}
207
- `.execute(db);
208
- const row = rowResult.rows[0];
209
- if (!row) continue;
210
-
211
- return {
212
- id: row.id,
213
- client_commit_id: row.client_commit_id,
214
- status: row.status,
215
- operations: parseOperations(row.operations_json),
216
- last_response_json: row.last_response_json,
217
- error: row.error,
218
- created_at: row.created_at,
219
- updated_at: row.updated_at,
220
- attempt_count: row.attempt_count,
221
- acked_commit_seq: row.acked_commit_seq,
222
- schema_version: row.schema_version ?? 1,
223
- };
224
- }
225
-
226
- // Another tab claimed it, retry with next candidate
227
- }
228
-
229
- return null;
230
- }
231
-
232
- async function markOutboxCommitSending<DB extends SyncClientDb>(
233
- db: Kysely<DB>,
234
- id: string
235
- ): Promise<void> {
236
- const now = Date.now();
237
-
238
- await sql`
239
- update ${sql.table('sync_outbox_commits')}
240
- set
241
- ${sql.ref('status')} = ${sql.val('sending')},
242
- ${sql.ref('updated_at')} = ${sql.val(now)},
243
- ${sql.ref('attempt_count')} = ${sql.ref('attempt_count')} + ${sql.val(1)},
244
- ${sql.ref('error')} = ${sql.val(null)},
245
- ${sql.ref('last_response_json')} = ${sql.val(null)}
246
- where ${sql.ref('id')} = ${sql.val(id)}
247
- `.execute(db);
248
- }
249
-
250
- export async function markOutboxCommitAcked<DB extends SyncClientDb>(
251
- db: Kysely<DB>,
252
- args: { id: string; commitSeq?: number | null; responseJson?: string | null }
253
- ): Promise<void> {
254
- const now = Date.now();
255
-
256
- await sql`
257
- update ${sql.table('sync_outbox_commits')}
258
- set
259
- ${sql.ref('status')} = ${sql.val('acked')},
260
- ${sql.ref('updated_at')} = ${sql.val(now)},
261
- ${sql.ref('acked_commit_seq')} = ${sql.val(args.commitSeq ?? null)},
262
- ${sql.ref('error')} = ${sql.val(null)},
263
- ${sql.ref('last_response_json')} = ${sql.val(args.responseJson ?? null)}
264
- where ${sql.ref('id')} = ${sql.val(args.id)}
265
- `.execute(db);
266
- }
267
-
268
- export async function markOutboxCommitFailed<DB extends SyncClientDb>(
269
- db: Kysely<DB>,
270
- args: { id: string; error: string; responseJson?: string | null }
271
- ): Promise<void> {
272
- const now = Date.now();
273
-
274
- await sql`
275
- update ${sql.table('sync_outbox_commits')}
276
- set
277
- ${sql.ref('status')} = ${sql.val('failed')},
278
- ${sql.ref('updated_at')} = ${sql.val(now)},
279
- ${sql.ref('error')} = ${sql.val(args.error)},
280
- ${sql.ref('last_response_json')} = ${sql.val(args.responseJson ?? null)}
281
- where ${sql.ref('id')} = ${sql.val(args.id)}
282
- `.execute(db);
283
- }
284
-
285
- export async function markOutboxCommitPending<DB extends SyncClientDb>(
286
- db: Kysely<DB>,
287
- args: { id: string; error?: string | null; responseJson?: string | null }
288
- ): Promise<void> {
289
- const now = Date.now();
290
-
291
- await sql`
292
- update ${sql.table('sync_outbox_commits')}
293
- set
294
- ${sql.ref('status')} = ${sql.val('pending')},
295
- ${sql.ref('updated_at')} = ${sql.val(now)},
296
- ${sql.ref('error')} = ${sql.val(args.error ?? null)},
297
- ${sql.ref('last_response_json')} = ${sql.val(args.responseJson ?? null)}
298
- where ${sql.ref('id')} = ${sql.val(args.id)}
299
- `.execute(db);
300
- }
301
-
302
- async function deleteAckedOutboxCommits<DB extends SyncClientDb>(
303
- db: Kysely<DB>
304
- ): Promise<number> {
305
- const res = await sql`
306
- delete from ${sql.table('sync_outbox_commits')}
307
- where ${sql.ref('status')} = ${sql.val('acked')}
308
- `.execute(db);
309
- return Number(res.numAffectedRows ?? 0);
310
- }
311
-
312
- async function deleteFailedOutboxCommits<DB extends SyncClientDb>(
313
- db: Kysely<DB>
314
- ): Promise<number> {
315
- const res = await sql`
316
- delete from ${sql.table('sync_outbox_commits')}
317
- where ${sql.ref('status')} = ${sql.val('failed')}
318
- `.execute(db);
319
- return Number(res.numAffectedRows ?? 0);
320
- }
321
-
322
- async function clearAllOutboxCommits<DB extends SyncClientDb>(
323
- db: Kysely<DB>
324
- ): Promise<number> {
325
- const res =
326
- await sql`delete from ${sql.table('sync_outbox_commits')}`.execute(db);
327
- return Number(res.numAffectedRows ?? 0);
328
- }
329
-
330
- /**
331
- * Outbox namespace - organized API for outbox operations.
332
- *
333
- * @example
334
- * ```typescript
335
- * import { outbox } from '@syncular/client';
336
- *
337
- * // Enqueue a new commit
338
- * const { id } = await outbox.enqueue(db, { operations: [...] });
339
- *
340
- * // Get next sendable commit
341
- * const commit = await outbox.getNextSendable(db);
342
- *
343
- * // Mark commit status
344
- * await outbox.mark.acked(db, { id, commitSeq: 42 });
345
- * await outbox.mark.failed(db, { id, error: 'Network error' });
346
- * await outbox.mark.pending(db, { id });
347
- * await outbox.mark.sending(db, id);
348
- *
349
- * // Cleanup commits
350
- * await outbox.cleanup.acked(db);
351
- * await outbox.cleanup.failed(db);
352
- * await outbox.cleanup.all(db);
353
- * ```
354
- */
355
- export const outbox = {
356
- /** Enqueue a new commit to the outbox */
357
- enqueue: enqueueOutboxCommit,
358
-
359
- /** Get the next sendable commit (atomically claims it) */
360
- getNextSendable: getNextSendableOutboxCommit,
361
-
362
- /** Mark commit status */
363
- mark: {
364
- /** Mark commit as sending (in progress) */
365
- sending: markOutboxCommitSending,
366
- /** Mark commit as acknowledged (successfully synced) */
367
- acked: markOutboxCommitAcked,
368
- /** Mark commit as failed (permanent error) */
369
- failed: markOutboxCommitFailed,
370
- /** Mark commit as pending (ready for retry) */
371
- pending: markOutboxCommitPending,
372
- },
373
-
374
- /** Cleanup operations */
375
- cleanup: {
376
- /** Delete all acknowledged commits */
377
- acked: deleteAckedOutboxCommits,
378
- /** Delete all failed commits */
379
- failed: deleteFailedOutboxCommits,
380
- /** Delete all commits (clear entire outbox) */
381
- all: clearAllOutboxCommits,
382
- },
383
- } as const;