@syncular/client 0.0.6-96 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +713 -11
- package/dist/auth-leases.d.ts +11 -0
- package/dist/auth-leases.d.ts.map +1 -0
- package/dist/auth-leases.js +140 -0
- package/dist/auth-leases.js.map +1 -0
- package/dist/blob-limits.d.ts +15 -0
- package/dist/blob-limits.d.ts.map +1 -0
- package/dist/blob-limits.js +66 -0
- package/dist/blob-limits.js.map +1 -0
- package/dist/bridge-client.d.ts +69 -0
- package/dist/bridge-client.d.ts.map +1 -0
- package/dist/bridge-client.js +282 -0
- package/dist/bridge-client.js.map +1 -0
- package/dist/client-config.d.ts +6 -0
- package/dist/client-config.d.ts.map +1 -0
- package/dist/client-config.js +18 -0
- package/dist/client-config.js.map +1 -0
- package/dist/client.d.ts +65 -397
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +150 -960
- package/dist/client.js.map +1 -1
- package/dist/command-history.d.ts +35 -0
- package/dist/command-history.d.ts.map +1 -0
- package/dist/command-history.js +378 -0
- package/dist/command-history.js.map +1 -0
- package/dist/console-diagnostics.d.ts +33 -0
- package/dist/console-diagnostics.d.ts.map +1 -0
- package/dist/console-diagnostics.js +496 -0
- package/dist/console-diagnostics.js.map +1 -0
- package/dist/crdt-yjs/index.d.ts +4 -0
- package/dist/crdt-yjs/index.d.ts.map +1 -0
- package/dist/crdt-yjs/index.js +4 -0
- package/dist/crdt-yjs/index.js.map +1 -0
- package/dist/crdt-yjs/webview-host-facade.d.ts +126 -0
- package/dist/crdt-yjs/webview-host-facade.d.ts.map +1 -0
- package/dist/crdt-yjs/webview-host-facade.js +284 -0
- package/dist/crdt-yjs/webview-host-facade.js.map +1 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +153 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +1 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.js +406 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.js.map +1 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +73 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +1 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js +169 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +1 -0
- package/dist/database.d.ts +83 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +916 -0
- package/dist/database.js.map +1 -0
- package/dist/diagnostics.d.ts +10 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +83 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +211 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated-bridge.d.ts +365 -0
- package/dist/generated-bridge.d.ts.map +1 -0
- package/dist/generated-bridge.js +250 -0
- package/dist/generated-bridge.js.map +1 -0
- package/dist/index.d.ts +13 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -25
- package/dist/index.js.map +1 -1
- package/dist/mutations.d.ts +4 -72
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +2 -568
- package/dist/mutations.js.map +1 -1
- package/dist/network.d.ts +3 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +17 -0
- package/dist/network.js.map +1 -0
- package/dist/react/index.d.ts +169 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +628 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react-native/index.d.ts +35 -0
- package/dist/react-native/index.d.ts.map +1 -0
- package/dist/react-native/index.js +49 -0
- package/dist/react-native/index.js.map +1 -0
- package/dist/runtime-contract.d.ts +13 -0
- package/dist/runtime-contract.d.ts.map +1 -0
- package/dist/runtime-contract.js +24 -0
- package/dist/runtime-contract.js.map +1 -0
- package/dist/rust-client.d.ts +106 -0
- package/dist/rust-client.d.ts.map +1 -0
- package/dist/rust-client.js +936 -0
- package/dist/rust-client.js.map +1 -0
- package/dist/sentry.d.ts +35 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +155 -0
- package/dist/sentry.js.map +1 -0
- package/dist/sql-safety.d.ts +3 -0
- package/dist/sql-safety.d.ts.map +1 -0
- package/dist/sql-safety.js +62 -0
- package/dist/sql-safety.js.map +1 -0
- package/dist/syncular-runtime-artifacts.json +61 -0
- package/dist/tauri/index.d.ts +35 -0
- package/dist/tauri/index.d.ts.map +1 -0
- package/dist/tauri/index.js +114 -0
- package/dist/tauri/index.js.map +1 -0
- package/dist/types.d.ts +1059 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/wasm/.syncular-wasm-profile +1 -0
- package/dist/wasm/syncular-runtime-artifact.json +21 -0
- package/dist/wasm/syncular.d.ts +207 -0
- package/dist/wasm/syncular.js +2341 -0
- package/dist/wasm/syncular_bg.wasm +0 -0
- package/dist/wasm/syncular_bg.wasm.d.ts +97 -0
- package/dist/wasm-bindings/runtime-contract.d.ts +22 -0
- package/dist/wasm-bindings/runtime-contract.d.ts.map +1 -0
- package/dist/wasm-bindings/runtime-contract.js +112 -0
- package/dist/wasm-bindings/runtime-contract.js.map +1 -0
- package/dist/wasm-core/.syncular-wasm-profile +1 -0
- package/dist/wasm-core/syncular-runtime-artifact.json +17 -0
- package/dist/wasm-core/syncular.d.ts +162 -0
- package/dist/wasm-core/syncular.js +1847 -0
- package/dist/wasm-core/syncular_bg.wasm +0 -0
- package/dist/wasm-core/syncular_bg.wasm.d.ts +77 -0
- package/dist/wasm-perf/.syncular-wasm-profile +1 -0
- package/dist/wasm-perf/syncular-runtime-artifact.json +21 -0
- package/dist/wasm-perf/syncular.d.ts +207 -0
- package/dist/wasm-perf/syncular.js +2341 -0
- package/dist/wasm-perf/syncular_bg.wasm +0 -0
- package/dist/wasm-perf/syncular_bg.wasm.d.ts +97 -0
- package/dist/wasm-runtime.d.ts +18 -0
- package/dist/wasm-runtime.d.ts.map +1 -0
- package/dist/wasm-runtime.js +28 -0
- package/dist/wasm-runtime.js.map +1 -0
- package/dist/worker-client.d.ts +123 -0
- package/dist/worker-client.d.ts.map +1 -0
- package/dist/worker-client.js +1686 -0
- package/dist/worker-client.js.map +1 -0
- package/dist/worker-entry.d.ts +2 -0
- package/dist/worker-entry.d.ts.map +1 -0
- package/dist/worker-entry.js +479 -0
- package/dist/worker-entry.js.map +1 -0
- package/dist/worker-protocol.d.ts +64 -0
- package/dist/worker-protocol.d.ts.map +1 -0
- package/dist/worker-protocol.js +16 -0
- package/dist/worker-protocol.js.map +1 -0
- package/dist/worker-realtime.d.ts +39 -0
- package/dist/worker-realtime.d.ts.map +1 -0
- package/dist/worker-realtime.js +677 -0
- package/dist/worker-realtime.js.map +1 -0
- package/package.json +124 -17
- package/src/auth-leases.ts +251 -0
- package/src/blob-limits.ts +98 -0
- package/src/bridge-client.ts +512 -0
- package/src/client-config.ts +29 -0
- package/src/client.ts +256 -1414
- package/src/command-history.ts +623 -0
- package/src/console-diagnostics.ts +617 -0
- package/src/crdt-yjs/index.ts +3 -0
- package/src/crdt-yjs/webview-host-facade.ts +477 -0
- package/src/crdt-yjs/yjs-document-field-adapter.ts +733 -0
- package/src/crdt-yjs/yjs-prosemirror-bridge.ts +272 -0
- package/src/database.ts +1342 -0
- package/src/diagnostics.ts +116 -0
- package/src/errors.ts +269 -0
- package/src/generated-bridge.ts +741 -0
- package/src/index.ts +21 -25
- package/src/mutations.ts +12 -821
- package/src/network.ts +32 -0
- package/src/react/index.ts +1036 -0
- package/src/react-native/index.ts +152 -0
- package/src/runtime-contract.ts +48 -0
- package/src/rust-client.ts +1488 -0
- package/src/sentry.ts +215 -0
- package/src/sql-safety.ts +61 -0
- package/src/tauri/index.ts +211 -0
- package/src/types.ts +1356 -0
- package/src/wasm-bindings/generated-wasm-bindings.d.ts +70 -0
- package/src/wasm-bindings/runtime-contract.ts +158 -0
- package/src/wasm-runtime.ts +76 -0
- package/src/worker-client.ts +2215 -0
- package/src/worker-entry.ts +584 -0
- package/src/worker-protocol.ts +109 -0
- package/src/worker-realtime.ts +843 -0
- package/dist/blobs/index.d.ts +0 -7
- package/dist/blobs/index.d.ts.map +0 -1
- package/dist/blobs/index.js +0 -7
- package/dist/blobs/index.js.map +0 -1
- package/dist/blobs/manager.d.ts +0 -345
- package/dist/blobs/manager.d.ts.map +0 -1
- package/dist/blobs/manager.js +0 -749
- package/dist/blobs/manager.js.map +0 -1
- package/dist/blobs/migrate.d.ts +0 -14
- package/dist/blobs/migrate.d.ts.map +0 -1
- package/dist/blobs/migrate.js +0 -59
- package/dist/blobs/migrate.js.map +0 -1
- package/dist/blobs/types.d.ts +0 -62
- package/dist/blobs/types.d.ts.map +0 -1
- package/dist/blobs/types.js +0 -5
- package/dist/blobs/types.js.map +0 -1
- package/dist/conflicts.d.ts +0 -31
- package/dist/conflicts.d.ts.map +0 -1
- package/dist/conflicts.js +0 -112
- package/dist/conflicts.js.map +0 -1
- package/dist/create-client.d.ts +0 -118
- package/dist/create-client.d.ts.map +0 -1
- package/dist/create-client.js +0 -178
- package/dist/create-client.js.map +0 -1
- package/dist/engine/SyncEngine.d.ts +0 -272
- package/dist/engine/SyncEngine.d.ts.map +0 -1
- package/dist/engine/SyncEngine.js +0 -1860
- package/dist/engine/SyncEngine.js.map +0 -1
- package/dist/engine/index.d.ts +0 -6
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -6
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/types.d.ts +0 -345
- package/dist/engine/types.d.ts.map +0 -1
- package/dist/engine/types.js +0 -7
- package/dist/engine/types.js.map +0 -1
- package/dist/handlers/collection.d.ts +0 -6
- package/dist/handlers/collection.d.ts.map +0 -1
- package/dist/handlers/collection.js +0 -21
- package/dist/handlers/collection.js.map +0 -1
- package/dist/handlers/create-handler.d.ts +0 -120
- package/dist/handlers/create-handler.d.ts.map +0 -1
- package/dist/handlers/create-handler.js +0 -145
- package/dist/handlers/create-handler.js.map +0 -1
- package/dist/handlers/types.d.ts +0 -83
- package/dist/handlers/types.d.ts.map +0 -1
- package/dist/handlers/types.js +0 -5
- package/dist/handlers/types.js.map +0 -1
- package/dist/migrate.d.ts +0 -19
- package/dist/migrate.d.ts.map +0 -1
- package/dist/migrate.js +0 -238
- package/dist/migrate.js.map +0 -1
- package/dist/outbox.d.ts +0 -112
- package/dist/outbox.d.ts.map +0 -1
- package/dist/outbox.js +0 -294
- package/dist/outbox.js.map +0 -1
- package/dist/plugins/incrementing-version.d.ts +0 -34
- package/dist/plugins/incrementing-version.d.ts.map +0 -1
- package/dist/plugins/incrementing-version.js +0 -83
- package/dist/plugins/incrementing-version.js.map +0 -1
- package/dist/plugins/index.d.ts +0 -3
- package/dist/plugins/index.d.ts.map +0 -1
- package/dist/plugins/index.js +0 -3
- package/dist/plugins/index.js.map +0 -1
- package/dist/plugins/types.d.ts +0 -49
- package/dist/plugins/types.d.ts.map +0 -1
- package/dist/plugins/types.js +0 -15
- package/dist/plugins/types.js.map +0 -1
- package/dist/proxy/connection.d.ts +0 -33
- package/dist/proxy/connection.d.ts.map +0 -1
- package/dist/proxy/connection.js +0 -153
- package/dist/proxy/connection.js.map +0 -1
- package/dist/proxy/dialect.d.ts +0 -46
- package/dist/proxy/dialect.d.ts.map +0 -1
- package/dist/proxy/dialect.js +0 -58
- package/dist/proxy/dialect.js.map +0 -1
- package/dist/proxy/driver.d.ts +0 -42
- package/dist/proxy/driver.d.ts.map +0 -1
- package/dist/proxy/driver.js +0 -78
- package/dist/proxy/driver.js.map +0 -1
- package/dist/proxy/index.d.ts +0 -10
- package/dist/proxy/index.d.ts.map +0 -1
- package/dist/proxy/index.js +0 -10
- package/dist/proxy/index.js.map +0 -1
- package/dist/proxy/mutations.d.ts +0 -9
- package/dist/proxy/mutations.d.ts.map +0 -1
- package/dist/proxy/mutations.js +0 -11
- package/dist/proxy/mutations.js.map +0 -1
- package/dist/pull-engine.d.ts +0 -48
- package/dist/pull-engine.d.ts.map +0 -1
- package/dist/pull-engine.js +0 -648
- package/dist/pull-engine.js.map +0 -1
- package/dist/push-engine.d.ts +0 -18
- package/dist/push-engine.d.ts.map +0 -1
- package/dist/push-engine.js +0 -167
- package/dist/push-engine.js.map +0 -1
- package/dist/query/FingerprintCollector.d.ts +0 -18
- package/dist/query/FingerprintCollector.d.ts.map +0 -1
- package/dist/query/FingerprintCollector.js +0 -28
- package/dist/query/FingerprintCollector.js.map +0 -1
- package/dist/query/QueryContext.d.ts +0 -33
- package/dist/query/QueryContext.d.ts.map +0 -1
- package/dist/query/QueryContext.js +0 -16
- package/dist/query/QueryContext.js.map +0 -1
- package/dist/query/fingerprint.d.ts +0 -61
- package/dist/query/fingerprint.d.ts.map +0 -1
- package/dist/query/fingerprint.js +0 -91
- package/dist/query/fingerprint.js.map +0 -1
- package/dist/query/index.d.ts +0 -7
- package/dist/query/index.d.ts.map +0 -1
- package/dist/query/index.js +0 -7
- package/dist/query/index.js.map +0 -1
- package/dist/query/tracked-select.d.ts +0 -18
- package/dist/query/tracked-select.d.ts.map +0 -1
- package/dist/query/tracked-select.js +0 -90
- package/dist/query/tracked-select.js.map +0 -1
- package/dist/schema.d.ts +0 -83
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js +0 -7
- package/dist/schema.js.map +0 -1
- package/dist/subscription-state.d.ts +0 -46
- package/dist/subscription-state.d.ts.map +0 -1
- package/dist/subscription-state.js +0 -185
- package/dist/subscription-state.js.map +0 -1
- package/dist/sync-loop.d.ts +0 -34
- package/dist/sync-loop.d.ts.map +0 -1
- package/dist/sync-loop.js +0 -253
- package/dist/sync-loop.js.map +0 -1
- package/dist/sync.d.ts +0 -32
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -55
- package/dist/sync.js.map +0 -1
- package/dist/utils/id.d.ts +0 -17
- package/dist/utils/id.d.ts.map +0 -1
- package/dist/utils/id.js +0 -46
- package/dist/utils/id.js.map +0 -1
- package/src/blobs/index.ts +0 -7
- package/src/blobs/manager.ts +0 -1027
- package/src/blobs/migrate.ts +0 -67
- package/src/blobs/types.ts +0 -84
- package/src/client.test.ts +0 -417
- package/src/conflicts.ts +0 -171
- package/src/create-client.test.ts +0 -83
- package/src/create-client.ts +0 -330
- package/src/engine/SyncEngine.test.ts +0 -254
- package/src/engine/SyncEngine.ts +0 -2375
- package/src/engine/index.ts +0 -6
- package/src/engine/types.ts +0 -419
- package/src/handlers/collection.ts +0 -36
- package/src/handlers/create-handler.ts +0 -330
- package/src/handlers/types.ts +0 -108
- package/src/migrate.ts +0 -318
- package/src/outbox.ts +0 -383
- package/src/plugins/incrementing-version.ts +0 -133
- package/src/plugins/index.ts +0 -2
- package/src/plugins/types.ts +0 -63
- package/src/proxy/connection.ts +0 -191
- package/src/proxy/dialect.ts +0 -76
- package/src/proxy/driver.ts +0 -126
- package/src/proxy/index.ts +0 -10
- package/src/proxy/mutations.ts +0 -18
- package/src/pull-engine.test.ts +0 -151
- package/src/pull-engine.ts +0 -855
- package/src/push-engine.ts +0 -216
- package/src/query/FingerprintCollector.ts +0 -29
- package/src/query/QueryContext.ts +0 -54
- package/src/query/fingerprint.ts +0 -109
- package/src/query/index.ts +0 -10
- package/src/query/tracked-select.ts +0 -139
- package/src/schema.ts +0 -94
- package/src/subscription-state.ts +0 -259
- package/src/sync-loop.ts +0 -379
- package/src/sync.ts +0 -170
- package/src/utils/id.ts +0 -55
package/dist/client.js
CHANGED
|
@@ -1,993 +1,183 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// Client Class
|
|
19
|
-
// ============================================================================
|
|
20
|
-
/**
|
|
21
|
-
* Unified sync client.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```typescript
|
|
25
|
-
* import { Client } from '@syncular/client';
|
|
26
|
-
* import { createHttpTransport } from '@syncular/transport-http';
|
|
27
|
-
*
|
|
28
|
-
* const client = new Client({
|
|
29
|
-
* db,
|
|
30
|
-
* transport: createHttpTransport({ baseUrl: '/api/sync', getHeaders }),
|
|
31
|
-
* tableHandlers,
|
|
32
|
-
* clientId: 'device-123',
|
|
33
|
-
* actorId: 'user-456',
|
|
34
|
-
* subscriptions: [{ id: 'tasks', table: 'tasks', scopes: { user_id: 'user-456' } }],
|
|
35
|
-
* });
|
|
36
|
-
*
|
|
37
|
-
* await client.start();
|
|
38
|
-
*
|
|
39
|
-
* // Mutations
|
|
40
|
-
* await client.mutations.tasks.insert({ title: 'New task' });
|
|
41
|
-
*
|
|
42
|
-
* // Events
|
|
43
|
-
* client.on('sync:complete', () => console.log('synced'));
|
|
44
|
-
* ```
|
|
45
|
-
*/
|
|
46
|
-
export class Client {
|
|
47
|
-
options;
|
|
48
|
-
engine = null;
|
|
49
|
-
started = false;
|
|
50
|
-
destroyed = false;
|
|
51
|
-
emittedConflictIds = new Set();
|
|
52
|
-
eventListeners = new Map();
|
|
53
|
-
outboxStats = {
|
|
54
|
-
pending: 0,
|
|
55
|
-
sending: 0,
|
|
56
|
-
failed: 0,
|
|
57
|
-
acked: 0,
|
|
58
|
-
total: 0,
|
|
1
|
+
import { isSyncularOfflineError } from './errors';
|
|
2
|
+
import { browserSyncularNetworkStatusSource } from './network';
|
|
3
|
+
export function getSyncularClientStatus(client) {
|
|
4
|
+
const lifecycle = client.lifecycleState();
|
|
5
|
+
const connection = client.connectionState();
|
|
6
|
+
const outbox = lifecycle.outbox ?? null;
|
|
7
|
+
const conflicts = lifecycle.conflicts ?? null;
|
|
8
|
+
return {
|
|
9
|
+
lifecycle,
|
|
10
|
+
connection,
|
|
11
|
+
outbox,
|
|
12
|
+
conflicts,
|
|
13
|
+
isConnected: connection.realtime === 'connected' && !connection.closed,
|
|
14
|
+
isSyncing: lifecycle.phase === 'syncing' || lifecycle.phase === 'recovering',
|
|
15
|
+
hasPendingMutations: (outbox?.pending ?? 0) + (outbox?.sending ?? 0) > 0,
|
|
16
|
+
hasConflicts: (conflicts?.unresolved ?? 0) > 0,
|
|
17
|
+
requiresAction: lifecycle.requiresAction,
|
|
59
18
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
19
|
+
}
|
|
20
|
+
export class SyncularClientLifecycle {
|
|
21
|
+
client;
|
|
22
|
+
options;
|
|
23
|
+
#started = false;
|
|
24
|
+
#pollTimer;
|
|
25
|
+
#unsubscribeDiagnostics;
|
|
26
|
+
#unsubscribeNetwork;
|
|
27
|
+
#syncInFlight;
|
|
28
|
+
#syncAgain = false;
|
|
29
|
+
#hasConnectedRealtime = false;
|
|
30
|
+
#realtimeStarted = false;
|
|
31
|
+
#network;
|
|
32
|
+
constructor(client, options = {}) {
|
|
33
|
+
this.client = client;
|
|
65
34
|
this.options = options;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
versionColumn: options.versionColumn ?? 'server_version',
|
|
71
|
-
omitColumns: options.omitColumns ?? [],
|
|
72
|
-
codecs: options.codecs,
|
|
73
|
-
codecDialect: options.codecDialect,
|
|
74
|
-
});
|
|
75
|
-
this.mutations = createMutationsApi(commitFn);
|
|
76
|
-
// Create blob client if storage provided
|
|
77
|
-
if (options.blobStorage && options.transport.blobs) {
|
|
78
|
-
this.blobs = this.createBlobClient(options.blobStorage, options.transport);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// ===========================================================================
|
|
82
|
-
// Identity Getters
|
|
83
|
-
// ===========================================================================
|
|
84
|
-
/** Client ID */
|
|
85
|
-
get clientId() {
|
|
86
|
-
return this.options.clientId;
|
|
35
|
+
this.#network =
|
|
36
|
+
options.network === false
|
|
37
|
+
? undefined
|
|
38
|
+
: (options.network ?? browserSyncularNetworkStatusSource());
|
|
87
39
|
}
|
|
88
|
-
/** Actor ID */
|
|
89
|
-
get actorId() {
|
|
90
|
-
return this.options.actorId;
|
|
91
|
-
}
|
|
92
|
-
/** Database instance */
|
|
93
|
-
get db() {
|
|
94
|
-
return this.options.db;
|
|
95
|
-
}
|
|
96
|
-
// ===========================================================================
|
|
97
|
-
// Lifecycle
|
|
98
|
-
// ===========================================================================
|
|
99
|
-
/**
|
|
100
|
-
* Start the client.
|
|
101
|
-
* Runs migrations and starts sync engine.
|
|
102
|
-
*/
|
|
103
40
|
async start() {
|
|
104
|
-
if (this
|
|
105
|
-
throw new Error('Client has been destroyed');
|
|
106
|
-
}
|
|
107
|
-
if (this.started) {
|
|
41
|
+
if (this.#started)
|
|
108
42
|
return;
|
|
43
|
+
this.#started = true;
|
|
44
|
+
this.#hasConnectedRealtime =
|
|
45
|
+
this.client.connectionState().realtime === 'connected';
|
|
46
|
+
this.#realtimeStarted = this.#hasConnectedRealtime;
|
|
47
|
+
this.#unsubscribeDiagnostics = this.client.addDiagnosticListener((event) => this.#handleDiagnostic(event));
|
|
48
|
+
this.#unsubscribeNetwork = this.#subscribeNetworkEvents();
|
|
49
|
+
try {
|
|
50
|
+
if (this.options.subscriptions) {
|
|
51
|
+
await this.client.setSubscriptions(this.options.subscriptions);
|
|
52
|
+
}
|
|
53
|
+
if (this.options.initialSync !== false && this.#isOnline()) {
|
|
54
|
+
await this.#syncForLifecycle();
|
|
55
|
+
}
|
|
56
|
+
if (this.options.realtime !== false && this.#isOnline()) {
|
|
57
|
+
await this.#startRealtimeForLifecycle();
|
|
58
|
+
}
|
|
59
|
+
this.#startPolling();
|
|
109
60
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
await ensureClientBlobSchema(this.options.db);
|
|
61
|
+
catch (error) {
|
|
62
|
+
await this.stop().catch(() => undefined);
|
|
63
|
+
throw error;
|
|
114
64
|
}
|
|
115
|
-
// Create and start engine
|
|
116
|
-
this.engine = new SyncEngine({
|
|
117
|
-
db: this.options.db,
|
|
118
|
-
transport: this.options.transport,
|
|
119
|
-
handlers: this.options.tableHandlers,
|
|
120
|
-
clientId: this.options.clientId,
|
|
121
|
-
actorId: this.options.actorId,
|
|
122
|
-
subscriptions: this.options.subscriptions.map((s) => ({
|
|
123
|
-
id: s.id,
|
|
124
|
-
table: s.table,
|
|
125
|
-
scopes: s.scopes ?? {},
|
|
126
|
-
params: s.params ?? {},
|
|
127
|
-
})),
|
|
128
|
-
plugins: this.options.plugins,
|
|
129
|
-
realtimeEnabled: this.options.realtimeEnabled,
|
|
130
|
-
pollIntervalMs: this.options.pollIntervalMs,
|
|
131
|
-
stateId: this.options.stateId,
|
|
132
|
-
migrate: undefined, // We already ran migrations
|
|
133
|
-
});
|
|
134
|
-
// Wire up engine events to client events
|
|
135
|
-
this.wireEngineEvents();
|
|
136
|
-
await this.engine.start();
|
|
137
|
-
this.started = true;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Stop the client (can be restarted).
|
|
141
|
-
*/
|
|
142
|
-
stop() {
|
|
143
|
-
this.engine?.stop();
|
|
144
65
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this
|
|
150
|
-
this
|
|
151
|
-
this
|
|
66
|
+
async stop() {
|
|
67
|
+
if (!this.#started)
|
|
68
|
+
return;
|
|
69
|
+
this.#started = false;
|
|
70
|
+
this.#stopPolling();
|
|
71
|
+
this.#unsubscribeDiagnostics?.();
|
|
72
|
+
this.#unsubscribeDiagnostics = undefined;
|
|
73
|
+
this.#unsubscribeNetwork?.();
|
|
74
|
+
this.#unsubscribeNetwork = undefined;
|
|
75
|
+
this.#syncAgain = false;
|
|
76
|
+
if (this.options.realtime !== false) {
|
|
77
|
+
await this.client.stopRealtime();
|
|
78
|
+
}
|
|
79
|
+
this.#realtimeStarted = false;
|
|
152
80
|
}
|
|
153
|
-
// ===========================================================================
|
|
154
|
-
// Sync
|
|
155
|
-
// ===========================================================================
|
|
156
|
-
/**
|
|
157
|
-
* Trigger a manual sync.
|
|
158
|
-
*/
|
|
159
81
|
async sync() {
|
|
160
|
-
if (
|
|
161
|
-
|
|
82
|
+
if (this.#syncInFlight) {
|
|
83
|
+
this.#syncAgain = true;
|
|
84
|
+
return this.#syncInFlight;
|
|
85
|
+
}
|
|
86
|
+
this.#syncInFlight = this.client.syncOnce().finally(() => {
|
|
87
|
+
this.#syncInFlight = undefined;
|
|
88
|
+
if (this.#syncAgain && this.#started) {
|
|
89
|
+
this.#syncAgain = false;
|
|
90
|
+
void this.sync().catch(() => undefined);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return this.#syncInFlight;
|
|
94
|
+
}
|
|
95
|
+
#handleDiagnostic(event) {
|
|
96
|
+
if (event.source === 'sync' && event.details?.resyncRequired === true) {
|
|
97
|
+
void this.client
|
|
98
|
+
.forceSubscriptionsBootstrap()
|
|
99
|
+
.then(() => this.sync())
|
|
100
|
+
.catch(() => undefined);
|
|
101
|
+
return;
|
|
162
102
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// ===========================================================================
|
|
168
|
-
/**
|
|
169
|
-
* Update subscriptions.
|
|
170
|
-
*/
|
|
171
|
-
updateSubscriptions(subscriptions) {
|
|
172
|
-
this.options.subscriptions = subscriptions;
|
|
173
|
-
if (this.engine) {
|
|
174
|
-
this.engine.updateSubscriptions(subscriptions.map((s) => ({
|
|
175
|
-
id: s.id,
|
|
176
|
-
table: s.table,
|
|
177
|
-
scopes: s.scopes ?? {},
|
|
178
|
-
params: s.params ?? {},
|
|
179
|
-
})));
|
|
103
|
+
if (event.source !== 'realtime' ||
|
|
104
|
+
event.code !== 'realtime.state' ||
|
|
105
|
+
event.details?.state == null) {
|
|
106
|
+
return;
|
|
180
107
|
}
|
|
108
|
+
const state = event.details.state;
|
|
109
|
+
if (state !== 'connected')
|
|
110
|
+
return;
|
|
111
|
+
const wasReconnect = this.#hasConnectedRealtime;
|
|
112
|
+
this.#hasConnectedRealtime = true;
|
|
113
|
+
const shouldSync = this.options.syncOnRealtimeConnect !== false &&
|
|
114
|
+
(wasReconnect || this.options.initialSync === false);
|
|
115
|
+
if (!shouldSync)
|
|
116
|
+
return;
|
|
117
|
+
void this.sync().catch(() => undefined);
|
|
181
118
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
params: s.params ?? {},
|
|
191
|
-
}));
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* List persisted subscription metadata rows.
|
|
195
|
-
*/
|
|
196
|
-
async listSubscriptionStates(args) {
|
|
197
|
-
if (!this.engine)
|
|
198
|
-
return [];
|
|
199
|
-
return this.engine.listSubscriptionStates(args);
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Read one persisted subscription metadata row.
|
|
203
|
-
*/
|
|
204
|
-
async getSubscriptionState(subscriptionId, options) {
|
|
205
|
-
if (!this.engine)
|
|
206
|
-
return null;
|
|
207
|
-
return this.engine.getSubscriptionState(subscriptionId, options);
|
|
208
|
-
}
|
|
209
|
-
// ===========================================================================
|
|
210
|
-
// State
|
|
211
|
-
// ===========================================================================
|
|
212
|
-
/**
|
|
213
|
-
* Get current client state.
|
|
214
|
-
*/
|
|
215
|
-
getState() {
|
|
216
|
-
const engineState = this.engine?.getState() ?? this.createInitialEngineState();
|
|
217
|
-
return {
|
|
218
|
-
clientId: this.options.clientId,
|
|
219
|
-
actorId: this.options.actorId,
|
|
220
|
-
enabled: engineState.enabled,
|
|
221
|
-
isSyncing: engineState.isSyncing,
|
|
222
|
-
connectionState: engineState.connectionState,
|
|
223
|
-
lastSyncAt: engineState.lastSyncAt,
|
|
224
|
-
error: engineState.error
|
|
225
|
-
? { code: engineState.error.code, message: engineState.error.message }
|
|
226
|
-
: null,
|
|
227
|
-
outbox: this.outboxStats,
|
|
119
|
+
#subscribeNetworkEvents() {
|
|
120
|
+
const network = this.#network;
|
|
121
|
+
if (!network?.addEventListener || !network.removeEventListener)
|
|
122
|
+
return;
|
|
123
|
+
const handleOnline = () => {
|
|
124
|
+
if (!this.#started)
|
|
125
|
+
return;
|
|
126
|
+
void this.#resumeOnline().catch(() => undefined);
|
|
228
127
|
};
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Get current transport health details.
|
|
232
|
-
*/
|
|
233
|
-
getTransportHealth() {
|
|
234
|
-
if (!this.engine)
|
|
235
|
-
return null;
|
|
236
|
-
return this.engine.getTransportHealth();
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Get computed sync progress across subscriptions.
|
|
240
|
-
*/
|
|
241
|
-
async getProgress() {
|
|
242
|
-
if (!this.engine)
|
|
243
|
-
return null;
|
|
244
|
-
return this.engine.getProgress();
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Get a diagnostics snapshot for support/debug flows.
|
|
248
|
-
*/
|
|
249
|
-
async getDiagnostics() {
|
|
250
|
-
if (!this.engine)
|
|
251
|
-
return null;
|
|
252
|
-
return this.engine.getDiagnostics();
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Get a serializable inspector snapshot for in-app debug tooling.
|
|
256
|
-
*/
|
|
257
|
-
async getInspectorSnapshot(options) {
|
|
258
|
-
if (!this.engine)
|
|
259
|
-
return null;
|
|
260
|
-
return this.engine.getInspectorSnapshot(options);
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Reset local sync metadata (and optionally synced app rows/outbox/conflicts).
|
|
264
|
-
*/
|
|
265
|
-
async reset(options) {
|
|
266
|
-
if (!this.engine) {
|
|
267
|
-
return {
|
|
268
|
-
deletedSubscriptionStates: 0,
|
|
269
|
-
deletedOutboxCommits: 0,
|
|
270
|
-
deletedConflicts: 0,
|
|
271
|
-
clearedTables: [],
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
return this.engine.reset(options);
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Run a built-in repair flow for common corruption scenarios.
|
|
278
|
-
*/
|
|
279
|
-
async repair(options) {
|
|
280
|
-
if (!this.engine) {
|
|
281
|
-
return {
|
|
282
|
-
deletedSubscriptionStates: 0,
|
|
283
|
-
deletedOutboxCommits: 0,
|
|
284
|
-
deletedConflicts: 0,
|
|
285
|
-
clearedTables: [],
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
return this.engine.repair(options);
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Wait until the channel reaches a target phase.
|
|
292
|
-
*/
|
|
293
|
-
async awaitPhase(phase, options = {}) {
|
|
294
|
-
if (!this.engine)
|
|
295
|
-
return null;
|
|
296
|
-
return this.engine.awaitPhase(phase, options);
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Wait until bootstrap completes for the default state or a specific subscription.
|
|
300
|
-
*/
|
|
301
|
-
async awaitBootstrapComplete(options = {}) {
|
|
302
|
-
if (!this.engine)
|
|
303
|
-
return null;
|
|
304
|
-
return this.engine.awaitBootstrapComplete(options);
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Subscribe to state changes (for useSyncExternalStore).
|
|
308
|
-
*/
|
|
309
|
-
subscribe(callback) {
|
|
310
|
-
if (!this.engine) {
|
|
311
|
-
// Return no-op unsubscribe before engine is started
|
|
312
|
-
return () => { };
|
|
313
|
-
}
|
|
314
|
-
return this.engine.subscribe(callback);
|
|
315
|
-
}
|
|
316
|
-
// ===========================================================================
|
|
317
|
-
// Events
|
|
318
|
-
// ===========================================================================
|
|
319
|
-
/**
|
|
320
|
-
* Subscribe to client events.
|
|
321
|
-
*/
|
|
322
|
-
on(event, handler) {
|
|
323
|
-
if (!this.eventListeners.has(event)) {
|
|
324
|
-
this.eventListeners.set(event, new Set());
|
|
325
|
-
}
|
|
326
|
-
this.eventListeners.get(event).add(handler);
|
|
128
|
+
network.addEventListener('online', handleOnline);
|
|
327
129
|
return () => {
|
|
328
|
-
|
|
130
|
+
network.removeEventListener?.('online', handleOnline);
|
|
329
131
|
};
|
|
330
132
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
listener(payload);
|
|
337
|
-
}
|
|
338
|
-
catch (err) {
|
|
339
|
-
console.error(`[Client] Error in ${event} listener:`, err);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
// ===========================================================================
|
|
345
|
-
// Conflicts
|
|
346
|
-
// ===========================================================================
|
|
347
|
-
/**
|
|
348
|
-
* Get pending conflicts.
|
|
349
|
-
*/
|
|
350
|
-
async getConflicts() {
|
|
351
|
-
if (!this.engine) {
|
|
352
|
-
return [];
|
|
353
|
-
}
|
|
354
|
-
const conflicts = await this.engine.getConflicts();
|
|
355
|
-
return conflicts.map((c) => this.mapConflictInfo(c));
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Resolve a conflict.
|
|
359
|
-
*/
|
|
360
|
-
async resolveConflict(id, resolution) {
|
|
361
|
-
const { resolveConflict } = await import('./conflicts.js');
|
|
362
|
-
const pendingBeforeResolve = await this.getConflicts();
|
|
363
|
-
const resolvedConflict = pendingBeforeResolve.find((c) => c.id === id);
|
|
364
|
-
// For 'keep-local' and 'keep-server', we just mark it resolved
|
|
365
|
-
// For 'custom', we would need to apply the payload - but that requires
|
|
366
|
-
// creating a new mutation, which the user should do separately
|
|
367
|
-
const resolutionStr = resolution.strategy === 'custom'
|
|
368
|
-
? `custom:${JSON.stringify(resolution.payload)}`
|
|
369
|
-
: resolution.strategy;
|
|
370
|
-
await resolveConflict(this.options.db, { id, resolution: resolutionStr });
|
|
371
|
-
countSyncMetric('sync.conflicts.resolved', 1, {
|
|
372
|
-
attributes: {
|
|
373
|
-
strategy: resolution.strategy,
|
|
374
|
-
},
|
|
375
|
-
});
|
|
376
|
-
this.emittedConflictIds.delete(id);
|
|
377
|
-
if (resolvedConflict) {
|
|
378
|
-
this.emit('conflict:resolved', resolvedConflict);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// ===========================================================================
|
|
382
|
-
// Outbox
|
|
383
|
-
// ===========================================================================
|
|
384
|
-
/**
|
|
385
|
-
* Get outbox statistics.
|
|
386
|
-
*/
|
|
387
|
-
async getOutboxStats() {
|
|
388
|
-
if (!this.engine) {
|
|
389
|
-
return this.outboxStats;
|
|
390
|
-
}
|
|
391
|
-
this.outboxStats = await this.engine.refreshOutboxStats({ emit: false });
|
|
392
|
-
return this.outboxStats;
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* Clear failed commits from outbox.
|
|
396
|
-
*/
|
|
397
|
-
async clearFailedCommits() {
|
|
398
|
-
if (!this.engine) {
|
|
399
|
-
return 0;
|
|
400
|
-
}
|
|
401
|
-
return this.engine.clearFailedCommits();
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Retry failed commits.
|
|
405
|
-
*/
|
|
406
|
-
async retryFailedCommits() {
|
|
407
|
-
// Mark failed commits as pending and trigger sync
|
|
408
|
-
const result = await sql `
|
|
409
|
-
update ${sql.table('sync_outbox_commits')}
|
|
410
|
-
set
|
|
411
|
-
${sql.ref('status')} = ${sql.val('pending')},
|
|
412
|
-
${sql.ref('attempt_count')} = ${sql.val(0)},
|
|
413
|
-
${sql.ref('error')} = ${sql.val(null)}
|
|
414
|
-
where ${sql.ref('status')} = ${sql.val('failed')}
|
|
415
|
-
`.execute(this.options.db);
|
|
416
|
-
const count = Number(result.numAffectedRows ?? 0n);
|
|
417
|
-
if (count > 0 && this.engine) {
|
|
418
|
-
await this.engine.refreshOutboxStats();
|
|
419
|
-
await this.engine.sync();
|
|
133
|
+
async #resumeOnline() {
|
|
134
|
+
if (!this.#started || !this.#isOnline())
|
|
135
|
+
return;
|
|
136
|
+
if (this.options.initialSync !== false) {
|
|
137
|
+
await this.#syncForLifecycle();
|
|
420
138
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
// ===========================================================================
|
|
424
|
-
// Presence
|
|
425
|
-
// ===========================================================================
|
|
426
|
-
/**
|
|
427
|
-
* Get presence for a scope.
|
|
428
|
-
*/
|
|
429
|
-
getPresence(scopeKey) {
|
|
430
|
-
if (!this.engine) {
|
|
431
|
-
return [];
|
|
139
|
+
if (this.options.realtime !== false && !this.#realtimeStarted) {
|
|
140
|
+
await this.#startRealtimeForLifecycle();
|
|
432
141
|
}
|
|
433
|
-
return this.engine.getPresence(scopeKey);
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Join presence for a scope key.
|
|
437
|
-
*/
|
|
438
|
-
joinPresence(scopeKey, metadata) {
|
|
439
|
-
this.engine?.joinPresence(scopeKey, metadata);
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* Leave presence for a scope key.
|
|
443
|
-
*/
|
|
444
|
-
leavePresence(scopeKey) {
|
|
445
|
-
this.engine?.leavePresence(scopeKey);
|
|
446
142
|
}
|
|
447
|
-
|
|
448
|
-
* Update presence metadata for a scope key.
|
|
449
|
-
*/
|
|
450
|
-
updatePresenceMetadata(scopeKey, metadata) {
|
|
451
|
-
this.engine?.updatePresenceMetadata(scopeKey, metadata);
|
|
452
|
-
}
|
|
453
|
-
// ===========================================================================
|
|
454
|
-
// Migration Info
|
|
455
|
-
// ===========================================================================
|
|
456
|
-
/**
|
|
457
|
-
* Get migration info.
|
|
458
|
-
*/
|
|
459
|
-
async getMigrationInfo() {
|
|
460
|
-
// Check if sync tables exist
|
|
461
|
-
let syncMigrated = false;
|
|
462
|
-
try {
|
|
463
|
-
await this.options.db
|
|
464
|
-
.selectFrom('sync_outbox_commits')
|
|
465
|
-
.selectAll()
|
|
466
|
-
.limit(1)
|
|
467
|
-
.execute();
|
|
468
|
-
syncMigrated = true;
|
|
469
|
-
}
|
|
470
|
-
catch {
|
|
471
|
-
syncMigrated = false;
|
|
472
|
-
}
|
|
473
|
-
// Check if blob tables exist
|
|
474
|
-
let blobsMigrated = false;
|
|
143
|
+
async #syncForLifecycle() {
|
|
475
144
|
try {
|
|
476
|
-
await this.
|
|
477
|
-
.selectFrom('sync_blob_cache')
|
|
478
|
-
.selectAll()
|
|
479
|
-
.limit(1)
|
|
480
|
-
.execute();
|
|
481
|
-
blobsMigrated = true;
|
|
145
|
+
await this.sync();
|
|
482
146
|
}
|
|
483
|
-
catch {
|
|
484
|
-
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (!isSyncularOfflineError(error))
|
|
149
|
+
throw error;
|
|
485
150
|
}
|
|
486
|
-
return { syncMigrated, blobsMigrated };
|
|
487
151
|
}
|
|
488
|
-
|
|
489
|
-
* Static: Check if migrations are needed.
|
|
490
|
-
*/
|
|
491
|
-
static async checkMigrations(db) {
|
|
492
|
-
let syncMigrated = false;
|
|
493
|
-
let blobsMigrated = false;
|
|
494
|
-
try {
|
|
495
|
-
await db.selectFrom('sync_outbox_commits').selectAll().limit(1).execute();
|
|
496
|
-
syncMigrated = true;
|
|
497
|
-
}
|
|
498
|
-
catch {
|
|
499
|
-
syncMigrated = false;
|
|
500
|
-
}
|
|
152
|
+
async #startRealtimeForLifecycle() {
|
|
501
153
|
try {
|
|
502
|
-
await
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
catch {
|
|
506
|
-
blobsMigrated = false;
|
|
154
|
+
await this.client.startRealtime(this.options.realtime);
|
|
155
|
+
this.#realtimeStarted = true;
|
|
507
156
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
/**
|
|
515
|
-
* Static: Run migrations.
|
|
516
|
-
*/
|
|
517
|
-
static async migrate(db, options) {
|
|
518
|
-
await ensureClientSyncSchema(db);
|
|
519
|
-
if (options?.blobs) {
|
|
520
|
-
await ensureClientBlobSchema(db);
|
|
157
|
+
catch (error) {
|
|
158
|
+
this.#realtimeStarted = false;
|
|
159
|
+
if (!isSyncularOfflineError(error))
|
|
160
|
+
throw error;
|
|
521
161
|
}
|
|
522
162
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
// ===========================================================================
|
|
526
|
-
createInitialEngineState() {
|
|
527
|
-
return {
|
|
528
|
-
enabled: false,
|
|
529
|
-
isSyncing: false,
|
|
530
|
-
connectionState: 'disconnected',
|
|
531
|
-
transportMode: 'polling',
|
|
532
|
-
lastSyncAt: null,
|
|
533
|
-
error: null,
|
|
534
|
-
pendingCount: 0,
|
|
535
|
-
retryCount: 0,
|
|
536
|
-
isRetrying: false,
|
|
537
|
-
};
|
|
163
|
+
#isOnline() {
|
|
164
|
+
return this.#network?.isOnline() !== false;
|
|
538
165
|
}
|
|
539
|
-
|
|
540
|
-
|
|
166
|
+
#startPolling() {
|
|
167
|
+
const interval = this.options.pollIntervalMs;
|
|
168
|
+
if (interval === false || interval === undefined || interval <= 0)
|
|
541
169
|
return;
|
|
542
|
-
this
|
|
543
|
-
this
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
this.engine.on('sync:live', (payload) => {
|
|
554
|
-
this.emit('sync:live', payload);
|
|
555
|
-
});
|
|
556
|
-
this.engine.on('sync:error', (error) => {
|
|
557
|
-
this.emit('sync:error', { code: error.code, message: error.message });
|
|
558
|
-
// Check for new conflicts after sync error
|
|
559
|
-
this.checkForNewConflicts();
|
|
560
|
-
});
|
|
561
|
-
this.engine.on('bootstrap:start', (payload) => {
|
|
562
|
-
this.emit('bootstrap:start', payload);
|
|
563
|
-
});
|
|
564
|
-
this.engine.on('bootstrap:progress', (payload) => {
|
|
565
|
-
this.emit('bootstrap:progress', payload);
|
|
566
|
-
});
|
|
567
|
-
this.engine.on('bootstrap:complete', (payload) => {
|
|
568
|
-
this.emit('bootstrap:complete', payload);
|
|
569
|
-
});
|
|
570
|
-
this.engine.on('connection:change', (payload) => {
|
|
571
|
-
this.emit('connection:change', payload);
|
|
572
|
-
});
|
|
573
|
-
this.engine.on('data:change', (payload) => {
|
|
574
|
-
this.emit('data:change', payload);
|
|
575
|
-
});
|
|
576
|
-
this.engine.on('outbox:change', (payload) => {
|
|
577
|
-
this.outboxStats = {
|
|
578
|
-
pending: payload.pendingCount,
|
|
579
|
-
sending: payload.sendingCount,
|
|
580
|
-
failed: payload.failedCount,
|
|
581
|
-
acked: payload.ackedCount ?? 0,
|
|
582
|
-
total: payload.pendingCount +
|
|
583
|
-
payload.sendingCount +
|
|
584
|
-
payload.failedCount +
|
|
585
|
-
(payload.ackedCount ?? 0),
|
|
586
|
-
};
|
|
587
|
-
this.emit('outbox:change', this.outboxStats);
|
|
588
|
-
});
|
|
589
|
-
this.engine.on('presence:change', (payload) => {
|
|
590
|
-
this.emit('presence:change', payload);
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
async checkForNewConflicts() {
|
|
594
|
-
const conflicts = await this.getConflicts();
|
|
595
|
-
const activeIds = new Set(conflicts.map((conflict) => conflict.id));
|
|
596
|
-
for (const id of this.emittedConflictIds) {
|
|
597
|
-
if (!activeIds.has(id)) {
|
|
598
|
-
this.emittedConflictIds.delete(id);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
for (const conflict of conflicts) {
|
|
602
|
-
if (this.emittedConflictIds.has(conflict.id)) {
|
|
603
|
-
continue;
|
|
604
|
-
}
|
|
605
|
-
this.emittedConflictIds.add(conflict.id);
|
|
606
|
-
this.emit('conflict:new', conflict);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
mapConflictInfo(info) {
|
|
610
|
-
let serverPayload = null;
|
|
611
|
-
if (info.serverRowJson) {
|
|
612
|
-
try {
|
|
613
|
-
serverPayload = JSON.parse(info.serverRowJson);
|
|
614
|
-
}
|
|
615
|
-
catch {
|
|
616
|
-
serverPayload = null;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
return {
|
|
620
|
-
id: info.id,
|
|
621
|
-
table: info.table,
|
|
622
|
-
rowId: info.rowId,
|
|
623
|
-
opIndex: info.opIndex,
|
|
624
|
-
localPayload: info.localPayload,
|
|
625
|
-
serverPayload,
|
|
626
|
-
serverVersion: info.serverVersion,
|
|
627
|
-
message: info.message,
|
|
628
|
-
code: info.code,
|
|
629
|
-
createdAt: info.createdAt,
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
createBlobClient(storage, transport) {
|
|
633
|
-
const db = this.options.db;
|
|
634
|
-
const blobs = transport.blobs;
|
|
635
|
-
const staleUploadingTimeoutMs = 30_000;
|
|
636
|
-
const maxUploadRetries = 3;
|
|
637
|
-
return {
|
|
638
|
-
async store(data, options) {
|
|
639
|
-
const bytes = await toUint8Array(data);
|
|
640
|
-
const mimeType = data instanceof Blob
|
|
641
|
-
? data.type
|
|
642
|
-
: (options?.mimeType ?? 'application/octet-stream');
|
|
643
|
-
// Compute hash
|
|
644
|
-
const hashHex = await computeSha256Hex(bytes);
|
|
645
|
-
const hash = `sha256:${hashHex}`;
|
|
646
|
-
// Store locally
|
|
647
|
-
await storage.write(hash, bytes);
|
|
648
|
-
// Store metadata
|
|
649
|
-
const now = Date.now();
|
|
650
|
-
await sql `
|
|
651
|
-
insert into ${sql.table('sync_blob_cache')} (
|
|
652
|
-
${sql.join([
|
|
653
|
-
sql.ref('hash'),
|
|
654
|
-
sql.ref('size'),
|
|
655
|
-
sql.ref('mime_type'),
|
|
656
|
-
sql.ref('cached_at'),
|
|
657
|
-
sql.ref('last_accessed_at'),
|
|
658
|
-
sql.ref('encrypted'),
|
|
659
|
-
sql.ref('key_id'),
|
|
660
|
-
sql.ref('body'),
|
|
661
|
-
])}
|
|
662
|
-
) values (
|
|
663
|
-
${sql.join([
|
|
664
|
-
sql.val(hash),
|
|
665
|
-
sql.val(bytes.length),
|
|
666
|
-
sql.val(mimeType),
|
|
667
|
-
sql.val(now),
|
|
668
|
-
sql.val(now),
|
|
669
|
-
sql.val(0),
|
|
670
|
-
sql.val(null),
|
|
671
|
-
sql.val(bytes),
|
|
672
|
-
])}
|
|
673
|
-
)
|
|
674
|
-
on conflict (${sql.ref('hash')}) do nothing
|
|
675
|
-
`.execute(db);
|
|
676
|
-
// Queue for upload or upload immediately
|
|
677
|
-
if (options?.immediate) {
|
|
678
|
-
// Initiate upload
|
|
679
|
-
const initResult = await blobs.initiateUpload({
|
|
680
|
-
hash,
|
|
681
|
-
size: bytes.length,
|
|
682
|
-
mimeType,
|
|
683
|
-
});
|
|
684
|
-
if (!initResult.exists && initResult.uploadUrl) {
|
|
685
|
-
// Upload to presigned URL
|
|
686
|
-
const uploadResponse = await fetch(initResult.uploadUrl, {
|
|
687
|
-
method: initResult.uploadMethod ?? 'PUT',
|
|
688
|
-
body: bytes.buffer,
|
|
689
|
-
headers: initResult.uploadHeaders,
|
|
690
|
-
});
|
|
691
|
-
if (!uploadResponse.ok) {
|
|
692
|
-
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
693
|
-
}
|
|
694
|
-
// Complete upload
|
|
695
|
-
await blobs.completeUpload(hash);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
else {
|
|
699
|
-
// Queue for later upload
|
|
700
|
-
await sql `
|
|
701
|
-
insert into ${sql.table('sync_blob_outbox')} (
|
|
702
|
-
${sql.join([
|
|
703
|
-
sql.ref('hash'),
|
|
704
|
-
sql.ref('size'),
|
|
705
|
-
sql.ref('mime_type'),
|
|
706
|
-
sql.ref('status'),
|
|
707
|
-
sql.ref('created_at'),
|
|
708
|
-
sql.ref('updated_at'),
|
|
709
|
-
sql.ref('attempt_count'),
|
|
710
|
-
sql.ref('error'),
|
|
711
|
-
sql.ref('encrypted'),
|
|
712
|
-
sql.ref('key_id'),
|
|
713
|
-
sql.ref('body'),
|
|
714
|
-
])}
|
|
715
|
-
) values (
|
|
716
|
-
${sql.join([
|
|
717
|
-
sql.val(hash),
|
|
718
|
-
sql.val(bytes.length),
|
|
719
|
-
sql.val(mimeType),
|
|
720
|
-
sql.val('pending'),
|
|
721
|
-
sql.val(now),
|
|
722
|
-
sql.val(now),
|
|
723
|
-
sql.val(0),
|
|
724
|
-
sql.val(null),
|
|
725
|
-
sql.val(0),
|
|
726
|
-
sql.val(null),
|
|
727
|
-
sql.val(bytes),
|
|
728
|
-
])}
|
|
729
|
-
)
|
|
730
|
-
on conflict (${sql.ref('hash')}) do nothing
|
|
731
|
-
`.execute(db);
|
|
732
|
-
}
|
|
733
|
-
return {
|
|
734
|
-
hash,
|
|
735
|
-
size: bytes.length,
|
|
736
|
-
mimeType,
|
|
737
|
-
};
|
|
738
|
-
},
|
|
739
|
-
async retrieve(ref) {
|
|
740
|
-
// Check local storage first
|
|
741
|
-
const local = await storage.read(ref.hash);
|
|
742
|
-
if (local) {
|
|
743
|
-
// Update access time
|
|
744
|
-
await sql `
|
|
745
|
-
update ${sql.table('sync_blob_cache')}
|
|
746
|
-
set ${sql.ref('last_accessed_at')} = ${sql.val(Date.now())}
|
|
747
|
-
where ${sql.ref('hash')} = ${sql.val(ref.hash)}
|
|
748
|
-
`.execute(db);
|
|
749
|
-
return local;
|
|
750
|
-
}
|
|
751
|
-
// Fetch from server
|
|
752
|
-
const { url } = await blobs.getDownloadUrl(ref.hash);
|
|
753
|
-
const response = await fetch(url);
|
|
754
|
-
if (!response.ok) {
|
|
755
|
-
throw new Error(`Download failed: ${response.statusText}`);
|
|
756
|
-
}
|
|
757
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
758
|
-
// Cache locally
|
|
759
|
-
await storage.write(ref.hash, bytes);
|
|
760
|
-
const now = Date.now();
|
|
761
|
-
await sql `
|
|
762
|
-
insert into ${sql.table('sync_blob_cache')} (
|
|
763
|
-
${sql.join([
|
|
764
|
-
sql.ref('hash'),
|
|
765
|
-
sql.ref('size'),
|
|
766
|
-
sql.ref('mime_type'),
|
|
767
|
-
sql.ref('cached_at'),
|
|
768
|
-
sql.ref('last_accessed_at'),
|
|
769
|
-
sql.ref('encrypted'),
|
|
770
|
-
sql.ref('key_id'),
|
|
771
|
-
sql.ref('body'),
|
|
772
|
-
])}
|
|
773
|
-
) values (
|
|
774
|
-
${sql.join([
|
|
775
|
-
sql.val(ref.hash),
|
|
776
|
-
sql.val(bytes.length),
|
|
777
|
-
sql.val(ref.mimeType),
|
|
778
|
-
sql.val(now),
|
|
779
|
-
sql.val(now),
|
|
780
|
-
sql.val(0),
|
|
781
|
-
sql.val(null),
|
|
782
|
-
sql.val(bytes),
|
|
783
|
-
])}
|
|
784
|
-
)
|
|
785
|
-
on conflict (${sql.ref('hash')}) do nothing
|
|
786
|
-
`.execute(db);
|
|
787
|
-
return bytes;
|
|
788
|
-
},
|
|
789
|
-
async isLocal(hash) {
|
|
790
|
-
return storage.exists(hash);
|
|
791
|
-
},
|
|
792
|
-
async preload(refs) {
|
|
793
|
-
await Promise.all(refs.map((ref) => this.retrieve(ref)));
|
|
794
|
-
},
|
|
795
|
-
async processUploadQueue() {
|
|
796
|
-
let uploaded = 0;
|
|
797
|
-
let failed = 0;
|
|
798
|
-
const now = Date.now();
|
|
799
|
-
const staleThreshold = now - staleUploadingTimeoutMs;
|
|
800
|
-
await sql `
|
|
801
|
-
update ${sql.table('sync_blob_outbox')}
|
|
802
|
-
set
|
|
803
|
-
${sql.ref('status')} = ${sql.val('failed')},
|
|
804
|
-
${sql.ref('attempt_count')} = ${sql.ref('attempt_count')} + ${sql.val(1)},
|
|
805
|
-
${sql.ref('error')} = ${sql.val('Upload timed out while in uploading state')},
|
|
806
|
-
${sql.ref('updated_at')} = ${sql.val(now)}
|
|
807
|
-
where ${sql.ref('status')} = ${sql.val('uploading')}
|
|
808
|
-
and ${sql.ref('updated_at')} < ${sql.val(staleThreshold)}
|
|
809
|
-
and ${sql.ref('attempt_count')} + ${sql.val(1)} >= ${sql.val(maxUploadRetries)}
|
|
810
|
-
`.execute(db);
|
|
811
|
-
await sql `
|
|
812
|
-
update ${sql.table('sync_blob_outbox')}
|
|
813
|
-
set
|
|
814
|
-
${sql.ref('status')} = ${sql.val('pending')},
|
|
815
|
-
${sql.ref('attempt_count')} = ${sql.ref('attempt_count')} + ${sql.val(1)},
|
|
816
|
-
${sql.ref('error')} = ${sql.val('Upload timed out while in uploading state; retrying')},
|
|
817
|
-
${sql.ref('updated_at')} = ${sql.val(now)}
|
|
818
|
-
where ${sql.ref('status')} = ${sql.val('uploading')}
|
|
819
|
-
and ${sql.ref('updated_at')} < ${sql.val(staleThreshold)}
|
|
820
|
-
and ${sql.ref('attempt_count')} + ${sql.val(1)} < ${sql.val(maxUploadRetries)}
|
|
821
|
-
`.execute(db);
|
|
822
|
-
const pendingResult = await sql `
|
|
823
|
-
select
|
|
824
|
-
${sql.ref('hash')},
|
|
825
|
-
${sql.ref('size')},
|
|
826
|
-
${sql.ref('mime_type')},
|
|
827
|
-
${sql.ref('body')},
|
|
828
|
-
${sql.ref('attempt_count')}
|
|
829
|
-
from ${sql.table('sync_blob_outbox')}
|
|
830
|
-
where ${sql.ref('status')} = ${sql.val('pending')}
|
|
831
|
-
and ${sql.ref('attempt_count')} < ${sql.val(maxUploadRetries)}
|
|
832
|
-
limit ${sql.val(10)}
|
|
833
|
-
`.execute(db);
|
|
834
|
-
const pending = pendingResult.rows;
|
|
835
|
-
for (const item of pending) {
|
|
836
|
-
const nextAttemptCount = item.attempt_count + 1;
|
|
837
|
-
try {
|
|
838
|
-
// Mark as uploading
|
|
839
|
-
await sql `
|
|
840
|
-
update ${sql.table('sync_blob_outbox')}
|
|
841
|
-
set
|
|
842
|
-
${sql.ref('status')} = ${sql.val('uploading')},
|
|
843
|
-
${sql.ref('attempt_count')} = ${sql.val(nextAttemptCount)},
|
|
844
|
-
${sql.ref('error')} = ${sql.val(null)},
|
|
845
|
-
${sql.ref('updated_at')} = ${sql.val(Date.now())}
|
|
846
|
-
where ${sql.ref('hash')} = ${sql.val(item.hash)}
|
|
847
|
-
and ${sql.ref('status')} = ${sql.val('pending')}
|
|
848
|
-
`.execute(db);
|
|
849
|
-
// Initiate upload
|
|
850
|
-
const initResult = await blobs.initiateUpload({
|
|
851
|
-
hash: item.hash,
|
|
852
|
-
size: item.size,
|
|
853
|
-
mimeType: item.mime_type,
|
|
854
|
-
});
|
|
855
|
-
if (!initResult.exists && initResult.uploadUrl && item.body) {
|
|
856
|
-
const uploadBody = new ArrayBuffer(item.body.byteLength);
|
|
857
|
-
new Uint8Array(uploadBody).set(item.body);
|
|
858
|
-
// Upload
|
|
859
|
-
const uploadResponse = await fetch(initResult.uploadUrl, {
|
|
860
|
-
method: initResult.uploadMethod ?? 'PUT',
|
|
861
|
-
body: uploadBody,
|
|
862
|
-
headers: initResult.uploadHeaders,
|
|
863
|
-
});
|
|
864
|
-
if (!uploadResponse.ok) {
|
|
865
|
-
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
866
|
-
}
|
|
867
|
-
// Complete
|
|
868
|
-
const completeResult = await blobs.completeUpload(item.hash);
|
|
869
|
-
if (!completeResult.ok) {
|
|
870
|
-
throw new Error(completeResult.error ?? 'Failed to complete blob upload');
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
// Mark as complete
|
|
874
|
-
await sql `
|
|
875
|
-
delete from ${sql.table('sync_blob_outbox')}
|
|
876
|
-
where ${sql.ref('hash')} = ${sql.val(item.hash)}
|
|
877
|
-
`.execute(db);
|
|
878
|
-
uploaded++;
|
|
879
|
-
}
|
|
880
|
-
catch (err) {
|
|
881
|
-
const nextStatus = nextAttemptCount >= maxUploadRetries ? 'failed' : 'pending';
|
|
882
|
-
await sql `
|
|
883
|
-
update ${sql.table('sync_blob_outbox')}
|
|
884
|
-
set
|
|
885
|
-
${sql.ref('status')} = ${sql.val(nextStatus)},
|
|
886
|
-
${sql.ref('error')} = ${sql.val(err instanceof Error ? err.message : 'Unknown error')},
|
|
887
|
-
${sql.ref('updated_at')} = ${sql.val(Date.now())}
|
|
888
|
-
where ${sql.ref('hash')} = ${sql.val(item.hash)}
|
|
889
|
-
`.execute(db);
|
|
890
|
-
if (nextStatus === 'failed') {
|
|
891
|
-
failed++;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
return { uploaded, failed };
|
|
896
|
-
},
|
|
897
|
-
async getUploadQueueStats() {
|
|
898
|
-
const rowsResult = await sql `
|
|
899
|
-
select
|
|
900
|
-
${sql.ref('status')} as status,
|
|
901
|
-
count(${sql.ref('hash')}) as count
|
|
902
|
-
from ${sql.table('sync_blob_outbox')}
|
|
903
|
-
group by ${sql.ref('status')}
|
|
904
|
-
`.execute(db);
|
|
905
|
-
const stats = { pending: 0, uploading: 0, failed: 0 };
|
|
906
|
-
for (const row of rowsResult.rows) {
|
|
907
|
-
if (row.status === 'pending')
|
|
908
|
-
stats.pending = Number(row.count);
|
|
909
|
-
if (row.status === 'uploading')
|
|
910
|
-
stats.uploading = Number(row.count);
|
|
911
|
-
if (row.status === 'failed')
|
|
912
|
-
stats.failed = Number(row.count);
|
|
913
|
-
}
|
|
914
|
-
return stats;
|
|
915
|
-
},
|
|
916
|
-
async getCacheStats() {
|
|
917
|
-
const result = await sql `
|
|
918
|
-
select
|
|
919
|
-
count(${sql.ref('hash')}) as count,
|
|
920
|
-
sum(${sql.ref('size')}) as totalBytes
|
|
921
|
-
from ${sql.table('sync_blob_cache')}
|
|
922
|
-
`.execute(db);
|
|
923
|
-
const row = result.rows[0];
|
|
924
|
-
return {
|
|
925
|
-
count: Number(row?.count ?? 0),
|
|
926
|
-
totalBytes: Number(row?.totalBytes ?? 0),
|
|
927
|
-
};
|
|
928
|
-
},
|
|
929
|
-
async pruneCache(maxBytes) {
|
|
930
|
-
if (!maxBytes)
|
|
931
|
-
return 0;
|
|
932
|
-
// Get current size
|
|
933
|
-
const stats = await this.getCacheStats();
|
|
934
|
-
if (stats.totalBytes <= maxBytes)
|
|
935
|
-
return 0;
|
|
936
|
-
// Get oldest entries to delete
|
|
937
|
-
const toFree = stats.totalBytes - maxBytes;
|
|
938
|
-
let freed = 0;
|
|
939
|
-
const oldEntriesResult = await sql `
|
|
940
|
-
select ${sql.ref('hash')}, ${sql.ref('size')}
|
|
941
|
-
from ${sql.table('sync_blob_cache')}
|
|
942
|
-
order by ${sql.ref('last_accessed_at')} asc
|
|
943
|
-
`.execute(db);
|
|
944
|
-
const oldEntries = oldEntriesResult.rows;
|
|
945
|
-
for (const entry of oldEntries) {
|
|
946
|
-
if (freed >= toFree)
|
|
947
|
-
break;
|
|
948
|
-
await storage.delete(entry.hash);
|
|
949
|
-
await sql `
|
|
950
|
-
delete from ${sql.table('sync_blob_cache')}
|
|
951
|
-
where ${sql.ref('hash')} = ${sql.val(entry.hash)}
|
|
952
|
-
`.execute(db);
|
|
953
|
-
freed += entry.size;
|
|
954
|
-
}
|
|
955
|
-
return freed;
|
|
956
|
-
},
|
|
957
|
-
async clearCache() {
|
|
958
|
-
if (storage.clear) {
|
|
959
|
-
await storage.clear();
|
|
960
|
-
}
|
|
961
|
-
else {
|
|
962
|
-
// Delete each entry individually
|
|
963
|
-
const entriesResult = await sql `
|
|
964
|
-
select ${sql.ref('hash')}
|
|
965
|
-
from ${sql.table('sync_blob_cache')}
|
|
966
|
-
`.execute(db);
|
|
967
|
-
for (const entry of entriesResult.rows) {
|
|
968
|
-
await storage.delete(entry.hash);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
await sql `delete from ${sql.table('sync_blob_cache')}`.execute(db);
|
|
972
|
-
},
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
// ============================================================================
|
|
977
|
-
// Helpers
|
|
978
|
-
// ============================================================================
|
|
979
|
-
async function toUint8Array(data) {
|
|
980
|
-
if (data instanceof Uint8Array) {
|
|
981
|
-
return data;
|
|
170
|
+
this.#pollTimer = setInterval(() => {
|
|
171
|
+
if (!this.#isOnline())
|
|
172
|
+
return;
|
|
173
|
+
void this.#syncForLifecycle();
|
|
174
|
+
}, interval);
|
|
175
|
+
}
|
|
176
|
+
#stopPolling() {
|
|
177
|
+
if (!this.#pollTimer)
|
|
178
|
+
return;
|
|
179
|
+
clearInterval(this.#pollTimer);
|
|
180
|
+
this.#pollTimer = undefined;
|
|
982
181
|
}
|
|
983
|
-
const buffer = await data.arrayBuffer();
|
|
984
|
-
return new Uint8Array(buffer);
|
|
985
|
-
}
|
|
986
|
-
async function computeSha256Hex(data) {
|
|
987
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', data.buffer);
|
|
988
|
-
const hashArray = new Uint8Array(hashBuffer);
|
|
989
|
-
return Array.from(hashArray)
|
|
990
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
991
|
-
.join('');
|
|
992
182
|
}
|
|
993
183
|
//# sourceMappingURL=client.js.map
|