@syncular/client 0.0.6-96 → 0.1.1
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 +183 -957
- 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 +954 -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 +12 -23
- 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 +938 -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 +1096 -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 +23 -0
- package/dist/wasm-runtime.d.ts.map +1 -0
- package/dist/wasm-runtime.js +69 -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 +1735 -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 +293 -1407
- 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 +1396 -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 +1491 -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 +1397 -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 +145 -0
- package/src/worker-client.ts +2289 -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,352 @@
|
|
|
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>;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface ClientOptions<DB extends SyncClientDb> {
|
|
88
|
-
/** Kysely database instance */
|
|
89
|
-
db: Kysely<DB>;
|
|
90
|
-
|
|
91
|
-
/** Transport for server communication (includes sync and blob operations) */
|
|
92
|
-
transport: SyncTransport;
|
|
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;
|
|
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[];
|
|
140
38
|
}
|
|
141
39
|
|
|
142
|
-
export interface
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export interface Conflict {
|
|
162
|
-
id: string;
|
|
163
|
-
table: string;
|
|
164
|
-
rowId: string;
|
|
165
|
-
opIndex: number;
|
|
166
|
-
localPayload: Record<string, unknown> | null;
|
|
167
|
-
serverPayload: Record<string, unknown> | null;
|
|
168
|
-
serverVersion: number | null;
|
|
169
|
-
message: string;
|
|
170
|
-
code: string | null;
|
|
171
|
-
createdAt: number;
|
|
47
|
+
hasPendingMutations: boolean;
|
|
48
|
+
hasConflicts: boolean;
|
|
49
|
+
requiresAction: boolean;
|
|
172
50
|
}
|
|
173
51
|
|
|
174
|
-
export interface
|
|
175
|
-
|
|
176
|
-
|
|
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>;
|
|
177
58
|
}
|
|
178
59
|
|
|
179
|
-
interface
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
184
70
|
}
|
|
185
71
|
|
|
186
|
-
export interface
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
options?: BlobStoreOptions
|
|
191
|
-
): Promise<BlobRef>;
|
|
192
|
-
|
|
193
|
-
/** Retrieve a blob (from local storage or fetch from server) */
|
|
194
|
-
retrieve(ref: BlobRef): Promise<Uint8Array>;
|
|
195
|
-
|
|
196
|
-
/** Check if blob is available locally */
|
|
197
|
-
isLocal(hash: string): Promise<boolean>;
|
|
198
|
-
|
|
199
|
-
/** Preload blobs for offline use */
|
|
200
|
-
preload(refs: BlobRef[]): Promise<void>;
|
|
201
|
-
|
|
202
|
-
/** Process pending uploads */
|
|
203
|
-
processUploadQueue(): Promise<{ uploaded: number; failed: number }>;
|
|
204
|
-
|
|
205
|
-
/** Get upload queue statistics */
|
|
206
|
-
getUploadQueueStats(): Promise<{
|
|
207
|
-
pending: number;
|
|
208
|
-
uploading: number;
|
|
209
|
-
failed: number;
|
|
210
|
-
}>;
|
|
211
|
-
|
|
212
|
-
/** Get cache statistics */
|
|
213
|
-
getCacheStats(): Promise<{ count: number; totalBytes: number }>;
|
|
214
|
-
|
|
215
|
-
/** Prune cache to free space */
|
|
216
|
-
pruneCache(maxBytes?: number): Promise<number>;
|
|
217
|
-
|
|
218
|
-
/** Clear all cached blobs */
|
|
219
|
-
clearCache(): Promise<void>;
|
|
72
|
+
export interface SyncularConflictsClientLike {
|
|
73
|
+
list(): Promise<SyncularConflictSummary[]>;
|
|
74
|
+
retryKeepLocal(id: string): Promise<string>;
|
|
75
|
+
resolve(id: string, resolution: SyncularConflictResolution): Promise<void>;
|
|
220
76
|
}
|
|
221
77
|
|
|
222
|
-
export interface
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
| 'blob:upload:error'
|
|
244
|
-
| 'presence:change';
|
|
245
|
-
|
|
246
|
-
type ClientEventPayloads = {
|
|
247
|
-
'sync:start': { timestamp: number };
|
|
248
|
-
'sync:complete': SyncResult;
|
|
249
|
-
'sync:live': { timestamp: number };
|
|
250
|
-
'sync:error': { code: string; message: string };
|
|
251
|
-
'bootstrap:start': {
|
|
252
|
-
timestamp: number;
|
|
253
|
-
stateId: string;
|
|
254
|
-
subscriptionId: string;
|
|
255
|
-
};
|
|
256
|
-
'bootstrap:progress': {
|
|
257
|
-
timestamp: number;
|
|
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[] };
|
|
113
|
+
type LifecycleClient = Pick<
|
|
114
|
+
SyncularRuntimeClient,
|
|
115
|
+
| 'addDiagnosticListener'
|
|
116
|
+
| 'connectionState'
|
|
117
|
+
| 'forceSubscriptionsBootstrap'
|
|
118
|
+
| 'setSubscriptions'
|
|
119
|
+
| 'startRealtime'
|
|
120
|
+
| 'stopRealtime'
|
|
121
|
+
| 'syncOnce'
|
|
122
|
+
>;
|
|
123
|
+
|
|
124
|
+
type QueuedSyncWaiter = {
|
|
125
|
+
resolve(result: SyncularSyncResult): void;
|
|
126
|
+
reject(error: unknown): void;
|
|
276
127
|
};
|
|
277
128
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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,
|
|
129
|
+
export function getSyncularClientStatus(
|
|
130
|
+
client: Pick<SyncularRuntimeClient, 'connectionState' | 'lifecycleState'>
|
|
131
|
+
): SyncularClientStatus {
|
|
132
|
+
const lifecycle = client.lifecycleState();
|
|
133
|
+
const connection = client.connectionState();
|
|
134
|
+
const outbox = lifecycle.outbox ?? null;
|
|
135
|
+
const conflicts = lifecycle.conflicts ?? null;
|
|
136
|
+
return {
|
|
137
|
+
lifecycle,
|
|
138
|
+
connection,
|
|
139
|
+
outbox,
|
|
140
|
+
conflicts,
|
|
141
|
+
isConnected: connection.realtime === 'connected' && !connection.closed,
|
|
142
|
+
isSyncing:
|
|
143
|
+
lifecycle.phase === 'syncing' || lifecycle.phase === 'recovering',
|
|
144
|
+
hasPendingMutations: (outbox?.pending ?? 0) + (outbox?.sending ?? 0) > 0,
|
|
145
|
+
hasConflicts: (conflicts?.unresolved ?? 0) > 0,
|
|
146
|
+
requiresAction: lifecycle.requiresAction,
|
|
328
147
|
};
|
|
148
|
+
}
|
|
329
149
|
|
|
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;
|
|
150
|
+
export class SyncularClientLifecycle {
|
|
151
|
+
#started = false;
|
|
152
|
+
#pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
153
|
+
#unsubscribeDiagnostics: (() => void) | undefined;
|
|
154
|
+
#unsubscribeNetwork: (() => void) | undefined;
|
|
155
|
+
#syncInFlight: Promise<SyncularSyncResult> | undefined;
|
|
156
|
+
#queuedSyncWaiters: QueuedSyncWaiter[] = [];
|
|
157
|
+
#hasConnectedRealtime = false;
|
|
158
|
+
#realtimeStarted = false;
|
|
159
|
+
readonly #network: SyncularNetworkStatusSource | undefined;
|
|
160
|
+
|
|
161
|
+
constructor(
|
|
162
|
+
private readonly client: LifecycleClient,
|
|
163
|
+
private readonly options: SyncularClientLifecycleOptions = {}
|
|
164
|
+
) {
|
|
165
|
+
this.#network =
|
|
166
|
+
options.network === false
|
|
167
|
+
? undefined
|
|
168
|
+
: (options.network ?? browserSyncularNetworkStatusSource());
|
|
376
169
|
}
|
|
377
170
|
|
|
378
|
-
// ===========================================================================
|
|
379
|
-
// Lifecycle
|
|
380
|
-
// ===========================================================================
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Start the client.
|
|
384
|
-
* Runs migrations and starts sync engine.
|
|
385
|
-
*/
|
|
386
171
|
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');
|
|
172
|
+
if (this.#started) return;
|
|
173
|
+
this.#started = true;
|
|
174
|
+
this.#hasConnectedRealtime =
|
|
175
|
+
this.client.connectionState().realtime === 'connected';
|
|
176
|
+
this.#realtimeStarted = this.#hasConnectedRealtime;
|
|
177
|
+
this.#unsubscribeDiagnostics = this.client.addDiagnosticListener((event) =>
|
|
178
|
+
this.#handleDiagnostic(event)
|
|
179
|
+
);
|
|
180
|
+
this.#unsubscribeNetwork = this.#subscribeNetworkEvents();
|
|
181
|
+
try {
|
|
182
|
+
if (this.options.subscriptions) {
|
|
183
|
+
await this.client.setSubscriptions(this.options.subscriptions);
|
|
184
|
+
}
|
|
185
|
+
if (this.options.initialSync !== false && this.#isOnline()) {
|
|
186
|
+
await this.#syncForLifecycle();
|
|
187
|
+
}
|
|
188
|
+
if (this.options.realtime !== false && this.#isOnline()) {
|
|
189
|
+
await this.#startRealtimeForLifecycle();
|
|
190
|
+
}
|
|
191
|
+
this.#startPolling();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
await this.stop().catch(() => undefined);
|
|
194
|
+
throw error;
|
|
453
195
|
}
|
|
454
|
-
return this.engine.sync();
|
|
455
196
|
}
|
|
456
197
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
);
|
|
198
|
+
async stop(): Promise<void> {
|
|
199
|
+
if (!this.#started) return;
|
|
200
|
+
this.#started = false;
|
|
201
|
+
this.#stopPolling();
|
|
202
|
+
this.#unsubscribeDiagnostics?.();
|
|
203
|
+
this.#unsubscribeDiagnostics = undefined;
|
|
204
|
+
this.#unsubscribeNetwork?.();
|
|
205
|
+
this.#unsubscribeNetwork = undefined;
|
|
206
|
+
this.#rejectQueuedSyncWaiters(
|
|
207
|
+
new Error('Syncular lifecycle stopped before queued sync could run')
|
|
208
|
+
);
|
|
209
|
+
if (this.options.realtime !== false) {
|
|
210
|
+
await this.client.stopRealtime();
|
|
482
211
|
}
|
|
212
|
+
this.#realtimeStarted = false;
|
|
483
213
|
}
|
|
484
214
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
};
|
|
215
|
+
async sync(): Promise<SyncularSyncResult> {
|
|
216
|
+
if (this.#syncInFlight) {
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
this.#queuedSyncWaiters.push({ resolve, reject });
|
|
219
|
+
});
|
|
594
220
|
}
|
|
595
|
-
|
|
221
|
+
this.#syncInFlight = this.#startSyncCycle();
|
|
222
|
+
return this.#syncInFlight;
|
|
596
223
|
}
|
|
597
224
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
deletedSubscriptionStates: 0,
|
|
605
|
-
deletedOutboxCommits: 0,
|
|
606
|
-
deletedConflicts: 0,
|
|
607
|
-
clearedTables: [],
|
|
608
|
-
};
|
|
225
|
+
#startSyncCycle(): Promise<SyncularSyncResult> {
|
|
226
|
+
let sync: Promise<SyncularSyncResult>;
|
|
227
|
+
try {
|
|
228
|
+
sync = Promise.resolve(this.client.syncOnce());
|
|
229
|
+
} catch (error) {
|
|
230
|
+
sync = Promise.reject(error);
|
|
609
231
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
232
|
+
const finish = () => {
|
|
233
|
+
if (this.#syncInFlight === sync) {
|
|
234
|
+
this.#syncInFlight = undefined;
|
|
235
|
+
}
|
|
236
|
+
const waiters = this.#queuedSyncWaiters.splice(0);
|
|
237
|
+
if (waiters.length === 0) return;
|
|
238
|
+
if (!this.#started) {
|
|
239
|
+
const error = new Error(
|
|
240
|
+
'Syncular lifecycle stopped before queued sync could run'
|
|
241
|
+
);
|
|
242
|
+
for (const waiter of waiters) waiter.reject(error);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const queued = this.#startSyncCycle();
|
|
246
|
+
this.#syncInFlight = queued;
|
|
247
|
+
queued.then(
|
|
248
|
+
(result) => {
|
|
249
|
+
for (const waiter of waiters) waiter.resolve(result);
|
|
250
|
+
},
|
|
251
|
+
(error) => {
|
|
252
|
+
for (const waiter of waiters) waiter.reject(error);
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
void sync.then(finish, finish).catch((error) => {
|
|
257
|
+
this.#rejectQueuedSyncWaiters(error);
|
|
258
|
+
});
|
|
259
|
+
return sync;
|
|
622
260
|
}
|
|
623
261
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
async awaitBootstrapComplete(
|
|
628
|
-
options: SyncAwaitBootstrapOptions = {}
|
|
629
|
-
): Promise<SyncProgress | null> {
|
|
630
|
-
if (!this.engine) return null;
|
|
631
|
-
return this.engine.awaitBootstrapComplete(options);
|
|
262
|
+
#rejectQueuedSyncWaiters(error: unknown): void {
|
|
263
|
+
const waiters = this.#queuedSyncWaiters.splice(0);
|
|
264
|
+
for (const waiter of waiters) waiter.reject(error);
|
|
632
265
|
}
|
|
633
266
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
return
|
|
267
|
+
#handleDiagnostic(event: SyncularDiagnosticEvent): void {
|
|
268
|
+
if (event.source === 'sync' && event.details?.resyncRequired === true) {
|
|
269
|
+
void this.client
|
|
270
|
+
.forceSubscriptionsBootstrap()
|
|
271
|
+
.then(() => this.sync())
|
|
272
|
+
.catch(() => undefined);
|
|
273
|
+
return;
|
|
641
274
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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());
|
|
275
|
+
if (
|
|
276
|
+
event.source !== 'realtime' ||
|
|
277
|
+
event.code !== 'realtime.state' ||
|
|
278
|
+
event.details?.state == null
|
|
279
|
+
) {
|
|
280
|
+
return;
|
|
658
281
|
}
|
|
659
|
-
|
|
660
|
-
|
|
282
|
+
const state = event.details.state as SyncularRealtimeConnectionState;
|
|
283
|
+
if (state !== 'connected') return;
|
|
284
|
+
const wasReconnect = this.#hasConnectedRealtime;
|
|
285
|
+
this.#hasConnectedRealtime = true;
|
|
286
|
+
const shouldSync =
|
|
287
|
+
this.options.syncOnRealtimeConnect !== false &&
|
|
288
|
+
(wasReconnect || this.options.initialSync === false);
|
|
289
|
+
if (!shouldSync) return;
|
|
290
|
+
void this.sync().catch(() => undefined);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#subscribeNetworkEvents(): (() => void) | undefined {
|
|
294
|
+
const network = this.#network;
|
|
295
|
+
if (!network?.addEventListener || !network.removeEventListener) return;
|
|
296
|
+
const handleOnline = () => {
|
|
297
|
+
if (!this.#started) return;
|
|
298
|
+
void this.#resumeOnline().catch(() => undefined);
|
|
299
|
+
};
|
|
300
|
+
network.addEventListener('online', handleOnline);
|
|
661
301
|
return () => {
|
|
662
|
-
|
|
302
|
+
network.removeEventListener?.('online', handleOnline);
|
|
663
303
|
};
|
|
664
304
|
}
|
|
665
305
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
}
|
|
678
|
-
}
|
|
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 [];
|
|
306
|
+
async #resumeOnline(): Promise<void> {
|
|
307
|
+
if (!this.#started || !this.#isOnline()) return;
|
|
308
|
+
if (this.options.initialSync !== false) {
|
|
309
|
+
await this.#syncForLifecycle();
|
|
692
310
|
}
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
});
|
|
723
|
-
|
|
724
|
-
this.emittedConflictIds.delete(id);
|
|
725
|
-
if (resolvedConflict) {
|
|
726
|
-
this.emit('conflict:resolved', resolvedConflict);
|
|
311
|
+
if (this.options.realtime !== false && !this.#realtimeStarted) {
|
|
312
|
+
await this.#startRealtimeForLifecycle();
|
|
727
313
|
}
|
|
728
314
|
}
|
|
729
315
|
|
|
730
|
-
|
|
731
|
-
// Outbox
|
|
732
|
-
// ===========================================================================
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* Get outbox statistics.
|
|
736
|
-
*/
|
|
737
|
-
async getOutboxStats(): Promise<OutboxStats> {
|
|
738
|
-
if (!this.engine) {
|
|
739
|
-
return this.outboxStats;
|
|
740
|
-
}
|
|
741
|
-
this.outboxStats = await this.engine.refreshOutboxStats({ emit: false });
|
|
742
|
-
return this.outboxStats;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/**
|
|
746
|
-
* Clear failed commits from outbox.
|
|
747
|
-
*/
|
|
748
|
-
async clearFailedCommits(): Promise<number> {
|
|
749
|
-
if (!this.engine) {
|
|
750
|
-
return 0;
|
|
751
|
-
}
|
|
752
|
-
return this.engine.clearFailedCommits();
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
/**
|
|
756
|
-
* Retry failed commits.
|
|
757
|
-
*/
|
|
758
|
-
async retryFailedCommits(): Promise<number> {
|
|
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();
|
|
773
|
-
}
|
|
774
|
-
return count;
|
|
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 [];
|
|
789
|
-
}
|
|
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
|
-
}
|
|
816
|
-
|
|
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;
|
|
316
|
+
async #syncForLifecycle(): Promise<void> {
|
|
827
317
|
try {
|
|
828
|
-
await this.
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
.limit(1)
|
|
832
|
-
.execute();
|
|
833
|
-
syncMigrated = true;
|
|
834
|
-
} catch {
|
|
835
|
-
syncMigrated = false;
|
|
318
|
+
await this.sync();
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (!isSyncularOfflineError(error)) throw error;
|
|
836
321
|
}
|
|
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
322
|
}
|
|
853
323
|
|
|
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
|
-
|
|
324
|
+
async #startRealtimeForLifecycle(): Promise<void> {
|
|
874
325
|
try {
|
|
875
|
-
await
|
|
876
|
-
|
|
877
|
-
} catch {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
return {
|
|
882
|
-
needsMigration: !syncMigrated,
|
|
883
|
-
syncMigrated,
|
|
884
|
-
blobsMigrated,
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
/**
|
|
889
|
-
* Static: Run migrations.
|
|
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
|
-
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// ===========================================================================
|
|
902
|
-
// Private Helpers
|
|
903
|
-
// ===========================================================================
|
|
904
|
-
|
|
905
|
-
private createInitialEngineState(): SyncEngineState {
|
|
906
|
-
return {
|
|
907
|
-
enabled: false,
|
|
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
|
-
};
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
private wireEngineEvents(): void {
|
|
920
|
-
if (!this.engine) return;
|
|
921
|
-
|
|
922
|
-
this.engine.on('sync:start', (payload) => {
|
|
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
|
-
});
|
|
984
|
-
}
|
|
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);
|
|
326
|
+
await this.client.startRealtime(this.options.realtime);
|
|
327
|
+
this.#realtimeStarted = true;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
this.#realtimeStarted = false;
|
|
330
|
+
if (!isSyncularOfflineError(error)) throw error;
|
|
1002
331
|
}
|
|
1003
332
|
}
|
|
1004
333
|
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
-
};
|
|
334
|
+
#isOnline(): boolean {
|
|
335
|
+
return this.#network?.isOnline() !== false;
|
|
1027
336
|
}
|
|
1028
337
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
-
};
|
|
338
|
+
#startPolling(): void {
|
|
339
|
+
const interval = this.options.pollIntervalMs;
|
|
340
|
+
if (interval === false || interval === undefined || interval <= 0) return;
|
|
341
|
+
this.#pollTimer = setInterval(() => {
|
|
342
|
+
if (!this.#isOnline()) return;
|
|
343
|
+
void this.#syncForLifecycle();
|
|
344
|
+
}, interval);
|
|
1440
345
|
}
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
// ============================================================================
|
|
1444
|
-
// Helpers
|
|
1445
|
-
// ============================================================================
|
|
1446
346
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
)
|
|
1450
|
-
|
|
1451
|
-
return data;
|
|
347
|
+
#stopPolling(): void {
|
|
348
|
+
if (!this.#pollTimer) return;
|
|
349
|
+
clearInterval(this.#pollTimer);
|
|
350
|
+
this.#pollTimer = undefined;
|
|
1452
351
|
}
|
|
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
352
|
}
|