@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,843 @@
1
+ import {
2
+ createSyncularSyncAttempt,
3
+ syncularDiagnosticAttemptFields,
4
+ } from './diagnostics';
5
+ import type {
6
+ ResolvedSyncularClientConfig,
7
+ SyncularDiagnosticEvent,
8
+ SyncularLiveQueryEvent,
9
+ SyncularRealtimeConnectionState,
10
+ SyncularSyncRequestOptions,
11
+ SyncularSyncResult,
12
+ } from './types';
13
+ import type {
14
+ SyncularWorkerEvent,
15
+ SyncularWorkerRealtimeOptions,
16
+ } from './worker-protocol';
17
+ import {
18
+ createSyncularWorkerErrorPayload,
19
+ SYNCULAR_WORKER_PROTOCOL_VERSION,
20
+ } from './worker-protocol';
21
+
22
+ export interface SyncularWorkerRealtimeClient {
23
+ syncPull(options?: SyncularSyncRequestOptions): Promise<SyncularSyncResult>;
24
+ applyRealtimeSyncPack?(bytes: Uint8Array): Promise<SyncularSyncResult>;
25
+ drainLiveQueryEvents<
26
+ Row extends Record<string, unknown> = Record<string, unknown>,
27
+ >(): Array<SyncularLiveQueryEvent<Row>>;
28
+ }
29
+
30
+ interface SyncularRealtimeSyncMessage {
31
+ cursor?: number;
32
+ reason?: string | null;
33
+ requiresPull?: boolean;
34
+ droppedCount?: number;
35
+ payloadBytes?: number;
36
+ syncPackBytes?: Uint8Array;
37
+ }
38
+
39
+ interface SyncularRealtimeHelloMessage {
40
+ protocolVersion?: number;
41
+ sessionId?: string;
42
+ shardKey?: string;
43
+ cursor?: number;
44
+ latestCursor?: number;
45
+ scopeCount?: number;
46
+ requiresSync?: boolean;
47
+ syncPackEncoding?: string | null;
48
+ }
49
+
50
+ interface SyncularRealtimePresenceMessage {
51
+ action: 'join' | 'leave' | 'update' | 'snapshot';
52
+ scopeKey: string;
53
+ clientId?: string;
54
+ actorId?: string;
55
+ metadata?: Record<string, unknown>;
56
+ entries?: Array<{
57
+ clientId: string;
58
+ actorId: string;
59
+ joinedAt: number;
60
+ metadata?: Record<string, unknown>;
61
+ }>;
62
+ }
63
+
64
+ export interface SyncularWorkerRealtimeSocket {
65
+ onopen: ((event: Event) => void) | null;
66
+ onmessage: ((event: MessageEvent) => void) | null;
67
+ onerror: ((event: Event) => void) | null;
68
+ onclose: ((event: CloseEvent) => void) | null;
69
+ send(data: string): void;
70
+ close(): void;
71
+ }
72
+
73
+ export interface SyncularWorkerRealtimeControllerOptions {
74
+ getClient(): SyncularWorkerRealtimeClient;
75
+ getConfig(): ResolvedSyncularClientConfig | undefined;
76
+ getLocationOrigin(): string;
77
+ createWebSocket(url: string): SyncularWorkerRealtimeSocket;
78
+ postEvent(event: SyncularWorkerEvent): void;
79
+ postDiagnostic?: (
80
+ event: Omit<SyncularDiagnosticEvent, 'at'> & { at?: number }
81
+ ) => void;
82
+ setTimeout?: typeof setTimeout;
83
+ clearTimeout?: typeof clearTimeout;
84
+ random?: () => number;
85
+ }
86
+
87
+ export class SyncularWorkerRealtimeController {
88
+ #socket: SyncularWorkerRealtimeSocket | undefined;
89
+ #state: SyncularRealtimeConnectionState = 'disconnected';
90
+ #options: SyncularWorkerRealtimeOptions | undefined;
91
+ #reconnectTimer: ReturnType<typeof setTimeout> | undefined;
92
+ #reconnectCatchupTimer: ReturnType<typeof setTimeout> | undefined;
93
+ #heartbeatTimer: ReturnType<typeof setTimeout> | undefined;
94
+ #reconnectAttempts = 0;
95
+ #stopped = true;
96
+ #syncInFlight: Promise<void> | undefined;
97
+ #syncAgainMessage: SyncularRealtimeSyncMessage | undefined;
98
+ #syncJitterTimers = new Set<ReturnType<typeof setTimeout>>();
99
+ readonly #setTimeout: typeof setTimeout;
100
+ readonly #clearTimeout: typeof clearTimeout;
101
+ readonly #random: () => number;
102
+
103
+ constructor(
104
+ private readonly controllerOptions: SyncularWorkerRealtimeControllerOptions
105
+ ) {
106
+ this.#setTimeout =
107
+ controllerOptions.setTimeout ??
108
+ (globalThis.setTimeout.bind(globalThis) as typeof setTimeout);
109
+ this.#clearTimeout =
110
+ controllerOptions.clearTimeout ??
111
+ (globalThis.clearTimeout.bind(globalThis) as typeof clearTimeout);
112
+ this.#random = controllerOptions.random ?? Math.random;
113
+ }
114
+
115
+ start(options: SyncularWorkerRealtimeOptions): void {
116
+ if (!this.controllerOptions.getConfig()) {
117
+ throw createSyncularWorkerErrorPayload(
118
+ 'worker.not_open',
119
+ 'Syncular worker client is not open'
120
+ );
121
+ }
122
+ this.stop();
123
+ this.#options = options;
124
+ this.#stopped = false;
125
+ this.#reconnectAttempts = 0;
126
+ this.#connect();
127
+ }
128
+
129
+ stop(): void {
130
+ this.#stopped = true;
131
+ this.#syncAgainMessage = undefined;
132
+ this.#syncInFlight = undefined;
133
+ this.#clearSyncJitterTimers();
134
+ this.#clearReconnectCatchupTimer();
135
+ this.#clearReconnectTimer();
136
+ this.#clearHeartbeatTimer();
137
+ this.#closeSocket();
138
+ this.#setState('disconnected');
139
+ }
140
+
141
+ sendPresence(
142
+ action: 'join' | 'leave' | 'update',
143
+ scopeKey: string,
144
+ metadata?: Record<string, unknown>
145
+ ): void {
146
+ const socket = this.#socket;
147
+ if (!socket || this.#state !== 'connected') {
148
+ this.#diagnostic({
149
+ level: 'warn',
150
+ code: 'realtime.presence_not_connected',
151
+ message: 'Syncular realtime presence send skipped while disconnected',
152
+ details: { action, scopeKey },
153
+ });
154
+ return;
155
+ }
156
+ socket.send(
157
+ JSON.stringify({
158
+ type: 'presence',
159
+ action,
160
+ scopeKey,
161
+ ...(metadata === undefined ? {} : { metadata }),
162
+ })
163
+ );
164
+ }
165
+
166
+ #connect(): void {
167
+ const config = this.controllerOptions.getConfig();
168
+ const options = this.#options;
169
+ if (!config || !options || this.#stopped) return;
170
+
171
+ this.#clearReconnectTimer();
172
+ this.#clearHeartbeatTimer();
173
+ this.#setState('connecting');
174
+
175
+ let socket: SyncularWorkerRealtimeSocket;
176
+ try {
177
+ socket = this.controllerOptions.createWebSocket(
178
+ resolveSyncularRealtimeUrl(
179
+ config,
180
+ options,
181
+ this.controllerOptions.getLocationOrigin()
182
+ )
183
+ );
184
+ } catch {
185
+ this.#diagnostic({
186
+ level: 'warn',
187
+ code: 'realtime.connect_failed',
188
+ message: 'Syncular realtime websocket creation failed',
189
+ details: { attempt: this.#reconnectAttempts + 1 },
190
+ });
191
+ this.#scheduleReconnect();
192
+ return;
193
+ }
194
+ this.#socket = socket;
195
+
196
+ socket.onopen = () => {
197
+ if (socket !== this.#socket) return;
198
+ this.#reconnectAttempts = 0;
199
+ this.#setState('connected');
200
+ this.#resetHeartbeatTimer();
201
+ };
202
+ socket.onmessage = (event) => {
203
+ if (socket !== this.#socket) return;
204
+ this.#resetHeartbeatTimer();
205
+ void this.#handleMessage(socket, event.data);
206
+ };
207
+ socket.onerror = () => {
208
+ if (socket !== this.#socket) return;
209
+ this.#diagnostic({
210
+ level: 'warn',
211
+ code: 'realtime.socket_error',
212
+ message: 'Syncular realtime websocket reported an error',
213
+ });
214
+ this.#reconnect();
215
+ };
216
+ socket.onclose = () => {
217
+ if (socket !== this.#socket) return;
218
+ this.#reconnect();
219
+ };
220
+ }
221
+
222
+ async #handleMessage(
223
+ socket: SyncularWorkerRealtimeSocket,
224
+ data: MessageEvent['data']
225
+ ): Promise<void> {
226
+ const bytes = await readRealtimeMessageBytes(data);
227
+ if (socket !== this.#socket) return;
228
+ if (bytes && isSyncPackBytes(bytes)) {
229
+ this.#diagnostic({
230
+ level: 'debug',
231
+ code: 'realtime.sync_wakeup',
232
+ message: 'Syncular realtime binary sync-pack received',
233
+ details: {
234
+ bytes: bytes.byteLength,
235
+ },
236
+ });
237
+ this.#clearReconnectCatchupTimer();
238
+ this.#scheduleSync({ syncPackBytes: bytes });
239
+ return;
240
+ }
241
+
242
+ const text = bytes ? new TextDecoder().decode(bytes) : data;
243
+ if (text == null) return;
244
+ if (typeof text !== 'string') return;
245
+ let message: unknown;
246
+ try {
247
+ message = JSON.parse(text);
248
+ } catch {
249
+ return;
250
+ }
251
+ const helloMessage = readSyncularRealtimeHelloMessage(message);
252
+ if (helloMessage) {
253
+ this.#diagnostic({
254
+ level: 'debug',
255
+ code: 'realtime.hello',
256
+ message: 'Syncular realtime session accepted',
257
+ details: { ...helloMessage },
258
+ });
259
+ this.#scheduleHelloCatchup(helloMessage);
260
+ return;
261
+ }
262
+ const presenceMessage = readSyncularRealtimePresenceMessage(message);
263
+ if (presenceMessage) {
264
+ this.controllerOptions.postEvent({
265
+ protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
266
+ type: 'presenceEvent',
267
+ ...presenceMessage,
268
+ });
269
+ return;
270
+ }
271
+ const syncMessage = readSyncularRealtimeSyncMessage(message);
272
+ if (syncMessage) {
273
+ this.#diagnostic({
274
+ level: 'debug',
275
+ code: 'realtime.sync_wakeup',
276
+ message: 'Syncular realtime sync wakeup received',
277
+ details: {
278
+ reason: syncMessage.reason ?? null,
279
+ requiresPull: syncMessage.requiresPull === true,
280
+ droppedCount: syncMessage.droppedCount ?? 0,
281
+ payloadBytes: syncMessage.payloadBytes ?? null,
282
+ },
283
+ });
284
+ this.#clearReconnectCatchupTimer();
285
+ this.#scheduleRealtimeWakeupSync(syncMessage);
286
+ }
287
+ }
288
+
289
+ #reconnect(): void {
290
+ this.#clearReconnectCatchupTimer();
291
+ this.#clearHeartbeatTimer();
292
+ this.#closeSocket();
293
+ this.#setState('disconnected');
294
+ this.#scheduleReconnect();
295
+ }
296
+
297
+ #scheduleReconnect(): void {
298
+ if (this.#stopped) return;
299
+ this.#clearReconnectTimer();
300
+ const initial = this.#options?.initialReconnectDelayMs ?? 1_000;
301
+ const max = this.#options?.maxReconnectDelayMs ?? 30_000;
302
+ const factor = this.#options?.reconnectBackoffFactor ?? 2;
303
+ const baseDelay = Math.min(
304
+ initial * factor ** this.#reconnectAttempts,
305
+ max
306
+ );
307
+ const jitterRatio = Math.max(0, this.#options?.reconnectJitterRatio ?? 0);
308
+ const jitter = baseDelay * jitterRatio * this.#random();
309
+ const delay = Math.min(baseDelay + jitter, max);
310
+ this.#diagnostic({
311
+ level: 'info',
312
+ code: 'realtime.reconnect_scheduled',
313
+ message: 'Syncular realtime reconnect scheduled',
314
+ details: {
315
+ attempt: this.#reconnectAttempts + 1,
316
+ baseDelayMs: baseDelay,
317
+ jitterMs: jitter,
318
+ delayMs: delay,
319
+ },
320
+ });
321
+ this.#reconnectAttempts += 1;
322
+ this.#reconnectTimer = this.#setTimeout(() => this.#connect(), delay);
323
+ }
324
+
325
+ #scheduleHelloCatchup(message: SyncularRealtimeHelloMessage): void {
326
+ this.#clearReconnectCatchupTimer();
327
+ if (!message.requiresSync) return;
328
+ const maxJitter = Math.max(0, this.#options?.pullRecoveryJitterMs ?? 0);
329
+ const delay = maxJitter > 0 ? maxJitter * this.#random() : 10;
330
+ const cursor = Number.isFinite(message.latestCursor)
331
+ ? message.latestCursor
332
+ : message.cursor;
333
+ const syncMessage: SyncularRealtimeSyncMessage = {
334
+ reason: 'reconnect-catchup',
335
+ requiresPull: true,
336
+ ...(Number.isFinite(cursor) ? { cursor } : {}),
337
+ };
338
+ this.#diagnostic({
339
+ level: 'debug',
340
+ code: 'realtime.reconnect_catchup_scheduled',
341
+ message: 'Syncular realtime reconnect catch-up scheduled from hello',
342
+ details: {
343
+ cursor: syncMessage.cursor ?? null,
344
+ delayMs: delay,
345
+ maxJitterMs: maxJitter,
346
+ },
347
+ });
348
+ this.#reconnectCatchupTimer = this.#setTimeout(() => {
349
+ this.#reconnectCatchupTimer = undefined;
350
+ this.#scheduleSync(syncMessage);
351
+ }, delay);
352
+ }
353
+
354
+ #scheduleSync(message?: SyncularRealtimeSyncMessage): void {
355
+ if (this.#stopped) return;
356
+ if (this.#syncInFlight) {
357
+ this.#syncAgainMessage = mergeRealtimeSyncMessages(
358
+ this.#syncAgainMessage,
359
+ message
360
+ );
361
+ return;
362
+ }
363
+ this.#syncInFlight = this.#runSync(message);
364
+ }
365
+
366
+ #scheduleRealtimeWakeupSync(message: SyncularRealtimeSyncMessage): void {
367
+ const maxJitter = Math.max(0, this.#options?.pullRecoveryJitterMs ?? 0);
368
+ if (maxJitter <= 0 || !realtimeMessageRequiresPull(message)) {
369
+ this.#scheduleSync(message);
370
+ return;
371
+ }
372
+ const delay = maxJitter * this.#random();
373
+ this.#diagnostic({
374
+ level: 'debug',
375
+ code: 'realtime.pull_recovery_jitter_scheduled',
376
+ message: 'Syncular realtime HTTP pull recovery delayed by jitter',
377
+ details: {
378
+ delayMs: delay,
379
+ maxJitterMs: maxJitter,
380
+ reason: message.reason ?? null,
381
+ cursor: message.cursor ?? null,
382
+ },
383
+ });
384
+ const timer = this.#setTimeout(() => {
385
+ this.#syncJitterTimers.delete(timer);
386
+ this.#scheduleSync(message);
387
+ }, delay);
388
+ this.#syncJitterTimers.add(timer);
389
+ }
390
+
391
+ async #runSync(message?: SyncularRealtimeSyncMessage): Promise<void> {
392
+ const socket = this.#socket;
393
+ let result: SyncularSyncResult | undefined;
394
+ try {
395
+ result = await this.#syncForMessage(message);
396
+ if (this.#stopped) return;
397
+ this.#ackRealtimeCursor(socket, message, result);
398
+ this.controllerOptions.postEvent({
399
+ protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
400
+ type: 'bootstrapChanged',
401
+ bootstrap: result.bootstrap,
402
+ });
403
+ if (result.changedTables.length > 0 || result.changedRows.length > 0) {
404
+ this.controllerOptions.postEvent({
405
+ protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
406
+ type: 'rowsChanged',
407
+ source: 'remotePull',
408
+ changedTables: result.changedTables,
409
+ changedRows: result.changedRows,
410
+ changedRowsTruncated: result.changedRowsTruncated,
411
+ });
412
+ }
413
+ const events = this.controllerOptions
414
+ .getClient()
415
+ .drainLiveQueryEvents<Record<string, unknown>>();
416
+ if (events.length > 0) {
417
+ this.controllerOptions.postEvent({
418
+ protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
419
+ type: 'liveQueryEvents',
420
+ events,
421
+ });
422
+ }
423
+ } catch {
424
+ this.#diagnostic({
425
+ level: 'warn',
426
+ code: 'realtime.sync_pull_failed',
427
+ message: 'Syncular realtime-triggered pull failed',
428
+ });
429
+ // Realtime is best-effort. Explicit sync methods still surface errors.
430
+ } finally {
431
+ this.#syncInFlight = undefined;
432
+ const nextMessage = this.#syncAgainMessage;
433
+ this.#syncAgainMessage = undefined;
434
+ if (
435
+ nextMessage &&
436
+ !this.#stopped &&
437
+ !syncResultCoversRealtimeMessage(result, nextMessage)
438
+ ) {
439
+ this.#scheduleSync(nextMessage);
440
+ }
441
+ }
442
+ }
443
+
444
+ #ackRealtimeCursor(
445
+ socket: SyncularWorkerRealtimeSocket | undefined,
446
+ message: SyncularRealtimeSyncMessage | undefined,
447
+ result: SyncularSyncResult
448
+ ): void {
449
+ if (!socket || socket !== this.#socket) return;
450
+ const subscriptionCursor = result.subscriptions.reduce(
451
+ (cursor, subscription) =>
452
+ Number.isFinite(subscription.nextCursor)
453
+ ? Math.max(cursor, subscription.nextCursor)
454
+ : cursor,
455
+ -1
456
+ );
457
+ const messageCursor = Number.isFinite(message?.cursor)
458
+ ? message!.cursor!
459
+ : -1;
460
+ const cursor = Math.max(subscriptionCursor, messageCursor);
461
+ if (!Number.isSafeInteger(cursor) || cursor < 0) return;
462
+ try {
463
+ socket.send(JSON.stringify({ type: 'ack', cursor }));
464
+ this.#diagnostic({
465
+ level: 'debug',
466
+ code: 'realtime.ack_sent',
467
+ message: 'Syncular realtime cursor ack sent',
468
+ details: { cursor },
469
+ });
470
+ } catch {
471
+ this.#diagnostic({
472
+ level: 'warn',
473
+ code: 'realtime.ack_failed',
474
+ message: 'Syncular realtime cursor ack failed',
475
+ details: { cursor },
476
+ });
477
+ }
478
+ }
479
+
480
+ async #syncForMessage(
481
+ message: SyncularRealtimeSyncMessage | undefined
482
+ ): Promise<SyncularSyncResult> {
483
+ const client = this.controllerOptions.getClient();
484
+ if (message && realtimeMessageRequiresPull(message)) {
485
+ const syncAttempt = createSyncularSyncAttempt();
486
+ this.#diagnostic({
487
+ level: 'debug',
488
+ code: 'realtime.pull_required',
489
+ message: 'Syncular realtime event requires HTTP pull recovery',
490
+ ...syncularDiagnosticAttemptFields(syncAttempt),
491
+ details: {
492
+ cursor: message.cursor ?? null,
493
+ reason: message.reason ?? null,
494
+ droppedCount: message.droppedCount ?? 0,
495
+ payloadBytes: message.payloadBytes ?? null,
496
+ },
497
+ });
498
+ return client.syncPull({ syncAttempt });
499
+ }
500
+ if (
501
+ message?.syncPackBytes &&
502
+ typeof client.applyRealtimeSyncPack === 'function'
503
+ ) {
504
+ try {
505
+ const result = await client.applyRealtimeSyncPack(
506
+ message.syncPackBytes
507
+ );
508
+ this.#diagnostic({
509
+ level: 'debug',
510
+ code: 'realtime.binary_applied',
511
+ message: 'Syncular realtime binary sync-pack applied',
512
+ details: {
513
+ bytes: message.syncPackBytes.byteLength,
514
+ totalMs: result.timings.totalMs,
515
+ syncPackDecodeMs: result.timings.syncPackDecodeMs,
516
+ pullTransformMs: result.timings.pullTransformMs,
517
+ integrityVerifyMs: result.timings.integrityVerifyMs,
518
+ pullApplyMs: result.timings.pullApplyMs,
519
+ commitApplyMs: result.timings.commitApplyMs,
520
+ subscriptionStateMs: result.timings.subscriptionStateMs,
521
+ notifyMs: result.timings.notifyMs,
522
+ changedRows: result.changedRows.length,
523
+ changedRowsTruncated: result.changedRowsTruncated,
524
+ },
525
+ });
526
+ return result;
527
+ } catch {
528
+ this.#diagnostic({
529
+ level: 'warn',
530
+ code: 'realtime.binary_apply_failed',
531
+ message:
532
+ 'Syncular realtime binary sync-pack apply failed; using HTTP pull recovery',
533
+ details: {
534
+ bytes: message.syncPackBytes.byteLength,
535
+ },
536
+ });
537
+ }
538
+ }
539
+ const syncAttempt = createSyncularSyncAttempt();
540
+ this.#diagnostic({
541
+ level: 'debug',
542
+ code: 'realtime.pull_required',
543
+ message: 'Syncular realtime event requires HTTP pull recovery',
544
+ ...syncularDiagnosticAttemptFields(syncAttempt),
545
+ details: {
546
+ cursor: message?.cursor ?? null,
547
+ reason: message?.syncPackBytes ? 'binary-apply-failed' : null,
548
+ droppedCount: message?.droppedCount ?? 0,
549
+ payloadBytes: message?.payloadBytes ?? null,
550
+ },
551
+ });
552
+ return client.syncPull({ syncAttempt });
553
+ }
554
+
555
+ #clearReconnectTimer(): void {
556
+ if (!this.#reconnectTimer) return;
557
+ this.#clearTimeout(this.#reconnectTimer);
558
+ this.#reconnectTimer = undefined;
559
+ }
560
+
561
+ #clearReconnectCatchupTimer(): void {
562
+ if (!this.#reconnectCatchupTimer) return;
563
+ this.#clearTimeout(this.#reconnectCatchupTimer);
564
+ this.#reconnectCatchupTimer = undefined;
565
+ }
566
+
567
+ #clearHeartbeatTimer(): void {
568
+ if (!this.#heartbeatTimer) return;
569
+ this.#clearTimeout(this.#heartbeatTimer);
570
+ this.#heartbeatTimer = undefined;
571
+ }
572
+
573
+ #clearSyncJitterTimers(): void {
574
+ for (const timer of this.#syncJitterTimers) {
575
+ this.#clearTimeout(timer);
576
+ }
577
+ this.#syncJitterTimers.clear();
578
+ }
579
+
580
+ #resetHeartbeatTimer(): void {
581
+ this.#clearHeartbeatTimer();
582
+ const heartbeatTimeoutMs = this.#options?.heartbeatTimeoutMs ?? 60_000;
583
+ if (heartbeatTimeoutMs <= 0) return;
584
+ this.#heartbeatTimer = this.#setTimeout(
585
+ () => this.#reconnect(),
586
+ heartbeatTimeoutMs
587
+ );
588
+ }
589
+
590
+ #closeSocket(): void {
591
+ if (!this.#socket) return;
592
+ const socket = this.#socket;
593
+ this.#socket = undefined;
594
+ socket.onopen = null;
595
+ socket.onmessage = null;
596
+ socket.onerror = null;
597
+ socket.onclose = null;
598
+ try {
599
+ socket.close();
600
+ } catch {
601
+ // best effort
602
+ }
603
+ }
604
+
605
+ #setState(state: SyncularRealtimeConnectionState): void {
606
+ if (this.#state === state) return;
607
+ this.#state = state;
608
+ this.controllerOptions.postEvent({
609
+ protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
610
+ type: 'realtimeState',
611
+ state,
612
+ });
613
+ }
614
+
615
+ #diagnostic(
616
+ event: Omit<SyncularDiagnosticEvent, 'at' | 'source'> & { at?: number }
617
+ ): void {
618
+ this.controllerOptions.postDiagnostic?.({
619
+ ...event,
620
+ source: 'realtime',
621
+ });
622
+ }
623
+ }
624
+
625
+ function realtimeMessageRequiresPull(
626
+ message: SyncularRealtimeSyncMessage
627
+ ): boolean {
628
+ return message.requiresPull === true || (message.droppedCount ?? 0) > 0;
629
+ }
630
+
631
+ function mergeRealtimeSyncMessages(
632
+ current: SyncularRealtimeSyncMessage | undefined,
633
+ next: SyncularRealtimeSyncMessage | undefined
634
+ ): SyncularRealtimeSyncMessage {
635
+ if (!current) return next ?? {};
636
+ if (!next) return current;
637
+ const cursor =
638
+ current.cursor === undefined
639
+ ? next.cursor
640
+ : next.cursor === undefined
641
+ ? current.cursor
642
+ : Math.max(current.cursor, next.cursor);
643
+ return {
644
+ cursor,
645
+ reason: next.reason ?? current.reason,
646
+ requiresPull: current.requiresPull === true || next.requiresPull === true,
647
+ droppedCount: Math.max(current.droppedCount ?? 0, next.droppedCount ?? 0),
648
+ payloadBytes: next.payloadBytes ?? current.payloadBytes,
649
+ syncPackBytes: next.syncPackBytes ?? current.syncPackBytes,
650
+ };
651
+ }
652
+
653
+ function syncResultCoversRealtimeMessage(
654
+ result: SyncularSyncResult | undefined,
655
+ message: SyncularRealtimeSyncMessage
656
+ ): boolean {
657
+ if (!result || message.cursor === undefined) return false;
658
+ return syncResultCursor(result) >= message.cursor;
659
+ }
660
+
661
+ function syncResultCursor(result: SyncularSyncResult): number {
662
+ return result.subscriptions.reduce(
663
+ (cursor, subscription) =>
664
+ Number.isFinite(subscription.nextCursor)
665
+ ? Math.max(cursor, subscription.nextCursor)
666
+ : cursor,
667
+ -1
668
+ );
669
+ }
670
+
671
+ export function resolveSyncularRealtimeUrl(
672
+ config: ResolvedSyncularClientConfig,
673
+ options: SyncularWorkerRealtimeOptions,
674
+ locationOrigin: string
675
+ ): string {
676
+ const url = new URL(options.wsUrl ?? config.baseUrl, locationOrigin);
677
+ if (!options.wsUrl) {
678
+ url.pathname = `${url.pathname.replace(/\/$/, '')}/realtime`;
679
+ }
680
+ if (url.protocol === 'http:') url.protocol = 'ws:';
681
+ if (url.protocol === 'https:') url.protocol = 'wss:';
682
+ url.searchParams.set('clientId', config.clientId);
683
+ url.searchParams.set('transportPath', 'direct');
684
+ url.searchParams.set('syncPackEncoding', 'binary-sync-pack-v1');
685
+ for (const [key, value] of Object.entries(options.params ?? {})) {
686
+ if (value) url.searchParams.set(key, value);
687
+ }
688
+ return url.toString();
689
+ }
690
+
691
+ export function isSyncularRealtimeSyncMessage(value: unknown): boolean {
692
+ return readSyncularRealtimeSyncMessage(value) !== null;
693
+ }
694
+
695
+ function readSyncularRealtimeHelloMessage(
696
+ value: unknown
697
+ ): SyncularRealtimeHelloMessage | null {
698
+ if (!value || typeof value !== 'object') return null;
699
+ if ((value as { event?: unknown }).event !== 'hello') return null;
700
+ const data = (value as { data?: unknown }).data;
701
+ if (!data || typeof data !== 'object') return {};
702
+ const record = data as Record<string, unknown>;
703
+ return {
704
+ protocolVersion:
705
+ typeof record.protocolVersion === 'number'
706
+ ? record.protocolVersion
707
+ : undefined,
708
+ sessionId:
709
+ typeof record.sessionId === 'string' ? record.sessionId : undefined,
710
+ shardKey: typeof record.shardKey === 'string' ? record.shardKey : undefined,
711
+ cursor: typeof record.cursor === 'number' ? record.cursor : undefined,
712
+ latestCursor:
713
+ typeof record.latestCursor === 'number' ? record.latestCursor : undefined,
714
+ scopeCount:
715
+ typeof record.scopeCount === 'number' ? record.scopeCount : undefined,
716
+ requiresSync:
717
+ typeof record.requiresSync === 'boolean'
718
+ ? record.requiresSync
719
+ : undefined,
720
+ syncPackEncoding:
721
+ typeof record.syncPackEncoding === 'string'
722
+ ? record.syncPackEncoding
723
+ : record.syncPackEncoding === null
724
+ ? null
725
+ : undefined,
726
+ };
727
+ }
728
+
729
+ function readSyncularRealtimeSyncMessage(
730
+ value: unknown
731
+ ): SyncularRealtimeSyncMessage | null {
732
+ if (!value || typeof value !== 'object') return null;
733
+ if ((value as { event?: unknown }).event !== 'sync') return null;
734
+ const data = (value as { data?: unknown }).data;
735
+ if (!data || typeof data !== 'object') return {};
736
+ const record = data as Record<string, unknown>;
737
+ const cursor = typeof record.cursor === 'number' ? record.cursor : undefined;
738
+ const reason = typeof record.reason === 'string' ? record.reason : null;
739
+ const requiresPull =
740
+ typeof record.requiresPull === 'boolean' ? record.requiresPull : undefined;
741
+ const droppedCount =
742
+ typeof record.droppedCount === 'number' &&
743
+ Number.isSafeInteger(record.droppedCount)
744
+ ? record.droppedCount
745
+ : undefined;
746
+ const payloadBytes =
747
+ typeof record.payloadBytes === 'number' &&
748
+ Number.isSafeInteger(record.payloadBytes)
749
+ ? record.payloadBytes
750
+ : undefined;
751
+ return {
752
+ cursor,
753
+ reason,
754
+ requiresPull,
755
+ droppedCount,
756
+ payloadBytes,
757
+ };
758
+ }
759
+
760
+ function readSyncularRealtimePresenceMessage(
761
+ value: unknown
762
+ ): SyncularRealtimePresenceMessage | null {
763
+ if (!value || typeof value !== 'object') return null;
764
+ if ((value as { event?: unknown }).event !== 'presence') return null;
765
+ const data = (value as { data?: unknown }).data;
766
+ if (!data || typeof data !== 'object') return null;
767
+ const presence = (data as { presence?: unknown }).presence;
768
+ if (!presence || typeof presence !== 'object') return null;
769
+ const record = presence as Record<string, unknown>;
770
+ const action = record.action;
771
+ if (
772
+ action !== 'join' &&
773
+ action !== 'leave' &&
774
+ action !== 'update' &&
775
+ action !== 'snapshot'
776
+ ) {
777
+ return null;
778
+ }
779
+ if (typeof record.scopeKey !== 'string' || !record.scopeKey) return null;
780
+ return {
781
+ action,
782
+ scopeKey: record.scopeKey,
783
+ clientId: typeof record.clientId === 'string' ? record.clientId : undefined,
784
+ actorId: typeof record.actorId === 'string' ? record.actorId : undefined,
785
+ metadata: objectRecordOrUndefined(record.metadata),
786
+ entries: Array.isArray(record.entries)
787
+ ? record.entries.flatMap(readPresenceEntry)
788
+ : undefined,
789
+ };
790
+ }
791
+
792
+ function readPresenceEntry(value: unknown): Array<{
793
+ clientId: string;
794
+ actorId: string;
795
+ joinedAt: number;
796
+ metadata?: Record<string, unknown>;
797
+ }> {
798
+ if (!value || typeof value !== 'object') return [];
799
+ const record = value as Record<string, unknown>;
800
+ if (typeof record.clientId !== 'string') return [];
801
+ if (typeof record.actorId !== 'string') return [];
802
+ return [
803
+ {
804
+ clientId: record.clientId,
805
+ actorId: record.actorId,
806
+ joinedAt:
807
+ typeof record.joinedAt === 'number' ? record.joinedAt : Date.now(),
808
+ metadata: objectRecordOrUndefined(record.metadata),
809
+ },
810
+ ];
811
+ }
812
+
813
+ function objectRecordOrUndefined(
814
+ value: unknown
815
+ ): Record<string, unknown> | undefined {
816
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
817
+ return undefined;
818
+ }
819
+ return { ...(value as Record<string, unknown>) };
820
+ }
821
+
822
+ async function readRealtimeMessageBytes(
823
+ data: MessageEvent['data']
824
+ ): Promise<Uint8Array | null> {
825
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
826
+ if (ArrayBuffer.isView(data)) {
827
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
828
+ }
829
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
830
+ return new Uint8Array(await data.arrayBuffer());
831
+ }
832
+ return null;
833
+ }
834
+
835
+ function isSyncPackBytes(bytes: Uint8Array): boolean {
836
+ return (
837
+ bytes.byteLength >= 4 &&
838
+ bytes[0] === 0x53 &&
839
+ bytes[1] === 0x53 &&
840
+ bytes[2] === 0x50 &&
841
+ bytes[3] === 0x31
842
+ );
843
+ }