@syncular/client 0.1.2 → 0.2.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 (233) hide show
  1. package/README.md +227 -709
  2. package/dist/apply.d.ts +80 -0
  3. package/dist/apply.js +301 -0
  4. package/dist/blob.d.ts +127 -0
  5. package/dist/blob.js +201 -0
  6. package/dist/bun-database.d.ts +22 -0
  7. package/dist/bun-database.js +61 -0
  8. package/dist/client.d.ts +320 -69
  9. package/dist/client.js +1627 -163
  10. package/dist/content-type.d.ts +2 -0
  11. package/dist/content-type.js +2 -0
  12. package/dist/database.d.ts +35 -80
  13. package/dist/database.js +32 -947
  14. package/dist/encryption.d.ts +47 -0
  15. package/dist/encryption.js +75 -0
  16. package/dist/errors.d.ts +8 -22
  17. package/dist/errors.js +10 -207
  18. package/dist/http.d.ts +32 -0
  19. package/dist/http.js +196 -0
  20. package/dist/index.d.ts +28 -16
  21. package/dist/index.js +25 -12
  22. package/dist/invalidation.d.ts +69 -0
  23. package/dist/invalidation.js +84 -0
  24. package/dist/leader-lock.d.ts +28 -0
  25. package/dist/leader-lock.js +38 -0
  26. package/dist/multi-tab.d.ts +134 -0
  27. package/dist/multi-tab.js +399 -0
  28. package/dist/node-database.d.ts +41 -0
  29. package/dist/node-database.js +133 -0
  30. package/dist/outbox.d.ts +56 -0
  31. package/dist/outbox.js +140 -0
  32. package/dist/schema.d.ts +107 -0
  33. package/dist/schema.js +267 -0
  34. package/dist/state.d.ts +40 -0
  35. package/dist/state.js +77 -0
  36. package/dist/transport.d.ts +44 -0
  37. package/dist/transport.js +7 -0
  38. package/dist/wasm-database.d.ts +30 -0
  39. package/dist/wasm-database.js +170 -0
  40. package/dist/window.d.ts +64 -0
  41. package/dist/window.js +0 -0
  42. package/dist/worker-entry.d.ts +16 -2
  43. package/dist/worker-entry.js +300 -456
  44. package/dist/worker-host.d.ts +174 -0
  45. package/dist/worker-host.js +536 -0
  46. package/dist/worker-protocol.d.ts +173 -61
  47. package/dist/worker-protocol.js +7 -16
  48. package/package.json +45 -112
  49. package/src/apply.ts +435 -0
  50. package/src/blob.ts +333 -0
  51. package/src/bun-database.ts +75 -0
  52. package/src/client.ts +2247 -277
  53. package/src/content-type.ts +2 -0
  54. package/src/database.ts +63 -1389
  55. package/src/encryption.ts +123 -0
  56. package/src/errors.ts +11 -265
  57. package/src/http.ts +254 -0
  58. package/src/index.ts +25 -20
  59. package/src/invalidation.ts +128 -0
  60. package/src/leader-lock.ts +68 -0
  61. package/src/multi-tab.ts +550 -0
  62. package/src/node-database.ts +180 -0
  63. package/src/outbox.ts +206 -0
  64. package/src/schema.ts +397 -0
  65. package/src/state.ts +126 -0
  66. package/src/transport.ts +52 -0
  67. package/src/wasm-database.ts +279 -0
  68. package/src/window.ts +0 -0
  69. package/src/worker-entry.ts +391 -545
  70. package/src/worker-host.ts +800 -0
  71. package/src/worker-protocol.ts +204 -99
  72. package/dist/auth-leases.d.ts +0 -11
  73. package/dist/auth-leases.d.ts.map +0 -1
  74. package/dist/auth-leases.js +0 -140
  75. package/dist/auth-leases.js.map +0 -1
  76. package/dist/blob-limits.d.ts +0 -15
  77. package/dist/blob-limits.d.ts.map +0 -1
  78. package/dist/blob-limits.js +0 -66
  79. package/dist/blob-limits.js.map +0 -1
  80. package/dist/bridge-client.d.ts +0 -69
  81. package/dist/bridge-client.d.ts.map +0 -1
  82. package/dist/bridge-client.js +0 -282
  83. package/dist/bridge-client.js.map +0 -1
  84. package/dist/client-config.d.ts +0 -6
  85. package/dist/client-config.d.ts.map +0 -1
  86. package/dist/client-config.js +0 -18
  87. package/dist/client-config.js.map +0 -1
  88. package/dist/client.d.ts.map +0 -1
  89. package/dist/client.js.map +0 -1
  90. package/dist/command-history.d.ts +0 -35
  91. package/dist/command-history.d.ts.map +0 -1
  92. package/dist/command-history.js +0 -378
  93. package/dist/command-history.js.map +0 -1
  94. package/dist/console-diagnostics.d.ts +0 -33
  95. package/dist/console-diagnostics.d.ts.map +0 -1
  96. package/dist/console-diagnostics.js +0 -496
  97. package/dist/console-diagnostics.js.map +0 -1
  98. package/dist/crdt-yjs/index.d.ts +0 -4
  99. package/dist/crdt-yjs/index.d.ts.map +0 -1
  100. package/dist/crdt-yjs/index.js +0 -4
  101. package/dist/crdt-yjs/index.js.map +0 -1
  102. package/dist/crdt-yjs/webview-host-facade.d.ts +0 -126
  103. package/dist/crdt-yjs/webview-host-facade.d.ts.map +0 -1
  104. package/dist/crdt-yjs/webview-host-facade.js +0 -284
  105. package/dist/crdt-yjs/webview-host-facade.js.map +0 -1
  106. package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +0 -153
  107. package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +0 -1
  108. package/dist/crdt-yjs/yjs-document-field-adapter.js +0 -406
  109. package/dist/crdt-yjs/yjs-document-field-adapter.js.map +0 -1
  110. package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +0 -73
  111. package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +0 -1
  112. package/dist/crdt-yjs/yjs-prosemirror-bridge.js +0 -169
  113. package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +0 -1
  114. package/dist/database.d.ts.map +0 -1
  115. package/dist/database.js.map +0 -1
  116. package/dist/diagnostics.d.ts +0 -10
  117. package/dist/diagnostics.d.ts.map +0 -1
  118. package/dist/diagnostics.js +0 -83
  119. package/dist/diagnostics.js.map +0 -1
  120. package/dist/errors.d.ts.map +0 -1
  121. package/dist/errors.js.map +0 -1
  122. package/dist/generated-bridge.d.ts +0 -365
  123. package/dist/generated-bridge.d.ts.map +0 -1
  124. package/dist/generated-bridge.js +0 -250
  125. package/dist/generated-bridge.js.map +0 -1
  126. package/dist/index.d.ts.map +0 -1
  127. package/dist/index.js.map +0 -1
  128. package/dist/mutations.d.ts +0 -72
  129. package/dist/mutations.d.ts.map +0 -1
  130. package/dist/mutations.js +0 -63
  131. package/dist/mutations.js.map +0 -1
  132. package/dist/network.d.ts +0 -3
  133. package/dist/network.d.ts.map +0 -1
  134. package/dist/network.js +0 -17
  135. package/dist/network.js.map +0 -1
  136. package/dist/react/index.d.ts +0 -169
  137. package/dist/react/index.d.ts.map +0 -1
  138. package/dist/react/index.js +0 -628
  139. package/dist/react/index.js.map +0 -1
  140. package/dist/react-native/index.d.ts +0 -35
  141. package/dist/react-native/index.d.ts.map +0 -1
  142. package/dist/react-native/index.js +0 -49
  143. package/dist/react-native/index.js.map +0 -1
  144. package/dist/runtime-contract.d.ts +0 -13
  145. package/dist/runtime-contract.d.ts.map +0 -1
  146. package/dist/runtime-contract.js +0 -24
  147. package/dist/runtime-contract.js.map +0 -1
  148. package/dist/rust-client.d.ts +0 -106
  149. package/dist/rust-client.d.ts.map +0 -1
  150. package/dist/rust-client.js +0 -938
  151. package/dist/rust-client.js.map +0 -1
  152. package/dist/sentry.d.ts +0 -35
  153. package/dist/sentry.d.ts.map +0 -1
  154. package/dist/sentry.js +0 -155
  155. package/dist/sentry.js.map +0 -1
  156. package/dist/sql-safety.d.ts +0 -3
  157. package/dist/sql-safety.d.ts.map +0 -1
  158. package/dist/sql-safety.js +0 -62
  159. package/dist/sql-safety.js.map +0 -1
  160. package/dist/syncular-runtime-artifacts.json +0 -61
  161. package/dist/tauri/index.d.ts +0 -35
  162. package/dist/tauri/index.d.ts.map +0 -1
  163. package/dist/tauri/index.js +0 -114
  164. package/dist/tauri/index.js.map +0 -1
  165. package/dist/types.d.ts +0 -1096
  166. package/dist/types.d.ts.map +0 -1
  167. package/dist/types.js +0 -2
  168. package/dist/types.js.map +0 -1
  169. package/dist/wasm/.syncular-wasm-profile +0 -1
  170. package/dist/wasm/syncular-runtime-artifact.json +0 -21
  171. package/dist/wasm/syncular.d.ts +0 -207
  172. package/dist/wasm/syncular.js +0 -2341
  173. package/dist/wasm/syncular_bg.wasm +0 -0
  174. package/dist/wasm/syncular_bg.wasm.d.ts +0 -97
  175. package/dist/wasm-bindings/runtime-contract.d.ts +0 -22
  176. package/dist/wasm-bindings/runtime-contract.d.ts.map +0 -1
  177. package/dist/wasm-bindings/runtime-contract.js +0 -112
  178. package/dist/wasm-bindings/runtime-contract.js.map +0 -1
  179. package/dist/wasm-core/.syncular-wasm-profile +0 -1
  180. package/dist/wasm-core/syncular-runtime-artifact.json +0 -17
  181. package/dist/wasm-core/syncular.d.ts +0 -162
  182. package/dist/wasm-core/syncular.js +0 -1847
  183. package/dist/wasm-core/syncular_bg.wasm +0 -0
  184. package/dist/wasm-core/syncular_bg.wasm.d.ts +0 -77
  185. package/dist/wasm-perf/.syncular-wasm-profile +0 -1
  186. package/dist/wasm-perf/syncular-runtime-artifact.json +0 -21
  187. package/dist/wasm-perf/syncular.d.ts +0 -207
  188. package/dist/wasm-perf/syncular.js +0 -2341
  189. package/dist/wasm-perf/syncular_bg.wasm +0 -0
  190. package/dist/wasm-perf/syncular_bg.wasm.d.ts +0 -97
  191. package/dist/wasm-runtime.d.ts +0 -23
  192. package/dist/wasm-runtime.d.ts.map +0 -1
  193. package/dist/wasm-runtime.js +0 -69
  194. package/dist/wasm-runtime.js.map +0 -1
  195. package/dist/worker-client.d.ts +0 -123
  196. package/dist/worker-client.d.ts.map +0 -1
  197. package/dist/worker-client.js +0 -1735
  198. package/dist/worker-client.js.map +0 -1
  199. package/dist/worker-entry.d.ts.map +0 -1
  200. package/dist/worker-entry.js.map +0 -1
  201. package/dist/worker-protocol.d.ts.map +0 -1
  202. package/dist/worker-protocol.js.map +0 -1
  203. package/dist/worker-realtime.d.ts +0 -39
  204. package/dist/worker-realtime.d.ts.map +0 -1
  205. package/dist/worker-realtime.js +0 -677
  206. package/dist/worker-realtime.js.map +0 -1
  207. package/src/auth-leases.ts +0 -251
  208. package/src/blob-limits.ts +0 -98
  209. package/src/bridge-client.ts +0 -512
  210. package/src/client-config.ts +0 -29
  211. package/src/command-history.ts +0 -623
  212. package/src/console-diagnostics.ts +0 -617
  213. package/src/crdt-yjs/index.ts +0 -3
  214. package/src/crdt-yjs/webview-host-facade.ts +0 -477
  215. package/src/crdt-yjs/yjs-document-field-adapter.ts +0 -733
  216. package/src/crdt-yjs/yjs-prosemirror-bridge.ts +0 -272
  217. package/src/diagnostics.ts +0 -116
  218. package/src/generated-bridge.ts +0 -741
  219. package/src/mutations.ts +0 -168
  220. package/src/network.ts +0 -32
  221. package/src/react/index.ts +0 -1036
  222. package/src/react-native/index.ts +0 -152
  223. package/src/runtime-contract.ts +0 -48
  224. package/src/rust-client.ts +0 -1491
  225. package/src/sentry.ts +0 -215
  226. package/src/sql-safety.ts +0 -61
  227. package/src/tauri/index.ts +0 -211
  228. package/src/types.ts +0 -1397
  229. package/src/wasm-bindings/generated-wasm-bindings.d.ts +0 -70
  230. package/src/wasm-bindings/runtime-contract.ts +0 -158
  231. package/src/wasm-runtime.ts +0 -145
  232. package/src/worker-client.ts +0 -2289
  233. package/src/worker-realtime.ts +0 -843
