@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.
- package/README.md +227 -709
- package/dist/apply.d.ts +80 -0
- package/dist/apply.js +301 -0
- package/dist/blob.d.ts +127 -0
- package/dist/blob.js +201 -0
- package/dist/bun-database.d.ts +22 -0
- package/dist/bun-database.js +61 -0
- package/dist/client.d.ts +320 -69
- package/dist/client.js +1627 -163
- package/dist/content-type.d.ts +2 -0
- package/dist/content-type.js +2 -0
- package/dist/database.d.ts +35 -80
- package/dist/database.js +32 -947
- package/dist/encryption.d.ts +47 -0
- package/dist/encryption.js +75 -0
- package/dist/errors.d.ts +8 -22
- package/dist/errors.js +10 -207
- package/dist/http.d.ts +32 -0
- package/dist/http.js +196 -0
- package/dist/index.d.ts +28 -16
- package/dist/index.js +25 -12
- package/dist/invalidation.d.ts +69 -0
- package/dist/invalidation.js +84 -0
- package/dist/leader-lock.d.ts +28 -0
- package/dist/leader-lock.js +38 -0
- package/dist/multi-tab.d.ts +134 -0
- package/dist/multi-tab.js +399 -0
- package/dist/node-database.d.ts +41 -0
- package/dist/node-database.js +133 -0
- package/dist/outbox.d.ts +56 -0
- package/dist/outbox.js +140 -0
- package/dist/schema.d.ts +107 -0
- package/dist/schema.js +267 -0
- package/dist/state.d.ts +40 -0
- package/dist/state.js +77 -0
- package/dist/transport.d.ts +44 -0
- package/dist/transport.js +7 -0
- package/dist/wasm-database.d.ts +30 -0
- package/dist/wasm-database.js +170 -0
- package/dist/window.d.ts +64 -0
- package/dist/window.js +0 -0
- package/dist/worker-entry.d.ts +16 -2
- package/dist/worker-entry.js +300 -456
- package/dist/worker-host.d.ts +174 -0
- package/dist/worker-host.js +536 -0
- package/dist/worker-protocol.d.ts +173 -61
- package/dist/worker-protocol.js +7 -16
- package/package.json +45 -112
- package/src/apply.ts +435 -0
- package/src/blob.ts +333 -0
- package/src/bun-database.ts +75 -0
- package/src/client.ts +2247 -277
- package/src/content-type.ts +2 -0
- package/src/database.ts +63 -1389
- package/src/encryption.ts +123 -0
- package/src/errors.ts +11 -265
- package/src/http.ts +254 -0
- package/src/index.ts +25 -20
- package/src/invalidation.ts +128 -0
- package/src/leader-lock.ts +68 -0
- package/src/multi-tab.ts +550 -0
- package/src/node-database.ts +180 -0
- package/src/outbox.ts +206 -0
- package/src/schema.ts +397 -0
- package/src/state.ts +126 -0
- package/src/transport.ts +52 -0
- package/src/wasm-database.ts +279 -0
- package/src/window.ts +0 -0
- package/src/worker-entry.ts +391 -545
- package/src/worker-host.ts +800 -0
- package/src/worker-protocol.ts +204 -99
- package/dist/auth-leases.d.ts +0 -11
- package/dist/auth-leases.d.ts.map +0 -1
- package/dist/auth-leases.js +0 -140
- package/dist/auth-leases.js.map +0 -1
- package/dist/blob-limits.d.ts +0 -15
- package/dist/blob-limits.d.ts.map +0 -1
- package/dist/blob-limits.js +0 -66
- package/dist/blob-limits.js.map +0 -1
- package/dist/bridge-client.d.ts +0 -69
- package/dist/bridge-client.d.ts.map +0 -1
- package/dist/bridge-client.js +0 -282
- package/dist/bridge-client.js.map +0 -1
- package/dist/client-config.d.ts +0 -6
- package/dist/client-config.d.ts.map +0 -1
- package/dist/client-config.js +0 -18
- package/dist/client-config.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/command-history.d.ts +0 -35
- package/dist/command-history.d.ts.map +0 -1
- package/dist/command-history.js +0 -378
- package/dist/command-history.js.map +0 -1
- package/dist/console-diagnostics.d.ts +0 -33
- package/dist/console-diagnostics.d.ts.map +0 -1
- package/dist/console-diagnostics.js +0 -496
- package/dist/console-diagnostics.js.map +0 -1
- package/dist/crdt-yjs/index.d.ts +0 -4
- package/dist/crdt-yjs/index.d.ts.map +0 -1
- package/dist/crdt-yjs/index.js +0 -4
- package/dist/crdt-yjs/index.js.map +0 -1
- package/dist/crdt-yjs/webview-host-facade.d.ts +0 -126
- package/dist/crdt-yjs/webview-host-facade.d.ts.map +0 -1
- package/dist/crdt-yjs/webview-host-facade.js +0 -284
- package/dist/crdt-yjs/webview-host-facade.js.map +0 -1
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +0 -153
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +0 -1
- package/dist/crdt-yjs/yjs-document-field-adapter.js +0 -406
- package/dist/crdt-yjs/yjs-document-field-adapter.js.map +0 -1
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +0 -73
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +0 -1
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js +0 -169
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +0 -1
- package/dist/database.d.ts.map +0 -1
- package/dist/database.js.map +0 -1
- package/dist/diagnostics.d.ts +0 -10
- package/dist/diagnostics.d.ts.map +0 -1
- package/dist/diagnostics.js +0 -83
- package/dist/diagnostics.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/generated-bridge.d.ts +0 -365
- package/dist/generated-bridge.d.ts.map +0 -1
- package/dist/generated-bridge.js +0 -250
- package/dist/generated-bridge.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mutations.d.ts +0 -72
- package/dist/mutations.d.ts.map +0 -1
- package/dist/mutations.js +0 -63
- package/dist/mutations.js.map +0 -1
- package/dist/network.d.ts +0 -3
- package/dist/network.d.ts.map +0 -1
- package/dist/network.js +0 -17
- package/dist/network.js.map +0 -1
- package/dist/react/index.d.ts +0 -169
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -628
- package/dist/react/index.js.map +0 -1
- package/dist/react-native/index.d.ts +0 -35
- package/dist/react-native/index.d.ts.map +0 -1
- package/dist/react-native/index.js +0 -49
- package/dist/react-native/index.js.map +0 -1
- package/dist/runtime-contract.d.ts +0 -13
- package/dist/runtime-contract.d.ts.map +0 -1
- package/dist/runtime-contract.js +0 -24
- package/dist/runtime-contract.js.map +0 -1
- package/dist/rust-client.d.ts +0 -106
- package/dist/rust-client.d.ts.map +0 -1
- package/dist/rust-client.js +0 -938
- package/dist/rust-client.js.map +0 -1
- package/dist/sentry.d.ts +0 -35
- package/dist/sentry.d.ts.map +0 -1
- package/dist/sentry.js +0 -155
- package/dist/sentry.js.map +0 -1
- package/dist/sql-safety.d.ts +0 -3
- package/dist/sql-safety.d.ts.map +0 -1
- package/dist/sql-safety.js +0 -62
- package/dist/sql-safety.js.map +0 -1
- package/dist/syncular-runtime-artifacts.json +0 -61
- package/dist/tauri/index.d.ts +0 -35
- package/dist/tauri/index.d.ts.map +0 -1
- package/dist/tauri/index.js +0 -114
- package/dist/tauri/index.js.map +0 -1
- package/dist/types.d.ts +0 -1096
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/wasm/.syncular-wasm-profile +0 -1
- package/dist/wasm/syncular-runtime-artifact.json +0 -21
- package/dist/wasm/syncular.d.ts +0 -207
- package/dist/wasm/syncular.js +0 -2341
- package/dist/wasm/syncular_bg.wasm +0 -0
- package/dist/wasm/syncular_bg.wasm.d.ts +0 -97
- package/dist/wasm-bindings/runtime-contract.d.ts +0 -22
- package/dist/wasm-bindings/runtime-contract.d.ts.map +0 -1
- package/dist/wasm-bindings/runtime-contract.js +0 -112
- package/dist/wasm-bindings/runtime-contract.js.map +0 -1
- package/dist/wasm-core/.syncular-wasm-profile +0 -1
- package/dist/wasm-core/syncular-runtime-artifact.json +0 -17
- package/dist/wasm-core/syncular.d.ts +0 -162
- package/dist/wasm-core/syncular.js +0 -1847
- package/dist/wasm-core/syncular_bg.wasm +0 -0
- package/dist/wasm-core/syncular_bg.wasm.d.ts +0 -77
- package/dist/wasm-perf/.syncular-wasm-profile +0 -1
- package/dist/wasm-perf/syncular-runtime-artifact.json +0 -21
- package/dist/wasm-perf/syncular.d.ts +0 -207
- package/dist/wasm-perf/syncular.js +0 -2341
- package/dist/wasm-perf/syncular_bg.wasm +0 -0
- package/dist/wasm-perf/syncular_bg.wasm.d.ts +0 -97
- package/dist/wasm-runtime.d.ts +0 -23
- package/dist/wasm-runtime.d.ts.map +0 -1
- package/dist/wasm-runtime.js +0 -69
- package/dist/wasm-runtime.js.map +0 -1
- package/dist/worker-client.d.ts +0 -123
- package/dist/worker-client.d.ts.map +0 -1
- package/dist/worker-client.js +0 -1735
- package/dist/worker-client.js.map +0 -1
- package/dist/worker-entry.d.ts.map +0 -1
- package/dist/worker-entry.js.map +0 -1
- package/dist/worker-protocol.d.ts.map +0 -1
- package/dist/worker-protocol.js.map +0 -1
- package/dist/worker-realtime.d.ts +0 -39
- package/dist/worker-realtime.d.ts.map +0 -1
- package/dist/worker-realtime.js +0 -677
- package/dist/worker-realtime.js.map +0 -1
- package/src/auth-leases.ts +0 -251
- package/src/blob-limits.ts +0 -98
- package/src/bridge-client.ts +0 -512
- package/src/client-config.ts +0 -29
- package/src/command-history.ts +0 -623
- package/src/console-diagnostics.ts +0 -617
- package/src/crdt-yjs/index.ts +0 -3
- package/src/crdt-yjs/webview-host-facade.ts +0 -477
- package/src/crdt-yjs/yjs-document-field-adapter.ts +0 -733
- package/src/crdt-yjs/yjs-prosemirror-bridge.ts +0 -272
- package/src/diagnostics.ts +0 -116
- package/src/generated-bridge.ts +0 -741
- package/src/mutations.ts +0 -168
- package/src/network.ts +0 -32
- package/src/react/index.ts +0 -1036
- package/src/react-native/index.ts +0 -152
- package/src/runtime-contract.ts +0 -48
- package/src/rust-client.ts +0 -1491
- package/src/sentry.ts +0 -215
- package/src/sql-safety.ts +0 -61
- package/src/tauri/index.ts +0 -211
- package/src/types.ts +0 -1397
- package/src/wasm-bindings/generated-wasm-bindings.d.ts +0 -70
- package/src/wasm-bindings/runtime-contract.ts +0 -158
- package/src/wasm-runtime.ts +0 -145
- package/src/worker-client.ts +0 -2289
- package/src/worker-realtime.ts +0 -843
package/src/worker-client.ts
DELETED
|
@@ -1,2289 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BlobRef,
|
|
3
|
-
SyncAuthLeaseIssueRequest,
|
|
4
|
-
SyncAuthOperation,
|
|
5
|
-
SyncOperation,
|
|
6
|
-
} from '@syncular/core';
|
|
7
|
-
import { issueSyncularAuthLease } from './auth-leases';
|
|
8
|
-
import { assertSyncularBlobPayloadLimit } from './blob-limits';
|
|
9
|
-
import {
|
|
10
|
-
isSyncularRemoteMode,
|
|
11
|
-
resolveSyncularClientConfig,
|
|
12
|
-
SYNCULAR_DEFAULT_STORAGE,
|
|
13
|
-
} from './client-config';
|
|
14
|
-
import {
|
|
15
|
-
appendSyncularDiagnosticEvent,
|
|
16
|
-
appendSyncularSyncTimings,
|
|
17
|
-
createSyncularSyncAttempt,
|
|
18
|
-
summarizeSyncularDiagnosticSubscriptions,
|
|
19
|
-
} from './diagnostics';
|
|
20
|
-
import {
|
|
21
|
-
isGeneratedSyncularOperationalStateWorkerRequestType,
|
|
22
|
-
type SyncularGeneratedWorkerRequestInput,
|
|
23
|
-
type SyncularGeneratedWorkerRequestType,
|
|
24
|
-
} from './generated-bridge';
|
|
25
|
-
import { browserSyncularNetworkStatusSource } from './network';
|
|
26
|
-
import { assertSyncularReadonlySql } from './sql-safety';
|
|
27
|
-
import type {
|
|
28
|
-
CreateSyncularDatabaseOptions,
|
|
29
|
-
ResolvedSyncularClientConfig,
|
|
30
|
-
SyncularApplyYjsEnvelopeToPayloadArgs,
|
|
31
|
-
SyncularApplyYjsTextUpdatesArgs,
|
|
32
|
-
SyncularApplyYjsTextUpdatesResult,
|
|
33
|
-
SyncularAuthHeaders,
|
|
34
|
-
SyncularAuthLeaseRecord,
|
|
35
|
-
SyncularBlobCacheStats,
|
|
36
|
-
SyncularBlobEncryptionConfig,
|
|
37
|
-
SyncularBlobStoreOptions,
|
|
38
|
-
SyncularBlobUploadQueueProcessOptions,
|
|
39
|
-
SyncularBlobUploadQueueStats,
|
|
40
|
-
SyncularBootstrapStatus,
|
|
41
|
-
SyncularBuildYjsTextUpdateArgs,
|
|
42
|
-
SyncularBuildYjsTextUpdateResult,
|
|
43
|
-
SyncularClientEventMap,
|
|
44
|
-
SyncularClientEventSink,
|
|
45
|
-
SyncularClientEventType,
|
|
46
|
-
SyncularConflictResolution,
|
|
47
|
-
SyncularConflictStats,
|
|
48
|
-
SyncularConflictSummary,
|
|
49
|
-
SyncularConnectionState,
|
|
50
|
-
SyncularCrdtDocumentSnapshot,
|
|
51
|
-
SyncularCrdtFieldCompactionReceipt,
|
|
52
|
-
SyncularCrdtFieldCompactionRequest,
|
|
53
|
-
SyncularCrdtFieldDescriptor,
|
|
54
|
-
SyncularCrdtFieldMaterialization,
|
|
55
|
-
SyncularCrdtFieldRequest,
|
|
56
|
-
SyncularCrdtFieldTextRequest,
|
|
57
|
-
SyncularCrdtFieldWriteReceipt,
|
|
58
|
-
SyncularCrdtFieldYjsUpdateRequest,
|
|
59
|
-
SyncularCrdtUpdateLogEntry,
|
|
60
|
-
SyncularDiagnosticEvent,
|
|
61
|
-
SyncularDiagnosticSink,
|
|
62
|
-
SyncularDiagnosticSnapshot,
|
|
63
|
-
SyncularEncryptedCrdtConfig,
|
|
64
|
-
SyncularEncryptionHelperMethod,
|
|
65
|
-
SyncularFieldEncryptionConfig,
|
|
66
|
-
SyncularLifecycleState,
|
|
67
|
-
SyncularLiveQueryDependencyHint,
|
|
68
|
-
SyncularLiveQueryDiagnostics,
|
|
69
|
-
SyncularLiveQueryEvent,
|
|
70
|
-
SyncularLiveQuerySnapshot,
|
|
71
|
-
SyncularLocalHealthRepairReport,
|
|
72
|
-
SyncularLocalHealthRepairRequest,
|
|
73
|
-
SyncularLocalHealthReport,
|
|
74
|
-
SyncularLocalSupportBundle,
|
|
75
|
-
SyncularLocalSupportBundleImportReport,
|
|
76
|
-
SyncularLocalSyncResetReport,
|
|
77
|
-
SyncularLocalSyncResetRequest,
|
|
78
|
-
SyncularNetworkStatusSource,
|
|
79
|
-
SyncularOutboxStats,
|
|
80
|
-
SyncularPresenceEntry,
|
|
81
|
-
SyncularPresenceSink,
|
|
82
|
-
SyncularRealtimeConnectionState,
|
|
83
|
-
SyncularRealtimeOptions,
|
|
84
|
-
SyncularRowsChangedSink,
|
|
85
|
-
SyncularRuntimeArtifact,
|
|
86
|
-
SyncularRuntimeClient,
|
|
87
|
-
SyncularRuntimeInfo,
|
|
88
|
-
SyncularSchemaState,
|
|
89
|
-
SyncularSqlResult,
|
|
90
|
-
SyncularStorageCompactionOptions,
|
|
91
|
-
SyncularStorageCompactionReport,
|
|
92
|
-
SyncularStorageFallbackInfo,
|
|
93
|
-
SyncularSubscriptionSpec,
|
|
94
|
-
SyncularSyncRequestOptions,
|
|
95
|
-
SyncularSyncResult,
|
|
96
|
-
SyncularSyncTimings,
|
|
97
|
-
SyncularTransportStats,
|
|
98
|
-
SyncularWorkerRequestTimeoutMs,
|
|
99
|
-
SyncularWorkerRequestTimeouts,
|
|
100
|
-
} from './types';
|
|
101
|
-
import {
|
|
102
|
-
getSyncularRuntimeArtifact,
|
|
103
|
-
selectSyncularRuntimeArtifact,
|
|
104
|
-
} from './wasm-runtime';
|
|
105
|
-
import type {
|
|
106
|
-
SyncularWorkerErrorPayload,
|
|
107
|
-
SyncularWorkerOutboundMessage,
|
|
108
|
-
SyncularWorkerRealtimeOptions,
|
|
109
|
-
SyncularWorkerRequest,
|
|
110
|
-
SyncularWorkerResponse,
|
|
111
|
-
SyncularWorkerRuntimeArtifact,
|
|
112
|
-
} from './worker-protocol';
|
|
113
|
-
import {
|
|
114
|
-
createSyncularWorkerErrorPayload,
|
|
115
|
-
SYNCULAR_WORKER_PROTOCOL_VERSION,
|
|
116
|
-
} from './worker-protocol';
|
|
117
|
-
|
|
118
|
-
type PendingRequest = {
|
|
119
|
-
type: SyncularWorkerRequestInput['type'];
|
|
120
|
-
timeout: ReturnType<typeof setTimeout> | undefined;
|
|
121
|
-
resolve(value: unknown): void;
|
|
122
|
-
reject(reason: unknown): void;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
type BlobOutboxRow = {
|
|
126
|
-
hash: string;
|
|
127
|
-
size: number;
|
|
128
|
-
mime_type: string;
|
|
129
|
-
encrypted: number | boolean;
|
|
130
|
-
key_id: string | null;
|
|
131
|
-
status: string;
|
|
132
|
-
error: string | null;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
type SyncularWorkerRequestInput = SyncularGeneratedWorkerRequestInput;
|
|
136
|
-
|
|
137
|
-
const DEFAULT_SYNCULAR_WORKER_REQUEST_TIMEOUT_MS = 30_000;
|
|
138
|
-
const NO_SYNCULAR_WORKER_REQUEST_TIMEOUT = 0;
|
|
139
|
-
|
|
140
|
-
const SYNCULAR_SYNC_WORKER_REQUEST_TYPES = new Set<string>([
|
|
141
|
-
'forceSubscriptionsBootstrap',
|
|
142
|
-
'syncPull',
|
|
143
|
-
'syncPush',
|
|
144
|
-
'syncOnce',
|
|
145
|
-
]);
|
|
146
|
-
|
|
147
|
-
const SYNCULAR_BLOB_WORKER_REQUEST_TYPES = new Set<string>([
|
|
148
|
-
'storeBlob',
|
|
149
|
-
'retrieveBlob',
|
|
150
|
-
'processBlobUploadQueue',
|
|
151
|
-
]);
|
|
152
|
-
|
|
153
|
-
const SYNCULAR_STORAGE_MAINTENANCE_WORKER_REQUEST_TYPES = new Set<string>([
|
|
154
|
-
'open',
|
|
155
|
-
'compactStorage',
|
|
156
|
-
'exportLocalSupportBundle',
|
|
157
|
-
'importLocalSupportBundle',
|
|
158
|
-
'repairLocalHealth',
|
|
159
|
-
'resetLocalSyncState',
|
|
160
|
-
]);
|
|
161
|
-
|
|
162
|
-
function syncularWorkerRequestTimeoutMs(
|
|
163
|
-
config:
|
|
164
|
-
| SyncularWorkerRequestTimeoutMs
|
|
165
|
-
| SyncularWorkerRequestTimeouts
|
|
166
|
-
| undefined,
|
|
167
|
-
type: SyncularGeneratedWorkerRequestType
|
|
168
|
-
): number {
|
|
169
|
-
if (typeof config === 'number' || config === false) {
|
|
170
|
-
return normalizeSyncularWorkerTimeoutMs(config);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const byType = config?.byType?.[type];
|
|
174
|
-
if (byType !== undefined) return normalizeSyncularWorkerTimeoutMs(byType);
|
|
175
|
-
|
|
176
|
-
if (SYNCULAR_SYNC_WORKER_REQUEST_TYPES.has(type)) {
|
|
177
|
-
return normalizeSyncularWorkerTimeoutMs(config?.syncMs ?? false);
|
|
178
|
-
}
|
|
179
|
-
if (SYNCULAR_BLOB_WORKER_REQUEST_TYPES.has(type)) {
|
|
180
|
-
return normalizeSyncularWorkerTimeoutMs(config?.blobMs ?? false);
|
|
181
|
-
}
|
|
182
|
-
if (SYNCULAR_STORAGE_MAINTENANCE_WORKER_REQUEST_TYPES.has(type)) {
|
|
183
|
-
return normalizeSyncularWorkerTimeoutMs(
|
|
184
|
-
config?.storageMaintenanceMs ?? false
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return normalizeSyncularWorkerTimeoutMs(
|
|
189
|
-
config?.defaultMs ?? DEFAULT_SYNCULAR_WORKER_REQUEST_TIMEOUT_MS
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function normalizeSyncularWorkerTimeoutMs(
|
|
194
|
-
value: SyncularWorkerRequestTimeoutMs
|
|
195
|
-
): number {
|
|
196
|
-
if (value === false) return NO_SYNCULAR_WORKER_REQUEST_TIMEOUT;
|
|
197
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
198
|
-
return NO_SYNCULAR_WORKER_REQUEST_TIMEOUT;
|
|
199
|
-
}
|
|
200
|
-
return Math.trunc(value);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export async function createSyncularWorkerClient(
|
|
204
|
-
options: CreateSyncularDatabaseOptions
|
|
205
|
-
): Promise<SyncularWorkerClient> {
|
|
206
|
-
const config = resolveSyncularClientConfig(options.config);
|
|
207
|
-
const runtime =
|
|
208
|
-
typeof options.runtime === 'string'
|
|
209
|
-
? getSyncularRuntimeArtifact(options.runtime)
|
|
210
|
-
: (options.runtime ??
|
|
211
|
-
selectSyncularRuntimeArtifact(
|
|
212
|
-
options.requiredRuntimeFeatures,
|
|
213
|
-
options.runtimeArtifacts
|
|
214
|
-
));
|
|
215
|
-
const worker =
|
|
216
|
-
typeof options.worker === 'function'
|
|
217
|
-
? options.worker()
|
|
218
|
-
: (options.worker ?? createDefaultSyncularWorker());
|
|
219
|
-
const client = new SyncularWorkerClient(worker, {
|
|
220
|
-
ownsWorker: options.worker == null,
|
|
221
|
-
requestTimeoutMs: options.requestTimeoutMs,
|
|
222
|
-
getHeaders: options.getHeaders,
|
|
223
|
-
authLifecycle: options.authLifecycle,
|
|
224
|
-
diagnostics: options.diagnostics,
|
|
225
|
-
rowsChangedDebounceMs: options.sync?.rowsChangedDebounceMs,
|
|
226
|
-
network:
|
|
227
|
-
options.sync?.network === false
|
|
228
|
-
? undefined
|
|
229
|
-
: (options.sync?.network ?? browserSyncularNetworkStatusSource()),
|
|
230
|
-
blobLimits: options.blobLimits,
|
|
231
|
-
});
|
|
232
|
-
try {
|
|
233
|
-
await client.open(config, runtime);
|
|
234
|
-
} catch (err) {
|
|
235
|
-
if (
|
|
236
|
-
options.config.storage == null &&
|
|
237
|
-
config.storage === SYNCULAR_DEFAULT_STORAGE &&
|
|
238
|
-
isOpfsOpenFailure(err)
|
|
239
|
-
) {
|
|
240
|
-
const fallbackConfig = { ...config, storage: 'indexedDb' as const };
|
|
241
|
-
client.setStorageFallback({
|
|
242
|
-
from: SYNCULAR_DEFAULT_STORAGE,
|
|
243
|
-
to: fallbackConfig.storage,
|
|
244
|
-
reason: errorMessage(err),
|
|
245
|
-
});
|
|
246
|
-
await client.open(fallbackConfig, runtime);
|
|
247
|
-
} else {
|
|
248
|
-
throw err;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return client;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export function getDefaultSyncularWorkerUrl(): URL {
|
|
255
|
-
const runtimeUrl = new URL(import.meta.url);
|
|
256
|
-
const sourceRuntime = runtimeUrl.pathname.endsWith('/src/worker-client.ts');
|
|
257
|
-
return new URL(
|
|
258
|
-
sourceRuntime ? './worker-entry.ts' : './worker-entry.js',
|
|
259
|
-
runtimeUrl
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export function createDefaultSyncularWorker(): Worker {
|
|
264
|
-
return new Worker(getDefaultSyncularWorkerUrl(), {
|
|
265
|
-
type: 'module',
|
|
266
|
-
credentials: 'same-origin',
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export class SyncularWorkerError extends Error {
|
|
271
|
-
readonly code: SyncularWorkerErrorPayload['code'];
|
|
272
|
-
readonly category: SyncularWorkerErrorPayload['category'];
|
|
273
|
-
readonly retryable: boolean;
|
|
274
|
-
readonly recommendedAction: SyncularWorkerErrorPayload['recommendedAction'];
|
|
275
|
-
readonly details: unknown;
|
|
276
|
-
|
|
277
|
-
constructor(payload: SyncularWorkerErrorPayload) {
|
|
278
|
-
super(payload.message);
|
|
279
|
-
this.name = payload.name ?? 'SyncularWorkerError';
|
|
280
|
-
this.code = payload.code;
|
|
281
|
-
this.category = payload.category;
|
|
282
|
-
this.retryable = payload.retryable === true;
|
|
283
|
-
this.recommendedAction = payload.recommendedAction;
|
|
284
|
-
this.details = payload.details;
|
|
285
|
-
if (payload.stack) this.stack = payload.stack;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export class SyncularWorkerClient implements SyncularRuntimeClient {
|
|
290
|
-
#nextId = 1;
|
|
291
|
-
#pending = new Map<number, PendingRequest>();
|
|
292
|
-
#closed = false;
|
|
293
|
-
#requestTimeouts:
|
|
294
|
-
| SyncularWorkerRequestTimeoutMs
|
|
295
|
-
| SyncularWorkerRequestTimeouts
|
|
296
|
-
| undefined;
|
|
297
|
-
#getHeaders: CreateSyncularDatabaseOptions['getHeaders'] | undefined;
|
|
298
|
-
#authHeaders: SyncularAuthHeaders = {};
|
|
299
|
-
#authLifecycle: CreateSyncularDatabaseOptions['authLifecycle'] | undefined;
|
|
300
|
-
#authRefreshInFlight: Promise<boolean> | undefined;
|
|
301
|
-
#config: ResolvedSyncularClientConfig | undefined;
|
|
302
|
-
#realtimeOptions: SyncularRealtimeOptions | undefined;
|
|
303
|
-
#realtimeState: SyncularRealtimeConnectionState = 'disconnected';
|
|
304
|
-
#storageFallback: SyncularStorageFallbackInfo | undefined;
|
|
305
|
-
#lastDiagnostic: SyncularDiagnosticEvent | undefined;
|
|
306
|
-
#lastError: { message: string; code?: string } | undefined;
|
|
307
|
-
#lastLifecycleState: SyncularLifecycleState | undefined;
|
|
308
|
-
#recoveryRequired = false;
|
|
309
|
-
#authRequired = false;
|
|
310
|
-
#subscriptions: SyncularSubscriptionSpec[] = [];
|
|
311
|
-
#lastBootstrap: SyncularBootstrapStatus | undefined;
|
|
312
|
-
#recentDiagnostics: SyncularDiagnosticEvent[] = [];
|
|
313
|
-
#recentSyncTimings: SyncularSyncTimings[] = [];
|
|
314
|
-
#rowsChangedDebounceMs: number | false;
|
|
315
|
-
#rowsChangedDebounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
316
|
-
#pendingRowsChanged:
|
|
317
|
-
| {
|
|
318
|
-
source: string;
|
|
319
|
-
changedTables: Set<string>;
|
|
320
|
-
changedRows: SyncularSyncResult['changedRows'];
|
|
321
|
-
changedRowsTruncated: boolean;
|
|
322
|
-
}
|
|
323
|
-
| undefined;
|
|
324
|
-
#lastOutboxStats: SyncularOutboxStats | undefined;
|
|
325
|
-
#lastConflictStats: SyncularConflictStats | undefined;
|
|
326
|
-
#lastBlobUploadStats: SyncularBlobUploadQueueStats | undefined;
|
|
327
|
-
#blobLimits: CreateSyncularDatabaseOptions['blobLimits'];
|
|
328
|
-
#network: SyncularNetworkStatusSource | undefined;
|
|
329
|
-
#networkOnline: boolean | undefined;
|
|
330
|
-
#unsubscribeNetwork: (() => void) | undefined;
|
|
331
|
-
#diagnosticListeners = new Set<SyncularDiagnosticSink>();
|
|
332
|
-
#rowsChangedListeners = new Set<SyncularRowsChangedSink>();
|
|
333
|
-
#eventListeners = new Map<
|
|
334
|
-
SyncularClientEventType,
|
|
335
|
-
Set<SyncularClientEventSink<SyncularClientEventType>>
|
|
336
|
-
>();
|
|
337
|
-
#presenceByScopeKey = new Map<string, SyncularPresenceEntry[]>();
|
|
338
|
-
#joinedPresence = new Map<string, Record<string, unknown> | undefined>();
|
|
339
|
-
#liveListeners = new Map<
|
|
340
|
-
string,
|
|
341
|
-
(event: SyncularLiveQueryEvent<Record<string, unknown>>) => void
|
|
342
|
-
>();
|
|
343
|
-
|
|
344
|
-
constructor(
|
|
345
|
-
private readonly worker: Worker,
|
|
346
|
-
options: {
|
|
347
|
-
ownsWorker: boolean;
|
|
348
|
-
requestTimeoutMs?: CreateSyncularDatabaseOptions['requestTimeoutMs'];
|
|
349
|
-
getHeaders?: CreateSyncularDatabaseOptions['getHeaders'];
|
|
350
|
-
authLifecycle?: CreateSyncularDatabaseOptions['authLifecycle'];
|
|
351
|
-
diagnostics?: SyncularDiagnosticSink;
|
|
352
|
-
rowsChangedDebounceMs?: number | false;
|
|
353
|
-
network?: SyncularNetworkStatusSource;
|
|
354
|
-
blobLimits?: CreateSyncularDatabaseOptions['blobLimits'];
|
|
355
|
-
}
|
|
356
|
-
) {
|
|
357
|
-
this.#requestTimeouts = options.requestTimeoutMs;
|
|
358
|
-
this.#getHeaders = options.getHeaders;
|
|
359
|
-
this.#authLifecycle = options.authLifecycle;
|
|
360
|
-
this.#blobLimits = options.blobLimits;
|
|
361
|
-
this.#network = options.network;
|
|
362
|
-
this.#networkOnline = this.#network?.isOnline();
|
|
363
|
-
this.#unsubscribeNetwork = this.#subscribeNetworkEvents();
|
|
364
|
-
this.#rowsChangedDebounceMs =
|
|
365
|
-
options.rowsChangedDebounceMs === false
|
|
366
|
-
? false
|
|
367
|
-
: Math.max(0, options.rowsChangedDebounceMs ?? 0);
|
|
368
|
-
if (options.diagnostics) {
|
|
369
|
-
this.#diagnosticListeners.add(options.diagnostics);
|
|
370
|
-
}
|
|
371
|
-
this.worker.onmessage = (
|
|
372
|
-
event: MessageEvent<SyncularWorkerOutboundMessage>
|
|
373
|
-
) => {
|
|
374
|
-
this.#handleWorkerMessage(event.data);
|
|
375
|
-
};
|
|
376
|
-
this.worker.onerror = (event) => {
|
|
377
|
-
const payload = createSyncularWorkerErrorPayload(
|
|
378
|
-
'worker.failed',
|
|
379
|
-
event.message || 'Syncular worker failed'
|
|
380
|
-
);
|
|
381
|
-
this.#emitDiagnostic({
|
|
382
|
-
level: 'error',
|
|
383
|
-
source: 'worker',
|
|
384
|
-
code: 'worker.failed',
|
|
385
|
-
message: payload.message,
|
|
386
|
-
});
|
|
387
|
-
this.#rejectAll(payload);
|
|
388
|
-
};
|
|
389
|
-
this.worker.onmessageerror = () => {
|
|
390
|
-
const payload = createSyncularWorkerErrorPayload(
|
|
391
|
-
'worker.message_unreadable',
|
|
392
|
-
'Syncular worker sent an unreadable message'
|
|
393
|
-
);
|
|
394
|
-
this.#emitDiagnostic({
|
|
395
|
-
level: 'error',
|
|
396
|
-
source: 'worker',
|
|
397
|
-
code: 'worker.message_unreadable',
|
|
398
|
-
message: payload.message,
|
|
399
|
-
});
|
|
400
|
-
this.#rejectAll(payload);
|
|
401
|
-
};
|
|
402
|
-
this.ownsWorker = options.ownsWorker;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
private readonly ownsWorker: boolean;
|
|
406
|
-
|
|
407
|
-
async open(
|
|
408
|
-
config: CreateSyncularDatabaseOptions['config'],
|
|
409
|
-
runtime?: CreateSyncularDatabaseOptions['runtime']
|
|
410
|
-
): Promise<void> {
|
|
411
|
-
const resolvedConfig = resolveSyncularClientConfig(config);
|
|
412
|
-
const resolvedRuntime =
|
|
413
|
-
typeof runtime === 'string'
|
|
414
|
-
? getSyncularRuntimeArtifact(runtime)
|
|
415
|
-
: runtime;
|
|
416
|
-
await this.#request({
|
|
417
|
-
type: 'open',
|
|
418
|
-
config: resolvedConfig,
|
|
419
|
-
runtime: serializeRuntimeArtifact(resolvedRuntime),
|
|
420
|
-
});
|
|
421
|
-
this.#config = resolvedConfig;
|
|
422
|
-
this.#realtimeOptions = undefined;
|
|
423
|
-
this.#subscriptions = [];
|
|
424
|
-
this.#lastBootstrap = undefined;
|
|
425
|
-
await this.#refreshAuthHeaders();
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async setAuthHeaders(headers: SyncularAuthHeaders): Promise<void> {
|
|
429
|
-
await this.#setAuthHeaders(headers, { restartRealtime: true });
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
async issueAuthLease(
|
|
433
|
-
request: SyncAuthLeaseIssueRequest
|
|
434
|
-
): Promise<SyncularAuthLeaseRecord> {
|
|
435
|
-
await this.#refreshAuthHeaders();
|
|
436
|
-
try {
|
|
437
|
-
return await this.#issueAuthLeaseWithCurrentHeaders(request);
|
|
438
|
-
} catch (error) {
|
|
439
|
-
const shouldRetry = await this.#resolveAuthRetry(error, 'authLeaseIssue');
|
|
440
|
-
if (!shouldRetry) throw error;
|
|
441
|
-
await this.#refreshAuthHeaders();
|
|
442
|
-
return this.#issueAuthLeaseWithCurrentHeaders(request);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
async upsertAuthLease(lease: SyncularAuthLeaseRecord): Promise<void> {
|
|
447
|
-
await this.#requestAndDrain({
|
|
448
|
-
type: 'upsertAuthLease',
|
|
449
|
-
lease,
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async authLease(leaseId: string): Promise<SyncularAuthLeaseRecord | null> {
|
|
454
|
-
return this.#request({
|
|
455
|
-
type: 'authLease',
|
|
456
|
-
leaseId,
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
async activeAuthLeases(
|
|
461
|
-
actorId?: string | null,
|
|
462
|
-
nowMs = Date.now()
|
|
463
|
-
): Promise<SyncularAuthLeaseRecord[]> {
|
|
464
|
-
return this.#request({
|
|
465
|
-
type: 'activeAuthLeases',
|
|
466
|
-
actorId: actorId ?? null,
|
|
467
|
-
nowMs: Math.trunc(nowMs),
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async setFieldEncryption(
|
|
472
|
-
config: SyncularFieldEncryptionConfig | null
|
|
473
|
-
): Promise<void> {
|
|
474
|
-
await this.#request({
|
|
475
|
-
type: 'setFieldEncryption',
|
|
476
|
-
config: config == null ? null : normalizeFieldEncryptionConfig(config),
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
async setEncryptedCrdt(
|
|
481
|
-
config: SyncularEncryptedCrdtConfig | null
|
|
482
|
-
): Promise<void> {
|
|
483
|
-
await this.#request({
|
|
484
|
-
type: 'setEncryptedCrdt',
|
|
485
|
-
config: config == null ? null : normalizeEncryptedCrdtConfig(config),
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
async setBlobEncryption(
|
|
490
|
-
config: SyncularBlobEncryptionConfig | null
|
|
491
|
-
): Promise<void> {
|
|
492
|
-
await this.#request({
|
|
493
|
-
type: 'setBlobEncryption',
|
|
494
|
-
config: config == null ? null : normalizeBlobEncryptionConfig(config),
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
async #setAuthHeaders(
|
|
499
|
-
headers: SyncularAuthHeaders,
|
|
500
|
-
options: { restartRealtime: boolean }
|
|
501
|
-
): Promise<void> {
|
|
502
|
-
this.#authHeaders = cloneAuthHeaders(headers);
|
|
503
|
-
await this.#request({
|
|
504
|
-
type: 'setAuthHeaders',
|
|
505
|
-
headers: cloneAuthHeaders(this.#authHeaders),
|
|
506
|
-
});
|
|
507
|
-
if (options.restartRealtime) await this.#restartRealtime();
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
async #issueAuthLeaseWithCurrentHeaders(
|
|
511
|
-
request: SyncAuthLeaseIssueRequest
|
|
512
|
-
): Promise<SyncularAuthLeaseRecord> {
|
|
513
|
-
const config = this.#config;
|
|
514
|
-
if (!config) {
|
|
515
|
-
throw new Error(
|
|
516
|
-
'Syncular worker client must be opened before auth lease issue'
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
assertSyncularRemoteMode(config, 'auth lease issue');
|
|
520
|
-
const lease = await issueSyncularAuthLease({
|
|
521
|
-
baseUrl: config.baseUrl,
|
|
522
|
-
headers: this.#authHeaders,
|
|
523
|
-
request,
|
|
524
|
-
appSchema: config.appSchema,
|
|
525
|
-
});
|
|
526
|
-
await this.upsertAuthLease(lease);
|
|
527
|
-
this.#emitDiagnostic({
|
|
528
|
-
level: 'info',
|
|
529
|
-
source: 'auth',
|
|
530
|
-
code: 'auth_lease.issued',
|
|
531
|
-
message: 'Syncular auth lease issued and stored',
|
|
532
|
-
details: {
|
|
533
|
-
leaseId: lease.leaseId,
|
|
534
|
-
expiresAtMs: lease.expiresAtMs,
|
|
535
|
-
schemaVersion: lease.schemaVersion,
|
|
536
|
-
},
|
|
537
|
-
});
|
|
538
|
-
return lease;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
async startRealtime(
|
|
542
|
-
options: boolean | SyncularRealtimeOptions = {}
|
|
543
|
-
): Promise<void> {
|
|
544
|
-
if (options === false) {
|
|
545
|
-
await this.stopRealtime();
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
const config = this.#config;
|
|
549
|
-
if (!config) {
|
|
550
|
-
throw new Error('Syncular worker client must be opened before realtime');
|
|
551
|
-
}
|
|
552
|
-
assertSyncularRemoteMode(config, 'realtime');
|
|
553
|
-
const realtimeOptions = options === true ? {} : options;
|
|
554
|
-
if (realtimeOptions.enabled === false) {
|
|
555
|
-
await this.stopRealtime();
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
this.#realtimeOptions = realtimeOptions;
|
|
559
|
-
await this.#refreshAuthHeaders({ restartRealtime: false });
|
|
560
|
-
await this.#request({
|
|
561
|
-
type: 'startRealtime',
|
|
562
|
-
options: await resolveRealtimeWorkerOptions(realtimeOptions, config),
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
async stopRealtime(): Promise<void> {
|
|
567
|
-
this.#realtimeOptions = undefined;
|
|
568
|
-
await this.#request({ type: 'stopRealtime' });
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
async setSubscriptions(
|
|
572
|
-
subscriptions: readonly SyncularSubscriptionSpec[]
|
|
573
|
-
): Promise<void> {
|
|
574
|
-
this.#subscriptions = [...subscriptions];
|
|
575
|
-
this.#lastBootstrap = undefined;
|
|
576
|
-
await this.#request({
|
|
577
|
-
type: 'setSubscriptions',
|
|
578
|
-
subscriptions: this.#subscriptions,
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
async forceSubscriptionsBootstrap(
|
|
583
|
-
subscriptionIds: readonly string[] = []
|
|
584
|
-
): Promise<number> {
|
|
585
|
-
return this.#request({
|
|
586
|
-
type: 'forceSubscriptionsBootstrap',
|
|
587
|
-
subscriptionIds: [...subscriptionIds],
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
async executeSql<
|
|
592
|
-
Row extends Record<string, unknown> = Record<string, unknown>,
|
|
593
|
-
>(
|
|
594
|
-
sql: string,
|
|
595
|
-
params: readonly unknown[] = []
|
|
596
|
-
): Promise<SyncularSqlResult<Row>> {
|
|
597
|
-
assertSyncularReadonlySql(sql);
|
|
598
|
-
return this.#request({
|
|
599
|
-
type: 'executeSql',
|
|
600
|
-
sql,
|
|
601
|
-
params: [...params],
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
executeUnsafeSql<
|
|
606
|
-
Row extends Record<string, unknown> = Record<string, unknown>,
|
|
607
|
-
>(
|
|
608
|
-
sql: string,
|
|
609
|
-
params: readonly unknown[] = []
|
|
610
|
-
): Promise<SyncularSqlResult<Row>> {
|
|
611
|
-
return this.#request({
|
|
612
|
-
type: 'executeUnsafeSql',
|
|
613
|
-
sql,
|
|
614
|
-
params: [...params],
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
subscribeQuery<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
619
|
-
sql: string,
|
|
620
|
-
params: readonly unknown[],
|
|
621
|
-
tables: readonly string[],
|
|
622
|
-
hints: readonly SyncularLiveQueryDependencyHint[] = []
|
|
623
|
-
): Promise<SyncularLiveQuerySnapshot<Row>> {
|
|
624
|
-
return this.#request({
|
|
625
|
-
type: 'subscribeQuery',
|
|
626
|
-
sql,
|
|
627
|
-
params: [...params],
|
|
628
|
-
tables: [...tables],
|
|
629
|
-
hints: hints.map((hint) => ({
|
|
630
|
-
table: hint.table,
|
|
631
|
-
rowIds: [...(hint.rowIds ?? [])],
|
|
632
|
-
fields: [...(hint.fields ?? [])],
|
|
633
|
-
})),
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
async unsubscribeQuery(id: string): Promise<void> {
|
|
638
|
-
await this.#request({ type: 'unsubscribeQuery', queryId: id });
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
drainLiveQueryEvents<
|
|
642
|
-
Row extends Record<string, unknown> = Record<string, unknown>,
|
|
643
|
-
>(): Promise<Array<SyncularLiveQueryEvent<Row>>> {
|
|
644
|
-
return this.#request({ type: 'drainLiveQueryEvents' });
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
liveQueryDiagnostics(): Promise<SyncularLiveQueryDiagnostics> {
|
|
648
|
-
return this.#request({ type: 'liveQueryDiagnostics' });
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
async applyMutation(
|
|
652
|
-
operation: SyncOperation,
|
|
653
|
-
localRow?: unknown
|
|
654
|
-
): Promise<string> {
|
|
655
|
-
return this.#requestAndDrain({
|
|
656
|
-
type: 'applyMutation',
|
|
657
|
-
operation,
|
|
658
|
-
localRow,
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
async applyLeasedMutation(
|
|
663
|
-
operation: SyncOperation,
|
|
664
|
-
localRow?: unknown
|
|
665
|
-
): Promise<string> {
|
|
666
|
-
return this.#requestAndDrain({
|
|
667
|
-
type: 'applyLeasedMutation',
|
|
668
|
-
operation,
|
|
669
|
-
localRow,
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
async applyMutationsBatch(
|
|
674
|
-
operations: Array<{ operation: SyncOperation; localRow?: unknown | null }>
|
|
675
|
-
): Promise<string[]> {
|
|
676
|
-
return this.#requestAndDrain({
|
|
677
|
-
type: 'applyMutationsBatch',
|
|
678
|
-
operations,
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
async applyMutationsCommit(
|
|
683
|
-
operations: Array<{ operation: SyncOperation; localRow?: unknown | null }>
|
|
684
|
-
): Promise<string> {
|
|
685
|
-
return this.#requestAndDrain({
|
|
686
|
-
type: 'applyMutationsCommit',
|
|
687
|
-
operations,
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
async applyLeasedMutationsCommit(
|
|
692
|
-
operations: Array<{ operation: SyncOperation; localRow?: unknown | null }>
|
|
693
|
-
): Promise<string> {
|
|
694
|
-
return this.#requestAndDrain({
|
|
695
|
-
type: 'applyLeasedMutationsCommit',
|
|
696
|
-
operations,
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
async syncPull(
|
|
701
|
-
options: SyncularSyncRequestOptions = {}
|
|
702
|
-
): Promise<SyncularSyncResult> {
|
|
703
|
-
return this.#syncWithAuthRetry({
|
|
704
|
-
type: 'syncPull',
|
|
705
|
-
syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
async syncPush(
|
|
710
|
-
options: SyncularSyncRequestOptions = {}
|
|
711
|
-
): Promise<SyncularSyncResult> {
|
|
712
|
-
return this.#syncWithAuthRetry({
|
|
713
|
-
type: 'syncPush',
|
|
714
|
-
syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
async syncOnce(
|
|
719
|
-
options: SyncularSyncRequestOptions = {}
|
|
720
|
-
): Promise<SyncularSyncResult> {
|
|
721
|
-
return this.#syncWithAuthRetry({
|
|
722
|
-
type: 'syncOnce',
|
|
723
|
-
syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
async resumeFromBackground(
|
|
728
|
-
options: SyncularSyncRequestOptions = {}
|
|
729
|
-
): Promise<SyncularSyncResult> {
|
|
730
|
-
this.#recoveryRequired = true;
|
|
731
|
-
this.#emitDiagnostic({
|
|
732
|
-
level: 'info',
|
|
733
|
-
source: 'client',
|
|
734
|
-
code: 'lifecycle.resume_from_background',
|
|
735
|
-
message: 'Syncular resumed from background',
|
|
736
|
-
});
|
|
737
|
-
await this.#refreshAuthHeaders({ restartRealtime: false });
|
|
738
|
-
await this.#restartRealtime();
|
|
739
|
-
return this.#syncWithAuthRetry(
|
|
740
|
-
{
|
|
741
|
-
type: 'syncOnce',
|
|
742
|
-
syncAttempt: options.syncAttempt ?? createSyncularSyncAttempt(),
|
|
743
|
-
},
|
|
744
|
-
{ refreshAuthHeaders: false }
|
|
745
|
-
);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
transportStats(): Promise<SyncularTransportStats> {
|
|
749
|
-
return this.#request({ type: 'transportStats' });
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
async resetTransportStats(): Promise<void> {
|
|
753
|
-
await this.#request({ type: 'resetTransportStats' });
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
conflictSummaries(): Promise<SyncularConflictSummary[]> {
|
|
757
|
-
return this.#request({ type: 'conflictSummaries' });
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
retryConflictKeepLocal(id: string): Promise<string> {
|
|
761
|
-
return this.#requestAndDrain({
|
|
762
|
-
type: 'retryConflictKeepLocal',
|
|
763
|
-
conflictId: id,
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
async resolveConflict(
|
|
768
|
-
id: string,
|
|
769
|
-
resolution: SyncularConflictResolution
|
|
770
|
-
): Promise<void> {
|
|
771
|
-
await this.#requestAndDrain({
|
|
772
|
-
type: 'resolveConflict',
|
|
773
|
-
conflictId: id,
|
|
774
|
-
resolution,
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
listTable<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
779
|
-
table: string
|
|
780
|
-
): Promise<Row[]> {
|
|
781
|
-
return this.#request({ type: 'listTable', table });
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
storeBlob(
|
|
785
|
-
data: Uint8Array,
|
|
786
|
-
options?: SyncularBlobStoreOptions
|
|
787
|
-
): Promise<BlobRef> {
|
|
788
|
-
try {
|
|
789
|
-
assertSyncularBlobPayloadLimit({
|
|
790
|
-
operation: 'store',
|
|
791
|
-
size: data.byteLength,
|
|
792
|
-
limits: this.#blobLimits,
|
|
793
|
-
options,
|
|
794
|
-
diagnostics: (event) => this.#emitDiagnostic(event),
|
|
795
|
-
});
|
|
796
|
-
} catch (error) {
|
|
797
|
-
return Promise.reject(error);
|
|
798
|
-
}
|
|
799
|
-
const request = { type: 'storeBlob' as const, data, options };
|
|
800
|
-
const result = options?.immediate
|
|
801
|
-
? this.#requestWithAuthRetry<BlobRef>(request, 'blobInitiateUpload')
|
|
802
|
-
: this.#request<BlobRef>(request);
|
|
803
|
-
return result.then((ref) => {
|
|
804
|
-
if (options?.immediate) {
|
|
805
|
-
this.#emitClientEvent('blobUploadCompleted', { ref });
|
|
806
|
-
}
|
|
807
|
-
void this.#emitOperationalState();
|
|
808
|
-
return ref;
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
async retrieveBlob(ref: BlobRef): Promise<Uint8Array> {
|
|
813
|
-
assertSyncularBlobPayloadLimit({
|
|
814
|
-
operation: 'retrieve',
|
|
815
|
-
size: ref.size,
|
|
816
|
-
limits: this.#blobLimits,
|
|
817
|
-
refHash: ref.hash,
|
|
818
|
-
diagnostics: (event) => this.#emitDiagnostic(event),
|
|
819
|
-
});
|
|
820
|
-
const wasLocal = this.#hasDiagnosticListeners()
|
|
821
|
-
? await this.#request<boolean>({
|
|
822
|
-
type: 'isBlobLocal',
|
|
823
|
-
hash: ref.hash,
|
|
824
|
-
}).catch(() => undefined)
|
|
825
|
-
: undefined;
|
|
826
|
-
try {
|
|
827
|
-
const bytes = await this.#requestWithAuthRetry<Uint8Array>(
|
|
828
|
-
{ type: 'retrieveBlob', ref },
|
|
829
|
-
'blobGetDownloadUrl'
|
|
830
|
-
);
|
|
831
|
-
if (wasLocal !== undefined) {
|
|
832
|
-
this.#emitDiagnostic({
|
|
833
|
-
level: 'info',
|
|
834
|
-
source: 'blob',
|
|
835
|
-
code: wasLocal ? 'blob.cache_hit' : 'blob.cache_miss',
|
|
836
|
-
message: wasLocal
|
|
837
|
-
? 'Syncular blob served from local cache'
|
|
838
|
-
: 'Syncular blob fetched after local cache miss',
|
|
839
|
-
details: {
|
|
840
|
-
hash: ref.hash,
|
|
841
|
-
size: ref.size,
|
|
842
|
-
mimeType: ref.mimeType,
|
|
843
|
-
encrypted: ref.encrypted === true,
|
|
844
|
-
...(ref.keyId ? { keyId: ref.keyId } : {}),
|
|
845
|
-
},
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
return bytes;
|
|
849
|
-
} catch (error) {
|
|
850
|
-
if (this.#hasDiagnosticListeners()) {
|
|
851
|
-
this.#emitDiagnostic({
|
|
852
|
-
level: 'warn',
|
|
853
|
-
source: 'blob',
|
|
854
|
-
code: 'blob.download_failed',
|
|
855
|
-
message: `Syncular blob download failed: ${errorMessage(error)}`,
|
|
856
|
-
details: {
|
|
857
|
-
hash: ref.hash,
|
|
858
|
-
size: ref.size,
|
|
859
|
-
mimeType: ref.mimeType,
|
|
860
|
-
encrypted: ref.encrypted === true,
|
|
861
|
-
...(ref.keyId ? { keyId: ref.keyId } : {}),
|
|
862
|
-
},
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
throw error;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
async isBlobLocal(hash: string): Promise<boolean> {
|
|
870
|
-
const local = await this.#request<boolean>({ type: 'isBlobLocal', hash });
|
|
871
|
-
if (this.#hasDiagnosticListeners()) {
|
|
872
|
-
this.#emitDiagnostic({
|
|
873
|
-
level: 'debug',
|
|
874
|
-
source: 'blob',
|
|
875
|
-
code: local ? 'blob.cache_hit' : 'blob.cache_miss',
|
|
876
|
-
message: local
|
|
877
|
-
? 'Syncular blob is available in local cache'
|
|
878
|
-
: 'Syncular blob is not available in local cache',
|
|
879
|
-
details: { hash },
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
return local;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
async processBlobUploadQueue(
|
|
886
|
-
options?: SyncularBlobUploadQueueProcessOptions
|
|
887
|
-
): Promise<{
|
|
888
|
-
uploaded: number;
|
|
889
|
-
failed: number;
|
|
890
|
-
}> {
|
|
891
|
-
const observeBlobEvents =
|
|
892
|
-
this.#hasClientEventListeners('blobUploadCompleted') ||
|
|
893
|
-
this.#hasClientEventListeners('blobUploadFailed') ||
|
|
894
|
-
this.#hasDiagnosticListeners();
|
|
895
|
-
const before = observeBlobEvents
|
|
896
|
-
? await this.#readBlobOutboxRows().catch(() => [])
|
|
897
|
-
: [];
|
|
898
|
-
const result = await this.#requestWithAuthRetry<{
|
|
899
|
-
uploaded: number;
|
|
900
|
-
failed: number;
|
|
901
|
-
}>({ type: 'processBlobUploadQueue', options }, 'blobInitiateUpload');
|
|
902
|
-
if (observeBlobEvents) {
|
|
903
|
-
const after = await this.#readBlobOutboxRows().catch(() => []);
|
|
904
|
-
this.#emitBlobUploadEvents(before, after);
|
|
905
|
-
}
|
|
906
|
-
if (this.#hasDiagnosticListeners()) {
|
|
907
|
-
this.#emitDiagnostic({
|
|
908
|
-
level: result.failed > 0 ? 'warn' : 'info',
|
|
909
|
-
source: 'blob',
|
|
910
|
-
code: 'blob.upload_queue_processed',
|
|
911
|
-
message: 'Syncular blob upload queue processed',
|
|
912
|
-
details: result,
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
void this.#emitOperationalState();
|
|
916
|
-
return result;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
async blobUploadQueueStats(): Promise<SyncularBlobUploadQueueStats> {
|
|
920
|
-
const stats = await this.#readBlobUploadStats();
|
|
921
|
-
this.#lastBlobUploadStats = stats;
|
|
922
|
-
return stats;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
blobCacheStats(): Promise<SyncularBlobCacheStats> {
|
|
926
|
-
return this.#request({ type: 'blobCacheStats' });
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
async pruneBlobCache(maxBytes?: number): Promise<number> {
|
|
930
|
-
const prunedBytes = await this.#request<number>({
|
|
931
|
-
type: 'pruneBlobCache',
|
|
932
|
-
maxBytes,
|
|
933
|
-
});
|
|
934
|
-
if (this.#hasDiagnosticListeners()) {
|
|
935
|
-
this.#emitDiagnostic({
|
|
936
|
-
level: 'info',
|
|
937
|
-
source: 'blob',
|
|
938
|
-
code: 'blob.cache_pruned',
|
|
939
|
-
message: 'Syncular blob cache pruned',
|
|
940
|
-
details: {
|
|
941
|
-
prunedBytes,
|
|
942
|
-
...(maxBytes !== undefined ? { maxBytes } : {}),
|
|
943
|
-
},
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
return prunedBytes;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
async clearBlobCache(): Promise<void> {
|
|
950
|
-
await this.#request({ type: 'clearBlobCache' });
|
|
951
|
-
if (this.#hasDiagnosticListeners()) {
|
|
952
|
-
this.#emitDiagnostic({
|
|
953
|
-
level: 'info',
|
|
954
|
-
source: 'blob',
|
|
955
|
-
code: 'blob.cache_cleared',
|
|
956
|
-
message: 'Syncular blob cache cleared',
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
compactStorage(
|
|
962
|
-
options: SyncularStorageCompactionOptions = {}
|
|
963
|
-
): Promise<SyncularStorageCompactionReport> {
|
|
964
|
-
return this.#request({ type: 'compactStorage', options });
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
generatedSchemaState(): Promise<SyncularSchemaState> {
|
|
968
|
-
return this.#request({ type: 'generatedSchemaState' });
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
localHealthCheck(): Promise<SyncularLocalHealthReport> {
|
|
972
|
-
return this.#request({ type: 'localHealthCheck' });
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
exportLocalSupportBundle(): Promise<SyncularLocalSupportBundle> {
|
|
976
|
-
return this.#request({ type: 'exportLocalSupportBundle' });
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
importLocalSupportBundle(
|
|
980
|
-
bundle: SyncularLocalSupportBundle | string
|
|
981
|
-
): Promise<SyncularLocalSupportBundleImportReport> {
|
|
982
|
-
return this.#request({
|
|
983
|
-
type: 'importLocalSupportBundle',
|
|
984
|
-
bundleJson: typeof bundle === 'string' ? bundle : JSON.stringify(bundle),
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
async repairLocalHealth(
|
|
989
|
-
request: SyncularLocalHealthRepairRequest
|
|
990
|
-
): Promise<SyncularLocalHealthRepairReport> {
|
|
991
|
-
const message = {
|
|
992
|
-
type: 'repairLocalHealth',
|
|
993
|
-
request: {
|
|
994
|
-
action: request.action,
|
|
995
|
-
subscriptionIds: [...(request.subscriptionIds ?? [])],
|
|
996
|
-
tables: [...(request.tables ?? [])],
|
|
997
|
-
},
|
|
998
|
-
} satisfies SyncularWorkerRequestInput;
|
|
999
|
-
const result =
|
|
1000
|
-
request.action === 'clearOrphanedSyncedRows'
|
|
1001
|
-
? await this.#requestAndDrain<SyncularLocalHealthRepairReport>(message)
|
|
1002
|
-
: await this.#request<SyncularLocalHealthRepairReport>(message);
|
|
1003
|
-
if (request.action === 'clearOrphanedSyncedRows') {
|
|
1004
|
-
this.#emitLifecycleChanged();
|
|
1005
|
-
}
|
|
1006
|
-
return result;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
async resetLocalSyncState(
|
|
1010
|
-
request: SyncularLocalSyncResetRequest = {}
|
|
1011
|
-
): Promise<SyncularLocalSyncResetReport> {
|
|
1012
|
-
const normalized = {
|
|
1013
|
-
subscriptionIds: [...(request.subscriptionIds ?? [])],
|
|
1014
|
-
clearSyncedRows: request.clearSyncedRows === true,
|
|
1015
|
-
};
|
|
1016
|
-
const result = await this.#requestAndDrain<SyncularLocalSyncResetReport>({
|
|
1017
|
-
type: 'resetLocalSyncState',
|
|
1018
|
-
request: normalized,
|
|
1019
|
-
});
|
|
1020
|
-
this.#lastBootstrap = undefined;
|
|
1021
|
-
this.#emitLifecycleChanged();
|
|
1022
|
-
return result;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
buildYjsTextUpdate(
|
|
1026
|
-
args: SyncularBuildYjsTextUpdateArgs
|
|
1027
|
-
): Promise<SyncularBuildYjsTextUpdateResult> {
|
|
1028
|
-
return this.#request({ type: 'buildYjsTextUpdate', args });
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
applyYjsTextUpdates(
|
|
1032
|
-
args: SyncularApplyYjsTextUpdatesArgs
|
|
1033
|
-
): Promise<SyncularApplyYjsTextUpdatesResult> {
|
|
1034
|
-
return this.#request({ type: 'applyYjsTextUpdates', args });
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
applyYjsEnvelopeToPayload(
|
|
1038
|
-
args: SyncularApplyYjsEnvelopeToPayloadArgs
|
|
1039
|
-
): Promise<Record<string, unknown>> {
|
|
1040
|
-
return this.#request({ type: 'applyYjsEnvelopeToPayload', args });
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
openCrdtField(
|
|
1044
|
-
request: SyncularCrdtFieldRequest
|
|
1045
|
-
): Promise<SyncularCrdtFieldDescriptor> {
|
|
1046
|
-
return this.#request({ type: 'openCrdtField', request });
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
applyCrdtFieldText(
|
|
1050
|
-
request: SyncularCrdtFieldTextRequest
|
|
1051
|
-
): Promise<SyncularCrdtFieldWriteReceipt> {
|
|
1052
|
-
return this.#requestAndDrain({ type: 'applyCrdtFieldText', request });
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
applyCrdtFieldYjsUpdate(
|
|
1056
|
-
request: SyncularCrdtFieldYjsUpdateRequest
|
|
1057
|
-
): Promise<SyncularCrdtFieldWriteReceipt> {
|
|
1058
|
-
return this.#requestAndDrain({
|
|
1059
|
-
type: 'applyCrdtFieldYjsUpdate',
|
|
1060
|
-
request,
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
materializeCrdtField(
|
|
1065
|
-
request: SyncularCrdtFieldRequest
|
|
1066
|
-
): Promise<SyncularCrdtFieldMaterialization> {
|
|
1067
|
-
return this.#request({ type: 'materializeCrdtField', request });
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
crdtDocumentSnapshot(
|
|
1071
|
-
request: SyncularCrdtFieldRequest
|
|
1072
|
-
): Promise<SyncularCrdtDocumentSnapshot> {
|
|
1073
|
-
return this.#request({ type: 'crdtDocumentSnapshot', request });
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
crdtUpdateLog(
|
|
1077
|
-
request: SyncularCrdtFieldRequest & { limit?: number }
|
|
1078
|
-
): Promise<SyncularCrdtUpdateLogEntry[]> {
|
|
1079
|
-
return this.#request({ type: 'crdtUpdateLog', request });
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
snapshotCrdtFieldStateVector(
|
|
1083
|
-
request: SyncularCrdtFieldRequest
|
|
1084
|
-
): Promise<{ stateVectorBase64: string }> {
|
|
1085
|
-
return this.#request({ type: 'snapshotCrdtFieldStateVector', request });
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
compactCrdtField(
|
|
1089
|
-
request: SyncularCrdtFieldCompactionRequest
|
|
1090
|
-
): Promise<SyncularCrdtFieldCompactionReceipt> {
|
|
1091
|
-
return this.#requestAndDrain({ type: 'compactCrdtField', request });
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
encryptionHelper(
|
|
1095
|
-
method: SyncularEncryptionHelperMethod,
|
|
1096
|
-
args: unknown = {}
|
|
1097
|
-
): Promise<unknown> {
|
|
1098
|
-
return this.#request({ type: 'encryptionHelper', method, args });
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
runtimeInfo(): Promise<SyncularRuntimeInfo> {
|
|
1102
|
-
return this.#request<SyncularRuntimeInfo>({ type: 'runtimeInfo' }).then(
|
|
1103
|
-
(info) => ({
|
|
1104
|
-
...info,
|
|
1105
|
-
storageFallback: this.#storageFallback,
|
|
1106
|
-
})
|
|
1107
|
-
);
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
connectionState(): SyncularConnectionState {
|
|
1111
|
-
return {
|
|
1112
|
-
closed: this.#closed,
|
|
1113
|
-
pendingRequests: this.#pending.size,
|
|
1114
|
-
realtime: this.#realtimeState,
|
|
1115
|
-
...(this.#storageFallback
|
|
1116
|
-
? { storageFallback: this.#storageFallback }
|
|
1117
|
-
: {}),
|
|
1118
|
-
...(this.#lastDiagnostic ? { lastDiagnostic: this.#lastDiagnostic } : {}),
|
|
1119
|
-
...(this.#lastError ? { lastError: this.#lastError } : {}),
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
lifecycleState(): SyncularLifecycleState {
|
|
1124
|
-
return this.#computeLifecycleState();
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
#computeLifecycleState(): SyncularLifecycleState {
|
|
1128
|
-
const outbox = this.#lastOutboxStats;
|
|
1129
|
-
const conflicts = this.#lastConflictStats;
|
|
1130
|
-
const blobUploads = this.#lastBlobUploadStats;
|
|
1131
|
-
const bootstrap = this.#lastBootstrap
|
|
1132
|
-
? {
|
|
1133
|
-
complete: this.#lastBootstrap.complete,
|
|
1134
|
-
criticalReady: this.#lastBootstrap.criticalReady,
|
|
1135
|
-
interactiveReady: this.#lastBootstrap.interactiveReady,
|
|
1136
|
-
isBootstrapping: this.#lastBootstrap.isBootstrapping,
|
|
1137
|
-
progressPercent: this.#lastBootstrap.progressPercent,
|
|
1138
|
-
}
|
|
1139
|
-
: undefined;
|
|
1140
|
-
const hasFailedOutbox = (outbox?.failed ?? 0) > 0;
|
|
1141
|
-
const hasConflicts = (conflicts?.unresolved ?? 0) > 0;
|
|
1142
|
-
const hasFailedBlobUploads = (blobUploads?.failed ?? 0) > 0;
|
|
1143
|
-
const authRequired =
|
|
1144
|
-
this.#authRequired || this.#lastError?.code === 'sync.auth_required';
|
|
1145
|
-
const offline = this.#networkOnline === false;
|
|
1146
|
-
const pendingSyncRequests = this.#pendingSyncRequestCount();
|
|
1147
|
-
const phase: SyncularLifecycleState['phase'] = this.#closed
|
|
1148
|
-
? 'closed'
|
|
1149
|
-
: authRequired
|
|
1150
|
-
? 'authRequired'
|
|
1151
|
-
: offline
|
|
1152
|
-
? 'offline'
|
|
1153
|
-
: this.#recoveryRequired || bootstrap?.isBootstrapping === true
|
|
1154
|
-
? 'recovering'
|
|
1155
|
-
: pendingSyncRequests > 0
|
|
1156
|
-
? 'syncing'
|
|
1157
|
-
: this.#realtimeState === 'connecting'
|
|
1158
|
-
? 'connecting'
|
|
1159
|
-
: hasFailedOutbox || hasConflicts || hasFailedBlobUploads
|
|
1160
|
-
? 'degraded'
|
|
1161
|
-
: bootstrap?.complete === true
|
|
1162
|
-
? 'complete'
|
|
1163
|
-
: 'offline';
|
|
1164
|
-
|
|
1165
|
-
return {
|
|
1166
|
-
phase,
|
|
1167
|
-
realtime: this.#realtimeState,
|
|
1168
|
-
online: this.#networkOnline ?? this.#realtimeState === 'connected',
|
|
1169
|
-
requiresAction:
|
|
1170
|
-
authRequired || hasFailedOutbox || hasConflicts || hasFailedBlobUploads,
|
|
1171
|
-
pendingRequests: this.#pending.size,
|
|
1172
|
-
...(bootstrap ? { bootstrap } : {}),
|
|
1173
|
-
...(outbox ? { outbox } : {}),
|
|
1174
|
-
...(conflicts ? { conflicts } : {}),
|
|
1175
|
-
...(blobUploads ? { blobUploads } : {}),
|
|
1176
|
-
...(this.#lastDiagnostic ? { lastDiagnostic: this.#lastDiagnostic } : {}),
|
|
1177
|
-
...(this.#lastError ? { lastError: this.#lastError } : {}),
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
async diagnosticSnapshot(): Promise<SyncularDiagnosticSnapshot> {
|
|
1182
|
-
const [runtime, transportStats] = await Promise.all([
|
|
1183
|
-
this.runtimeInfo(),
|
|
1184
|
-
this.transportStats().catch(() => undefined),
|
|
1185
|
-
]);
|
|
1186
|
-
const outboxStats =
|
|
1187
|
-
this.#lastOutboxStats ??
|
|
1188
|
-
(await this.#readOutboxStats().catch(() => undefined));
|
|
1189
|
-
if (outboxStats) this.#lastOutboxStats = outboxStats;
|
|
1190
|
-
const conflictStats =
|
|
1191
|
-
this.#lastConflictStats ??
|
|
1192
|
-
(await this.#readConflictStats().catch(() => undefined));
|
|
1193
|
-
if (conflictStats) this.#lastConflictStats = conflictStats;
|
|
1194
|
-
const blobUploadStats =
|
|
1195
|
-
this.#lastBlobUploadStats ??
|
|
1196
|
-
(await this.blobUploadQueueStats().catch(() => undefined));
|
|
1197
|
-
return {
|
|
1198
|
-
generatedAt: Date.now(),
|
|
1199
|
-
runtime,
|
|
1200
|
-
connection: this.connectionState(),
|
|
1201
|
-
subscriptions: summarizeSyncularDiagnosticSubscriptions(
|
|
1202
|
-
this.#subscriptions,
|
|
1203
|
-
this.#lastBootstrap
|
|
1204
|
-
),
|
|
1205
|
-
recentDiagnostics: [...this.#recentDiagnostics],
|
|
1206
|
-
recentSyncTimings: [...this.#recentSyncTimings],
|
|
1207
|
-
...(this.#lastBootstrap ? { bootstrap: this.#lastBootstrap } : {}),
|
|
1208
|
-
...(transportStats ? { transportStats } : {}),
|
|
1209
|
-
...(outboxStats ? { outboxStats } : {}),
|
|
1210
|
-
...(conflictStats ? { conflictStats } : {}),
|
|
1211
|
-
...(blobUploadStats ? { blobUploadStats } : {}),
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
setStorageFallback(fallback: SyncularStorageFallbackInfo): void {
|
|
1216
|
-
this.#storageFallback = fallback;
|
|
1217
|
-
this.#emitDiagnostic({
|
|
1218
|
-
level: 'warn',
|
|
1219
|
-
source: 'storage',
|
|
1220
|
-
code: 'storage.fallback',
|
|
1221
|
-
message: `Syncular storage fell back from ${fallback.from} to ${fallback.to}`,
|
|
1222
|
-
details: {
|
|
1223
|
-
from: fallback.from,
|
|
1224
|
-
to: fallback.to,
|
|
1225
|
-
reason: fallback.reason,
|
|
1226
|
-
},
|
|
1227
|
-
});
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
addDiagnosticListener(listener: SyncularDiagnosticSink): () => void {
|
|
1231
|
-
this.#diagnosticListeners.add(listener);
|
|
1232
|
-
return () => {
|
|
1233
|
-
this.#diagnosticListeners.delete(listener);
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
addEventListener<T extends SyncularClientEventType>(
|
|
1238
|
-
event: T,
|
|
1239
|
-
listener: SyncularClientEventSink<T>
|
|
1240
|
-
): () => void {
|
|
1241
|
-
const listeners = this.#eventListeners.get(event) ?? new Set();
|
|
1242
|
-
listeners.add(listener as SyncularClientEventSink<SyncularClientEventType>);
|
|
1243
|
-
this.#eventListeners.set(event, listeners);
|
|
1244
|
-
return () => {
|
|
1245
|
-
listeners.delete(
|
|
1246
|
-
listener as SyncularClientEventSink<SyncularClientEventType>
|
|
1247
|
-
);
|
|
1248
|
-
if (listeners.size === 0) this.#eventListeners.delete(event);
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
addRowsChangedListener(listener: SyncularRowsChangedSink): () => void {
|
|
1253
|
-
this.#rowsChangedListeners.add(listener);
|
|
1254
|
-
return () => {
|
|
1255
|
-
this.#rowsChangedListeners.delete(listener);
|
|
1256
|
-
};
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
getPresence<TMetadata = Record<string, unknown>>(
|
|
1260
|
-
scopeKey: string
|
|
1261
|
-
): SyncularPresenceEntry<TMetadata>[] {
|
|
1262
|
-
return (this.#presenceByScopeKey.get(scopeKey) ??
|
|
1263
|
-
[]) as SyncularPresenceEntry<TMetadata>[];
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
joinPresence(scopeKey: string, metadata?: Record<string, unknown>): void {
|
|
1267
|
-
this.#joinedPresence.set(scopeKey, metadata);
|
|
1268
|
-
void this.#request({
|
|
1269
|
-
type: 'sendPresence',
|
|
1270
|
-
action: 'join',
|
|
1271
|
-
scopeKey,
|
|
1272
|
-
...(metadata === undefined ? {} : { metadata }),
|
|
1273
|
-
}).catch((error) => {
|
|
1274
|
-
this.#emitDiagnostic({
|
|
1275
|
-
level: 'warn',
|
|
1276
|
-
source: 'realtime',
|
|
1277
|
-
code: 'realtime.presence_join_failed',
|
|
1278
|
-
message: `Syncular presence join failed: ${errorMessage(error)}`,
|
|
1279
|
-
details: { scopeKey },
|
|
1280
|
-
});
|
|
1281
|
-
});
|
|
1282
|
-
this.#applyPresenceEvent({
|
|
1283
|
-
action: 'join',
|
|
1284
|
-
scopeKey,
|
|
1285
|
-
clientId: this.#config?.clientId ?? '',
|
|
1286
|
-
actorId: this.#config?.actorId ?? '',
|
|
1287
|
-
metadata,
|
|
1288
|
-
});
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
leavePresence(scopeKey: string): void {
|
|
1292
|
-
this.#joinedPresence.delete(scopeKey);
|
|
1293
|
-
void this.#request({
|
|
1294
|
-
type: 'sendPresence',
|
|
1295
|
-
action: 'leave',
|
|
1296
|
-
scopeKey,
|
|
1297
|
-
}).catch((error) => {
|
|
1298
|
-
this.#emitDiagnostic({
|
|
1299
|
-
level: 'warn',
|
|
1300
|
-
source: 'realtime',
|
|
1301
|
-
code: 'realtime.presence_leave_failed',
|
|
1302
|
-
message: `Syncular presence leave failed: ${errorMessage(error)}`,
|
|
1303
|
-
details: { scopeKey },
|
|
1304
|
-
});
|
|
1305
|
-
});
|
|
1306
|
-
this.#applyPresenceEvent({
|
|
1307
|
-
action: 'leave',
|
|
1308
|
-
scopeKey,
|
|
1309
|
-
clientId: this.#config?.clientId ?? '',
|
|
1310
|
-
actorId: this.#config?.actorId ?? '',
|
|
1311
|
-
});
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
updatePresenceMetadata(
|
|
1315
|
-
scopeKey: string,
|
|
1316
|
-
metadata: Record<string, unknown>
|
|
1317
|
-
): void {
|
|
1318
|
-
if (this.#joinedPresence.has(scopeKey)) {
|
|
1319
|
-
this.#joinedPresence.set(scopeKey, metadata);
|
|
1320
|
-
}
|
|
1321
|
-
void this.#request({
|
|
1322
|
-
type: 'sendPresence',
|
|
1323
|
-
action: 'update',
|
|
1324
|
-
scopeKey,
|
|
1325
|
-
metadata,
|
|
1326
|
-
}).catch((error) => {
|
|
1327
|
-
this.#emitDiagnostic({
|
|
1328
|
-
level: 'warn',
|
|
1329
|
-
source: 'realtime',
|
|
1330
|
-
code: 'realtime.presence_update_failed',
|
|
1331
|
-
message: `Syncular presence update failed: ${errorMessage(error)}`,
|
|
1332
|
-
details: { scopeKey },
|
|
1333
|
-
});
|
|
1334
|
-
});
|
|
1335
|
-
this.#applyPresenceEvent({
|
|
1336
|
-
action: 'update',
|
|
1337
|
-
scopeKey,
|
|
1338
|
-
clientId: this.#config?.clientId ?? '',
|
|
1339
|
-
actorId: this.#config?.actorId ?? '',
|
|
1340
|
-
metadata,
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
addPresenceListener<TMetadata = Record<string, unknown>>(
|
|
1345
|
-
listener: SyncularPresenceSink<TMetadata>
|
|
1346
|
-
): () => void {
|
|
1347
|
-
return this.addEventListener(
|
|
1348
|
-
'presenceChanged',
|
|
1349
|
-
listener as SyncularClientEventSink<'presenceChanged'>
|
|
1350
|
-
);
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
addLiveQueryListener(
|
|
1354
|
-
queryId: string,
|
|
1355
|
-
listener: (event: SyncularLiveQueryEvent<Record<string, unknown>>) => void
|
|
1356
|
-
): void {
|
|
1357
|
-
this.#liveListeners.set(queryId, listener);
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
removeLiveQueryListener(queryId: string): void {
|
|
1361
|
-
this.#liveListeners.delete(queryId);
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
async close(): Promise<void> {
|
|
1365
|
-
if (this.#closed) return;
|
|
1366
|
-
this.#closed = true;
|
|
1367
|
-
this.#realtimeState = 'disconnected';
|
|
1368
|
-
this.#unsubscribeNetwork?.();
|
|
1369
|
-
this.#unsubscribeNetwork = undefined;
|
|
1370
|
-
this.#emitLifecycleChanged();
|
|
1371
|
-
try {
|
|
1372
|
-
await this.#request({ type: 'close' });
|
|
1373
|
-
} finally {
|
|
1374
|
-
this.#flushRowsChanged();
|
|
1375
|
-
this.#rejectAll(
|
|
1376
|
-
createSyncularWorkerErrorPayload(
|
|
1377
|
-
'worker.closed',
|
|
1378
|
-
'Syncular worker client closed'
|
|
1379
|
-
)
|
|
1380
|
-
);
|
|
1381
|
-
if (this.ownsWorker) this.worker.terminate();
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
async #requestAndDrain<T>(request: SyncularWorkerRequestInput): Promise<T> {
|
|
1386
|
-
const value = await this.#request<T>(request);
|
|
1387
|
-
await this.#emitLiveEvents();
|
|
1388
|
-
if (shouldEmitOperationalState(request.type)) {
|
|
1389
|
-
void this.#emitOperationalState();
|
|
1390
|
-
}
|
|
1391
|
-
return value;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
async #syncWithAuthRetry(
|
|
1395
|
-
request: Extract<
|
|
1396
|
-
SyncularWorkerRequestInput,
|
|
1397
|
-
{ type: 'syncPull' | 'syncPush' | 'syncOnce' }
|
|
1398
|
-
>,
|
|
1399
|
-
options: { refreshAuthHeaders: boolean } = { refreshAuthHeaders: true }
|
|
1400
|
-
): Promise<SyncularSyncResult> {
|
|
1401
|
-
const config = this.#config;
|
|
1402
|
-
if (!config) {
|
|
1403
|
-
throw new Error('Syncular worker client must be opened before sync');
|
|
1404
|
-
}
|
|
1405
|
-
assertSyncularRemoteMode(config, request.type);
|
|
1406
|
-
if (options.refreshAuthHeaders) {
|
|
1407
|
-
await this.#refreshAuthHeaders({ restartRealtime: false });
|
|
1408
|
-
}
|
|
1409
|
-
try {
|
|
1410
|
-
const result = await this.#requestAndDrain<SyncularSyncResult>(request);
|
|
1411
|
-
this.#emitBootstrapChanged(result);
|
|
1412
|
-
return result;
|
|
1413
|
-
} catch (error) {
|
|
1414
|
-
const shouldRetry = await this.#resolveAuthRetry(error, 'sync');
|
|
1415
|
-
if (!shouldRetry) throw error;
|
|
1416
|
-
await this.#refreshAuthHeaders({ restartRealtime: false });
|
|
1417
|
-
const result = await this.#requestAndDrain<SyncularSyncResult>(request);
|
|
1418
|
-
this.#emitBootstrapChanged(result);
|
|
1419
|
-
return result;
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
async #requestWithAuthRetry<T>(
|
|
1424
|
-
request: SyncularWorkerRequestInput,
|
|
1425
|
-
operation: SyncAuthOperation
|
|
1426
|
-
): Promise<T> {
|
|
1427
|
-
await this.#refreshAuthHeaders();
|
|
1428
|
-
try {
|
|
1429
|
-
return await this.#request<T>(request);
|
|
1430
|
-
} catch (error) {
|
|
1431
|
-
const shouldRetry = await this.#resolveAuthRetry(error, operation);
|
|
1432
|
-
if (!shouldRetry) throw error;
|
|
1433
|
-
await this.#refreshAuthHeaders();
|
|
1434
|
-
return this.#request<T>(request);
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
async #refreshAuthHeaders(
|
|
1439
|
-
options: { restartRealtime: boolean } = { restartRealtime: true }
|
|
1440
|
-
): Promise<void> {
|
|
1441
|
-
if (!this.#getHeaders) return;
|
|
1442
|
-
const headers = await this.#getHeaders();
|
|
1443
|
-
this.#emitDiagnostic({
|
|
1444
|
-
level: 'debug',
|
|
1445
|
-
source: 'auth',
|
|
1446
|
-
code: 'auth.headers_refreshed',
|
|
1447
|
-
message: 'Syncular auth headers refreshed',
|
|
1448
|
-
details: { headerCount: Object.keys(headers).length },
|
|
1449
|
-
});
|
|
1450
|
-
await this.#setAuthHeaders(headers, options);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
async #restartRealtime(): Promise<void> {
|
|
1454
|
-
const realtimeOptions = this.#realtimeOptions;
|
|
1455
|
-
const config = this.#config;
|
|
1456
|
-
if (!realtimeOptions || !config || this.#closed) return;
|
|
1457
|
-
await this.#request({
|
|
1458
|
-
type: 'startRealtime',
|
|
1459
|
-
options: await resolveRealtimeWorkerOptions(realtimeOptions, config),
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
async #resolveAuthRetry(
|
|
1464
|
-
error: unknown,
|
|
1465
|
-
operation: SyncAuthOperation
|
|
1466
|
-
): Promise<boolean> {
|
|
1467
|
-
const status = authStatusFromError(error);
|
|
1468
|
-
const lifecycle = this.#authLifecycle;
|
|
1469
|
-
if (!status || !lifecycle) return false;
|
|
1470
|
-
|
|
1471
|
-
const context = { operation, status };
|
|
1472
|
-
this.#emitDiagnostic({
|
|
1473
|
-
level: 'warn',
|
|
1474
|
-
source: 'auth',
|
|
1475
|
-
code: 'auth.expired',
|
|
1476
|
-
message: `Syncular auth expired during ${operation}`,
|
|
1477
|
-
details: context,
|
|
1478
|
-
});
|
|
1479
|
-
await lifecycle.onAuthExpired?.(context);
|
|
1480
|
-
const refreshResult = await this.#refreshAuthSingleFlight(context);
|
|
1481
|
-
this.#emitDiagnostic({
|
|
1482
|
-
level: refreshResult ? 'info' : 'warn',
|
|
1483
|
-
source: 'auth',
|
|
1484
|
-
code: refreshResult ? 'auth.refresh_succeeded' : 'auth.refresh_failed',
|
|
1485
|
-
message: refreshResult
|
|
1486
|
-
? 'Syncular auth refresh succeeded'
|
|
1487
|
-
: 'Syncular auth refresh did not produce fresh credentials',
|
|
1488
|
-
details: context,
|
|
1489
|
-
});
|
|
1490
|
-
if (lifecycle.retryWithFreshToken) {
|
|
1491
|
-
return Boolean(
|
|
1492
|
-
await lifecycle.retryWithFreshToken({ ...context, refreshResult })
|
|
1493
|
-
);
|
|
1494
|
-
}
|
|
1495
|
-
return refreshResult;
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
#refreshAuthSingleFlight(context: {
|
|
1499
|
-
operation: SyncAuthOperation;
|
|
1500
|
-
status: 401 | 403;
|
|
1501
|
-
}): Promise<boolean> {
|
|
1502
|
-
const refreshToken = this.#authLifecycle?.refreshToken;
|
|
1503
|
-
if (!refreshToken) return Promise.resolve(false);
|
|
1504
|
-
this.#authRefreshInFlight ??= Promise.resolve(refreshToken(context))
|
|
1505
|
-
.then(Boolean)
|
|
1506
|
-
.finally(() => {
|
|
1507
|
-
this.#authRefreshInFlight = undefined;
|
|
1508
|
-
});
|
|
1509
|
-
return this.#authRefreshInFlight;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
async #emitLiveEvents(): Promise<void> {
|
|
1513
|
-
const events = await this.drainLiveQueryEvents<Record<string, unknown>>();
|
|
1514
|
-
for (const event of events) {
|
|
1515
|
-
this.#liveListeners.get(event.queryId)?.(event);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
#request<T>(request: SyncularWorkerRequestInput): Promise<T> {
|
|
1520
|
-
if (this.#closed && request.type !== 'close') {
|
|
1521
|
-
return Promise.reject(
|
|
1522
|
-
new SyncularWorkerError(
|
|
1523
|
-
createSyncularWorkerErrorPayload(
|
|
1524
|
-
'worker.closed',
|
|
1525
|
-
'Syncular worker client is closed'
|
|
1526
|
-
)
|
|
1527
|
-
)
|
|
1528
|
-
);
|
|
1529
|
-
}
|
|
1530
|
-
const id = this.#nextId++;
|
|
1531
|
-
const message = {
|
|
1532
|
-
...request,
|
|
1533
|
-
id,
|
|
1534
|
-
protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
|
|
1535
|
-
} as SyncularWorkerRequest;
|
|
1536
|
-
return new Promise<T>((resolve, reject) => {
|
|
1537
|
-
const requestTimeoutMs = this.#timeoutMsForRequest(request.type);
|
|
1538
|
-
const timeout =
|
|
1539
|
-
requestTimeoutMs > 0
|
|
1540
|
-
? setTimeout(() => {
|
|
1541
|
-
this.#pending.delete(id);
|
|
1542
|
-
this.#sendCancel(id);
|
|
1543
|
-
this.#emitDiagnostic({
|
|
1544
|
-
level: 'warn',
|
|
1545
|
-
source: 'worker',
|
|
1546
|
-
code: 'worker.request_timeout',
|
|
1547
|
-
message: `Syncular worker request ${request.type} timed out`,
|
|
1548
|
-
details: {
|
|
1549
|
-
requestId: id,
|
|
1550
|
-
requestType: request.type,
|
|
1551
|
-
timeoutMs: requestTimeoutMs,
|
|
1552
|
-
},
|
|
1553
|
-
});
|
|
1554
|
-
const error = new SyncularWorkerError(
|
|
1555
|
-
createSyncularWorkerErrorPayload(
|
|
1556
|
-
'worker.request_timeout',
|
|
1557
|
-
`Syncular worker request ${request.type} timed out after ${requestTimeoutMs}ms`,
|
|
1558
|
-
{ details: { requestId: id, requestType: request.type } }
|
|
1559
|
-
)
|
|
1560
|
-
);
|
|
1561
|
-
this.#lastError = {
|
|
1562
|
-
message: error.message,
|
|
1563
|
-
code: error.code,
|
|
1564
|
-
};
|
|
1565
|
-
this.#emitLifecycleChanged();
|
|
1566
|
-
reject(error);
|
|
1567
|
-
}, requestTimeoutMs)
|
|
1568
|
-
: undefined;
|
|
1569
|
-
this.#pending.set(id, {
|
|
1570
|
-
type: request.type,
|
|
1571
|
-
timeout,
|
|
1572
|
-
resolve: (value) => resolve(value as T),
|
|
1573
|
-
reject,
|
|
1574
|
-
});
|
|
1575
|
-
this.#emitLifecycleChanged();
|
|
1576
|
-
this.worker.postMessage(message);
|
|
1577
|
-
});
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
#timeoutMsForRequest(type: SyncularGeneratedWorkerRequestType): number {
|
|
1581
|
-
return syncularWorkerRequestTimeoutMs(this.#requestTimeouts, type);
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
#handleWorkerMessage(message: SyncularWorkerOutboundMessage): void {
|
|
1585
|
-
if (message.protocolVersion !== SYNCULAR_WORKER_PROTOCOL_VERSION) {
|
|
1586
|
-
this.#rejectAll(
|
|
1587
|
-
createSyncularWorkerErrorPayload(
|
|
1588
|
-
'worker.protocol_mismatch',
|
|
1589
|
-
`Unsupported Syncular worker protocol ${message.protocolVersion}`,
|
|
1590
|
-
{ details: { supported: SYNCULAR_WORKER_PROTOCOL_VERSION } }
|
|
1591
|
-
)
|
|
1592
|
-
);
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
if (isWorkerEvent(message)) {
|
|
1596
|
-
this.#handleWorkerEvent(message);
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
this.#handleResponse(message);
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
#handleResponse(response: SyncularWorkerResponse): void {
|
|
1603
|
-
const pending = this.#pending.get(response.id);
|
|
1604
|
-
if (!pending) return;
|
|
1605
|
-
this.#pending.delete(response.id);
|
|
1606
|
-
if (pending.timeout) clearTimeout(pending.timeout);
|
|
1607
|
-
if (response.ok) {
|
|
1608
|
-
pending.resolve(response.value);
|
|
1609
|
-
} else {
|
|
1610
|
-
const error = new SyncularWorkerError(response.error);
|
|
1611
|
-
this.#lastError = {
|
|
1612
|
-
message: error.message,
|
|
1613
|
-
code: error.code,
|
|
1614
|
-
};
|
|
1615
|
-
pending.reject(error);
|
|
1616
|
-
}
|
|
1617
|
-
this.#emitLifecycleChanged();
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
#handleWorkerEvent(
|
|
1621
|
-
event: Exclude<SyncularWorkerOutboundMessage, SyncularWorkerResponse>
|
|
1622
|
-
): void {
|
|
1623
|
-
if (event.type === 'liveQueryEvents') {
|
|
1624
|
-
for (const liveEvent of event.events) {
|
|
1625
|
-
this.#liveListeners.get(liveEvent.queryId)?.(liveEvent);
|
|
1626
|
-
}
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
1629
|
-
if (event.type === 'rowsChanged') {
|
|
1630
|
-
this.#emitRowsChanged({
|
|
1631
|
-
source: event.source,
|
|
1632
|
-
changedTables: event.changedTables,
|
|
1633
|
-
changedRows: event.changedRows,
|
|
1634
|
-
changedRowsTruncated: event.changedRowsTruncated,
|
|
1635
|
-
});
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
if (event.type === 'bootstrapChanged') {
|
|
1639
|
-
this.#lastBootstrap = event.bootstrap;
|
|
1640
|
-
if (event.bootstrap.complete) {
|
|
1641
|
-
this.#recoveryRequired = false;
|
|
1642
|
-
this.#authRequired = false;
|
|
1643
|
-
this.#lastError = undefined;
|
|
1644
|
-
}
|
|
1645
|
-
this.#emitClientEvent('bootstrapChanged', event.bootstrap);
|
|
1646
|
-
this.#emitLifecycleChanged();
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
|
-
if (event.type === 'realtimeState') {
|
|
1650
|
-
this.#realtimeState = event.state;
|
|
1651
|
-
this.#emitDiagnostic({
|
|
1652
|
-
level: 'info',
|
|
1653
|
-
source: 'realtime',
|
|
1654
|
-
code: 'realtime.state',
|
|
1655
|
-
message: `Syncular realtime is ${event.state}`,
|
|
1656
|
-
details: { state: event.state },
|
|
1657
|
-
});
|
|
1658
|
-
if (event.state === 'connected') {
|
|
1659
|
-
this.#rejoinPresence();
|
|
1660
|
-
}
|
|
1661
|
-
this.#emitLifecycleChanged();
|
|
1662
|
-
return;
|
|
1663
|
-
}
|
|
1664
|
-
if (event.type === 'presenceEvent') {
|
|
1665
|
-
this.#applyPresenceEvent(event);
|
|
1666
|
-
return;
|
|
1667
|
-
}
|
|
1668
|
-
this.#emitDiagnostic(event.event);
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
#emitClientEvent<T extends SyncularClientEventType>(
|
|
1672
|
-
event: T,
|
|
1673
|
-
payload: SyncularClientEventMap[T]
|
|
1674
|
-
): void {
|
|
1675
|
-
const listeners = this.#eventListeners.get(event);
|
|
1676
|
-
if (!listeners || listeners.size === 0) return;
|
|
1677
|
-
for (const listener of listeners) {
|
|
1678
|
-
try {
|
|
1679
|
-
listener(payload as SyncularClientEventMap[SyncularClientEventType]);
|
|
1680
|
-
} catch {
|
|
1681
|
-
// Client event listeners must never break sync control flow.
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
#emitLifecycleChanged(): void {
|
|
1687
|
-
const state = this.#computeLifecycleState();
|
|
1688
|
-
if (this.#lastLifecycleState && sameJson(this.#lastLifecycleState, state)) {
|
|
1689
|
-
return;
|
|
1690
|
-
}
|
|
1691
|
-
this.#lastLifecycleState = state;
|
|
1692
|
-
this.#emitClientEvent('lifecycleChanged', state);
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
#pendingSyncRequestCount(): number {
|
|
1696
|
-
let count = 0;
|
|
1697
|
-
for (const pending of this.#pending.values()) {
|
|
1698
|
-
if (
|
|
1699
|
-
pending.type === 'syncOnce' ||
|
|
1700
|
-
pending.type === 'syncPull' ||
|
|
1701
|
-
pending.type === 'syncPush'
|
|
1702
|
-
) {
|
|
1703
|
-
count += 1;
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
return count;
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
#subscribeNetworkEvents(): (() => void) | undefined {
|
|
1710
|
-
const network = this.#network;
|
|
1711
|
-
if (!network?.addEventListener || !network.removeEventListener) return;
|
|
1712
|
-
const refresh = () => {
|
|
1713
|
-
const next = network.isOnline();
|
|
1714
|
-
if (next === this.#networkOnline) return;
|
|
1715
|
-
this.#networkOnline = next;
|
|
1716
|
-
this.#emitLifecycleChanged();
|
|
1717
|
-
};
|
|
1718
|
-
network.addEventListener('online', refresh);
|
|
1719
|
-
network.addEventListener('offline', refresh);
|
|
1720
|
-
return () => {
|
|
1721
|
-
network.removeEventListener?.('online', refresh);
|
|
1722
|
-
network.removeEventListener?.('offline', refresh);
|
|
1723
|
-
};
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
#hasClientEventListeners(event: SyncularClientEventType): boolean {
|
|
1727
|
-
return (this.#eventListeners.get(event)?.size ?? 0) > 0;
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
#hasDiagnosticListeners(): boolean {
|
|
1731
|
-
return this.#diagnosticListeners.size > 0;
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
#emitRowsChanged(event: SyncularClientEventMap['rowsChanged']): void {
|
|
1735
|
-
if (
|
|
1736
|
-
this.#rowsChangedDebounceMs === false ||
|
|
1737
|
-
this.#rowsChangedDebounceMs <= 0
|
|
1738
|
-
) {
|
|
1739
|
-
this.#deliverRowsChanged(event);
|
|
1740
|
-
return;
|
|
1741
|
-
}
|
|
1742
|
-
if (!this.#pendingRowsChanged) {
|
|
1743
|
-
this.#pendingRowsChanged = {
|
|
1744
|
-
source: event.source,
|
|
1745
|
-
changedTables: new Set(event.changedTables),
|
|
1746
|
-
changedRows: [...event.changedRows],
|
|
1747
|
-
changedRowsTruncated: event.changedRowsTruncated === true,
|
|
1748
|
-
};
|
|
1749
|
-
} else {
|
|
1750
|
-
const pending = this.#pendingRowsChanged;
|
|
1751
|
-
pending.source =
|
|
1752
|
-
pending.source === event.source ? pending.source : 'mixed';
|
|
1753
|
-
for (const table of event.changedTables) pending.changedTables.add(table);
|
|
1754
|
-
pending.changedRows.push(...event.changedRows);
|
|
1755
|
-
pending.changedRowsTruncated ||= event.changedRowsTruncated === true;
|
|
1756
|
-
}
|
|
1757
|
-
if (this.#rowsChangedDebounceTimer) return;
|
|
1758
|
-
this.#rowsChangedDebounceTimer = setTimeout(() => {
|
|
1759
|
-
this.#rowsChangedDebounceTimer = undefined;
|
|
1760
|
-
this.#flushRowsChanged();
|
|
1761
|
-
}, this.#rowsChangedDebounceMs);
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
#flushRowsChanged(): void {
|
|
1765
|
-
if (this.#rowsChangedDebounceTimer) {
|
|
1766
|
-
clearTimeout(this.#rowsChangedDebounceTimer);
|
|
1767
|
-
this.#rowsChangedDebounceTimer = undefined;
|
|
1768
|
-
}
|
|
1769
|
-
const pending = this.#pendingRowsChanged;
|
|
1770
|
-
this.#pendingRowsChanged = undefined;
|
|
1771
|
-
if (!pending) return;
|
|
1772
|
-
this.#deliverRowsChanged({
|
|
1773
|
-
source: pending.source,
|
|
1774
|
-
changedTables: [...pending.changedTables],
|
|
1775
|
-
changedRows: pending.changedRows,
|
|
1776
|
-
changedRowsTruncated: pending.changedRowsTruncated,
|
|
1777
|
-
});
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
#deliverRowsChanged(event: SyncularClientEventMap['rowsChanged']): void {
|
|
1781
|
-
for (const listener of this.#rowsChangedListeners) {
|
|
1782
|
-
try {
|
|
1783
|
-
listener(event);
|
|
1784
|
-
} catch {
|
|
1785
|
-
// Row-change listeners must never break worker event handling.
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
this.#emitClientEvent('rowsChanged', event);
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
#emitBootstrapChanged(result: SyncularSyncResult): void {
|
|
1792
|
-
this.#lastBootstrap = result.bootstrap;
|
|
1793
|
-
this.#recoveryRequired = false;
|
|
1794
|
-
this.#authRequired = false;
|
|
1795
|
-
this.#lastError = undefined;
|
|
1796
|
-
appendSyncularSyncTimings(this.#recentSyncTimings, result.timings);
|
|
1797
|
-
this.#emitClientEvent('bootstrapChanged', result.bootstrap);
|
|
1798
|
-
this.#emitLifecycleChanged();
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
async #emitOperationalState(): Promise<void> {
|
|
1802
|
-
const observeLifecycle = this.#hasClientEventListeners('lifecycleChanged');
|
|
1803
|
-
const observeOutbox =
|
|
1804
|
-
observeLifecycle || this.#hasClientEventListeners('outboxChanged');
|
|
1805
|
-
const observeConflicts =
|
|
1806
|
-
observeLifecycle || this.#hasClientEventListeners('conflictsChanged');
|
|
1807
|
-
const observeBlobUploads =
|
|
1808
|
-
observeLifecycle || this.#hasClientEventListeners('blobUploadsChanged');
|
|
1809
|
-
if (!observeOutbox && !observeConflicts && !observeBlobUploads) {
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
const [outboxStats, conflictStats, blobUploadStats] = await Promise.all([
|
|
1813
|
-
observeOutbox
|
|
1814
|
-
? this.#readOutboxStats().catch(() => undefined)
|
|
1815
|
-
: Promise.resolve(undefined),
|
|
1816
|
-
observeConflicts
|
|
1817
|
-
? this.#readConflictStats().catch(() => undefined)
|
|
1818
|
-
: Promise.resolve(undefined),
|
|
1819
|
-
observeBlobUploads
|
|
1820
|
-
? this.#readBlobUploadStats().catch(() => undefined)
|
|
1821
|
-
: Promise.resolve(undefined),
|
|
1822
|
-
]);
|
|
1823
|
-
if (outboxStats && !sameJson(this.#lastOutboxStats, outboxStats)) {
|
|
1824
|
-
this.#lastOutboxStats = outboxStats;
|
|
1825
|
-
this.#emitClientEvent('outboxChanged', outboxStats);
|
|
1826
|
-
}
|
|
1827
|
-
if (conflictStats && !sameJson(this.#lastConflictStats, conflictStats)) {
|
|
1828
|
-
this.#lastConflictStats = conflictStats;
|
|
1829
|
-
this.#emitClientEvent('conflictsChanged', conflictStats);
|
|
1830
|
-
}
|
|
1831
|
-
if (
|
|
1832
|
-
blobUploadStats &&
|
|
1833
|
-
!sameJson(this.#lastBlobUploadStats, blobUploadStats)
|
|
1834
|
-
) {
|
|
1835
|
-
this.#lastBlobUploadStats = blobUploadStats;
|
|
1836
|
-
this.#emitClientEvent('blobUploadsChanged', blobUploadStats);
|
|
1837
|
-
}
|
|
1838
|
-
this.#emitLifecycleChanged();
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
#readBlobUploadStats(): Promise<SyncularBlobUploadQueueStats> {
|
|
1842
|
-
return this.#request({ type: 'blobUploadQueueStats' });
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
async #readOutboxStats(): Promise<SyncularOutboxStats> {
|
|
1846
|
-
const result = await this.#request<
|
|
1847
|
-
SyncularSqlResult<{ status: string; count: number }>
|
|
1848
|
-
>({
|
|
1849
|
-
type: 'executeUnsafeSql',
|
|
1850
|
-
sql: 'select status, count(*) as count from sync_outbox_commits group by status',
|
|
1851
|
-
params: [],
|
|
1852
|
-
});
|
|
1853
|
-
const stats: SyncularOutboxStats = {
|
|
1854
|
-
pending: 0,
|
|
1855
|
-
sending: 0,
|
|
1856
|
-
failed: 0,
|
|
1857
|
-
acked: 0,
|
|
1858
|
-
total: 0,
|
|
1859
|
-
};
|
|
1860
|
-
for (const row of result.rows) {
|
|
1861
|
-
const count = coerceCount(row.count);
|
|
1862
|
-
if (row.status === 'pending') stats.pending = count;
|
|
1863
|
-
if (row.status === 'sending') stats.sending = count;
|
|
1864
|
-
if (row.status === 'failed') stats.failed = count;
|
|
1865
|
-
if (row.status === 'acked') stats.acked = count;
|
|
1866
|
-
stats.total += count;
|
|
1867
|
-
}
|
|
1868
|
-
return stats;
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
async #readConflictStats(): Promise<SyncularConflictStats> {
|
|
1872
|
-
const result = await this.#request<
|
|
1873
|
-
SyncularSqlResult<{
|
|
1874
|
-
unresolved: number;
|
|
1875
|
-
resolved: number;
|
|
1876
|
-
total: number;
|
|
1877
|
-
}>
|
|
1878
|
-
>({
|
|
1879
|
-
type: 'executeUnsafeSql',
|
|
1880
|
-
sql:
|
|
1881
|
-
'select ' +
|
|
1882
|
-
'coalesce(sum(case when resolved_at is null then 1 else 0 end), 0) as unresolved, ' +
|
|
1883
|
-
'coalesce(sum(case when resolved_at is not null then 1 else 0 end), 0) as resolved, ' +
|
|
1884
|
-
'count(*) as total from sync_conflicts',
|
|
1885
|
-
params: [],
|
|
1886
|
-
});
|
|
1887
|
-
const row = result.rows[0];
|
|
1888
|
-
return {
|
|
1889
|
-
unresolved: coerceCount(row?.unresolved),
|
|
1890
|
-
resolved: coerceCount(row?.resolved),
|
|
1891
|
-
total: coerceCount(row?.total),
|
|
1892
|
-
};
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
async #readBlobOutboxRows(): Promise<BlobOutboxRow[]> {
|
|
1896
|
-
const result = await this.#request<SyncularSqlResult<BlobOutboxRow>>({
|
|
1897
|
-
type: 'executeUnsafeSql',
|
|
1898
|
-
sql:
|
|
1899
|
-
'select hash, size, mime_type, encrypted, key_id, status, error ' +
|
|
1900
|
-
'from sync_blob_outbox order by created_at asc',
|
|
1901
|
-
params: [],
|
|
1902
|
-
});
|
|
1903
|
-
return result.rows;
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
#emitBlobUploadEvents(
|
|
1907
|
-
beforeRows: readonly BlobOutboxRow[],
|
|
1908
|
-
afterRows: readonly BlobOutboxRow[]
|
|
1909
|
-
): void {
|
|
1910
|
-
const after = new Map(afterRows.map((row) => [row.hash, row]));
|
|
1911
|
-
for (const before of beforeRows) {
|
|
1912
|
-
const next = after.get(before.hash);
|
|
1913
|
-
if (!next) {
|
|
1914
|
-
const ref = {
|
|
1915
|
-
hash: before.hash,
|
|
1916
|
-
size: coerceCount(before.size),
|
|
1917
|
-
mimeType: before.mime_type,
|
|
1918
|
-
encrypted: before.encrypted === true || before.encrypted === 1,
|
|
1919
|
-
...(before.key_id ? { keyId: before.key_id } : {}),
|
|
1920
|
-
};
|
|
1921
|
-
this.#emitClientEvent('blobUploadCompleted', {
|
|
1922
|
-
ref,
|
|
1923
|
-
});
|
|
1924
|
-
if (this.#hasDiagnosticListeners()) {
|
|
1925
|
-
this.#emitDiagnostic({
|
|
1926
|
-
level: 'info',
|
|
1927
|
-
source: 'blob',
|
|
1928
|
-
code: 'blob.upload_completed',
|
|
1929
|
-
message: 'Syncular blob upload completed',
|
|
1930
|
-
details: ref,
|
|
1931
|
-
});
|
|
1932
|
-
}
|
|
1933
|
-
continue;
|
|
1934
|
-
}
|
|
1935
|
-
if (before.status !== 'failed' && next.status === 'failed') {
|
|
1936
|
-
const ref = {
|
|
1937
|
-
hash: next.hash,
|
|
1938
|
-
size: coerceCount(next.size),
|
|
1939
|
-
mimeType: next.mime_type,
|
|
1940
|
-
encrypted: next.encrypted === true || next.encrypted === 1,
|
|
1941
|
-
...(next.key_id ? { keyId: next.key_id } : {}),
|
|
1942
|
-
};
|
|
1943
|
-
this.#emitClientEvent('blobUploadFailed', {
|
|
1944
|
-
hash: next.hash,
|
|
1945
|
-
error: next.error ?? 'Blob upload failed',
|
|
1946
|
-
ref,
|
|
1947
|
-
});
|
|
1948
|
-
if (this.#hasDiagnosticListeners()) {
|
|
1949
|
-
this.#emitDiagnostic({
|
|
1950
|
-
level: 'warn',
|
|
1951
|
-
source: 'blob',
|
|
1952
|
-
code: 'blob.upload_failed',
|
|
1953
|
-
message: next.error ?? 'Syncular blob upload failed',
|
|
1954
|
-
details: {
|
|
1955
|
-
...ref,
|
|
1956
|
-
error: next.error ?? 'Blob upload failed',
|
|
1957
|
-
},
|
|
1958
|
-
});
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
#applyPresenceEvent(event: {
|
|
1965
|
-
action: 'join' | 'leave' | 'update' | 'snapshot';
|
|
1966
|
-
scopeKey: string;
|
|
1967
|
-
clientId?: string;
|
|
1968
|
-
actorId?: string;
|
|
1969
|
-
metadata?: Record<string, unknown>;
|
|
1970
|
-
entries?: SyncularPresenceEntry[];
|
|
1971
|
-
}): void {
|
|
1972
|
-
const scopeKey = normalizePresenceScopeKey(event.scopeKey);
|
|
1973
|
-
if (!scopeKey) return;
|
|
1974
|
-
const current = this.#presenceByScopeKey.get(scopeKey) ?? [];
|
|
1975
|
-
let next: SyncularPresenceEntry[];
|
|
1976
|
-
if (event.action === 'snapshot') {
|
|
1977
|
-
next = event.entries ?? [];
|
|
1978
|
-
} else {
|
|
1979
|
-
if (!event.clientId || !event.actorId) return;
|
|
1980
|
-
if (event.action === 'leave') {
|
|
1981
|
-
next = current.filter((entry) => entry.clientId !== event.clientId);
|
|
1982
|
-
} else if (event.action === 'update') {
|
|
1983
|
-
const existing = current.find(
|
|
1984
|
-
(entry) => entry.clientId === event.clientId
|
|
1985
|
-
);
|
|
1986
|
-
if (!existing) return;
|
|
1987
|
-
next = current.map((entry) =>
|
|
1988
|
-
entry.clientId === event.clientId
|
|
1989
|
-
? { ...entry, metadata: event.metadata }
|
|
1990
|
-
: entry
|
|
1991
|
-
);
|
|
1992
|
-
} else {
|
|
1993
|
-
const existing = current.find(
|
|
1994
|
-
(entry) => entry.clientId === event.clientId
|
|
1995
|
-
);
|
|
1996
|
-
next = [
|
|
1997
|
-
...current.filter((entry) => entry.clientId !== event.clientId),
|
|
1998
|
-
{
|
|
1999
|
-
clientId: event.clientId,
|
|
2000
|
-
actorId: event.actorId,
|
|
2001
|
-
joinedAt: existing?.joinedAt ?? Date.now(),
|
|
2002
|
-
metadata: event.metadata,
|
|
2003
|
-
},
|
|
2004
|
-
];
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
if (arePresenceEntriesEqual(current, next)) return;
|
|
2008
|
-
if (next.length === 0) {
|
|
2009
|
-
this.#presenceByScopeKey.delete(scopeKey);
|
|
2010
|
-
} else {
|
|
2011
|
-
this.#presenceByScopeKey.set(scopeKey, next);
|
|
2012
|
-
}
|
|
2013
|
-
this.#emitClientEvent('presenceChanged', {
|
|
2014
|
-
scopeKey,
|
|
2015
|
-
presence: next,
|
|
2016
|
-
});
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
#rejoinPresence(): void {
|
|
2020
|
-
if (this.#joinedPresence.size === 0) return;
|
|
2021
|
-
for (const [scopeKey, metadata] of this.#joinedPresence) {
|
|
2022
|
-
void this.#request({
|
|
2023
|
-
type: 'sendPresence',
|
|
2024
|
-
action: 'join',
|
|
2025
|
-
scopeKey,
|
|
2026
|
-
...(metadata === undefined ? {} : { metadata }),
|
|
2027
|
-
}).catch((error) => {
|
|
2028
|
-
this.#emitDiagnostic({
|
|
2029
|
-
level: 'warn',
|
|
2030
|
-
source: 'realtime',
|
|
2031
|
-
code: 'realtime.presence_rejoin_failed',
|
|
2032
|
-
message: `Syncular presence rejoin failed: ${errorMessage(error)}`,
|
|
2033
|
-
details: { scopeKey },
|
|
2034
|
-
});
|
|
2035
|
-
});
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
#rejectAll(reason: SyncularWorkerErrorPayload | Error): void {
|
|
2040
|
-
const error =
|
|
2041
|
-
reason instanceof Error ? reason : new SyncularWorkerError(reason);
|
|
2042
|
-
this.#lastError = {
|
|
2043
|
-
message: error.message,
|
|
2044
|
-
...(error instanceof SyncularWorkerError ? { code: error.code } : {}),
|
|
2045
|
-
};
|
|
2046
|
-
for (const pending of this.#pending.values()) {
|
|
2047
|
-
if (pending.timeout) clearTimeout(pending.timeout);
|
|
2048
|
-
pending.reject(error);
|
|
2049
|
-
}
|
|
2050
|
-
this.#pending.clear();
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
#sendCancel(requestId: number): void {
|
|
2054
|
-
const id = this.#nextId++;
|
|
2055
|
-
this.worker.postMessage({
|
|
2056
|
-
id,
|
|
2057
|
-
protocolVersion: SYNCULAR_WORKER_PROTOCOL_VERSION,
|
|
2058
|
-
type: 'cancel',
|
|
2059
|
-
requestId,
|
|
2060
|
-
} satisfies SyncularWorkerRequest);
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
#emitDiagnostic(
|
|
2064
|
-
event: Omit<SyncularDiagnosticEvent, 'at'> & { at?: number }
|
|
2065
|
-
): void {
|
|
2066
|
-
const diagnostic: SyncularDiagnosticEvent = {
|
|
2067
|
-
at: event.at ?? Date.now(),
|
|
2068
|
-
level: event.level,
|
|
2069
|
-
source: event.source,
|
|
2070
|
-
code: event.code,
|
|
2071
|
-
message: event.message,
|
|
2072
|
-
...(event.syncAttemptId ? { syncAttemptId: event.syncAttemptId } : {}),
|
|
2073
|
-
...(event.traceId ? { traceId: event.traceId } : {}),
|
|
2074
|
-
...(event.spanId ? { spanId: event.spanId } : {}),
|
|
2075
|
-
...(event.clientId ? { clientId: event.clientId } : {}),
|
|
2076
|
-
...(event.subscriptionId ? { subscriptionId: event.subscriptionId } : {}),
|
|
2077
|
-
...(event.table ? { table: event.table } : {}),
|
|
2078
|
-
...(event.rowId ? { rowId: event.rowId } : {}),
|
|
2079
|
-
...(event.cursor !== undefined ? { cursor: event.cursor } : {}),
|
|
2080
|
-
...(event.details ? { details: event.details } : {}),
|
|
2081
|
-
};
|
|
2082
|
-
if (diagnostic.details?.resyncRequired === true) {
|
|
2083
|
-
this.#recoveryRequired = true;
|
|
2084
|
-
}
|
|
2085
|
-
if (
|
|
2086
|
-
diagnostic.code === 'auth.expired' ||
|
|
2087
|
-
diagnostic.code === 'auth.refresh_failed' ||
|
|
2088
|
-
diagnostic.code === 'sync.auth_required'
|
|
2089
|
-
) {
|
|
2090
|
-
this.#authRequired = true;
|
|
2091
|
-
}
|
|
2092
|
-
if (diagnostic.code === 'auth.refresh_succeeded') {
|
|
2093
|
-
this.#authRequired = false;
|
|
2094
|
-
}
|
|
2095
|
-
this.#lastDiagnostic = diagnostic;
|
|
2096
|
-
appendSyncularDiagnosticEvent(this.#recentDiagnostics, diagnostic);
|
|
2097
|
-
this.#emitLifecycleChanged();
|
|
2098
|
-
if (this.#diagnosticListeners.size === 0) return;
|
|
2099
|
-
for (const listener of this.#diagnosticListeners) {
|
|
2100
|
-
try {
|
|
2101
|
-
listener(diagnostic);
|
|
2102
|
-
} catch {
|
|
2103
|
-
// Diagnostics must never break client control flow.
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
function isOpfsOpenFailure(error: unknown): boolean {
|
|
2110
|
-
if (!(error instanceof SyncularWorkerError)) return false;
|
|
2111
|
-
const message = errorMessage(error).toLowerCase();
|
|
2112
|
-
return (
|
|
2113
|
-
message.includes('opfs') ||
|
|
2114
|
-
message.includes('sahpool') ||
|
|
2115
|
-
message.includes('sync access handle')
|
|
2116
|
-
);
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
function shouldEmitOperationalState(
|
|
2120
|
-
type: SyncularWorkerRequestInput['type']
|
|
2121
|
-
): boolean {
|
|
2122
|
-
return isGeneratedSyncularOperationalStateWorkerRequestType(type);
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
function errorMessage(error: unknown): string {
|
|
2126
|
-
return error instanceof Error ? error.message : String(error);
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
function normalizePresenceScopeKey(scopeKey: string): string {
|
|
2130
|
-
const separator = scopeKey.indexOf('::');
|
|
2131
|
-
return separator >= 0 ? scopeKey.slice(separator + 2) : scopeKey;
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
function coerceCount(value: unknown): number {
|
|
2135
|
-
const numeric = typeof value === 'number' ? value : Number(value ?? 0);
|
|
2136
|
-
return Number.isFinite(numeric) && numeric > 0 ? Math.trunc(numeric) : 0;
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
function sameJson(left: unknown, right: unknown): boolean {
|
|
2140
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
function cloneAuthHeaders(headers: SyncularAuthHeaders): SyncularAuthHeaders {
|
|
2144
|
-
return Object.fromEntries(Object.entries(headers));
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
function serializeRuntimeArtifact(
|
|
2148
|
-
runtime: SyncularRuntimeArtifact | undefined
|
|
2149
|
-
): SyncularWorkerRuntimeArtifact | undefined {
|
|
2150
|
-
if (!runtime) return undefined;
|
|
2151
|
-
return {
|
|
2152
|
-
wasmGlueUrl:
|
|
2153
|
-
runtime.wasmGlueUrl == null
|
|
2154
|
-
? undefined
|
|
2155
|
-
: runtimeUrlHref(runtime.wasmGlueUrl),
|
|
2156
|
-
wasmUrl:
|
|
2157
|
-
runtime.wasmUrl == null ? undefined : runtimeUrlHref(runtime.wasmUrl),
|
|
2158
|
-
};
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
function runtimeUrlHref(value: string | URL | Request): string {
|
|
2162
|
-
if (value instanceof Request) return value.url;
|
|
2163
|
-
return value instanceof URL ? value.href : value;
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
function normalizeFieldEncryptionConfig(
|
|
2167
|
-
config: SyncularFieldEncryptionConfig
|
|
2168
|
-
): Omit<SyncularFieldEncryptionConfig, 'keys'> & {
|
|
2169
|
-
keys: Record<string, string>;
|
|
2170
|
-
} {
|
|
2171
|
-
const keys: Record<string, string> = {};
|
|
2172
|
-
for (const [kid, value] of Object.entries(config.keys)) {
|
|
2173
|
-
keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
|
|
2174
|
-
}
|
|
2175
|
-
return { ...config, keys };
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
function normalizeEncryptedCrdtConfig(
|
|
2179
|
-
config: SyncularEncryptedCrdtConfig
|
|
2180
|
-
): Omit<SyncularEncryptedCrdtConfig, 'keys'> & {
|
|
2181
|
-
keys: Record<string, string>;
|
|
2182
|
-
} {
|
|
2183
|
-
const keys: Record<string, string> = {};
|
|
2184
|
-
for (const [kid, value] of Object.entries(config.keys)) {
|
|
2185
|
-
keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
|
|
2186
|
-
}
|
|
2187
|
-
return { ...config, keys };
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
function normalizeBlobEncryptionConfig(
|
|
2191
|
-
config: SyncularBlobEncryptionConfig
|
|
2192
|
-
): Omit<SyncularBlobEncryptionConfig, 'keys'> & {
|
|
2193
|
-
keys: Record<string, string>;
|
|
2194
|
-
} {
|
|
2195
|
-
const keys: Record<string, string> = {};
|
|
2196
|
-
for (const [kid, value] of Object.entries(config.keys)) {
|
|
2197
|
-
keys[kid] = value instanceof Uint8Array ? bytesToBase64Url(value) : value;
|
|
2198
|
-
}
|
|
2199
|
-
return { ...config, keys };
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
function arePresenceEntriesEqual(
|
|
2203
|
-
left: readonly SyncularPresenceEntry[],
|
|
2204
|
-
right: readonly SyncularPresenceEntry[]
|
|
2205
|
-
): boolean {
|
|
2206
|
-
if (left.length !== right.length) return false;
|
|
2207
|
-
for (let index = 0; index < left.length; index += 1) {
|
|
2208
|
-
const a = left[index]!;
|
|
2209
|
-
const b = right[index]!;
|
|
2210
|
-
if (
|
|
2211
|
-
a.clientId !== b.clientId ||
|
|
2212
|
-
a.actorId !== b.actorId ||
|
|
2213
|
-
a.joinedAt !== b.joinedAt ||
|
|
2214
|
-
JSON.stringify(a.metadata ?? {}) !== JSON.stringify(b.metadata ?? {})
|
|
2215
|
-
) {
|
|
2216
|
-
return false;
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
return true;
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
function bytesToBase64Url(bytes: Uint8Array): string {
|
|
2223
|
-
let binary = '';
|
|
2224
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
2225
|
-
return btoa(binary)
|
|
2226
|
-
.replace(/\+/g, '-')
|
|
2227
|
-
.replace(/\//g, '_')
|
|
2228
|
-
.replace(/=+$/g, '');
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
async function resolveRealtimeWorkerOptions(
|
|
2232
|
-
options: SyncularRealtimeOptions,
|
|
2233
|
-
config: ResolvedSyncularClientConfig
|
|
2234
|
-
): Promise<SyncularWorkerRealtimeOptions> {
|
|
2235
|
-
const params = {
|
|
2236
|
-
...(options.params ?? {}),
|
|
2237
|
-
...((await options.getParams?.({ clientId: config.clientId })) ?? {}),
|
|
2238
|
-
};
|
|
2239
|
-
return {
|
|
2240
|
-
...(options.wsUrl ? { wsUrl: options.wsUrl } : {}),
|
|
2241
|
-
...(Object.keys(params).length > 0 ? { params } : {}),
|
|
2242
|
-
...(options.initialReconnectDelayMs != null
|
|
2243
|
-
? { initialReconnectDelayMs: options.initialReconnectDelayMs }
|
|
2244
|
-
: {}),
|
|
2245
|
-
...(options.maxReconnectDelayMs != null
|
|
2246
|
-
? { maxReconnectDelayMs: options.maxReconnectDelayMs }
|
|
2247
|
-
: {}),
|
|
2248
|
-
...(options.reconnectBackoffFactor != null
|
|
2249
|
-
? { reconnectBackoffFactor: options.reconnectBackoffFactor }
|
|
2250
|
-
: {}),
|
|
2251
|
-
...(options.heartbeatTimeoutMs != null
|
|
2252
|
-
? { heartbeatTimeoutMs: options.heartbeatTimeoutMs }
|
|
2253
|
-
: {}),
|
|
2254
|
-
};
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
function assertSyncularRemoteMode(
|
|
2258
|
-
config: ResolvedSyncularClientConfig,
|
|
2259
|
-
operation: string
|
|
2260
|
-
): void {
|
|
2261
|
-
if (isSyncularRemoteMode(config)) return;
|
|
2262
|
-
throw new Error(
|
|
2263
|
-
`Syncular ${operation} requires remote mode; current mode is ${config.mode}`
|
|
2264
|
-
);
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
function isWorkerEvent(
|
|
2268
|
-
message: SyncularWorkerOutboundMessage
|
|
2269
|
-
): message is Exclude<SyncularWorkerOutboundMessage, SyncularWorkerResponse> {
|
|
2270
|
-
return 'type' in message && !('id' in message);
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
function authStatusFromError(error: unknown): 401 | 403 | undefined {
|
|
2274
|
-
const details =
|
|
2275
|
-
error instanceof SyncularWorkerError ? error.details : undefined;
|
|
2276
|
-
if (
|
|
2277
|
-
details &&
|
|
2278
|
-
typeof details === 'object' &&
|
|
2279
|
-
'status' in details &&
|
|
2280
|
-
(details.status === 401 || details.status === 403)
|
|
2281
|
-
) {
|
|
2282
|
-
return details.status;
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
const message = errorMessage(error);
|
|
2286
|
-
const match = /\bHTTP (401|403)\b/.exec(message);
|
|
2287
|
-
if (!match) return undefined;
|
|
2288
|
-
return match[1] === '401' ? 401 : 403;
|
|
2289
|
-
}
|