@syncular/client 0.1.3 → 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,1735 +0,0 @@
1
- import { issueSyncularAuthLease } from './auth-leases.js';
2
- import { assertSyncularBlobPayloadLimit } from './blob-limits.js';
3
- import { isSyncularRemoteMode, resolveSyncularClientConfig, SYNCULAR_DEFAULT_STORAGE, } from './client-config.js';
4
- import { appendSyncularDiagnosticEvent, appendSyncularSyncTimings, createSyncularSyncAttempt, summarizeSyncularDiagnosticSubscriptions, } from './diagnostics.js';
5
- import { isGeneratedSyncularOperationalStateWorkerRequestType, } from './generated-bridge.js';
6
- import { browserSyncularNetworkStatusSource } from './network.js';
7
- import { assertSyncularReadonlySql } from './sql-safety.js';
8
- import { getSyncularRuntimeArtifact, selectSyncularRuntimeArtifact, } from './wasm-runtime.js';
9
- import { createSyncularWorkerErrorPayload, SYNCULAR_WORKER_PROTOCOL_VERSION, } from './worker-protocol.js';
10
- const DEFAULT_SYNCULAR_WORKER_REQUEST_TIMEOUT_MS = 30_000;
11
- const NO_SYNCULAR_WORKER_REQUEST_TIMEOUT = 0;
12
- const SYNCULAR_SYNC_WORKER_REQUEST_TYPES = new Set([
13
- 'forceSubscriptionsBootstrap',
14
- 'syncPull',
15
- 'syncPush',
16
- 'syncOnce',
17
- ]);
18
- const SYNCULAR_BLOB_WORKER_REQUEST_TYPES = new Set([
19
- 'storeBlob',
20
- 'retrieveBlob',
21
- 'processBlobUploadQueue',
22
- ]);
23
- const SYNCULAR_STORAGE_MAINTENANCE_WORKER_REQUEST_TYPES = new Set([
24
- 'open',
25
- 'compactStorage',
26
- 'exportLocalSupportBundle',
27
- 'importLocalSupportBundle',
28
- 'repairLocalHealth',
29
- 'resetLocalSyncState',
30
- ]);
31
- function syncularWorkerRequestTimeoutMs(config, type) {
32
- if (typeof config === 'number' || config === false) {
33
- return normalizeSyncularWorkerTimeoutMs(config);
34
- }
35
- const byType = config?.byType?.[type];
36
- if (byType !== undefined)
37
- return normalizeSyncularWorkerTimeoutMs(byType);
38
- if (SYNCULAR_SYNC_WORKER_REQUEST_TYPES.has(type)) {
39
- return normalizeSyncularWorkerTimeoutMs(config?.syncMs ?? false);
40
- }
41
- if (SYNCULAR_BLOB_WORKER_REQUEST_TYPES.has(type)) {
42
- return normalizeSyncularWorkerTimeoutMs(config?.blobMs ?? false);
43
- }
44
- if (SYNCULAR_STORAGE_MAINTENANCE_WORKER_REQUEST_TYPES.has(type)) {
45
- return normalizeSyncularWorkerTimeoutMs(config?.storageMaintenanceMs ?? false);
46
- }
47
- return normalizeSyncularWorkerTimeoutMs(config?.defaultMs ?? DEFAULT_SYNCULAR_WORKER_REQUEST_TIMEOUT_MS);
48
- }
49
- function normalizeSyncularWorkerTimeoutMs(value) {
50
- if (value === false)
51
- return NO_SYNCULAR_WORKER_REQUEST_TIMEOUT;
52
- if (!Number.isFinite(value) || value <= 0) {
53
- return NO_SYNCULAR_WORKER_REQUEST_TIMEOUT;
54
- }
55
- return Math.trunc(value);
56
- }
57
- export async function createSyncularWorkerClient(options) {
58
- const config = resolveSyncularClientConfig(options.config);
59
- const runtime = typeof options.runtime === 'string'
60
- ? getSyncularRuntimeArtifact(options.runtime)
61
- : (options.runtime ??
62
- selectSyncularRuntimeArtifact(options.requiredRuntimeFeatures, options.runtimeArtifacts));
63
- const worker = typeof options.worker === 'function'
64
- ? options.worker()
65
- : (options.worker ?? createDefaultSyncularWorker());
66
- const client = new SyncularWorkerClient(worker, {
67
- ownsWorker: options.worker == null,
68
- requestTimeoutMs: options.requestTimeoutMs,
69
- getHeaders: options.getHeaders,
70
- authLifecycle: options.authLifecycle,
71
- diagnostics: options.diagnostics,
72
- rowsChangedDebounceMs: options.sync?.rowsChangedDebounceMs,
73
- network: options.sync?.network === false
74
- ? undefined
75
- : (options.sync?.network ?? browserSyncularNetworkStatusSource()),
76
- blobLimits: options.blobLimits,
77
- });
78
- try {
79
- await client.open(config, runtime);
80
- }
81
- catch (err) {
82
- if (options.config.storage == null &&
83
- config.storage === SYNCULAR_DEFAULT_STORAGE &&
84
- isOpfsOpenFailure(err)) {
85
- const fallbackConfig = { ...config, storage: 'indexedDb' };
86
- client.setStorageFallback({
87
- from: SYNCULAR_DEFAULT_STORAGE,
88
- to: fallbackConfig.storage,
89
- reason: errorMessage(err),
90
- });
91
- await client.open(fallbackConfig, runtime);
92
- }
93
- else {
94
- throw err;
95
- }
96
- }
97
- return client;
98
- }
99
- export function getDefaultSyncularWorkerUrl() {
100
- const runtimeUrl = new URL(import.meta.url);
101
- const sourceRuntime = runtimeUrl.pathname.endsWith('/src/worker-client.ts');
102
- return new URL(sourceRuntime ? './worker-entry.ts' : './worker-entry.js', runtimeUrl);
103
- }
104
- export function createDefaultSyncularWorker() {
105
- return new Worker(getDefaultSyncularWorkerUrl(), {
106
- type: 'module',
107
- credentials: 'same-origin',
108
- });
109
- }
110
- export class SyncularWorkerError extends Error {
111
- code;
112
- category;
113
- retryable;
114
- recommendedAction;
115
- details;
116
- constructor(payload) {
117
- super(payload.message);
118
- this.name = payload.name ?? 'SyncularWorkerError';
119
- this.code = payload.code;
120
- this.category = payload.category;
121
- this.retryable = payload.retryable === true;
122
- this.recommendedAction = payload.recommendedAction;
123
- this.details = payload.details;
124
- if (payload.stack)
125
- this.stack = payload.stack;
126
- }
127
- }
128
- export class SyncularWorkerClient {
129
- worker;
130
- #nextId = 1;
131
- #pending = new Map();
132
- #closed = false;
133
- #requestTimeouts;
134
- #getHeaders;
135
- #authHeaders = {};
136
- #authLifecycle;
137
- #authRefreshInFlight;
138
- #config;
139
- #realtimeOptions;
140
- #realtimeState = 'disconnected';
141
- #storageFallback;
142
- #lastDiagnostic;
143
- #lastError;
144
- #lastLifecycleState;
145
- #recoveryRequired = false;
146
- #authRequired = false;
147
- #subscriptions = [];
148
- #lastBootstrap;
149
- #recentDiagnostics = [];
150
- #recentSyncTimings = [];
151
- #rowsChangedDebounceMs;
152
- #rowsChangedDebounceTimer;
153
- #pendingRowsChanged;
154
- #lastOutboxStats;
155
- #lastConflictStats;
156
- #lastBlobUploadStats;
157
- #blobLimits;
158
- #network;
159
- #networkOnline;
160
- #unsubscribeNetwork;
161
- #diagnosticListeners = new Set();
162
- #rowsChangedListeners = new Set();
163
- #eventListeners = new Map();
164
- #presenceByScopeKey = new Map();
165
- #joinedPresence = new Map();
166
- #liveListeners = new Map();
167
- constructor(worker, options) {
168
- this.worker = worker;
169
- this.#requestTimeouts = options.requestTimeoutMs;
170
- this.#getHeaders = options.getHeaders;
171
- this.#authLifecycle = options.authLifecycle;
172
- this.#blobLimits = options.blobLimits;
173
- this.#network = options.network;
174
- this.#networkOnline = this.#network?.isOnline();
175
- this.#unsubscribeNetwork = this.#subscribeNetworkEvents();
176
- this.#rowsChangedDebounceMs =
177
- options.rowsChangedDebounceMs === false
178
- ? false
179
- : Math.max(0, options.rowsChangedDebounceMs ?? 0);
180
- if (options.diagnostics) {
181
- this.#diagnosticListeners.add(options.diagnostics);
182
- }
183
- this.worker.onmessage = (event) => {
184
- this.#handleWorkerMessage(event.data);
185
- };
186
- this.worker.onerror = (event) => {
187
- const payload = createSyncularWorkerErrorPayload('worker.failed', event.message || 'Syncular worker failed');
188
- this.#emitDiagnostic({
189
- level: 'error',
190
- source: 'worker',
191
- code: 'worker.failed',
192
- message: payload.message,
193
- });
194
- this.#rejectAll(payload);
195
- };
196
- this.worker.onmessageerror = () => {
197
- const payload = createSyncularWorkerErrorPayload('worker.message_unreadable', 'Syncular worker sent an unreadable message');
198
- this.#emitDiagnostic({
199
- level: 'error',
200
- source: 'worker',
201
- code: 'worker.message_unreadable',
202
- message: payload.message,
203
- });
204
- this.#rejectAll(payload);
205
- };
206
- this.ownsWorker = options.ownsWorker;
207
- }
208
- ownsWorker;
209
- async open(config, runtime) {
210
- const resolvedConfig = resolveSyncularClientConfig(config);
211
- const resolvedRuntime = typeof runtime === 'string'
212
- ? getSyncularRuntimeArtifact(runtime)
213
- : runtime;
214
- await this.#request({
215
- type: 'open',
216
- config: resolvedConfig,
217
- runtime: serializeRuntimeArtifact(resolvedRuntime),
218
- });
219
- this.#config = resolvedConfig;
220
- this.#realtimeOptions = undefined;
221
- this.#subscriptions = [];
222
- this.#lastBootstrap = undefined;
223
- await this.#refreshAuthHeaders();
224
- }
225
- async setAuthHeaders(headers) {
226
- await this.#setAuthHeaders(headers, { restartRealtime: true });
227
- }
228
- async issueAuthLease(request) {
229
- await this.#refreshAuthHeaders();
230
- try {
231
- return await this.#issueAuthLeaseWithCurrentHeaders(request);
232
- }
233
- catch (error) {
234
- const shouldRetry = await this.#resolveAuthRetry(error, 'authLeaseIssue');
235
- if (!shouldRetry)
236
- throw error;
237
- await this.#refreshAuthHeaders();
238
- return this.#issueAuthLeaseWithCurrentHeaders(request);
239
- }
240
- }
241
- async upsertAuthLease(lease) {
242
- await this.#requestAndDrain({
243
- type: 'upsertAuthLease',
244
- lease,
245
- });
246
- }
247
- async authLease(leaseId) {
248
- return this.#request({
249
- type: 'authLease',
250
- leaseId,
251
- });
252
- }
253
- async activeAuthLeases(actorId, nowMs = Date.now()) {
254
- return this.#request({
255
- type: 'activeAuthLeases',
256
- actorId: actorId ?? null,
257
- nowMs: Math.trunc(nowMs),
258
- });
259
- }
260
- async setFieldEncryption(config) {
261
- await this.#request({
262
- type: 'setFieldEncryption',
263
- config: config == null ? null : normalizeFieldEncryptionConfig(config),
264
- });
265
- }
266
- async setEncryptedCrdt(config) {
267
- await this.#request({
268
- type: 'setEncryptedCrdt',
269
- config: config == null ? null : normalizeEncryptedCrdtConfig(config),
270
- });
271
- }
272
- async setBlobEncryption(config) {
273
- await this.#request({
274
- type: 'setBlobEncryption',
275
- config: config == null ? null : normalizeBlobEncryptionConfig(config),
276
- });
277
- }
278
- async #setAuthHeaders(headers, options) {
279
- this.#authHeaders = cloneAuthHeaders(headers);
280
- await this.#request({
281
- type: 'setAuthHeaders',
282
- headers: cloneAuthHeaders(this.#authHeaders),
283
- });
284
- if (options.restartRealtime)
285
- await this.#restartRealtime();
286
- }
287
- async #issueAuthLeaseWithCurrentHeaders(request) {
288
- const config = this.#config;
289
- if (!config) {
290
- throw new Error('Syncular worker client must be opened before auth lease issue');
291
- }
292
- assertSyncularRemoteMode(config, 'auth lease issue');
293
- const lease = await issueSyncularAuthLease({
294
- baseUrl: config.baseUrl,
295
- headers: this.#authHeaders,
296
- request,
297
- appSchema: config.appSchema,
298
- });
299
- await this.upsertAuthLease(lease);
300
- this.#emitDiagnostic({
301
- level: 'info',
302
- source: 'auth',
303
- code: 'auth_lease.issued',
304
- message: 'Syncular auth lease issued and stored',
305
- details: {
306
- leaseId: lease.leaseId,
307
- expiresAtMs: lease.expiresAtMs,
308
- schemaVersion: lease.schemaVersion,
309
- },
310
- });
311
- return lease;
312
- }
313
- async startRealtime(options = {}) {
314
- if (options === false) {
315
- await this.stopRealtime();
316
- return;
317
- }
318
- const config = this.#config;
319
- if (!config) {
320
- throw new Error('Syncular worker client must be opened before realtime');
321
- }
322
- assertSyncularRemoteMode(config, 'realtime');
323
- const realtimeOptions = options === true ? {} : options;
324
- if (realtimeOptions.enabled === false) {
325
- await this.stopRealtime();
326
- return;
327
- }
328
- this.#realtimeOptions = realtimeOptions;
329
- await this.#refreshAuthHeaders({ restartRealtime: false });
330
- await this.#request({
331
- type: 'startRealtime',
332
- options: await resolveRealtimeWorkerOptions(realtimeOptions, config),
333
- });
334
- }
335
- async stopRealtime() {
336
- this.#realtimeOptions = undefined;
337
- await this.#request({ type: 'stopRealtime' });
338
- }
339
- async setSubscriptions(subscriptions) {
340
- this.#subscriptions = [...subscriptions];
341
- this.#lastBootstrap = undefined;
342
- await this.#request({
343
- type: 'setSubscriptions',
344
- subscriptions: this.#subscriptions,
345
- });
346
- }
347
- async forceSubscriptionsBootstrap(subscriptionIds = []) {
348
- return this.#request({
349
- type: 'forceSubscriptionsBootstrap',
350
- subscriptionIds: [...subscriptionIds],
351
- });
352
- }
353
- async executeSql(sql, params = []) {
354
- assertSyncularReadonlySql(sql);
355
- return this.#request({
356
- type: 'executeSql',
357
- sql,
358
- params: [...params],
359
- });
360
- }
361
- executeUnsafeSql(sql, params = []) {
362
- return this.#request({
363
- type: 'executeUnsafeSql',
364
- sql,
365
- params: [...params],
366
- });
367
- }
368
- subscribeQuery(sql, params, tables, hints = []) {
369
- return this.#request({
370
- type: 'subscribeQuery',
371
- sql,
372
- params: [...params],
373
- tables: [...tables],
374
- hints: hints.map((hint) => ({
375
- table: hint.table,
376
- rowIds: [...(hint.rowIds ?? [])],
377
- fields: [...(hint.fields ?? [])],
378
- })),
379
- });
380
- }
381
- async unsubscribeQuery(id) {
382
- await this.#request({ type: 'unsubscribeQuery', queryId: id });
383
- }
384
- drainLiveQueryEvents() {
385
- return this.#request({ type: 'drainLiveQueryEvents' });
386
- }
387
- liveQueryDiagnostics() {
388
- return this.#request({ type: 'liveQueryDiagnostics' });
389
- }
390
- async applyMutation(operation, localRow) {
391
- return this.#requestAndDrain({
392
- type: 'applyMutation',
393
- operation,
394
- localRow,
395
- });
396
- }
397
- async applyLeasedMutation(operation, localRow) {
398
- return this.#requestAndDrain({
399
- type: 'applyLeasedMutation',
400
- operation,
401
- localRow,
402
- });
403
- }
404
- async applyMutationsBatch(operations) {
405
- return this.#requestAndDrain({
406
- type: 'applyMutationsBatch',
407
- operations,
408
- });
409
- }
410
- async applyMutationsCommit(operations) {
411
- return this.#requestAndDrain({
412
- type: 'applyMutationsCommit',
413
- operations,
414
- });
415
- }
416
- async applyLeasedMutationsCommit(operations) {
417
- return this.#requestAndDrain({
418
- type: 'applyLeasedMutationsCommit',
419
- operations,
420
- });
421
- }
422
- async syncPull(options = {}) {
423
- return this.#syncWithAuthRetry({
424
- type: 'syncPull',
425
- syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
426
- });
427
- }
428
- async syncPush(options = {}) {
429
- return this.#syncWithAuthRetry({
430
- type: 'syncPush',
431
- syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
432
- });
433
- }
434
- async syncOnce(options = {}) {
435
- return this.#syncWithAuthRetry({
436
- type: 'syncOnce',
437
- syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
438
- });
439
- }
440
- async resumeFromBackground(options = {}) {
441
- this.#recoveryRequired = true;
442
- this.#emitDiagnostic({
443
- level: 'info',
444
- source: 'client',
445
- code: 'lifecycle.resume_from_background',
446
- message: 'Syncular resumed from background',
447
- });
448
- await this.#refreshAuthHeaders({ restartRealtime: false });
449
- await this.#restartRealtime();
450
- return this.#syncWithAuthRetry({
451
- type: 'syncOnce',
452
- syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
453
- }, { refreshAuthHeaders: false });
454
- }
455
- transportStats() {
456
- return this.#request({ type: 'transportStats' });
457
- }
458
- async resetTransportStats() {
459
- await this.#request({ type: 'resetTransportStats' });
460
- }
461
- conflictSummaries() {
462
- return this.#request({ type: 'conflictSummaries' });
463
- }
464
- retryConflictKeepLocal(id) {
465
- return this.#requestAndDrain({
466
- type: 'retryConflictKeepLocal',
467
- conflictId: id,
468
- });
469
- }
470
- async resolveConflict(id, resolution) {
471
- await this.#requestAndDrain({
472
- type: 'resolveConflict',
473
- conflictId: id,
474
- resolution,
475
- });
476
- }
477
- listTable(table) {
478
- return this.#request({ type: 'listTable', table });
479
- }
480
- storeBlob(data, options) {
481
- try {
482
- assertSyncularBlobPayloadLimit({
483
- operation: 'store',
484
- size: data.byteLength,
485
- limits: this.#blobLimits,
486
- options,
487
- diagnostics: (event) => this.#emitDiagnostic(event),
488
- });
489
- }
490
- catch (error) {
491
- return Promise.reject(error);
492
- }
493
- const request = { type: 'storeBlob', data, options };
494
- const result = options?.immediate
495
- ? this.#requestWithAuthRetry(request, 'blobInitiateUpload')
496
- : this.#request(request);
497
- return result.then((ref) => {
498
- if (options?.immediate) {
499
- this.#emitClientEvent('blobUploadCompleted', { ref });
500
- }
501
- void this.#emitOperationalState();
502
- return ref;
503
- });
504
- }
505
- async retrieveBlob(ref) {
506
- assertSyncularBlobPayloadLimit({
507
- operation: 'retrieve',
508
- size: ref.size,
509
- limits: this.#blobLimits,
510
- refHash: ref.hash,
511
- diagnostics: (event) => this.#emitDiagnostic(event),
512
- });
513
- const wasLocal = this.#hasDiagnosticListeners()
514
- ? await this.#request({
515
- type: 'isBlobLocal',
516
- hash: ref.hash,
517
- }).catch(() => undefined)
518
- : undefined;
519
- try {
520
- const bytes = await this.#requestWithAuthRetry({ type: 'retrieveBlob', ref }, 'blobGetDownloadUrl');
521
- if (wasLocal !== undefined) {
522
- this.#emitDiagnostic({
523
- level: 'info',
524
- source: 'blob',
525
- code: wasLocal ? 'blob.cache_hit' : 'blob.cache_miss',
526
- message: wasLocal
527
- ? 'Syncular blob served from local cache'
528
- : 'Syncular blob fetched after local cache miss',
529
- details: {
530
- hash: ref.hash,
531
- size: ref.size,
532
- mimeType: ref.mimeType,
533
- encrypted: ref.encrypted === true,
534
- ...(ref.keyId ? { keyId: ref.keyId } : {}),
535
- },
536
- });
537
- }
538
- return bytes;
539
- }
540
- catch (error) {
541
- if (this.#hasDiagnosticListeners()) {
542
- this.#emitDiagnostic({
543
- level: 'warn',
544
- source: 'blob',
545
- code: 'blob.download_failed',
546
- message: `Syncular blob download failed: ${errorMessage(error)}`,
547
- details: {
548
- hash: ref.hash,
549
- size: ref.size,
550
- mimeType: ref.mimeType,
551
- encrypted: ref.encrypted === true,
552
- ...(ref.keyId ? { keyId: ref.keyId } : {}),
553
- },
554
- });
555
- }
556
- throw error;
557
- }
558
- }
559
- async isBlobLocal(hash) {
560
- const local = await this.#request({ type: 'isBlobLocal', hash });
561
- if (this.#hasDiagnosticListeners()) {
562
- this.#emitDiagnostic({
563
- level: 'debug',
564
- source: 'blob',
565
- code: local ? 'blob.cache_hit' : 'blob.cache_miss',
566
- message: local
567
- ? 'Syncular blob is available in local cache'
568
- : 'Syncular blob is not available in local cache',
569
- details: { hash },
570
- });
571
- }
572
- return local;
573
- }
574
- async processBlobUploadQueue(options) {
575
- const observeBlobEvents = this.#hasClientEventListeners('blobUploadCompleted') ||
576
- this.#hasClientEventListeners('blobUploadFailed') ||
577
- this.#hasDiagnosticListeners();
578
- const before = observeBlobEvents
579
- ? await this.#readBlobOutboxRows().catch(() => [])
580
- : [];
581
- const result = await this.#requestWithAuthRetry({ type: 'processBlobUploadQueue', options }, 'blobInitiateUpload');
582
- if (observeBlobEvents) {
583
- const after = await this.#readBlobOutboxRows().catch(() => []);
584
- this.#emitBlobUploadEvents(before, after);
585
- }
586
- if (this.#hasDiagnosticListeners()) {
587
- this.#emitDiagnostic({
588
- level: result.failed > 0 ? 'warn' : 'info',
589
- source: 'blob',
590
- code: 'blob.upload_queue_processed',
591
- message: 'Syncular blob upload queue processed',
592
- details: result,
593
- });
594
- }
595
- void this.#emitOperationalState();
596
- return result;
597
- }
598
- async blobUploadQueueStats() {
599
- const stats = await this.#readBlobUploadStats();
600
- this.#lastBlobUploadStats = stats;
601
- return stats;
602
- }
603
- blobCacheStats() {
604
- return this.#request({ type: 'blobCacheStats' });
605
- }
606
- async pruneBlobCache(maxBytes) {
607
- const prunedBytes = await this.#request({
608
- type: 'pruneBlobCache',
609
- maxBytes,
610
- });
611
- if (this.#hasDiagnosticListeners()) {
612
- this.#emitDiagnostic({
613
- level: 'info',
614
- source: 'blob',
615
- code: 'blob.cache_pruned',
616
- message: 'Syncular blob cache pruned',
617
- details: {
618
- prunedBytes,
619
- ...(maxBytes !== undefined ? { maxBytes } : {}),
620
- },
621
- });
622
- }
623
- return prunedBytes;
624
- }
625
- async clearBlobCache() {
626
- await this.#request({ type: 'clearBlobCache' });
627
- if (this.#hasDiagnosticListeners()) {
628
- this.#emitDiagnostic({
629
- level: 'info',
630
- source: 'blob',
631
- code: 'blob.cache_cleared',
632
- message: 'Syncular blob cache cleared',
633
- });
634
- }
635
- }
636
- compactStorage(options = {}) {
637
- return this.#request({ type: 'compactStorage', options });
638
- }
639
- generatedSchemaState() {
640
- return this.#request({ type: 'generatedSchemaState' });
641
- }
642
- localHealthCheck() {
643
- return this.#request({ type: 'localHealthCheck' });
644
- }
645
- exportLocalSupportBundle() {
646
- return this.#request({ type: 'exportLocalSupportBundle' });
647
- }
648
- importLocalSupportBundle(bundle) {
649
- return this.#request({
650
- type: 'importLocalSupportBundle',
651
- bundleJson: typeof bundle === 'string' ? bundle : JSON.stringify(bundle),
652
- });
653
- }
654
- async repairLocalHealth(request) {
655
- const message = {
656
- type: 'repairLocalHealth',
657
- request: {
658
- action: request.action,
659
- subscriptionIds: [...(request.subscriptionIds ?? [])],
660
- tables: [...(request.tables ?? [])],
661
- },
662
- };
663
- const result = request.action === 'clearOrphanedSyncedRows'
664
- ? await this.#requestAndDrain(message)
665
- : await this.#request(message);
666
- if (request.action === 'clearOrphanedSyncedRows') {
667
- this.#emitLifecycleChanged();
668
- }
669
- return result;
670
- }
671
- async resetLocalSyncState(request = {}) {
672
- const normalized = {
673
- subscriptionIds: [...(request.subscriptionIds ?? [])],
674
- clearSyncedRows: request.clearSyncedRows === true,
675
- };
676
- const result = await this.#requestAndDrain({
677
- type: 'resetLocalSyncState',
678
- request: normalized,
679
- });
680
- this.#lastBootstrap = undefined;
681
- this.#emitLifecycleChanged();
682
- return result;
683
- }
684
- buildYjsTextUpdate(args) {
685
- return this.#request({ type: 'buildYjsTextUpdate', args });
686
- }
687
- applyYjsTextUpdates(args) {
688
- return this.#request({ type: 'applyYjsTextUpdates', args });
689
- }
690
- applyYjsEnvelopeToPayload(args) {
691
- return this.#request({ type: 'applyYjsEnvelopeToPayload', args });
692
- }
693
- openCrdtField(request) {
694
- return this.#request({ type: 'openCrdtField', request });
695
- }
696
- applyCrdtFieldText(request) {
697
- return this.#requestAndDrain({ type: 'applyCrdtFieldText', request });
698
- }
699
- applyCrdtFieldYjsUpdate(request) {
700
- return this.#requestAndDrain({
701
- type: 'applyCrdtFieldYjsUpdate',
702
- request,
703
- });
704
- }
705
- materializeCrdtField(request) {
706
- return this.#request({ type: 'materializeCrdtField', request });
707
- }
708
- crdtDocumentSnapshot(request) {
709
- return this.#request({ type: 'crdtDocumentSnapshot', request });
710
- }
711
- crdtUpdateLog(request) {
712
- return this.#request({ type: 'crdtUpdateLog', request });
713
- }
714
- snapshotCrdtFieldStateVector(request) {
715
- return this.#request({ type: 'snapshotCrdtFieldStateVector', request });
716
- }
717
- compactCrdtField(request) {
718
- return this.#requestAndDrain({ type: 'compactCrdtField', request });
719
- }
720
- encryptionHelper(method, args = {}) {
721
- return this.#request({ type: 'encryptionHelper', method, args });
722
- }
723
- runtimeInfo() {
724
- return this.#request({ type: 'runtimeInfo' }).then((info) => ({
725
- ...info,
726
- storageFallback: this.#storageFallback,
727
- }));
728
- }
729
- connectionState() {
730
- return {
731
- closed: this.#closed,
732
- pendingRequests: this.#pending.size,
733
- realtime: this.#realtimeState,
734
- ...(this.#storageFallback
735
- ? { storageFallback: this.#storageFallback }
736
- : {}),
737
- ...(this.#lastDiagnostic ? { lastDiagnostic: this.#lastDiagnostic } : {}),
738
- ...(this.#lastError ? { lastError: this.#lastError } : {}),
739
- };
740
- }
741
- lifecycleState() {
742
- return this.#computeLifecycleState();
743
- }
744
- #computeLifecycleState() {
745
- const outbox = this.#lastOutboxStats;
746
- const conflicts = this.#lastConflictStats;
747
- const blobUploads = this.#lastBlobUploadStats;
748
- const bootstrap = this.#lastBootstrap
749
- ? {
750
- complete: this.#lastBootstrap.complete,
751
- criticalReady: this.#lastBootstrap.criticalReady,
752
- interactiveReady: this.#lastBootstrap.interactiveReady,
753
- isBootstrapping: this.#lastBootstrap.isBootstrapping,
754
- progressPercent: this.#lastBootstrap.progressPercent,
755
- }
756
- : undefined;
757
- const hasFailedOutbox = (outbox?.failed ?? 0) > 0;
758
- const hasConflicts = (conflicts?.unresolved ?? 0) > 0;
759
- const hasFailedBlobUploads = (blobUploads?.failed ?? 0) > 0;
760
- const authRequired = this.#authRequired || this.#lastError?.code === 'sync.auth_required';
761
- const offline = this.#networkOnline === false;
762
- const pendingSyncRequests = this.#pendingSyncRequestCount();
763
- const phase = this.#closed
764
- ? 'closed'
765
- : authRequired
766
- ? 'authRequired'
767
- : offline
768
- ? 'offline'
769
- : this.#recoveryRequired || bootstrap?.isBootstrapping === true
770
- ? 'recovering'
771
- : pendingSyncRequests > 0
772
- ? 'syncing'
773
- : this.#realtimeState === 'connecting'
774
- ? 'connecting'
775
- : hasFailedOutbox || hasConflicts || hasFailedBlobUploads
776
- ? 'degraded'
777
- : bootstrap?.complete === true
778
- ? 'complete'
779
- : 'offline';
780
- return {
781
- phase,
782
- realtime: this.#realtimeState,
783
- online: this.#networkOnline ?? this.#realtimeState === 'connected',
784
- requiresAction: authRequired || hasFailedOutbox || hasConflicts || hasFailedBlobUploads,
785
- pendingRequests: this.#pending.size,
786
- ...(bootstrap ? { bootstrap } : {}),
787
- ...(outbox ? { outbox } : {}),
788
- ...(conflicts ? { conflicts } : {}),
789
- ...(blobUploads ? { blobUploads } : {}),
790
- ...(this.#lastDiagnostic ? { lastDiagnostic: this.#lastDiagnostic } : {}),
791
- ...(this.#lastError ? { lastError: this.#lastError } : {}),
792
- };
793
- }
794
- async diagnosticSnapshot() {
795
- const [runtime, transportStats] = await Promise.all([
796
- this.runtimeInfo(),
797
- this.transportStats().catch(() => undefined),
798
- ]);
799
- const outboxStats = this.#lastOutboxStats ??
800
- (await this.#readOutboxStats().catch(() => undefined));
801
- if (outboxStats)
802
- this.#lastOutboxStats = outboxStats;
803
- const conflictStats = this.#lastConflictStats ??
804
- (await this.#readConflictStats().catch(() => undefined));
805
- if (conflictStats)
806
- this.#lastConflictStats = conflictStats;
807
- const blobUploadStats = this.#lastBlobUploadStats ??
808
- (await this.blobUploadQueueStats().catch(() => undefined));
809
- return {
810
- generatedAt: Date.now(),
811
- runtime,
812
- connection: this.connectionState(),
813
- subscriptions: summarizeSyncularDiagnosticSubscriptions(this.#subscriptions, this.#lastBootstrap),
814
- recentDiagnostics: [...this.#recentDiagnostics],
815
- recentSyncTimings: [...this.#recentSyncTimings],
816
- ...(this.#lastBootstrap ? { bootstrap: this.#lastBootstrap } : {}),
817
- ...(transportStats ? { transportStats } : {}),
818
- ...(outboxStats ? { outboxStats } : {}),
819
- ...(conflictStats ? { conflictStats } : {}),
820
- ...(blobUploadStats ? { blobUploadStats } : {}),
821
- };
822
- }
823
- setStorageFallback(fallback) {
824
- this.#storageFallback = fallback;
825
- this.#emitDiagnostic({
826
- level: 'warn',
827
- source: 'storage',
828
- code: 'storage.fallback',
829
- message: `Syncular storage fell back from ${fallback.from} to ${fallback.to}`,
830
- details: {
831
- from: fallback.from,
832
- to: fallback.to,
833
- reason: fallback.reason,
834
- },
835
- });
836
- }
837
- addDiagnosticListener(listener) {
838
- this.#diagnosticListeners.add(listener);
839
- return () => {
840
- this.#diagnosticListeners.delete(listener);
841
- };
842
- }
843
- addEventListener(event, listener) {
844
- const listeners = this.#eventListeners.get(event) ?? new Set();
845
- listeners.add(listener);
846
- this.#eventListeners.set(event, listeners);
847
- return () => {
848
- listeners.delete(listener);
849
- if (listeners.size === 0)
850
- this.#eventListeners.delete(event);
851
- };
852
- }
853
- addRowsChangedListener(listener) {
854
- this.#rowsChangedListeners.add(listener);
855
- return () => {
856
- this.#rowsChangedListeners.delete(listener);
857
- };
858
- }
859
- getPresence(scopeKey) {
860
- return (this.#presenceByScopeKey.get(scopeKey) ??
861
- []);
862
- }
863
- joinPresence(scopeKey, metadata) {
864
- this.#joinedPresence.set(scopeKey, metadata);
865
- void this.#request({
866
- type: 'sendPresence',
867
- action: 'join',
868
- scopeKey,
869
- ...(metadata === undefined ? {} : { metadata }),
870
- }).catch((error) => {
871
- this.#emitDiagnostic({
872
- level: 'warn',
873
- source: 'realtime',
874
- code: 'realtime.presence_join_failed',
875
- message: `Syncular presence join failed: ${errorMessage(error)}`,
876
- details: { scopeKey },
877
- });
878
- });
879
- this.#applyPresenceEvent({
880
- action: 'join',
881
- scopeKey,
882
- clientId: this.#config?.clientId ?? '',
883
- actorId: this.#config?.actorId ?? '',
884
- metadata,
885
- });
886
- }
887
- leavePresence(scopeKey) {
888
- this.#joinedPresence.delete(scopeKey);
889
- void this.#request({
890
- type: 'sendPresence',
891
- action: 'leave',
892
- scopeKey,
893
- }).catch((error) => {
894
- this.#emitDiagnostic({
895
- level: 'warn',
896
- source: 'realtime',
897
- code: 'realtime.presence_leave_failed',
898
- message: `Syncular presence leave failed: ${errorMessage(error)}`,
899
- details: { scopeKey },
900
- });
901
- });
902
- this.#applyPresenceEvent({
903
- action: 'leave',
904
- scopeKey,
905
- clientId: this.#config?.clientId ?? '',
906
- actorId: this.#config?.actorId ?? '',
907
- });
908
- }
909
- updatePresenceMetadata(scopeKey, metadata) {
910
- if (this.#joinedPresence.has(scopeKey)) {
911
- this.#joinedPresence.set(scopeKey, metadata);
912
- }
913
- void this.#request({
914
- type: 'sendPresence',
915
- action: 'update',
916
- scopeKey,
917
- metadata,
918
- }).catch((error) => {
919
- this.#emitDiagnostic({
920
- level: 'warn',
921
- source: 'realtime',
922
- code: 'realtime.presence_update_failed',
923
- message: `Syncular presence update failed: ${errorMessage(error)}`,
924
- details: { scopeKey },
925
- });
926
- });
927
- this.#applyPresenceEvent({
928
- action: 'update',
929
- scopeKey,
930
- clientId: this.#config?.clientId ?? '',
931
- actorId: this.#config?.actorId ?? '',
932
- metadata,
933
- });
934
- }
935
- addPresenceListener(listener) {
936
- return this.addEventListener('presenceChanged', listener);
937
- }
938
- addLiveQueryListener(queryId, listener) {
939
- this.#liveListeners.set(queryId, listener);
940
- }
941
- removeLiveQueryListener(queryId) {
942
- this.#liveListeners.delete(queryId);
943
- }
944
- async close() {
945
- if (this.#closed)
946
- return;
947
- this.#closed = true;
948
- this.#realtimeState = 'disconnected';
949
- this.#unsubscribeNetwork?.();
950
- this.#unsubscribeNetwork = undefined;
951
- this.#emitLifecycleChanged();
952
- try {
953
- await this.#request({ type: 'close' });
954
- }
955
- finally {
956
- this.#flushRowsChanged();
957
- this.#rejectAll(createSyncularWorkerErrorPayload('worker.closed', 'Syncular worker client closed'));
958
- if (this.ownsWorker)
959
- this.worker.terminate();
960
- }
961
- }
962
- async #requestAndDrain(request) {
963
- const value = await this.#request(request);
964
- await this.#emitLiveEvents();
965
- if (shouldEmitOperationalState(request.type)) {
966
- void this.#emitOperationalState();
967
- }
968
- return value;
969
- }
970
- async #syncWithAuthRetry(request, options = { refreshAuthHeaders: true }) {
971
- const config = this.#config;
972
- if (!config) {
973
- throw new Error('Syncular worker client must be opened before sync');
974
- }
975
- assertSyncularRemoteMode(config, request.type);
976
- if (options.refreshAuthHeaders) {
977
- await this.#refreshAuthHeaders({ restartRealtime: false });
978
- }
979
- try {
980
- const result = await this.#requestAndDrain(request);
981
- this.#emitBootstrapChanged(result);
982
- return result;
983
- }
984
- catch (error) {
985
- const shouldRetry = await this.#resolveAuthRetry(error, 'sync');
986
- if (!shouldRetry)
987
- throw error;
988
- await this.#refreshAuthHeaders({ restartRealtime: false });
989
- const result = await this.#requestAndDrain(request);
990
- this.#emitBootstrapChanged(result);
991
- return result;
992
- }
993
- }
994
- async #requestWithAuthRetry(request, operation) {
995
- await this.#refreshAuthHeaders();
996
- try {
997
- return await this.#request(request);
998
- }
999
- catch (error) {
1000
- const shouldRetry = await this.#resolveAuthRetry(error, operation);
1001
- if (!shouldRetry)
1002
- throw error;
1003
- await this.#refreshAuthHeaders();
1004
- return this.#request(request);
1005
- }
1006
- }
1007
- async #refreshAuthHeaders(options = { restartRealtime: true }) {
1008
- if (!this.#getHeaders)
1009
- return;
1010
- const headers = await this.#getHeaders();
1011
- this.#emitDiagnostic({
1012
- level: 'debug',
1013
- source: 'auth',
1014
- code: 'auth.headers_refreshed',
1015
- message: 'Syncular auth headers refreshed',
1016
- details: { headerCount: Object.keys(headers).length },
1017
- });
1018
- await this.#setAuthHeaders(headers, options);
1019
- }
1020
- async #restartRealtime() {
1021
- const realtimeOptions = this.#realtimeOptions;
1022
- const config = this.#config;
1023
- if (!realtimeOptions || !config || this.#closed)
1024
- return;
1025
- await this.#request({
1026
- type: 'startRealtime',
1027
- options: await resolveRealtimeWorkerOptions(realtimeOptions, config),
1028
- });
1029
- }
1030
- async #resolveAuthRetry(error, operation) {
1031
- const status = authStatusFromError(error);
1032
- const lifecycle = this.#authLifecycle;
1033
- if (!status || !lifecycle)
1034
- return false;
1035
- const context = { operation, status };
1036
- this.#emitDiagnostic({
1037
- level: 'warn',
1038
- source: 'auth',
1039
- code: 'auth.expired',
1040
- message: `Syncular auth expired during ${operation}`,
1041
- details: context,
1042
- });
1043
- await lifecycle.onAuthExpired?.(context);
1044
- const refreshResult = await this.#refreshAuthSingleFlight(context);
1045
- this.#emitDiagnostic({
1046
- level: refreshResult ? 'info' : 'warn',
1047
- source: 'auth',
1048
- code: refreshResult ? 'auth.refresh_succeeded' : 'auth.refresh_failed',
1049
- message: refreshResult
1050
- ? 'Syncular auth refresh succeeded'
1051
- : 'Syncular auth refresh did not produce fresh credentials',
1052
- details: context,
1053
- });
1054
- if (lifecycle.retryWithFreshToken) {
1055
- return Boolean(await lifecycle.retryWithFreshToken({ ...context, refreshResult }));
1056
- }
1057
- return refreshResult;
1058
- }
1059
- #refreshAuthSingleFlight(context) {
1060
- const refreshToken = this.#authLifecycle?.refreshToken;
1061
- if (!refreshToken)
1062
- return Promise.resolve(false);
1063
- this.#authRefreshInFlight ??= Promise.resolve(refreshToken(context))
1064
- .then(Boolean)
1065
- .finally(() => {
1066
- this.#authRefreshInFlight = undefined;
1067
- });
1068
- return this.#authRefreshInFlight;
1069
- }
1070
- async #emitLiveEvents() {
1071
- const events = await this.drainLiveQueryEvents();
1072
- for (const event of events) {
1073
- this.#liveListeners.get(event.queryId)?.(event);
1074
- }
1075
- }
1076
- #request(request) {
1077
- if (this.#closed && request.type !== 'close') {
1078
- return Promise.reject(new SyncularWorkerError(createSyncularWorkerErrorPayload('worker.closed', 'Syncular worker client is closed')));
1079
- }
1080
- const id = this.#nextId++;
1081
- const message = {
1082
- ...request,
1083
- id,
1084
- protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
1085
- };
1086
- return new Promise((resolve, reject) => {
1087
- const requestTimeoutMs = this.#timeoutMsForRequest(request.type);
1088
- const timeout = requestTimeoutMs > 0
1089
- ? setTimeout(() => {
1090
- this.#pending.delete(id);
1091
- this.#sendCancel(id);
1092
- this.#emitDiagnostic({
1093
- level: 'warn',
1094
- source: 'worker',
1095
- code: 'worker.request_timeout',
1096
- message: `Syncular worker request ${request.type} timed out`,
1097
- details: {
1098
- requestId: id,
1099
- requestType: request.type,
1100
- timeoutMs: requestTimeoutMs,
1101
- },
1102
- });
1103
- const error = new SyncularWorkerError(createSyncularWorkerErrorPayload('worker.request_timeout', `Syncular worker request ${request.type} timed out after ${requestTimeoutMs}ms`, { details: { requestId: id, requestType: request.type } }));
1104
- this.#lastError = {
1105
- message: error.message,
1106
- code: error.code,
1107
- };
1108
- this.#emitLifecycleChanged();
1109
- reject(error);
1110
- }, requestTimeoutMs)
1111
- : undefined;
1112
- this.#pending.set(id, {
1113
- type: request.type,
1114
- timeout,
1115
- resolve: (value) => resolve(value),
1116
- reject,
1117
- });
1118
- this.#emitLifecycleChanged();
1119
- this.worker.postMessage(message);
1120
- });
1121
- }
1122
- #timeoutMsForRequest(type) {
1123
- return syncularWorkerRequestTimeoutMs(this.#requestTimeouts, type);
1124
- }
1125
- #handleWorkerMessage(message) {
1126
- if (message.protocolVersion !== SYNCULAR_WORKER_PROTOCOL_VERSION) {
1127
- this.#rejectAll(createSyncularWorkerErrorPayload('worker.protocol_mismatch', `Unsupported Syncular worker protocol ${message.protocolVersion}`, { details: { supported: SYNCULAR_WORKER_PROTOCOL_VERSION } }));
1128
- return;
1129
- }
1130
- if (isWorkerEvent(message)) {
1131
- this.#handleWorkerEvent(message);
1132
- return;
1133
- }
1134
- this.#handleResponse(message);
1135
- }
1136
- #handleResponse(response) {
1137
- const pending = this.#pending.get(response.id);
1138
- if (!pending)
1139
- return;
1140
- this.#pending.delete(response.id);
1141
- if (pending.timeout)
1142
- clearTimeout(pending.timeout);
1143
- if (response.ok) {
1144
- pending.resolve(response.value);
1145
- }
1146
- else {
1147
- const error = new SyncularWorkerError(response.error);
1148
- this.#lastError = {
1149
- message: error.message,
1150
- code: error.code,
1151
- };
1152
- pending.reject(error);
1153
- }
1154
- this.#emitLifecycleChanged();
1155
- }
1156
- #handleWorkerEvent(event) {
1157
- if (event.type === 'liveQueryEvents') {
1158
- for (const liveEvent of event.events) {
1159
- this.#liveListeners.get(liveEvent.queryId)?.(liveEvent);
1160
- }
1161
- return;
1162
- }
1163
- if (event.type === 'rowsChanged') {
1164
- this.#emitRowsChanged({
1165
- source: event.source,
1166
- changedTables: event.changedTables,
1167
- changedRows: event.changedRows,
1168
- changedRowsTruncated: event.changedRowsTruncated,
1169
- });
1170
- return;
1171
- }
1172
- if (event.type === 'bootstrapChanged') {
1173
- this.#lastBootstrap = event.bootstrap;
1174
- if (event.bootstrap.complete) {
1175
- this.#recoveryRequired = false;
1176
- this.#authRequired = false;
1177
- this.#lastError = undefined;
1178
- }
1179
- this.#emitClientEvent('bootstrapChanged', event.bootstrap);
1180
- this.#emitLifecycleChanged();
1181
- return;
1182
- }
1183
- if (event.type === 'realtimeState') {
1184
- this.#realtimeState = event.state;
1185
- this.#emitDiagnostic({
1186
- level: 'info',
1187
- source: 'realtime',
1188
- code: 'realtime.state',
1189
- message: `Syncular realtime is ${event.state}`,
1190
- details: { state: event.state },
1191
- });
1192
- if (event.state === 'connected') {
1193
- this.#rejoinPresence();
1194
- }
1195
- this.#emitLifecycleChanged();
1196
- return;
1197
- }
1198
- if (event.type === 'presenceEvent') {
1199
- this.#applyPresenceEvent(event);
1200
- return;
1201
- }
1202
- this.#emitDiagnostic(event.event);
1203
- }
1204
- #emitClientEvent(event, payload) {
1205
- const listeners = this.#eventListeners.get(event);
1206
- if (!listeners || listeners.size === 0)
1207
- return;
1208
- for (const listener of listeners) {
1209
- try {
1210
- listener(payload);
1211
- }
1212
- catch {
1213
- // Client event listeners must never break sync control flow.
1214
- }
1215
- }
1216
- }
1217
- #emitLifecycleChanged() {
1218
- const state = this.#computeLifecycleState();
1219
- if (this.#lastLifecycleState && sameJson(this.#lastLifecycleState, state)) {
1220
- return;
1221
- }
1222
- this.#lastLifecycleState = state;
1223
- this.#emitClientEvent('lifecycleChanged', state);
1224
- }
1225
- #pendingSyncRequestCount() {
1226
- let count = 0;
1227
- for (const pending of this.#pending.values()) {
1228
- if (pending.type === 'syncOnce' ||
1229
- pending.type === 'syncPull' ||
1230
- pending.type === 'syncPush') {
1231
- count += 1;
1232
- }
1233
- }
1234
- return count;
1235
- }
1236
- #subscribeNetworkEvents() {
1237
- const network = this.#network;
1238
- if (!network?.addEventListener || !network.removeEventListener)
1239
- return;
1240
- const refresh = () => {
1241
- const next = network.isOnline();
1242
- if (next === this.#networkOnline)
1243
- return;
1244
- this.#networkOnline = next;
1245
- this.#emitLifecycleChanged();
1246
- };
1247
- network.addEventListener('online', refresh);
1248
- network.addEventListener('offline', refresh);
1249
- return () => {
1250
- network.removeEventListener?.('online', refresh);
1251
- network.removeEventListener?.('offline', refresh);
1252
- };
1253
- }
1254
- #hasClientEventListeners(event) {
1255
- return (this.#eventListeners.get(event)?.size ?? 0) > 0;
1256
- }
1257
- #hasDiagnosticListeners() {
1258
- return this.#diagnosticListeners.size > 0;
1259
- }
1260
- #emitRowsChanged(event) {
1261
- if (this.#rowsChangedDebounceMs === false ||
1262
- this.#rowsChangedDebounceMs <= 0) {
1263
- this.#deliverRowsChanged(event);
1264
- return;
1265
- }
1266
- if (!this.#pendingRowsChanged) {
1267
- this.#pendingRowsChanged = {
1268
- source: event.source,
1269
- changedTables: new Set(event.changedTables),
1270
- changedRows: [...event.changedRows],
1271
- changedRowsTruncated: event.changedRowsTruncated === true,
1272
- };
1273
- }
1274
- else {
1275
- const pending = this.#pendingRowsChanged;
1276
- pending.source =
1277
- pending.source === event.source ? pending.source : 'mixed';
1278
- for (const table of event.changedTables)
1279
- pending.changedTables.add(table);
1280
- pending.changedRows.push(...event.changedRows);
1281
- pending.changedRowsTruncated ||= event.changedRowsTruncated === true;
1282
- }
1283
- if (this.#rowsChangedDebounceTimer)
1284
- return;
1285
- this.#rowsChangedDebounceTimer = setTimeout(() => {
1286
- this.#rowsChangedDebounceTimer = undefined;
1287
- this.#flushRowsChanged();
1288
- }, this.#rowsChangedDebounceMs);
1289
- }
1290
- #flushRowsChanged() {
1291
- if (this.#rowsChangedDebounceTimer) {
1292
- clearTimeout(this.#rowsChangedDebounceTimer);
1293
- this.#rowsChangedDebounceTimer = undefined;
1294
- }
1295
- const pending = this.#pendingRowsChanged;
1296
- this.#pendingRowsChanged = undefined;
1297
- if (!pending)
1298
- return;
1299
- this.#deliverRowsChanged({
1300
- source: pending.source,
1301
- changedTables: [...pending.changedTables],
1302
- changedRows: pending.changedRows,
1303
- changedRowsTruncated: pending.changedRowsTruncated,
1304
- });
1305
- }
1306
- #deliverRowsChanged(event) {
1307
- for (const listener of this.#rowsChangedListeners) {
1308
- try {
1309
- listener(event);
1310
- }
1311
- catch {
1312
- // Row-change listeners must never break worker event handling.
1313
- }
1314
- }
1315
- this.#emitClientEvent('rowsChanged', event);
1316
- }
1317
- #emitBootstrapChanged(result) {
1318
- this.#lastBootstrap = result.bootstrap;
1319
- this.#recoveryRequired = false;
1320
- this.#authRequired = false;
1321
- this.#lastError = undefined;
1322
- appendSyncularSyncTimings(this.#recentSyncTimings, result.timings);
1323
- this.#emitClientEvent('bootstrapChanged', result.bootstrap);
1324
- this.#emitLifecycleChanged();
1325
- }
1326
- async #emitOperationalState() {
1327
- const observeLifecycle = this.#hasClientEventListeners('lifecycleChanged');
1328
- const observeOutbox = observeLifecycle || this.#hasClientEventListeners('outboxChanged');
1329
- const observeConflicts = observeLifecycle || this.#hasClientEventListeners('conflictsChanged');
1330
- const observeBlobUploads = observeLifecycle || this.#hasClientEventListeners('blobUploadsChanged');
1331
- if (!observeOutbox && !observeConflicts && !observeBlobUploads) {
1332
- return;
1333
- }
1334
- const [outboxStats, conflictStats, blobUploadStats] = await Promise.all([
1335
- observeOutbox
1336
- ? this.#readOutboxStats().catch(() => undefined)
1337
- : Promise.resolve(undefined),
1338
- observeConflicts
1339
- ? this.#readConflictStats().catch(() => undefined)
1340
- : Promise.resolve(undefined),
1341
- observeBlobUploads
1342
- ? this.#readBlobUploadStats().catch(() => undefined)
1343
- : Promise.resolve(undefined),
1344
- ]);
1345
- if (outboxStats && !sameJson(this.#lastOutboxStats, outboxStats)) {
1346
- this.#lastOutboxStats = outboxStats;
1347
- this.#emitClientEvent('outboxChanged', outboxStats);
1348
- }
1349
- if (conflictStats && !sameJson(this.#lastConflictStats, conflictStats)) {
1350
- this.#lastConflictStats = conflictStats;
1351
- this.#emitClientEvent('conflictsChanged', conflictStats);
1352
- }
1353
- if (blobUploadStats &&
1354
- !sameJson(this.#lastBlobUploadStats, blobUploadStats)) {
1355
- this.#lastBlobUploadStats = blobUploadStats;
1356
- this.#emitClientEvent('blobUploadsChanged', blobUploadStats);
1357
- }
1358
- this.#emitLifecycleChanged();
1359
- }
1360
- #readBlobUploadStats() {
1361
- return this.#request({ type: 'blobUploadQueueStats' });
1362
- }
1363
- async #readOutboxStats() {
1364
- const result = await this.#request({
1365
- type: 'executeUnsafeSql',
1366
- sql: 'select status, count(*) as count from sync_outbox_commits group by status',
1367
- params: [],
1368
- });
1369
- const stats = {
1370
- pending: 0,
1371
- sending: 0,
1372
- failed: 0,
1373
- acked: 0,
1374
- total: 0,
1375
- };
1376
- for (const row of result.rows) {
1377
- const count = coerceCount(row.count);
1378
- if (row.status === 'pending')
1379
- stats.pending = count;
1380
- if (row.status === 'sending')
1381
- stats.sending = count;
1382
- if (row.status === 'failed')
1383
- stats.failed = count;
1384
- if (row.status === 'acked')
1385
- stats.acked = count;
1386
- stats.total += count;
1387
- }
1388
- return stats;
1389
- }
1390
- async #readConflictStats() {
1391
- const result = await this.#request({
1392
- type: 'executeUnsafeSql',
1393
- sql: 'select ' +
1394
- 'coalesce(sum(case when resolved_at is null then 1 else 0 end), 0) as unresolved, ' +
1395
- 'coalesce(sum(case when resolved_at is not null then 1 else 0 end), 0) as resolved, ' +
1396
- 'count(*) as total from sync_conflicts',
1397
- params: [],
1398
- });
1399
- const row = result.rows[0];
1400
- return {
1401
- unresolved: coerceCount(row?.unresolved),
1402
- resolved: coerceCount(row?.resolved),
1403
- total: coerceCount(row?.total),
1404
- };
1405
- }
1406
- async #readBlobOutboxRows() {
1407
- const result = await this.#request({
1408
- type: 'executeUnsafeSql',
1409
- sql: 'select hash, size, mime_type, encrypted, key_id, status, error ' +
1410
- 'from sync_blob_outbox order by created_at asc',
1411
- params: [],
1412
- });
1413
- return result.rows;
1414
- }
1415
- #emitBlobUploadEvents(beforeRows, afterRows) {
1416
- const after = new Map(afterRows.map((row) => [row.hash, row]));
1417
- for (const before of beforeRows) {
1418
- const next = after.get(before.hash);
1419
- if (!next) {
1420
- const ref = {
1421
- hash: before.hash,
1422
- size: coerceCount(before.size),
1423
- mimeType: before.mime_type,
1424
- encrypted: before.encrypted === true || before.encrypted === 1,
1425
- ...(before.key_id ? { keyId: before.key_id } : {}),
1426
- };
1427
- this.#emitClientEvent('blobUploadCompleted', {
1428
- ref,
1429
- });
1430
- if (this.#hasDiagnosticListeners()) {
1431
- this.#emitDiagnostic({
1432
- level: 'info',
1433
- source: 'blob',
1434
- code: 'blob.upload_completed',
1435
- message: 'Syncular blob upload completed',
1436
- details: ref,
1437
- });
1438
- }
1439
- continue;
1440
- }
1441
- if (before.status !== 'failed' && next.status === 'failed') {
1442
- const ref = {
1443
- hash: next.hash,
1444
- size: coerceCount(next.size),
1445
- mimeType: next.mime_type,
1446
- encrypted: next.encrypted === true || next.encrypted === 1,
1447
- ...(next.key_id ? { keyId: next.key_id } : {}),
1448
- };
1449
- this.#emitClientEvent('blobUploadFailed', {
1450
- hash: next.hash,
1451
- error: next.error ?? 'Blob upload failed',
1452
- ref,
1453
- });
1454
- if (this.#hasDiagnosticListeners()) {
1455
- this.#emitDiagnostic({
1456
- level: 'warn',
1457
- source: 'blob',
1458
- code: 'blob.upload_failed',
1459
- message: next.error ?? 'Syncular blob upload failed',
1460
- details: {
1461
- ...ref,
1462
- error: next.error ?? 'Blob upload failed',
1463
- },
1464
- });
1465
- }
1466
- }
1467
- }
1468
- }
1469
- #applyPresenceEvent(event) {
1470
- const scopeKey = normalizePresenceScopeKey(event.scopeKey);
1471
- if (!scopeKey)
1472
- return;
1473
- const current = this.#presenceByScopeKey.get(scopeKey) ?? [];
1474
- let next;
1475
- if (event.action === 'snapshot') {
1476
- next = event.entries ?? [];
1477
- }
1478
- else {
1479
- if (!event.clientId || !event.actorId)
1480
- return;
1481
- if (event.action === 'leave') {
1482
- next = current.filter((entry) => entry.clientId !== event.clientId);
1483
- }
1484
- else if (event.action === 'update') {
1485
- const existing = current.find((entry) => entry.clientId === event.clientId);
1486
- if (!existing)
1487
- return;
1488
- next = current.map((entry) => entry.clientId === event.clientId
1489
- ? { ...entry, metadata: event.metadata }
1490
- : entry);
1491
- }
1492
- else {
1493
- const existing = current.find((entry) => entry.clientId === event.clientId);
1494
- next = [
1495
- ...current.filter((entry) => entry.clientId !== event.clientId),
1496
- {
1497
- clientId: event.clientId,
1498
- actorId: event.actorId,
1499
- joinedAt: existing?.joinedAt ?? Date.now(),
1500
- metadata: event.metadata,
1501
- },
1502
- ];
1503
- }
1504
- }
1505
- if (arePresenceEntriesEqual(current, next))
1506
- return;
1507
- if (next.length === 0) {
1508
- this.#presenceByScopeKey.delete(scopeKey);
1509
- }
1510
- else {
1511
- this.#presenceByScopeKey.set(scopeKey, next);
1512
- }
1513
- this.#emitClientEvent('presenceChanged', {
1514
- scopeKey,
1515
- presence: next,
1516
- });
1517
- }
1518
- #rejoinPresence() {
1519
- if (this.#joinedPresence.size === 0)
1520
- return;
1521
- for (const [scopeKey, metadata] of this.#joinedPresence) {
1522
- void this.#request({
1523
- type: 'sendPresence',
1524
- action: 'join',
1525
- scopeKey,
1526
- ...(metadata === undefined ? {} : { metadata }),
1527
- }).catch((error) => {
1528
- this.#emitDiagnostic({
1529
- level: 'warn',
1530
- source: 'realtime',
1531
- code: 'realtime.presence_rejoin_failed',
1532
- message: `Syncular presence rejoin failed: ${errorMessage(error)}`,
1533
- details: { scopeKey },
1534
- });
1535
- });
1536
- }
1537
- }
1538
- #rejectAll(reason) {
1539
- const error = reason instanceof Error ? reason : new SyncularWorkerError(reason);
1540
- this.#lastError = {
1541
- message: error.message,
1542
- ...(error instanceof SyncularWorkerError ? { code: error.code } : {}),
1543
- };
1544
- for (const pending of this.#pending.values()) {
1545
- if (pending.timeout)
1546
- clearTimeout(pending.timeout);
1547
- pending.reject(error);
1548
- }
1549
- this.#pending.clear();
1550
- }
1551
- #sendCancel(requestId) {
1552
- const id = this.#nextId++;
1553
- this.worker.postMessage({
1554
- id,
1555
- protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
1556
- type: 'cancel',
1557
- requestId,
1558
- });
1559
- }
1560
- #emitDiagnostic(event) {
1561
- const diagnostic = {
1562
- at: event.at ?? Date.now(),
1563
- level: event.level,
1564
- source: event.source,
1565
- code: event.code,
1566
- message: event.message,
1567
- ...(event.syncAttemptId ? { syncAttemptId: event.syncAttemptId } : {}),
1568
- ...(event.traceId ? { traceId: event.traceId } : {}),
1569
- ...(event.spanId ? { spanId: event.spanId } : {}),
1570
- ...(event.clientId ? { clientId: event.clientId } : {}),
1571
- ...(event.subscriptionId ? { subscriptionId: event.subscriptionId } : {}),
1572
- ...(event.table ? { table: event.table } : {}),
1573
- ...(event.rowId ? { rowId: event.rowId } : {}),
1574
- ...(event.cursor !== undefined ? { cursor: event.cursor } : {}),
1575
- ...(event.details ? { details: event.details } : {}),
1576
- };
1577
- if (diagnostic.details?.resyncRequired === true) {
1578
- this.#recoveryRequired = true;
1579
- }
1580
- if (diagnostic.code === 'auth.expired' ||
1581
- diagnostic.code === 'auth.refresh_failed' ||
1582
- diagnostic.code === 'sync.auth_required') {
1583
- this.#authRequired = true;
1584
- }
1585
- if (diagnostic.code === 'auth.refresh_succeeded') {
1586
- this.#authRequired = false;
1587
- }
1588
- this.#lastDiagnostic = diagnostic;
1589
- appendSyncularDiagnosticEvent(this.#recentDiagnostics, diagnostic);
1590
- this.#emitLifecycleChanged();
1591
- if (this.#diagnosticListeners.size === 0)
1592
- return;
1593
- for (const listener of this.#diagnosticListeners) {
1594
- try {
1595
- listener(diagnostic);
1596
- }
1597
- catch {
1598
- // Diagnostics must never break client control flow.
1599
- }
1600
- }
1601
- }
1602
- }
1603
- function isOpfsOpenFailure(error) {
1604
- if (!(error instanceof SyncularWorkerError))
1605
- return false;
1606
- const message = errorMessage(error).toLowerCase();
1607
- return (message.includes('opfs') ||
1608
- message.includes('sahpool') ||
1609
- message.includes('sync access handle'));
1610
- }
1611
- function shouldEmitOperationalState(type) {
1612
- return isGeneratedSyncularOperationalStateWorkerRequestType(type);
1613
- }
1614
- function errorMessage(error) {
1615
- return error instanceof Error ? error.message : String(error);
1616
- }
1617
- function normalizePresenceScopeKey(scopeKey) {
1618
- const separator = scopeKey.indexOf('::');
1619
- return separator >= 0 ? scopeKey.slice(separator + 2) : scopeKey;
1620
- }
1621
- function coerceCount(value) {
1622
- const numeric = typeof value === 'number' ? value : Number(value ?? 0);
1623
- return Number.isFinite(numeric) && numeric > 0 ? Math.trunc(numeric) : 0;
1624
- }
1625
- function sameJson(left, right) {
1626
- return JSON.stringify(left) === JSON.stringify(right);
1627
- }
1628
- function cloneAuthHeaders(headers) {
1629
- return Object.fromEntries(Object.entries(headers));
1630
- }
1631
- function serializeRuntimeArtifact(runtime) {
1632
- if (!runtime)
1633
- return undefined;
1634
- return {
1635
- wasmGlueUrl: runtime.wasmGlueUrl == null
1636
- ? undefined
1637
- : runtimeUrlHref(runtime.wasmGlueUrl),
1638
- wasmUrl: runtime.wasmUrl == null ? undefined : runtimeUrlHref(runtime.wasmUrl),
1639
- };
1640
- }
1641
- function runtimeUrlHref(value) {
1642
- if (value instanceof Request)
1643
- return value.url;
1644
- return value instanceof URL ? value.href : value;
1645
- }
1646
- function normalizeFieldEncryptionConfig(config) {
1647
- const keys = {};
1648
- for (const [kid, value] of Object.entries(config.keys)) {
1649
- keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
1650
- }
1651
- return { ...config, keys };
1652
- }
1653
- function normalizeEncryptedCrdtConfig(config) {
1654
- const keys = {};
1655
- for (const [kid, value] of Object.entries(config.keys)) {
1656
- keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
1657
- }
1658
- return { ...config, keys };
1659
- }
1660
- function normalizeBlobEncryptionConfig(config) {
1661
- const keys = {};
1662
- for (const [kid, value] of Object.entries(config.keys)) {
1663
- keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
1664
- }
1665
- return { ...config, keys };
1666
- }
1667
- function arePresenceEntriesEqual(left, right) {
1668
- if (left.length !== right.length)
1669
- return false;
1670
- for (let index = 0; index < left.length; index += 1) {
1671
- const a = left[index];
1672
- const b = right[index];
1673
- if (a.clientId !== b.clientId ||
1674
- a.actorId !== b.actorId ||
1675
- a.joinedAt !== b.joinedAt ||
1676
- JSON.stringify(a.metadata ?? {}) !== JSON.stringify(b.metadata ?? {})) {
1677
- return false;
1678
- }
1679
- }
1680
- return true;
1681
- }
1682
- function bytesToBase64Url(bytes) {
1683
- let binary = '';
1684
- for (const byte of bytes)
1685
- binary += String.fromCharCode(byte);
1686
- return btoa(binary)
1687
- .replace(/\+/g, '-')
1688
- .replace(/\//g, '_')
1689
- .replace(/=+$/g, '');
1690
- }
1691
- async function resolveRealtimeWorkerOptions(options, config) {
1692
- const params = {
1693
- ...(options.params ?? {}),
1694
- ...((await options.getParams?.({ clientId: config.clientId })) ?? {}),
1695
- };
1696
- return {
1697
- ...(options.wsUrl ? { wsUrl: options.wsUrl } : {}),
1698
- ...(Object.keys(params).length > 0 ? { params } : {}),
1699
- ...(options.initialReconnectDelayMs != null
1700
- ? { initialReconnectDelayMs: options.initialReconnectDelayMs }
1701
- : {}),
1702
- ...(options.maxReconnectDelayMs != null
1703
- ? { maxReconnectDelayMs: options.maxReconnectDelayMs }
1704
- : {}),
1705
- ...(options.reconnectBackoffFactor != null
1706
- ? { reconnectBackoffFactor: options.reconnectBackoffFactor }
1707
- : {}),
1708
- ...(options.heartbeatTimeoutMs != null
1709
- ? { heartbeatTimeoutMs: options.heartbeatTimeoutMs }
1710
- : {}),
1711
- };
1712
- }
1713
- function assertSyncularRemoteMode(config, operation) {
1714
- if (isSyncularRemoteMode(config))
1715
- return;
1716
- throw new Error(`Syncular ${operation} requires remote mode; current mode is ${config.mode}`);
1717
- }
1718
- function isWorkerEvent(message) {
1719
- return 'type' in message && !('id' in message);
1720
- }
1721
- function authStatusFromError(error) {
1722
- const details = error instanceof SyncularWorkerError ? error.details : undefined;
1723
- if (details &&
1724
- typeof details === 'object' &&
1725
- 'status' in details &&
1726
- (details.status === 401 || details.status === 403)) {
1727
- return details.status;
1728
- }
1729
- const message = errorMessage(error);
1730
- const match = /\bHTTP (401|403)\b/.exec(message);
1731
- if (!match)
1732
- return undefined;
1733
- return match[1] === '401' ? 401 : 403;
1734
- }
1735
- //# sourceMappingURL=worker-client.js.map