@syncular/client 0.0.6-95 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (356) hide show
  1. package/README.md +713 -11
  2. package/dist/auth-leases.d.ts +11 -0
  3. package/dist/auth-leases.d.ts.map +1 -0
  4. package/dist/auth-leases.js +140 -0
  5. package/dist/auth-leases.js.map +1 -0
  6. package/dist/blob-limits.d.ts +15 -0
  7. package/dist/blob-limits.d.ts.map +1 -0
  8. package/dist/blob-limits.js +66 -0
  9. package/dist/blob-limits.js.map +1 -0
  10. package/dist/bridge-client.d.ts +69 -0
  11. package/dist/bridge-client.d.ts.map +1 -0
  12. package/dist/bridge-client.js +282 -0
  13. package/dist/bridge-client.js.map +1 -0
  14. package/dist/client-config.d.ts +6 -0
  15. package/dist/client-config.d.ts.map +1 -0
  16. package/dist/client-config.js +18 -0
  17. package/dist/client-config.js.map +1 -0
  18. package/dist/client.d.ts +65 -397
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +150 -960
  21. package/dist/client.js.map +1 -1
  22. package/dist/command-history.d.ts +35 -0
  23. package/dist/command-history.d.ts.map +1 -0
  24. package/dist/command-history.js +378 -0
  25. package/dist/command-history.js.map +1 -0
  26. package/dist/console-diagnostics.d.ts +33 -0
  27. package/dist/console-diagnostics.d.ts.map +1 -0
  28. package/dist/console-diagnostics.js +496 -0
  29. package/dist/console-diagnostics.js.map +1 -0
  30. package/dist/crdt-yjs/index.d.ts +4 -0
  31. package/dist/crdt-yjs/index.d.ts.map +1 -0
  32. package/dist/crdt-yjs/index.js +4 -0
  33. package/dist/crdt-yjs/index.js.map +1 -0
  34. package/dist/crdt-yjs/webview-host-facade.d.ts +126 -0
  35. package/dist/crdt-yjs/webview-host-facade.d.ts.map +1 -0
  36. package/dist/crdt-yjs/webview-host-facade.js +284 -0
  37. package/dist/crdt-yjs/webview-host-facade.js.map +1 -0
  38. package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +153 -0
  39. package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +1 -0
  40. package/dist/crdt-yjs/yjs-document-field-adapter.js +406 -0
  41. package/dist/crdt-yjs/yjs-document-field-adapter.js.map +1 -0
  42. package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +73 -0
  43. package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +1 -0
  44. package/dist/crdt-yjs/yjs-prosemirror-bridge.js +169 -0
  45. package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +1 -0
  46. package/dist/database.d.ts +83 -0
  47. package/dist/database.d.ts.map +1 -0
  48. package/dist/database.js +916 -0
  49. package/dist/database.js.map +1 -0
  50. package/dist/diagnostics.d.ts +10 -0
  51. package/dist/diagnostics.d.ts.map +1 -0
  52. package/dist/diagnostics.js +83 -0
  53. package/dist/diagnostics.js.map +1 -0
  54. package/dist/errors.d.ts +24 -0
  55. package/dist/errors.d.ts.map +1 -0
  56. package/dist/errors.js +211 -0
  57. package/dist/errors.js.map +1 -0
  58. package/dist/generated-bridge.d.ts +365 -0
  59. package/dist/generated-bridge.d.ts.map +1 -0
  60. package/dist/generated-bridge.js +250 -0
  61. package/dist/generated-bridge.js.map +1 -0
  62. package/dist/index.d.ts +13 -23
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +14 -25
  65. package/dist/index.js.map +1 -1
  66. package/dist/mutations.d.ts +4 -72
  67. package/dist/mutations.d.ts.map +1 -1
  68. package/dist/mutations.js +2 -568
  69. package/dist/mutations.js.map +1 -1
  70. package/dist/network.d.ts +3 -0
  71. package/dist/network.d.ts.map +1 -0
  72. package/dist/network.js +17 -0
  73. package/dist/network.js.map +1 -0
  74. package/dist/react/index.d.ts +169 -0
  75. package/dist/react/index.d.ts.map +1 -0
  76. package/dist/react/index.js +628 -0
  77. package/dist/react/index.js.map +1 -0
  78. package/dist/react-native/index.d.ts +35 -0
  79. package/dist/react-native/index.d.ts.map +1 -0
  80. package/dist/react-native/index.js +49 -0
  81. package/dist/react-native/index.js.map +1 -0
  82. package/dist/runtime-contract.d.ts +13 -0
  83. package/dist/runtime-contract.d.ts.map +1 -0
  84. package/dist/runtime-contract.js +24 -0
  85. package/dist/runtime-contract.js.map +1 -0
  86. package/dist/rust-client.d.ts +106 -0
  87. package/dist/rust-client.d.ts.map +1 -0
  88. package/dist/rust-client.js +936 -0
  89. package/dist/rust-client.js.map +1 -0
  90. package/dist/sentry.d.ts +35 -0
  91. package/dist/sentry.d.ts.map +1 -0
  92. package/dist/sentry.js +155 -0
  93. package/dist/sentry.js.map +1 -0
  94. package/dist/sql-safety.d.ts +3 -0
  95. package/dist/sql-safety.d.ts.map +1 -0
  96. package/dist/sql-safety.js +62 -0
  97. package/dist/sql-safety.js.map +1 -0
  98. package/dist/syncular-runtime-artifacts.json +61 -0
  99. package/dist/tauri/index.d.ts +35 -0
  100. package/dist/tauri/index.d.ts.map +1 -0
  101. package/dist/tauri/index.js +114 -0
  102. package/dist/tauri/index.js.map +1 -0
  103. package/dist/types.d.ts +1059 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +2 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/wasm/.syncular-wasm-profile +1 -0
  108. package/dist/wasm/syncular-runtime-artifact.json +21 -0
  109. package/dist/wasm/syncular.d.ts +207 -0
  110. package/dist/wasm/syncular.js +2341 -0
  111. package/dist/wasm/syncular_bg.wasm +0 -0
  112. package/dist/wasm/syncular_bg.wasm.d.ts +97 -0
  113. package/dist/wasm-bindings/runtime-contract.d.ts +22 -0
  114. package/dist/wasm-bindings/runtime-contract.d.ts.map +1 -0
  115. package/dist/wasm-bindings/runtime-contract.js +112 -0
  116. package/dist/wasm-bindings/runtime-contract.js.map +1 -0
  117. package/dist/wasm-core/.syncular-wasm-profile +1 -0
  118. package/dist/wasm-core/syncular-runtime-artifact.json +17 -0
  119. package/dist/wasm-core/syncular.d.ts +162 -0
  120. package/dist/wasm-core/syncular.js +1847 -0
  121. package/dist/wasm-core/syncular_bg.wasm +0 -0
  122. package/dist/wasm-core/syncular_bg.wasm.d.ts +77 -0
  123. package/dist/wasm-perf/.syncular-wasm-profile +1 -0
  124. package/dist/wasm-perf/syncular-runtime-artifact.json +21 -0
  125. package/dist/wasm-perf/syncular.d.ts +207 -0
  126. package/dist/wasm-perf/syncular.js +2341 -0
  127. package/dist/wasm-perf/syncular_bg.wasm +0 -0
  128. package/dist/wasm-perf/syncular_bg.wasm.d.ts +97 -0
  129. package/dist/wasm-runtime.d.ts +18 -0
  130. package/dist/wasm-runtime.d.ts.map +1 -0
  131. package/dist/wasm-runtime.js +28 -0
  132. package/dist/wasm-runtime.js.map +1 -0
  133. package/dist/worker-client.d.ts +123 -0
  134. package/dist/worker-client.d.ts.map +1 -0
  135. package/dist/worker-client.js +1686 -0
  136. package/dist/worker-client.js.map +1 -0
  137. package/dist/worker-entry.d.ts +2 -0
  138. package/dist/worker-entry.d.ts.map +1 -0
  139. package/dist/worker-entry.js +479 -0
  140. package/dist/worker-entry.js.map +1 -0
  141. package/dist/worker-protocol.d.ts +64 -0
  142. package/dist/worker-protocol.d.ts.map +1 -0
  143. package/dist/worker-protocol.js +16 -0
  144. package/dist/worker-protocol.js.map +1 -0
  145. package/dist/worker-realtime.d.ts +39 -0
  146. package/dist/worker-realtime.d.ts.map +1 -0
  147. package/dist/worker-realtime.js +677 -0
  148. package/dist/worker-realtime.js.map +1 -0
  149. package/package.json +124 -17
  150. package/src/auth-leases.ts +251 -0
  151. package/src/blob-limits.ts +98 -0
  152. package/src/bridge-client.ts +512 -0
  153. package/src/client-config.ts +29 -0
  154. package/src/client.ts +256 -1414
  155. package/src/command-history.ts +623 -0
  156. package/src/console-diagnostics.ts +617 -0
  157. package/src/crdt-yjs/index.ts +3 -0
  158. package/src/crdt-yjs/webview-host-facade.ts +477 -0
  159. package/src/crdt-yjs/yjs-document-field-adapter.ts +733 -0
  160. package/src/crdt-yjs/yjs-prosemirror-bridge.ts +272 -0
  161. package/src/database.ts +1342 -0
  162. package/src/diagnostics.ts +116 -0
  163. package/src/errors.ts +269 -0
  164. package/src/generated-bridge.ts +741 -0
  165. package/src/index.ts +21 -25
  166. package/src/mutations.ts +12 -821
  167. package/src/network.ts +32 -0
  168. package/src/react/index.ts +1036 -0
  169. package/src/react-native/index.ts +152 -0
  170. package/src/runtime-contract.ts +48 -0
  171. package/src/rust-client.ts +1488 -0
  172. package/src/sentry.ts +215 -0
  173. package/src/sql-safety.ts +61 -0
  174. package/src/tauri/index.ts +211 -0
  175. package/src/types.ts +1356 -0
  176. package/src/wasm-bindings/generated-wasm-bindings.d.ts +70 -0
  177. package/src/wasm-bindings/runtime-contract.ts +158 -0
  178. package/src/wasm-runtime.ts +76 -0
  179. package/src/worker-client.ts +2215 -0
  180. package/src/worker-entry.ts +584 -0
  181. package/src/worker-protocol.ts +109 -0
  182. package/src/worker-realtime.ts +843 -0
  183. package/dist/blobs/index.d.ts +0 -7
  184. package/dist/blobs/index.d.ts.map +0 -1
  185. package/dist/blobs/index.js +0 -7
  186. package/dist/blobs/index.js.map +0 -1
  187. package/dist/blobs/manager.d.ts +0 -345
  188. package/dist/blobs/manager.d.ts.map +0 -1
  189. package/dist/blobs/manager.js +0 -749
  190. package/dist/blobs/manager.js.map +0 -1
  191. package/dist/blobs/migrate.d.ts +0 -14
  192. package/dist/blobs/migrate.d.ts.map +0 -1
  193. package/dist/blobs/migrate.js +0 -59
  194. package/dist/blobs/migrate.js.map +0 -1
  195. package/dist/blobs/types.d.ts +0 -62
  196. package/dist/blobs/types.d.ts.map +0 -1
  197. package/dist/blobs/types.js +0 -5
  198. package/dist/blobs/types.js.map +0 -1
  199. package/dist/conflicts.d.ts +0 -31
  200. package/dist/conflicts.d.ts.map +0 -1
  201. package/dist/conflicts.js +0 -112
  202. package/dist/conflicts.js.map +0 -1
  203. package/dist/create-client.d.ts +0 -118
  204. package/dist/create-client.d.ts.map +0 -1
  205. package/dist/create-client.js +0 -178
  206. package/dist/create-client.js.map +0 -1
  207. package/dist/engine/SyncEngine.d.ts +0 -272
  208. package/dist/engine/SyncEngine.d.ts.map +0 -1
  209. package/dist/engine/SyncEngine.js +0 -1860
  210. package/dist/engine/SyncEngine.js.map +0 -1
  211. package/dist/engine/index.d.ts +0 -6
  212. package/dist/engine/index.d.ts.map +0 -1
  213. package/dist/engine/index.js +0 -6
  214. package/dist/engine/index.js.map +0 -1
  215. package/dist/engine/types.d.ts +0 -345
  216. package/dist/engine/types.d.ts.map +0 -1
  217. package/dist/engine/types.js +0 -7
  218. package/dist/engine/types.js.map +0 -1
  219. package/dist/handlers/collection.d.ts +0 -6
  220. package/dist/handlers/collection.d.ts.map +0 -1
  221. package/dist/handlers/collection.js +0 -21
  222. package/dist/handlers/collection.js.map +0 -1
  223. package/dist/handlers/create-handler.d.ts +0 -120
  224. package/dist/handlers/create-handler.d.ts.map +0 -1
  225. package/dist/handlers/create-handler.js +0 -145
  226. package/dist/handlers/create-handler.js.map +0 -1
  227. package/dist/handlers/types.d.ts +0 -83
  228. package/dist/handlers/types.d.ts.map +0 -1
  229. package/dist/handlers/types.js +0 -5
  230. package/dist/handlers/types.js.map +0 -1
  231. package/dist/migrate.d.ts +0 -19
  232. package/dist/migrate.d.ts.map +0 -1
  233. package/dist/migrate.js +0 -238
  234. package/dist/migrate.js.map +0 -1
  235. package/dist/outbox.d.ts +0 -112
  236. package/dist/outbox.d.ts.map +0 -1
  237. package/dist/outbox.js +0 -294
  238. package/dist/outbox.js.map +0 -1
  239. package/dist/plugins/incrementing-version.d.ts +0 -34
  240. package/dist/plugins/incrementing-version.d.ts.map +0 -1
  241. package/dist/plugins/incrementing-version.js +0 -83
  242. package/dist/plugins/incrementing-version.js.map +0 -1
  243. package/dist/plugins/index.d.ts +0 -3
  244. package/dist/plugins/index.d.ts.map +0 -1
  245. package/dist/plugins/index.js +0 -3
  246. package/dist/plugins/index.js.map +0 -1
  247. package/dist/plugins/types.d.ts +0 -49
  248. package/dist/plugins/types.d.ts.map +0 -1
  249. package/dist/plugins/types.js +0 -15
  250. package/dist/plugins/types.js.map +0 -1
  251. package/dist/proxy/connection.d.ts +0 -33
  252. package/dist/proxy/connection.d.ts.map +0 -1
  253. package/dist/proxy/connection.js +0 -153
  254. package/dist/proxy/connection.js.map +0 -1
  255. package/dist/proxy/dialect.d.ts +0 -46
  256. package/dist/proxy/dialect.d.ts.map +0 -1
  257. package/dist/proxy/dialect.js +0 -58
  258. package/dist/proxy/dialect.js.map +0 -1
  259. package/dist/proxy/driver.d.ts +0 -42
  260. package/dist/proxy/driver.d.ts.map +0 -1
  261. package/dist/proxy/driver.js +0 -78
  262. package/dist/proxy/driver.js.map +0 -1
  263. package/dist/proxy/index.d.ts +0 -10
  264. package/dist/proxy/index.d.ts.map +0 -1
  265. package/dist/proxy/index.js +0 -10
  266. package/dist/proxy/index.js.map +0 -1
  267. package/dist/proxy/mutations.d.ts +0 -9
  268. package/dist/proxy/mutations.d.ts.map +0 -1
  269. package/dist/proxy/mutations.js +0 -11
  270. package/dist/proxy/mutations.js.map +0 -1
  271. package/dist/pull-engine.d.ts +0 -48
  272. package/dist/pull-engine.d.ts.map +0 -1
  273. package/dist/pull-engine.js +0 -648
  274. package/dist/pull-engine.js.map +0 -1
  275. package/dist/push-engine.d.ts +0 -18
  276. package/dist/push-engine.d.ts.map +0 -1
  277. package/dist/push-engine.js +0 -167
  278. package/dist/push-engine.js.map +0 -1
  279. package/dist/query/FingerprintCollector.d.ts +0 -18
  280. package/dist/query/FingerprintCollector.d.ts.map +0 -1
  281. package/dist/query/FingerprintCollector.js +0 -28
  282. package/dist/query/FingerprintCollector.js.map +0 -1
  283. package/dist/query/QueryContext.d.ts +0 -33
  284. package/dist/query/QueryContext.d.ts.map +0 -1
  285. package/dist/query/QueryContext.js +0 -16
  286. package/dist/query/QueryContext.js.map +0 -1
  287. package/dist/query/fingerprint.d.ts +0 -61
  288. package/dist/query/fingerprint.d.ts.map +0 -1
  289. package/dist/query/fingerprint.js +0 -91
  290. package/dist/query/fingerprint.js.map +0 -1
  291. package/dist/query/index.d.ts +0 -7
  292. package/dist/query/index.d.ts.map +0 -1
  293. package/dist/query/index.js +0 -7
  294. package/dist/query/index.js.map +0 -1
  295. package/dist/query/tracked-select.d.ts +0 -18
  296. package/dist/query/tracked-select.d.ts.map +0 -1
  297. package/dist/query/tracked-select.js +0 -90
  298. package/dist/query/tracked-select.js.map +0 -1
  299. package/dist/schema.d.ts +0 -83
  300. package/dist/schema.d.ts.map +0 -1
  301. package/dist/schema.js +0 -7
  302. package/dist/schema.js.map +0 -1
  303. package/dist/subscription-state.d.ts +0 -46
  304. package/dist/subscription-state.d.ts.map +0 -1
  305. package/dist/subscription-state.js +0 -185
  306. package/dist/subscription-state.js.map +0 -1
  307. package/dist/sync-loop.d.ts +0 -34
  308. package/dist/sync-loop.d.ts.map +0 -1
  309. package/dist/sync-loop.js +0 -253
  310. package/dist/sync-loop.js.map +0 -1
  311. package/dist/sync.d.ts +0 -32
  312. package/dist/sync.d.ts.map +0 -1
  313. package/dist/sync.js +0 -55
  314. package/dist/sync.js.map +0 -1
  315. package/dist/utils/id.d.ts +0 -17
  316. package/dist/utils/id.d.ts.map +0 -1
  317. package/dist/utils/id.js +0 -46
  318. package/dist/utils/id.js.map +0 -1
  319. package/src/blobs/index.ts +0 -7
  320. package/src/blobs/manager.ts +0 -1027
  321. package/src/blobs/migrate.ts +0 -67
  322. package/src/blobs/types.ts +0 -84
  323. package/src/client.test.ts +0 -417
  324. package/src/conflicts.ts +0 -171
  325. package/src/create-client.test.ts +0 -83
  326. package/src/create-client.ts +0 -330
  327. package/src/engine/SyncEngine.test.ts +0 -254
  328. package/src/engine/SyncEngine.ts +0 -2375
  329. package/src/engine/index.ts +0 -6
  330. package/src/engine/types.ts +0 -419
  331. package/src/handlers/collection.ts +0 -36
  332. package/src/handlers/create-handler.ts +0 -330
  333. package/src/handlers/types.ts +0 -108
  334. package/src/migrate.ts +0 -318
  335. package/src/outbox.ts +0 -383
  336. package/src/plugins/incrementing-version.ts +0 -133
  337. package/src/plugins/index.ts +0 -2
  338. package/src/plugins/types.ts +0 -63
  339. package/src/proxy/connection.ts +0 -191
  340. package/src/proxy/dialect.ts +0 -76
  341. package/src/proxy/driver.ts +0 -126
  342. package/src/proxy/index.ts +0 -10
  343. package/src/proxy/mutations.ts +0 -18
  344. package/src/pull-engine.test.ts +0 -151
  345. package/src/pull-engine.ts +0 -855
  346. package/src/push-engine.ts +0 -216
  347. package/src/query/FingerprintCollector.ts +0 -29
  348. package/src/query/QueryContext.ts +0 -54
  349. package/src/query/fingerprint.ts +0 -109
  350. package/src/query/index.ts +0 -10
  351. package/src/query/tracked-select.ts +0 -139
  352. package/src/schema.ts +0 -94
  353. package/src/subscription-state.ts +0 -259
  354. package/src/sync-loop.ts +0 -379
  355. package/src/sync.ts +0 -170
  356. package/src/utils/id.ts +0 -55
