@syncular/client 0.0.6-96 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +713 -11
- package/dist/auth-leases.d.ts +11 -0
- package/dist/auth-leases.d.ts.map +1 -0
- package/dist/auth-leases.js +140 -0
- package/dist/auth-leases.js.map +1 -0
- package/dist/blob-limits.d.ts +15 -0
- package/dist/blob-limits.d.ts.map +1 -0
- package/dist/blob-limits.js +66 -0
- package/dist/blob-limits.js.map +1 -0
- package/dist/bridge-client.d.ts +69 -0
- package/dist/bridge-client.d.ts.map +1 -0
- package/dist/bridge-client.js +282 -0
- package/dist/bridge-client.js.map +1 -0
- package/dist/client-config.d.ts +6 -0
- package/dist/client-config.d.ts.map +1 -0
- package/dist/client-config.js +18 -0
- package/dist/client-config.js.map +1 -0
- package/dist/client.d.ts +65 -397
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +150 -960
- package/dist/client.js.map +1 -1
- package/dist/command-history.d.ts +35 -0
- package/dist/command-history.d.ts.map +1 -0
- package/dist/command-history.js +378 -0
- package/dist/command-history.js.map +1 -0
- package/dist/console-diagnostics.d.ts +33 -0
- package/dist/console-diagnostics.d.ts.map +1 -0
- package/dist/console-diagnostics.js +496 -0
- package/dist/console-diagnostics.js.map +1 -0
- package/dist/crdt-yjs/index.d.ts +4 -0
- package/dist/crdt-yjs/index.d.ts.map +1 -0
- package/dist/crdt-yjs/index.js +4 -0
- package/dist/crdt-yjs/index.js.map +1 -0
- package/dist/crdt-yjs/webview-host-facade.d.ts +126 -0
- package/dist/crdt-yjs/webview-host-facade.d.ts.map +1 -0
- package/dist/crdt-yjs/webview-host-facade.js +284 -0
- package/dist/crdt-yjs/webview-host-facade.js.map +1 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +153 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +1 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.js +406 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.js.map +1 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +73 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +1 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js +169 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +1 -0
- package/dist/database.d.ts +83 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +916 -0
- package/dist/database.js.map +1 -0
- package/dist/diagnostics.d.ts +10 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +83 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +211 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated-bridge.d.ts +365 -0
- package/dist/generated-bridge.d.ts.map +1 -0
- package/dist/generated-bridge.js +250 -0
- package/dist/generated-bridge.js.map +1 -0
- package/dist/index.d.ts +13 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -25
- package/dist/index.js.map +1 -1
- package/dist/mutations.d.ts +4 -72
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +2 -568
- package/dist/mutations.js.map +1 -1
- package/dist/network.d.ts +3 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +17 -0
- package/dist/network.js.map +1 -0
- package/dist/react/index.d.ts +169 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +628 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react-native/index.d.ts +35 -0
- package/dist/react-native/index.d.ts.map +1 -0
- package/dist/react-native/index.js +49 -0
- package/dist/react-native/index.js.map +1 -0
- package/dist/runtime-contract.d.ts +13 -0
- package/dist/runtime-contract.d.ts.map +1 -0
- package/dist/runtime-contract.js +24 -0
- package/dist/runtime-contract.js.map +1 -0
- package/dist/rust-client.d.ts +106 -0
- package/dist/rust-client.d.ts.map +1 -0
- package/dist/rust-client.js +936 -0
- package/dist/rust-client.js.map +1 -0
- package/dist/sentry.d.ts +35 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +155 -0
- package/dist/sentry.js.map +1 -0
- package/dist/sql-safety.d.ts +3 -0
- package/dist/sql-safety.d.ts.map +1 -0
- package/dist/sql-safety.js +62 -0
- package/dist/sql-safety.js.map +1 -0
- package/dist/syncular-runtime-artifacts.json +61 -0
- package/dist/tauri/index.d.ts +35 -0
- package/dist/tauri/index.d.ts.map +1 -0
- package/dist/tauri/index.js +114 -0
- package/dist/tauri/index.js.map +1 -0
- package/dist/types.d.ts +1059 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/wasm/.syncular-wasm-profile +1 -0
- package/dist/wasm/syncular-runtime-artifact.json +21 -0
- package/dist/wasm/syncular.d.ts +207 -0
- package/dist/wasm/syncular.js +2341 -0
- package/dist/wasm/syncular_bg.wasm +0 -0
- package/dist/wasm/syncular_bg.wasm.d.ts +97 -0
- package/dist/wasm-bindings/runtime-contract.d.ts +22 -0
- package/dist/wasm-bindings/runtime-contract.d.ts.map +1 -0
- package/dist/wasm-bindings/runtime-contract.js +112 -0
- package/dist/wasm-bindings/runtime-contract.js.map +1 -0
- package/dist/wasm-core/.syncular-wasm-profile +1 -0
- package/dist/wasm-core/syncular-runtime-artifact.json +17 -0
- package/dist/wasm-core/syncular.d.ts +162 -0
- package/dist/wasm-core/syncular.js +1847 -0
- package/dist/wasm-core/syncular_bg.wasm +0 -0
- package/dist/wasm-core/syncular_bg.wasm.d.ts +77 -0
- package/dist/wasm-perf/.syncular-wasm-profile +1 -0
- package/dist/wasm-perf/syncular-runtime-artifact.json +21 -0
- package/dist/wasm-perf/syncular.d.ts +207 -0
- package/dist/wasm-perf/syncular.js +2341 -0
- package/dist/wasm-perf/syncular_bg.wasm +0 -0
- package/dist/wasm-perf/syncular_bg.wasm.d.ts +97 -0
- package/dist/wasm-runtime.d.ts +18 -0
- package/dist/wasm-runtime.d.ts.map +1 -0
- package/dist/wasm-runtime.js +28 -0
- package/dist/wasm-runtime.js.map +1 -0
- package/dist/worker-client.d.ts +123 -0
- package/dist/worker-client.d.ts.map +1 -0
- package/dist/worker-client.js +1686 -0
- package/dist/worker-client.js.map +1 -0
- package/dist/worker-entry.d.ts +2 -0
- package/dist/worker-entry.d.ts.map +1 -0
- package/dist/worker-entry.js +479 -0
- package/dist/worker-entry.js.map +1 -0
- package/dist/worker-protocol.d.ts +64 -0
- package/dist/worker-protocol.d.ts.map +1 -0
- package/dist/worker-protocol.js +16 -0
- package/dist/worker-protocol.js.map +1 -0
- package/dist/worker-realtime.d.ts +39 -0
- package/dist/worker-realtime.d.ts.map +1 -0
- package/dist/worker-realtime.js +677 -0
- package/dist/worker-realtime.js.map +1 -0
- package/package.json +124 -17
- package/src/auth-leases.ts +251 -0
- package/src/blob-limits.ts +98 -0
- package/src/bridge-client.ts +512 -0
- package/src/client-config.ts +29 -0
- package/src/client.ts +256 -1414
- package/src/command-history.ts +623 -0
- package/src/console-diagnostics.ts +617 -0
- package/src/crdt-yjs/index.ts +3 -0
- package/src/crdt-yjs/webview-host-facade.ts +477 -0
- package/src/crdt-yjs/yjs-document-field-adapter.ts +733 -0
- package/src/crdt-yjs/yjs-prosemirror-bridge.ts +272 -0
- package/src/database.ts +1342 -0
- package/src/diagnostics.ts +116 -0
- package/src/errors.ts +269 -0
- package/src/generated-bridge.ts +741 -0
- package/src/index.ts +21 -25
- package/src/mutations.ts +12 -821
- package/src/network.ts +32 -0
- package/src/react/index.ts +1036 -0
- package/src/react-native/index.ts +152 -0
- package/src/runtime-contract.ts +48 -0
- package/src/rust-client.ts +1488 -0
- package/src/sentry.ts +215 -0
- package/src/sql-safety.ts +61 -0
- package/src/tauri/index.ts +211 -0
- package/src/types.ts +1356 -0
- package/src/wasm-bindings/generated-wasm-bindings.d.ts +70 -0
- package/src/wasm-bindings/runtime-contract.ts +158 -0
- package/src/wasm-runtime.ts +76 -0
- package/src/worker-client.ts +2215 -0
- package/src/worker-entry.ts +584 -0
- package/src/worker-protocol.ts +109 -0
- package/src/worker-realtime.ts +843 -0
- package/dist/blobs/index.d.ts +0 -7
- package/dist/blobs/index.d.ts.map +0 -1
- package/dist/blobs/index.js +0 -7
- package/dist/blobs/index.js.map +0 -1
- package/dist/blobs/manager.d.ts +0 -345
- package/dist/blobs/manager.d.ts.map +0 -1
- package/dist/blobs/manager.js +0 -749
- package/dist/blobs/manager.js.map +0 -1
- package/dist/blobs/migrate.d.ts +0 -14
- package/dist/blobs/migrate.d.ts.map +0 -1
- package/dist/blobs/migrate.js +0 -59
- package/dist/blobs/migrate.js.map +0 -1
- package/dist/blobs/types.d.ts +0 -62
- package/dist/blobs/types.d.ts.map +0 -1
- package/dist/blobs/types.js +0 -5
- package/dist/blobs/types.js.map +0 -1
- package/dist/conflicts.d.ts +0 -31
- package/dist/conflicts.d.ts.map +0 -1
- package/dist/conflicts.js +0 -112
- package/dist/conflicts.js.map +0 -1
- package/dist/create-client.d.ts +0 -118
- package/dist/create-client.d.ts.map +0 -1
- package/dist/create-client.js +0 -178
- package/dist/create-client.js.map +0 -1
- package/dist/engine/SyncEngine.d.ts +0 -272
- package/dist/engine/SyncEngine.d.ts.map +0 -1
- package/dist/engine/SyncEngine.js +0 -1860
- package/dist/engine/SyncEngine.js.map +0 -1
- package/dist/engine/index.d.ts +0 -6
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -6
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/types.d.ts +0 -345
- package/dist/engine/types.d.ts.map +0 -1
- package/dist/engine/types.js +0 -7
- package/dist/engine/types.js.map +0 -1
- package/dist/handlers/collection.d.ts +0 -6
- package/dist/handlers/collection.d.ts.map +0 -1
- package/dist/handlers/collection.js +0 -21
- package/dist/handlers/collection.js.map +0 -1
- package/dist/handlers/create-handler.d.ts +0 -120
- package/dist/handlers/create-handler.d.ts.map +0 -1
- package/dist/handlers/create-handler.js +0 -145
- package/dist/handlers/create-handler.js.map +0 -1
- package/dist/handlers/types.d.ts +0 -83
- package/dist/handlers/types.d.ts.map +0 -1
- package/dist/handlers/types.js +0 -5
- package/dist/handlers/types.js.map +0 -1
- package/dist/migrate.d.ts +0 -19
- package/dist/migrate.d.ts.map +0 -1
- package/dist/migrate.js +0 -238
- package/dist/migrate.js.map +0 -1
- package/dist/outbox.d.ts +0 -112
- package/dist/outbox.d.ts.map +0 -1
- package/dist/outbox.js +0 -294
- package/dist/outbox.js.map +0 -1
- package/dist/plugins/incrementing-version.d.ts +0 -34
- package/dist/plugins/incrementing-version.d.ts.map +0 -1
- package/dist/plugins/incrementing-version.js +0 -83
- package/dist/plugins/incrementing-version.js.map +0 -1
- package/dist/plugins/index.d.ts +0 -3
- package/dist/plugins/index.d.ts.map +0 -1
- package/dist/plugins/index.js +0 -3
- package/dist/plugins/index.js.map +0 -1
- package/dist/plugins/types.d.ts +0 -49
- package/dist/plugins/types.d.ts.map +0 -1
- package/dist/plugins/types.js +0 -15
- package/dist/plugins/types.js.map +0 -1
- package/dist/proxy/connection.d.ts +0 -33
- package/dist/proxy/connection.d.ts.map +0 -1
- package/dist/proxy/connection.js +0 -153
- package/dist/proxy/connection.js.map +0 -1
- package/dist/proxy/dialect.d.ts +0 -46
- package/dist/proxy/dialect.d.ts.map +0 -1
- package/dist/proxy/dialect.js +0 -58
- package/dist/proxy/dialect.js.map +0 -1
- package/dist/proxy/driver.d.ts +0 -42
- package/dist/proxy/driver.d.ts.map +0 -1
- package/dist/proxy/driver.js +0 -78
- package/dist/proxy/driver.js.map +0 -1
- package/dist/proxy/index.d.ts +0 -10
- package/dist/proxy/index.d.ts.map +0 -1
- package/dist/proxy/index.js +0 -10
- package/dist/proxy/index.js.map +0 -1
- package/dist/proxy/mutations.d.ts +0 -9
- package/dist/proxy/mutations.d.ts.map +0 -1
- package/dist/proxy/mutations.js +0 -11
- package/dist/proxy/mutations.js.map +0 -1
- package/dist/pull-engine.d.ts +0 -48
- package/dist/pull-engine.d.ts.map +0 -1
- package/dist/pull-engine.js +0 -648
- package/dist/pull-engine.js.map +0 -1
- package/dist/push-engine.d.ts +0 -18
- package/dist/push-engine.d.ts.map +0 -1
- package/dist/push-engine.js +0 -167
- package/dist/push-engine.js.map +0 -1
- package/dist/query/FingerprintCollector.d.ts +0 -18
- package/dist/query/FingerprintCollector.d.ts.map +0 -1
- package/dist/query/FingerprintCollector.js +0 -28
- package/dist/query/FingerprintCollector.js.map +0 -1
- package/dist/query/QueryContext.d.ts +0 -33
- package/dist/query/QueryContext.d.ts.map +0 -1
- package/dist/query/QueryContext.js +0 -16
- package/dist/query/QueryContext.js.map +0 -1
- package/dist/query/fingerprint.d.ts +0 -61
- package/dist/query/fingerprint.d.ts.map +0 -1
- package/dist/query/fingerprint.js +0 -91
- package/dist/query/fingerprint.js.map +0 -1
- package/dist/query/index.d.ts +0 -7
- package/dist/query/index.d.ts.map +0 -1
- package/dist/query/index.js +0 -7
- package/dist/query/index.js.map +0 -1
- package/dist/query/tracked-select.d.ts +0 -18
- package/dist/query/tracked-select.d.ts.map +0 -1
- package/dist/query/tracked-select.js +0 -90
- package/dist/query/tracked-select.js.map +0 -1
- package/dist/schema.d.ts +0 -83
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js +0 -7
- package/dist/schema.js.map +0 -1
- package/dist/subscription-state.d.ts +0 -46
- package/dist/subscription-state.d.ts.map +0 -1
- package/dist/subscription-state.js +0 -185
- package/dist/subscription-state.js.map +0 -1
- package/dist/sync-loop.d.ts +0 -34
- package/dist/sync-loop.d.ts.map +0 -1
- package/dist/sync-loop.js +0 -253
- package/dist/sync-loop.js.map +0 -1
- package/dist/sync.d.ts +0 -32
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -55
- package/dist/sync.js.map +0 -1
- package/dist/utils/id.d.ts +0 -17
- package/dist/utils/id.d.ts.map +0 -1
- package/dist/utils/id.js +0 -46
- package/dist/utils/id.js.map +0 -1
- package/src/blobs/index.ts +0 -7
- package/src/blobs/manager.ts +0 -1027
- package/src/blobs/migrate.ts +0 -67
- package/src/blobs/types.ts +0 -84
- package/src/client.test.ts +0 -417
- package/src/conflicts.ts +0 -171
- package/src/create-client.test.ts +0 -83
- package/src/create-client.ts +0 -330
- package/src/engine/SyncEngine.test.ts +0 -254
- package/src/engine/SyncEngine.ts +0 -2375
- package/src/engine/index.ts +0 -6
- package/src/engine/types.ts +0 -419
- package/src/handlers/collection.ts +0 -36
- package/src/handlers/create-handler.ts +0 -330
- package/src/handlers/types.ts +0 -108
- package/src/migrate.ts +0 -318
- package/src/outbox.ts +0 -383
- package/src/plugins/incrementing-version.ts +0 -133
- package/src/plugins/index.ts +0 -2
- package/src/plugins/types.ts +0 -63
- package/src/proxy/connection.ts +0 -191
- package/src/proxy/dialect.ts +0 -76
- package/src/proxy/driver.ts +0 -126
- package/src/proxy/index.ts +0 -10
- package/src/proxy/mutations.ts +0 -18
- package/src/pull-engine.test.ts +0 -151
- package/src/pull-engine.ts +0 -855
- package/src/push-engine.ts +0 -216
- package/src/query/FingerprintCollector.ts +0 -29
- package/src/query/QueryContext.ts +0 -54
- package/src/query/fingerprint.ts +0 -109
- package/src/query/index.ts +0 -10
- package/src/query/tracked-select.ts +0 -139
- package/src/schema.ts +0 -94
- package/src/subscription-state.ts +0 -259
- package/src/sync-loop.ts +0 -379
- package/src/sync.ts +0 -170
- package/src/utils/id.ts +0 -55
package/src/database.ts
ADDED
|
@@ -0,0 +1,1342 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyCodecsToDbRow,
|
|
3
|
+
type BlobRef,
|
|
4
|
+
type ColumnCodecDialect,
|
|
5
|
+
type ColumnCodecSource,
|
|
6
|
+
createColumnCodecsPlugin,
|
|
7
|
+
createTableColumnCodecsResolver,
|
|
8
|
+
randomId,
|
|
9
|
+
type SyncAuthLeaseIssueRequest,
|
|
10
|
+
type SyncOperation,
|
|
11
|
+
} from '@syncular/core';
|
|
12
|
+
import {
|
|
13
|
+
type CompiledQuery,
|
|
14
|
+
type DatabaseConnection,
|
|
15
|
+
type Dialect,
|
|
16
|
+
Kysely,
|
|
17
|
+
CompiledQuery as KyselyCompiledQuery,
|
|
18
|
+
type QueryResult,
|
|
19
|
+
} from 'kysely';
|
|
20
|
+
import { BaseSqliteDialect, BaseSqliteDriver } from 'kysely-generic-sqlite';
|
|
21
|
+
import {
|
|
22
|
+
assertSyncularBlobPayloadLimit,
|
|
23
|
+
type SyncularBlobLimitInput,
|
|
24
|
+
syncularBlobInputSize,
|
|
25
|
+
} from './blob-limits';
|
|
26
|
+
import {
|
|
27
|
+
getSyncularClientStatus,
|
|
28
|
+
SyncularClientLifecycle,
|
|
29
|
+
type SyncularClientStatus,
|
|
30
|
+
type SyncularConflictsClientLike,
|
|
31
|
+
type SyncularPresenceClientLike,
|
|
32
|
+
} from './client';
|
|
33
|
+
import { createSyncularConsoleDiagnosticsPublisher } from './console-diagnostics';
|
|
34
|
+
import { isSyncularOfflineError } from './errors';
|
|
35
|
+
import {
|
|
36
|
+
createMutationsApi,
|
|
37
|
+
type MutationsApi,
|
|
38
|
+
type MutationsCommitFn,
|
|
39
|
+
type MutationsTx,
|
|
40
|
+
} from './mutations';
|
|
41
|
+
import { browserSyncularNetworkStatusSource } from './network';
|
|
42
|
+
import { assertSyncularReadonlySql } from './sql-safety';
|
|
43
|
+
import type {
|
|
44
|
+
CreateSyncularDatabaseOptions,
|
|
45
|
+
SyncularAuthLeaseRecord,
|
|
46
|
+
SyncularBlobStoreOptions,
|
|
47
|
+
SyncularBlobs,
|
|
48
|
+
SyncularClientEventSink,
|
|
49
|
+
SyncularClientEventType,
|
|
50
|
+
SyncularCrdtYjsFieldConfig,
|
|
51
|
+
SyncularDiagnosticSnapshot,
|
|
52
|
+
SyncularLiveQueries,
|
|
53
|
+
SyncularLiveQueryDependencyHint,
|
|
54
|
+
SyncularLiveQueryEvent,
|
|
55
|
+
SyncularLiveQueryOptions,
|
|
56
|
+
SyncularLiveQuerySubscription,
|
|
57
|
+
SyncularNetworkStatusSource,
|
|
58
|
+
SyncularRuntimeClient,
|
|
59
|
+
SyncularSqlClient,
|
|
60
|
+
SyncularSubscriptionSpec,
|
|
61
|
+
SyncularSyncRequestOptions,
|
|
62
|
+
SyncularSyncResult,
|
|
63
|
+
SyncularTableConfigMap,
|
|
64
|
+
SyncularUnsafeSqlClient,
|
|
65
|
+
} from './types';
|
|
66
|
+
import { createSyncularWorkerClient } from './worker-client';
|
|
67
|
+
|
|
68
|
+
type SyncularDriverClient = SyncularRuntimeClient &
|
|
69
|
+
Partial<SyncularUnsafeSqlClient>;
|
|
70
|
+
type SyncularConnectionClient = SyncularSqlClient &
|
|
71
|
+
Partial<SyncularUnsafeSqlClient>;
|
|
72
|
+
|
|
73
|
+
export interface SyncularDialect extends Dialect, SyncularLiveQueries {
|
|
74
|
+
destroyLiveQueries(): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface SyncularDialectOptions {
|
|
78
|
+
appTables?: readonly string[];
|
|
79
|
+
tableConfig?: SyncularTableConfigMap;
|
|
80
|
+
unsafeWrites?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface SyncularDatabase<DB> extends SyncularLiveQueries {
|
|
84
|
+
db: Kysely<DB>;
|
|
85
|
+
client: SyncularRuntimeClient;
|
|
86
|
+
dialect: SyncularDialect;
|
|
87
|
+
mutations: MutationsApi<DB, undefined>;
|
|
88
|
+
leasedMutations: MutationsApi<DB, undefined>;
|
|
89
|
+
blobs: SyncularBlobs;
|
|
90
|
+
/**
|
|
91
|
+
* The sync lifecycle owned by this database. It starts automatically while
|
|
92
|
+
* the database opens (remote mode, unless `lifecycle.autoStart` is false)
|
|
93
|
+
* and stops on `close()`. Reach for it directly only for advanced control;
|
|
94
|
+
* `start`/`stop`/`sync` proxy to it.
|
|
95
|
+
*/
|
|
96
|
+
lifecycle: SyncularClientLifecycle;
|
|
97
|
+
start(): Promise<void>;
|
|
98
|
+
stop(): Promise<void>;
|
|
99
|
+
sync(): Promise<SyncularSyncResult>;
|
|
100
|
+
on<T extends SyncularClientEventType>(
|
|
101
|
+
event: T,
|
|
102
|
+
listener: SyncularClientEventSink<T>
|
|
103
|
+
): () => void;
|
|
104
|
+
getStatus(): SyncularClientStatus;
|
|
105
|
+
setSubscriptions(
|
|
106
|
+
subscriptions: readonly SyncularSubscriptionSpec[]
|
|
107
|
+
): Promise<void>;
|
|
108
|
+
resumeFromBackground(
|
|
109
|
+
options?: SyncularSyncRequestOptions
|
|
110
|
+
): Promise<SyncularSyncResult>;
|
|
111
|
+
issueAuthLease(
|
|
112
|
+
request: SyncAuthLeaseIssueRequest
|
|
113
|
+
): Promise<SyncularAuthLeaseRecord>;
|
|
114
|
+
upsertAuthLease(lease: SyncularAuthLeaseRecord): Promise<void>;
|
|
115
|
+
authLease(leaseId: string): Promise<SyncularAuthLeaseRecord | null>;
|
|
116
|
+
activeAuthLeases(
|
|
117
|
+
actorId?: string | null,
|
|
118
|
+
nowMs?: number
|
|
119
|
+
): Promise<SyncularAuthLeaseRecord[]>;
|
|
120
|
+
diagnosticSnapshot(): Promise<SyncularDiagnosticSnapshot>;
|
|
121
|
+
presence: SyncularPresenceClientLike;
|
|
122
|
+
conflicts: SyncularConflictsClientLike;
|
|
123
|
+
close(): Promise<void>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function withSyncularSchemaWrites<DB, Result>(
|
|
127
|
+
database: Pick<SyncularDatabase<DB>, 'client'>,
|
|
128
|
+
callback: (db: Kysely<any>) => Promise<Result>
|
|
129
|
+
): Promise<Result> {
|
|
130
|
+
const client = assertUnsafeSqlClient(database.client);
|
|
131
|
+
const dialect = createSyncularDialect(client, { unsafeWrites: true });
|
|
132
|
+
const db = new Kysely<any>({ dialect });
|
|
133
|
+
try {
|
|
134
|
+
return await callback(db);
|
|
135
|
+
} finally {
|
|
136
|
+
await dialect.destroyLiveQueries();
|
|
137
|
+
await db.destroy();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface SyncularMutationsMeta {
|
|
142
|
+
operations: SyncOperation[];
|
|
143
|
+
localMutations: Array<{
|
|
144
|
+
table: string;
|
|
145
|
+
rowId: string;
|
|
146
|
+
op: 'upsert' | 'delete';
|
|
147
|
+
}>;
|
|
148
|
+
clientCommitIds: string[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface SyncularMutationsOptions {
|
|
152
|
+
client: Pick<
|
|
153
|
+
SyncularRuntimeClient,
|
|
154
|
+
| 'applyMutation'
|
|
155
|
+
| 'applyMutationsBatch'
|
|
156
|
+
| 'applyMutationsCommit'
|
|
157
|
+
| 'applyYjsEnvelopeToPayload'
|
|
158
|
+
| 'executeSql'
|
|
159
|
+
> &
|
|
160
|
+
Partial<
|
|
161
|
+
Pick<
|
|
162
|
+
SyncularRuntimeClient,
|
|
163
|
+
'applyLeasedMutation' | 'applyLeasedMutationsCommit'
|
|
164
|
+
>
|
|
165
|
+
>;
|
|
166
|
+
requireAuthLease?: boolean;
|
|
167
|
+
codecs?: ColumnCodecSource;
|
|
168
|
+
codecDialect?: ColumnCodecDialect;
|
|
169
|
+
idColumn?: string;
|
|
170
|
+
versionColumn?: string | null;
|
|
171
|
+
omitColumns?: string[];
|
|
172
|
+
tableConfig?: SyncularTableConfigMap;
|
|
173
|
+
readBaseVersion?: (args: {
|
|
174
|
+
table: string;
|
|
175
|
+
rowId: string;
|
|
176
|
+
idColumn: string;
|
|
177
|
+
versionColumn: string;
|
|
178
|
+
}) => Promise<number | null>;
|
|
179
|
+
afterCommit?: (meta: SyncularMutationsMeta) => void | Promise<void>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function createSyncularDatabase<DB>(
|
|
183
|
+
options: CreateSyncularDatabaseOptions
|
|
184
|
+
): Promise<SyncularDatabase<DB>> {
|
|
185
|
+
const client = await createSyncularWorkerClient(options);
|
|
186
|
+
const dialect = createSyncularDialect(client, {
|
|
187
|
+
appTables: options.appTables,
|
|
188
|
+
tableConfig: options.tableConfig,
|
|
189
|
+
});
|
|
190
|
+
const db = new Kysely<DB>({
|
|
191
|
+
dialect,
|
|
192
|
+
plugins: options.codecs
|
|
193
|
+
? [
|
|
194
|
+
createColumnCodecsPlugin({
|
|
195
|
+
codecs: options.codecs,
|
|
196
|
+
dialect: 'sqlite',
|
|
197
|
+
}),
|
|
198
|
+
]
|
|
199
|
+
: undefined,
|
|
200
|
+
});
|
|
201
|
+
let closed = false;
|
|
202
|
+
const localClientMode = (options.config.mode ?? 'remote') !== 'remote';
|
|
203
|
+
const mutationSyncScheduler = createMutationSyncScheduler(client, {
|
|
204
|
+
enabled: options.sync?.autoSyncAfterMutation ?? !localClientMode,
|
|
205
|
+
debounceMs: options.sync?.mutationSyncDebounceMs ?? 10,
|
|
206
|
+
network:
|
|
207
|
+
options.sync?.network === false
|
|
208
|
+
? undefined
|
|
209
|
+
: (options.sync?.network ?? browserSyncularNetworkStatusSource()),
|
|
210
|
+
isClosed: () => closed,
|
|
211
|
+
});
|
|
212
|
+
const blobUploadScheduler = createBlobUploadScheduler(client, {
|
|
213
|
+
enabled: options.sync?.autoProcessBlobUploadsAfterStore ?? false,
|
|
214
|
+
debounceMs: options.sync?.blobUploadDebounceMs ?? 10,
|
|
215
|
+
isClosed: () => closed,
|
|
216
|
+
});
|
|
217
|
+
const consoleDiagnostics =
|
|
218
|
+
options.consoleDiagnostics === undefined ||
|
|
219
|
+
options.consoleDiagnostics === false
|
|
220
|
+
? undefined
|
|
221
|
+
: createSyncularConsoleDiagnosticsPublisher(client, {
|
|
222
|
+
...(options.consoleDiagnostics === true
|
|
223
|
+
? {}
|
|
224
|
+
: options.consoleDiagnostics),
|
|
225
|
+
config: options.config,
|
|
226
|
+
isClosed: () => closed,
|
|
227
|
+
});
|
|
228
|
+
const mutations = createSyncularMutations<DB>({
|
|
229
|
+
client,
|
|
230
|
+
codecs: options.codecs,
|
|
231
|
+
codecDialect: 'sqlite',
|
|
232
|
+
tableConfig: options.tableConfig,
|
|
233
|
+
readBaseVersion: (args) => readCurrentBaseVersion(client, args),
|
|
234
|
+
afterCommit: () => mutationSyncScheduler.schedule(),
|
|
235
|
+
});
|
|
236
|
+
const leasedMutations = createSyncularMutations<DB>({
|
|
237
|
+
client,
|
|
238
|
+
requireAuthLease: true,
|
|
239
|
+
codecs: options.codecs,
|
|
240
|
+
codecDialect: 'sqlite',
|
|
241
|
+
tableConfig: options.tableConfig,
|
|
242
|
+
readBaseVersion: (args) => readCurrentBaseVersion(client, args),
|
|
243
|
+
afterCommit: () => mutationSyncScheduler.schedule(),
|
|
244
|
+
});
|
|
245
|
+
const blobs = createSyncularBlobClient(client, {
|
|
246
|
+
diagnostics: options.diagnostics,
|
|
247
|
+
limits: options.blobLimits,
|
|
248
|
+
afterStore: ({ options }) => {
|
|
249
|
+
if (options?.immediate) return;
|
|
250
|
+
blobUploadScheduler.schedule();
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
const lifecycle = new SyncularClientLifecycle(client, {
|
|
254
|
+
realtime: options.realtime ?? true,
|
|
255
|
+
initialSync: options.lifecycle?.initialSync,
|
|
256
|
+
syncOnRealtimeConnect: options.lifecycle?.syncOnRealtimeConnect,
|
|
257
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
258
|
+
network: options.sync?.network,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const database: SyncularDatabase<DB> = {
|
|
262
|
+
db,
|
|
263
|
+
client,
|
|
264
|
+
dialect,
|
|
265
|
+
mutations,
|
|
266
|
+
leasedMutations,
|
|
267
|
+
blobs,
|
|
268
|
+
lifecycle,
|
|
269
|
+
live: (query, liveOptions) => dialect.live(query, liveOptions),
|
|
270
|
+
start: () => lifecycle.start(),
|
|
271
|
+
stop: () => lifecycle.stop(),
|
|
272
|
+
sync: () => lifecycle.sync(),
|
|
273
|
+
on: (event, listener) => client.addEventListener(event, listener),
|
|
274
|
+
getStatus: () => getSyncularClientStatus(client),
|
|
275
|
+
setSubscriptions: (subscriptions) => client.setSubscriptions(subscriptions),
|
|
276
|
+
resumeFromBackground: (syncOptions) =>
|
|
277
|
+
client.resumeFromBackground(syncOptions),
|
|
278
|
+
issueAuthLease: (request) => client.issueAuthLease(request),
|
|
279
|
+
upsertAuthLease: (lease) => client.upsertAuthLease(lease),
|
|
280
|
+
authLease: (leaseId) => client.authLease(leaseId),
|
|
281
|
+
activeAuthLeases: (actorId, nowMs) =>
|
|
282
|
+
client.activeAuthLeases(actorId, nowMs),
|
|
283
|
+
diagnosticSnapshot: () => client.diagnosticSnapshot(),
|
|
284
|
+
presence: {
|
|
285
|
+
get: (scopeKey) => client.getPresence(scopeKey),
|
|
286
|
+
join: (scopeKey, metadata) => client.joinPresence(scopeKey, metadata),
|
|
287
|
+
leave: (scopeKey) => client.leavePresence(scopeKey),
|
|
288
|
+
updateMetadata: (scopeKey, metadata) =>
|
|
289
|
+
client.updatePresenceMetadata(scopeKey, metadata),
|
|
290
|
+
onChange: (listener) => client.addPresenceListener(listener),
|
|
291
|
+
},
|
|
292
|
+
conflicts: {
|
|
293
|
+
list: () => client.conflictSummaries(),
|
|
294
|
+
retryKeepLocal: (id) => client.retryConflictKeepLocal(id),
|
|
295
|
+
resolve: (id, resolution) => client.resolveConflict(id, resolution),
|
|
296
|
+
},
|
|
297
|
+
async close() {
|
|
298
|
+
if (closed) return;
|
|
299
|
+
closed = true;
|
|
300
|
+
mutationSyncScheduler.destroy();
|
|
301
|
+
blobUploadScheduler.destroy();
|
|
302
|
+
consoleDiagnostics?.destroy();
|
|
303
|
+
try {
|
|
304
|
+
await lifecycle.stop().catch(() => undefined);
|
|
305
|
+
await dialect.destroyLiveQueries();
|
|
306
|
+
await db.destroy();
|
|
307
|
+
} finally {
|
|
308
|
+
await client.close();
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
if (options.subscriptions) {
|
|
315
|
+
await client.setSubscriptions(options.subscriptions);
|
|
316
|
+
}
|
|
317
|
+
if (options.lifecycle?.autoStart ?? !localClientMode) {
|
|
318
|
+
await lifecycle.start();
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
await database.close().catch(() => undefined);
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return database;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function createMutationSyncScheduler(
|
|
329
|
+
client: Pick<SyncularRuntimeClient, 'syncOnce'>,
|
|
330
|
+
options: {
|
|
331
|
+
enabled: boolean;
|
|
332
|
+
debounceMs: number | false;
|
|
333
|
+
network: SyncularNetworkStatusSource | undefined;
|
|
334
|
+
isClosed: () => boolean;
|
|
335
|
+
}
|
|
336
|
+
): { schedule(): void; destroy(): void } {
|
|
337
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
338
|
+
let inFlight: Promise<void> | undefined;
|
|
339
|
+
let queued = false;
|
|
340
|
+
const isOnline = () => options.network?.isOnline() !== false;
|
|
341
|
+
const clear = () => {
|
|
342
|
+
if (timer) clearTimeout(timer);
|
|
343
|
+
timer = undefined;
|
|
344
|
+
};
|
|
345
|
+
const run = async () => {
|
|
346
|
+
if (options.isClosed()) return;
|
|
347
|
+
if (!isOnline()) {
|
|
348
|
+
queued = true;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (inFlight) {
|
|
352
|
+
queued = true;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
inFlight = client
|
|
356
|
+
.syncOnce()
|
|
357
|
+
.then(() => undefined)
|
|
358
|
+
.catch((error) => {
|
|
359
|
+
if (!isOnline() || isSyncularOfflineError(error)) queued = true;
|
|
360
|
+
})
|
|
361
|
+
.finally(() => {
|
|
362
|
+
inFlight = undefined;
|
|
363
|
+
if (queued && !options.isClosed() && isOnline()) {
|
|
364
|
+
queued = false;
|
|
365
|
+
schedule();
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
await inFlight;
|
|
369
|
+
};
|
|
370
|
+
const schedule = () => {
|
|
371
|
+
if (!options.enabled || options.isClosed()) return;
|
|
372
|
+
if (!isOnline()) {
|
|
373
|
+
queued = true;
|
|
374
|
+
clear();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (inFlight) {
|
|
378
|
+
queued = true;
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
clear();
|
|
382
|
+
if (options.debounceMs === false || options.debounceMs <= 0) {
|
|
383
|
+
queueMicrotask(() => void run());
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
timer = setTimeout(() => void run(), options.debounceMs);
|
|
387
|
+
};
|
|
388
|
+
const handleOnline = () => {
|
|
389
|
+
if (!queued || options.isClosed()) return;
|
|
390
|
+
queued = false;
|
|
391
|
+
schedule();
|
|
392
|
+
};
|
|
393
|
+
options.network?.addEventListener?.('online', handleOnline);
|
|
394
|
+
return {
|
|
395
|
+
schedule,
|
|
396
|
+
destroy() {
|
|
397
|
+
clear();
|
|
398
|
+
queued = false;
|
|
399
|
+
options.network?.removeEventListener?.('online', handleOnline);
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function createBlobUploadScheduler(
|
|
405
|
+
client: Pick<SyncularRuntimeClient, 'processBlobUploadQueue'>,
|
|
406
|
+
options: {
|
|
407
|
+
enabled: boolean;
|
|
408
|
+
debounceMs: number | false;
|
|
409
|
+
isClosed: () => boolean;
|
|
410
|
+
}
|
|
411
|
+
): { schedule(): void; destroy(): void } {
|
|
412
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
413
|
+
let inFlight: Promise<void> | undefined;
|
|
414
|
+
let queued = false;
|
|
415
|
+
const clear = () => {
|
|
416
|
+
if (timer) clearTimeout(timer);
|
|
417
|
+
timer = undefined;
|
|
418
|
+
};
|
|
419
|
+
const run = async () => {
|
|
420
|
+
if (options.isClosed()) return;
|
|
421
|
+
if (inFlight) {
|
|
422
|
+
queued = true;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
inFlight = client
|
|
426
|
+
.processBlobUploadQueue()
|
|
427
|
+
.then(() => undefined)
|
|
428
|
+
.catch(() => undefined)
|
|
429
|
+
.finally(() => {
|
|
430
|
+
inFlight = undefined;
|
|
431
|
+
if (queued && !options.isClosed()) {
|
|
432
|
+
queued = false;
|
|
433
|
+
schedule();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
await inFlight;
|
|
437
|
+
};
|
|
438
|
+
const schedule = () => {
|
|
439
|
+
if (!options.enabled || options.isClosed()) return;
|
|
440
|
+
if (inFlight) {
|
|
441
|
+
queued = true;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
clear();
|
|
445
|
+
if (options.debounceMs === false || options.debounceMs <= 0) {
|
|
446
|
+
queueMicrotask(() => void run());
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
timer = setTimeout(() => void run(), options.debounceMs);
|
|
450
|
+
};
|
|
451
|
+
return {
|
|
452
|
+
schedule,
|
|
453
|
+
destroy() {
|
|
454
|
+
clear();
|
|
455
|
+
queued = false;
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function createSyncularBlobClient(
|
|
461
|
+
client: Pick<
|
|
462
|
+
SyncularRuntimeClient,
|
|
463
|
+
| 'storeBlob'
|
|
464
|
+
| 'retrieveBlob'
|
|
465
|
+
| 'isBlobLocal'
|
|
466
|
+
| 'processBlobUploadQueue'
|
|
467
|
+
| 'blobUploadQueueStats'
|
|
468
|
+
| 'blobCacheStats'
|
|
469
|
+
| 'pruneBlobCache'
|
|
470
|
+
| 'clearBlobCache'
|
|
471
|
+
>,
|
|
472
|
+
hooks: {
|
|
473
|
+
limits?: CreateSyncularDatabaseOptions['blobLimits'];
|
|
474
|
+
diagnostics?: CreateSyncularDatabaseOptions['diagnostics'];
|
|
475
|
+
afterStore?: (args: {
|
|
476
|
+
ref: BlobRef;
|
|
477
|
+
options?: SyncularBlobStoreOptions;
|
|
478
|
+
}) => void | Promise<void>;
|
|
479
|
+
} = {}
|
|
480
|
+
): SyncularBlobs {
|
|
481
|
+
const assertRetrieveWithinLimit = (ref: BlobRef) =>
|
|
482
|
+
assertSyncularBlobPayloadLimit({
|
|
483
|
+
operation: 'retrieve',
|
|
484
|
+
size: ref.size,
|
|
485
|
+
limits: hooks.limits,
|
|
486
|
+
refHash: ref.hash,
|
|
487
|
+
diagnostics: hooks.diagnostics,
|
|
488
|
+
});
|
|
489
|
+
return {
|
|
490
|
+
async store(data, storeOptions) {
|
|
491
|
+
assertSyncularBlobPayloadLimit({
|
|
492
|
+
operation: 'store',
|
|
493
|
+
size: syncularBlobInputSize(data as SyncularBlobLimitInput),
|
|
494
|
+
limits: hooks.limits,
|
|
495
|
+
options: storeOptions,
|
|
496
|
+
diagnostics: hooks.diagnostics,
|
|
497
|
+
});
|
|
498
|
+
const ref = await client.storeBlob(
|
|
499
|
+
await toUint8Array(data),
|
|
500
|
+
storeOptions
|
|
501
|
+
);
|
|
502
|
+
await hooks.afterStore?.({
|
|
503
|
+
ref,
|
|
504
|
+
...(storeOptions === undefined ? {} : { options: storeOptions }),
|
|
505
|
+
});
|
|
506
|
+
return ref;
|
|
507
|
+
},
|
|
508
|
+
async retrieve(ref) {
|
|
509
|
+
assertRetrieveWithinLimit(ref);
|
|
510
|
+
return client.retrieveBlob(ref);
|
|
511
|
+
},
|
|
512
|
+
isLocal(hash) {
|
|
513
|
+
return client.isBlobLocal(hash);
|
|
514
|
+
},
|
|
515
|
+
async preload(refs) {
|
|
516
|
+
for (const ref of refs) assertRetrieveWithinLimit(ref);
|
|
517
|
+
await Promise.all(refs.map((ref) => client.retrieveBlob(ref)));
|
|
518
|
+
},
|
|
519
|
+
processUploadQueue(options) {
|
|
520
|
+
return client.processBlobUploadQueue(options);
|
|
521
|
+
},
|
|
522
|
+
getUploadQueueStats() {
|
|
523
|
+
return client.blobUploadQueueStats();
|
|
524
|
+
},
|
|
525
|
+
getCacheStats() {
|
|
526
|
+
return client.blobCacheStats();
|
|
527
|
+
},
|
|
528
|
+
pruneCache(maxBytes) {
|
|
529
|
+
return client.pruneBlobCache(maxBytes);
|
|
530
|
+
},
|
|
531
|
+
clearCache() {
|
|
532
|
+
return client.clearBlobCache();
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function createSyncularDialect(
|
|
538
|
+
client: SyncularRuntimeClient,
|
|
539
|
+
options: SyncularDialectOptions = {}
|
|
540
|
+
): SyncularDialect {
|
|
541
|
+
const driver = new SyncularDriver(client, options);
|
|
542
|
+
const dialect = new BaseSqliteDialect(() => driver) as SyncularDialect;
|
|
543
|
+
dialect.live = (query, liveOptions) => driver.live(query, liveOptions);
|
|
544
|
+
dialect.destroyLiveQueries = () => driver.destroy();
|
|
545
|
+
return dialect;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function createSyncularMutations<DB>(
|
|
549
|
+
options: SyncularMutationsOptions
|
|
550
|
+
): MutationsApi<DB, undefined> {
|
|
551
|
+
return createMutationsApi(createSyncularCommit<DB>(options));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function createSyncularCommit<DB>(
|
|
555
|
+
options: SyncularMutationsOptions
|
|
556
|
+
): MutationsCommitFn<DB, SyncularMutationsMeta, undefined> {
|
|
557
|
+
const idColumn = options.idColumn ?? 'id';
|
|
558
|
+
const versionColumn = options.versionColumn ?? 'server_version';
|
|
559
|
+
const omitColumns = options.omitColumns ?? [];
|
|
560
|
+
const codecDialect = options.codecDialect ?? 'sqlite';
|
|
561
|
+
const resolveTableCodecs = createTableColumnCodecsResolver(options.codecs, {
|
|
562
|
+
dialect: codecDialect,
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
return async (fn) => {
|
|
566
|
+
const operations: SyncOperation[] = [];
|
|
567
|
+
const batch: Array<{
|
|
568
|
+
operation: SyncOperation;
|
|
569
|
+
localRow?: unknown | null;
|
|
570
|
+
}> = [];
|
|
571
|
+
const localMutations: SyncularMutationsMeta['localMutations'] = [];
|
|
572
|
+
const txTableCache = new Map<string, unknown>();
|
|
573
|
+
|
|
574
|
+
const makeTxTable = (table: string) => {
|
|
575
|
+
const cached = txTableCache.get(table);
|
|
576
|
+
if (cached) return cached;
|
|
577
|
+
const tableConfig = options.tableConfig?.[table];
|
|
578
|
+
const tableIdColumn = tableConfig?.primaryKeyColumn ?? idColumn;
|
|
579
|
+
const tableVersionColumn =
|
|
580
|
+
tableConfig?.serverVersionColumn === undefined
|
|
581
|
+
? versionColumn
|
|
582
|
+
: tableConfig.serverVersionColumn;
|
|
583
|
+
const softDeleteColumn = tableConfig?.softDeleteColumn ?? null;
|
|
584
|
+
const crdtYjsFields = serverMergeCrdtYjsFields(
|
|
585
|
+
tableConfig?.crdtYjsFields ?? []
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
const prepareInsert = async (values: unknown) => {
|
|
589
|
+
const raw = objectRecord(values);
|
|
590
|
+
const rawId = raw[tableIdColumn];
|
|
591
|
+
const id = typeof rawId === 'string' && rawId ? rawId : randomId();
|
|
592
|
+
const row = { ...raw, [tableIdColumn]: id };
|
|
593
|
+
const payload = sanitizeOperationPayload(row, {
|
|
594
|
+
omit: [
|
|
595
|
+
tableIdColumn,
|
|
596
|
+
...(tableVersionColumn ? [tableVersionColumn] : []),
|
|
597
|
+
...omitColumns,
|
|
598
|
+
],
|
|
599
|
+
});
|
|
600
|
+
const syncPayload = stripCrdtYjsMaterializedPayloadFields(
|
|
601
|
+
payload,
|
|
602
|
+
crdtYjsFields
|
|
603
|
+
);
|
|
604
|
+
const localPayload = sanitizeOperationPayload(row, {
|
|
605
|
+
omit: [
|
|
606
|
+
tableIdColumn,
|
|
607
|
+
...(tableVersionColumn ? [tableVersionColumn] : []),
|
|
608
|
+
],
|
|
609
|
+
});
|
|
610
|
+
const transformedLocalPayload = await transformCrdtYjsMutationPayload({
|
|
611
|
+
client: options.client,
|
|
612
|
+
table,
|
|
613
|
+
rowId: id,
|
|
614
|
+
payload: localPayload,
|
|
615
|
+
crdtYjsFields,
|
|
616
|
+
});
|
|
617
|
+
const localDbPayload = applyCodecsToDbRow(
|
|
618
|
+
transformedLocalPayload,
|
|
619
|
+
resolveTableCodecs(table, transformedLocalPayload),
|
|
620
|
+
codecDialect
|
|
621
|
+
);
|
|
622
|
+
const operation: SyncOperation = {
|
|
623
|
+
table,
|
|
624
|
+
row_id: id,
|
|
625
|
+
op: 'upsert',
|
|
626
|
+
payload: syncPayload,
|
|
627
|
+
base_version: null,
|
|
628
|
+
};
|
|
629
|
+
return {
|
|
630
|
+
id,
|
|
631
|
+
operation,
|
|
632
|
+
localRow: { ...localDbPayload, [tableIdColumn]: id },
|
|
633
|
+
};
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const tableApi = {
|
|
637
|
+
async insert(values: unknown) {
|
|
638
|
+
const prepared = await prepareInsert(values);
|
|
639
|
+
const { id, operation, localRow } = prepared;
|
|
640
|
+
operations.push(operation);
|
|
641
|
+
batch.push({ operation, localRow });
|
|
642
|
+
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
643
|
+
return id;
|
|
644
|
+
},
|
|
645
|
+
async insertMany(rows: unknown[]) {
|
|
646
|
+
if (rows.length === 0) {
|
|
647
|
+
throw new Error('insertMany requires at least one row');
|
|
648
|
+
}
|
|
649
|
+
const ids: string[] = [];
|
|
650
|
+
for (const row of rows) {
|
|
651
|
+
const { id, operation, localRow } = await prepareInsert(row);
|
|
652
|
+
ids.push(id);
|
|
653
|
+
operations.push(operation);
|
|
654
|
+
batch.push({ operation, localRow });
|
|
655
|
+
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
656
|
+
}
|
|
657
|
+
return ids;
|
|
658
|
+
},
|
|
659
|
+
async update(
|
|
660
|
+
id: string,
|
|
661
|
+
patch: unknown,
|
|
662
|
+
opts?: { baseVersion?: number | null }
|
|
663
|
+
) {
|
|
664
|
+
await applyUpsert(id, patch, opts, { mergeExistingLocalRow: true });
|
|
665
|
+
},
|
|
666
|
+
async delete(id: string, opts?: { baseVersion?: number | null }) {
|
|
667
|
+
const baseVersion = await resolveBaseVersion({
|
|
668
|
+
options,
|
|
669
|
+
table,
|
|
670
|
+
rowId: id,
|
|
671
|
+
idColumn: tableIdColumn,
|
|
672
|
+
versionColumn: tableVersionColumn,
|
|
673
|
+
explicit: opts?.baseVersion,
|
|
674
|
+
});
|
|
675
|
+
if (softDeleteColumn) {
|
|
676
|
+
const payload = { [softDeleteColumn]: 1 };
|
|
677
|
+
const localDbPayload = applyCodecsToDbRow(
|
|
678
|
+
payload,
|
|
679
|
+
resolveTableCodecs(table, payload),
|
|
680
|
+
codecDialect
|
|
681
|
+
);
|
|
682
|
+
const operation: SyncOperation = {
|
|
683
|
+
table,
|
|
684
|
+
row_id: id,
|
|
685
|
+
op: 'upsert',
|
|
686
|
+
payload,
|
|
687
|
+
base_version: baseVersion,
|
|
688
|
+
};
|
|
689
|
+
operations.push(operation);
|
|
690
|
+
batch.push({
|
|
691
|
+
operation,
|
|
692
|
+
localRow: { ...localDbPayload, [tableIdColumn]: id },
|
|
693
|
+
});
|
|
694
|
+
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const operation: SyncOperation = {
|
|
698
|
+
table,
|
|
699
|
+
row_id: id,
|
|
700
|
+
op: 'delete',
|
|
701
|
+
payload: null,
|
|
702
|
+
base_version: baseVersion,
|
|
703
|
+
};
|
|
704
|
+
operations.push(operation);
|
|
705
|
+
batch.push({ operation, localRow: null });
|
|
706
|
+
localMutations.push({ table, rowId: id, op: 'delete' });
|
|
707
|
+
},
|
|
708
|
+
async upsert(
|
|
709
|
+
id: string,
|
|
710
|
+
patch: unknown,
|
|
711
|
+
opts?: { baseVersion?: number | null }
|
|
712
|
+
) {
|
|
713
|
+
await applyUpsert(id, patch, opts, { mergeExistingLocalRow: false });
|
|
714
|
+
},
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const applyUpsert = async (
|
|
718
|
+
id: string,
|
|
719
|
+
patch: unknown,
|
|
720
|
+
opts: { baseVersion?: number | null } | undefined,
|
|
721
|
+
behavior: { mergeExistingLocalRow: boolean }
|
|
722
|
+
) => {
|
|
723
|
+
const rawPatch = objectRecord(patch);
|
|
724
|
+
const payload = sanitizeOperationPayload(rawPatch, {
|
|
725
|
+
omit: [
|
|
726
|
+
tableIdColumn,
|
|
727
|
+
...(tableVersionColumn ? [tableVersionColumn] : []),
|
|
728
|
+
...omitColumns,
|
|
729
|
+
],
|
|
730
|
+
});
|
|
731
|
+
const syncPayload = stripCrdtYjsMaterializedPayloadFields(
|
|
732
|
+
payload,
|
|
733
|
+
crdtYjsFields
|
|
734
|
+
);
|
|
735
|
+
const existingLocalRow = behavior.mergeExistingLocalRow
|
|
736
|
+
? await readExistingLocalRow({
|
|
737
|
+
client: options.client,
|
|
738
|
+
table,
|
|
739
|
+
rowId: id,
|
|
740
|
+
idColumn: tableIdColumn,
|
|
741
|
+
})
|
|
742
|
+
: null;
|
|
743
|
+
const rawLocalPayload = existingLocalRow
|
|
744
|
+
? { ...existingLocalRow, ...rawPatch }
|
|
745
|
+
: rawPatch;
|
|
746
|
+
const localPayload = sanitizeOperationPayload(rawLocalPayload, {
|
|
747
|
+
omit: [
|
|
748
|
+
tableIdColumn,
|
|
749
|
+
...(tableVersionColumn ? [tableVersionColumn] : []),
|
|
750
|
+
],
|
|
751
|
+
});
|
|
752
|
+
const existingRow = await readCrdtYjsExistingRow({
|
|
753
|
+
client: options.client,
|
|
754
|
+
table,
|
|
755
|
+
rowId: id,
|
|
756
|
+
idColumn: tableIdColumn,
|
|
757
|
+
crdtYjsFields,
|
|
758
|
+
});
|
|
759
|
+
const transformedLocalPayload = await transformCrdtYjsMutationPayload({
|
|
760
|
+
client: options.client,
|
|
761
|
+
table,
|
|
762
|
+
rowId: id,
|
|
763
|
+
payload: localPayload,
|
|
764
|
+
existingRow,
|
|
765
|
+
crdtYjsFields,
|
|
766
|
+
});
|
|
767
|
+
const localDbPayload = applyCodecsToDbRow(
|
|
768
|
+
transformedLocalPayload,
|
|
769
|
+
resolveTableCodecs(table, transformedLocalPayload),
|
|
770
|
+
codecDialect
|
|
771
|
+
);
|
|
772
|
+
const operation: SyncOperation = {
|
|
773
|
+
table,
|
|
774
|
+
row_id: id,
|
|
775
|
+
op: 'upsert',
|
|
776
|
+
payload: syncPayload,
|
|
777
|
+
base_version: await resolveBaseVersion({
|
|
778
|
+
options,
|
|
779
|
+
table,
|
|
780
|
+
rowId: id,
|
|
781
|
+
idColumn: tableIdColumn,
|
|
782
|
+
versionColumn: tableVersionColumn,
|
|
783
|
+
explicit: opts?.baseVersion,
|
|
784
|
+
}),
|
|
785
|
+
};
|
|
786
|
+
operations.push(operation);
|
|
787
|
+
batch.push({
|
|
788
|
+
operation,
|
|
789
|
+
localRow: { ...localDbPayload, [tableIdColumn]: id },
|
|
790
|
+
});
|
|
791
|
+
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
txTableCache.set(table, tableApi);
|
|
795
|
+
return tableApi;
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const tx = new Proxy(
|
|
799
|
+
{},
|
|
800
|
+
{
|
|
801
|
+
get(_target, prop) {
|
|
802
|
+
if (prop === 'then') return undefined;
|
|
803
|
+
if (typeof prop !== 'string') return undefined;
|
|
804
|
+
return makeTxTable(prop);
|
|
805
|
+
},
|
|
806
|
+
}
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
const result = await fn(tx as MutationsTx<DB>);
|
|
810
|
+
if (operations.length === 0) throw new Error('No mutations were enqueued');
|
|
811
|
+
const clientCommitId = options.requireAuthLease
|
|
812
|
+
? await requireLeasedMutationClient(
|
|
813
|
+
options.client
|
|
814
|
+
).applyLeasedMutationsCommit(batch)
|
|
815
|
+
: await options.client.applyMutationsCommit(batch);
|
|
816
|
+
const meta = {
|
|
817
|
+
operations,
|
|
818
|
+
localMutations,
|
|
819
|
+
clientCommitIds: [clientCommitId],
|
|
820
|
+
};
|
|
821
|
+
await options.afterCommit?.(meta);
|
|
822
|
+
return {
|
|
823
|
+
result,
|
|
824
|
+
receipt: { commitId: clientCommitId, clientCommitId },
|
|
825
|
+
meta,
|
|
826
|
+
};
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function requireLeasedMutationClient(
|
|
831
|
+
client: SyncularMutationsOptions['client']
|
|
832
|
+
): Required<
|
|
833
|
+
Pick<
|
|
834
|
+
SyncularRuntimeClient,
|
|
835
|
+
'applyLeasedMutation' | 'applyLeasedMutationsCommit'
|
|
836
|
+
>
|
|
837
|
+
> {
|
|
838
|
+
if (typeof client.applyLeasedMutationsCommit !== 'function') {
|
|
839
|
+
throw new Error(
|
|
840
|
+
'Syncular leased mutations require applyLeasedMutationsCommit support'
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
if (typeof client.applyLeasedMutation !== 'function') {
|
|
844
|
+
throw new Error(
|
|
845
|
+
'Syncular leased mutations require applyLeasedMutation support'
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
return client as Required<
|
|
849
|
+
Pick<
|
|
850
|
+
SyncularRuntimeClient,
|
|
851
|
+
'applyLeasedMutation' | 'applyLeasedMutationsCommit'
|
|
852
|
+
>
|
|
853
|
+
>;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
class SyncularDriver extends BaseSqliteDriver {
|
|
857
|
+
#listeners = new Map<
|
|
858
|
+
string,
|
|
859
|
+
(event: SyncularLiveQueryEvent<Record<string, unknown>>) => void
|
|
860
|
+
>();
|
|
861
|
+
readonly #appTables: Set<string> | undefined;
|
|
862
|
+
readonly #tableConfig: SyncularTableConfigMap | undefined;
|
|
863
|
+
|
|
864
|
+
constructor(
|
|
865
|
+
private readonly client: SyncularDriverClient,
|
|
866
|
+
options: SyncularDialectOptions = {}
|
|
867
|
+
) {
|
|
868
|
+
super(async () => {
|
|
869
|
+
this.conn = new SyncularConnection(client, options.unsafeWrites ?? false);
|
|
870
|
+
});
|
|
871
|
+
this.#appTables =
|
|
872
|
+
options.appTables == null
|
|
873
|
+
? undefined
|
|
874
|
+
: new Set(normalizeLiveQueryTables(options.appTables));
|
|
875
|
+
this.#tableConfig = options.tableConfig;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async live<Row extends Record<string, unknown>>(
|
|
879
|
+
query: { compile(): CompiledQuery },
|
|
880
|
+
options: SyncularLiveQueryOptions<Row>
|
|
881
|
+
): Promise<SyncularLiveQuerySubscription> {
|
|
882
|
+
const compiled = query.compile();
|
|
883
|
+
const tables = normalizeLiveQueryTables(
|
|
884
|
+
options.tables ?? inferTablesFromCompiledQuery(compiled, this.#appTables),
|
|
885
|
+
this.#appTables
|
|
886
|
+
);
|
|
887
|
+
if (tables.length === 0) {
|
|
888
|
+
throw new Error(
|
|
889
|
+
'Could not infer live query table dependencies. Pass { tables: [...] } explicitly.'
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
const snapshot = await this.client.subscribeQuery<Row>(
|
|
893
|
+
compiled.sql,
|
|
894
|
+
compiled.parameters,
|
|
895
|
+
tables,
|
|
896
|
+
inferDependencyHintsFromCompiledQuery(compiled, tables, this.#tableConfig)
|
|
897
|
+
);
|
|
898
|
+
const listener = (
|
|
899
|
+
event: SyncularLiveQueryEvent<Record<string, unknown>>
|
|
900
|
+
) => {
|
|
901
|
+
const typed = event as SyncularLiveQueryEvent<Row>;
|
|
902
|
+
options.onChange(typed.rows, { ...typed, initial: false });
|
|
903
|
+
};
|
|
904
|
+
this.#listeners.set(snapshot.id, listener);
|
|
905
|
+
this.client.addLiveQueryListener(snapshot.id, listener);
|
|
906
|
+
options.onChange(snapshot.rows, {
|
|
907
|
+
queryId: snapshot.id,
|
|
908
|
+
version: Date.now(),
|
|
909
|
+
changedRows: [],
|
|
910
|
+
rows: snapshot.rows,
|
|
911
|
+
initial: true,
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
return {
|
|
915
|
+
id: snapshot.id,
|
|
916
|
+
unsubscribe: () => {
|
|
917
|
+
void this.#unsubscribeQuery(snapshot.id).catch(() => undefined);
|
|
918
|
+
},
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
|
923
|
+
await connection.executeQuery(KyselyCompiledQuery.raw('begin'));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
override async destroy(): Promise<void> {
|
|
927
|
+
const ids = [...this.#listeners.keys()];
|
|
928
|
+
for (const id of ids) this.client.removeLiveQueryListener(id);
|
|
929
|
+
this.#listeners.clear();
|
|
930
|
+
await Promise.allSettled(ids.map((id) => this.client.unsubscribeQuery(id)));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async #unsubscribeQuery(id: string): Promise<void> {
|
|
934
|
+
if (!this.#listeners.has(id)) return;
|
|
935
|
+
this.#listeners.delete(id);
|
|
936
|
+
this.client.removeLiveQueryListener(id);
|
|
937
|
+
await this.client.unsubscribeQuery(id);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
class SyncularConnection implements DatabaseConnection {
|
|
942
|
+
constructor(
|
|
943
|
+
private readonly client: SyncularConnectionClient,
|
|
944
|
+
private readonly unsafeWrites: boolean
|
|
945
|
+
) {}
|
|
946
|
+
|
|
947
|
+
async executeQuery<R>(compiledQuery: CompiledQuery): Promise<QueryResult<R>> {
|
|
948
|
+
if (!this.unsafeWrites) assertSyncularReadonlySql(compiledQuery.sql);
|
|
949
|
+
const result = this.unsafeWrites
|
|
950
|
+
? await assertUnsafeSqlClient(this.client).executeUnsafeSql<
|
|
951
|
+
R & Record<string, unknown>
|
|
952
|
+
>(compiledQuery.sql, compiledQuery.parameters)
|
|
953
|
+
: await this.client.executeSql<R & Record<string, unknown>>(
|
|
954
|
+
compiledQuery.sql,
|
|
955
|
+
compiledQuery.parameters
|
|
956
|
+
);
|
|
957
|
+
return {
|
|
958
|
+
rows: result.rows as R[],
|
|
959
|
+
numAffectedRows:
|
|
960
|
+
result.numAffectedRows == null
|
|
961
|
+
? undefined
|
|
962
|
+
: BigInt(result.numAffectedRows),
|
|
963
|
+
insertId: result.insertId == null ? undefined : BigInt(result.insertId),
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
streamQuery<R>(
|
|
968
|
+
_compiledQuery: CompiledQuery,
|
|
969
|
+
_chunkSize?: number
|
|
970
|
+
): AsyncIterableIterator<QueryResult<R>> {
|
|
971
|
+
throw new Error('syncular sqlite dialect does not support streaming');
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function assertUnsafeSqlClient<Client extends SyncularSqlClient>(
|
|
976
|
+
client: Client
|
|
977
|
+
): Client & SyncularUnsafeSqlClient {
|
|
978
|
+
const maybe = client as Partial<SyncularUnsafeSqlClient>;
|
|
979
|
+
if (typeof maybe.executeUnsafeSql === 'function') {
|
|
980
|
+
return client as Client & SyncularUnsafeSqlClient;
|
|
981
|
+
}
|
|
982
|
+
throw new Error(
|
|
983
|
+
'Syncular schema installation requires an internal unsafe SQL client.'
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function objectRecord(value: unknown): Record<string, unknown> {
|
|
988
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
989
|
+
return { ...(value as Record<string, unknown>) };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
async function toUint8Array(
|
|
993
|
+
data: Blob | File | Uint8Array
|
|
994
|
+
): Promise<Uint8Array> {
|
|
995
|
+
if (data instanceof Uint8Array) return data;
|
|
996
|
+
return new Uint8Array(await data.arrayBuffer());
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function sanitizeOperationPayload(
|
|
1000
|
+
payload: Record<string, unknown>,
|
|
1001
|
+
args: { omit: readonly string[] }
|
|
1002
|
+
): Record<string, unknown> {
|
|
1003
|
+
if (args.omit.length === 0) return payload;
|
|
1004
|
+
const omit = new Set(args.omit);
|
|
1005
|
+
const out: Record<string, unknown> = {};
|
|
1006
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
1007
|
+
if (!omit.has(key)) out[key] = value;
|
|
1008
|
+
}
|
|
1009
|
+
return out;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function serverMergeCrdtYjsFields(
|
|
1013
|
+
fields: readonly SyncularCrdtYjsFieldConfig[]
|
|
1014
|
+
): readonly SyncularCrdtYjsFieldConfig[] {
|
|
1015
|
+
return fields.filter(
|
|
1016
|
+
(field) => field.syncMode === undefined || field.syncMode === 'server-merge'
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async function transformCrdtYjsMutationPayload(args: {
|
|
1021
|
+
client: Pick<SyncularRuntimeClient, 'applyYjsEnvelopeToPayload'>;
|
|
1022
|
+
table: string;
|
|
1023
|
+
rowId: string;
|
|
1024
|
+
payload: Record<string, unknown>;
|
|
1025
|
+
existingRow?: Record<string, unknown> | null;
|
|
1026
|
+
crdtYjsFields: readonly SyncularCrdtYjsFieldConfig[];
|
|
1027
|
+
}): Promise<Record<string, unknown>> {
|
|
1028
|
+
if (args.crdtYjsFields.length === 0 || !('__yjs' in args.payload)) {
|
|
1029
|
+
return args.payload;
|
|
1030
|
+
}
|
|
1031
|
+
return args.client.applyYjsEnvelopeToPayload({
|
|
1032
|
+
table: args.table,
|
|
1033
|
+
rowId: args.rowId,
|
|
1034
|
+
payload: args.payload,
|
|
1035
|
+
existingRow: args.existingRow ?? null,
|
|
1036
|
+
rules: args.crdtYjsFields.map((field) => ({
|
|
1037
|
+
table: args.table,
|
|
1038
|
+
field: field.field,
|
|
1039
|
+
stateColumn: field.stateColumn,
|
|
1040
|
+
containerKey: field.containerKey ?? field.field,
|
|
1041
|
+
rowIdField: field.rowIdField ?? 'id',
|
|
1042
|
+
kind: field.kind ?? 'text',
|
|
1043
|
+
})),
|
|
1044
|
+
stripEnvelope: true,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function stripCrdtYjsMaterializedPayloadFields(
|
|
1049
|
+
payload: Record<string, unknown>,
|
|
1050
|
+
crdtYjsFields: readonly SyncularCrdtYjsFieldConfig[]
|
|
1051
|
+
): Record<string, unknown> {
|
|
1052
|
+
const envelope = objectRecord(payload.__yjs);
|
|
1053
|
+
if (crdtYjsFields.length === 0 || Object.keys(envelope).length === 0) {
|
|
1054
|
+
return payload;
|
|
1055
|
+
}
|
|
1056
|
+
let next: Record<string, unknown> | null = null;
|
|
1057
|
+
const ensureNext = () => {
|
|
1058
|
+
next ??= { ...payload };
|
|
1059
|
+
return next;
|
|
1060
|
+
};
|
|
1061
|
+
for (const field of crdtYjsFields) {
|
|
1062
|
+
if (!(field.field in envelope)) continue;
|
|
1063
|
+
delete ensureNext()[field.field];
|
|
1064
|
+
delete ensureNext()[field.stateColumn];
|
|
1065
|
+
}
|
|
1066
|
+
return next ?? payload;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async function readCrdtYjsExistingRow(args: {
|
|
1070
|
+
client: Pick<SyncularRuntimeClient, 'executeSql'>;
|
|
1071
|
+
table: string;
|
|
1072
|
+
rowId: string;
|
|
1073
|
+
idColumn: string;
|
|
1074
|
+
crdtYjsFields: readonly SyncularCrdtYjsFieldConfig[];
|
|
1075
|
+
}): Promise<Record<string, unknown> | null> {
|
|
1076
|
+
if (args.crdtYjsFields.length === 0) return null;
|
|
1077
|
+
const columns = new Set<string>([args.idColumn]);
|
|
1078
|
+
for (const field of args.crdtYjsFields) {
|
|
1079
|
+
columns.add(field.field);
|
|
1080
|
+
columns.add(field.stateColumn);
|
|
1081
|
+
}
|
|
1082
|
+
const result = await args.client.executeSql(
|
|
1083
|
+
`select ${[...columns].map(quoteSqlIdentifier).join(', ')} from ${quoteSqlIdentifier(args.table)} where ${quoteSqlIdentifier(args.idColumn)} = ? limit 1`,
|
|
1084
|
+
[args.rowId]
|
|
1085
|
+
);
|
|
1086
|
+
return result.rows[0] ?? null;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async function readExistingLocalRow(args: {
|
|
1090
|
+
client: Pick<SyncularRuntimeClient, 'executeSql'>;
|
|
1091
|
+
table: string;
|
|
1092
|
+
rowId: string;
|
|
1093
|
+
idColumn: string;
|
|
1094
|
+
}): Promise<Record<string, unknown> | null> {
|
|
1095
|
+
const result = await args.client.executeSql(
|
|
1096
|
+
`select * from ${quoteSqlIdentifier(args.table)} where ${quoteSqlIdentifier(args.idColumn)} = ? limit 1`,
|
|
1097
|
+
[args.rowId]
|
|
1098
|
+
);
|
|
1099
|
+
return result.rows[0] ?? null;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function quoteSqlIdentifier(identifier: string): string {
|
|
1103
|
+
if (!identifier) throw new Error('SQLite identifier cannot be empty');
|
|
1104
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
async function readCurrentBaseVersion(
|
|
1108
|
+
client: Pick<SyncularRuntimeClient, 'executeSql'>,
|
|
1109
|
+
args: {
|
|
1110
|
+
table: string;
|
|
1111
|
+
rowId: string;
|
|
1112
|
+
idColumn: string;
|
|
1113
|
+
versionColumn: string;
|
|
1114
|
+
}
|
|
1115
|
+
): Promise<number | null> {
|
|
1116
|
+
const result = await client.executeSql(
|
|
1117
|
+
`select ${quoteSqlIdentifier(args.versionColumn)} as version from ${quoteSqlIdentifier(args.table)} where ${quoteSqlIdentifier(args.idColumn)} = ? limit 1`,
|
|
1118
|
+
[args.rowId]
|
|
1119
|
+
);
|
|
1120
|
+
return coerceBaseVersion(result.rows[0]?.version);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function coerceBaseVersion(value: unknown): number | null {
|
|
1124
|
+
if (value == null) return null;
|
|
1125
|
+
const numeric = typeof value === 'number' ? value : Number(value);
|
|
1126
|
+
if (!Number.isFinite(numeric) || numeric < 0) return null;
|
|
1127
|
+
return numeric;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async function resolveBaseVersion(args: {
|
|
1131
|
+
options: SyncularMutationsOptions;
|
|
1132
|
+
table: string;
|
|
1133
|
+
rowId: string;
|
|
1134
|
+
idColumn: string;
|
|
1135
|
+
versionColumn: string | null;
|
|
1136
|
+
explicit?: number | null;
|
|
1137
|
+
}): Promise<number | null> {
|
|
1138
|
+
if (args.explicit !== undefined) return coerceBaseVersion(args.explicit);
|
|
1139
|
+
if (!args.versionColumn) return null;
|
|
1140
|
+
const readBaseVersion =
|
|
1141
|
+
args.options.readBaseVersion ??
|
|
1142
|
+
((readArgs: {
|
|
1143
|
+
table: string;
|
|
1144
|
+
rowId: string;
|
|
1145
|
+
idColumn: string;
|
|
1146
|
+
versionColumn: string;
|
|
1147
|
+
}) => readCurrentBaseVersion(args.options.client, readArgs));
|
|
1148
|
+
return readBaseVersion({
|
|
1149
|
+
table: args.table,
|
|
1150
|
+
rowId: args.rowId,
|
|
1151
|
+
idColumn: args.idColumn,
|
|
1152
|
+
versionColumn: args.versionColumn,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function inferTablesFromCompiledQuery(
|
|
1157
|
+
query: CompiledQuery,
|
|
1158
|
+
appTables?: ReadonlySet<string>
|
|
1159
|
+
): string[] {
|
|
1160
|
+
const tables = new Set<string>();
|
|
1161
|
+
collectTables(query.query as OperationNodeLike, tables);
|
|
1162
|
+
return [...tables].filter(
|
|
1163
|
+
(table) => appTables == null || appTables.has(table)
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function inferDependencyHintsFromCompiledQuery(
|
|
1168
|
+
query: CompiledQuery,
|
|
1169
|
+
tables: readonly string[],
|
|
1170
|
+
tableConfig?: SyncularTableConfigMap
|
|
1171
|
+
): SyncularLiveQueryDependencyHint[] {
|
|
1172
|
+
if (tables.length !== 1) return [];
|
|
1173
|
+
const table = tables[0]!;
|
|
1174
|
+
const primaryKey = tableConfig?.[table]?.primaryKeyColumn ?? 'id';
|
|
1175
|
+
const rowIds = primaryKeyEqualityValues(query.query, table, primaryKey);
|
|
1176
|
+
return rowIds.length > 0 ? [{ table, rowIds }] : [];
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function primaryKeyEqualityValues(
|
|
1180
|
+
query: unknown,
|
|
1181
|
+
table: string,
|
|
1182
|
+
primaryKey: string
|
|
1183
|
+
): string[] {
|
|
1184
|
+
if (!isOperationNode(query) || query.kind !== 'SelectQueryNode') return [];
|
|
1185
|
+
const where = query.where;
|
|
1186
|
+
if (!isOperationNode(where) || where.kind !== 'WhereNode') return [];
|
|
1187
|
+
return collectConjunctivePrimaryKeyValues(where.where, table, primaryKey);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function collectConjunctivePrimaryKeyValues(
|
|
1191
|
+
node: unknown,
|
|
1192
|
+
table: string,
|
|
1193
|
+
primaryKey: string
|
|
1194
|
+
): string[] {
|
|
1195
|
+
if (!isOperationNode(node)) return [];
|
|
1196
|
+
if (node.kind === 'AndNode') {
|
|
1197
|
+
return uniqueStrings([
|
|
1198
|
+
...collectConjunctivePrimaryKeyValues(node.left, table, primaryKey),
|
|
1199
|
+
...collectConjunctivePrimaryKeyValues(node.right, table, primaryKey),
|
|
1200
|
+
]);
|
|
1201
|
+
}
|
|
1202
|
+
const equality = primaryKeyEqualityValue(node, table, primaryKey);
|
|
1203
|
+
return equality == null ? [] : [equality];
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function primaryKeyEqualityValue(
|
|
1207
|
+
node: unknown,
|
|
1208
|
+
table: string,
|
|
1209
|
+
primaryKey: string
|
|
1210
|
+
): string | undefined {
|
|
1211
|
+
if (!isOperationNode(node) || node.kind !== 'BinaryOperationNode') {
|
|
1212
|
+
return undefined;
|
|
1213
|
+
}
|
|
1214
|
+
const operator = node.operator;
|
|
1215
|
+
if (
|
|
1216
|
+
!isOperationNode(operator) ||
|
|
1217
|
+
operator.kind !== 'OperatorNode' ||
|
|
1218
|
+
operator.operator !== '='
|
|
1219
|
+
) {
|
|
1220
|
+
return undefined;
|
|
1221
|
+
}
|
|
1222
|
+
return (
|
|
1223
|
+
equalityValueForReference(
|
|
1224
|
+
node.leftOperand,
|
|
1225
|
+
node.rightOperand,
|
|
1226
|
+
table,
|
|
1227
|
+
primaryKey
|
|
1228
|
+
) ??
|
|
1229
|
+
equalityValueForReference(
|
|
1230
|
+
node.rightOperand,
|
|
1231
|
+
node.leftOperand,
|
|
1232
|
+
table,
|
|
1233
|
+
primaryKey
|
|
1234
|
+
)
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function equalityValueForReference(
|
|
1239
|
+
reference: unknown,
|
|
1240
|
+
value: unknown,
|
|
1241
|
+
table: string,
|
|
1242
|
+
primaryKey: string
|
|
1243
|
+
): string | undefined {
|
|
1244
|
+
if (!referenceMatchesColumn(reference, table, primaryKey)) return undefined;
|
|
1245
|
+
if (!isOperationNode(value) || value.kind !== 'ValueNode') return undefined;
|
|
1246
|
+
if (typeof value.value === 'string') return value.value;
|
|
1247
|
+
if (typeof value.value === 'number' && Number.isFinite(value.value)) {
|
|
1248
|
+
return String(value.value);
|
|
1249
|
+
}
|
|
1250
|
+
return undefined;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function referenceMatchesColumn(
|
|
1254
|
+
reference: unknown,
|
|
1255
|
+
table: string,
|
|
1256
|
+
column: string
|
|
1257
|
+
): boolean {
|
|
1258
|
+
if (!isOperationNode(reference) || reference.kind !== 'ReferenceNode') {
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
if (
|
|
1262
|
+
reference.table != null &&
|
|
1263
|
+
tableNameFromTableNode(reference.table) !== table
|
|
1264
|
+
) {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
const columnNode = reference.column;
|
|
1268
|
+
if (!isOperationNode(columnNode) || columnNode.kind !== 'ColumnNode') {
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
const identifier = columnNode.column;
|
|
1272
|
+
return (
|
|
1273
|
+
isOperationNode(identifier) &&
|
|
1274
|
+
identifier.kind === 'IdentifierNode' &&
|
|
1275
|
+
identifier.name === column
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function uniqueStrings(values: readonly string[]): string[] {
|
|
1280
|
+
return [...new Set(values)];
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
interface OperationNodeLike {
|
|
1284
|
+
kind?: string;
|
|
1285
|
+
[key: string]: unknown;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function collectTables(node: unknown, tables: Set<string>): void {
|
|
1289
|
+
if (Array.isArray(node)) {
|
|
1290
|
+
for (const item of node) collectTables(item, tables);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (!isOperationNode(node)) return;
|
|
1294
|
+
|
|
1295
|
+
if (node.kind === 'TableNode') collectTableNode(node, tables);
|
|
1296
|
+
for (const value of Object.values(node)) collectTables(value, tables);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function collectTableNode(node: unknown, tables: Set<string>): void {
|
|
1300
|
+
const table = tableNameFromTableNode(node);
|
|
1301
|
+
if (table != null) tables.add(table);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function tableNameFromTableNode(node: unknown): string | undefined {
|
|
1305
|
+
if (!isOperationNode(node)) return undefined;
|
|
1306
|
+
if (node.kind === 'AliasNode') return tableNameFromTableNode(node.node);
|
|
1307
|
+
if (node.kind !== 'TableNode') return undefined;
|
|
1308
|
+
const table = node.table;
|
|
1309
|
+
if (!isOperationNode(table) || table.kind !== 'SchemableIdentifierNode') {
|
|
1310
|
+
return undefined;
|
|
1311
|
+
}
|
|
1312
|
+
const identifier = table.identifier;
|
|
1313
|
+
if (!isOperationNode(identifier) || identifier.kind !== 'IdentifierNode') {
|
|
1314
|
+
return undefined;
|
|
1315
|
+
}
|
|
1316
|
+
return typeof identifier.name === 'string' ? identifier.name : undefined;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function isOperationNode(value: unknown): value is OperationNodeLike {
|
|
1320
|
+
return Boolean(value && typeof value === 'object' && 'kind' in value);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function normalizeLiveQueryTables(
|
|
1324
|
+
tables: readonly string[],
|
|
1325
|
+
appTables?: ReadonlySet<string>
|
|
1326
|
+
): string[] {
|
|
1327
|
+
const normalized: string[] = [];
|
|
1328
|
+
for (const table of tables) {
|
|
1329
|
+
if (table.length === 0) {
|
|
1330
|
+
throw new Error(
|
|
1331
|
+
'Live query table dependencies must not contain empty names.'
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
if (appTables != null && !appTables.has(table)) {
|
|
1335
|
+
throw new Error(
|
|
1336
|
+
`Live query table ${table} is not part of the generated app schema.`
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
if (!normalized.includes(table)) normalized.push(table);
|
|
1340
|
+
}
|
|
1341
|
+
return normalized;
|
|
1342
|
+
}
|