@syncular/client 0.0.6-95 → 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/src/client.ts
CHANGED
|
@@ -1,1466 +1,308 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* - Optional blob support
|
|
7
|
-
* - Automatic migrations
|
|
8
|
-
* - Event-driven state management
|
|
9
|
-
* - Conflict handling with events
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type {
|
|
13
|
-
BlobRef,
|
|
14
|
-
ColumnCodecDialect,
|
|
15
|
-
ColumnCodecSource,
|
|
16
|
-
SyncTransport,
|
|
17
|
-
} from '@syncular/core';
|
|
18
|
-
import { countSyncMetric } from '@syncular/core';
|
|
19
|
-
import type { Kysely } from 'kysely';
|
|
20
|
-
import { sql } from 'kysely';
|
|
21
|
-
import { ensureClientBlobSchema } from './blobs/migrate';
|
|
22
|
-
import { SyncEngine } from './engine/SyncEngine';
|
|
1
|
+
import type { BlobRef, SyncAuthLeaseIssueRequest } from '@syncular/core';
|
|
2
|
+
import type { SyncularDatabase } from './database';
|
|
3
|
+
import { isSyncularOfflineError } from './errors';
|
|
4
|
+
import type { MutationsApi } from './mutations';
|
|
5
|
+
import { browserSyncularNetworkStatusSource } from './network';
|
|
23
6
|
import type {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Pluggable client-side blob storage adapter.
|
|
58
|
-
* Implementations handle platform-specific binary storage (OPFS, Expo FileSystem, etc.)
|
|
59
|
-
* Metadata is stored separately in the main SQLite db.
|
|
60
|
-
*/
|
|
61
|
-
export interface ClientBlobStorage {
|
|
62
|
-
/** Write blob data from bytes or stream */
|
|
63
|
-
write(
|
|
64
|
-
hash: string,
|
|
65
|
-
data: Uint8Array | ReadableStream<Uint8Array>
|
|
66
|
-
): Promise<void>;
|
|
67
|
-
|
|
68
|
-
/** Read blob data, null if not found */
|
|
69
|
-
read(hash: string): Promise<Uint8Array | null>;
|
|
70
|
-
|
|
71
|
-
/** Read blob data as stream, null if not found */
|
|
72
|
-
readStream?(hash: string): Promise<ReadableStream<Uint8Array> | null>;
|
|
73
|
-
|
|
74
|
-
/** Delete blob data */
|
|
75
|
-
delete(hash: string): Promise<void>;
|
|
76
|
-
|
|
77
|
-
/** Check if blob exists in storage */
|
|
78
|
-
exists(hash: string): Promise<boolean>;
|
|
79
|
-
|
|
80
|
-
/** Get total storage usage in bytes (for cache management) */
|
|
81
|
-
getUsage?(): Promise<number>;
|
|
82
|
-
|
|
83
|
-
/** Clear all blobs (for cache reset) */
|
|
84
|
-
clear?(): Promise<void>;
|
|
7
|
+
SyncularAuthLeaseRecord,
|
|
8
|
+
SyncularBlobUploadQueueProcessOptions,
|
|
9
|
+
SyncularBlobUploadQueueStats,
|
|
10
|
+
SyncularClientEventSink,
|
|
11
|
+
SyncularClientEventType,
|
|
12
|
+
SyncularConflictResolution,
|
|
13
|
+
SyncularConflictStats,
|
|
14
|
+
SyncularConflictSummary,
|
|
15
|
+
SyncularConnectionState,
|
|
16
|
+
SyncularDiagnosticEvent,
|
|
17
|
+
SyncularDiagnosticSnapshot,
|
|
18
|
+
SyncularLifecycleState,
|
|
19
|
+
SyncularNetworkStatusSource,
|
|
20
|
+
SyncularOutboxStats,
|
|
21
|
+
SyncularPresenceEntry,
|
|
22
|
+
SyncularPresenceSink,
|
|
23
|
+
SyncularRealtimeConnectionState,
|
|
24
|
+
SyncularRealtimeOptions,
|
|
25
|
+
SyncularRuntimeClient,
|
|
26
|
+
SyncularSubscriptionSpec,
|
|
27
|
+
SyncularSyncRequestOptions,
|
|
28
|
+
SyncularSyncResult,
|
|
29
|
+
} from './types';
|
|
30
|
+
|
|
31
|
+
export interface SyncularClientLifecycleOptions {
|
|
32
|
+
initialSync?: boolean;
|
|
33
|
+
realtime?: boolean | SyncularRealtimeOptions;
|
|
34
|
+
syncOnRealtimeConnect?: boolean;
|
|
35
|
+
pollIntervalMs?: number | false;
|
|
36
|
+
network?: SyncularNetworkStatusSource | false;
|
|
37
|
+
subscriptions?: readonly SyncularSubscriptionSpec[];
|
|
85
38
|
}
|
|
86
39
|
|
|
87
|
-
export interface
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
/** Table handlers for applying snapshots and changes */
|
|
95
|
-
tableHandlers: ClientHandlerCollection<DB>;
|
|
96
|
-
|
|
97
|
-
/** Unique client identifier (e.g., device ID) */
|
|
98
|
-
clientId: string;
|
|
99
|
-
|
|
100
|
-
/** Current actor/user identifier */
|
|
101
|
-
actorId: string;
|
|
102
|
-
|
|
103
|
-
/** Subscriptions to sync */
|
|
104
|
-
subscriptions: Array<{
|
|
105
|
-
id: string;
|
|
106
|
-
table: string;
|
|
107
|
-
scopes?: Record<string, string | string[]>;
|
|
108
|
-
params?: Record<string, unknown>;
|
|
109
|
-
}>;
|
|
110
|
-
|
|
111
|
-
/** Optional: Local blob storage adapter (enables blob support) */
|
|
112
|
-
blobStorage?: ClientBlobStorage;
|
|
113
|
-
|
|
114
|
-
/** Optional: Sync plugins */
|
|
115
|
-
plugins?: SyncClientPlugin[];
|
|
116
|
-
|
|
117
|
-
/** Optional: Enable realtime transport mode */
|
|
118
|
-
realtimeEnabled?: boolean;
|
|
119
|
-
|
|
120
|
-
/** Optional: Polling interval in milliseconds (default: 10000) */
|
|
121
|
-
pollIntervalMs?: number;
|
|
122
|
-
|
|
123
|
-
/** Optional: State ID for multi-tenant scenarios */
|
|
124
|
-
stateId?: string;
|
|
125
|
-
|
|
126
|
-
/** Optional: ID column name (default: 'id') */
|
|
127
|
-
idColumn?: string;
|
|
128
|
-
|
|
129
|
-
/** Optional: Version column name (default: 'server_version') */
|
|
130
|
-
versionColumn?: string;
|
|
131
|
-
|
|
132
|
-
/** Optional: Columns to omit from sync */
|
|
133
|
-
omitColumns?: string[];
|
|
134
|
-
|
|
135
|
-
/** Optional: Column codec resolver */
|
|
136
|
-
codecs?: ColumnCodecSource;
|
|
137
|
-
|
|
138
|
-
/** Optional: Codec dialect override (default: 'sqlite') */
|
|
139
|
-
codecDialect?: ColumnCodecDialect;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export interface ClientState {
|
|
143
|
-
/** Client ID */
|
|
144
|
-
clientId: string;
|
|
145
|
-
/** Actor ID */
|
|
146
|
-
actorId: string;
|
|
147
|
-
/** Whether sync is enabled (actorId and clientId are set) */
|
|
148
|
-
enabled: boolean;
|
|
149
|
-
/** Whether a sync is currently in progress */
|
|
40
|
+
export interface SyncularClientStatus {
|
|
41
|
+
lifecycle: SyncularLifecycleState;
|
|
42
|
+
connection: SyncularConnectionState;
|
|
43
|
+
outbox: SyncularOutboxStats | null;
|
|
44
|
+
conflicts: SyncularConflictStats | null;
|
|
45
|
+
isConnected: boolean;
|
|
150
46
|
isSyncing: boolean;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
lastSyncAt: number | null;
|
|
155
|
-
/** Current error if any */
|
|
156
|
-
error: { code: string; message: string } | null;
|
|
157
|
-
/** Outbox statistics */
|
|
158
|
-
outbox: OutboxStats;
|
|
47
|
+
hasPendingMutations: boolean;
|
|
48
|
+
hasConflicts: boolean;
|
|
49
|
+
requiresAction: boolean;
|
|
159
50
|
}
|
|
160
51
|
|
|
161
|
-
export interface
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
serverPayload: Record<string, unknown> | null;
|
|
168
|
-
serverVersion: number | null;
|
|
169
|
-
message: string;
|
|
170
|
-
code: string | null;
|
|
171
|
-
createdAt: number;
|
|
52
|
+
export interface SyncularBlobClientLike {
|
|
53
|
+
getUploadQueueStats(): Promise<SyncularBlobUploadQueueStats>;
|
|
54
|
+
processUploadQueue(
|
|
55
|
+
options?: SyncularBlobUploadQueueProcessOptions
|
|
56
|
+
): Promise<{ uploaded: number; failed: number }>;
|
|
57
|
+
retrieve(ref: BlobRef): Promise<Uint8Array>;
|
|
172
58
|
}
|
|
173
59
|
|
|
174
|
-
export interface
|
|
175
|
-
|
|
176
|
-
|
|
60
|
+
export interface SyncularPresenceClientLike {
|
|
61
|
+
get<TMetadata = Record<string, unknown>>(
|
|
62
|
+
scopeKey: string
|
|
63
|
+
): SyncularPresenceEntry<TMetadata>[];
|
|
64
|
+
join(scopeKey: string, metadata?: Record<string, unknown>): void;
|
|
65
|
+
leave(scopeKey: string): void;
|
|
66
|
+
updateMetadata(scopeKey: string, metadata: Record<string, unknown>): void;
|
|
67
|
+
onChange<TMetadata = Record<string, unknown>>(
|
|
68
|
+
listener: SyncularPresenceSink<TMetadata>
|
|
69
|
+
): () => void;
|
|
177
70
|
}
|
|
178
71
|
|
|
179
|
-
interface
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
immediate?: boolean;
|
|
72
|
+
export interface SyncularConflictsClientLike {
|
|
73
|
+
list(): Promise<SyncularConflictSummary[]>;
|
|
74
|
+
retryKeepLocal(id: string): Promise<string>;
|
|
75
|
+
resolve(id: string, resolution: SyncularConflictResolution): Promise<void>;
|
|
184
76
|
}
|
|
185
77
|
|
|
186
|
-
export interface
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
clearCache(): Promise<void>;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export interface MigrationInfo {
|
|
223
|
-
/** Whether sync schema is migrated */
|
|
224
|
-
syncMigrated: boolean;
|
|
225
|
-
/** Whether blob schema is migrated */
|
|
226
|
-
blobsMigrated: boolean;
|
|
78
|
+
export interface SyncularClientLike<DB> {
|
|
79
|
+
db: SyncularDatabase<DB>['db'];
|
|
80
|
+
dialect?: SyncularDatabase<DB>['dialect'] | unknown;
|
|
81
|
+
mutations: MutationsApi<DB, any>;
|
|
82
|
+
leasedMutations: MutationsApi<DB, any>;
|
|
83
|
+
blobs: SyncularBlobClientLike;
|
|
84
|
+
on<T extends SyncularClientEventType>(
|
|
85
|
+
event: T,
|
|
86
|
+
listener: SyncularClientEventSink<T>
|
|
87
|
+
): () => void;
|
|
88
|
+
getStatus(): SyncularClientStatus;
|
|
89
|
+
setSubscriptions(
|
|
90
|
+
subscriptions: readonly SyncularSubscriptionSpec[]
|
|
91
|
+
): Promise<void>;
|
|
92
|
+
resumeFromBackground(
|
|
93
|
+
options?: SyncularSyncRequestOptions
|
|
94
|
+
): Promise<SyncularSyncResult>;
|
|
95
|
+
issueAuthLease(
|
|
96
|
+
request: SyncAuthLeaseIssueRequest
|
|
97
|
+
): Promise<SyncularAuthLeaseRecord>;
|
|
98
|
+
upsertAuthLease(lease: SyncularAuthLeaseRecord): Promise<void>;
|
|
99
|
+
authLease(leaseId: string): Promise<SyncularAuthLeaseRecord | null>;
|
|
100
|
+
activeAuthLeases(
|
|
101
|
+
actorId?: string | null,
|
|
102
|
+
nowMs?: number
|
|
103
|
+
): Promise<SyncularAuthLeaseRecord[]>;
|
|
104
|
+
diagnosticSnapshot(): Promise<SyncularDiagnosticSnapshot>;
|
|
105
|
+
presence: SyncularPresenceClientLike;
|
|
106
|
+
conflicts: SyncularConflictsClientLike;
|
|
107
|
+
start(): Promise<void>;
|
|
108
|
+
stop(): Promise<void>;
|
|
109
|
+
sync(): Promise<SyncularSyncResult>;
|
|
110
|
+
close(): Promise<void>;
|
|
227
111
|
}
|
|
228
112
|
|
|
229
|
-
type
|
|
230
|
-
|
|
231
|
-
| '
|
|
232
|
-
| '
|
|
233
|
-
| '
|
|
234
|
-
| '
|
|
235
|
-
| '
|
|
236
|
-
| '
|
|
237
|
-
| '
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
| '
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
stateId: string;
|
|
259
|
-
subscriptionId: string;
|
|
260
|
-
progress: SubscriptionProgress;
|
|
261
|
-
};
|
|
262
|
-
'bootstrap:complete': {
|
|
263
|
-
timestamp: number;
|
|
264
|
-
stateId: string;
|
|
265
|
-
subscriptionId: string;
|
|
266
|
-
durationMs: number;
|
|
267
|
-
};
|
|
268
|
-
'connection:change': { previous: string; current: string };
|
|
269
|
-
'data:change': { scopes: string[]; timestamp: number };
|
|
270
|
-
'outbox:change': OutboxStats;
|
|
271
|
-
'conflict:new': Conflict;
|
|
272
|
-
'conflict:resolved': Conflict;
|
|
273
|
-
'blob:upload:complete': BlobRef;
|
|
274
|
-
'blob:upload:error': { hash: string; error: string };
|
|
275
|
-
'presence:change': { scopeKey: string; presence: PresenceEntry[] };
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
type ClientEventHandler<E extends ClientEventType> = (
|
|
279
|
-
payload: ClientEventPayloads[E]
|
|
280
|
-
) => void;
|
|
281
|
-
|
|
282
|
-
// ============================================================================
|
|
283
|
-
// Client Class
|
|
284
|
-
// ============================================================================
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Unified sync client.
|
|
288
|
-
*
|
|
289
|
-
* @example
|
|
290
|
-
* ```typescript
|
|
291
|
-
* import { Client } from '@syncular/client';
|
|
292
|
-
* import { createHttpTransport } from '@syncular/transport-http';
|
|
293
|
-
*
|
|
294
|
-
* const client = new Client({
|
|
295
|
-
* db,
|
|
296
|
-
* transport: createHttpTransport({ baseUrl: '/api/sync', getHeaders }),
|
|
297
|
-
* tableHandlers,
|
|
298
|
-
* clientId: 'device-123',
|
|
299
|
-
* actorId: 'user-456',
|
|
300
|
-
* subscriptions: [{ id: 'tasks', table: 'tasks', scopes: { user_id: 'user-456' } }],
|
|
301
|
-
* });
|
|
302
|
-
*
|
|
303
|
-
* await client.start();
|
|
304
|
-
*
|
|
305
|
-
* // Mutations
|
|
306
|
-
* await client.mutations.tasks.insert({ title: 'New task' });
|
|
307
|
-
*
|
|
308
|
-
* // Events
|
|
309
|
-
* client.on('sync:complete', () => console.log('synced'));
|
|
310
|
-
* ```
|
|
311
|
-
*/
|
|
312
|
-
export class Client<DB extends SyncClientDb = SyncClientDb> {
|
|
313
|
-
private readonly options: ClientOptions<DB>;
|
|
314
|
-
private engine: SyncEngine<DB> | null = null;
|
|
315
|
-
private started = false;
|
|
316
|
-
private destroyed = false;
|
|
317
|
-
private emittedConflictIds = new Set<string>();
|
|
318
|
-
private eventListeners = new Map<
|
|
319
|
-
ClientEventType,
|
|
320
|
-
Set<ClientEventHandler<any>>
|
|
321
|
-
>();
|
|
322
|
-
private outboxStats: OutboxStats = {
|
|
323
|
-
pending: 0,
|
|
324
|
-
sending: 0,
|
|
325
|
-
failed: 0,
|
|
326
|
-
acked: 0,
|
|
327
|
-
total: 0,
|
|
113
|
+
type LifecycleClient = Pick<
|
|
114
|
+
SyncularRuntimeClient,
|
|
115
|
+
| 'addDiagnosticListener'
|
|
116
|
+
| 'connectionState'
|
|
117
|
+
| 'forceSubscriptionsBootstrap'
|
|
118
|
+
| 'setSubscriptions'
|
|
119
|
+
| 'startRealtime'
|
|
120
|
+
| 'stopRealtime'
|
|
121
|
+
| 'syncOnce'
|
|
122
|
+
>;
|
|
123
|
+
|
|
124
|
+
export function getSyncularClientStatus(
|
|
125
|
+
client: Pick<SyncularRuntimeClient, 'connectionState' | 'lifecycleState'>
|
|
126
|
+
): SyncularClientStatus {
|
|
127
|
+
const lifecycle = client.lifecycleState();
|
|
128
|
+
const connection = client.connectionState();
|
|
129
|
+
const outbox = lifecycle.outbox ?? null;
|
|
130
|
+
const conflicts = lifecycle.conflicts ?? null;
|
|
131
|
+
return {
|
|
132
|
+
lifecycle,
|
|
133
|
+
connection,
|
|
134
|
+
outbox,
|
|
135
|
+
conflicts,
|
|
136
|
+
isConnected: connection.realtime === 'connected' && !connection.closed,
|
|
137
|
+
isSyncing:
|
|
138
|
+
lifecycle.phase === 'syncing' || lifecycle.phase === 'recovering',
|
|
139
|
+
hasPendingMutations: (outbox?.pending ?? 0) + (outbox?.sending ?? 0) > 0,
|
|
140
|
+
hasConflicts: (conflicts?.unresolved ?? 0) > 0,
|
|
141
|
+
requiresAction: lifecycle.requiresAction,
|
|
328
142
|
};
|
|
143
|
+
}
|
|
329
144
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
// Create blob client if storage provided
|
|
351
|
-
if (options.blobStorage && options.transport.blobs) {
|
|
352
|
-
this.blobs = this.createBlobClient(
|
|
353
|
-
options.blobStorage,
|
|
354
|
-
options.transport
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// ===========================================================================
|
|
360
|
-
// Identity Getters
|
|
361
|
-
// ===========================================================================
|
|
362
|
-
|
|
363
|
-
/** Client ID */
|
|
364
|
-
get clientId(): string {
|
|
365
|
-
return this.options.clientId;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** Actor ID */
|
|
369
|
-
get actorId(): string {
|
|
370
|
-
return this.options.actorId;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/** Database instance */
|
|
374
|
-
get db(): Kysely<DB> {
|
|
375
|
-
return this.options.db;
|
|
145
|
+
export class SyncularClientLifecycle {
|
|
146
|
+
#started = false;
|
|
147
|
+
#pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
148
|
+
#unsubscribeDiagnostics: (() => void) | undefined;
|
|
149
|
+
#unsubscribeNetwork: (() => void) | undefined;
|
|
150
|
+
#syncInFlight: Promise<SyncularSyncResult> | undefined;
|
|
151
|
+
#syncAgain = false;
|
|
152
|
+
#hasConnectedRealtime = false;
|
|
153
|
+
#realtimeStarted = false;
|
|
154
|
+
readonly #network: SyncularNetworkStatusSource | undefined;
|
|
155
|
+
|
|
156
|
+
constructor(
|
|
157
|
+
private readonly client: LifecycleClient,
|
|
158
|
+
private readonly options: SyncularClientLifecycleOptions = {}
|
|
159
|
+
) {
|
|
160
|
+
this.#network =
|
|
161
|
+
options.network === false
|
|
162
|
+
? undefined
|
|
163
|
+
: (options.network ?? browserSyncularNetworkStatusSource());
|
|
376
164
|
}
|
|
377
165
|
|
|
378
|
-
// ===========================================================================
|
|
379
|
-
// Lifecycle
|
|
380
|
-
// ===========================================================================
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Start the client.
|
|
384
|
-
* Runs migrations and starts sync engine.
|
|
385
|
-
*/
|
|
386
166
|
async start(): Promise<void> {
|
|
387
|
-
if (this
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
scopes: s.scopes ?? {},
|
|
411
|
-
params: s.params ?? {},
|
|
412
|
-
})),
|
|
413
|
-
plugins: this.options.plugins,
|
|
414
|
-
realtimeEnabled: this.options.realtimeEnabled,
|
|
415
|
-
pollIntervalMs: this.options.pollIntervalMs,
|
|
416
|
-
stateId: this.options.stateId,
|
|
417
|
-
migrate: undefined, // We already ran migrations
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
// Wire up engine events to client events
|
|
421
|
-
this.wireEngineEvents();
|
|
422
|
-
|
|
423
|
-
await this.engine.start();
|
|
424
|
-
this.started = true;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Stop the client (can be restarted).
|
|
429
|
-
*/
|
|
430
|
-
stop(): void {
|
|
431
|
-
this.engine?.stop();
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Destroy the client (cannot be restarted).
|
|
436
|
-
*/
|
|
437
|
-
destroy(): void {
|
|
438
|
-
this.engine?.destroy();
|
|
439
|
-
this.eventListeners.clear();
|
|
440
|
-
this.destroyed = true;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// ===========================================================================
|
|
444
|
-
// Sync
|
|
445
|
-
// ===========================================================================
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Trigger a manual sync.
|
|
449
|
-
*/
|
|
450
|
-
async sync(): Promise<SyncResult> {
|
|
451
|
-
if (!this.engine) {
|
|
452
|
-
throw new Error('Client not started');
|
|
453
|
-
}
|
|
454
|
-
return this.engine.sync();
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// ===========================================================================
|
|
458
|
-
// Subscriptions
|
|
459
|
-
// ===========================================================================
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Update subscriptions.
|
|
463
|
-
*/
|
|
464
|
-
updateSubscriptions(
|
|
465
|
-
subscriptions: Array<{
|
|
466
|
-
id: string;
|
|
467
|
-
table: string;
|
|
468
|
-
scopes?: Record<string, string | string[]>;
|
|
469
|
-
params?: Record<string, unknown>;
|
|
470
|
-
}>
|
|
471
|
-
): void {
|
|
472
|
-
this.options.subscriptions = subscriptions;
|
|
473
|
-
if (this.engine) {
|
|
474
|
-
this.engine.updateSubscriptions(
|
|
475
|
-
subscriptions.map((s) => ({
|
|
476
|
-
id: s.id,
|
|
477
|
-
table: s.table,
|
|
478
|
-
scopes: s.scopes ?? {},
|
|
479
|
-
params: s.params ?? {},
|
|
480
|
-
}))
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Get current subscriptions.
|
|
487
|
-
*/
|
|
488
|
-
getSubscriptions(): Array<{
|
|
489
|
-
id: string;
|
|
490
|
-
table: string;
|
|
491
|
-
scopes: Record<string, string | string[]>;
|
|
492
|
-
params: Record<string, unknown>;
|
|
493
|
-
}> {
|
|
494
|
-
return this.options.subscriptions.map((s) => ({
|
|
495
|
-
id: s.id,
|
|
496
|
-
table: s.table,
|
|
497
|
-
scopes: s.scopes ?? {},
|
|
498
|
-
params: s.params ?? {},
|
|
499
|
-
}));
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* List persisted subscription metadata rows.
|
|
504
|
-
*/
|
|
505
|
-
async listSubscriptionStates(args?: {
|
|
506
|
-
stateId?: string;
|
|
507
|
-
table?: string;
|
|
508
|
-
status?: 'active' | 'revoked';
|
|
509
|
-
}): Promise<SubscriptionState[]> {
|
|
510
|
-
if (!this.engine) return [];
|
|
511
|
-
return this.engine.listSubscriptionStates(args);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Read one persisted subscription metadata row.
|
|
516
|
-
*/
|
|
517
|
-
async getSubscriptionState(
|
|
518
|
-
subscriptionId: string,
|
|
519
|
-
options?: { stateId?: string }
|
|
520
|
-
): Promise<SubscriptionState | null> {
|
|
521
|
-
if (!this.engine) return null;
|
|
522
|
-
return this.engine.getSubscriptionState(subscriptionId, options);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// ===========================================================================
|
|
526
|
-
// State
|
|
527
|
-
// ===========================================================================
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Get current client state.
|
|
531
|
-
*/
|
|
532
|
-
getState(): ClientState {
|
|
533
|
-
const engineState =
|
|
534
|
-
this.engine?.getState() ?? this.createInitialEngineState();
|
|
535
|
-
return {
|
|
536
|
-
clientId: this.options.clientId,
|
|
537
|
-
actorId: this.options.actorId,
|
|
538
|
-
enabled: engineState.enabled,
|
|
539
|
-
isSyncing: engineState.isSyncing,
|
|
540
|
-
connectionState: engineState.connectionState,
|
|
541
|
-
lastSyncAt: engineState.lastSyncAt,
|
|
542
|
-
error: engineState.error
|
|
543
|
-
? { code: engineState.error.code, message: engineState.error.message }
|
|
544
|
-
: null,
|
|
545
|
-
outbox: this.outboxStats,
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Get current transport health details.
|
|
551
|
-
*/
|
|
552
|
-
getTransportHealth(): TransportHealth | null {
|
|
553
|
-
if (!this.engine) return null;
|
|
554
|
-
return this.engine.getTransportHealth();
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Get computed sync progress across subscriptions.
|
|
559
|
-
*/
|
|
560
|
-
async getProgress(): Promise<SyncProgress | null> {
|
|
561
|
-
if (!this.engine) return null;
|
|
562
|
-
return this.engine.getProgress();
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Get a diagnostics snapshot for support/debug flows.
|
|
567
|
-
*/
|
|
568
|
-
async getDiagnostics(): Promise<SyncDiagnostics | null> {
|
|
569
|
-
if (!this.engine) return null;
|
|
570
|
-
return this.engine.getDiagnostics();
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Get a serializable inspector snapshot for in-app debug tooling.
|
|
575
|
-
*/
|
|
576
|
-
async getInspectorSnapshot(
|
|
577
|
-
options?: SyncInspectorOptions
|
|
578
|
-
): Promise<SyncInspectorSnapshot | null> {
|
|
579
|
-
if (!this.engine) return null;
|
|
580
|
-
return this.engine.getInspectorSnapshot(options);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Reset local sync metadata (and optionally synced app rows/outbox/conflicts).
|
|
585
|
-
*/
|
|
586
|
-
async reset(options: SyncResetOptions): Promise<SyncResetResult> {
|
|
587
|
-
if (!this.engine) {
|
|
588
|
-
return {
|
|
589
|
-
deletedSubscriptionStates: 0,
|
|
590
|
-
deletedOutboxCommits: 0,
|
|
591
|
-
deletedConflicts: 0,
|
|
592
|
-
clearedTables: [],
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
return this.engine.reset(options);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Run a built-in repair flow for common corruption scenarios.
|
|
600
|
-
*/
|
|
601
|
-
async repair(options: SyncRepairOptions): Promise<SyncResetResult> {
|
|
602
|
-
if (!this.engine) {
|
|
603
|
-
return {
|
|
604
|
-
deletedSubscriptionStates: 0,
|
|
605
|
-
deletedOutboxCommits: 0,
|
|
606
|
-
deletedConflicts: 0,
|
|
607
|
-
clearedTables: [],
|
|
608
|
-
};
|
|
167
|
+
if (this.#started) return;
|
|
168
|
+
this.#started = true;
|
|
169
|
+
this.#hasConnectedRealtime =
|
|
170
|
+
this.client.connectionState().realtime === 'connected';
|
|
171
|
+
this.#realtimeStarted = this.#hasConnectedRealtime;
|
|
172
|
+
this.#unsubscribeDiagnostics = this.client.addDiagnosticListener((event) =>
|
|
173
|
+
this.#handleDiagnostic(event)
|
|
174
|
+
);
|
|
175
|
+
this.#unsubscribeNetwork = this.#subscribeNetworkEvents();
|
|
176
|
+
try {
|
|
177
|
+
if (this.options.subscriptions) {
|
|
178
|
+
await this.client.setSubscriptions(this.options.subscriptions);
|
|
179
|
+
}
|
|
180
|
+
if (this.options.initialSync !== false && this.#isOnline()) {
|
|
181
|
+
await this.#syncForLifecycle();
|
|
182
|
+
}
|
|
183
|
+
if (this.options.realtime !== false && this.#isOnline()) {
|
|
184
|
+
await this.#startRealtimeForLifecycle();
|
|
185
|
+
}
|
|
186
|
+
this.#startPolling();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
await this.stop().catch(() => undefined);
|
|
189
|
+
throw error;
|
|
609
190
|
}
|
|
610
|
-
return this.engine.repair(options);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* Wait until the channel reaches a target phase.
|
|
615
|
-
*/
|
|
616
|
-
async awaitPhase(
|
|
617
|
-
phase: SyncProgress['channelPhase'],
|
|
618
|
-
options: SyncAwaitPhaseOptions = {}
|
|
619
|
-
): Promise<SyncProgress | null> {
|
|
620
|
-
if (!this.engine) return null;
|
|
621
|
-
return this.engine.awaitPhase(phase, options);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Wait until bootstrap completes for the default state or a specific subscription.
|
|
626
|
-
*/
|
|
627
|
-
async awaitBootstrapComplete(
|
|
628
|
-
options: SyncAwaitBootstrapOptions = {}
|
|
629
|
-
): Promise<SyncProgress | null> {
|
|
630
|
-
if (!this.engine) return null;
|
|
631
|
-
return this.engine.awaitBootstrapComplete(options);
|
|
632
191
|
}
|
|
633
192
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
193
|
+
async stop(): Promise<void> {
|
|
194
|
+
if (!this.#started) return;
|
|
195
|
+
this.#started = false;
|
|
196
|
+
this.#stopPolling();
|
|
197
|
+
this.#unsubscribeDiagnostics?.();
|
|
198
|
+
this.#unsubscribeDiagnostics = undefined;
|
|
199
|
+
this.#unsubscribeNetwork?.();
|
|
200
|
+
this.#unsubscribeNetwork = undefined;
|
|
201
|
+
this.#syncAgain = false;
|
|
202
|
+
if (this.options.realtime !== false) {
|
|
203
|
+
await this.client.stopRealtime();
|
|
641
204
|
}
|
|
642
|
-
|
|
205
|
+
this.#realtimeStarted = false;
|
|
643
206
|
}
|
|
644
207
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
/**
|
|
650
|
-
* Subscribe to client events.
|
|
651
|
-
*/
|
|
652
|
-
on<E extends ClientEventType>(
|
|
653
|
-
event: E,
|
|
654
|
-
handler: ClientEventHandler<E>
|
|
655
|
-
): () => void {
|
|
656
|
-
if (!this.eventListeners.has(event)) {
|
|
657
|
-
this.eventListeners.set(event, new Set());
|
|
208
|
+
async sync(): Promise<SyncularSyncResult> {
|
|
209
|
+
if (this.#syncInFlight) {
|
|
210
|
+
this.#syncAgain = true;
|
|
211
|
+
return this.#syncInFlight;
|
|
658
212
|
}
|
|
659
|
-
this.
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
private emit<E extends ClientEventType>(
|
|
667
|
-
event: E,
|
|
668
|
-
payload: ClientEventPayloads[E]
|
|
669
|
-
): void {
|
|
670
|
-
const listeners = this.eventListeners.get(event);
|
|
671
|
-
if (listeners) {
|
|
672
|
-
for (const listener of listeners) {
|
|
673
|
-
try {
|
|
674
|
-
listener(payload);
|
|
675
|
-
} catch (err) {
|
|
676
|
-
console.error(`[Client] Error in ${event} listener:`, err);
|
|
677
|
-
}
|
|
213
|
+
this.#syncInFlight = this.client.syncOnce().finally(() => {
|
|
214
|
+
this.#syncInFlight = undefined;
|
|
215
|
+
if (this.#syncAgain && this.#started) {
|
|
216
|
+
this.#syncAgain = false;
|
|
217
|
+
void this.sync().catch(() => undefined);
|
|
678
218
|
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// ===========================================================================
|
|
683
|
-
// Conflicts
|
|
684
|
-
// ===========================================================================
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Get pending conflicts.
|
|
688
|
-
*/
|
|
689
|
-
async getConflicts(): Promise<Conflict[]> {
|
|
690
|
-
if (!this.engine) {
|
|
691
|
-
return [];
|
|
692
|
-
}
|
|
693
|
-
const conflicts = await this.engine.getConflicts();
|
|
694
|
-
return conflicts.map((c) => this.mapConflictInfo(c));
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Resolve a conflict.
|
|
699
|
-
*/
|
|
700
|
-
async resolveConflict(
|
|
701
|
-
id: string,
|
|
702
|
-
resolution: ConflictResolution
|
|
703
|
-
): Promise<void> {
|
|
704
|
-
const { resolveConflict } = await import('./conflicts');
|
|
705
|
-
const pendingBeforeResolve = await this.getConflicts();
|
|
706
|
-
const resolvedConflict = pendingBeforeResolve.find((c) => c.id === id);
|
|
707
|
-
|
|
708
|
-
// For 'keep-local' and 'keep-server', we just mark it resolved
|
|
709
|
-
// For 'custom', we would need to apply the payload - but that requires
|
|
710
|
-
// creating a new mutation, which the user should do separately
|
|
711
|
-
const resolutionStr =
|
|
712
|
-
resolution.strategy === 'custom'
|
|
713
|
-
? `custom:${JSON.stringify(resolution.payload)}`
|
|
714
|
-
: resolution.strategy;
|
|
715
|
-
|
|
716
|
-
await resolveConflict(this.options.db, { id, resolution: resolutionStr });
|
|
717
|
-
|
|
718
|
-
countSyncMetric('sync.conflicts.resolved', 1, {
|
|
719
|
-
attributes: {
|
|
720
|
-
strategy: resolution.strategy,
|
|
721
|
-
},
|
|
722
219
|
});
|
|
723
|
-
|
|
724
|
-
this.emittedConflictIds.delete(id);
|
|
725
|
-
if (resolvedConflict) {
|
|
726
|
-
this.emit('conflict:resolved', resolvedConflict);
|
|
727
|
-
}
|
|
220
|
+
return this.#syncInFlight;
|
|
728
221
|
}
|
|
729
222
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
async getOutboxStats(): Promise<OutboxStats> {
|
|
738
|
-
if (!this.engine) {
|
|
739
|
-
return this.outboxStats;
|
|
223
|
+
#handleDiagnostic(event: SyncularDiagnosticEvent): void {
|
|
224
|
+
if (event.source === 'sync' && event.details?.resyncRequired === true) {
|
|
225
|
+
void this.client
|
|
226
|
+
.forceSubscriptionsBootstrap()
|
|
227
|
+
.then(() => this.sync())
|
|
228
|
+
.catch(() => undefined);
|
|
229
|
+
return;
|
|
740
230
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
*/
|
|
748
|
-
async clearFailedCommits(): Promise<number> {
|
|
749
|
-
if (!this.engine) {
|
|
750
|
-
return 0;
|
|
231
|
+
if (
|
|
232
|
+
event.source !== 'realtime' ||
|
|
233
|
+
event.code !== 'realtime.state' ||
|
|
234
|
+
event.details?.state == null
|
|
235
|
+
) {
|
|
236
|
+
return;
|
|
751
237
|
}
|
|
752
|
-
|
|
238
|
+
const state = event.details.state as SyncularRealtimeConnectionState;
|
|
239
|
+
if (state !== 'connected') return;
|
|
240
|
+
const wasReconnect = this.#hasConnectedRealtime;
|
|
241
|
+
this.#hasConnectedRealtime = true;
|
|
242
|
+
const shouldSync =
|
|
243
|
+
this.options.syncOnRealtimeConnect !== false &&
|
|
244
|
+
(wasReconnect || this.options.initialSync === false);
|
|
245
|
+
if (!shouldSync) return;
|
|
246
|
+
void this.sync().catch(() => undefined);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#subscribeNetworkEvents(): (() => void) | undefined {
|
|
250
|
+
const network = this.#network;
|
|
251
|
+
if (!network?.addEventListener || !network.removeEventListener) return;
|
|
252
|
+
const handleOnline = () => {
|
|
253
|
+
if (!this.#started) return;
|
|
254
|
+
void this.#resumeOnline().catch(() => undefined);
|
|
255
|
+
};
|
|
256
|
+
network.addEventListener('online', handleOnline);
|
|
257
|
+
return () => {
|
|
258
|
+
network.removeEventListener?.('online', handleOnline);
|
|
259
|
+
};
|
|
753
260
|
}
|
|
754
261
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
// Mark failed commits as pending and trigger sync
|
|
760
|
-
const result = await sql`
|
|
761
|
-
update ${sql.table('sync_outbox_commits')}
|
|
762
|
-
set
|
|
763
|
-
${sql.ref('status')} = ${sql.val('pending')},
|
|
764
|
-
${sql.ref('attempt_count')} = ${sql.val(0)},
|
|
765
|
-
${sql.ref('error')} = ${sql.val(null)}
|
|
766
|
-
where ${sql.ref('status')} = ${sql.val('failed')}
|
|
767
|
-
`.execute(this.options.db);
|
|
768
|
-
|
|
769
|
-
const count = Number(result.numAffectedRows ?? 0n);
|
|
770
|
-
if (count > 0 && this.engine) {
|
|
771
|
-
await this.engine.refreshOutboxStats();
|
|
772
|
-
await this.engine.sync();
|
|
262
|
+
async #resumeOnline(): Promise<void> {
|
|
263
|
+
if (!this.#started || !this.#isOnline()) return;
|
|
264
|
+
if (this.options.initialSync !== false) {
|
|
265
|
+
await this.#syncForLifecycle();
|
|
773
266
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
// ===========================================================================
|
|
778
|
-
// Presence
|
|
779
|
-
// ===========================================================================
|
|
780
|
-
|
|
781
|
-
/**
|
|
782
|
-
* Get presence for a scope.
|
|
783
|
-
*/
|
|
784
|
-
getPresence<TMetadata = Record<string, unknown>>(
|
|
785
|
-
scopeKey: string
|
|
786
|
-
): PresenceEntry<TMetadata>[] {
|
|
787
|
-
if (!this.engine) {
|
|
788
|
-
return [];
|
|
267
|
+
if (this.options.realtime !== false && !this.#realtimeStarted) {
|
|
268
|
+
await this.#startRealtimeForLifecycle();
|
|
789
269
|
}
|
|
790
|
-
return this.engine.getPresence<TMetadata>(scopeKey);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
/**
|
|
794
|
-
* Join presence for a scope key.
|
|
795
|
-
*/
|
|
796
|
-
joinPresence(scopeKey: string, metadata?: Record<string, unknown>): void {
|
|
797
|
-
this.engine?.joinPresence(scopeKey, metadata);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
/**
|
|
801
|
-
* Leave presence for a scope key.
|
|
802
|
-
*/
|
|
803
|
-
leavePresence(scopeKey: string): void {
|
|
804
|
-
this.engine?.leavePresence(scopeKey);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Update presence metadata for a scope key.
|
|
809
|
-
*/
|
|
810
|
-
updatePresenceMetadata(
|
|
811
|
-
scopeKey: string,
|
|
812
|
-
metadata: Record<string, unknown>
|
|
813
|
-
): void {
|
|
814
|
-
this.engine?.updatePresenceMetadata(scopeKey, metadata);
|
|
815
270
|
}
|
|
816
271
|
|
|
817
|
-
|
|
818
|
-
// Migration Info
|
|
819
|
-
// ===========================================================================
|
|
820
|
-
|
|
821
|
-
/**
|
|
822
|
-
* Get migration info.
|
|
823
|
-
*/
|
|
824
|
-
async getMigrationInfo(): Promise<MigrationInfo> {
|
|
825
|
-
// Check if sync tables exist
|
|
826
|
-
let syncMigrated = false;
|
|
272
|
+
async #syncForLifecycle(): Promise<void> {
|
|
827
273
|
try {
|
|
828
|
-
await this.
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
.limit(1)
|
|
832
|
-
.execute();
|
|
833
|
-
syncMigrated = true;
|
|
834
|
-
} catch {
|
|
835
|
-
syncMigrated = false;
|
|
274
|
+
await this.sync();
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (!isSyncularOfflineError(error)) throw error;
|
|
836
277
|
}
|
|
837
|
-
|
|
838
|
-
// Check if blob tables exist
|
|
839
|
-
let blobsMigrated = false;
|
|
840
|
-
try {
|
|
841
|
-
await this.options.db
|
|
842
|
-
.selectFrom('sync_blob_cache')
|
|
843
|
-
.selectAll()
|
|
844
|
-
.limit(1)
|
|
845
|
-
.execute();
|
|
846
|
-
blobsMigrated = true;
|
|
847
|
-
} catch {
|
|
848
|
-
blobsMigrated = false;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
return { syncMigrated, blobsMigrated };
|
|
852
278
|
}
|
|
853
279
|
|
|
854
|
-
|
|
855
|
-
* Static: Check if migrations are needed.
|
|
856
|
-
*/
|
|
857
|
-
static async checkMigrations<DB extends SyncClientDb>(
|
|
858
|
-
db: Kysely<DB>
|
|
859
|
-
): Promise<{
|
|
860
|
-
needsMigration: boolean;
|
|
861
|
-
syncMigrated: boolean;
|
|
862
|
-
blobsMigrated: boolean;
|
|
863
|
-
}> {
|
|
864
|
-
let syncMigrated = false;
|
|
865
|
-
let blobsMigrated = false;
|
|
866
|
-
|
|
867
|
-
try {
|
|
868
|
-
await db.selectFrom('sync_outbox_commits').selectAll().limit(1).execute();
|
|
869
|
-
syncMigrated = true;
|
|
870
|
-
} catch {
|
|
871
|
-
syncMigrated = false;
|
|
872
|
-
}
|
|
873
|
-
|
|
280
|
+
async #startRealtimeForLifecycle(): Promise<void> {
|
|
874
281
|
try {
|
|
875
|
-
await
|
|
876
|
-
|
|
877
|
-
} catch {
|
|
878
|
-
|
|
282
|
+
await this.client.startRealtime(this.options.realtime);
|
|
283
|
+
this.#realtimeStarted = true;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
this.#realtimeStarted = false;
|
|
286
|
+
if (!isSyncularOfflineError(error)) throw error;
|
|
879
287
|
}
|
|
880
|
-
|
|
881
|
-
return {
|
|
882
|
-
needsMigration: !syncMigrated,
|
|
883
|
-
syncMigrated,
|
|
884
|
-
blobsMigrated,
|
|
885
|
-
};
|
|
886
288
|
}
|
|
887
289
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
*/
|
|
891
|
-
static async migrate<DB extends SyncClientDb>(
|
|
892
|
-
db: Kysely<DB>,
|
|
893
|
-
options?: { blobs?: boolean }
|
|
894
|
-
): Promise<void> {
|
|
895
|
-
await ensureClientSyncSchema(db);
|
|
896
|
-
if (options?.blobs) {
|
|
897
|
-
await ensureClientBlobSchema(db);
|
|
898
|
-
}
|
|
290
|
+
#isOnline(): boolean {
|
|
291
|
+
return this.#network?.isOnline() !== false;
|
|
899
292
|
}
|
|
900
293
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
isSyncing: false,
|
|
909
|
-
connectionState: 'disconnected',
|
|
910
|
-
transportMode: 'polling',
|
|
911
|
-
lastSyncAt: null,
|
|
912
|
-
error: null,
|
|
913
|
-
pendingCount: 0,
|
|
914
|
-
retryCount: 0,
|
|
915
|
-
isRetrying: false,
|
|
916
|
-
};
|
|
294
|
+
#startPolling(): void {
|
|
295
|
+
const interval = this.options.pollIntervalMs;
|
|
296
|
+
if (interval === false || interval === undefined || interval <= 0) return;
|
|
297
|
+
this.#pollTimer = setInterval(() => {
|
|
298
|
+
if (!this.#isOnline()) return;
|
|
299
|
+
void this.#syncForLifecycle();
|
|
300
|
+
}, interval);
|
|
917
301
|
}
|
|
918
302
|
|
|
919
|
-
|
|
920
|
-
if (!this
|
|
921
|
-
|
|
922
|
-
this
|
|
923
|
-
this.emit('sync:start', payload);
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
this.engine.on('sync:complete', (payload) => {
|
|
927
|
-
this.emit('sync:complete', {
|
|
928
|
-
success: true,
|
|
929
|
-
pushedCommits: payload.pushedCommits,
|
|
930
|
-
pullRounds: payload.pullRounds,
|
|
931
|
-
pullResponse: payload.pullResponse,
|
|
932
|
-
});
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
this.engine.on('sync:live', (payload) => {
|
|
936
|
-
this.emit('sync:live', payload);
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
this.engine.on('sync:error', (error) => {
|
|
940
|
-
this.emit('sync:error', { code: error.code, message: error.message });
|
|
941
|
-
|
|
942
|
-
// Check for new conflicts after sync error
|
|
943
|
-
this.checkForNewConflicts();
|
|
944
|
-
});
|
|
945
|
-
|
|
946
|
-
this.engine.on('bootstrap:start', (payload) => {
|
|
947
|
-
this.emit('bootstrap:start', payload);
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
this.engine.on('bootstrap:progress', (payload) => {
|
|
951
|
-
this.emit('bootstrap:progress', payload);
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
this.engine.on('bootstrap:complete', (payload) => {
|
|
955
|
-
this.emit('bootstrap:complete', payload);
|
|
956
|
-
});
|
|
957
|
-
|
|
958
|
-
this.engine.on('connection:change', (payload) => {
|
|
959
|
-
this.emit('connection:change', payload);
|
|
960
|
-
});
|
|
961
|
-
|
|
962
|
-
this.engine.on('data:change', (payload) => {
|
|
963
|
-
this.emit('data:change', payload);
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
this.engine.on('outbox:change', (payload) => {
|
|
967
|
-
this.outboxStats = {
|
|
968
|
-
pending: payload.pendingCount,
|
|
969
|
-
sending: payload.sendingCount,
|
|
970
|
-
failed: payload.failedCount,
|
|
971
|
-
acked: payload.ackedCount ?? 0,
|
|
972
|
-
total:
|
|
973
|
-
payload.pendingCount +
|
|
974
|
-
payload.sendingCount +
|
|
975
|
-
payload.failedCount +
|
|
976
|
-
(payload.ackedCount ?? 0),
|
|
977
|
-
};
|
|
978
|
-
this.emit('outbox:change', this.outboxStats);
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
this.engine.on('presence:change', (payload) => {
|
|
982
|
-
this.emit('presence:change', payload);
|
|
983
|
-
});
|
|
303
|
+
#stopPolling(): void {
|
|
304
|
+
if (!this.#pollTimer) return;
|
|
305
|
+
clearInterval(this.#pollTimer);
|
|
306
|
+
this.#pollTimer = undefined;
|
|
984
307
|
}
|
|
985
|
-
|
|
986
|
-
private async checkForNewConflicts(): Promise<void> {
|
|
987
|
-
const conflicts = await this.getConflicts();
|
|
988
|
-
const activeIds = new Set(conflicts.map((conflict) => conflict.id));
|
|
989
|
-
|
|
990
|
-
for (const id of this.emittedConflictIds) {
|
|
991
|
-
if (!activeIds.has(id)) {
|
|
992
|
-
this.emittedConflictIds.delete(id);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
for (const conflict of conflicts) {
|
|
997
|
-
if (this.emittedConflictIds.has(conflict.id)) {
|
|
998
|
-
continue;
|
|
999
|
-
}
|
|
1000
|
-
this.emittedConflictIds.add(conflict.id);
|
|
1001
|
-
this.emit('conflict:new', conflict);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
private mapConflictInfo(info: ConflictInfo): Conflict {
|
|
1006
|
-
let serverPayload: Record<string, unknown> | null = null;
|
|
1007
|
-
if (info.serverRowJson) {
|
|
1008
|
-
try {
|
|
1009
|
-
serverPayload = JSON.parse(info.serverRowJson);
|
|
1010
|
-
} catch {
|
|
1011
|
-
serverPayload = null;
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
return {
|
|
1016
|
-
id: info.id,
|
|
1017
|
-
table: info.table,
|
|
1018
|
-
rowId: info.rowId,
|
|
1019
|
-
opIndex: info.opIndex,
|
|
1020
|
-
localPayload: info.localPayload,
|
|
1021
|
-
serverPayload,
|
|
1022
|
-
serverVersion: info.serverVersion,
|
|
1023
|
-
message: info.message,
|
|
1024
|
-
code: info.code,
|
|
1025
|
-
createdAt: info.createdAt,
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
private createBlobClient(
|
|
1030
|
-
storage: ClientBlobStorage,
|
|
1031
|
-
transport: SyncTransport
|
|
1032
|
-
): BlobClient {
|
|
1033
|
-
const db = this.options.db;
|
|
1034
|
-
const blobs = transport.blobs!;
|
|
1035
|
-
const staleUploadingTimeoutMs = 30_000;
|
|
1036
|
-
const maxUploadRetries = 3;
|
|
1037
|
-
|
|
1038
|
-
return {
|
|
1039
|
-
async store(data, options) {
|
|
1040
|
-
const bytes = await toUint8Array(data);
|
|
1041
|
-
const mimeType =
|
|
1042
|
-
data instanceof Blob
|
|
1043
|
-
? data.type
|
|
1044
|
-
: (options?.mimeType ?? 'application/octet-stream');
|
|
1045
|
-
|
|
1046
|
-
// Compute hash
|
|
1047
|
-
const hashHex = await computeSha256Hex(bytes);
|
|
1048
|
-
const hash = `sha256:${hashHex}`;
|
|
1049
|
-
|
|
1050
|
-
// Store locally
|
|
1051
|
-
await storage.write(hash, bytes);
|
|
1052
|
-
|
|
1053
|
-
// Store metadata
|
|
1054
|
-
const now = Date.now();
|
|
1055
|
-
await sql`
|
|
1056
|
-
insert into ${sql.table('sync_blob_cache')} (
|
|
1057
|
-
${sql.join([
|
|
1058
|
-
sql.ref('hash'),
|
|
1059
|
-
sql.ref('size'),
|
|
1060
|
-
sql.ref('mime_type'),
|
|
1061
|
-
sql.ref('cached_at'),
|
|
1062
|
-
sql.ref('last_accessed_at'),
|
|
1063
|
-
sql.ref('encrypted'),
|
|
1064
|
-
sql.ref('key_id'),
|
|
1065
|
-
sql.ref('body'),
|
|
1066
|
-
])}
|
|
1067
|
-
) values (
|
|
1068
|
-
${sql.join([
|
|
1069
|
-
sql.val(hash),
|
|
1070
|
-
sql.val(bytes.length),
|
|
1071
|
-
sql.val(mimeType),
|
|
1072
|
-
sql.val(now),
|
|
1073
|
-
sql.val(now),
|
|
1074
|
-
sql.val(0),
|
|
1075
|
-
sql.val(null),
|
|
1076
|
-
sql.val(bytes),
|
|
1077
|
-
])}
|
|
1078
|
-
)
|
|
1079
|
-
on conflict (${sql.ref('hash')}) do nothing
|
|
1080
|
-
`.execute(db);
|
|
1081
|
-
|
|
1082
|
-
// Queue for upload or upload immediately
|
|
1083
|
-
if (options?.immediate) {
|
|
1084
|
-
// Initiate upload
|
|
1085
|
-
const initResult = await blobs.initiateUpload({
|
|
1086
|
-
hash,
|
|
1087
|
-
size: bytes.length,
|
|
1088
|
-
mimeType,
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
if (!initResult.exists && initResult.uploadUrl) {
|
|
1092
|
-
// Upload to presigned URL
|
|
1093
|
-
const uploadResponse = await fetch(initResult.uploadUrl, {
|
|
1094
|
-
method: initResult.uploadMethod ?? 'PUT',
|
|
1095
|
-
body: bytes.buffer as ArrayBuffer,
|
|
1096
|
-
headers: initResult.uploadHeaders,
|
|
1097
|
-
});
|
|
1098
|
-
|
|
1099
|
-
if (!uploadResponse.ok) {
|
|
1100
|
-
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// Complete upload
|
|
1104
|
-
await blobs.completeUpload(hash);
|
|
1105
|
-
}
|
|
1106
|
-
} else {
|
|
1107
|
-
// Queue for later upload
|
|
1108
|
-
await sql`
|
|
1109
|
-
insert into ${sql.table('sync_blob_outbox')} (
|
|
1110
|
-
${sql.join([
|
|
1111
|
-
sql.ref('hash'),
|
|
1112
|
-
sql.ref('size'),
|
|
1113
|
-
sql.ref('mime_type'),
|
|
1114
|
-
sql.ref('status'),
|
|
1115
|
-
sql.ref('created_at'),
|
|
1116
|
-
sql.ref('updated_at'),
|
|
1117
|
-
sql.ref('attempt_count'),
|
|
1118
|
-
sql.ref('error'),
|
|
1119
|
-
sql.ref('encrypted'),
|
|
1120
|
-
sql.ref('key_id'),
|
|
1121
|
-
sql.ref('body'),
|
|
1122
|
-
])}
|
|
1123
|
-
) values (
|
|
1124
|
-
${sql.join([
|
|
1125
|
-
sql.val(hash),
|
|
1126
|
-
sql.val(bytes.length),
|
|
1127
|
-
sql.val(mimeType),
|
|
1128
|
-
sql.val('pending'),
|
|
1129
|
-
sql.val(now),
|
|
1130
|
-
sql.val(now),
|
|
1131
|
-
sql.val(0),
|
|
1132
|
-
sql.val(null),
|
|
1133
|
-
sql.val(0),
|
|
1134
|
-
sql.val(null),
|
|
1135
|
-
sql.val(bytes),
|
|
1136
|
-
])}
|
|
1137
|
-
)
|
|
1138
|
-
on conflict (${sql.ref('hash')}) do nothing
|
|
1139
|
-
`.execute(db);
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
return {
|
|
1143
|
-
hash,
|
|
1144
|
-
size: bytes.length,
|
|
1145
|
-
mimeType,
|
|
1146
|
-
};
|
|
1147
|
-
},
|
|
1148
|
-
|
|
1149
|
-
async retrieve(ref) {
|
|
1150
|
-
// Check local storage first
|
|
1151
|
-
const local = await storage.read(ref.hash);
|
|
1152
|
-
if (local) {
|
|
1153
|
-
// Update access time
|
|
1154
|
-
await sql`
|
|
1155
|
-
update ${sql.table('sync_blob_cache')}
|
|
1156
|
-
set ${sql.ref('last_accessed_at')} = ${sql.val(Date.now())}
|
|
1157
|
-
where ${sql.ref('hash')} = ${sql.val(ref.hash)}
|
|
1158
|
-
`.execute(db);
|
|
1159
|
-
return local;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Fetch from server
|
|
1163
|
-
const { url } = await blobs.getDownloadUrl(ref.hash);
|
|
1164
|
-
const response = await fetch(url);
|
|
1165
|
-
if (!response.ok) {
|
|
1166
|
-
throw new Error(`Download failed: ${response.statusText}`);
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1170
|
-
|
|
1171
|
-
// Cache locally
|
|
1172
|
-
await storage.write(ref.hash, bytes);
|
|
1173
|
-
const now = Date.now();
|
|
1174
|
-
await sql`
|
|
1175
|
-
insert into ${sql.table('sync_blob_cache')} (
|
|
1176
|
-
${sql.join([
|
|
1177
|
-
sql.ref('hash'),
|
|
1178
|
-
sql.ref('size'),
|
|
1179
|
-
sql.ref('mime_type'),
|
|
1180
|
-
sql.ref('cached_at'),
|
|
1181
|
-
sql.ref('last_accessed_at'),
|
|
1182
|
-
sql.ref('encrypted'),
|
|
1183
|
-
sql.ref('key_id'),
|
|
1184
|
-
sql.ref('body'),
|
|
1185
|
-
])}
|
|
1186
|
-
) values (
|
|
1187
|
-
${sql.join([
|
|
1188
|
-
sql.val(ref.hash),
|
|
1189
|
-
sql.val(bytes.length),
|
|
1190
|
-
sql.val(ref.mimeType),
|
|
1191
|
-
sql.val(now),
|
|
1192
|
-
sql.val(now),
|
|
1193
|
-
sql.val(0),
|
|
1194
|
-
sql.val(null),
|
|
1195
|
-
sql.val(bytes),
|
|
1196
|
-
])}
|
|
1197
|
-
)
|
|
1198
|
-
on conflict (${sql.ref('hash')}) do nothing
|
|
1199
|
-
`.execute(db);
|
|
1200
|
-
|
|
1201
|
-
return bytes;
|
|
1202
|
-
},
|
|
1203
|
-
|
|
1204
|
-
async isLocal(hash) {
|
|
1205
|
-
return storage.exists(hash);
|
|
1206
|
-
},
|
|
1207
|
-
|
|
1208
|
-
async preload(refs) {
|
|
1209
|
-
await Promise.all(refs.map((ref) => this.retrieve(ref)));
|
|
1210
|
-
},
|
|
1211
|
-
|
|
1212
|
-
async processUploadQueue() {
|
|
1213
|
-
let uploaded = 0;
|
|
1214
|
-
let failed = 0;
|
|
1215
|
-
const now = Date.now();
|
|
1216
|
-
const staleThreshold = now - staleUploadingTimeoutMs;
|
|
1217
|
-
|
|
1218
|
-
await sql`
|
|
1219
|
-
update ${sql.table('sync_blob_outbox')}
|
|
1220
|
-
set
|
|
1221
|
-
${sql.ref('status')} = ${sql.val('failed')},
|
|
1222
|
-
${sql.ref('attempt_count')} = ${sql.ref('attempt_count')} + ${sql.val(
|
|
1223
|
-
1
|
|
1224
|
-
)},
|
|
1225
|
-
${sql.ref('error')} = ${sql.val(
|
|
1226
|
-
'Upload timed out while in uploading state'
|
|
1227
|
-
)},
|
|
1228
|
-
${sql.ref('updated_at')} = ${sql.val(now)}
|
|
1229
|
-
where ${sql.ref('status')} = ${sql.val('uploading')}
|
|
1230
|
-
and ${sql.ref('updated_at')} < ${sql.val(staleThreshold)}
|
|
1231
|
-
and ${sql.ref('attempt_count')} + ${sql.val(1)} >= ${sql.val(
|
|
1232
|
-
maxUploadRetries
|
|
1233
|
-
)}
|
|
1234
|
-
`.execute(db);
|
|
1235
|
-
|
|
1236
|
-
await sql`
|
|
1237
|
-
update ${sql.table('sync_blob_outbox')}
|
|
1238
|
-
set
|
|
1239
|
-
${sql.ref('status')} = ${sql.val('pending')},
|
|
1240
|
-
${sql.ref('attempt_count')} = ${sql.ref('attempt_count')} + ${sql.val(
|
|
1241
|
-
1
|
|
1242
|
-
)},
|
|
1243
|
-
${sql.ref('error')} = ${sql.val(
|
|
1244
|
-
'Upload timed out while in uploading state; retrying'
|
|
1245
|
-
)},
|
|
1246
|
-
${sql.ref('updated_at')} = ${sql.val(now)}
|
|
1247
|
-
where ${sql.ref('status')} = ${sql.val('uploading')}
|
|
1248
|
-
and ${sql.ref('updated_at')} < ${sql.val(staleThreshold)}
|
|
1249
|
-
and ${sql.ref('attempt_count')} + ${sql.val(1)} < ${sql.val(
|
|
1250
|
-
maxUploadRetries
|
|
1251
|
-
)}
|
|
1252
|
-
`.execute(db);
|
|
1253
|
-
|
|
1254
|
-
const pendingResult = await sql<{
|
|
1255
|
-
hash: string;
|
|
1256
|
-
size: number;
|
|
1257
|
-
mime_type: string;
|
|
1258
|
-
body: Uint8Array | null;
|
|
1259
|
-
attempt_count: number;
|
|
1260
|
-
}>`
|
|
1261
|
-
select
|
|
1262
|
-
${sql.ref('hash')},
|
|
1263
|
-
${sql.ref('size')},
|
|
1264
|
-
${sql.ref('mime_type')},
|
|
1265
|
-
${sql.ref('body')},
|
|
1266
|
-
${sql.ref('attempt_count')}
|
|
1267
|
-
from ${sql.table('sync_blob_outbox')}
|
|
1268
|
-
where ${sql.ref('status')} = ${sql.val('pending')}
|
|
1269
|
-
and ${sql.ref('attempt_count')} < ${sql.val(maxUploadRetries)}
|
|
1270
|
-
limit ${sql.val(10)}
|
|
1271
|
-
`.execute(db);
|
|
1272
|
-
const pending = pendingResult.rows;
|
|
1273
|
-
|
|
1274
|
-
for (const item of pending) {
|
|
1275
|
-
const nextAttemptCount = item.attempt_count + 1;
|
|
1276
|
-
try {
|
|
1277
|
-
// Mark as uploading
|
|
1278
|
-
await sql`
|
|
1279
|
-
update ${sql.table('sync_blob_outbox')}
|
|
1280
|
-
set
|
|
1281
|
-
${sql.ref('status')} = ${sql.val('uploading')},
|
|
1282
|
-
${sql.ref('attempt_count')} = ${sql.val(nextAttemptCount)},
|
|
1283
|
-
${sql.ref('error')} = ${sql.val(null)},
|
|
1284
|
-
${sql.ref('updated_at')} = ${sql.val(Date.now())}
|
|
1285
|
-
where ${sql.ref('hash')} = ${sql.val(item.hash)}
|
|
1286
|
-
and ${sql.ref('status')} = ${sql.val('pending')}
|
|
1287
|
-
`.execute(db);
|
|
1288
|
-
|
|
1289
|
-
// Initiate upload
|
|
1290
|
-
const initResult = await blobs.initiateUpload({
|
|
1291
|
-
hash: item.hash,
|
|
1292
|
-
size: item.size,
|
|
1293
|
-
mimeType: item.mime_type,
|
|
1294
|
-
});
|
|
1295
|
-
|
|
1296
|
-
if (!initResult.exists && initResult.uploadUrl && item.body) {
|
|
1297
|
-
const uploadBody = new ArrayBuffer(item.body.byteLength);
|
|
1298
|
-
new Uint8Array(uploadBody).set(item.body);
|
|
1299
|
-
|
|
1300
|
-
// Upload
|
|
1301
|
-
const uploadResponse = await fetch(initResult.uploadUrl, {
|
|
1302
|
-
method: initResult.uploadMethod ?? 'PUT',
|
|
1303
|
-
body: uploadBody,
|
|
1304
|
-
headers: initResult.uploadHeaders,
|
|
1305
|
-
});
|
|
1306
|
-
|
|
1307
|
-
if (!uploadResponse.ok) {
|
|
1308
|
-
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
// Complete
|
|
1312
|
-
const completeResult = await blobs.completeUpload(item.hash);
|
|
1313
|
-
if (!completeResult.ok) {
|
|
1314
|
-
throw new Error(
|
|
1315
|
-
completeResult.error ?? 'Failed to complete blob upload'
|
|
1316
|
-
);
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
// Mark as complete
|
|
1321
|
-
await sql`
|
|
1322
|
-
delete from ${sql.table('sync_blob_outbox')}
|
|
1323
|
-
where ${sql.ref('hash')} = ${sql.val(item.hash)}
|
|
1324
|
-
`.execute(db);
|
|
1325
|
-
|
|
1326
|
-
uploaded++;
|
|
1327
|
-
} catch (err) {
|
|
1328
|
-
const nextStatus =
|
|
1329
|
-
nextAttemptCount >= maxUploadRetries ? 'failed' : 'pending';
|
|
1330
|
-
|
|
1331
|
-
await sql`
|
|
1332
|
-
update ${sql.table('sync_blob_outbox')}
|
|
1333
|
-
set
|
|
1334
|
-
${sql.ref('status')} = ${sql.val(nextStatus)},
|
|
1335
|
-
${sql.ref('error')} = ${sql.val(
|
|
1336
|
-
err instanceof Error ? err.message : 'Unknown error'
|
|
1337
|
-
)},
|
|
1338
|
-
${sql.ref('updated_at')} = ${sql.val(Date.now())}
|
|
1339
|
-
where ${sql.ref('hash')} = ${sql.val(item.hash)}
|
|
1340
|
-
`.execute(db);
|
|
1341
|
-
|
|
1342
|
-
if (nextStatus === 'failed') {
|
|
1343
|
-
failed++;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
return { uploaded, failed };
|
|
1349
|
-
},
|
|
1350
|
-
|
|
1351
|
-
async getUploadQueueStats() {
|
|
1352
|
-
const rowsResult = await sql<{
|
|
1353
|
-
status: string;
|
|
1354
|
-
count: number | bigint;
|
|
1355
|
-
}>`
|
|
1356
|
-
select
|
|
1357
|
-
${sql.ref('status')} as status,
|
|
1358
|
-
count(${sql.ref('hash')}) as count
|
|
1359
|
-
from ${sql.table('sync_blob_outbox')}
|
|
1360
|
-
group by ${sql.ref('status')}
|
|
1361
|
-
`.execute(db);
|
|
1362
|
-
|
|
1363
|
-
const stats = { pending: 0, uploading: 0, failed: 0 };
|
|
1364
|
-
for (const row of rowsResult.rows) {
|
|
1365
|
-
if (row.status === 'pending') stats.pending = Number(row.count);
|
|
1366
|
-
if (row.status === 'uploading') stats.uploading = Number(row.count);
|
|
1367
|
-
if (row.status === 'failed') stats.failed = Number(row.count);
|
|
1368
|
-
}
|
|
1369
|
-
return stats;
|
|
1370
|
-
},
|
|
1371
|
-
|
|
1372
|
-
async getCacheStats() {
|
|
1373
|
-
const result = await sql<{
|
|
1374
|
-
count: number | bigint;
|
|
1375
|
-
totalBytes: number | bigint | null;
|
|
1376
|
-
}>`
|
|
1377
|
-
select
|
|
1378
|
-
count(${sql.ref('hash')}) as count,
|
|
1379
|
-
sum(${sql.ref('size')}) as totalBytes
|
|
1380
|
-
from ${sql.table('sync_blob_cache')}
|
|
1381
|
-
`.execute(db);
|
|
1382
|
-
const row = result.rows[0];
|
|
1383
|
-
|
|
1384
|
-
return {
|
|
1385
|
-
count: Number(row?.count ?? 0),
|
|
1386
|
-
totalBytes: Number(row?.totalBytes ?? 0),
|
|
1387
|
-
};
|
|
1388
|
-
},
|
|
1389
|
-
|
|
1390
|
-
async pruneCache(maxBytes) {
|
|
1391
|
-
if (!maxBytes) return 0;
|
|
1392
|
-
|
|
1393
|
-
// Get current size
|
|
1394
|
-
const stats = await this.getCacheStats();
|
|
1395
|
-
if (stats.totalBytes <= maxBytes) return 0;
|
|
1396
|
-
|
|
1397
|
-
// Get oldest entries to delete
|
|
1398
|
-
const toFree = stats.totalBytes - maxBytes;
|
|
1399
|
-
let freed = 0;
|
|
1400
|
-
|
|
1401
|
-
const oldEntriesResult = await sql<{ hash: string; size: number }>`
|
|
1402
|
-
select ${sql.ref('hash')}, ${sql.ref('size')}
|
|
1403
|
-
from ${sql.table('sync_blob_cache')}
|
|
1404
|
-
order by ${sql.ref('last_accessed_at')} asc
|
|
1405
|
-
`.execute(db);
|
|
1406
|
-
const oldEntries = oldEntriesResult.rows;
|
|
1407
|
-
|
|
1408
|
-
for (const entry of oldEntries) {
|
|
1409
|
-
if (freed >= toFree) break;
|
|
1410
|
-
|
|
1411
|
-
await storage.delete(entry.hash);
|
|
1412
|
-
await sql`
|
|
1413
|
-
delete from ${sql.table('sync_blob_cache')}
|
|
1414
|
-
where ${sql.ref('hash')} = ${sql.val(entry.hash)}
|
|
1415
|
-
`.execute(db);
|
|
1416
|
-
freed += entry.size;
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
return freed;
|
|
1420
|
-
},
|
|
1421
|
-
|
|
1422
|
-
async clearCache() {
|
|
1423
|
-
if (storage.clear) {
|
|
1424
|
-
await storage.clear();
|
|
1425
|
-
} else {
|
|
1426
|
-
// Delete each entry individually
|
|
1427
|
-
const entriesResult = await sql<{ hash: string }>`
|
|
1428
|
-
select ${sql.ref('hash')}
|
|
1429
|
-
from ${sql.table('sync_blob_cache')}
|
|
1430
|
-
`.execute(db);
|
|
1431
|
-
|
|
1432
|
-
for (const entry of entriesResult.rows) {
|
|
1433
|
-
await storage.delete(entry.hash);
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
await sql`delete from ${sql.table('sync_blob_cache')}`.execute(db);
|
|
1438
|
-
},
|
|
1439
|
-
};
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
// ============================================================================
|
|
1444
|
-
// Helpers
|
|
1445
|
-
// ============================================================================
|
|
1446
|
-
|
|
1447
|
-
async function toUint8Array(
|
|
1448
|
-
data: Blob | File | Uint8Array
|
|
1449
|
-
): Promise<Uint8Array> {
|
|
1450
|
-
if (data instanceof Uint8Array) {
|
|
1451
|
-
return data;
|
|
1452
|
-
}
|
|
1453
|
-
const buffer = await data.arrayBuffer();
|
|
1454
|
-
return new Uint8Array(buffer);
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
async function computeSha256Hex(data: Uint8Array): Promise<string> {
|
|
1458
|
-
const hashBuffer = await crypto.subtle.digest(
|
|
1459
|
-
'SHA-256',
|
|
1460
|
-
data.buffer as ArrayBuffer
|
|
1461
|
-
);
|
|
1462
|
-
const hashArray = new Uint8Array(hashBuffer);
|
|
1463
|
-
return Array.from(hashArray)
|
|
1464
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
1465
|
-
.join('');
|
|
1466
308
|
}
|