@@ -0,0 +1,623 @@
1
+ import { randomId } from '@syncular/core';
2
+ import {
3
+ createMutationsApi,
4
+ type MutationReceipt,
5
+ type MutationsApi,
6
+ type MutationsCommitFn,
7
+ type MutationsTx,
8
+ type TableMutationsTx,
9
+ } from './mutations';
10
+ import type {
11
+ SyncularSqlClient,
12
+ SyncularTableConfigMap,
13
+ SyncularUnsafeSqlClient,
14
+ } from './types';
15
+
16
+ type AnyDb = Record<string, Record<string, unknown>>;
17
+ type MutationScope = 'mutations' | 'leasedMutations';
18
+ type RowSnapshot = Record<string, unknown> | null;
19
+
20
+ interface HistoryRow extends Record<string, unknown> {
21
+ id: string;
22
+ mutation_scope: MutationScope;
23
+ state: 'done' | 'undone';
24
+ entries_json: string;
25
+ client_commit_id: string;
26
+ undo_client_commit_id: string | null;
27
+ redo_client_commit_id: string | null;
28
+ created_at: number;
29
+ updated_at: number;
30
+ }
31
+
32
+ interface CommandEntry {
33
+ table: string;
34
+ rowId: string;
35
+ before: RowSnapshot;
36
+ after: RowSnapshot;
37
+ }
38
+
39
+ interface PendingCommandEntry {
40
+ table: string;
41
+ rowId: string;
42
+ before: RowSnapshot;
43
+ }
44
+
45
+ export type SyncularCommandHistoryErrorCode =
46
+ | 'sync.command_history_empty'
47
+ | 'sync.command_history_conflict'
48
+ | 'sync.command_history_storage_unavailable'
49
+ | 'sync.command_history_table_unsupported'
50
+ | 'sync.command_history_unsafe_field';
51
+
52
+ export class SyncularCommandHistoryError extends Error {
53
+ readonly code: SyncularCommandHistoryErrorCode;
54
+ readonly commandId?: string;
55
+
56
+ constructor(
57
+ code: SyncularCommandHistoryErrorCode,
58
+ message: string,
59
+ options: { commandId?: string } = {}
60
+ ) {
61
+ super(message);
62
+ this.name = 'SyncularCommandHistoryError';
63
+ this.code = code;
64
+ this.commandId = options.commandId;
65
+ }
66
+ }
67
+
68
+ export interface SyncularCommandHistoryReceipt extends MutationReceipt {
69
+ commandId: string;
70
+ }
71
+
72
+ export interface SyncularCommandHistory {
73
+ canUndo(): Promise<boolean>;
74
+ canRedo(): Promise<boolean>;
75
+ undoLast(): Promise<SyncularCommandHistoryReceipt>;
76
+ redoLast(): Promise<SyncularCommandHistoryReceipt>;
77
+ }
78
+
79
+ export interface SyncularCommandHistoryController<DB> {
80
+ history: SyncularCommandHistory;
81
+ wrapMutations(
82
+ mutations: MutationsApi<DB, undefined>,
83
+ scope: MutationScope
84
+ ): MutationsApi<DB, undefined>;
85
+ }
86
+
87
+ export interface CreateSyncularCommandHistoryOptions<DB> {
88
+ client: SyncularSqlClient & Partial<SyncularUnsafeSqlClient>;
89
+ tableConfig: SyncularTableConfigMap;
90
+ mutations: MutationsApi<DB, undefined>;
91
+ leasedMutations: MutationsApi<DB, undefined>;
92
+ nowMs?: () => number;
93
+ idFactory?: () => string;
94
+ }
95
+
96
+ export function createSyncularCommandHistory<DB>(
97
+ options: CreateSyncularCommandHistoryOptions<DB>
98
+ ): SyncularCommandHistoryController<DB> {
99
+ const store = new CommandHistoryStore(options);
100
+ const history = new CommandHistory(options, store);
101
+
102
+ return {
103
+ history,
104
+ wrapMutations(mutations, scope) {
105
+ return createMutationsApi(createTrackedCommit(mutations, scope, history));
106
+ },
107
+ };
108
+ }
109
+
110
+ function createTrackedCommit<DB>(
111
+ mutations: MutationsApi<DB, undefined>,
112
+ scope: MutationScope,
113
+ history: CommandHistory<DB>
114
+ ): MutationsCommitFn<DB, void, undefined> {
115
+ return async (fn) => {
116
+ await history.ensureReady();
117
+ const recorder = new PendingCommandRecorder(history);
118
+ const { result, commit } = await mutations.$commit(async (tx) =>
119
+ fn(createTrackedTx(tx, recorder) as MutationsTx<DB>)
120
+ );
121
+ await recorder.recordCommitted(scope, commit);
122
+ return { result, receipt: commit, meta: undefined };
123
+ };
124
+ }
125
+
126
+ function createTrackedTx<DB>(
127
+ tx: MutationsTx<DB>,
128
+ recorder: PendingCommandRecorder<DB>
129
+ ): MutationsTx<DB> {
130
+ const tableCache = new Map<string, TableMutationsTx<AnyDb, string>>();
131
+ return new Proxy(
132
+ {},
133
+ {
134
+ get(_target, prop) {
135
+ if (prop === 'then') return undefined;
136
+ if (typeof prop !== 'string') return undefined;
137
+ const cached = tableCache.get(prop);
138
+ if (cached) return cached;
139
+ const tableTx = tx[prop] as TableMutationsTx<AnyDb, string> | undefined;
140
+ if (!tableTx) return undefined;
141
+ const tracked = createTrackedTableTx(prop, tableTx, recorder);
142
+ tableCache.set(prop, tracked);
143
+ return tracked;
144
+ },
145
+ }
146
+ ) as MutationsTx<DB>;
147
+ }
148
+
149
+ function createTrackedTableTx<DB>(
150
+ table: string,
151
+ tableTx: TableMutationsTx<AnyDb, string>,
152
+ recorder: PendingCommandRecorder<DB>
153
+ ): TableMutationsTx<AnyDb, string> {
154
+ return {
155
+ async insert(values) {
156
+ const id = await tableTx.insert(values);
157
+ await recorder.track(table, id);
158
+ return id;
159
+ },
160
+ async insertMany(rows) {
161
+ const ids = await tableTx.insertMany(rows);
162
+ for (const id of ids) await recorder.track(table, id);
163
+ return ids;
164
+ },
165
+ async update(id, patch, options) {
166
+ await recorder.track(table, id);
167
+ await tableTx.update(id, patch, options);
168
+ },
169
+ async delete(id, options) {
170
+ await recorder.track(table, id);
171
+ await tableTx.delete(id, options);
172
+ },
173
+ async upsert(id, patch, options) {
174
+ await recorder.track(table, id);
175
+ await tableTx.upsert(id, patch, options);
176
+ },
177
+ };
178
+ }
179
+
180
+ class PendingCommandRecorder<DB> {
181
+ readonly #entries = new Map<string, PendingCommandEntry>();
182
+
183
+ constructor(private readonly history: CommandHistory<DB>) {}
184
+
185
+ async track(table: string, rowId: string): Promise<void> {
186
+ const key = `${table}\0${rowId}`;
187
+ if (this.#entries.has(key)) return;
188
+ this.#entries.set(key, {
189
+ table,
190
+ rowId,
191
+ before: await this.history.readRow(table, rowId),
192
+ });
193
+ }
194
+
195
+ async recordCommitted(
196
+ scope: MutationScope,
197
+ receipt: MutationReceipt
198
+ ): Promise<void> {
199
+ if (this.#entries.size === 0) return;
200
+ const entries: CommandEntry[] = [];
201
+ for (const entry of this.#entries.values()) {
202
+ const after = await this.history.readRow(entry.table, entry.rowId);
203
+ if (sameSnapshot(entry.before, after)) continue;
204
+ entries.push({ ...entry, after });
205
+ }
206
+ if (entries.length === 0) return;
207
+ await this.history.record(scope, entries, receipt);
208
+ }
209
+ }
210
+
211
+ class CommandHistory<DB> implements SyncularCommandHistory {
212
+ constructor(
213
+ private readonly options: CreateSyncularCommandHistoryOptions<DB>,
214
+ private readonly store: CommandHistoryStore<DB>
215
+ ) {}
216
+
217
+ ensureReady(): Promise<void> {
218
+ return this.store.ensureReady();
219
+ }
220
+
221
+ readRow(table: string, rowId: string): Promise<RowSnapshot> {
222
+ const config = this.options.tableConfig[table];
223
+ if (!config) {
224
+ throw new SyncularCommandHistoryError(
225
+ 'sync.command_history_table_unsupported',
226
+ `Cannot record undo history for unsupported table ${table}`
227
+ );
228
+ }
229
+ return readCurrentRow(
230
+ this.options.client,
231
+ table,
232
+ config.primaryKeyColumn ?? 'id',
233
+ rowId
234
+ );
235
+ }
236
+
237
+ record(
238
+ scope: MutationScope,
239
+ entries: CommandEntry[],
240
+ receipt: MutationReceipt
241
+ ): Promise<void> {
242
+ return this.store.insert(scope, entries, receipt);
243
+ }
244
+
245
+ async canUndo(): Promise<boolean> {
246
+ await this.ensureReady();
247
+ return (await this.store.latest('done')) !== null;
248
+ }
249
+
250
+ async canRedo(): Promise<boolean> {
251
+ await this.ensureReady();
252
+ return (await this.store.latest('undone')) !== null;
253
+ }
254
+
255
+ async undoLast(): Promise<SyncularCommandHistoryReceipt> {
256
+ await this.ensureReady();
257
+ const command = await this.store.latest('done');
258
+ if (!command) {
259
+ throw new SyncularCommandHistoryError(
260
+ 'sync.command_history_empty',
261
+ 'There is no Syncular command to undo'
262
+ );
263
+ }
264
+ const entries = decodeEntries(command);
265
+ this.assertSafeReplay(command.id, entries);
266
+ await this.assertCurrentRows(command.id, entries, 'after');
267
+ const commit = await this.applySnapshots(
268
+ command.mutation_scope,
269
+ [...entries].reverse().map((entry) => ({
270
+ table: entry.table,
271
+ rowId: entry.rowId,
272
+ snapshot: entry.before,
273
+ }))
274
+ );
275
+ await this.store.mark(command.id, 'undone', commit);
276
+ return { ...commit, commandId: command.id };
277
+ }
278
+
279
+ async redoLast(): Promise<SyncularCommandHistoryReceipt> {
280
+ await this.ensureReady();
281
+ const command = await this.store.latest('undone');
282
+ if (!command) {
283
+ throw new SyncularCommandHistoryError(
284
+ 'sync.command_history_empty',
285
+ 'There is no Syncular command to redo'
286
+ );
287
+ }
288
+ const entries = decodeEntries(command);
289
+ this.assertSafeReplay(command.id, entries);
290
+ await this.assertCurrentRows(command.id, entries, 'before');
291
+ const commit = await this.applySnapshots(
292
+ command.mutation_scope,
293
+ entries.map((entry) => ({
294
+ table: entry.table,
295
+ rowId: entry.rowId,
296
+ snapshot: entry.after,
297
+ }))
298
+ );
299
+ await this.store.mark(command.id, 'done', commit);
300
+ return { ...commit, commandId: command.id };
301
+ }
302
+
303
+ private async assertCurrentRows(
304
+ commandId: string,
305
+ entries: readonly CommandEntry[],
306
+ expected: 'before' | 'after'
307
+ ): Promise<void> {
308
+ for (const entry of entries) {
309
+ const config = this.options.tableConfig[entry.table];
310
+ if (!config) {
311
+ throw new SyncularCommandHistoryError(
312
+ 'sync.command_history_table_unsupported',
313
+ `Cannot replay undo history for unsupported table ${entry.table}`,
314
+ { commandId }
315
+ );
316
+ }
317
+ const current = await this.readRow(entry.table, entry.rowId);
318
+ if (!sameReplaySnapshot(config, current, entry[expected])) {
319
+ throw new SyncularCommandHistoryError(
320
+ 'sync.command_history_conflict',
321
+ `Cannot ${expected === 'after' ? 'undo' : 'redo'} Syncular command ${commandId}; ${entry.table}.${entry.rowId} changed since the command was recorded`,
322
+ { commandId }
323
+ );
324
+ }
325
+ }
326
+ }
327
+
328
+ private assertSafeReplay(
329
+ commandId: string,
330
+ entries: readonly CommandEntry[]
331
+ ): void {
332
+ for (const entry of entries) {
333
+ const config = this.options.tableConfig[entry.table];
334
+ if (!config) {
335
+ throw new SyncularCommandHistoryError(
336
+ 'sync.command_history_table_unsupported',
337
+ `Cannot replay undo history for unsupported table ${entry.table}`,
338
+ { commandId }
339
+ );
340
+ }
341
+ const unsafeFields = unsafeChangedFields(config, entry);
342
+ if (unsafeFields.length > 0) {
343
+ throw new SyncularCommandHistoryError(
344
+ 'sync.command_history_unsafe_field',
345
+ `Cannot replay Syncular command ${commandId}; ${entry.table}.${entry.rowId} changed unsafe fields: ${unsafeFields.join(', ')}`,
346
+ { commandId }
347
+ );
348
+ }
349
+ }
350
+ }
351
+
352
+ private async applySnapshots(
353
+ scope: MutationScope,
354
+ snapshots: Array<{ table: string; rowId: string; snapshot: RowSnapshot }>
355
+ ): Promise<MutationReceipt> {
356
+ const mutations =
357
+ scope === 'leasedMutations'
358
+ ? this.options.leasedMutations
359
+ : this.options.mutations;
360
+ const { commit } = await mutations.$commit(async (tx) => {
361
+ for (const snapshot of snapshots) {
362
+ const table = tx[snapshot.table] as
363
+ | TableMutationsTx<AnyDb, string>
364
+ | undefined;
365
+ if (!table) {
366
+ throw new SyncularCommandHistoryError(
367
+ 'sync.command_history_table_unsupported',
368
+ `Cannot apply undo history for unsupported table ${snapshot.table}`
369
+ );
370
+ }
371
+ if (snapshot.snapshot === null) {
372
+ await table.delete(snapshot.rowId);
373
+ } else {
374
+ await table.upsert(
375
+ snapshot.rowId,
376
+ this.mutationPayloadForSnapshot(snapshot.table, snapshot.snapshot)
377
+ );
378
+ }
379
+ }
380
+ });
381
+ return commit;
382
+ }
383
+
384
+ private mutationPayloadForSnapshot(
385
+ table: string,
386
+ snapshot: Record<string, unknown>
387
+ ): Record<string, unknown> {
388
+ const config = this.options.tableConfig[table];
389
+ if (!config) {
390
+ throw new SyncularCommandHistoryError(
391
+ 'sync.command_history_table_unsupported',
392
+ `Cannot apply undo history for unsupported table ${table}`
393
+ );
394
+ }
395
+ const payload = { ...snapshot };
396
+ delete payload[config.primaryKeyColumn ?? 'id'];
397
+ if (config.serverVersionColumn) delete payload[config.serverVersionColumn];
398
+ for (const field of config.crdtYjsFields ?? []) {
399
+ delete payload[field.stateColumn];
400
+ }
401
+ return payload;
402
+ }
403
+ }
404
+
405
+ class CommandHistoryStore<DB> {
406
+ #ready: Promise<void> | undefined;
407
+
408
+ constructor(
409
+ private readonly options: CreateSyncularCommandHistoryOptions<DB>
410
+ ) {}
411
+
412
+ ensureReady(): Promise<void> {
413
+ this.#ready ??= this.#ensureReady();
414
+ return this.#ready;
415
+ }
416
+
417
+ async insert(
418
+ scope: MutationScope,
419
+ entries: readonly CommandEntry[],
420
+ receipt: MutationReceipt
421
+ ): Promise<void> {
422
+ await this.ensureReady();
423
+ const now = this.now();
424
+ const id = this.options.idFactory?.() ?? `cmd_${randomId()}`;
425
+ const client = this.unsafeClient();
426
+ await client.executeUnsafeSql(
427
+ "delete from sync_command_history where state = 'undone'"
428
+ );
429
+ await client.executeUnsafeSql(
430
+ `insert into sync_command_history (
431
+ id,
432
+ mutation_scope,
433
+ state,
434
+ entries_json,
435
+ client_commit_id,
436
+ created_at,
437
+ updated_at
438
+ ) values (?, ?, 'done', ?, ?, ?, ?)`,
439
+ [id, scope, JSON.stringify(entries), receipt.clientCommitId, now, now]
440
+ );
441
+ }
442
+
443
+ async latest(state: 'done' | 'undone'): Promise<HistoryRow | null> {
444
+ await this.ensureReady();
445
+ const result = await this.options.client.executeSql<HistoryRow>(
446
+ `select id, mutation_scope, state, entries_json, client_commit_id,
447
+ undo_client_commit_id, redo_client_commit_id, created_at, updated_at
448
+ from sync_command_history
449
+ where state = ?
450
+ order by updated_at desc, created_at desc, id desc
451
+ limit 1`,
452
+ [state]
453
+ );
454
+ return result.rows[0] ?? null;
455
+ }
456
+
457
+ async mark(
458
+ id: string,
459
+ state: 'done' | 'undone',
460
+ receipt: MutationReceipt
461
+ ): Promise<void> {
462
+ await this.ensureReady();
463
+ const replayColumn =
464
+ state === 'undone' ? 'undo_client_commit_id' : 'redo_client_commit_id';
465
+ await this.unsafeClient().executeUnsafeSql(
466
+ `update sync_command_history
467
+ set state = ?, updated_at = ?, ${replayColumn} = ?
468
+ where id = ?`,
469
+ [state, this.now(), receipt.clientCommitId, id]
470
+ );
471
+ }
472
+
473
+ async #ensureReady(): Promise<void> {
474
+ const client = this.unsafeClient();
475
+ await client.executeUnsafeSql(
476
+ `create table if not exists sync_command_history (
477
+ id text primary key,
478
+ mutation_scope text not null,
479
+ state text not null check (state in ('done', 'undone')),
480
+ entries_json text not null,
481
+ client_commit_id text not null,
482
+ undo_client_commit_id text null,
483
+ redo_client_commit_id text null,
484
+ created_at integer not null,
485
+ updated_at integer not null
486
+ )`
487
+ );
488
+ await client.executeUnsafeSql(
489
+ `create index if not exists idx_sync_command_history_state_updated
490
+ on sync_command_history (state, updated_at, created_at)`
491
+ );
492
+ }
493
+
494
+ private unsafeClient(): SyncularUnsafeSqlClient {
495
+ const client = this.options.client;
496
+ if (typeof client.executeUnsafeSql === 'function') {
497
+ return client as SyncularUnsafeSqlClient;
498
+ }
499
+ throw new SyncularCommandHistoryError(
500
+ 'sync.command_history_storage_unavailable',
501
+ 'Syncular command history requires internal SQLite write access'
502
+ );
503
+ }
504
+
505
+ private now(): number {
506
+ return Math.trunc(this.options.nowMs?.() ?? Date.now());
507
+ }
508
+ }
509
+
510
+ async function readCurrentRow(
511
+ client: SyncularSqlClient,
512
+ table: string,
513
+ idColumn: string,
514
+ rowId: string
515
+ ): Promise<RowSnapshot> {
516
+ const result = await client.executeSql(
517
+ `select * from ${quoteIdentifier(table)} where ${quoteIdentifier(idColumn)} = ? limit 1`,
518
+ [rowId]
519
+ );
520
+ const row = result.rows[0];
521
+ return row ? normalizeSnapshot(row) : null;
522
+ }
523
+
524
+ function decodeEntries(row: HistoryRow): CommandEntry[] {
525
+ const entries = JSON.parse(row.entries_json) as CommandEntry[];
526
+ if (!Array.isArray(entries)) {
527
+ throw new SyncularCommandHistoryError(
528
+ 'sync.command_history_conflict',
529
+ `Syncular command history row ${row.id} is malformed`,
530
+ { commandId: row.id }
531
+ );
532
+ }
533
+ return entries;
534
+ }
535
+
536
+ function normalizeSnapshot(
537
+ row: Record<string, unknown>
538
+ ): Record<string, unknown> {
539
+ return Object.fromEntries(
540
+ Object.entries(row).sort(([a], [b]) => a.localeCompare(b))
541
+ );
542
+ }
543
+
544
+ function sameSnapshot(left: RowSnapshot, right: RowSnapshot): boolean {
545
+ return stableJson(left) === stableJson(right);
546
+ }
547
+
548
+ function sameReplaySnapshot(
549
+ config: SyncularTableConfigMap[string],
550
+ left: RowSnapshot,
551
+ right: RowSnapshot
552
+ ): boolean {
553
+ return (
554
+ stableJson(replayComparableSnapshot(config, left)) ===
555
+ stableJson(replayComparableSnapshot(config, right))
556
+ );
557
+ }
558
+
559
+ function replayComparableSnapshot(
560
+ config: SyncularTableConfigMap[string],
561
+ snapshot: RowSnapshot
562
+ ): RowSnapshot {
563
+ if (snapshot === null) return null;
564
+ const comparable = { ...snapshot };
565
+ if (config.serverVersionColumn) delete comparable[config.serverVersionColumn];
566
+ for (const field of config.crdtYjsFields ?? []) {
567
+ delete comparable[field.stateColumn];
568
+ }
569
+ return comparable;
570
+ }
571
+
572
+ function unsafeChangedFields(
573
+ config: SyncularTableConfigMap[string],
574
+ entry: CommandEntry
575
+ ): string[] {
576
+ const rowLifecycleReplay = entry.before === null || entry.after === null;
577
+ const unsafeFields = new Set<string>();
578
+ for (const column of config.blobColumns ?? []) unsafeFields.add(column);
579
+ for (const field of config.encryptedFields ?? [])
580
+ unsafeFields.add(field.field);
581
+ if (!rowLifecycleReplay) {
582
+ for (const field of config.crdtYjsFields ?? []) {
583
+ unsafeFields.add(field.field);
584
+ unsafeFields.add(field.stateColumn);
585
+ }
586
+ }
587
+
588
+ const changed: string[] = [];
589
+ for (const field of unsafeFields) {
590
+ const before = entry.before?.[field];
591
+ const after = entry.after?.[field];
592
+ if (
593
+ stableJson(nullishToNull(before)) === stableJson(nullishToNull(after))
594
+ ) {
595
+ continue;
596
+ }
597
+ if (rowLifecycleReplay && before == null && after == null) continue;
598
+ changed.push(field);
599
+ }
600
+ return changed;
601
+ }
602
+
603
+ function nullishToNull(value: unknown): unknown {
604
+ return value == null ? null : value;
605
+ }
606
+
607
+ function stableJson(value: unknown): string {
608
+ return JSON.stringify(sortValue(value));
609
+ }
610
+
611
+ function sortValue(value: unknown): unknown {
612
+ if (Array.isArray(value)) return value.map(sortValue);
613
+ if (!value || typeof value !== 'object') return value;
614
+ return Object.fromEntries(
615
+ Object.entries(value as Record<string, unknown>)
616
+ .sort(([a], [b]) => a.localeCompare(b))
617
+ .map(([key, nested]) => [key, sortValue(nested)])
618
+ );
619
+ }
620
+
621
+ function quoteIdentifier(identifier: string): string {
622
+ return `"${identifier.replaceAll('"', '""')}"`;
623
+ }