@@ -1,1491 +0,0 @@
1
- import type {
2
- BlobRef,
3
- SyncAuthLeaseIssueRequest,
4
- SyncOperation,
5
- } from '@syncular/core';
6
- import { issueSyncularAuthLease } from './auth-leases';
7
- import { assertSyncularBlobPayloadLimit } from './blob-limits';
8
- import { resolveSyncularClientConfig } from './client-config';
9
- import {
10
- appendSyncularDiagnosticEvent,
11
- appendSyncularSyncTimings,
12
- createSyncularSyncAttempt,
13
- summarizeSyncularDiagnosticSubscriptions,
14
- syncularDiagnosticAttemptFields,
15
- syncularSyncAttemptHeaders,
16
- } from './diagnostics';
17
- import { SyncularClientError, toSyncularClientError } from './errors';
18
- import { createSyncularRuntimeInfo } from './runtime-contract';
19
- import { assertSyncularReadonlySql } from './sql-safety';
20
- import type {
21
- ResolvedSyncularClientConfig,
22
- SyncularApplyYjsEnvelopeToPayloadArgs,
23
- SyncularApplyYjsTextUpdatesArgs,
24
- SyncularApplyYjsTextUpdatesResult,
25
- SyncularAuthHeaders,
26
- SyncularAuthLeaseRecord,
27
- SyncularBlobCacheStats,
28
- SyncularBlobEncryptionConfig,
29
- SyncularBlobLimits,
30
- SyncularBlobStoreOptions,
31
- SyncularBlobUploadQueueProcessOptions,
32
- SyncularBlobUploadQueueStats,
33
- SyncularBootstrapState,
34
- SyncularBootstrapStatus,
35
- SyncularBootstrapSubscriptionPhase,
36
- SyncularBuildYjsTextUpdateArgs,
37
- SyncularBuildYjsTextUpdateResult,
38
- SyncularChangedRow,
39
- SyncularClientConfig,
40
- SyncularConflictResolution,
41
- SyncularConflictSummary,
42
- SyncularConnectionState,
43
- SyncularCrdtDocumentSnapshot,
44
- SyncularCrdtFieldCompactionReceipt,
45
- SyncularCrdtFieldCompactionRequest,
46
- SyncularCrdtFieldDescriptor,
47
- SyncularCrdtFieldMaterialization,
48
- SyncularCrdtFieldRequest,
49
- SyncularCrdtFieldTextRequest,
50
- SyncularCrdtFieldWriteReceipt,
51
- SyncularCrdtFieldYjsUpdateRequest,
52
- SyncularCrdtUpdateLogEntry,
53
- SyncularDiagnosticEvent,
54
- SyncularDiagnosticSink,
55
- SyncularDiagnosticSnapshot,
56
- SyncularEncryptedCrdtConfig,
57
- SyncularEncryptionHelperMethod,
58
- SyncularFieldEncryptionConfig,
59
- SyncularLifecycleState,
60
- SyncularLiveQueryDependencyHint,
61
- SyncularLiveQueryDiagnostics,
62
- SyncularLiveQueryEvent,
63
- SyncularLiveQuerySnapshot,
64
- SyncularLocalHealthRepairReport,
65
- SyncularLocalHealthRepairRequest,
66
- SyncularLocalHealthReport,
67
- SyncularLocalSupportBundle,
68
- SyncularLocalSupportBundleImportReport,
69
- SyncularLocalSyncResetReport,
70
- SyncularLocalSyncResetRequest,
71
- SyncularPullOptions,
72
- SyncularRowsChangedEvent,
73
- SyncularRowsChangedSink,
74
- SyncularRuntimeArtifact,
75
- SyncularRuntimeInfo,
76
- SyncularSchemaState,
77
- SyncularSqlResult,
78
- SyncularStorageCompactionOptions,
79
- SyncularStorageCompactionReport,
80
- SyncularSubscriptionSpec,
81
- SyncularSyncAttempt,
82
- SyncularSyncRequestOptions,
83
- SyncularSyncResult,
84
- SyncularSyncTimings,
85
- SyncularTransportStats,
86
- } from './types';
87
- import {
88
- getSyncularWasmGlueUrl,
89
- getSyncularWasmUrl,
90
- loadSyncularWasmGlue,
91
- prepareSyncularWasmModuleInput,
92
- readSyncularRustRuntimeInfo,
93
- type SyncularRustOwnedSqliteClient,
94
- type SyncularWasmGlue,
95
- } from './wasm-runtime';
96
-
97
- interface CreateSyncularRustClientOptions {
98
- module?: SyncularWasmGlue | Promise<SyncularWasmGlue>;
99
- wasmGlueUrl?: string | URL;
100
- wasmUrl?: string | URL | Request;
101
- runtime?: SyncularRuntimeArtifact;
102
- config: SyncularClientConfig;
103
- blobLimits?: SyncularBlobLimits;
104
- }
105
-
106
- type RawSyncResult = {
107
- changed_tables?: string[];
108
- changed_rows?: SyncularChangedRow[];
109
- changed_rows_truncated?: boolean;
110
- subscriptions?: Array<{
111
- id: string;
112
- table: string;
113
- status: string;
114
- scopes: Record<string, string | string[]>;
115
- next_cursor: number;
116
- bootstrap_phase?: number;
117
- bootstrap_state?: RawBootstrapState | null;
118
- ready?: boolean;
119
- phase?: SyncularBootstrapSubscriptionPhase;
120
- progress_percent?: number;
121
- snapshot_rows?: unknown[];
122
- commits?: unknown[];
123
- }>;
124
- bootstrap?: RawBootstrapStatus;
125
- pushed_commits?: number;
126
- timings?: {
127
- total_ms?: number;
128
- push_ms?: number;
129
- pull_ms?: number;
130
- pull_request_ms?: number;
131
- sync_pack_decode_ms?: number;
132
- pull_transform_ms?: number;
133
- integrity_verify_ms?: number;
134
- snapshot_fetch_ms?: number;
135
- pull_apply_ms?: number;
136
- scope_clear_ms?: number;
137
- snapshot_row_apply_ms?: number;
138
- snapshot_artifact_apply_ms?: number;
139
- snapshot_artifact_checkpoint_ms?: number;
140
- snapshot_artifact_checkpoint_count?: number;
141
- snapshot_chunk_apply_ms?: number;
142
- snapshot_chunk_materialize_ms?: number;
143
- snapshot_chunk_reset_ms?: number;
144
- snapshot_chunk_bind_ms?: number;
145
- snapshot_chunk_step_ms?: number;
146
- commit_apply_ms?: number;
147
- subscription_state_ms?: number;
148
- notify_ms?: number;
149
- };
150
- };
151
-
152
- type RawBootstrapState = {
153
- asOfCommitSeq?: number;
154
- tables?: string[];
155
- tableIndex?: number;
156
- rowCursor?: string | null;
157
- };
158
-
159
- type RawBootstrapStatus = {
160
- channel_phase?: string;
161
- progress_percent?: number;
162
- is_bootstrapping?: boolean;
163
- critical_ready?: boolean;
164
- interactive_ready?: boolean;
165
- complete?: boolean;
166
- active_phase?: number | null;
167
- expected_subscription_ids?: string[];
168
- ready_subscription_ids?: string[];
169
- pending_subscription_ids?: string[];
170
- subscriptions?: Array<{
171
- id: string;
172
- table: string;
173
- expected?: boolean;
174
- ready?: boolean;
175
- status?: string | null;
176
- phase?: SyncularBootstrapSubscriptionPhase;
177
- progress_percent?: number;
178
- cursor?: number | null;
179
- bootstrap_state?: RawBootstrapState | null;
180
- bootstrap_phase?: number;
181
- }>;
182
- phases?: Array<{
183
- phase?: number;
184
- expected_subscription_ids?: string[];
185
- ready_subscription_ids?: string[];
186
- pending_subscription_ids?: string[];
187
- is_ready?: boolean;
188
- progress_percent?: number;
189
- }>;
190
- };
191
-
192
- type RawConflictSummary = {
193
- id: string;
194
- client_commit_id: string;
195
- op_index: number;
196
- result_status: string;
197
- message: string;
198
- code: string | null;
199
- server_version: number | null;
200
- resolved_at: number | null;
201
- resolution: string | null;
202
- };
203
-
204
- type SyncularBootstrapSubscriptionStatusEntry = {
205
- id: string;
206
- table: string;
207
- status: string | null;
208
- ready: boolean;
209
- phase: SyncularBootstrapSubscriptionPhase;
210
- progressPercent: number;
211
- cursor: number | null;
212
- bootstrapState: SyncularBootstrapState | null;
213
- bootstrapPhase: number;
214
- };
215
-
216
- export async function openSyncularRustClient(
217
- options: CreateSyncularRustClientOptions
218
- ): Promise<SyncularRustClient> {
219
- const wasmGlueUrl =
220
- options.runtime?.wasmGlueUrl ??
221
- options.wasmGlueUrl ??
222
- getSyncularWasmGlueUrl();
223
- const mod = await (options.module ??
224
- (options.runtime?.wasmGlueUrl || options.wasmGlueUrl
225
- ? loadSyncularWasmGlueFromUrl(wasmGlueUrl)
226
- : loadSyncularWasmGlue()));
227
- const wasmUrl =
228
- options.runtime?.wasmUrl ?? options.wasmUrl ?? getSyncularWasmUrl();
229
- const config = resolveSyncularClientConfig(options.config);
230
- await mod.default({
231
- module_or_path: await prepareSyncularWasmModuleInput(wasmUrl),
232
- });
233
- const rustRuntimeInfo = readSyncularRustRuntimeInfo(mod);
234
- return new SyncularRustClient(
235
- await mod.openSyncularRustOwnedSqliteClient(config),
236
- createSyncularRuntimeInfo({
237
- storage: config.storage,
238
- wasmGlueUrl,
239
- wasmUrl,
240
- rust: rustRuntimeInfo,
241
- }),
242
- config,
243
- config.pull,
244
- options.blobLimits
245
- );
246
- }
247
-
248
- function loadSyncularWasmGlueFromUrl(
249
- wasmGlueUrl: string | URL
250
- ): Promise<SyncularWasmGlue> {
251
- const href = wasmGlueUrl instanceof URL ? wasmGlueUrl.href : wasmGlueUrl;
252
- return import(/* @vite-ignore */ href) as Promise<SyncularWasmGlue>;
253
- }
254
-
255
- export class SyncularRustClient {
256
- #rowsChangedListeners = new Set<SyncularRowsChangedSink>();
257
- #diagnosticListeners = new Set<SyncularDiagnosticSink>();
258
- #recentDiagnostics: SyncularDiagnosticEvent[] = [];
259
- #recentSyncTimings: SyncularSyncTimings[] = [];
260
- #subscriptions: SyncularSubscriptionSpec[] = [];
261
- #authHeaders: SyncularAuthHeaders = {};
262
- #bootstrapById = new Map<string, SyncularBootstrapSubscriptionStatusEntry>();
263
- #closed = false;
264
-
265
- constructor(
266
- private readonly raw: SyncularRustOwnedSqliteClient,
267
- private readonly runtime: SyncularRuntimeInfo,
268
- private readonly config: ResolvedSyncularClientConfig,
269
- private readonly pullOptions: SyncularPullOptions | undefined,
270
- private readonly blobLimits: SyncularBlobLimits | undefined
271
- ) {}
272
-
273
- #assertRuntimeFeature(feature: 'crdt-yjs' | 'e2ee', apiName: string): void {
274
- if (this.runtime.rust?.features.includes(feature)) return;
275
- throw new Error(
276
- `Syncular runtime feature "${feature}" is required for ${apiName}. ` +
277
- 'Select the full browser WASM runtime artifact.'
278
- );
279
- }
280
-
281
- setSubscriptions(subscriptions: readonly SyncularSubscriptionSpec[]): void {
282
- this.#subscriptions = [...subscriptions];
283
- this.#bootstrapById.clear();
284
- this.raw.setSubscriptionsJson(JSON.stringify(subscriptions));
285
- this.#emitDiagnostic({
286
- at: Date.now(),
287
- level: 'info',
288
- source: 'client',
289
- code: 'client.subscriptions.updated',
290
- message: 'Syncular subscriptions updated',
291
- details: { subscriptionCount: subscriptions.length },
292
- });
293
- }
294
-
295
- async forceSubscriptionsBootstrap(
296
- subscriptionIds: readonly string[] = []
297
- ): Promise<number> {
298
- const count = parseJson<number>(
299
- await this.raw.forceSubscriptionsBootstrapJson(
300
- JSON.stringify(subscriptionIds)
301
- )
302
- );
303
- for (const id of subscriptionIds.length > 0
304
- ? subscriptionIds
305
- : this.#subscriptions.map((subscription) => subscription.id)) {
306
- this.#bootstrapById.delete(id);
307
- }
308
- return count;
309
- }
310
-
311
- setAuthHeaders(headers: SyncularAuthHeaders): void {
312
- this.#authHeaders = { ...headers };
313
- this.raw.setAuthHeadersJson(JSON.stringify(this.#authHeaders));
314
- }
315
-
316
- async issueAuthLease(
317
- request: SyncAuthLeaseIssueRequest
318
- ): Promise<SyncularAuthLeaseRecord> {
319
- const lease = await issueSyncularAuthLease({
320
- baseUrl: this.config.baseUrl,
321
- headers: this.#authHeaders,
322
- request,
323
- appSchema: this.config.appSchema,
324
- });
325
- await this.upsertAuthLease(lease);
326
- this.#emitDiagnostic({
327
- at: Date.now(),
328
- level: 'info',
329
- source: 'auth',
330
- code: 'auth_lease.issued',
331
- message: 'Syncular auth lease issued and stored',
332
- details: {
333
- leaseId: lease.leaseId,
334
- expiresAtMs: lease.expiresAtMs,
335
- schemaVersion: lease.schemaVersion,
336
- },
337
- });
338
- return lease;
339
- }
340
-
341
- async upsertAuthLease(lease: SyncularAuthLeaseRecord): Promise<void> {
342
- this.raw.upsertAuthLeaseJson(JSON.stringify(lease));
343
- }
344
-
345
- async authLease(leaseId: string): Promise<SyncularAuthLeaseRecord | null> {
346
- return parseJson<SyncularAuthLeaseRecord | null>(
347
- this.raw.authLeaseJson(leaseId)
348
- );
349
- }
350
-
351
- async activeAuthLeases(
352
- actorId?: string | null,
353
- nowMs = Date.now()
354
- ): Promise<SyncularAuthLeaseRecord[]> {
355
- return parseJson<SyncularAuthLeaseRecord[]>(
356
- this.raw.activeAuthLeasesJson(actorId ?? null, BigInt(Math.trunc(nowMs)))
357
- );
358
- }
359
-
360
- setFieldEncryption(config: SyncularFieldEncryptionConfig | null): void {
361
- if (config == null && !this.runtime.rust?.features.includes('e2ee')) return;
362
- this.#assertRuntimeFeature('e2ee', 'setFieldEncryption');
363
- this.raw.setFieldEncryptionJson(
364
- config == null
365
- ? 'null'
366
- : JSON.stringify(normalizeFieldEncryptionConfig(config))
367
- );
368
- }
369
-
370
- setEncryptedCrdt(config: SyncularEncryptedCrdtConfig | null): void {
371
- if (config == null && !this.runtime.rust?.features.includes('e2ee')) return;
372
- this.#assertRuntimeFeature('e2ee', 'setEncryptedCrdt');
373
- this.raw.setEncryptedCrdtJson(
374
- config == null
375
- ? 'null'
376
- : JSON.stringify(normalizeEncryptedCrdtConfig(config))
377
- );
378
- }
379
-
380
- setBlobEncryption(config: SyncularBlobEncryptionConfig | null): void {
381
- if (config == null && !this.runtime.rust?.features.includes('e2ee')) return;
382
- this.#assertRuntimeFeature('e2ee', 'setBlobEncryption');
383
- this.raw.setBlobEncryptionJson(
384
- config == null
385
- ? 'null'
386
- : JSON.stringify(normalizeBlobEncryptionConfig(config))
387
- );
388
- }
389
-
390
- setAbortSignal(signal?: AbortSignal | null): void {
391
- this.raw.setAbortSignal(signal ?? null);
392
- }
393
-
394
- async applyMutation(
395
- operation: SyncOperation,
396
- localRow?: unknown
397
- ): Promise<string> {
398
- const commitId = await this.raw.applyMutationJson(
399
- JSON.stringify(operation),
400
- localRow == null ? null : JSON.stringify(localRow)
401
- );
402
- this.#drainAndEmitRowsChanged();
403
- return commitId;
404
- }
405
-
406
- async applyLeasedMutation(
407
- operation: SyncOperation,
408
- localRow?: unknown
409
- ): Promise<string> {
410
- const commitId = await this.raw.applyLeasedMutationJson(
411
- JSON.stringify(operation),
412
- localRow == null ? null : JSON.stringify(localRow)
413
- );
414
- this.#drainAndEmitRowsChanged();
415
- return commitId;
416
- }
417
-
418
- async applyMutationsBatch(
419
- operations: Array<{ operation: SyncOperation; localRow?: unknown | null }>
420
- ): Promise<string[]> {
421
- const commitIds = parseJson<string[]>(
422
- await this.raw.applyMutationsBatchJson(JSON.stringify(operations))
423
- );
424
- this.#drainAndEmitRowsChanged();
425
- return commitIds;
426
- }
427
-
428
- async applyMutationsCommit(
429
- operations: Array<{ operation: SyncOperation; localRow?: unknown | null }>
430
- ): Promise<string> {
431
- const commitId = parseJson<string>(
432
- await this.raw.applyMutationsCommitJson(JSON.stringify(operations))
433
- );
434
- this.#drainAndEmitRowsChanged();
435
- return commitId;
436
- }
437
-
438
- async applyLeasedMutationsCommit(
439
- operations: Array<{ operation: SyncOperation; localRow?: unknown | null }>
440
- ): Promise<string> {
441
- const commitId = parseJson<string>(
442
- await this.raw.applyLeasedMutationsCommitJson(JSON.stringify(operations))
443
- );
444
- this.#drainAndEmitRowsChanged();
445
- return commitId;
446
- }
447
-
448
- async syncPull(
449
- options: SyncularSyncRequestOptions = {}
450
- ): Promise<SyncularSyncResult> {
451
- const result = await this.#runTracedSync(
452
- 'syncPull',
453
- options.syncAttempt,
454
- async () =>
455
- this.#decorateSyncResult(parseSyncResult(await this.raw.syncPullJson()))
456
- );
457
- this.#captureSyncTimings(result);
458
- this.#emitRowsChanged('remotePull', result);
459
- return result;
460
- }
461
-
462
- async applyRealtimeSyncPack(bytes: Uint8Array): Promise<SyncularSyncResult> {
463
- const result = this.#decorateSyncResult(
464
- parseSyncResult(await this.raw.applyRealtimeSyncPackBytes(bytes))
465
- );
466
- this.#captureSyncTimings(result);
467
- this.#emitRowsChanged('remotePull', result);
468
- return result;
469
- }
470
-
471
- async syncPush(
472
- options: SyncularSyncRequestOptions = {}
473
- ): Promise<SyncularSyncResult> {
474
- try {
475
- const result = await this.#runTracedSync(
476
- 'syncPush',
477
- options.syncAttempt,
478
- async () =>
479
- this.#decorateSyncResult(
480
- parseSyncResult(await this.raw.syncPushJson())
481
- )
482
- );
483
- this.#captureSyncTimings(result);
484
- this.#emitRowsChanged('localWrite', result);
485
- return result;
486
- } catch (error) {
487
- this.raw.recoverSyncPushErrorJson(String(error));
488
- throw error;
489
- }
490
- }
491
-
492
- async syncOnce(
493
- options: SyncularSyncRequestOptions = {}
494
- ): Promise<SyncularSyncResult> {
495
- try {
496
- const result = await this.#runTracedSync(
497
- 'syncOnce',
498
- options.syncAttempt,
499
- async () =>
500
- this.#decorateSyncResult(
501
- parseSyncResult(await this.raw.syncOnceJson())
502
- )
503
- );
504
- this.#captureSyncTimings(result);
505
- this.#emitRowsChanged('remotePull', result);
506
- return result;
507
- } catch (error) {
508
- this.raw.recoverSyncPushErrorJson(String(error));
509
- throw error;
510
- }
511
- }
512
-
513
- resumeFromBackground(
514
- options: SyncularSyncRequestOptions = {}
515
- ): Promise<SyncularSyncResult> {
516
- return this.syncOnce(options);
517
- }
518
-
519
- transportStats(): SyncularTransportStats {
520
- return parseJson(this.raw.transportStatsJson());
521
- }
522
-
523
- resetTransportStats(): void {
524
- this.raw.resetTransportStats();
525
- }
526
-
527
- async conflictSummaries(): Promise<SyncularConflictSummary[]> {
528
- return parseConflictSummaries(await this.raw.conflictSummariesJson());
529
- }
530
-
531
- retryConflictKeepLocal(id: string): Promise<string> {
532
- return this.raw.retryConflictKeepLocal(id);
533
- }
534
-
535
- resolveConflict(
536
- id: string,
537
- resolution: SyncularConflictResolution
538
- ): Promise<void> {
539
- return this.raw.resolveConflict(id, resolution);
540
- }
541
-
542
- executeSql<Row extends Record<string, unknown> = Record<string, unknown>>(
543
- sql: string,
544
- params: readonly unknown[] = []
545
- ): SyncularSqlResult<Row> {
546
- assertSyncularReadonlySql(sql);
547
- return this.raw.executeSqlValue(sql, params) as SyncularSqlResult<Row>;
548
- }
549
-
550
- executeUnsafeSql<
551
- Row extends Record<string, unknown> = Record<string, unknown>,
552
- >(sql: string, params: readonly unknown[] = []): SyncularSqlResult<Row> {
553
- return this.raw.executeUnsafeSqlValue(
554
- sql,
555
- params
556
- ) as SyncularSqlResult<Row>;
557
- }
558
-
559
- subscribeQuery<Row extends Record<string, unknown> = Record<string, unknown>>(
560
- sql: string,
561
- params: readonly unknown[],
562
- tables: readonly string[],
563
- hints: readonly SyncularLiveQueryDependencyHint[] = []
564
- ): SyncularLiveQuerySnapshot<Row> {
565
- return parseJson(
566
- this.raw.subscribeQueryJson(
567
- sql,
568
- stringifyParams(params),
569
- JSON.stringify(tables),
570
- JSON.stringify(hints)
571
- )
572
- );
573
- }
574
-
575
- unsubscribeQuery(id: string): void {
576
- this.raw.unsubscribeQuery(id);
577
- }
578
-
579
- drainLiveQueryEvents<
580
- Row extends Record<string, unknown> = Record<string, unknown>,
581
- >(): Array<SyncularLiveQueryEvent<Row>> {
582
- return parseJson(this.raw.drainLiveQueryEventsJson());
583
- }
584
-
585
- liveQueryDiagnostics(): SyncularLiveQueryDiagnostics {
586
- return parseJson(this.raw.liveQueryDiagnosticsJson());
587
- }
588
-
589
- addRowsChangedListener(listener: SyncularRowsChangedSink): () => void {
590
- this.#rowsChangedListeners.add(listener);
591
- return () => {
592
- this.#rowsChangedListeners.delete(listener);
593
- };
594
- }
595
-
596
- async listTable<
597
- Row extends Record<string, unknown> = Record<string, unknown>,
598
- >(table: string): Promise<Row[]> {
599
- return parseJson(await this.raw.listTableJson(table));
600
- }
601
-
602
- async storeBlob(
603
- data: Uint8Array,
604
- options: SyncularBlobStoreOptions = {}
605
- ): Promise<BlobRef> {
606
- assertSyncularBlobPayloadLimit({
607
- operation: 'store',
608
- size: data.byteLength,
609
- limits: this.blobLimits,
610
- options,
611
- diagnostics: (event) => this.#emitDiagnostic(event),
612
- });
613
- return parseJson(
614
- await this.raw.storeBlobJson(data, JSON.stringify(options))
615
- );
616
- }
617
-
618
- async retrieveBlob(ref: BlobRef): Promise<Uint8Array> {
619
- assertSyncularBlobPayloadLimit({
620
- operation: 'retrieve',
621
- size: ref.size,
622
- limits: this.blobLimits,
623
- refHash: ref.hash,
624
- diagnostics: (event) => this.#emitDiagnostic(event),
625
- });
626
- const wasLocal = this.#hasDiagnosticListeners()
627
- ? this.raw.isBlobLocal(ref.hash)
628
- : undefined;
629
- try {
630
- const bytes = await this.raw.retrieveBlob(JSON.stringify(ref));
631
- if (wasLocal !== undefined) {
632
- this.#emitDiagnostic({
633
- at: Date.now(),
634
- level: 'info',
635
- source: 'blob',
636
- code: wasLocal ? 'blob.cache_hit' : 'blob.cache_miss',
637
- message: wasLocal
638
- ? 'Syncular blob served from local cache'
639
- : 'Syncular blob fetched after local cache miss',
640
- details: {
641
- hash: ref.hash,
642
- size: ref.size,
643
- mimeType: ref.mimeType,
644
- encrypted: ref.encrypted === true,
645
- ...(ref.keyId ? { keyId: ref.keyId } : {}),
646
- },
647
- });
648
- }
649
- return bytes;
650
- } catch (error) {
651
- if (this.#hasDiagnosticListeners()) {
652
- this.#emitDiagnostic({
653
- at: Date.now(),
654
- level: 'warn',
655
- source: 'blob',
656
- code: 'blob.download_failed',
657
- message: `Syncular blob download failed: ${String(error)}`,
658
- details: {
659
- hash: ref.hash,
660
- size: ref.size,
661
- mimeType: ref.mimeType,
662
- encrypted: ref.encrypted === true,
663
- ...(ref.keyId ? { keyId: ref.keyId } : {}),
664
- },
665
- });
666
- }
667
- throw error;
668
- }
669
- }
670
-
671
- isBlobLocal(hash: string): boolean {
672
- const local = this.raw.isBlobLocal(hash);
673
- if (this.#hasDiagnosticListeners()) {
674
- this.#emitDiagnostic({
675
- at: Date.now(),
676
- level: 'debug',
677
- source: 'blob',
678
- code: local ? 'blob.cache_hit' : 'blob.cache_miss',
679
- message: local
680
- ? 'Syncular blob is available in local cache'
681
- : 'Syncular blob is not available in local cache',
682
- details: { hash },
683
- });
684
- }
685
- return local;
686
- }
687
-
688
- async processBlobUploadQueue(
689
- options?: SyncularBlobUploadQueueProcessOptions
690
- ): Promise<{
691
- uploaded: number;
692
- failed: number;
693
- }> {
694
- const result = parseJson<{ uploaded: number; failed: number }>(
695
- await this.raw.processBlobUploadQueueJson(options?.retryNow === true)
696
- );
697
- if (this.#hasDiagnosticListeners()) {
698
- this.#emitDiagnostic({
699
- at: Date.now(),
700
- level: result.failed > 0 ? 'warn' : 'info',
701
- source: 'blob',
702
- code: 'blob.upload_queue_processed',
703
- message: 'Syncular blob upload queue processed',
704
- details: result,
705
- });
706
- }
707
- return result;
708
- }
709
-
710
- blobUploadQueueStats(): SyncularBlobUploadQueueStats {
711
- return parseJson(this.raw.blobUploadQueueStatsJson());
712
- }
713
-
714
- blobCacheStats(): SyncularBlobCacheStats {
715
- return parseJson(this.raw.blobCacheStatsJson());
716
- }
717
-
718
- pruneBlobCache(maxBytes = 0): number {
719
- const prunedBytes = Number(this.raw.pruneBlobCache(BigInt(maxBytes)));
720
- if (this.#hasDiagnosticListeners()) {
721
- this.#emitDiagnostic({
722
- at: Date.now(),
723
- level: 'info',
724
- source: 'blob',
725
- code: 'blob.cache_pruned',
726
- message: 'Syncular blob cache pruned',
727
- details: { prunedBytes, maxBytes },
728
- });
729
- }
730
- return prunedBytes;
731
- }
732
-
733
- clearBlobCache(): void {
734
- this.raw.clearBlobCache();
735
- if (this.#hasDiagnosticListeners()) {
736
- this.#emitDiagnostic({
737
- at: Date.now(),
738
- level: 'info',
739
- source: 'blob',
740
- code: 'blob.cache_cleared',
741
- message: 'Syncular blob cache cleared',
742
- });
743
- }
744
- }
745
-
746
- compactStorage(
747
- options: SyncularStorageCompactionOptions = {}
748
- ): SyncularStorageCompactionReport {
749
- return parseJson(this.raw.compactStorageJson(JSON.stringify(options)));
750
- }
751
-
752
- generatedSchemaState(): SyncularSchemaState {
753
- return parseJson(this.raw.generatedSchemaStateJson());
754
- }
755
-
756
- async localHealthCheck(): Promise<SyncularLocalHealthReport> {
757
- return parseJson(await this.raw.localHealthCheckJson());
758
- }
759
-
760
- async exportLocalSupportBundle(): Promise<SyncularLocalSupportBundle> {
761
- return parseJson(await this.raw.exportLocalSupportBundleJson());
762
- }
763
-
764
- async importLocalSupportBundle(
765
- bundle: SyncularLocalSupportBundle | string
766
- ): Promise<SyncularLocalSupportBundleImportReport> {
767
- const bundleJson =
768
- typeof bundle === 'string' ? bundle : JSON.stringify(bundle);
769
- return parseJson(await this.raw.importLocalSupportBundleJson(bundleJson));
770
- }
771
-
772
- async repairLocalHealth(
773
- request: SyncularLocalHealthRepairRequest
774
- ): Promise<SyncularLocalHealthRepairReport> {
775
- const normalized = {
776
- action: request.action,
777
- subscriptionIds: [...(request.subscriptionIds ?? [])],
778
- tables: [...(request.tables ?? [])],
779
- };
780
- return parseJson(
781
- await this.raw.repairLocalHealthJson(JSON.stringify(normalized))
782
- );
783
- }
784
-
785
- async resetLocalSyncState(
786
- request: SyncularLocalSyncResetRequest = {}
787
- ): Promise<SyncularLocalSyncResetReport> {
788
- const normalized = {
789
- subscriptionIds: [...(request.subscriptionIds ?? [])],
790
- clearSyncedRows: request.clearSyncedRows === true,
791
- };
792
- const result = parseJson<SyncularLocalSyncResetReport>(
793
- await this.raw.resetLocalSyncStateJson(JSON.stringify(normalized))
794
- );
795
- for (const id of normalized.subscriptionIds.length > 0
796
- ? normalized.subscriptionIds
797
- : this.#subscriptions.map((subscription) => subscription.id)) {
798
- this.#bootstrapById.delete(id);
799
- }
800
- return result;
801
- }
802
-
803
- buildYjsTextUpdate(
804
- args: SyncularBuildYjsTextUpdateArgs
805
- ): SyncularBuildYjsTextUpdateResult {
806
- this.#assertRuntimeFeature('crdt-yjs', 'buildYjsTextUpdate');
807
- return parseJson(this.raw.buildYjsTextUpdateJson(JSON.stringify(args)));
808
- }
809
-
810
- applyYjsTextUpdates(
811
- args: SyncularApplyYjsTextUpdatesArgs
812
- ): SyncularApplyYjsTextUpdatesResult {
813
- this.#assertRuntimeFeature('crdt-yjs', 'applyYjsTextUpdates');
814
- return parseJson(this.raw.applyYjsTextUpdatesJson(JSON.stringify(args)));
815
- }
816
-
817
- applyYjsEnvelopeToPayload(
818
- args: SyncularApplyYjsEnvelopeToPayloadArgs
819
- ): Record<string, unknown> {
820
- this.#assertRuntimeFeature('crdt-yjs', 'applyYjsEnvelopeToPayload');
821
- const result = parseJson<{ payload: Record<string, unknown> }>(
822
- this.raw.applyYjsEnvelopeToPayloadJson(JSON.stringify(args))
823
- );
824
- return result.payload;
825
- }
826
-
827
- async openCrdtField(
828
- request: SyncularCrdtFieldRequest
829
- ): Promise<SyncularCrdtFieldDescriptor> {
830
- this.#assertRuntimeFeature('crdt-yjs', 'openCrdtField');
831
- return parseJson(this.raw.openCrdtFieldJson(JSON.stringify(request)));
832
- }
833
-
834
- async applyCrdtFieldText(
835
- request: SyncularCrdtFieldTextRequest
836
- ): Promise<SyncularCrdtFieldWriteReceipt> {
837
- this.#assertRuntimeFeature('crdt-yjs', 'applyCrdtFieldText');
838
- const receipt = parseJson<SyncularCrdtFieldWriteReceipt>(
839
- this.raw.applyCrdtFieldTextJson(JSON.stringify(request))
840
- );
841
- this.#drainAndEmitRowsChanged();
842
- return receipt;
843
- }
844
-
845
- async applyCrdtFieldYjsUpdate(
846
- request: SyncularCrdtFieldYjsUpdateRequest
847
- ): Promise<SyncularCrdtFieldWriteReceipt> {
848
- this.#assertRuntimeFeature('crdt-yjs', 'applyCrdtFieldYjsUpdate');
849
- const receipt = parseJson<SyncularCrdtFieldWriteReceipt>(
850
- this.raw.applyCrdtFieldYjsUpdateJson(JSON.stringify(request))
851
- );
852
- this.#drainAndEmitRowsChanged();
853
- return receipt;
854
- }
855
-
856
- async materializeCrdtField(
857
- request: SyncularCrdtFieldRequest
858
- ): Promise<SyncularCrdtFieldMaterialization> {
859
- this.#assertRuntimeFeature('crdt-yjs', 'materializeCrdtField');
860
- return parseJson(
861
- this.raw.materializeCrdtFieldJson(JSON.stringify(request))
862
- );
863
- }
864
-
865
- async crdtDocumentSnapshot(
866
- request: SyncularCrdtFieldRequest
867
- ): Promise<SyncularCrdtDocumentSnapshot> {
868
- this.#assertRuntimeFeature('crdt-yjs', 'crdtDocumentSnapshot');
869
- return parseJson(
870
- this.raw.crdtDocumentSnapshotJson(JSON.stringify(request))
871
- );
872
- }
873
-
874
- async crdtUpdateLog(
875
- request: SyncularCrdtFieldRequest & { limit?: number }
876
- ): Promise<SyncularCrdtUpdateLogEntry[]> {
877
- this.#assertRuntimeFeature('crdt-yjs', 'crdtUpdateLog');
878
- return parseJson(this.raw.crdtUpdateLogJson(JSON.stringify(request)));
879
- }
880
-
881
- async snapshotCrdtFieldStateVector(
882
- request: SyncularCrdtFieldRequest
883
- ): Promise<{ stateVectorBase64: string }> {
884
- this.#assertRuntimeFeature('crdt-yjs', 'snapshotCrdtFieldStateVector');
885
- return parseJson(
886
- this.raw.snapshotCrdtFieldStateVectorJson(JSON.stringify(request))
887
- );
888
- }
889
-
890
- async compactCrdtField(
891
- request: SyncularCrdtFieldCompactionRequest
892
- ): Promise<SyncularCrdtFieldCompactionReceipt> {
893
- this.#assertRuntimeFeature('crdt-yjs', 'compactCrdtField');
894
- const receipt = parseJson<SyncularCrdtFieldCompactionReceipt>(
895
- this.raw.compactCrdtFieldJson(JSON.stringify(request))
896
- );
897
- this.#drainAndEmitRowsChanged();
898
- return receipt;
899
- }
900
-
901
- encryptionHelper<T = unknown>(
902
- method: SyncularEncryptionHelperMethod,
903
- args: unknown = {}
904
- ): T {
905
- this.#assertRuntimeFeature('e2ee', 'encryptionHelper');
906
- return parseJson(
907
- this.raw.encryptionHelperJson(method, JSON.stringify(args))
908
- );
909
- }
910
-
911
- async runtimeInfo(): Promise<SyncularRuntimeInfo> {
912
- return this.runtime;
913
- }
914
-
915
- connectionState(): SyncularConnectionState {
916
- const lastDiagnostic =
917
- this.#recentDiagnostics[this.#recentDiagnostics.length - 1];
918
- return {
919
- closed: this.#closed,
920
- pendingRequests: 0,
921
- realtime: 'disconnected',
922
- ...(lastDiagnostic ? { lastDiagnostic } : {}),
923
- };
924
- }
925
-
926
- lifecycleState(): SyncularLifecycleState {
927
- const bootstrap = buildBootstrapStatus(
928
- this.#subscriptions,
929
- this.#bootstrapById,
930
- this.pullOptions
931
- );
932
- const lastDiagnostic =
933
- this.#recentDiagnostics[this.#recentDiagnostics.length - 1];
934
- return {
935
- phase: this.#closed
936
- ? 'closed'
937
- : bootstrap.complete
938
- ? 'complete'
939
- : 'offline',
940
- realtime: 'disconnected',
941
- online: false,
942
- requiresAction: false,
943
- pendingRequests: 0,
944
- bootstrap: {
945
- complete: bootstrap.complete,
946
- criticalReady: bootstrap.criticalReady,
947
- interactiveReady: bootstrap.interactiveReady,
948
- isBootstrapping: bootstrap.isBootstrapping,
949
- progressPercent: bootstrap.progressPercent,
950
- },
951
- ...(lastDiagnostic ? { lastDiagnostic } : {}),
952
- };
953
- }
954
-
955
- async diagnosticSnapshot(): Promise<SyncularDiagnosticSnapshot> {
956
- const bootstrap = buildBootstrapStatus(
957
- this.#subscriptions,
958
- this.#bootstrapById,
959
- this.pullOptions
960
- );
961
- return {
962
- generatedAt: Date.now(),
963
- runtime: this.runtime,
964
- connection: this.connectionState(),
965
- subscriptions: summarizeSyncularDiagnosticSubscriptions(
966
- this.#subscriptions,
967
- bootstrap
968
- ),
969
- recentDiagnostics: [...this.#recentDiagnostics],
970
- recentSyncTimings: [...this.#recentSyncTimings],
971
- bootstrap,
972
- transportStats: this.transportStats(),
973
- };
974
- }
975
-
976
- addDiagnosticListener(listener: SyncularDiagnosticSink): () => void {
977
- this.#diagnosticListeners.add(listener);
978
- return () => {
979
- this.#diagnosticListeners.delete(listener);
980
- };
981
- }
982
-
983
- #decorateSyncResult(result: SyncularSyncResult): SyncularSyncResult {
984
- for (const subscription of result.subscriptions) {
985
- this.#bootstrapById.set(subscription.id, {
986
- id: subscription.id,
987
- table: subscription.table,
988
- status: subscription.status,
989
- ready: subscription.ready,
990
- phase: subscription.phase,
991
- progressPercent: subscription.progressPercent,
992
- cursor: subscription.nextCursor,
993
- bootstrapState: subscription.bootstrapState,
994
- bootstrapPhase: subscription.bootstrapPhase,
995
- });
996
- }
997
- return {
998
- ...result,
999
- bootstrap: buildBootstrapStatus(
1000
- this.#subscriptions,
1001
- this.#bootstrapById,
1002
- this.pullOptions
1003
- ),
1004
- };
1005
- }
1006
-
1007
- #emitRowsChanged(
1008
- source: 'localWrite' | 'remotePull',
1009
- result: SyncularSyncResult
1010
- ): void {
1011
- if (
1012
- this.#rowsChangedListeners.size === 0 ||
1013
- (result.changedTables.length === 0 && result.changedRows.length === 0)
1014
- ) {
1015
- return;
1016
- }
1017
- for (const listener of this.#rowsChangedListeners) {
1018
- try {
1019
- listener({
1020
- source,
1021
- changedTables: result.changedTables,
1022
- changedRows: result.changedRows,
1023
- changedRowsTruncated: result.changedRowsTruncated,
1024
- });
1025
- } catch {
1026
- // Row-change listeners must never break client control flow.
1027
- }
1028
- }
1029
- }
1030
-
1031
- #captureSyncTimings(result: SyncularSyncResult): void {
1032
- appendSyncularSyncTimings(this.#recentSyncTimings, result.timings);
1033
- }
1034
-
1035
- async #runTracedSync<T>(
1036
- requestType: 'syncPull' | 'syncPush' | 'syncOnce',
1037
- providedAttempt: SyncularSyncAttempt | undefined,
1038
- run: () => Promise<T>
1039
- ): Promise<T> {
1040
- const syncAttempt = providedAttempt ?? createSyncularSyncAttempt();
1041
- const startedAt = Date.now();
1042
- this.#emitDiagnostic({
1043
- at: startedAt,
1044
- level: 'debug',
1045
- source: 'sync',
1046
- code: `sync.${requestType}.started`,
1047
- message: `Syncular ${requestType} started`,
1048
- ...syncularDiagnosticAttemptFields(syncAttempt),
1049
- });
1050
- this.raw.setAuthHeadersJson(
1051
- JSON.stringify({
1052
- ...this.#authHeaders,
1053
- ...syncularSyncAttemptHeaders(syncAttempt),
1054
- })
1055
- );
1056
- try {
1057
- const result = await run();
1058
- this.#emitDiagnostic({
1059
- at: Date.now(),
1060
- level: 'info',
1061
- source: 'sync',
1062
- code: `sync.${requestType}.completed`,
1063
- message: `Syncular ${requestType} completed`,
1064
- ...syncularDiagnosticAttemptFields(syncAttempt),
1065
- details: { durationMs: Date.now() - startedAt },
1066
- });
1067
- if (isSyncularSyncResult(result)) {
1068
- this.#emitScopeRevokedDiagnostic(requestType, result, syncAttempt);
1069
- }
1070
- return result;
1071
- } catch (error) {
1072
- const classifiedError = toSyncularClientError(error);
1073
- const classified =
1074
- classifiedError instanceof SyncularClientError ? classifiedError : null;
1075
- this.#emitDiagnostic({
1076
- at: Date.now(),
1077
- level: 'warn',
1078
- source: 'sync',
1079
- code: classified?.code ?? `sync.${requestType}.failed`,
1080
- message: `Syncular ${requestType} failed`,
1081
- ...syncularDiagnosticAttemptFields(syncAttempt),
1082
- details: {
1083
- durationMs: Date.now() - startedAt,
1084
- error:
1085
- classifiedError instanceof Error
1086
- ? classifiedError.message
1087
- : String(classifiedError),
1088
- ...(classified
1089
- ? {
1090
- errorCode: classified.code,
1091
- category: classified.category,
1092
- retryable: classified.retryable,
1093
- recommendedAction: classified.recommendedAction,
1094
- ...(classified.details ?? {}),
1095
- }
1096
- : {}),
1097
- },
1098
- });
1099
- throw classifiedError;
1100
- } finally {
1101
- this.raw.setAuthHeadersJson(JSON.stringify(this.#authHeaders));
1102
- }
1103
- }
1104
-
1105
- #emitDiagnostic(event: SyncularDiagnosticEvent): void {
1106
- appendSyncularDiagnosticEvent(this.#recentDiagnostics, event);
1107
- if (this.#diagnosticListeners.size === 0) return;
1108
- for (const listener of this.#diagnosticListeners) {
1109
- try {
1110
- listener(event);
1111
- } catch {
1112
- // Diagnostics must never break client control flow.
1113
- }
1114
- }
1115
- }
1116
-
1117
- #hasDiagnosticListeners(): boolean {
1118
- return this.#diagnosticListeners.size > 0;
1119
- }
1120
-
1121
- #drainAndEmitRowsChanged(): void {
1122
- let events: SyncularRowsChangedEvent[];
1123
- try {
1124
- events = parseJson(this.raw.drainRowsChangedEventsJson());
1125
- } catch {
1126
- return;
1127
- }
1128
- if (this.#rowsChangedListeners.size === 0) return;
1129
- for (const event of events) {
1130
- if (event.changedTables.length === 0 && event.changedRows.length === 0) {
1131
- continue;
1132
- }
1133
- for (const listener of this.#rowsChangedListeners) {
1134
- try {
1135
- listener(event);
1136
- } catch {
1137
- // Row-change listeners must never break client control flow.
1138
- }
1139
- }
1140
- }
1141
- }
1142
-
1143
- close(): void {
1144
- this.#closed = true;
1145
- this.raw.close();
1146
- }
1147
-
1148
- #emitScopeRevokedDiagnostic(
1149
- requestType: 'syncPull' | 'syncPush' | 'syncOnce',
1150
- result: SyncularSyncResult,
1151
- syncAttempt: SyncularSyncAttempt
1152
- ): void {
1153
- const revokedSubscriptionIds = result.subscriptions
1154
- .filter((subscription) => subscription.status === 'revoked')
1155
- .map((subscription) => subscription.id);
1156
- if (revokedSubscriptionIds.length === 0) return;
1157
-
1158
- this.#emitDiagnostic({
1159
- at: Date.now(),
1160
- level: 'warn',
1161
- source: 'sync',
1162
- code: 'sync.scope_revoked',
1163
- message: 'Syncular subscription scope revoked',
1164
- ...syncularDiagnosticAttemptFields(syncAttempt),
1165
- details: {
1166
- requestType,
1167
- revokedSubscriptionIds,
1168
- revokedSubscriptionCount: revokedSubscriptionIds.length,
1169
- },
1170
- });
1171
- }
1172
- }
1173
-
1174
- function isSyncularSyncResult(value: unknown): value is SyncularSyncResult {
1175
- return (
1176
- value !== null &&
1177
- typeof value === 'object' &&
1178
- Array.isArray((value as { subscriptions?: unknown }).subscriptions)
1179
- );
1180
- }
1181
-
1182
- function parseSyncResult(value: string): SyncularSyncResult {
1183
- const raw = parseJson<RawSyncResult>(value);
1184
- return {
1185
- changedTables: raw.changed_tables ?? [],
1186
- changedRows: raw.changed_rows ?? [],
1187
- changedRowsTruncated: raw.changed_rows_truncated ?? false,
1188
- subscriptions: (raw.subscriptions ?? []).map((subscription) => ({
1189
- id: subscription.id,
1190
- table: subscription.table,
1191
- status: subscription.status,
1192
- scopes: subscription.scopes,
1193
- nextCursor: subscription.next_cursor,
1194
- bootstrapPhase: subscription.bootstrap_phase ?? 0,
1195
- bootstrapState: parseBootstrapState(subscription.bootstrap_state),
1196
- ready: subscription.ready === true,
1197
- phase: subscription.phase ?? 'pending',
1198
- progressPercent: subscription.progress_percent ?? 0,
1199
- snapshotRows: subscription.snapshot_rows ?? [],
1200
- commits: subscription.commits ?? [],
1201
- })),
1202
- bootstrap: parseBootstrapStatus(raw.bootstrap),
1203
- pushedCommits: raw.pushed_commits ?? 0,
1204
- timings: {
1205
- totalMs: raw.timings?.total_ms ?? 0,
1206
- pushMs: raw.timings?.push_ms ?? 0,
1207
- pullMs: raw.timings?.pull_ms ?? 0,
1208
- pullRequestMs: raw.timings?.pull_request_ms ?? 0,
1209
- syncPackDecodeMs: raw.timings?.sync_pack_decode_ms ?? 0,
1210
- pullTransformMs: raw.timings?.pull_transform_ms ?? 0,
1211
- integrityVerifyMs: raw.timings?.integrity_verify_ms ?? 0,
1212
- snapshotFetchMs: raw.timings?.snapshot_fetch_ms ?? 0,
1213
- pullApplyMs: raw.timings?.pull_apply_ms ?? 0,
1214
- scopeClearMs: raw.timings?.scope_clear_ms ?? 0,
1215
- snapshotRowApplyMs: raw.timings?.snapshot_row_apply_ms ?? 0,
1216
- snapshotArtifactApplyMs: raw.timings?.snapshot_artifact_apply_ms ?? 0,
1217
- snapshotArtifactCheckpointMs:
1218
- raw.timings?.snapshot_artifact_checkpoint_ms ?? 0,
1219
- snapshotArtifactCheckpointCount:
1220
- raw.timings?.snapshot_artifact_checkpoint_count ?? 0,
1221
- snapshotChunkApplyMs: raw.timings?.snapshot_chunk_apply_ms ?? 0,
1222
- snapshotChunkMaterializeMs:
1223
- raw.timings?.snapshot_chunk_materialize_ms ?? 0,
1224
- snapshotChunkResetMs: raw.timings?.snapshot_chunk_reset_ms ?? 0,
1225
- snapshotChunkBindMs: raw.timings?.snapshot_chunk_bind_ms ?? 0,
1226
- snapshotChunkStepMs: raw.timings?.snapshot_chunk_step_ms ?? 0,
1227
- commitApplyMs: raw.timings?.commit_apply_ms ?? 0,
1228
- subscriptionStateMs: raw.timings?.subscription_state_ms ?? 0,
1229
- notifyMs: raw.timings?.notify_ms ?? 0,
1230
- },
1231
- };
1232
- }
1233
-
1234
- function parseBootstrapState(
1235
- state: RawBootstrapState | null | undefined
1236
- ): SyncularBootstrapStatus['subscriptions'][number]['bootstrapState'] {
1237
- if (!state) return null;
1238
- return {
1239
- asOfCommitSeq: state.asOfCommitSeq ?? 0,
1240
- tables: state.tables ?? [],
1241
- tableIndex: state.tableIndex ?? 0,
1242
- rowCursor: state.rowCursor ?? null,
1243
- };
1244
- }
1245
-
1246
- function parseBootstrapStatus(
1247
- raw: RawBootstrapStatus | null | undefined
1248
- ): SyncularBootstrapStatus {
1249
- return {
1250
- channelPhase: raw?.channel_phase ?? 'idle',
1251
- progressPercent: raw?.progress_percent ?? 100,
1252
- isBootstrapping: raw?.is_bootstrapping === true,
1253
- criticalReady: raw?.critical_ready ?? true,
1254
- interactiveReady: raw?.interactive_ready ?? true,
1255
- complete: raw?.complete ?? true,
1256
- activePhase: raw?.active_phase ?? null,
1257
- expectedSubscriptionIds: raw?.expected_subscription_ids ?? [],
1258
- readySubscriptionIds: raw?.ready_subscription_ids ?? [],
1259
- pendingSubscriptionIds: raw?.pending_subscription_ids ?? [],
1260
- subscriptions: (raw?.subscriptions ?? []).map((subscription) => ({
1261
- id: subscription.id,
1262
- table: subscription.table,
1263
- expected: subscription.expected ?? true,
1264
- ready: subscription.ready === true,
1265
- status: subscription.status ?? null,
1266
- phase: subscription.phase ?? 'pending',
1267
- progressPercent: subscription.progress_percent ?? 0,
1268
- cursor: subscription.cursor ?? null,
1269
- bootstrapState: parseBootstrapState(subscription.bootstrap_state),
1270
- bootstrapPhase: subscription.bootstrap_phase ?? 0,
1271
- })),
1272
- phases: (raw?.phases ?? []).map((phase) => ({
1273
- phase: phase.phase ?? 0,
1274
- expectedSubscriptionIds: phase.expected_subscription_ids ?? [],
1275
- readySubscriptionIds: phase.ready_subscription_ids ?? [],
1276
- pendingSubscriptionIds: phase.pending_subscription_ids ?? [],
1277
- isReady: phase.is_ready === true,
1278
- progressPercent: phase.progress_percent ?? 0,
1279
- })),
1280
- };
1281
- }
1282
-
1283
- function buildBootstrapStatus(
1284
- configuredSubscriptions: readonly SyncularSubscriptionSpec[],
1285
- cachedById: ReadonlyMap<string, SyncularBootstrapSubscriptionStatusEntry>,
1286
- pullOptions: SyncularPullOptions | undefined
1287
- ): SyncularBootstrapStatus {
1288
- const criticalPhase = normalizeBootstrapPhase(
1289
- pullOptions?.criticalBootstrapPhase
1290
- );
1291
- const interactivePhase = Math.max(
1292
- criticalPhase,
1293
- normalizeBootstrapPhase(pullOptions?.interactiveBootstrapPhase ?? 1)
1294
- );
1295
- const subscriptions = configuredSubscriptions.map((spec) => {
1296
- const cached = cachedById.get(spec.id);
1297
- const bootstrapPhase = normalizeBootstrapPhase(spec.bootstrapPhase);
1298
- return {
1299
- id: spec.id,
1300
- table: spec.table,
1301
- expected: true,
1302
- ready: cached?.ready ?? false,
1303
- status: cached?.status ?? null,
1304
- phase: cached?.phase ?? 'pending',
1305
- progressPercent: cached?.progressPercent ?? 0,
1306
- cursor: cached?.cursor ?? null,
1307
- bootstrapState: cached?.bootstrapState ?? null,
1308
- bootstrapPhase,
1309
- };
1310
- });
1311
- const expectedSubscriptionIds = subscriptions.map(
1312
- (subscription) => subscription.id
1313
- );
1314
- const readySubscriptionIds = subscriptions
1315
- .filter((subscription) => subscription.ready)
1316
- .map((subscription) => subscription.id);
1317
- const pendingSubscriptionIds = subscriptions
1318
- .filter((subscription) => !subscription.ready)
1319
- .map((subscription) => subscription.id);
1320
- const complete = pendingSubscriptionIds.length === 0;
1321
- const criticalReady = subscriptions.every(
1322
- (subscription) =>
1323
- subscription.bootstrapPhase > criticalPhase || subscription.ready
1324
- );
1325
- const interactiveReady = subscriptions.every(
1326
- (subscription) =>
1327
- subscription.bootstrapPhase > interactivePhase || subscription.ready
1328
- );
1329
- const activePhase =
1330
- subscriptions
1331
- .filter((subscription) => !subscription.ready)
1332
- .reduce<number | null>(
1333
- (lowest, subscription) =>
1334
- lowest === null || subscription.bootstrapPhase < lowest
1335
- ? subscription.bootstrapPhase
1336
- : lowest,
1337
- null
1338
- ) ?? null;
1339
- const hasError = subscriptions.some(
1340
- (subscription) => subscription.phase === 'error'
1341
- );
1342
- const isBootstrapping = subscriptions.some(
1343
- (subscription) => !subscription.ready && subscription.phase !== 'error'
1344
- );
1345
- const channelPhase = hasError
1346
- ? 'error'
1347
- : isBootstrapping
1348
- ? 'bootstrapping'
1349
- : complete && expectedSubscriptionIds.length > 0
1350
- ? 'live'
1351
- : 'idle';
1352
- const progressPercent =
1353
- subscriptions.length === 0
1354
- ? 100
1355
- : Math.round(
1356
- subscriptions.reduce(
1357
- (sum, subscription) => sum + subscription.progressPercent,
1358
- 0
1359
- ) / subscriptions.length
1360
- );
1361
- const phaseMap = new Map<
1362
- number,
1363
- {
1364
- expectedSubscriptionIds: string[];
1365
- readySubscriptionIds: string[];
1366
- pendingSubscriptionIds: string[];
1367
- progressPercent: number;
1368
- }
1369
- >();
1370
- for (const subscription of subscriptions) {
1371
- const phase = phaseMap.get(subscription.bootstrapPhase) ?? {
1372
- expectedSubscriptionIds: [],
1373
- readySubscriptionIds: [],
1374
- pendingSubscriptionIds: [],
1375
- progressPercent: 0,
1376
- };
1377
- phase.expectedSubscriptionIds.push(subscription.id);
1378
- phase.progressPercent += subscription.progressPercent;
1379
- if (subscription.ready) {
1380
- phase.readySubscriptionIds.push(subscription.id);
1381
- } else {
1382
- phase.pendingSubscriptionIds.push(subscription.id);
1383
- }
1384
- phaseMap.set(subscription.bootstrapPhase, phase);
1385
- }
1386
- const phases = [...phaseMap.entries()]
1387
- .sort(([left], [right]) => left - right)
1388
- .map(([phase, summary]) => ({
1389
- phase,
1390
- expectedSubscriptionIds: summary.expectedSubscriptionIds,
1391
- readySubscriptionIds: summary.readySubscriptionIds,
1392
- pendingSubscriptionIds: summary.pendingSubscriptionIds,
1393
- isReady: summary.pendingSubscriptionIds.length === 0,
1394
- progressPercent:
1395
- summary.expectedSubscriptionIds.length === 0
1396
- ? 100
1397
- : Math.round(
1398
- summary.progressPercent / summary.expectedSubscriptionIds.length
1399
- ),
1400
- }));
1401
-
1402
- return {
1403
- channelPhase,
1404
- progressPercent,
1405
- isBootstrapping,
1406
- criticalReady,
1407
- interactiveReady,
1408
- complete,
1409
- activePhase,
1410
- expectedSubscriptionIds,
1411
- readySubscriptionIds,
1412
- pendingSubscriptionIds,
1413
- subscriptions,
1414
- phases,
1415
- };
1416
- }
1417
-
1418
- function normalizeBootstrapPhase(value: number | null | undefined): number {
1419
- return Number.isFinite(value) ? Math.max(0, Math.trunc(value!)) : 0;
1420
- }
1421
-
1422
- function parseConflictSummaries(value: string): SyncularConflictSummary[] {
1423
- return parseJson<RawConflictSummary[]>(value).map((conflict) => ({
1424
- id: conflict.id,
1425
- clientCommitId: conflict.client_commit_id,
1426
- opIndex: conflict.op_index,
1427
- resultStatus: conflict.result_status,
1428
- message: conflict.message,
1429
- code: conflict.code,
1430
- serverVersion: conflict.server_version,
1431
- resolvedAt: conflict.resolved_at,
1432
- resolution: conflict.resolution,
1433
- }));
1434
- }
1435
-
1436
- function parseJson<T>(value: string): T {
1437
- return JSON.parse(value) as T;
1438
- }
1439
-
1440
- function stringifyParams(params: readonly unknown[]): string {
1441
- return JSON.stringify(params, (_key, value) => {
1442
- if (typeof value === 'bigint') return Number(value);
1443
- if (value instanceof Uint8Array) return Array.from(value);
1444
- return value;
1445
- });
1446
- }
1447
-
1448
- function normalizeFieldEncryptionConfig(
1449
- config: SyncularFieldEncryptionConfig
1450
- ): Omit<SyncularFieldEncryptionConfig, 'keys'> & {
1451
- keys: Record<string, string>;
1452
- } {
1453
- const keys: Record<string, string> = {};
1454
- for (const [kid, value] of Object.entries(config.keys)) {
1455
- keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
1456
- }
1457
- return { ...config, keys };
1458
- }
1459
-
1460
- function normalizeEncryptedCrdtConfig(
1461
- config: SyncularEncryptedCrdtConfig
1462
- ): Omit<SyncularEncryptedCrdtConfig, 'keys'> & {
1463
- keys: Record<string, string>;
1464
- } {
1465
- const keys: Record<string, string> = {};
1466
- for (const [kid, value] of Object.entries(config.keys)) {
1467
- keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
1468
- }
1469
- return { ...config, keys };
1470
- }
1471
-
1472
- function normalizeBlobEncryptionConfig(
1473
- config: SyncularBlobEncryptionConfig
1474
- ): Omit<SyncularBlobEncryptionConfig, 'keys'> & {
1475
- keys: Record<string, string>;
1476
- } {
1477
- const keys: Record<string, string> = {};
1478
- for (const [kid, value] of Object.entries(config.keys)) {
1479
- keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
1480
- }
1481
- return { ...config, keys };
1482
- }
1483
-
1484
- function bytesToBase64Url(bytes: Uint8Array): string {
1485
- let binary = '';
1486
- for (const byte of bytes) binary += String.fromCharCode(byte);
1487
- return btoa(binary)
1488
- .replace(/\+/g, '-')
1489
- .replace(/\//g, '_')
1490
- .replace(/=+$/g, '');
1491
- }