@syncular/client 0.0.6-96 → 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,1036 @@
1
+ import {
2
+ type CreateSyncularDatabaseOptions,
3
+ createSyncularDatabase,
4
+ type MutationReceipt,
5
+ type MutationsApi,
6
+ type SyncularBlobUploadQueueStats,
7
+ type SyncularChangedRow,
8
+ type SyncularClientEventMap,
9
+ type SyncularClientEventType,
10
+ type SyncularClientLike,
11
+ type SyncularClientStatus,
12
+ type SyncularConflictStats,
13
+ type SyncularConnectionState,
14
+ type SyncularLiveQueryOptions,
15
+ type SyncularLiveQuerySubscription,
16
+ type SyncularOutboxStats,
17
+ type SyncularPresenceEntry,
18
+ type SyncularRowsChangedEvent,
19
+ type TableMutations,
20
+ } from '@syncular/client';
21
+ import type { BlobRef } from '@syncular/core';
22
+ import type { CompiledQuery, Kysely } from 'kysely';
23
+ import {
24
+ createContext,
25
+ createElement,
26
+ type ReactNode,
27
+ startTransition,
28
+ useCallback,
29
+ useContext,
30
+ useEffect,
31
+ useMemo,
32
+ useRef,
33
+ useState,
34
+ useSyncExternalStore,
35
+ } from 'react';
36
+
37
+ export type SyncularReactStatus = 'idle' | 'loading' | 'ready' | 'error';
38
+
39
+ export interface SyncProviderProps<DB> {
40
+ children?: ReactNode;
41
+ client?: SyncularClientLike<DB>;
42
+ options?: CreateSyncularDatabaseOptions;
43
+ optionsKey?: unknown;
44
+ closeOnUnmount?: boolean;
45
+ onClient?: (client: SyncularClientLike<DB>) => void;
46
+ onError?: (error: unknown) => void;
47
+ }
48
+
49
+ export interface SyncContextValue {
50
+ status: SyncularReactStatus;
51
+ error: unknown;
52
+ }
53
+
54
+ export interface SyncQueryContext<DB> {
55
+ db: Kysely<DB>;
56
+ selectFrom: Kysely<DB>['selectFrom'];
57
+ }
58
+
59
+ export interface UseSyncQueryOptions {
60
+ enabled?: boolean;
61
+ deps?: readonly unknown[];
62
+ keyField?: string;
63
+ watchTables?: readonly string[];
64
+ tables?: readonly string[];
65
+ pollIntervalMs?: number;
66
+ staleAfterMs?: number;
67
+ structuralSharing?: boolean;
68
+ refreshOnDataChange?: boolean;
69
+ loadingOnRefresh?: boolean;
70
+ transitionUpdates?: boolean;
71
+ }
72
+
73
+ export interface UseSyncQueryResult<T> {
74
+ data: T | undefined;
75
+ isLoading: boolean;
76
+ error: Error | null;
77
+ isStale: boolean;
78
+ lastSyncAt: number | null;
79
+ refetch: () => Promise<void>;
80
+ }
81
+
82
+ export interface UseQueryOptions {
83
+ enabled?: boolean;
84
+ deps?: readonly unknown[];
85
+ keyField?: string;
86
+ }
87
+
88
+ export interface UseQueryResult<T> {
89
+ data: T | undefined;
90
+ isLoading: boolean;
91
+ error: Error | null;
92
+ refetch: () => Promise<void>;
93
+ }
94
+
95
+ export type MutationInput =
96
+ | {
97
+ rowId: string;
98
+ op: 'delete';
99
+ payload?: null;
100
+ baseVersion?: number | null;
101
+ }
102
+ | {
103
+ rowId: string;
104
+ op: 'upsert';
105
+ payload?: Record<string, unknown> | null;
106
+ baseVersion?: number | null;
107
+ };
108
+
109
+ export type MutationResult = MutationReceipt;
110
+
111
+ export interface FluentMutation {
112
+ update: (
113
+ rowId: string,
114
+ payload: Record<string, unknown>,
115
+ options?: { baseVersion?: number | null }
116
+ ) => Promise<MutationResult>;
117
+ upsert: (
118
+ rowId: string,
119
+ payload: Record<string, unknown>,
120
+ options?: { baseVersion?: number | null }
121
+ ) => Promise<MutationResult>;
122
+ delete: (
123
+ rowId: string,
124
+ options?: { baseVersion?: number | null }
125
+ ) => Promise<MutationResult>;
126
+ }
127
+
128
+ export interface UseMutationOptions<TTable extends string> {
129
+ table: TTable;
130
+ syncImmediately?: boolean;
131
+ onSuccess?: (result: MutationResult) => void;
132
+ onError?: (error: Error) => void;
133
+ }
134
+
135
+ export interface UseMutationResult {
136
+ mutate: FluentMutation;
137
+ mutateMany: (inputs: readonly MutationInput[]) => Promise<MutationResult>;
138
+ isPending: boolean;
139
+ error: Error | null;
140
+ reset: () => void;
141
+ }
142
+
143
+ export type SyncMode = 'background' | 'await' | false;
144
+
145
+ export interface UseMutationsOptions {
146
+ sync?: SyncMode;
147
+ onSuccess?: (result: MutationReceipt & { opCount: number }) => void;
148
+ onError?: (error: Error) => void;
149
+ }
150
+
151
+ export type MutationsHook<DB> = MutationsApi<DB, { sync?: SyncMode }> & {
152
+ readonly $isPending: boolean;
153
+ readonly $error: Error | null;
154
+ $reset: () => void;
155
+ };
156
+
157
+ export interface UseRowsChangedOptions {
158
+ tables?: readonly string[];
159
+ enabled?: boolean;
160
+ }
161
+
162
+ export interface UsePresenceResult<
163
+ TMetadata extends Record<string, unknown> = Record<string, unknown>,
164
+ > {
165
+ presence: SyncularPresenceEntry<TMetadata>[];
166
+ isLoading: boolean;
167
+ }
168
+
169
+ export interface UsePresenceWithJoinOptions<
170
+ TMetadata extends Record<string, unknown> = Record<string, unknown>,
171
+ > {
172
+ metadata?: TMetadata;
173
+ autoJoin?: boolean;
174
+ enabled?: boolean;
175
+ }
176
+
177
+ export interface UsePresenceWithJoinResult<
178
+ TMetadata extends Record<string, unknown> = Record<string, unknown>,
179
+ > extends UsePresenceResult<TMetadata> {
180
+ updateMetadata: (metadata: TMetadata) => void;
181
+ join: (metadata?: TMetadata) => void;
182
+ leave: () => void;
183
+ isJoined: boolean;
184
+ }
185
+
186
+ export interface UseSyncConnectionResult {
187
+ state: SyncularConnectionState;
188
+ isConnected: boolean;
189
+ isReconnecting: boolean;
190
+ reconnect: () => Promise<void>;
191
+ disconnect: () => Promise<void>;
192
+ }
193
+
194
+ export interface UseBlobUploadQueueResult {
195
+ stats: SyncularBlobUploadQueueStats | null;
196
+ refresh(): Promise<void>;
197
+ process(): Promise<{ uploaded: number; failed: number }>;
198
+ }
199
+
200
+ type ReactContextInternal<DB> = {
201
+ client: SyncularClientLike<DB> | null;
202
+ status: SyncularReactStatus;
203
+ error: unknown;
204
+ };
205
+
206
+ type ExecutableQuery<TResult> = {
207
+ execute(): Promise<TResult>;
208
+ compile?: () => CompiledQuery;
209
+ };
210
+
211
+ type SyncQueryFn<DB, TResult> = (
212
+ ctx: SyncQueryContext<DB>
213
+ ) => ExecutableQuery<TResult> | Promise<TResult> | TResult;
214
+
215
+ type KnownTableKey<DB> = string extends keyof DB
216
+ ? string
217
+ : Extract<keyof DB, string>;
218
+
219
+ type AnyTableMutations = TableMutations<
220
+ Record<string, Record<string, unknown>>,
221
+ string
222
+ >;
223
+
224
+ export function createSyncularReact<DB>() {
225
+ const Context = createContext<ReactContextInternal<DB> | null>(null);
226
+
227
+ function SyncProvider(
228
+ props: SyncProviderProps<DB>
229
+ ): ReturnType<typeof createElement> {
230
+ const {
231
+ children,
232
+ client: providedClient,
233
+ closeOnUnmount,
234
+ onClient,
235
+ onError,
236
+ options,
237
+ optionsKey,
238
+ } = props;
239
+ const optionsRef = useLatest(options);
240
+ const onClientRef = useLatest(onClient);
241
+ const onErrorRef = useLatest(onError);
242
+ const lifecycleKey = optionsKey;
243
+ const [value, setValue] = useState<ReactContextInternal<DB>>({
244
+ client: providedClient ?? null,
245
+ status: providedClient ? 'ready' : options ? 'loading' : 'idle',
246
+ error: null,
247
+ });
248
+
249
+ useEffect(() => {
250
+ void lifecycleKey;
251
+ let cancelled = false;
252
+ let ownedClient: SyncularClientLike<DB> | null = null;
253
+
254
+ if (providedClient) {
255
+ setValue({ client: providedClient, status: 'ready', error: null });
256
+ onClientRef.current?.(providedClient);
257
+ return () => {
258
+ if (closeOnUnmount) {
259
+ void providedClient.close().catch(() => undefined);
260
+ }
261
+ };
262
+ }
263
+
264
+ const currentOptions = optionsRef.current;
265
+ if (!currentOptions) {
266
+ setValue({ client: null, status: 'idle', error: null });
267
+ return undefined;
268
+ }
269
+
270
+ setValue((previous) => ({
271
+ client: previous.client,
272
+ status: 'loading',
273
+ error: null,
274
+ }));
275
+ void createSyncularDatabase<DB>(currentOptions)
276
+ .then((client) => {
277
+ if (cancelled) {
278
+ void client.close().catch(() => undefined);
279
+ return;
280
+ }
281
+ ownedClient = client;
282
+ setValue({ client, status: 'ready', error: null });
283
+ onClientRef.current?.(client);
284
+ })
285
+ .catch((error) => {
286
+ if (cancelled) return;
287
+ setValue({ client: null, status: 'error', error });
288
+ onErrorRef.current?.(error);
289
+ });
290
+
291
+ return () => {
292
+ cancelled = true;
293
+ if (ownedClient) void ownedClient.close().catch(() => undefined);
294
+ };
295
+ }, [
296
+ closeOnUnmount,
297
+ lifecycleKey,
298
+ onClientRef,
299
+ onErrorRef,
300
+ optionsRef,
301
+ providedClient,
302
+ ]);
303
+
304
+ return createElement(Context.Provider, { value, children });
305
+ }
306
+
307
+ function useSyncContext(): SyncContextValue {
308
+ const value = useContext(Context);
309
+ if (!value) throw new Error('SyncProvider is required for Syncular hooks.');
310
+ return { status: value.status, error: value.error };
311
+ }
312
+
313
+ function useClient(): SyncularClientLike<DB> {
314
+ const value = useContext(Context);
315
+ if (!value) throw new Error('SyncProvider is required for Syncular hooks.');
316
+ if (!value.client) {
317
+ if (value.status === 'error') throw value.error;
318
+ throw new Error('Syncular client is not ready yet.');
319
+ }
320
+ return value.client;
321
+ }
322
+
323
+ function useSyncStatus(): SyncularClientStatus {
324
+ const client = useClient();
325
+ const readStatus = useCallback(() => client.getStatus(), [client]);
326
+ const getSnapshot = useStableSnapshot(readStatus);
327
+ return useSyncExternalStore(
328
+ useCallback(
329
+ (notify) => client.on('lifecycleChanged', () => notify()),
330
+ [client]
331
+ ),
332
+ getSnapshot,
333
+ getSnapshot
334
+ );
335
+ }
336
+
337
+ function useSyncConnection(): UseSyncConnectionResult {
338
+ const client = useClient();
339
+ const status = useSyncStatus();
340
+ const state = status.connection;
341
+ return {
342
+ state,
343
+ isConnected: status.isConnected,
344
+ isReconnecting: state.realtime === 'connecting',
345
+ reconnect: client.start,
346
+ disconnect: client.stop,
347
+ };
348
+ }
349
+
350
+ function useSyncEvent<T extends SyncularClientEventType>(
351
+ event: T,
352
+ listener: (payload: SyncularClientEventMap[T]) => void,
353
+ enabled = true
354
+ ): void {
355
+ const client = useClient();
356
+ const listenerRef = useLatest(listener);
357
+ useEffect(() => {
358
+ if (!enabled) return undefined;
359
+ return client.on(event, (payload) => listenerRef.current(payload));
360
+ }, [client, enabled, event, listenerRef]);
361
+ }
362
+
363
+ function useRowsChanged(
364
+ listener: (event: SyncularRowsChangedEvent) => void,
365
+ options: UseRowsChangedOptions = {}
366
+ ): void {
367
+ const listenerRef = useLatest(listener);
368
+ useSyncEvent(
369
+ 'rowsChanged',
370
+ (event) => {
371
+ const tables = options.tables == null ? null : new Set(options.tables);
372
+ if (tables && !event.changedTables.some((table) => tables.has(table))) {
373
+ return;
374
+ }
375
+ listenerRef.current(event);
376
+ },
377
+ options.enabled !== false
378
+ );
379
+ }
380
+
381
+ function useSyncQuery<TResult>(
382
+ query: SyncQueryFn<DB, TResult>,
383
+ options: UseSyncQueryOptions = {}
384
+ ): UseSyncQueryResult<TResult> {
385
+ const client = useClient();
386
+ const queryRef = useLatest(query);
387
+ const optionsRef = useLatest(options);
388
+ const enabled = options.enabled !== false;
389
+ const deps = options.deps ?? [];
390
+ const tables = options.watchTables ?? options.tables;
391
+ const tablesRef = useLatest(tables);
392
+ const tablesKey = stableStringListKey(tables);
393
+ const watchedTables = useMemo(() => {
394
+ void tablesKey;
395
+ return tablesRef.current == null ? null : new Set(tablesRef.current);
396
+ }, [tablesKey, tablesRef]);
397
+ const [data, setData] = useState<TResult | undefined>(undefined);
398
+ const [isLoading, setIsLoading] = useState(enabled);
399
+ const [error, setError] = useState<Error | null>(null);
400
+ const [lastSyncAt, setLastSyncAt] = useState<number | null>(null);
401
+
402
+ const publishResult = useCallback(
403
+ (result: TResult) => {
404
+ const publish = () => {
405
+ setData((previous) =>
406
+ shareQueryResult(
407
+ previous,
408
+ result,
409
+ optionsRef.current.keyField ?? 'id',
410
+ optionsRef.current.structuralSharing !== false
411
+ )
412
+ );
413
+ setLastSyncAt(Date.now());
414
+ setIsLoading(false);
415
+ };
416
+ if (optionsRef.current.transitionUpdates === false) {
417
+ publish();
418
+ } else {
419
+ startTransition(publish);
420
+ }
421
+ },
422
+ [optionsRef]
423
+ );
424
+
425
+ const execute = useCallback(async () => {
426
+ if (!enabled) return;
427
+ if (optionsRef.current.loadingOnRefresh !== false) setIsLoading(true);
428
+ setError(null);
429
+ try {
430
+ const result = await executeSyncQuery(client.db, queryRef.current);
431
+ publishResult(result);
432
+ } catch (caught) {
433
+ setError(toError(caught));
434
+ setIsLoading(false);
435
+ }
436
+ }, [client, enabled, optionsRef, publishResult, queryRef]);
437
+
438
+ useEffect(() => {
439
+ if (!enabled) {
440
+ setIsLoading(false);
441
+ return undefined;
442
+ }
443
+ void execute();
444
+ if (!options.pollIntervalMs || options.pollIntervalMs <= 0) {
445
+ return undefined;
446
+ }
447
+ const timer = setInterval(() => {
448
+ void execute();
449
+ }, options.pollIntervalMs);
450
+ return () => clearInterval(timer);
451
+ }, [enabled, execute, options.pollIntervalMs, ...deps]);
452
+
453
+ useEffect(() => {
454
+ if (!enabled || options.refreshOnDataChange === false) return undefined;
455
+ let disposed = false;
456
+ let fallbackUnsubscribe: (() => void) | undefined;
457
+ let liveSubscription: SyncularLiveQuerySubscription | undefined;
458
+
459
+ const startFallback = () => {
460
+ if (disposed || fallbackUnsubscribe) return;
461
+ fallbackUnsubscribe = client.on('rowsChanged', (event) => {
462
+ if (
463
+ watchedTables &&
464
+ !event.changedTables.some((table) => watchedTables.has(table))
465
+ ) {
466
+ return;
467
+ }
468
+ void execute();
469
+ });
470
+ };
471
+
472
+ const startLiveQuery = async () => {
473
+ if (!hasLiveQueries(client)) {
474
+ startFallback();
475
+ return;
476
+ }
477
+ try {
478
+ const queryResult = await Promise.resolve(
479
+ queryRef.current({
480
+ db: client.db,
481
+ selectFrom: client.db.selectFrom.bind(
482
+ client.db
483
+ ) as Kysely<DB>['selectFrom'],
484
+ })
485
+ );
486
+ if (!isCompilableExecutableQuery<TResult>(queryResult)) {
487
+ startFallback();
488
+ return;
489
+ }
490
+ const liveTables = tablesRef.current;
491
+ liveSubscription = await client.live(queryResult, {
492
+ ...(liveTables == null ? {} : { tables: liveTables }),
493
+ onChange: (rows) => {
494
+ if (!disposed) publishResult(rows as TResult);
495
+ },
496
+ });
497
+ } catch (caught) {
498
+ if (!disposed) {
499
+ setError(toError(caught));
500
+ setIsLoading(false);
501
+ }
502
+ }
503
+ };
504
+
505
+ void startLiveQuery();
506
+
507
+ return () => {
508
+ disposed = true;
509
+ fallbackUnsubscribe?.();
510
+ liveSubscription?.unsubscribe();
511
+ };
512
+ }, [
513
+ client,
514
+ enabled,
515
+ execute,
516
+ options.refreshOnDataChange,
517
+ publishResult,
518
+ queryRef,
519
+ tablesRef,
520
+ watchedTables,
521
+ ]);
522
+
523
+ return {
524
+ data,
525
+ isLoading,
526
+ error,
527
+ isStale:
528
+ options.staleAfterMs != null &&
529
+ lastSyncAt != null &&
530
+ Date.now() - lastSyncAt > options.staleAfterMs,
531
+ lastSyncAt,
532
+ refetch: execute,
533
+ };
534
+ }
535
+
536
+ function useQuery<TResult>(
537
+ query: SyncQueryFn<DB, TResult>,
538
+ options: UseQueryOptions = {}
539
+ ): UseQueryResult<TResult> {
540
+ const result = useSyncQuery(query, {
541
+ ...options,
542
+ refreshOnDataChange: false,
543
+ loadingOnRefresh: true,
544
+ });
545
+ return {
546
+ data: result.data,
547
+ isLoading: result.isLoading,
548
+ error: result.error,
549
+ refetch: result.refetch,
550
+ };
551
+ }
552
+
553
+ function useMutation<TTable extends KnownTableKey<DB> & string>(
554
+ options: UseMutationOptions<TTable>
555
+ ): UseMutationResult {
556
+ return useMutationFor('mutations', options);
557
+ }
558
+
559
+ function useLeasedMutation<TTable extends KnownTableKey<DB> & string>(
560
+ options: UseMutationOptions<TTable>
561
+ ): UseMutationResult {
562
+ return useMutationFor('leasedMutations', options);
563
+ }
564
+
565
+ function useMutationFor<TTable extends KnownTableKey<DB> & string>(
566
+ apiName: 'mutations' | 'leasedMutations',
567
+ options: UseMutationOptions<TTable>
568
+ ): UseMutationResult {
569
+ const client = useClient();
570
+ const optionsRef = useLatest(options);
571
+ const [isPending, setIsPending] = useState(false);
572
+ const [error, setError] = useState<Error | null>(null);
573
+ const reset = useCallback(() => setError(null), []);
574
+
575
+ const run = useCallback(
576
+ async (operation: () => Promise<MutationReceipt>) => {
577
+ setIsPending(true);
578
+ setError(null);
579
+ try {
580
+ const result = await operation();
581
+ if (optionsRef.current.syncImmediately !== false) {
582
+ await client.sync();
583
+ }
584
+ optionsRef.current.onSuccess?.(result);
585
+ return result;
586
+ } catch (caught) {
587
+ const nextError = toError(caught);
588
+ setError(nextError);
589
+ optionsRef.current.onError?.(nextError);
590
+ throw nextError;
591
+ } finally {
592
+ setIsPending(false);
593
+ }
594
+ },
595
+ [client, optionsRef]
596
+ );
597
+
598
+ const mutate = useMemo<FluentMutation>(() => {
599
+ const table = client[apiName].$table(options.table);
600
+ return {
601
+ update: (rowId, payload, mutationOptions) =>
602
+ run(() => table.update(rowId, payload, mutationOptions)),
603
+ upsert: (rowId, payload, mutationOptions) =>
604
+ run(() => table.upsert(rowId, payload, mutationOptions)),
605
+ delete: (rowId, mutationOptions) =>
606
+ run(() => table.delete(rowId, mutationOptions)),
607
+ };
608
+ }, [apiName, client, options.table, run]);
609
+
610
+ const mutateMany = useCallback(
611
+ (inputs: readonly MutationInput[]) =>
612
+ run(async () => {
613
+ const { commit } = await client[apiName].$commit(async (tx) => {
614
+ const table = tx[optionsRef.current.table]!;
615
+ for (const input of inputs) {
616
+ if (input.op === 'delete') {
617
+ await table.delete(input.rowId, {
618
+ baseVersion: input.baseVersion,
619
+ });
620
+ } else {
621
+ await table.upsert(input.rowId, input.payload ?? {}, {
622
+ baseVersion: input.baseVersion,
623
+ });
624
+ }
625
+ }
626
+ });
627
+ return commit;
628
+ }),
629
+ [apiName, client, optionsRef, run]
630
+ );
631
+
632
+ return { mutate, mutateMany, isPending, error, reset };
633
+ }
634
+
635
+ function useMutations(options: UseMutationsOptions = {}): MutationsHook<DB> {
636
+ return useMutationsFor('mutations', options);
637
+ }
638
+
639
+ function useLeasedMutations(
640
+ options: UseMutationsOptions = {}
641
+ ): MutationsHook<DB> {
642
+ return useMutationsFor('leasedMutations', options);
643
+ }
644
+
645
+ function useMutationsFor(
646
+ apiName: 'mutations' | 'leasedMutations',
647
+ options: UseMutationsOptions = {}
648
+ ): MutationsHook<DB> {
649
+ const client = useClient();
650
+ const optionsRef = useLatest(options);
651
+ const [isPending, setIsPending] = useState(false);
652
+ const [error, setError] = useState<Error | null>(null);
653
+ const reset = useCallback(() => setError(null), []);
654
+
655
+ const run = useCallback(
656
+ async <TResult extends MutationReceipt>(
657
+ operation: () => Promise<TResult>,
658
+ opCount: number,
659
+ syncOverride?: SyncMode
660
+ ) => {
661
+ setIsPending(true);
662
+ setError(null);
663
+ try {
664
+ const result = await operation();
665
+ const syncMode =
666
+ syncOverride ?? optionsRef.current.sync ?? 'background';
667
+ if (syncMode === 'await') {
668
+ await client.sync();
669
+ } else if (syncMode === 'background') {
670
+ void client.sync().catch(() => undefined);
671
+ }
672
+ optionsRef.current.onSuccess?.({ ...result, opCount });
673
+ return result;
674
+ } catch (caught) {
675
+ const nextError = toError(caught);
676
+ setError(nextError);
677
+ optionsRef.current.onError?.(nextError);
678
+ throw nextError;
679
+ } finally {
680
+ setIsPending(false);
681
+ }
682
+ },
683
+ [client, optionsRef]
684
+ );
685
+
686
+ return useMemo(
687
+ () =>
688
+ new Proxy(
689
+ {},
690
+ {
691
+ get(_target, prop) {
692
+ if (prop === 'then') return undefined;
693
+ if (prop === '$isPending') return isPending;
694
+ if (prop === '$error') return error;
695
+ if (prop === '$reset') return reset;
696
+ if (prop === '$table') {
697
+ return (table: string) =>
698
+ wrapMutationsTable(client, apiName, table, run);
699
+ }
700
+ if (prop === '$commit') {
701
+ return async <TResult>(
702
+ fn: Parameters<MutationsApi<DB>['$commit']>[0],
703
+ commitOptions?: { sync?: SyncMode }
704
+ ) => {
705
+ let callbackResult: TResult | undefined;
706
+ const commit = await run(
707
+ async () => {
708
+ const result = await client[apiName].$commit(fn);
709
+ callbackResult = result.result as TResult;
710
+ return result.commit;
711
+ },
712
+ 0,
713
+ commitOptions?.sync
714
+ );
715
+ return { result: callbackResult as TResult, commit };
716
+ };
717
+ }
718
+ if (typeof prop !== 'string') return undefined;
719
+ return wrapMutationsTable(client, apiName, prop, run);
720
+ },
721
+ }
722
+ ) as MutationsHook<DB>,
723
+ [apiName, client, error, isPending, reset, run]
724
+ );
725
+ }
726
+
727
+ function useOutboxStats(): SyncularOutboxStats | null {
728
+ const [stats, setStats] = useState<SyncularOutboxStats | null>(null);
729
+ useSyncEvent('outboxChanged', setStats);
730
+ return stats;
731
+ }
732
+
733
+ function useConflictStats(): SyncularConflictStats | null {
734
+ const [stats, setStats] = useState<SyncularConflictStats | null>(null);
735
+ useSyncEvent('conflictsChanged', setStats);
736
+ return stats;
737
+ }
738
+
739
+ function usePresence<
740
+ TMetadata extends Record<string, unknown> = Record<string, unknown>,
741
+ >(
742
+ scopeKey: string,
743
+ options: { enabled?: boolean } = {}
744
+ ): UsePresenceResult<TMetadata> {
745
+ const client = useClient();
746
+ const enabled = options.enabled !== false;
747
+ const [presence, setPresence] = useState<
748
+ SyncularPresenceEntry<TMetadata>[]
749
+ >([]);
750
+ const [isLoading, setIsLoading] = useState(enabled);
751
+
752
+ useEffect(() => {
753
+ if (!enabled) {
754
+ setPresence([]);
755
+ setIsLoading(false);
756
+ return undefined;
757
+ }
758
+ setPresence(client.presence.get<TMetadata>(scopeKey));
759
+ setIsLoading(false);
760
+ return client.presence.onChange<TMetadata>((event) => {
761
+ if (event.scopeKey === scopeKey) setPresence(event.presence);
762
+ });
763
+ }, [client, enabled, scopeKey]);
764
+
765
+ return { presence, isLoading };
766
+ }
767
+
768
+ function usePresenceWithJoin<
769
+ TMetadata extends Record<string, unknown> = Record<string, unknown>,
770
+ >(
771
+ scopeKey: string,
772
+ options: UsePresenceWithJoinOptions<TMetadata> = {}
773
+ ): UsePresenceWithJoinResult<TMetadata> {
774
+ const client = useClient();
775
+ const metadataRef = useLatest(options.metadata);
776
+ const enabled = options.enabled !== false;
777
+ const base = usePresence<TMetadata>(scopeKey, { enabled });
778
+ const [isJoined, setIsJoined] = useState(false);
779
+
780
+ const join = useCallback(
781
+ (metadata?: TMetadata) => {
782
+ client.presence.join(scopeKey, metadata);
783
+ setIsJoined(true);
784
+ },
785
+ [client, scopeKey]
786
+ );
787
+ const leave = useCallback(() => {
788
+ client.presence.leave(scopeKey);
789
+ setIsJoined(false);
790
+ }, [client, scopeKey]);
791
+ const updateMetadata = useCallback(
792
+ (metadata: TMetadata) => {
793
+ client.presence.updateMetadata(scopeKey, metadata);
794
+ },
795
+ [client, scopeKey]
796
+ );
797
+
798
+ useEffect(() => {
799
+ if (!enabled || options.autoJoin === false) return undefined;
800
+ join(metadataRef.current);
801
+ return () => leave();
802
+ }, [enabled, join, leave, metadataRef, options.autoJoin]);
803
+
804
+ return { ...base, updateMetadata, join, leave, isJoined };
805
+ }
806
+
807
+ function useBlobUploadQueue(): UseBlobUploadQueueResult {
808
+ const client = useClient();
809
+ const [stats, setStats] = useState<SyncularBlobUploadQueueStats | null>(
810
+ null
811
+ );
812
+ const refresh = useCallback(async () => {
813
+ setStats(await client.blobs.getUploadQueueStats());
814
+ }, [client]);
815
+ const process = useCallback(async () => {
816
+ const result = await client.blobs.processUploadQueue();
817
+ await refresh();
818
+ return result;
819
+ }, [client, refresh]);
820
+
821
+ useEffect(() => {
822
+ void refresh().catch(() => undefined);
823
+ return client.on('blobUploadCompleted', () => {
824
+ void refresh().catch(() => undefined);
825
+ });
826
+ }, [client, refresh]);
827
+
828
+ return { stats, refresh, process };
829
+ }
830
+
831
+ function useBlob(ref: BlobRef | null | undefined): Uint8Array | null {
832
+ const client = useClient();
833
+ const [data, setData] = useState<Uint8Array | null>(null);
834
+ useEffect(() => {
835
+ let disposed = false;
836
+ if (!ref) {
837
+ setData(null);
838
+ return undefined;
839
+ }
840
+ void client.blobs.retrieve(ref).then((next) => {
841
+ if (!disposed) setData(next);
842
+ });
843
+ return () => {
844
+ disposed = true;
845
+ };
846
+ }, [client, ref]);
847
+ return data;
848
+ }
849
+
850
+ return {
851
+ SyncProvider,
852
+ useBlob,
853
+ useBlobUploadQueue,
854
+ useConflictStats,
855
+ useLeasedMutation,
856
+ useLeasedMutations,
857
+ useMutation,
858
+ useMutations,
859
+ useOutboxStats,
860
+ usePresence,
861
+ usePresenceWithJoin,
862
+ useQuery,
863
+ useRowsChanged,
864
+ useSyncConnection,
865
+ useSyncContext,
866
+ useSyncStatus,
867
+ useSyncQuery,
868
+ };
869
+ }
870
+
871
+ async function executeSyncQuery<DB, TResult>(
872
+ db: Kysely<DB>,
873
+ query: SyncQueryFn<DB, TResult>
874
+ ): Promise<TResult> {
875
+ const ctx: SyncQueryContext<DB> = {
876
+ db,
877
+ selectFrom: db.selectFrom.bind(db) as Kysely<DB>['selectFrom'],
878
+ };
879
+ const result = await Promise.resolve(query(ctx));
880
+ if (isExecutableQuery<TResult>(result)) return await result.execute();
881
+ return result;
882
+ }
883
+
884
+ function isExecutableQuery<TResult>(
885
+ value: unknown
886
+ ): value is ExecutableQuery<TResult> {
887
+ return isRecord(value) && typeof value.execute === 'function';
888
+ }
889
+
890
+ function isCompilableExecutableQuery<TResult>(
891
+ value: unknown
892
+ ): value is ExecutableQuery<TResult> & { compile: () => CompiledQuery } {
893
+ return (
894
+ isExecutableQuery<TResult>(value) && typeof value.compile === 'function'
895
+ );
896
+ }
897
+
898
+ type LiveQueryCapableClient<DB> = SyncularClientLike<DB> & {
899
+ live<Row extends Record<string, unknown>>(
900
+ query: { compile(): CompiledQuery },
901
+ options: SyncularLiveQueryOptions<Row>
902
+ ): Promise<SyncularLiveQuerySubscription>;
903
+ };
904
+
905
+ function hasLiveQueries<DB>(
906
+ client: SyncularClientLike<DB>
907
+ ): client is LiveQueryCapableClient<DB> {
908
+ return typeof (client as { live?: unknown }).live === 'function';
909
+ }
910
+
911
+ function wrapMutationsTable<DB>(
912
+ client: SyncularClientLike<DB>,
913
+ apiName: 'mutations' | 'leasedMutations',
914
+ table: string,
915
+ run: <TResult extends MutationReceipt>(
916
+ operation: () => Promise<TResult>,
917
+ opCount: number,
918
+ syncOverride?: SyncMode
919
+ ) => Promise<TResult>
920
+ ): AnyTableMutations {
921
+ const tableApi = client[apiName].$table(table);
922
+ return {
923
+ insert: (values) => run(() => tableApi.insert(values), 1),
924
+ insertMany: (rows) => run(() => tableApi.insertMany(rows), rows.length),
925
+ update: (id, patch, options) =>
926
+ run(() => tableApi.update(id, patch, options), 1),
927
+ delete: (id, options) => run(() => tableApi.delete(id, options), 1),
928
+ upsert: (id, patch, options) =>
929
+ run(() => tableApi.upsert(id, patch, options), 1),
930
+ };
931
+ }
932
+
933
+ function useLatest<T>(value: T): { readonly current: T } {
934
+ const ref = useRef(value);
935
+ ref.current = value;
936
+ return ref;
937
+ }
938
+
939
+ function stableStringListKey(values: readonly string[] | undefined): string {
940
+ return values == null ? '' : values.join('\u0001');
941
+ }
942
+
943
+ function useStableSnapshot<T>(read: () => T): () => T {
944
+ const ref = useRef<{ value: T; json: string } | null>(null);
945
+ return useCallback(() => {
946
+ const value = read();
947
+ const json = JSON.stringify(value);
948
+ if (ref.current?.json === json) return ref.current.value;
949
+ ref.current = { value, json };
950
+ return value;
951
+ }, [read]);
952
+ }
953
+
954
+ function isRecord(value: unknown): value is Record<string, unknown> {
955
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
956
+ }
957
+
958
+ function toError(error: unknown): Error {
959
+ return error instanceof Error ? error : new Error(String(error));
960
+ }
961
+
962
+ function shareQueryResult<TResult>(
963
+ previous: TResult | undefined,
964
+ next: TResult,
965
+ keyField: string,
966
+ enabled: boolean
967
+ ): TResult {
968
+ if (!enabled || previous === undefined) return next;
969
+ if (Array.isArray(previous) && Array.isArray(next)) {
970
+ return shareArrayResult(previous, next, keyField) as TResult;
971
+ }
972
+ if (
973
+ isRecord(previous) &&
974
+ isRecord(next) &&
975
+ shallowEqualRecords(previous, next)
976
+ ) {
977
+ return previous;
978
+ }
979
+ return next;
980
+ }
981
+
982
+ function shareArrayResult<T>(previous: T[], next: T[], keyField: string): T[] {
983
+ if (previous.length === 0 || next.length === 0) return next;
984
+ const previousByKey = new Map<string, T>();
985
+ for (const item of previous) {
986
+ const key = keyedValue(item, keyField);
987
+ if (key == null) return next;
988
+ previousByKey.set(key, item);
989
+ }
990
+
991
+ let changed = previous.length !== next.length;
992
+ const shared = next.map((item, index) => {
993
+ const key = keyedValue(item, keyField);
994
+ const previousItem = key == null ? undefined : previousByKey.get(key);
995
+ if (
996
+ previousItem !== undefined &&
997
+ isRecord(previousItem) &&
998
+ isRecord(item) &&
999
+ shallowEqualRecords(previousItem, item)
1000
+ ) {
1001
+ if (!Object.is(previousItem, previous[index])) changed = true;
1002
+ return previousItem;
1003
+ }
1004
+ if (!Object.is(item, previous[index])) changed = true;
1005
+ return item;
1006
+ });
1007
+
1008
+ return changed ? shared : previous;
1009
+ }
1010
+
1011
+ function keyedValue(value: unknown, keyField: string): string | null {
1012
+ if (!isRecord(value)) return null;
1013
+ const key = value[keyField];
1014
+ return key == null ? null : String(key);
1015
+ }
1016
+
1017
+ function shallowEqualRecords(
1018
+ left: Record<string, unknown>,
1019
+ right: Record<string, unknown>
1020
+ ): boolean {
1021
+ if (left === right) return true;
1022
+ const leftKeys = Object.keys(left);
1023
+ const rightKeys = Object.keys(right);
1024
+ if (leftKeys.length !== rightKeys.length) return false;
1025
+ for (const key of leftKeys) {
1026
+ if (!(key in right) || !Object.is(left[key], right[key])) return false;
1027
+ }
1028
+ return true;
1029
+ }
1030
+
1031
+ export function changedRowsForTable(
1032
+ event: SyncularRowsChangedEvent,
1033
+ table: string
1034
+ ): SyncularChangedRow[] {
1035
+ return event.changedRows.filter((row) => row.table === table);
1036
+ }