@syncular/client 0.0.6-95 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +713 -11
- package/dist/auth-leases.d.ts +11 -0
- package/dist/auth-leases.d.ts.map +1 -0
- package/dist/auth-leases.js +140 -0
- package/dist/auth-leases.js.map +1 -0
- package/dist/blob-limits.d.ts +15 -0
- package/dist/blob-limits.d.ts.map +1 -0
- package/dist/blob-limits.js +66 -0
- package/dist/blob-limits.js.map +1 -0
- package/dist/bridge-client.d.ts +69 -0
- package/dist/bridge-client.d.ts.map +1 -0
- package/dist/bridge-client.js +282 -0
- package/dist/bridge-client.js.map +1 -0
- package/dist/client-config.d.ts +6 -0
- package/dist/client-config.d.ts.map +1 -0
- package/dist/client-config.js +18 -0
- package/dist/client-config.js.map +1 -0
- package/dist/client.d.ts +65 -397
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +150 -960
- package/dist/client.js.map +1 -1
- package/dist/command-history.d.ts +35 -0
- package/dist/command-history.d.ts.map +1 -0
- package/dist/command-history.js +378 -0
- package/dist/command-history.js.map +1 -0
- package/dist/console-diagnostics.d.ts +33 -0
- package/dist/console-diagnostics.d.ts.map +1 -0
- package/dist/console-diagnostics.js +496 -0
- package/dist/console-diagnostics.js.map +1 -0
- package/dist/crdt-yjs/index.d.ts +4 -0
- package/dist/crdt-yjs/index.d.ts.map +1 -0
- package/dist/crdt-yjs/index.js +4 -0
- package/dist/crdt-yjs/index.js.map +1 -0
- package/dist/crdt-yjs/webview-host-facade.d.ts +126 -0
- package/dist/crdt-yjs/webview-host-facade.d.ts.map +1 -0
- package/dist/crdt-yjs/webview-host-facade.js +284 -0
- package/dist/crdt-yjs/webview-host-facade.js.map +1 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +153 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +1 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.js +406 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.js.map +1 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +73 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +1 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js +169 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +1 -0
- package/dist/database.d.ts +83 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +916 -0
- package/dist/database.js.map +1 -0
- package/dist/diagnostics.d.ts +10 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +83 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +211 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated-bridge.d.ts +365 -0
- package/dist/generated-bridge.d.ts.map +1 -0
- package/dist/generated-bridge.js +250 -0
- package/dist/generated-bridge.js.map +1 -0
- package/dist/index.d.ts +13 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -25
- package/dist/index.js.map +1 -1
- package/dist/mutations.d.ts +4 -72
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +2 -568
- package/dist/mutations.js.map +1 -1
- package/dist/network.d.ts +3 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +17 -0
- package/dist/network.js.map +1 -0
- package/dist/react/index.d.ts +169 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +628 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react-native/index.d.ts +35 -0
- package/dist/react-native/index.d.ts.map +1 -0
- package/dist/react-native/index.js +49 -0
- package/dist/react-native/index.js.map +1 -0
- package/dist/runtime-contract.d.ts +13 -0
- package/dist/runtime-contract.d.ts.map +1 -0
- package/dist/runtime-contract.js +24 -0
- package/dist/runtime-contract.js.map +1 -0
- package/dist/rust-client.d.ts +106 -0
- package/dist/rust-client.d.ts.map +1 -0
- package/dist/rust-client.js +936 -0
- package/dist/rust-client.js.map +1 -0
- package/dist/sentry.d.ts +35 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +155 -0
- package/dist/sentry.js.map +1 -0
- package/dist/sql-safety.d.ts +3 -0
- package/dist/sql-safety.d.ts.map +1 -0
- package/dist/sql-safety.js +62 -0
- package/dist/sql-safety.js.map +1 -0
- package/dist/syncular-runtime-artifacts.json +61 -0
- package/dist/tauri/index.d.ts +35 -0
- package/dist/tauri/index.d.ts.map +1 -0
- package/dist/tauri/index.js +114 -0
- package/dist/tauri/index.js.map +1 -0
- package/dist/types.d.ts +1059 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/wasm/.syncular-wasm-profile +1 -0
- package/dist/wasm/syncular-runtime-artifact.json +21 -0
- package/dist/wasm/syncular.d.ts +207 -0
- package/dist/wasm/syncular.js +2341 -0
- package/dist/wasm/syncular_bg.wasm +0 -0
- package/dist/wasm/syncular_bg.wasm.d.ts +97 -0
- package/dist/wasm-bindings/runtime-contract.d.ts +22 -0
- package/dist/wasm-bindings/runtime-contract.d.ts.map +1 -0
- package/dist/wasm-bindings/runtime-contract.js +112 -0
- package/dist/wasm-bindings/runtime-contract.js.map +1 -0
- package/dist/wasm-core/.syncular-wasm-profile +1 -0
- package/dist/wasm-core/syncular-runtime-artifact.json +17 -0
- package/dist/wasm-core/syncular.d.ts +162 -0
- package/dist/wasm-core/syncular.js +1847 -0
- package/dist/wasm-core/syncular_bg.wasm +0 -0
- package/dist/wasm-core/syncular_bg.wasm.d.ts +77 -0
- package/dist/wasm-perf/.syncular-wasm-profile +1 -0
- package/dist/wasm-perf/syncular-runtime-artifact.json +21 -0
- package/dist/wasm-perf/syncular.d.ts +207 -0
- package/dist/wasm-perf/syncular.js +2341 -0
- package/dist/wasm-perf/syncular_bg.wasm +0 -0
- package/dist/wasm-perf/syncular_bg.wasm.d.ts +97 -0
- package/dist/wasm-runtime.d.ts +18 -0
- package/dist/wasm-runtime.d.ts.map +1 -0
- package/dist/wasm-runtime.js +28 -0
- package/dist/wasm-runtime.js.map +1 -0
- package/dist/worker-client.d.ts +123 -0
- package/dist/worker-client.d.ts.map +1 -0
- package/dist/worker-client.js +1686 -0
- package/dist/worker-client.js.map +1 -0
- package/dist/worker-entry.d.ts +2 -0
- package/dist/worker-entry.d.ts.map +1 -0
- package/dist/worker-entry.js +479 -0
- package/dist/worker-entry.js.map +1 -0
- package/dist/worker-protocol.d.ts +64 -0
- package/dist/worker-protocol.d.ts.map +1 -0
- package/dist/worker-protocol.js +16 -0
- package/dist/worker-protocol.js.map +1 -0
- package/dist/worker-realtime.d.ts +39 -0
- package/dist/worker-realtime.d.ts.map +1 -0
- package/dist/worker-realtime.js +677 -0
- package/dist/worker-realtime.js.map +1 -0
- package/package.json +124 -17
- package/src/auth-leases.ts +251 -0
- package/src/blob-limits.ts +98 -0
- package/src/bridge-client.ts +512 -0
- package/src/client-config.ts +29 -0
- package/src/client.ts +256 -1414
- package/src/command-history.ts +623 -0
- package/src/console-diagnostics.ts +617 -0
- package/src/crdt-yjs/index.ts +3 -0
- package/src/crdt-yjs/webview-host-facade.ts +477 -0
- package/src/crdt-yjs/yjs-document-field-adapter.ts +733 -0
- package/src/crdt-yjs/yjs-prosemirror-bridge.ts +272 -0
- package/src/database.ts +1342 -0
- package/src/diagnostics.ts +116 -0
- package/src/errors.ts +269 -0
- package/src/generated-bridge.ts +741 -0
- package/src/index.ts +21 -25
- package/src/mutations.ts +12 -821
- package/src/network.ts +32 -0
- package/src/react/index.ts +1036 -0
- package/src/react-native/index.ts +152 -0
- package/src/runtime-contract.ts +48 -0
- package/src/rust-client.ts +1488 -0
- package/src/sentry.ts +215 -0
- package/src/sql-safety.ts +61 -0
- package/src/tauri/index.ts +211 -0
- package/src/types.ts +1356 -0
- package/src/wasm-bindings/generated-wasm-bindings.d.ts +70 -0
- package/src/wasm-bindings/runtime-contract.ts +158 -0
- package/src/wasm-runtime.ts +76 -0
- package/src/worker-client.ts +2215 -0
- package/src/worker-entry.ts +584 -0
- package/src/worker-protocol.ts +109 -0
- package/src/worker-realtime.ts +843 -0
- package/dist/blobs/index.d.ts +0 -7
- package/dist/blobs/index.d.ts.map +0 -1
- package/dist/blobs/index.js +0 -7
- package/dist/blobs/index.js.map +0 -1
- package/dist/blobs/manager.d.ts +0 -345
- package/dist/blobs/manager.d.ts.map +0 -1
- package/dist/blobs/manager.js +0 -749
- package/dist/blobs/manager.js.map +0 -1
- package/dist/blobs/migrate.d.ts +0 -14
- package/dist/blobs/migrate.d.ts.map +0 -1
- package/dist/blobs/migrate.js +0 -59
- package/dist/blobs/migrate.js.map +0 -1
- package/dist/blobs/types.d.ts +0 -62
- package/dist/blobs/types.d.ts.map +0 -1
- package/dist/blobs/types.js +0 -5
- package/dist/blobs/types.js.map +0 -1
- package/dist/conflicts.d.ts +0 -31
- package/dist/conflicts.d.ts.map +0 -1
- package/dist/conflicts.js +0 -112
- package/dist/conflicts.js.map +0 -1
- package/dist/create-client.d.ts +0 -118
- package/dist/create-client.d.ts.map +0 -1
- package/dist/create-client.js +0 -178
- package/dist/create-client.js.map +0 -1
- package/dist/engine/SyncEngine.d.ts +0 -272
- package/dist/engine/SyncEngine.d.ts.map +0 -1
- package/dist/engine/SyncEngine.js +0 -1860
- package/dist/engine/SyncEngine.js.map +0 -1
- package/dist/engine/index.d.ts +0 -6
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -6
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/types.d.ts +0 -345
- package/dist/engine/types.d.ts.map +0 -1
- package/dist/engine/types.js +0 -7
- package/dist/engine/types.js.map +0 -1
- package/dist/handlers/collection.d.ts +0 -6
- package/dist/handlers/collection.d.ts.map +0 -1
- package/dist/handlers/collection.js +0 -21
- package/dist/handlers/collection.js.map +0 -1
- package/dist/handlers/create-handler.d.ts +0 -120
- package/dist/handlers/create-handler.d.ts.map +0 -1
- package/dist/handlers/create-handler.js +0 -145
- package/dist/handlers/create-handler.js.map +0 -1
- package/dist/handlers/types.d.ts +0 -83
- package/dist/handlers/types.d.ts.map +0 -1
- package/dist/handlers/types.js +0 -5
- package/dist/handlers/types.js.map +0 -1
- package/dist/migrate.d.ts +0 -19
- package/dist/migrate.d.ts.map +0 -1
- package/dist/migrate.js +0 -238
- package/dist/migrate.js.map +0 -1
- package/dist/outbox.d.ts +0 -112
- package/dist/outbox.d.ts.map +0 -1
- package/dist/outbox.js +0 -294
- package/dist/outbox.js.map +0 -1
- package/dist/plugins/incrementing-version.d.ts +0 -34
- package/dist/plugins/incrementing-version.d.ts.map +0 -1
- package/dist/plugins/incrementing-version.js +0 -83
- package/dist/plugins/incrementing-version.js.map +0 -1
- package/dist/plugins/index.d.ts +0 -3
- package/dist/plugins/index.d.ts.map +0 -1
- package/dist/plugins/index.js +0 -3
- package/dist/plugins/index.js.map +0 -1
- package/dist/plugins/types.d.ts +0 -49
- package/dist/plugins/types.d.ts.map +0 -1
- package/dist/plugins/types.js +0 -15
- package/dist/plugins/types.js.map +0 -1
- package/dist/proxy/connection.d.ts +0 -33
- package/dist/proxy/connection.d.ts.map +0 -1
- package/dist/proxy/connection.js +0 -153
- package/dist/proxy/connection.js.map +0 -1
- package/dist/proxy/dialect.d.ts +0 -46
- package/dist/proxy/dialect.d.ts.map +0 -1
- package/dist/proxy/dialect.js +0 -58
- package/dist/proxy/dialect.js.map +0 -1
- package/dist/proxy/driver.d.ts +0 -42
- package/dist/proxy/driver.d.ts.map +0 -1
- package/dist/proxy/driver.js +0 -78
- package/dist/proxy/driver.js.map +0 -1
- package/dist/proxy/index.d.ts +0 -10
- package/dist/proxy/index.d.ts.map +0 -1
- package/dist/proxy/index.js +0 -10
- package/dist/proxy/index.js.map +0 -1
- package/dist/proxy/mutations.d.ts +0 -9
- package/dist/proxy/mutations.d.ts.map +0 -1
- package/dist/proxy/mutations.js +0 -11
- package/dist/proxy/mutations.js.map +0 -1
- package/dist/pull-engine.d.ts +0 -48
- package/dist/pull-engine.d.ts.map +0 -1
- package/dist/pull-engine.js +0 -648
- package/dist/pull-engine.js.map +0 -1
- package/dist/push-engine.d.ts +0 -18
- package/dist/push-engine.d.ts.map +0 -1
- package/dist/push-engine.js +0 -167
- package/dist/push-engine.js.map +0 -1
- package/dist/query/FingerprintCollector.d.ts +0 -18
- package/dist/query/FingerprintCollector.d.ts.map +0 -1
- package/dist/query/FingerprintCollector.js +0 -28
- package/dist/query/FingerprintCollector.js.map +0 -1
- package/dist/query/QueryContext.d.ts +0 -33
- package/dist/query/QueryContext.d.ts.map +0 -1
- package/dist/query/QueryContext.js +0 -16
- package/dist/query/QueryContext.js.map +0 -1
- package/dist/query/fingerprint.d.ts +0 -61
- package/dist/query/fingerprint.d.ts.map +0 -1
- package/dist/query/fingerprint.js +0 -91
- package/dist/query/fingerprint.js.map +0 -1
- package/dist/query/index.d.ts +0 -7
- package/dist/query/index.d.ts.map +0 -1
- package/dist/query/index.js +0 -7
- package/dist/query/index.js.map +0 -1
- package/dist/query/tracked-select.d.ts +0 -18
- package/dist/query/tracked-select.d.ts.map +0 -1
- package/dist/query/tracked-select.js +0 -90
- package/dist/query/tracked-select.js.map +0 -1
- package/dist/schema.d.ts +0 -83
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js +0 -7
- package/dist/schema.js.map +0 -1
- package/dist/subscription-state.d.ts +0 -46
- package/dist/subscription-state.d.ts.map +0 -1
- package/dist/subscription-state.js +0 -185
- package/dist/subscription-state.js.map +0 -1
- package/dist/sync-loop.d.ts +0 -34
- package/dist/sync-loop.d.ts.map +0 -1
- package/dist/sync-loop.js +0 -253
- package/dist/sync-loop.js.map +0 -1
- package/dist/sync.d.ts +0 -32
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -55
- package/dist/sync.js.map +0 -1
- package/dist/utils/id.d.ts +0 -17
- package/dist/utils/id.d.ts.map +0 -1
- package/dist/utils/id.js +0 -46
- package/dist/utils/id.js.map +0 -1
- package/src/blobs/index.ts +0 -7
- package/src/blobs/manager.ts +0 -1027
- package/src/blobs/migrate.ts +0 -67
- package/src/blobs/types.ts +0 -84
- package/src/client.test.ts +0 -417
- package/src/conflicts.ts +0 -171
- package/src/create-client.test.ts +0 -83
- package/src/create-client.ts +0 -330
- package/src/engine/SyncEngine.test.ts +0 -254
- package/src/engine/SyncEngine.ts +0 -2375
- package/src/engine/index.ts +0 -6
- package/src/engine/types.ts +0 -419
- package/src/handlers/collection.ts +0 -36
- package/src/handlers/create-handler.ts +0 -330
- package/src/handlers/types.ts +0 -108
- package/src/migrate.ts +0 -318
- package/src/outbox.ts +0 -383
- package/src/plugins/incrementing-version.ts +0 -133
- package/src/plugins/index.ts +0 -2
- package/src/plugins/types.ts +0 -63
- package/src/proxy/connection.ts +0 -191
- package/src/proxy/dialect.ts +0 -76
- package/src/proxy/driver.ts +0 -126
- package/src/proxy/index.ts +0 -10
- package/src/proxy/mutations.ts +0 -18
- package/src/pull-engine.test.ts +0 -151
- package/src/pull-engine.ts +0 -855
- package/src/push-engine.ts +0 -216
- package/src/query/FingerprintCollector.ts +0 -29
- package/src/query/QueryContext.ts +0 -54
- package/src/query/fingerprint.ts +0 -109
- package/src/query/index.ts +0 -10
- package/src/query/tracked-select.ts +0 -139
- package/src/schema.ts +0 -94
- package/src/subscription-state.ts +0 -259
- package/src/sync-loop.ts +0 -379
- package/src/sync.ts +0 -170
- package/src/utils/id.ts +0 -55
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CreateSyncularDatabaseOptions,
|
|
3
|
+
createSyncularDatabase,
|
|
4
|
+
type MutationReceipt,
|
|
5
|
+
type MutationsApi,
|
|
6
|
+
type SyncularBlobUploadQueueStats,
|
|
7
|
+
type SyncularChangedRow,
|
|
8
|
+
type SyncularClientEventMap,
|
|
9
|
+
type SyncularClientEventType,
|
|
10
|
+
type SyncularClientLike,
|
|
11
|
+
type SyncularClientStatus,
|
|
12
|
+
type SyncularConflictStats,
|
|
13
|
+
type SyncularConnectionState,
|
|
14
|
+
type SyncularLiveQueryOptions,
|
|
15
|
+
type SyncularLiveQuerySubscription,
|
|
16
|
+
type SyncularOutboxStats,
|
|
17
|
+
type SyncularPresenceEntry,
|
|
18
|
+
type SyncularRowsChangedEvent,
|
|
19
|
+
type TableMutations,
|
|
20
|
+
} from '@syncular/client';
|
|
21
|
+
import type { BlobRef } from '@syncular/core';
|
|
22
|
+
import type { CompiledQuery, Kysely } from 'kysely';
|
|
23
|
+
import {
|
|
24
|
+
createContext,
|
|
25
|
+
createElement,
|
|
26
|
+
type ReactNode,
|
|
27
|
+
startTransition,
|
|
28
|
+
useCallback,
|
|
29
|
+
useContext,
|
|
30
|
+
useEffect,
|
|
31
|
+
useMemo,
|
|
32
|
+
useRef,
|
|
33
|
+
useState,
|
|
34
|
+
useSyncExternalStore,
|
|
35
|
+
} from 'react';
|
|
36
|
+
|
|
37
|
+
export type SyncularReactStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
38
|
+
|
|
39
|
+
export interface SyncProviderProps<DB> {
|
|
40
|
+
children?: ReactNode;
|
|
41
|
+
client?: SyncularClientLike<DB>;
|
|
42
|
+
options?: CreateSyncularDatabaseOptions;
|
|
43
|
+
optionsKey?: unknown;
|
|
44
|
+
closeOnUnmount?: boolean;
|
|
45
|
+
onClient?: (client: SyncularClientLike<DB>) => void;
|
|
46
|
+
onError?: (error: unknown) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SyncContextValue {
|
|
50
|
+
status: SyncularReactStatus;
|
|
51
|
+
error: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SyncQueryContext<DB> {
|
|
55
|
+
db: Kysely<DB>;
|
|
56
|
+
selectFrom: Kysely<DB>['selectFrom'];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface UseSyncQueryOptions {
|
|
60
|
+
enabled?: boolean;
|
|
61
|
+
deps?: readonly unknown[];
|
|
62
|
+
keyField?: string;
|
|
63
|
+
watchTables?: readonly string[];
|
|
64
|
+
tables?: readonly string[];
|
|
65
|
+
pollIntervalMs?: number;
|
|
66
|
+
staleAfterMs?: number;
|
|
67
|
+
structuralSharing?: boolean;
|
|
68
|
+
refreshOnDataChange?: boolean;
|
|
69
|
+
loadingOnRefresh?: boolean;
|
|
70
|
+
transitionUpdates?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface UseSyncQueryResult<T> {
|
|
74
|
+
data: T | undefined;
|
|
75
|
+
isLoading: boolean;
|
|
76
|
+
error: Error | null;
|
|
77
|
+
isStale: boolean;
|
|
78
|
+
lastSyncAt: number | null;
|
|
79
|
+
refetch: () => Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface UseQueryOptions {
|
|
83
|
+
enabled?: boolean;
|
|
84
|
+
deps?: readonly unknown[];
|
|
85
|
+
keyField?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface UseQueryResult<T> {
|
|
89
|
+
data: T | undefined;
|
|
90
|
+
isLoading: boolean;
|
|
91
|
+
error: Error | null;
|
|
92
|
+
refetch: () => Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type MutationInput =
|
|
96
|
+
| {
|
|
97
|
+
rowId: string;
|
|
98
|
+
op: 'delete';
|
|
99
|
+
payload?: null;
|
|
100
|
+
baseVersion?: number | null;
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
rowId: string;
|
|
104
|
+
op: 'upsert';
|
|
105
|
+
payload?: Record<string, unknown> | null;
|
|
106
|
+
baseVersion?: number | null;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type MutationResult = MutationReceipt;
|
|
110
|
+
|
|
111
|
+
export interface FluentMutation {
|
|
112
|
+
update: (
|
|
113
|
+
rowId: string,
|
|
114
|
+
payload: Record<string, unknown>,
|
|
115
|
+
options?: { baseVersion?: number | null }
|
|
116
|
+
) => Promise<MutationResult>;
|
|
117
|
+
upsert: (
|
|
118
|
+
rowId: string,
|
|
119
|
+
payload: Record<string, unknown>,
|
|
120
|
+
options?: { baseVersion?: number | null }
|
|
121
|
+
) => Promise<MutationResult>;
|
|
122
|
+
delete: (
|
|
123
|
+
rowId: string,
|
|
124
|
+
options?: { baseVersion?: number | null }
|
|
125
|
+
) => Promise<MutationResult>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface UseMutationOptions<TTable extends string> {
|
|
129
|
+
table: TTable;
|
|
130
|
+
syncImmediately?: boolean;
|
|
131
|
+
onSuccess?: (result: MutationResult) => void;
|
|
132
|
+
onError?: (error: Error) => void;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface UseMutationResult {
|
|
136
|
+
mutate: FluentMutation;
|
|
137
|
+
mutateMany: (inputs: readonly MutationInput[]) => Promise<MutationResult>;
|
|
138
|
+
isPending: boolean;
|
|
139
|
+
error: Error | null;
|
|
140
|
+
reset: () => void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type SyncMode = 'background' | 'await' | false;
|
|
144
|
+
|
|
145
|
+
export interface UseMutationsOptions {
|
|
146
|
+
sync?: SyncMode;
|
|
147
|
+
onSuccess?: (result: MutationReceipt & { opCount: number }) => void;
|
|
148
|
+
onError?: (error: Error) => void;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type MutationsHook<DB> = MutationsApi<DB, { sync?: SyncMode }> & {
|
|
152
|
+
readonly $isPending: boolean;
|
|
153
|
+
readonly $error: Error | null;
|
|
154
|
+
$reset: () => void;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export interface UseRowsChangedOptions {
|
|
158
|
+
tables?: readonly string[];
|
|
159
|
+
enabled?: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface UsePresenceResult<
|
|
163
|
+
TMetadata extends Record<string, unknown> = Record<string, unknown>,
|
|
164
|
+
> {
|
|
165
|
+
presence: SyncularPresenceEntry<TMetadata>[];
|
|
166
|
+
isLoading: boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface UsePresenceWithJoinOptions<
|
|
170
|
+
TMetadata extends Record<string, unknown> = Record<string, unknown>,
|
|
171
|
+
> {
|
|
172
|
+
metadata?: TMetadata;
|
|
173
|
+
autoJoin?: boolean;
|
|
174
|
+
enabled?: boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface UsePresenceWithJoinResult<
|
|
178
|
+
TMetadata extends Record<string, unknown> = Record<string, unknown>,
|
|
179
|
+
> extends UsePresenceResult<TMetadata> {
|
|
180
|
+
updateMetadata: (metadata: TMetadata) => void;
|
|
181
|
+
join: (metadata?: TMetadata) => void;
|
|
182
|
+
leave: () => void;
|
|
183
|
+
isJoined: boolean;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface UseSyncConnectionResult {
|
|
187
|
+
state: SyncularConnectionState;
|
|
188
|
+
isConnected: boolean;
|
|
189
|
+
isReconnecting: boolean;
|
|
190
|
+
reconnect: () => Promise<void>;
|
|
191
|
+
disconnect: () => Promise<void>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface UseBlobUploadQueueResult {
|
|
195
|
+
stats: SyncularBlobUploadQueueStats | null;
|
|
196
|
+
refresh(): Promise<void>;
|
|
197
|
+
process(): Promise<{ uploaded: number; failed: number }>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
type ReactContextInternal<DB> = {
|
|
201
|
+
client: SyncularClientLike<DB> | null;
|
|
202
|
+
status: SyncularReactStatus;
|
|
203
|
+
error: unknown;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
type ExecutableQuery<TResult> = {
|
|
207
|
+
execute(): Promise<TResult>;
|
|
208
|
+
compile?: () => CompiledQuery;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
type SyncQueryFn<DB, TResult> = (
|
|
212
|
+
ctx: SyncQueryContext<DB>
|
|
213
|
+
) => ExecutableQuery<TResult> | Promise<TResult> | TResult;
|
|
214
|
+
|
|
215
|
+
type KnownTableKey<DB> = string extends keyof DB
|
|
216
|
+
? string
|
|
217
|
+
: Extract<keyof DB, string>;
|
|
218
|
+
|
|
219
|
+
type AnyTableMutations = TableMutations<
|
|
220
|
+
Record<string, Record<string, unknown>>,
|
|
221
|
+
string
|
|
222
|
+
>;
|
|
223
|
+
|
|
224
|
+
export function createSyncularReact<DB>() {
|
|
225
|
+
const Context = createContext<ReactContextInternal<DB> | null>(null);
|
|
226
|
+
|
|
227
|
+
function SyncProvider(
|
|
228
|
+
props: SyncProviderProps<DB>
|
|
229
|
+
): ReturnType<typeof createElement> {
|
|
230
|
+
const {
|
|
231
|
+
children,
|
|
232
|
+
client: providedClient,
|
|
233
|
+
closeOnUnmount,
|
|
234
|
+
onClient,
|
|
235
|
+
onError,
|
|
236
|
+
options,
|
|
237
|
+
optionsKey,
|
|
238
|
+
} = props;
|
|
239
|
+
const optionsRef = useLatest(options);
|
|
240
|
+
const onClientRef = useLatest(onClient);
|
|
241
|
+
const onErrorRef = useLatest(onError);
|
|
242
|
+
const lifecycleKey = optionsKey;
|
|
243
|
+
const [value, setValue] = useState<ReactContextInternal<DB>>({
|
|
244
|
+
client: providedClient ?? null,
|
|
245
|
+
status: providedClient ? 'ready' : options ? 'loading' : 'idle',
|
|
246
|
+
error: null,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
void lifecycleKey;
|
|
251
|
+
let cancelled = false;
|
|
252
|
+
let ownedClient: SyncularClientLike<DB> | null = null;
|
|
253
|
+
|
|
254
|
+
if (providedClient) {
|
|
255
|
+
setValue({ client: providedClient, status: 'ready', error: null });
|
|
256
|
+
onClientRef.current?.(providedClient);
|
|
257
|
+
return () => {
|
|
258
|
+
if (closeOnUnmount) {
|
|
259
|
+
void providedClient.close().catch(() => undefined);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const currentOptions = optionsRef.current;
|
|
265
|
+
if (!currentOptions) {
|
|
266
|
+
setValue({ client: null, status: 'idle', error: null });
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
setValue((previous) => ({
|
|
271
|
+
client: previous.client,
|
|
272
|
+
status: 'loading',
|
|
273
|
+
error: null,
|
|
274
|
+
}));
|
|
275
|
+
void createSyncularDatabase<DB>(currentOptions)
|
|
276
|
+
.then((client) => {
|
|
277
|
+
if (cancelled) {
|
|
278
|
+
void client.close().catch(() => undefined);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
ownedClient = client;
|
|
282
|
+
setValue({ client, status: 'ready', error: null });
|
|
283
|
+
onClientRef.current?.(client);
|
|
284
|
+
})
|
|
285
|
+
.catch((error) => {
|
|
286
|
+
if (cancelled) return;
|
|
287
|
+
setValue({ client: null, status: 'error', error });
|
|
288
|
+
onErrorRef.current?.(error);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return () => {
|
|
292
|
+
cancelled = true;
|
|
293
|
+
if (ownedClient) void ownedClient.close().catch(() => undefined);
|
|
294
|
+
};
|
|
295
|
+
}, [
|
|
296
|
+
closeOnUnmount,
|
|
297
|
+
lifecycleKey,
|
|
298
|
+
onClientRef,
|
|
299
|
+
onErrorRef,
|
|
300
|
+
optionsRef,
|
|
301
|
+
providedClient,
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
return createElement(Context.Provider, { value, children });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function useSyncContext(): SyncContextValue {
|
|
308
|
+
const value = useContext(Context);
|
|
309
|
+
if (!value) throw new Error('SyncProvider is required for Syncular hooks.');
|
|
310
|
+
return { status: value.status, error: value.error };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function useClient(): SyncularClientLike<DB> {
|
|
314
|
+
const value = useContext(Context);
|
|
315
|
+
if (!value) throw new Error('SyncProvider is required for Syncular hooks.');
|
|
316
|
+
if (!value.client) {
|
|
317
|
+
if (value.status === 'error') throw value.error;
|
|
318
|
+
throw new Error('Syncular client is not ready yet.');
|
|
319
|
+
}
|
|
320
|
+
return value.client;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function useSyncStatus(): SyncularClientStatus {
|
|
324
|
+
const client = useClient();
|
|
325
|
+
const readStatus = useCallback(() => client.getStatus(), [client]);
|
|
326
|
+
const getSnapshot = useStableSnapshot(readStatus);
|
|
327
|
+
return useSyncExternalStore(
|
|
328
|
+
useCallback(
|
|
329
|
+
(notify) => client.on('lifecycleChanged', () => notify()),
|
|
330
|
+
[client]
|
|
331
|
+
),
|
|
332
|
+
getSnapshot,
|
|
333
|
+
getSnapshot
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function useSyncConnection(): UseSyncConnectionResult {
|
|
338
|
+
const client = useClient();
|
|
339
|
+
const status = useSyncStatus();
|
|
340
|
+
const state = status.connection;
|
|
341
|
+
return {
|
|
342
|
+
state,
|
|
343
|
+
isConnected: status.isConnected,
|
|
344
|
+
isReconnecting: state.realtime === 'connecting',
|
|
345
|
+
reconnect: client.start,
|
|
346
|
+
disconnect: client.stop,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function useSyncEvent<T extends SyncularClientEventType>(
|
|
351
|
+
event: T,
|
|
352
|
+
listener: (payload: SyncularClientEventMap[T]) => void,
|
|
353
|
+
enabled = true
|
|
354
|
+
): void {
|
|
355
|
+
const client = useClient();
|
|
356
|
+
const listenerRef = useLatest(listener);
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
if (!enabled) return undefined;
|
|
359
|
+
return client.on(event, (payload) => listenerRef.current(payload));
|
|
360
|
+
}, [client, enabled, event, listenerRef]);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function useRowsChanged(
|
|
364
|
+
listener: (event: SyncularRowsChangedEvent) => void,
|
|
365
|
+
options: UseRowsChangedOptions = {}
|
|
366
|
+
): void {
|
|
367
|
+
const listenerRef = useLatest(listener);
|
|
368
|
+
useSyncEvent(
|
|
369
|
+
'rowsChanged',
|
|
370
|
+
(event) => {
|
|
371
|
+
const tables = options.tables == null ? null : new Set(options.tables);
|
|
372
|
+
if (tables && !event.changedTables.some((table) => tables.has(table))) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
listenerRef.current(event);
|
|
376
|
+
},
|
|
377
|
+
options.enabled !== false
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function useSyncQuery<TResult>(
|
|
382
|
+
query: SyncQueryFn<DB, TResult>,
|
|
383
|
+
options: UseSyncQueryOptions = {}
|
|
384
|
+
): UseSyncQueryResult<TResult> {
|
|
385
|
+
const client = useClient();
|
|
386
|
+
const queryRef = useLatest(query);
|
|
387
|
+
const optionsRef = useLatest(options);
|
|
388
|
+
const enabled = options.enabled !== false;
|
|
389
|
+
const deps = options.deps ?? [];
|
|
390
|
+
const tables = options.watchTables ?? options.tables;
|
|
391
|
+
const tablesRef = useLatest(tables);
|
|
392
|
+
const tablesKey = stableStringListKey(tables);
|
|
393
|
+
const watchedTables = useMemo(() => {
|
|
394
|
+
void tablesKey;
|
|
395
|
+
return tablesRef.current == null ? null : new Set(tablesRef.current);
|
|
396
|
+
}, [tablesKey, tablesRef]);
|
|
397
|
+
const [data, setData] = useState<TResult | undefined>(undefined);
|
|
398
|
+
const [isLoading, setIsLoading] = useState(enabled);
|
|
399
|
+
const [error, setError] = useState<Error | null>(null);
|
|
400
|
+
const [lastSyncAt, setLastSyncAt] = useState<number | null>(null);
|
|
401
|
+
|
|
402
|
+
const publishResult = useCallback(
|
|
403
|
+
(result: TResult) => {
|
|
404
|
+
const publish = () => {
|
|
405
|
+
setData((previous) =>
|
|
406
|
+
shareQueryResult(
|
|
407
|
+
previous,
|
|
408
|
+
result,
|
|
409
|
+
optionsRef.current.keyField ?? 'id',
|
|
410
|
+
optionsRef.current.structuralSharing !== false
|
|
411
|
+
)
|
|
412
|
+
);
|
|
413
|
+
setLastSyncAt(Date.now());
|
|
414
|
+
setIsLoading(false);
|
|
415
|
+
};
|
|
416
|
+
if (optionsRef.current.transitionUpdates === false) {
|
|
417
|
+
publish();
|
|
418
|
+
} else {
|
|
419
|
+
startTransition(publish);
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
[optionsRef]
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const execute = useCallback(async () => {
|
|
426
|
+
if (!enabled) return;
|
|
427
|
+
if (optionsRef.current.loadingOnRefresh !== false) setIsLoading(true);
|
|
428
|
+
setError(null);
|
|
429
|
+
try {
|
|
430
|
+
const result = await executeSyncQuery(client.db, queryRef.current);
|
|
431
|
+
publishResult(result);
|
|
432
|
+
} catch (caught) {
|
|
433
|
+
setError(toError(caught));
|
|
434
|
+
setIsLoading(false);
|
|
435
|
+
}
|
|
436
|
+
}, [client, enabled, optionsRef, publishResult, queryRef]);
|
|
437
|
+
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
if (!enabled) {
|
|
440
|
+
setIsLoading(false);
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
void execute();
|
|
444
|
+
if (!options.pollIntervalMs || options.pollIntervalMs <= 0) {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
const timer = setInterval(() => {
|
|
448
|
+
void execute();
|
|
449
|
+
}, options.pollIntervalMs);
|
|
450
|
+
return () => clearInterval(timer);
|
|
451
|
+
}, [enabled, execute, options.pollIntervalMs, ...deps]);
|
|
452
|
+
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
if (!enabled || options.refreshOnDataChange === false) return undefined;
|
|
455
|
+
let disposed = false;
|
|
456
|
+
let fallbackUnsubscribe: (() => void) | undefined;
|
|
457
|
+
let liveSubscription: SyncularLiveQuerySubscription | undefined;
|
|
458
|
+
|
|
459
|
+
const startFallback = () => {
|
|
460
|
+
if (disposed || fallbackUnsubscribe) return;
|
|
461
|
+
fallbackUnsubscribe = client.on('rowsChanged', (event) => {
|
|
462
|
+
if (
|
|
463
|
+
watchedTables &&
|
|
464
|
+
!event.changedTables.some((table) => watchedTables.has(table))
|
|
465
|
+
) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
void execute();
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const startLiveQuery = async () => {
|
|
473
|
+
if (!hasLiveQueries(client)) {
|
|
474
|
+
startFallback();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const queryResult = await Promise.resolve(
|
|
479
|
+
queryRef.current({
|
|
480
|
+
db: client.db,
|
|
481
|
+
selectFrom: client.db.selectFrom.bind(
|
|
482
|
+
client.db
|
|
483
|
+
) as Kysely<DB>['selectFrom'],
|
|
484
|
+
})
|
|
485
|
+
);
|
|
486
|
+
if (!isCompilableExecutableQuery<TResult>(queryResult)) {
|
|
487
|
+
startFallback();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const liveTables = tablesRef.current;
|
|
491
|
+
liveSubscription = await client.live(queryResult, {
|
|
492
|
+
...(liveTables == null ? {} : { tables: liveTables }),
|
|
493
|
+
onChange: (rows) => {
|
|
494
|
+
if (!disposed) publishResult(rows as TResult);
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
} catch (caught) {
|
|
498
|
+
if (!disposed) {
|
|
499
|
+
setError(toError(caught));
|
|
500
|
+
setIsLoading(false);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
void startLiveQuery();
|
|
506
|
+
|
|
507
|
+
return () => {
|
|
508
|
+
disposed = true;
|
|
509
|
+
fallbackUnsubscribe?.();
|
|
510
|
+
liveSubscription?.unsubscribe();
|
|
511
|
+
};
|
|
512
|
+
}, [
|
|
513
|
+
client,
|
|
514
|
+
enabled,
|
|
515
|
+
execute,
|
|
516
|
+
options.refreshOnDataChange,
|
|
517
|
+
publishResult,
|
|
518
|
+
queryRef,
|
|
519
|
+
tablesRef,
|
|
520
|
+
watchedTables,
|
|
521
|
+
]);
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
data,
|
|
525
|
+
isLoading,
|
|
526
|
+
error,
|
|
527
|
+
isStale:
|
|
528
|
+
options.staleAfterMs != null &&
|
|
529
|
+
lastSyncAt != null &&
|
|
530
|
+
Date.now() - lastSyncAt > options.staleAfterMs,
|
|
531
|
+
lastSyncAt,
|
|
532
|
+
refetch: execute,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function useQuery<TResult>(
|
|
537
|
+
query: SyncQueryFn<DB, TResult>,
|
|
538
|
+
options: UseQueryOptions = {}
|
|
539
|
+
): UseQueryResult<TResult> {
|
|
540
|
+
const result = useSyncQuery(query, {
|
|
541
|
+
...options,
|
|
542
|
+
refreshOnDataChange: false,
|
|
543
|
+
loadingOnRefresh: true,
|
|
544
|
+
});
|
|
545
|
+
return {
|
|
546
|
+
data: result.data,
|
|
547
|
+
isLoading: result.isLoading,
|
|
548
|
+
error: result.error,
|
|
549
|
+
refetch: result.refetch,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function useMutation<TTable extends KnownTableKey<DB> & string>(
|
|
554
|
+
options: UseMutationOptions<TTable>
|
|
555
|
+
): UseMutationResult {
|
|
556
|
+
return useMutationFor('mutations', options);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function useLeasedMutation<TTable extends KnownTableKey<DB> & string>(
|
|
560
|
+
options: UseMutationOptions<TTable>
|
|
561
|
+
): UseMutationResult {
|
|
562
|
+
return useMutationFor('leasedMutations', options);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function useMutationFor<TTable extends KnownTableKey<DB> & string>(
|
|
566
|
+
apiName: 'mutations' | 'leasedMutations',
|
|
567
|
+
options: UseMutationOptions<TTable>
|
|
568
|
+
): UseMutationResult {
|
|
569
|
+
const client = useClient();
|
|
570
|
+
const optionsRef = useLatest(options);
|
|
571
|
+
const [isPending, setIsPending] = useState(false);
|
|
572
|
+
const [error, setError] = useState<Error | null>(null);
|
|
573
|
+
const reset = useCallback(() => setError(null), []);
|
|
574
|
+
|
|
575
|
+
const run = useCallback(
|
|
576
|
+
async (operation: () => Promise<MutationReceipt>) => {
|
|
577
|
+
setIsPending(true);
|
|
578
|
+
setError(null);
|
|
579
|
+
try {
|
|
580
|
+
const result = await operation();
|
|
581
|
+
if (optionsRef.current.syncImmediately !== false) {
|
|
582
|
+
await client.sync();
|
|
583
|
+
}
|
|
584
|
+
optionsRef.current.onSuccess?.(result);
|
|
585
|
+
return result;
|
|
586
|
+
} catch (caught) {
|
|
587
|
+
const nextError = toError(caught);
|
|
588
|
+
setError(nextError);
|
|
589
|
+
optionsRef.current.onError?.(nextError);
|
|
590
|
+
throw nextError;
|
|
591
|
+
} finally {
|
|
592
|
+
setIsPending(false);
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
[client, optionsRef]
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
const mutate = useMemo<FluentMutation>(() => {
|
|
599
|
+
const table = client[apiName].$table(options.table);
|
|
600
|
+
return {
|
|
601
|
+
update: (rowId, payload, mutationOptions) =>
|
|
602
|
+
run(() => table.update(rowId, payload, mutationOptions)),
|
|
603
|
+
upsert: (rowId, payload, mutationOptions) =>
|
|
604
|
+
run(() => table.upsert(rowId, payload, mutationOptions)),
|
|
605
|
+
delete: (rowId, mutationOptions) =>
|
|
606
|
+
run(() => table.delete(rowId, mutationOptions)),
|
|
607
|
+
};
|
|
608
|
+
}, [apiName, client, options.table, run]);
|
|
609
|
+
|
|
610
|
+
const mutateMany = useCallback(
|
|
611
|
+
(inputs: readonly MutationInput[]) =>
|
|
612
|
+
run(async () => {
|
|
613
|
+
const { commit } = await client[apiName].$commit(async (tx) => {
|
|
614
|
+
const table = tx[optionsRef.current.table]!;
|
|
615
|
+
for (const input of inputs) {
|
|
616
|
+
if (input.op === 'delete') {
|
|
617
|
+
await table.delete(input.rowId, {
|
|
618
|
+
baseVersion: input.baseVersion,
|
|
619
|
+
});
|
|
620
|
+
} else {
|
|
621
|
+
await table.upsert(input.rowId, input.payload ?? {}, {
|
|
622
|
+
baseVersion: input.baseVersion,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
return commit;
|
|
628
|
+
}),
|
|
629
|
+
[apiName, client, optionsRef, run]
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
return { mutate, mutateMany, isPending, error, reset };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function useMutations(options: UseMutationsOptions = {}): MutationsHook<DB> {
|
|
636
|
+
return useMutationsFor('mutations', options);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function useLeasedMutations(
|
|
640
|
+
options: UseMutationsOptions = {}
|
|
641
|
+
): MutationsHook<DB> {
|
|
642
|
+
return useMutationsFor('leasedMutations', options);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function useMutationsFor(
|
|
646
|
+
apiName: 'mutations' | 'leasedMutations',
|
|
647
|
+
options: UseMutationsOptions = {}
|
|
648
|
+
): MutationsHook<DB> {
|
|
649
|
+
const client = useClient();
|
|
650
|
+
const optionsRef = useLatest(options);
|
|
651
|
+
const [isPending, setIsPending] = useState(false);
|
|
652
|
+
const [error, setError] = useState<Error | null>(null);
|
|
653
|
+
const reset = useCallback(() => setError(null), []);
|
|
654
|
+
|
|
655
|
+
const run = useCallback(
|
|
656
|
+
async <TResult extends MutationReceipt>(
|
|
657
|
+
operation: () => Promise<TResult>,
|
|
658
|
+
opCount: number,
|
|
659
|
+
syncOverride?: SyncMode
|
|
660
|
+
) => {
|
|
661
|
+
setIsPending(true);
|
|
662
|
+
setError(null);
|
|
663
|
+
try {
|
|
664
|
+
const result = await operation();
|
|
665
|
+
const syncMode =
|
|
666
|
+
syncOverride ?? optionsRef.current.sync ?? 'background';
|
|
667
|
+
if (syncMode === 'await') {
|
|
668
|
+
await client.sync();
|
|
669
|
+
} else if (syncMode === 'background') {
|
|
670
|
+
void client.sync().catch(() => undefined);
|
|
671
|
+
}
|
|
672
|
+
optionsRef.current.onSuccess?.({ ...result, opCount });
|
|
673
|
+
return result;
|
|
674
|
+
} catch (caught) {
|
|
675
|
+
const nextError = toError(caught);
|
|
676
|
+
setError(nextError);
|
|
677
|
+
optionsRef.current.onError?.(nextError);
|
|
678
|
+
throw nextError;
|
|
679
|
+
} finally {
|
|
680
|
+
setIsPending(false);
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
[client, optionsRef]
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
return useMemo(
|
|
687
|
+
() =>
|
|
688
|
+
new Proxy(
|
|
689
|
+
{},
|
|
690
|
+
{
|
|
691
|
+
get(_target, prop) {
|
|
692
|
+
if (prop === 'then') return undefined;
|
|
693
|
+
if (prop === '$isPending') return isPending;
|
|
694
|
+
if (prop === '$error') return error;
|
|
695
|
+
if (prop === '$reset') return reset;
|
|
696
|
+
if (prop === '$table') {
|
|
697
|
+
return (table: string) =>
|
|
698
|
+
wrapMutationsTable(client, apiName, table, run);
|
|
699
|
+
}
|
|
700
|
+
if (prop === '$commit') {
|
|
701
|
+
return async <TResult>(
|
|
702
|
+
fn: Parameters<MutationsApi<DB>['$commit']>[0],
|
|
703
|
+
commitOptions?: { sync?: SyncMode }
|
|
704
|
+
) => {
|
|
705
|
+
let callbackResult: TResult | undefined;
|
|
706
|
+
const commit = await run(
|
|
707
|
+
async () => {
|
|
708
|
+
const result = await client[apiName].$commit(fn);
|
|
709
|
+
callbackResult = result.result as TResult;
|
|
710
|
+
return result.commit;
|
|
711
|
+
},
|
|
712
|
+
0,
|
|
713
|
+
commitOptions?.sync
|
|
714
|
+
);
|
|
715
|
+
return { result: callbackResult as TResult, commit };
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
if (typeof prop !== 'string') return undefined;
|
|
719
|
+
return wrapMutationsTable(client, apiName, prop, run);
|
|
720
|
+
},
|
|
721
|
+
}
|
|
722
|
+
) as MutationsHook<DB>,
|
|
723
|
+
[apiName, client, error, isPending, reset, run]
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function useOutboxStats(): SyncularOutboxStats | null {
|
|
728
|
+
const [stats, setStats] = useState<SyncularOutboxStats | null>(null);
|
|
729
|
+
useSyncEvent('outboxChanged', setStats);
|
|
730
|
+
return stats;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function useConflictStats(): SyncularConflictStats | null {
|
|
734
|
+
const [stats, setStats] = useState<SyncularConflictStats | null>(null);
|
|
735
|
+
useSyncEvent('conflictsChanged', setStats);
|
|
736
|
+
return stats;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function usePresence<
|
|
740
|
+
TMetadata extends Record<string, unknown> = Record<string, unknown>,
|
|
741
|
+
>(
|
|
742
|
+
scopeKey: string,
|
|
743
|
+
options: { enabled?: boolean } = {}
|
|
744
|
+
): UsePresenceResult<TMetadata> {
|
|
745
|
+
const client = useClient();
|
|
746
|
+
const enabled = options.enabled !== false;
|
|
747
|
+
const [presence, setPresence] = useState<
|
|
748
|
+
SyncularPresenceEntry<TMetadata>[]
|
|
749
|
+
>([]);
|
|
750
|
+
const [isLoading, setIsLoading] = useState(enabled);
|
|
751
|
+
|
|
752
|
+
useEffect(() => {
|
|
753
|
+
if (!enabled) {
|
|
754
|
+
setPresence([]);
|
|
755
|
+
setIsLoading(false);
|
|
756
|
+
return undefined;
|
|
757
|
+
}
|
|
758
|
+
setPresence(client.presence.get<TMetadata>(scopeKey));
|
|
759
|
+
setIsLoading(false);
|
|
760
|
+
return client.presence.onChange<TMetadata>((event) => {
|
|
761
|
+
if (event.scopeKey === scopeKey) setPresence(event.presence);
|
|
762
|
+
});
|
|
763
|
+
}, [client, enabled, scopeKey]);
|
|
764
|
+
|
|
765
|
+
return { presence, isLoading };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function usePresenceWithJoin<
|
|
769
|
+
TMetadata extends Record<string, unknown> = Record<string, unknown>,
|
|
770
|
+
>(
|
|
771
|
+
scopeKey: string,
|
|
772
|
+
options: UsePresenceWithJoinOptions<TMetadata> = {}
|
|
773
|
+
): UsePresenceWithJoinResult<TMetadata> {
|
|
774
|
+
const client = useClient();
|
|
775
|
+
const metadataRef = useLatest(options.metadata);
|
|
776
|
+
const enabled = options.enabled !== false;
|
|
777
|
+
const base = usePresence<TMetadata>(scopeKey, { enabled });
|
|
778
|
+
const [isJoined, setIsJoined] = useState(false);
|
|
779
|
+
|
|
780
|
+
const join = useCallback(
|
|
781
|
+
(metadata?: TMetadata) => {
|
|
782
|
+
client.presence.join(scopeKey, metadata);
|
|
783
|
+
setIsJoined(true);
|
|
784
|
+
},
|
|
785
|
+
[client, scopeKey]
|
|
786
|
+
);
|
|
787
|
+
const leave = useCallback(() => {
|
|
788
|
+
client.presence.leave(scopeKey);
|
|
789
|
+
setIsJoined(false);
|
|
790
|
+
}, [client, scopeKey]);
|
|
791
|
+
const updateMetadata = useCallback(
|
|
792
|
+
(metadata: TMetadata) => {
|
|
793
|
+
client.presence.updateMetadata(scopeKey, metadata);
|
|
794
|
+
},
|
|
795
|
+
[client, scopeKey]
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
useEffect(() => {
|
|
799
|
+
if (!enabled || options.autoJoin === false) return undefined;
|
|
800
|
+
join(metadataRef.current);
|
|
801
|
+
return () => leave();
|
|
802
|
+
}, [enabled, join, leave, metadataRef, options.autoJoin]);
|
|
803
|
+
|
|
804
|
+
return { ...base, updateMetadata, join, leave, isJoined };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function useBlobUploadQueue(): UseBlobUploadQueueResult {
|
|
808
|
+
const client = useClient();
|
|
809
|
+
const [stats, setStats] = useState<SyncularBlobUploadQueueStats | null>(
|
|
810
|
+
null
|
|
811
|
+
);
|
|
812
|
+
const refresh = useCallback(async () => {
|
|
813
|
+
setStats(await client.blobs.getUploadQueueStats());
|
|
814
|
+
}, [client]);
|
|
815
|
+
const process = useCallback(async () => {
|
|
816
|
+
const result = await client.blobs.processUploadQueue();
|
|
817
|
+
await refresh();
|
|
818
|
+
return result;
|
|
819
|
+
}, [client, refresh]);
|
|
820
|
+
|
|
821
|
+
useEffect(() => {
|
|
822
|
+
void refresh().catch(() => undefined);
|
|
823
|
+
return client.on('blobUploadCompleted', () => {
|
|
824
|
+
void refresh().catch(() => undefined);
|
|
825
|
+
});
|
|
826
|
+
}, [client, refresh]);
|
|
827
|
+
|
|
828
|
+
return { stats, refresh, process };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function useBlob(ref: BlobRef | null | undefined): Uint8Array | null {
|
|
832
|
+
const client = useClient();
|
|
833
|
+
const [data, setData] = useState<Uint8Array | null>(null);
|
|
834
|
+
useEffect(() => {
|
|
835
|
+
let disposed = false;
|
|
836
|
+
if (!ref) {
|
|
837
|
+
setData(null);
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
void client.blobs.retrieve(ref).then((next) => {
|
|
841
|
+
if (!disposed) setData(next);
|
|
842
|
+
});
|
|
843
|
+
return () => {
|
|
844
|
+
disposed = true;
|
|
845
|
+
};
|
|
846
|
+
}, [client, ref]);
|
|
847
|
+
return data;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return {
|
|
851
|
+
SyncProvider,
|
|
852
|
+
useBlob,
|
|
853
|
+
useBlobUploadQueue,
|
|
854
|
+
useConflictStats,
|
|
855
|
+
useLeasedMutation,
|
|
856
|
+
useLeasedMutations,
|
|
857
|
+
useMutation,
|
|
858
|
+
useMutations,
|
|
859
|
+
useOutboxStats,
|
|
860
|
+
usePresence,
|
|
861
|
+
usePresenceWithJoin,
|
|
862
|
+
useQuery,
|
|
863
|
+
useRowsChanged,
|
|
864
|
+
useSyncConnection,
|
|
865
|
+
useSyncContext,
|
|
866
|
+
useSyncStatus,
|
|
867
|
+
useSyncQuery,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function executeSyncQuery<DB, TResult>(
|
|
872
|
+
db: Kysely<DB>,
|
|
873
|
+
query: SyncQueryFn<DB, TResult>
|
|
874
|
+
): Promise<TResult> {
|
|
875
|
+
const ctx: SyncQueryContext<DB> = {
|
|
876
|
+
db,
|
|
877
|
+
selectFrom: db.selectFrom.bind(db) as Kysely<DB>['selectFrom'],
|
|
878
|
+
};
|
|
879
|
+
const result = await Promise.resolve(query(ctx));
|
|
880
|
+
if (isExecutableQuery<TResult>(result)) return await result.execute();
|
|
881
|
+
return result;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function isExecutableQuery<TResult>(
|
|
885
|
+
value: unknown
|
|
886
|
+
): value is ExecutableQuery<TResult> {
|
|
887
|
+
return isRecord(value) && typeof value.execute === 'function';
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function isCompilableExecutableQuery<TResult>(
|
|
891
|
+
value: unknown
|
|
892
|
+
): value is ExecutableQuery<TResult> & { compile: () => CompiledQuery } {
|
|
893
|
+
return (
|
|
894
|
+
isExecutableQuery<TResult>(value) && typeof value.compile === 'function'
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
type LiveQueryCapableClient<DB> = SyncularClientLike<DB> & {
|
|
899
|
+
live<Row extends Record<string, unknown>>(
|
|
900
|
+
query: { compile(): CompiledQuery },
|
|
901
|
+
options: SyncularLiveQueryOptions<Row>
|
|
902
|
+
): Promise<SyncularLiveQuerySubscription>;
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
function hasLiveQueries<DB>(
|
|
906
|
+
client: SyncularClientLike<DB>
|
|
907
|
+
): client is LiveQueryCapableClient<DB> {
|
|
908
|
+
return typeof (client as { live?: unknown }).live === 'function';
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function wrapMutationsTable<DB>(
|
|
912
|
+
client: SyncularClientLike<DB>,
|
|
913
|
+
apiName: 'mutations' | 'leasedMutations',
|
|
914
|
+
table: string,
|
|
915
|
+
run: <TResult extends MutationReceipt>(
|
|
916
|
+
operation: () => Promise<TResult>,
|
|
917
|
+
opCount: number,
|
|
918
|
+
syncOverride?: SyncMode
|
|
919
|
+
) => Promise<TResult>
|
|
920
|
+
): AnyTableMutations {
|
|
921
|
+
const tableApi = client[apiName].$table(table);
|
|
922
|
+
return {
|
|
923
|
+
insert: (values) => run(() => tableApi.insert(values), 1),
|
|
924
|
+
insertMany: (rows) => run(() => tableApi.insertMany(rows), rows.length),
|
|
925
|
+
update: (id, patch, options) =>
|
|
926
|
+
run(() => tableApi.update(id, patch, options), 1),
|
|
927
|
+
delete: (id, options) => run(() => tableApi.delete(id, options), 1),
|
|
928
|
+
upsert: (id, patch, options) =>
|
|
929
|
+
run(() => tableApi.upsert(id, patch, options), 1),
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function useLatest<T>(value: T): { readonly current: T } {
|
|
934
|
+
const ref = useRef(value);
|
|
935
|
+
ref.current = value;
|
|
936
|
+
return ref;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function stableStringListKey(values: readonly string[] | undefined): string {
|
|
940
|
+
return values == null ? '' : values.join('\u0001');
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function useStableSnapshot<T>(read: () => T): () => T {
|
|
944
|
+
const ref = useRef<{ value: T; json: string } | null>(null);
|
|
945
|
+
return useCallback(() => {
|
|
946
|
+
const value = read();
|
|
947
|
+
const json = JSON.stringify(value);
|
|
948
|
+
if (ref.current?.json === json) return ref.current.value;
|
|
949
|
+
ref.current = { value, json };
|
|
950
|
+
return value;
|
|
951
|
+
}, [read]);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
955
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function toError(error: unknown): Error {
|
|
959
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function shareQueryResult<TResult>(
|
|
963
|
+
previous: TResult | undefined,
|
|
964
|
+
next: TResult,
|
|
965
|
+
keyField: string,
|
|
966
|
+
enabled: boolean
|
|
967
|
+
): TResult {
|
|
968
|
+
if (!enabled || previous === undefined) return next;
|
|
969
|
+
if (Array.isArray(previous) && Array.isArray(next)) {
|
|
970
|
+
return shareArrayResult(previous, next, keyField) as TResult;
|
|
971
|
+
}
|
|
972
|
+
if (
|
|
973
|
+
isRecord(previous) &&
|
|
974
|
+
isRecord(next) &&
|
|
975
|
+
shallowEqualRecords(previous, next)
|
|
976
|
+
) {
|
|
977
|
+
return previous;
|
|
978
|
+
}
|
|
979
|
+
return next;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function shareArrayResult<T>(previous: T[], next: T[], keyField: string): T[] {
|
|
983
|
+
if (previous.length === 0 || next.length === 0) return next;
|
|
984
|
+
const previousByKey = new Map<string, T>();
|
|
985
|
+
for (const item of previous) {
|
|
986
|
+
const key = keyedValue(item, keyField);
|
|
987
|
+
if (key == null) return next;
|
|
988
|
+
previousByKey.set(key, item);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
let changed = previous.length !== next.length;
|
|
992
|
+
const shared = next.map((item, index) => {
|
|
993
|
+
const key = keyedValue(item, keyField);
|
|
994
|
+
const previousItem = key == null ? undefined : previousByKey.get(key);
|
|
995
|
+
if (
|
|
996
|
+
previousItem !== undefined &&
|
|
997
|
+
isRecord(previousItem) &&
|
|
998
|
+
isRecord(item) &&
|
|
999
|
+
shallowEqualRecords(previousItem, item)
|
|
1000
|
+
) {
|
|
1001
|
+
if (!Object.is(previousItem, previous[index])) changed = true;
|
|
1002
|
+
return previousItem;
|
|
1003
|
+
}
|
|
1004
|
+
if (!Object.is(item, previous[index])) changed = true;
|
|
1005
|
+
return item;
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
return changed ? shared : previous;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function keyedValue(value: unknown, keyField: string): string | null {
|
|
1012
|
+
if (!isRecord(value)) return null;
|
|
1013
|
+
const key = value[keyField];
|
|
1014
|
+
return key == null ? null : String(key);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function shallowEqualRecords(
|
|
1018
|
+
left: Record<string, unknown>,
|
|
1019
|
+
right: Record<string, unknown>
|
|
1020
|
+
): boolean {
|
|
1021
|
+
if (left === right) return true;
|
|
1022
|
+
const leftKeys = Object.keys(left);
|
|
1023
|
+
const rightKeys = Object.keys(right);
|
|
1024
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
1025
|
+
for (const key of leftKeys) {
|
|
1026
|
+
if (!(key in right) || !Object.is(left[key], right[key])) return false;
|
|
1027
|
+
}
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
export function changedRowsForTable(
|
|
1032
|
+
event: SyncularRowsChangedEvent,
|
|
1033
|
+
table: string
|
|
1034
|
+
): SyncularChangedRow[] {
|
|
1035
|
+
return event.changedRows.filter((row) => row.table === table);
|
|
1036
|
+
}
|