@powersync/common 1.40.0 → 1.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.cjs +10809 -22
- package/dist/bundle.cjs.map +1 -0
- package/dist/bundle.mjs +10730 -22
- package/dist/bundle.mjs.map +1 -0
- package/dist/bundle.node.cjs +10809 -0
- package/dist/bundle.node.cjs.map +1 -0
- package/dist/bundle.node.mjs +10730 -0
- package/dist/bundle.node.mjs.map +1 -0
- package/dist/index.d.cts +5 -1
- package/lib/client/AbstractPowerSyncDatabase.js +1 -0
- package/lib/client/AbstractPowerSyncDatabase.js.map +1 -0
- package/lib/client/AbstractPowerSyncOpenFactory.js +1 -0
- package/lib/client/AbstractPowerSyncOpenFactory.js.map +1 -0
- package/lib/client/ConnectionManager.js +1 -0
- package/lib/client/ConnectionManager.js.map +1 -0
- package/lib/client/CustomQuery.js +1 -0
- package/lib/client/CustomQuery.js.map +1 -0
- package/lib/client/Query.js +1 -0
- package/lib/client/Query.js.map +1 -0
- package/lib/client/SQLOpenFactory.js +1 -0
- package/lib/client/SQLOpenFactory.js.map +1 -0
- package/lib/client/compilableQueryWatch.js +1 -0
- package/lib/client/compilableQueryWatch.js.map +1 -0
- package/lib/client/connection/PowerSyncBackendConnector.js +1 -0
- package/lib/client/connection/PowerSyncBackendConnector.js.map +1 -0
- package/lib/client/connection/PowerSyncCredentials.js +1 -0
- package/lib/client/connection/PowerSyncCredentials.js.map +1 -0
- package/lib/client/constants.js +1 -0
- package/lib/client/constants.js.map +1 -0
- package/lib/client/runOnSchemaChange.js +1 -0
- package/lib/client/runOnSchemaChange.js.map +1 -0
- package/lib/client/sync/bucket/BucketStorageAdapter.js +1 -0
- package/lib/client/sync/bucket/BucketStorageAdapter.js.map +1 -0
- package/lib/client/sync/bucket/CrudBatch.js +1 -0
- package/lib/client/sync/bucket/CrudBatch.js.map +1 -0
- package/lib/client/sync/bucket/CrudEntry.js +1 -0
- package/lib/client/sync/bucket/CrudEntry.js.map +1 -0
- package/lib/client/sync/bucket/CrudTransaction.js +1 -0
- package/lib/client/sync/bucket/CrudTransaction.js.map +1 -0
- package/lib/client/sync/bucket/OpType.js +1 -0
- package/lib/client/sync/bucket/OpType.js.map +1 -0
- package/lib/client/sync/bucket/OplogEntry.js +1 -0
- package/lib/client/sync/bucket/OplogEntry.js.map +1 -0
- package/lib/client/sync/bucket/SqliteBucketStorage.js +1 -0
- package/lib/client/sync/bucket/SqliteBucketStorage.js.map +1 -0
- package/lib/client/sync/bucket/SyncDataBatch.js +1 -0
- package/lib/client/sync/bucket/SyncDataBatch.js.map +1 -0
- package/lib/client/sync/bucket/SyncDataBucket.js +1 -0
- package/lib/client/sync/bucket/SyncDataBucket.js.map +1 -0
- package/lib/client/sync/stream/AbstractRemote.d.ts +5 -0
- package/lib/client/sync/stream/AbstractRemote.js +9 -2
- package/lib/client/sync/stream/AbstractRemote.js.map +1 -0
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +1 -0
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -0
- package/lib/client/sync/stream/WebsocketClientTransport.js +1 -0
- package/lib/client/sync/stream/WebsocketClientTransport.js.map +1 -0
- package/lib/client/sync/stream/core-instruction.js +1 -0
- package/lib/client/sync/stream/core-instruction.js.map +1 -0
- package/lib/client/sync/stream/streaming-sync-types.js +1 -0
- package/lib/client/sync/stream/streaming-sync-types.js.map +1 -0
- package/lib/client/sync/sync-streams.js +1 -0
- package/lib/client/sync/sync-streams.js.map +1 -0
- package/lib/client/triggers/TriggerManager.js +1 -0
- package/lib/client/triggers/TriggerManager.js.map +1 -0
- package/lib/client/triggers/TriggerManagerImpl.js +1 -0
- package/lib/client/triggers/TriggerManagerImpl.js.map +1 -0
- package/lib/client/triggers/sanitizeSQL.js +1 -0
- package/lib/client/triggers/sanitizeSQL.js.map +1 -0
- package/lib/client/watched/GetAllQuery.js +1 -0
- package/lib/client/watched/GetAllQuery.js.map +1 -0
- package/lib/client/watched/WatchedQuery.js +1 -0
- package/lib/client/watched/WatchedQuery.js.map +1 -0
- package/lib/client/watched/processors/AbstractQueryProcessor.js +1 -0
- package/lib/client/watched/processors/AbstractQueryProcessor.js.map +1 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.js +1 -0
- package/lib/client/watched/processors/DifferentialQueryProcessor.js.map +1 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.js +1 -0
- package/lib/client/watched/processors/OnChangeQueryProcessor.js.map +1 -0
- package/lib/client/watched/processors/comparators.js +1 -0
- package/lib/client/watched/processors/comparators.js.map +1 -0
- package/lib/db/DBAdapter.js +1 -0
- package/lib/db/DBAdapter.js.map +1 -0
- package/lib/db/crud/SyncProgress.js +1 -0
- package/lib/db/crud/SyncProgress.js.map +1 -0
- package/lib/db/crud/SyncStatus.js +1 -0
- package/lib/db/crud/SyncStatus.js.map +1 -0
- package/lib/db/crud/UploadQueueStatus.js +1 -0
- package/lib/db/crud/UploadQueueStatus.js.map +1 -0
- package/lib/db/schema/Column.js +1 -0
- package/lib/db/schema/Column.js.map +1 -0
- package/lib/db/schema/Index.js +1 -0
- package/lib/db/schema/Index.js.map +1 -0
- package/lib/db/schema/IndexedColumn.js +1 -0
- package/lib/db/schema/IndexedColumn.js.map +1 -0
- package/lib/db/schema/RawTable.js +1 -0
- package/lib/db/schema/RawTable.js.map +1 -0
- package/lib/db/schema/Schema.d.ts +0 -1
- package/lib/db/schema/Schema.js +4 -8
- package/lib/db/schema/Schema.js.map +1 -0
- package/lib/db/schema/Table.js +1 -0
- package/lib/db/schema/Table.js.map +1 -0
- package/lib/db/schema/TableV2.js +1 -0
- package/lib/db/schema/TableV2.js.map +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -0
- package/lib/types/types.js +1 -0
- package/lib/types/types.js.map +1 -0
- package/lib/utils/AbortOperation.js +1 -0
- package/lib/utils/AbortOperation.js.map +1 -0
- package/lib/utils/BaseObserver.js +1 -0
- package/lib/utils/BaseObserver.js.map +1 -0
- package/lib/utils/ControlledExecutor.js +1 -0
- package/lib/utils/ControlledExecutor.js.map +1 -0
- package/lib/utils/DataStream.js +1 -0
- package/lib/utils/DataStream.js.map +1 -0
- package/lib/utils/Logger.js +1 -0
- package/lib/utils/Logger.js.map +1 -0
- package/lib/utils/MetaBaseObserver.js +1 -0
- package/lib/utils/MetaBaseObserver.js.map +1 -0
- package/lib/utils/async.js +1 -0
- package/lib/utils/async.js.map +1 -0
- package/lib/utils/mutex.js +1 -0
- package/lib/utils/mutex.js.map +1 -0
- package/lib/utils/parseQuery.js +1 -0
- package/lib/utils/parseQuery.js.map +1 -0
- package/package.json +23 -15
- package/src/client/AbstractPowerSyncDatabase.ts +1343 -0
- package/src/client/AbstractPowerSyncOpenFactory.ts +39 -0
- package/src/client/ConnectionManager.ts +402 -0
- package/src/client/CustomQuery.ts +56 -0
- package/src/client/Query.ts +106 -0
- package/src/client/SQLOpenFactory.ts +55 -0
- package/src/client/compilableQueryWatch.ts +55 -0
- package/src/client/connection/PowerSyncBackendConnector.ts +25 -0
- package/src/client/connection/PowerSyncCredentials.ts +5 -0
- package/src/client/constants.ts +1 -0
- package/src/client/runOnSchemaChange.ts +31 -0
- package/src/client/sync/bucket/BucketStorageAdapter.ts +118 -0
- package/src/client/sync/bucket/CrudBatch.ts +21 -0
- package/src/client/sync/bucket/CrudEntry.ts +172 -0
- package/src/client/sync/bucket/CrudTransaction.ts +21 -0
- package/src/client/sync/bucket/OpType.ts +23 -0
- package/src/client/sync/bucket/OplogEntry.ts +50 -0
- package/src/client/sync/bucket/SqliteBucketStorage.ts +395 -0
- package/src/client/sync/bucket/SyncDataBatch.ts +11 -0
- package/src/client/sync/bucket/SyncDataBucket.ts +49 -0
- package/src/client/sync/stream/AbstractRemote.ts +626 -0
- package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +1258 -0
- package/src/client/sync/stream/WebsocketClientTransport.ts +80 -0
- package/src/client/sync/stream/core-instruction.ts +99 -0
- package/src/client/sync/stream/streaming-sync-types.ts +205 -0
- package/src/client/sync/sync-streams.ts +107 -0
- package/src/client/triggers/TriggerManager.ts +384 -0
- package/src/client/triggers/TriggerManagerImpl.ts +314 -0
- package/src/client/triggers/sanitizeSQL.ts +66 -0
- package/src/client/watched/GetAllQuery.ts +46 -0
- package/src/client/watched/WatchedQuery.ts +121 -0
- package/src/client/watched/processors/AbstractQueryProcessor.ts +226 -0
- package/src/client/watched/processors/DifferentialQueryProcessor.ts +305 -0
- package/src/client/watched/processors/OnChangeQueryProcessor.ts +122 -0
- package/src/client/watched/processors/comparators.ts +57 -0
- package/src/db/DBAdapter.ts +134 -0
- package/src/db/crud/SyncProgress.ts +100 -0
- package/src/db/crud/SyncStatus.ts +308 -0
- package/src/db/crud/UploadQueueStatus.ts +20 -0
- package/src/db/schema/Column.ts +60 -0
- package/src/db/schema/Index.ts +39 -0
- package/src/db/schema/IndexedColumn.ts +42 -0
- package/src/db/schema/RawTable.ts +67 -0
- package/src/db/schema/Schema.ts +76 -0
- package/src/db/schema/Table.ts +359 -0
- package/src/db/schema/TableV2.ts +9 -0
- package/src/index.ts +52 -0
- package/src/types/types.ts +9 -0
- package/src/utils/AbortOperation.ts +17 -0
- package/src/utils/BaseObserver.ts +41 -0
- package/src/utils/ControlledExecutor.ts +72 -0
- package/src/utils/DataStream.ts +211 -0
- package/src/utils/Logger.ts +47 -0
- package/src/utils/MetaBaseObserver.ts +81 -0
- package/src/utils/async.ts +61 -0
- package/src/utils/mutex.ts +34 -0
- package/src/utils/parseQuery.ts +25 -0
|
@@ -0,0 +1,1343 @@
|
|
|
1
|
+
import { Mutex } from 'async-mutex';
|
|
2
|
+
import { EventIterator } from 'event-iterator';
|
|
3
|
+
import Logger, { ILogger } from 'js-logger';
|
|
4
|
+
import {
|
|
5
|
+
BatchedUpdateNotification,
|
|
6
|
+
DBAdapter,
|
|
7
|
+
QueryResult,
|
|
8
|
+
Transaction,
|
|
9
|
+
UpdateNotification,
|
|
10
|
+
isBatchedUpdateNotification
|
|
11
|
+
} from '../db/DBAdapter.js';
|
|
12
|
+
import { SyncStatus } from '../db/crud/SyncStatus.js';
|
|
13
|
+
import { UploadQueueStats } from '../db/crud/UploadQueueStatus.js';
|
|
14
|
+
import { Schema } from '../db/schema/Schema.js';
|
|
15
|
+
import { BaseObserver } from '../utils/BaseObserver.js';
|
|
16
|
+
import { ControlledExecutor } from '../utils/ControlledExecutor.js';
|
|
17
|
+
import { symbolAsyncIterator, throttleTrailing } from '../utils/async.js';
|
|
18
|
+
import {
|
|
19
|
+
ConnectionManager,
|
|
20
|
+
CreateSyncImplementationOptions,
|
|
21
|
+
InternalSubscriptionAdapter
|
|
22
|
+
} from './ConnectionManager.js';
|
|
23
|
+
import { CustomQuery } from './CustomQuery.js';
|
|
24
|
+
import { ArrayQueryDefinition, Query } from './Query.js';
|
|
25
|
+
import { SQLOpenFactory, SQLOpenOptions, isDBAdapter, isSQLOpenFactory, isSQLOpenOptions } from './SQLOpenFactory.js';
|
|
26
|
+
import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js';
|
|
27
|
+
import { BucketStorageAdapter, PSInternalTable } from './sync/bucket/BucketStorageAdapter.js';
|
|
28
|
+
import { CrudBatch } from './sync/bucket/CrudBatch.js';
|
|
29
|
+
import { CrudEntry, CrudEntryJSON } from './sync/bucket/CrudEntry.js';
|
|
30
|
+
import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
|
|
31
|
+
import {
|
|
32
|
+
DEFAULT_CRUD_UPLOAD_THROTTLE_MS,
|
|
33
|
+
DEFAULT_RETRY_DELAY_MS,
|
|
34
|
+
InternalConnectionOptions,
|
|
35
|
+
StreamingSyncImplementation,
|
|
36
|
+
StreamingSyncImplementationListener,
|
|
37
|
+
type AdditionalConnectionOptions,
|
|
38
|
+
type PowerSyncConnectionOptions,
|
|
39
|
+
type RequiredAdditionalConnectionOptions
|
|
40
|
+
} from './sync/stream/AbstractStreamingSyncImplementation.js';
|
|
41
|
+
import { TriggerManager } from './triggers/TriggerManager.js';
|
|
42
|
+
import { TriggerManagerImpl } from './triggers/TriggerManagerImpl.js';
|
|
43
|
+
import { DEFAULT_WATCH_THROTTLE_MS, WatchCompatibleQuery } from './watched/WatchedQuery.js';
|
|
44
|
+
import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js';
|
|
45
|
+
import { WatchedQueryComparator } from './watched/processors/comparators.js';
|
|
46
|
+
import { coreStatusToJs, CoreSyncStatus } from './sync/stream/core-instruction.js';
|
|
47
|
+
import { SyncStream } from './sync/sync-streams.js';
|
|
48
|
+
|
|
49
|
+
export interface DisconnectAndClearOptions {
|
|
50
|
+
/** When set to false, data in local-only tables is preserved. */
|
|
51
|
+
clearLocal?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface BasePowerSyncDatabaseOptions extends AdditionalConnectionOptions {
|
|
55
|
+
/** Schema used for the local database. */
|
|
56
|
+
schema: Schema;
|
|
57
|
+
/**
|
|
58
|
+
* @deprecated Use {@link retryDelayMs} instead as this will be removed in future releases.
|
|
59
|
+
*/
|
|
60
|
+
retryDelay?: number;
|
|
61
|
+
logger?: ILogger;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface PowerSyncDatabaseOptions extends BasePowerSyncDatabaseOptions {
|
|
65
|
+
/**
|
|
66
|
+
* Source for a SQLite database connection.
|
|
67
|
+
* This can be either:
|
|
68
|
+
* - A {@link DBAdapter} if providing an instantiated SQLite connection
|
|
69
|
+
* - A {@link SQLOpenFactory} which will be used to open a SQLite connection
|
|
70
|
+
* - {@link SQLOpenOptions} for opening a SQLite connection with a default {@link SQLOpenFactory}
|
|
71
|
+
*/
|
|
72
|
+
database: DBAdapter | SQLOpenFactory | SQLOpenOptions;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface PowerSyncDatabaseOptionsWithDBAdapter extends BasePowerSyncDatabaseOptions {
|
|
76
|
+
database: DBAdapter;
|
|
77
|
+
}
|
|
78
|
+
export interface PowerSyncDatabaseOptionsWithOpenFactory extends BasePowerSyncDatabaseOptions {
|
|
79
|
+
database: SQLOpenFactory;
|
|
80
|
+
}
|
|
81
|
+
export interface PowerSyncDatabaseOptionsWithSettings extends BasePowerSyncDatabaseOptions {
|
|
82
|
+
database: SQLOpenOptions;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SQLOnChangeOptions {
|
|
86
|
+
signal?: AbortSignal;
|
|
87
|
+
tables?: string[];
|
|
88
|
+
/** The minimum interval between queries. */
|
|
89
|
+
throttleMs?: number;
|
|
90
|
+
/**
|
|
91
|
+
* @deprecated All tables specified in {@link tables} will be watched, including PowerSync tables with prefixes.
|
|
92
|
+
*
|
|
93
|
+
* Allows for watching any SQL table
|
|
94
|
+
* by not removing PowerSync table name prefixes
|
|
95
|
+
*/
|
|
96
|
+
rawTableNames?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Emits an empty result set immediately
|
|
99
|
+
*/
|
|
100
|
+
triggerImmediate?: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface SQLWatchOptions extends SQLOnChangeOptions {
|
|
104
|
+
/**
|
|
105
|
+
* Optional comparator which will be used to compare the results of the query.
|
|
106
|
+
* The watched query will only yield results if the comparator returns false.
|
|
107
|
+
*/
|
|
108
|
+
comparator?: WatchedQueryComparator<QueryResult>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface WatchOnChangeEvent {
|
|
112
|
+
changedTables: string[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface WatchHandler {
|
|
116
|
+
onResult: (results: QueryResult) => void;
|
|
117
|
+
onError?: (error: Error) => void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface WatchOnChangeHandler {
|
|
121
|
+
onChange: (event: WatchOnChangeEvent) => Promise<void> | void;
|
|
122
|
+
onError?: (error: Error) => void;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface PowerSyncDBListener extends StreamingSyncImplementationListener {
|
|
126
|
+
initialized: () => void;
|
|
127
|
+
schemaChanged: (schema: Schema) => void;
|
|
128
|
+
closing: () => Promise<void> | void;
|
|
129
|
+
closed: () => Promise<void> | void;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface PowerSyncCloseOptions {
|
|
133
|
+
/**
|
|
134
|
+
* Disconnect the sync stream client if connected.
|
|
135
|
+
* This is usually true, but can be false for Web when using
|
|
136
|
+
* multiple tabs and a shared sync provider.
|
|
137
|
+
*/
|
|
138
|
+
disconnect?: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const POWERSYNC_TABLE_MATCH = /(^ps_data__|^ps_data_local__)/;
|
|
142
|
+
|
|
143
|
+
const DEFAULT_DISCONNECT_CLEAR_OPTIONS: DisconnectAndClearOptions = {
|
|
144
|
+
clearLocal: true
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const DEFAULT_POWERSYNC_CLOSE_OPTIONS: PowerSyncCloseOptions = {
|
|
148
|
+
disconnect: true
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const DEFAULT_POWERSYNC_DB_OPTIONS = {
|
|
152
|
+
retryDelayMs: 5000,
|
|
153
|
+
crudUploadThrottleMs: DEFAULT_CRUD_UPLOAD_THROTTLE_MS
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const DEFAULT_CRUD_BATCH_LIMIT = 100;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Requesting nested or recursive locks can block the application in some circumstances.
|
|
160
|
+
* This default lock timeout will act as a failsafe to throw an error if a lock cannot
|
|
161
|
+
* be obtained.
|
|
162
|
+
*/
|
|
163
|
+
export const DEFAULT_LOCK_TIMEOUT_MS = 120_000; // 2 mins
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Tests if the input is a {@link PowerSyncDatabaseOptionsWithSettings}
|
|
167
|
+
* @internal
|
|
168
|
+
*/
|
|
169
|
+
export const isPowerSyncDatabaseOptionsWithSettings = (test: any): test is PowerSyncDatabaseOptionsWithSettings => {
|
|
170
|
+
return typeof test == 'object' && isSQLOpenOptions(test.database);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDBListener> {
|
|
174
|
+
/**
|
|
175
|
+
* Returns true if the connection is closed.
|
|
176
|
+
*/
|
|
177
|
+
closed: boolean;
|
|
178
|
+
ready: boolean;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Current connection status.
|
|
182
|
+
*/
|
|
183
|
+
currentStatus: SyncStatus;
|
|
184
|
+
|
|
185
|
+
sdkVersion: string;
|
|
186
|
+
|
|
187
|
+
protected bucketStorageAdapter: BucketStorageAdapter;
|
|
188
|
+
protected _isReadyPromise: Promise<void>;
|
|
189
|
+
protected connectionManager: ConnectionManager;
|
|
190
|
+
private subscriptions: InternalSubscriptionAdapter;
|
|
191
|
+
|
|
192
|
+
get syncStreamImplementation() {
|
|
193
|
+
return this.connectionManager.syncStreamImplementation;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The connector used to connect to the PowerSync service.
|
|
198
|
+
*
|
|
199
|
+
* @returns The connector used to connect to the PowerSync service or null if `connect()` has not been called.
|
|
200
|
+
*/
|
|
201
|
+
get connector() {
|
|
202
|
+
return this.connectionManager.connector;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The resolved connection options used to connect to the PowerSync service.
|
|
207
|
+
*
|
|
208
|
+
* @returns The resolved connection options used to connect to the PowerSync service or null if `connect()` has not been called.
|
|
209
|
+
*/
|
|
210
|
+
get connectionOptions() {
|
|
211
|
+
return this.connectionManager.connectionOptions;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
protected _schema: Schema;
|
|
215
|
+
|
|
216
|
+
private _database: DBAdapter;
|
|
217
|
+
|
|
218
|
+
protected runExclusiveMutex: Mutex;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @experimental
|
|
222
|
+
* Allows creating SQLite triggers which can be used to track various operations on SQLite tables.
|
|
223
|
+
*/
|
|
224
|
+
readonly triggers: TriggerManager;
|
|
225
|
+
|
|
226
|
+
logger: ILogger;
|
|
227
|
+
|
|
228
|
+
constructor(options: PowerSyncDatabaseOptionsWithDBAdapter);
|
|
229
|
+
constructor(options: PowerSyncDatabaseOptionsWithOpenFactory);
|
|
230
|
+
constructor(options: PowerSyncDatabaseOptionsWithSettings);
|
|
231
|
+
constructor(options: PowerSyncDatabaseOptions); // Note this is important for extending this class and maintaining API compatibility
|
|
232
|
+
constructor(protected options: PowerSyncDatabaseOptions) {
|
|
233
|
+
super();
|
|
234
|
+
|
|
235
|
+
const { database, schema } = options;
|
|
236
|
+
|
|
237
|
+
if (typeof schema?.toJSON != 'function') {
|
|
238
|
+
throw new Error('The `schema` option should be provided and should be an instance of `Schema`.');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (isDBAdapter(database)) {
|
|
242
|
+
this._database = database;
|
|
243
|
+
} else if (isSQLOpenFactory(database)) {
|
|
244
|
+
this._database = database.openDB();
|
|
245
|
+
} else if (isPowerSyncDatabaseOptionsWithSettings(options)) {
|
|
246
|
+
this._database = this.openDBAdapter(options);
|
|
247
|
+
} else {
|
|
248
|
+
throw new Error('The provided `database` option is invalid.');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.logger = options.logger ?? Logger.get(`PowerSyncDatabase[${this._database.name}]`);
|
|
252
|
+
|
|
253
|
+
this.bucketStorageAdapter = this.generateBucketStorageAdapter();
|
|
254
|
+
this.closed = false;
|
|
255
|
+
this.currentStatus = new SyncStatus({});
|
|
256
|
+
this.options = { ...DEFAULT_POWERSYNC_DB_OPTIONS, ...options };
|
|
257
|
+
this._schema = schema;
|
|
258
|
+
this.ready = false;
|
|
259
|
+
this.sdkVersion = '';
|
|
260
|
+
this.runExclusiveMutex = new Mutex();
|
|
261
|
+
|
|
262
|
+
// Start async init
|
|
263
|
+
this.subscriptions = {
|
|
264
|
+
firstStatusMatching: (predicate, abort) => this.waitForStatus(predicate, abort),
|
|
265
|
+
resolveOfflineSyncStatus: () => this.resolveOfflineSyncStatus(),
|
|
266
|
+
rustSubscriptionsCommand: async (payload) => {
|
|
267
|
+
await this.writeTransaction((tx) => {
|
|
268
|
+
return tx.execute('select powersync_control(?,?)', ['subscriptions', JSON.stringify(payload)]);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
this.connectionManager = new ConnectionManager({
|
|
273
|
+
createSyncImplementation: async (connector, options) => {
|
|
274
|
+
await this.waitForReady();
|
|
275
|
+
return this.runExclusive(async () => {
|
|
276
|
+
const sync = this.generateSyncStreamImplementation(connector, this.resolvedConnectionOptions(options));
|
|
277
|
+
const onDispose = sync.registerListener({
|
|
278
|
+
statusChanged: (status) => {
|
|
279
|
+
this.currentStatus = new SyncStatus({
|
|
280
|
+
...status.toJSON(),
|
|
281
|
+
hasSynced: this.currentStatus?.hasSynced || !!status.lastSyncedAt
|
|
282
|
+
});
|
|
283
|
+
this.iterateListeners((cb) => cb.statusChanged?.(this.currentStatus));
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
await sync.waitForReady();
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
sync,
|
|
290
|
+
onDispose
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
logger: this.logger
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
this._isReadyPromise = this.initialize();
|
|
298
|
+
|
|
299
|
+
this.triggers = new TriggerManagerImpl({
|
|
300
|
+
db: this,
|
|
301
|
+
schema: this.schema
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Schema used for the local database.
|
|
307
|
+
*/
|
|
308
|
+
get schema() {
|
|
309
|
+
return this._schema;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The underlying database.
|
|
314
|
+
*
|
|
315
|
+
* For the most part, behavior is the same whether querying on the underlying database, or on {@link AbstractPowerSyncDatabase}.
|
|
316
|
+
*/
|
|
317
|
+
get database() {
|
|
318
|
+
return this._database;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Whether a connection to the PowerSync service is currently open.
|
|
323
|
+
*/
|
|
324
|
+
get connected() {
|
|
325
|
+
return this.currentStatus?.connected || false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
get connecting() {
|
|
329
|
+
return this.currentStatus?.connecting || false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Opens the DBAdapter given open options using a default open factory
|
|
334
|
+
*/
|
|
335
|
+
protected abstract openDBAdapter(options: PowerSyncDatabaseOptionsWithSettings): DBAdapter;
|
|
336
|
+
|
|
337
|
+
protected abstract generateSyncStreamImplementation(
|
|
338
|
+
connector: PowerSyncBackendConnector,
|
|
339
|
+
options: CreateSyncImplementationOptions & RequiredAdditionalConnectionOptions
|
|
340
|
+
): StreamingSyncImplementation;
|
|
341
|
+
|
|
342
|
+
protected abstract generateBucketStorageAdapter(): BucketStorageAdapter;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @returns A promise which will resolve once initialization is completed.
|
|
346
|
+
*/
|
|
347
|
+
async waitForReady(): Promise<void> {
|
|
348
|
+
if (this.ready) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
await this._isReadyPromise;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Wait for the first sync operation to complete.
|
|
357
|
+
*
|
|
358
|
+
* @param request Either an abort signal (after which the promise will complete regardless of
|
|
359
|
+
* whether a full sync was completed) or an object providing an abort signal and a priority target.
|
|
360
|
+
* When a priority target is set, the promise may complete when all buckets with the given (or higher)
|
|
361
|
+
* priorities have been synchronized. This can be earlier than a complete sync.
|
|
362
|
+
* @returns A promise which will resolve once the first full sync has completed.
|
|
363
|
+
*/
|
|
364
|
+
async waitForFirstSync(request?: AbortSignal | { signal?: AbortSignal; priority?: number }): Promise<void> {
|
|
365
|
+
const signal = request instanceof AbortSignal ? request : request?.signal;
|
|
366
|
+
const priority = request && 'priority' in request ? request.priority : undefined;
|
|
367
|
+
|
|
368
|
+
const statusMatches =
|
|
369
|
+
priority === undefined
|
|
370
|
+
? (status: SyncStatus) => status.hasSynced
|
|
371
|
+
: (status: SyncStatus) => status.statusForPriority(priority).hasSynced;
|
|
372
|
+
|
|
373
|
+
return this.waitForStatus(statusMatches, signal);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Waits for the first sync status for which the `status` callback returns a truthy value.
|
|
378
|
+
*/
|
|
379
|
+
async waitForStatus(predicate: (status: SyncStatus) => any, signal?: AbortSignal): Promise<void> {
|
|
380
|
+
if (predicate(this.currentStatus)) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return new Promise((resolve) => {
|
|
385
|
+
const dispose = this.registerListener({
|
|
386
|
+
statusChanged: (status) => {
|
|
387
|
+
if (predicate(status)) {
|
|
388
|
+
abort();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
function abort() {
|
|
394
|
+
dispose();
|
|
395
|
+
resolve();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (signal?.aborted) {
|
|
399
|
+
abort();
|
|
400
|
+
} else {
|
|
401
|
+
signal?.addEventListener('abort', abort);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Allows for extended implementations to execute custom initialization
|
|
408
|
+
* logic as part of the total init process
|
|
409
|
+
*/
|
|
410
|
+
abstract _initialize(): Promise<void>;
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Entry point for executing initialization logic.
|
|
414
|
+
* This is to be automatically executed in the constructor.
|
|
415
|
+
*/
|
|
416
|
+
protected async initialize() {
|
|
417
|
+
await this._initialize();
|
|
418
|
+
await this.bucketStorageAdapter.init();
|
|
419
|
+
await this._loadVersion();
|
|
420
|
+
await this.updateSchema(this.options.schema);
|
|
421
|
+
await this.resolveOfflineSyncStatus();
|
|
422
|
+
await this.database.execute('PRAGMA RECURSIVE_TRIGGERS=TRUE');
|
|
423
|
+
this.ready = true;
|
|
424
|
+
this.iterateListeners((cb) => cb.initialized?.());
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private async _loadVersion() {
|
|
428
|
+
try {
|
|
429
|
+
const { version } = await this.database.get<{ version: string }>('SELECT powersync_rs_version() as version');
|
|
430
|
+
this.sdkVersion = version;
|
|
431
|
+
} catch (e) {
|
|
432
|
+
throw new Error(`The powersync extension is not loaded correctly. Details: ${e.message}`);
|
|
433
|
+
}
|
|
434
|
+
let versionInts: number[];
|
|
435
|
+
try {
|
|
436
|
+
versionInts = this.sdkVersion!.split(/[.\/]/)
|
|
437
|
+
.slice(0, 3)
|
|
438
|
+
.map((n) => parseInt(n));
|
|
439
|
+
} catch (e) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`Unsupported powersync extension version. Need >=0.4.5 <1.0.0, got: ${this.sdkVersion}. Details: ${e.message}`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Validate >=0.4.5 <1.0.0
|
|
446
|
+
if (versionInts[0] != 0 || versionInts[1] < 4 || (versionInts[1] == 4 && versionInts[2] < 5)) {
|
|
447
|
+
throw new Error(`Unsupported powersync extension version. Need >=0.4.5 <1.0.0, got: ${this.sdkVersion}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
protected async resolveOfflineSyncStatus() {
|
|
452
|
+
const result = await this.database.get<{ r: string }>('SELECT powersync_offline_sync_status() as r');
|
|
453
|
+
const parsed = JSON.parse(result.r) as CoreSyncStatus;
|
|
454
|
+
|
|
455
|
+
const updatedStatus = new SyncStatus({
|
|
456
|
+
...this.currentStatus.toJSON(),
|
|
457
|
+
...coreStatusToJs(parsed)
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (!updatedStatus.isEqual(this.currentStatus)) {
|
|
461
|
+
this.currentStatus = updatedStatus;
|
|
462
|
+
this.iterateListeners((l) => l.statusChanged?.(this.currentStatus));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Replace the schema with a new version. This is for advanced use cases - typically the schema should just be specified once in the constructor.
|
|
468
|
+
*
|
|
469
|
+
* Cannot be used while connected - this should only be called before {@link AbstractPowerSyncDatabase.connect}.
|
|
470
|
+
*/
|
|
471
|
+
async updateSchema(schema: Schema) {
|
|
472
|
+
if (this.syncStreamImplementation) {
|
|
473
|
+
throw new Error('Cannot update schema while connected');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* TODO
|
|
478
|
+
* Validations only show a warning for now.
|
|
479
|
+
* The next major release should throw an exception.
|
|
480
|
+
*/
|
|
481
|
+
try {
|
|
482
|
+
schema.validate();
|
|
483
|
+
} catch (ex) {
|
|
484
|
+
this.logger.warn('Schema validation failed. Unexpected behaviour could occur', ex);
|
|
485
|
+
}
|
|
486
|
+
this._schema = schema;
|
|
487
|
+
|
|
488
|
+
await this.database.execute('SELECT powersync_replace_schema(?)', [JSON.stringify(this.schema.toJSON())]);
|
|
489
|
+
await this.database.refreshSchema();
|
|
490
|
+
this.iterateListeners(async (cb) => cb.schemaChanged?.(schema));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Wait for initialization to complete.
|
|
495
|
+
* While initializing is automatic, this helps to catch and report initialization errors.
|
|
496
|
+
*/
|
|
497
|
+
async init() {
|
|
498
|
+
return this.waitForReady();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Use the options passed in during connect, or fallback to the options set during database creation or fallback to the default options
|
|
502
|
+
protected resolvedConnectionOptions(
|
|
503
|
+
options: CreateSyncImplementationOptions
|
|
504
|
+
): CreateSyncImplementationOptions & RequiredAdditionalConnectionOptions {
|
|
505
|
+
return {
|
|
506
|
+
...options,
|
|
507
|
+
retryDelayMs:
|
|
508
|
+
options?.retryDelayMs ?? this.options.retryDelayMs ?? this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS,
|
|
509
|
+
crudUploadThrottleMs:
|
|
510
|
+
options?.crudUploadThrottleMs ?? this.options.crudUploadThrottleMs ?? DEFAULT_CRUD_UPLOAD_THROTTLE_MS
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* @deprecated Use {@link AbstractPowerSyncDatabase#close} instead.
|
|
516
|
+
* Clears all listeners registered by {@link AbstractPowerSyncDatabase#registerListener}.
|
|
517
|
+
*/
|
|
518
|
+
dispose(): void {
|
|
519
|
+
return super.dispose();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Locking mechanism for exclusively running critical portions of connect/disconnect operations.
|
|
524
|
+
* Locking here is mostly only important on web for multiple tab scenarios.
|
|
525
|
+
*/
|
|
526
|
+
protected runExclusive<T>(callback: () => Promise<T>): Promise<T> {
|
|
527
|
+
return this.runExclusiveMutex.runExclusive(callback);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Connects to stream of events from the PowerSync instance.
|
|
532
|
+
*/
|
|
533
|
+
async connect(connector: PowerSyncBackendConnector, options?: PowerSyncConnectionOptions) {
|
|
534
|
+
const resolvedOptions: InternalConnectionOptions = options ?? {};
|
|
535
|
+
resolvedOptions.serializedSchema = this.schema.toJSON();
|
|
536
|
+
|
|
537
|
+
return this.connectionManager.connect(connector, resolvedOptions);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Close the sync connection.
|
|
542
|
+
*
|
|
543
|
+
* Use {@link connect} to connect again.
|
|
544
|
+
*/
|
|
545
|
+
async disconnect() {
|
|
546
|
+
return this.connectionManager.disconnect();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Disconnect and clear the database.
|
|
551
|
+
* Use this when logging out.
|
|
552
|
+
* The database can still be queried after this is called, but the tables
|
|
553
|
+
* would be empty.
|
|
554
|
+
*
|
|
555
|
+
* To preserve data in local-only tables, set clearLocal to false.
|
|
556
|
+
*/
|
|
557
|
+
async disconnectAndClear(options = DEFAULT_DISCONNECT_CLEAR_OPTIONS) {
|
|
558
|
+
await this.disconnect();
|
|
559
|
+
await this.waitForReady();
|
|
560
|
+
|
|
561
|
+
const { clearLocal } = options;
|
|
562
|
+
|
|
563
|
+
// TODO DB name, verify this is necessary with extension
|
|
564
|
+
await this.database.writeTransaction(async (tx) => {
|
|
565
|
+
await tx.execute('SELECT powersync_clear(?)', [clearLocal ? 1 : 0]);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// The data has been deleted - reset the sync status
|
|
569
|
+
this.currentStatus = new SyncStatus({});
|
|
570
|
+
this.iterateListeners((l) => l.statusChanged?.(this.currentStatus));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Create a sync stream to query its status or to subscribe to it.
|
|
575
|
+
*
|
|
576
|
+
* @param name The name of the stream to subscribe to.
|
|
577
|
+
* @param params Optional parameters for the stream subscription.
|
|
578
|
+
* @returns A {@link SyncStream} instance that can be subscribed to.
|
|
579
|
+
* @experimental Sync streams are currently in alpha.
|
|
580
|
+
*/
|
|
581
|
+
syncStream(name: string, params?: Record<string, any>): SyncStream {
|
|
582
|
+
return this.connectionManager.stream(this.subscriptions, name, params ?? null);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Close the database, releasing resources.
|
|
587
|
+
*
|
|
588
|
+
* Also disconnects any active connection.
|
|
589
|
+
*
|
|
590
|
+
* Once close is called, this connection cannot be used again - a new one
|
|
591
|
+
* must be constructed.
|
|
592
|
+
*/
|
|
593
|
+
async close(options: PowerSyncCloseOptions = DEFAULT_POWERSYNC_CLOSE_OPTIONS) {
|
|
594
|
+
await this.waitForReady();
|
|
595
|
+
|
|
596
|
+
if (this.closed) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
await this.iterateAsyncListeners(async (cb) => cb.closing?.());
|
|
601
|
+
|
|
602
|
+
const { disconnect } = options;
|
|
603
|
+
if (disconnect) {
|
|
604
|
+
await this.disconnect();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
await this.connectionManager.close();
|
|
608
|
+
await this.database.close();
|
|
609
|
+
this.closed = true;
|
|
610
|
+
await this.iterateAsyncListeners(async (cb) => cb.closed?.());
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Get upload queue size estimate and count.
|
|
615
|
+
*/
|
|
616
|
+
async getUploadQueueStats(includeSize?: boolean): Promise<UploadQueueStats> {
|
|
617
|
+
return this.readTransaction(async (tx) => {
|
|
618
|
+
if (includeSize) {
|
|
619
|
+
const result = await tx.execute(
|
|
620
|
+
`SELECT SUM(cast(data as blob) + 20) as size, count(*) as count FROM ${PSInternalTable.CRUD}`
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
const row = result.rows!.item(0);
|
|
624
|
+
return new UploadQueueStats(row?.count ?? 0, row?.size ?? 0);
|
|
625
|
+
} else {
|
|
626
|
+
const result = await tx.execute(`SELECT count(*) as count FROM ${PSInternalTable.CRUD}`);
|
|
627
|
+
const row = result.rows!.item(0);
|
|
628
|
+
return new UploadQueueStats(row?.count ?? 0);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Get a batch of CRUD data to upload.
|
|
635
|
+
*
|
|
636
|
+
* Returns null if there is no data to upload.
|
|
637
|
+
*
|
|
638
|
+
* Use this from the {@link PowerSyncBackendConnector.uploadData} callback.
|
|
639
|
+
*
|
|
640
|
+
* Once the data have been successfully uploaded, call {@link CrudBatch.complete} before
|
|
641
|
+
* requesting the next batch.
|
|
642
|
+
*
|
|
643
|
+
* Use {@link limit} to specify the maximum number of updates to return in a single
|
|
644
|
+
* batch.
|
|
645
|
+
*
|
|
646
|
+
* This method does include transaction ids in the result, but does not group
|
|
647
|
+
* data by transaction. One batch may contain data from multiple transactions,
|
|
648
|
+
* and a single transaction may be split over multiple batches.
|
|
649
|
+
*
|
|
650
|
+
* @param limit Maximum number of CRUD entries to include in the batch
|
|
651
|
+
* @returns A batch of CRUD operations to upload, or null if there are none
|
|
652
|
+
*/
|
|
653
|
+
async getCrudBatch(limit: number = DEFAULT_CRUD_BATCH_LIMIT): Promise<CrudBatch | null> {
|
|
654
|
+
const result = await this.getAll<CrudEntryJSON>(
|
|
655
|
+
`SELECT id, tx_id, data FROM ${PSInternalTable.CRUD} ORDER BY id ASC LIMIT ?`,
|
|
656
|
+
[limit + 1]
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
const all: CrudEntry[] = result.map((row) => CrudEntry.fromRow(row)) ?? [];
|
|
660
|
+
|
|
661
|
+
let haveMore = false;
|
|
662
|
+
if (all.length > limit) {
|
|
663
|
+
all.pop();
|
|
664
|
+
haveMore = true;
|
|
665
|
+
}
|
|
666
|
+
if (all.length == 0) {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const last = all[all.length - 1];
|
|
671
|
+
return new CrudBatch(all, haveMore, async (writeCheckpoint?: string) =>
|
|
672
|
+
this.handleCrudCheckpoint(last.clientId, writeCheckpoint)
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Get the next recorded transaction to upload.
|
|
678
|
+
*
|
|
679
|
+
* Returns null if there is no data to upload.
|
|
680
|
+
*
|
|
681
|
+
* Use this from the {@link PowerSyncBackendConnector.uploadData} callback.
|
|
682
|
+
*
|
|
683
|
+
* Once the data have been successfully uploaded, call {@link CrudTransaction.complete} before
|
|
684
|
+
* requesting the next transaction.
|
|
685
|
+
*
|
|
686
|
+
* Unlike {@link getCrudBatch}, this only returns data from a single transaction at a time.
|
|
687
|
+
* All data for the transaction is loaded into memory.
|
|
688
|
+
*
|
|
689
|
+
* @returns A transaction of CRUD operations to upload, or null if there are none
|
|
690
|
+
*/
|
|
691
|
+
async getNextCrudTransaction(): Promise<CrudTransaction | null> {
|
|
692
|
+
const iterator = this.getCrudTransactions()[symbolAsyncIterator]();
|
|
693
|
+
return (await iterator.next()).value;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Returns an async iterator of completed transactions with local writes against the database.
|
|
698
|
+
*
|
|
699
|
+
* This is typically used from the {@link PowerSyncBackendConnector.uploadData} callback. Each entry emitted by the
|
|
700
|
+
* returned iterator is a full transaction containing all local writes made while that transaction was active.
|
|
701
|
+
*
|
|
702
|
+
* Unlike {@link getNextCrudTransaction}, which always returns the oldest transaction that hasn't been
|
|
703
|
+
* {@link CrudTransaction.complete}d yet, this iterator can be used to receive multiple transactions. Calling
|
|
704
|
+
* {@link CrudTransaction.complete} will mark that and all prior transactions emitted by the iterator as completed.
|
|
705
|
+
*
|
|
706
|
+
* This can be used to upload multiple transactions in a single batch, e.g with:
|
|
707
|
+
*
|
|
708
|
+
* ```JavaScript
|
|
709
|
+
* let lastTransaction = null;
|
|
710
|
+
* let batch = [];
|
|
711
|
+
*
|
|
712
|
+
* for await (const transaction of database.getCrudTransactions()) {
|
|
713
|
+
* batch.push(...transaction.crud);
|
|
714
|
+
* lastTransaction = transaction;
|
|
715
|
+
*
|
|
716
|
+
* if (batch.length > 10) {
|
|
717
|
+
* break;
|
|
718
|
+
* }
|
|
719
|
+
* }
|
|
720
|
+
* ```
|
|
721
|
+
*
|
|
722
|
+
* If there is no local data to upload, the async iterator complete without emitting any items.
|
|
723
|
+
*
|
|
724
|
+
* Note that iterating over async iterables requires a [polyfill](https://github.com/powersync-ja/powersync-js/tree/main/packages/react-native#babel-plugins-watched-queries)
|
|
725
|
+
* for React Native.
|
|
726
|
+
*/
|
|
727
|
+
getCrudTransactions(): AsyncIterable<CrudTransaction, null> {
|
|
728
|
+
return {
|
|
729
|
+
[symbolAsyncIterator]: () => {
|
|
730
|
+
let lastCrudItemId = -1;
|
|
731
|
+
const sql = `
|
|
732
|
+
WITH RECURSIVE crud_entries AS (
|
|
733
|
+
SELECT id, tx_id, data FROM ps_crud WHERE id = (SELECT min(id) FROM ps_crud WHERE id > ?)
|
|
734
|
+
UNION ALL
|
|
735
|
+
SELECT ps_crud.id, ps_crud.tx_id, ps_crud.data FROM ps_crud
|
|
736
|
+
INNER JOIN crud_entries ON crud_entries.id + 1 = rowid
|
|
737
|
+
WHERE crud_entries.tx_id = ps_crud.tx_id
|
|
738
|
+
)
|
|
739
|
+
SELECT * FROM crud_entries;
|
|
740
|
+
`;
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
next: async () => {
|
|
744
|
+
const nextTransaction = await this.database.getAll<CrudEntryJSON>(sql, [lastCrudItemId]);
|
|
745
|
+
if (nextTransaction.length == 0) {
|
|
746
|
+
return { done: true, value: null };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const items = nextTransaction.map((row) => CrudEntry.fromRow(row));
|
|
750
|
+
const last = items[items.length - 1];
|
|
751
|
+
const txId = last.transactionId;
|
|
752
|
+
lastCrudItemId = last.clientId;
|
|
753
|
+
|
|
754
|
+
return {
|
|
755
|
+
done: false,
|
|
756
|
+
value: new CrudTransaction(
|
|
757
|
+
items,
|
|
758
|
+
async (writeCheckpoint?: string) => this.handleCrudCheckpoint(last.clientId, writeCheckpoint),
|
|
759
|
+
txId
|
|
760
|
+
)
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Get an unique client id for this database.
|
|
770
|
+
*
|
|
771
|
+
* The id is not reset when the database is cleared, only when the database is deleted.
|
|
772
|
+
*
|
|
773
|
+
* @returns A unique identifier for the database instance
|
|
774
|
+
*/
|
|
775
|
+
async getClientId(): Promise<string> {
|
|
776
|
+
return this.bucketStorageAdapter.getClientId();
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
private async handleCrudCheckpoint(lastClientId: number, writeCheckpoint?: string) {
|
|
780
|
+
return this.writeTransaction(async (tx) => {
|
|
781
|
+
await tx.execute(`DELETE FROM ${PSInternalTable.CRUD} WHERE id <= ?`, [lastClientId]);
|
|
782
|
+
if (writeCheckpoint) {
|
|
783
|
+
const check = await tx.execute(`SELECT 1 FROM ${PSInternalTable.CRUD} LIMIT 1`);
|
|
784
|
+
if (!check.rows?.length) {
|
|
785
|
+
await tx.execute(`UPDATE ${PSInternalTable.BUCKETS} SET target_op = CAST(? as INTEGER) WHERE name='$local'`, [
|
|
786
|
+
writeCheckpoint
|
|
787
|
+
]);
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
await tx.execute(`UPDATE ${PSInternalTable.BUCKETS} SET target_op = CAST(? as INTEGER) WHERE name='$local'`, [
|
|
791
|
+
this.bucketStorageAdapter.getMaxOpId()
|
|
792
|
+
]);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Execute a SQL write (INSERT/UPDATE/DELETE) query
|
|
799
|
+
* and optionally return results.
|
|
800
|
+
*
|
|
801
|
+
* @param sql The SQL query to execute
|
|
802
|
+
* @param parameters Optional array of parameters to bind to the query
|
|
803
|
+
* @returns The query result as an object with structured key-value pairs
|
|
804
|
+
*/
|
|
805
|
+
async execute(sql: string, parameters?: any[]) {
|
|
806
|
+
return this.writeLock((tx) => tx.execute(sql, parameters));
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Execute a SQL write (INSERT/UPDATE/DELETE) query directly on the database without any PowerSync processing.
|
|
811
|
+
* This bypasses certain PowerSync abstractions and is useful for accessing the raw database results.
|
|
812
|
+
*
|
|
813
|
+
* @param sql The SQL query to execute
|
|
814
|
+
* @param parameters Optional array of parameters to bind to the query
|
|
815
|
+
* @returns The raw query result from the underlying database as a nested array of raw values, where each row is
|
|
816
|
+
* represented as an array of column values without field names.
|
|
817
|
+
*/
|
|
818
|
+
async executeRaw(sql: string, parameters?: any[]) {
|
|
819
|
+
await this.waitForReady();
|
|
820
|
+
return this.database.executeRaw(sql, parameters);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Execute a write query (INSERT/UPDATE/DELETE) multiple times with each parameter set
|
|
825
|
+
* and optionally return results.
|
|
826
|
+
* This is faster than executing separately with each parameter set.
|
|
827
|
+
*
|
|
828
|
+
* @param sql The SQL query to execute
|
|
829
|
+
* @param parameters Optional 2D array of parameter sets, where each inner array is a set of parameters for one execution
|
|
830
|
+
* @returns The query result
|
|
831
|
+
*/
|
|
832
|
+
async executeBatch(sql: string, parameters?: any[][]) {
|
|
833
|
+
await this.waitForReady();
|
|
834
|
+
return this.database.executeBatch(sql, parameters);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Execute a read-only query and return results.
|
|
839
|
+
*
|
|
840
|
+
* @param sql The SQL query to execute
|
|
841
|
+
* @param parameters Optional array of parameters to bind to the query
|
|
842
|
+
* @returns An array of results
|
|
843
|
+
*/
|
|
844
|
+
async getAll<T>(sql: string, parameters?: any[]): Promise<T[]> {
|
|
845
|
+
await this.waitForReady();
|
|
846
|
+
return this.database.getAll(sql, parameters);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Execute a read-only query and return the first result, or null if the ResultSet is empty.
|
|
851
|
+
*
|
|
852
|
+
* @param sql The SQL query to execute
|
|
853
|
+
* @param parameters Optional array of parameters to bind to the query
|
|
854
|
+
* @returns The first result if found, or null if no results are returned
|
|
855
|
+
*/
|
|
856
|
+
async getOptional<T>(sql: string, parameters?: any[]): Promise<T | null> {
|
|
857
|
+
await this.waitForReady();
|
|
858
|
+
return this.database.getOptional(sql, parameters);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Execute a read-only query and return the first result, error if the ResultSet is empty.
|
|
863
|
+
*
|
|
864
|
+
* @param sql The SQL query to execute
|
|
865
|
+
* @param parameters Optional array of parameters to bind to the query
|
|
866
|
+
* @returns The first result matching the query
|
|
867
|
+
* @throws Error if no rows are returned
|
|
868
|
+
*/
|
|
869
|
+
async get<T>(sql: string, parameters?: any[]): Promise<T> {
|
|
870
|
+
await this.waitForReady();
|
|
871
|
+
return this.database.get(sql, parameters);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Takes a read lock, without starting a transaction.
|
|
876
|
+
* In most cases, {@link readTransaction} should be used instead.
|
|
877
|
+
*/
|
|
878
|
+
async readLock<T>(callback: (db: DBAdapter) => Promise<T>) {
|
|
879
|
+
await this.waitForReady();
|
|
880
|
+
return this.database.readLock(callback);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Takes a global lock, without starting a transaction.
|
|
885
|
+
* In most cases, {@link writeTransaction} should be used instead.
|
|
886
|
+
*/
|
|
887
|
+
async writeLock<T>(callback: (db: DBAdapter) => Promise<T>) {
|
|
888
|
+
await this.waitForReady();
|
|
889
|
+
return this.database.writeLock(callback);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Open a read-only transaction.
|
|
894
|
+
* Read transactions can run concurrently to a write transaction.
|
|
895
|
+
* Changes from any write transaction are not visible to read transactions started before it.
|
|
896
|
+
*
|
|
897
|
+
* @param callback Function to execute within the transaction
|
|
898
|
+
* @param lockTimeout Time in milliseconds to wait for a lock before throwing an error
|
|
899
|
+
* @returns The result of the callback
|
|
900
|
+
* @throws Error if the lock cannot be obtained within the timeout period
|
|
901
|
+
*/
|
|
902
|
+
async readTransaction<T>(
|
|
903
|
+
callback: (tx: Transaction) => Promise<T>,
|
|
904
|
+
lockTimeout: number = DEFAULT_LOCK_TIMEOUT_MS
|
|
905
|
+
): Promise<T> {
|
|
906
|
+
await this.waitForReady();
|
|
907
|
+
return this.database.readTransaction(
|
|
908
|
+
async (tx) => {
|
|
909
|
+
const res = await callback({ ...tx });
|
|
910
|
+
await tx.rollback();
|
|
911
|
+
return res;
|
|
912
|
+
},
|
|
913
|
+
{ timeoutMs: lockTimeout }
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Open a read-write transaction.
|
|
919
|
+
* This takes a global lock - only one write transaction can execute against the database at a time.
|
|
920
|
+
* Statements within the transaction must be done on the provided {@link Transaction} interface.
|
|
921
|
+
*
|
|
922
|
+
* @param callback Function to execute within the transaction
|
|
923
|
+
* @param lockTimeout Time in milliseconds to wait for a lock before throwing an error
|
|
924
|
+
* @returns The result of the callback
|
|
925
|
+
* @throws Error if the lock cannot be obtained within the timeout period
|
|
926
|
+
*/
|
|
927
|
+
async writeTransaction<T>(
|
|
928
|
+
callback: (tx: Transaction) => Promise<T>,
|
|
929
|
+
lockTimeout: number = DEFAULT_LOCK_TIMEOUT_MS
|
|
930
|
+
): Promise<T> {
|
|
931
|
+
await this.waitForReady();
|
|
932
|
+
return this.database.writeTransaction(
|
|
933
|
+
async (tx) => {
|
|
934
|
+
const res = await callback(tx);
|
|
935
|
+
await tx.commit();
|
|
936
|
+
return res;
|
|
937
|
+
},
|
|
938
|
+
{ timeoutMs: lockTimeout }
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* This version of `watch` uses {@link AsyncGenerator}, for documentation see {@link watchWithAsyncGenerator}.
|
|
944
|
+
* Can be overloaded to use a callback handler instead, for documentation see {@link watchWithCallback}.
|
|
945
|
+
*
|
|
946
|
+
* @example
|
|
947
|
+
* ```javascript
|
|
948
|
+
* async *attachmentIds() {
|
|
949
|
+
* for await (const result of this.powersync.watch(
|
|
950
|
+
* `SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL`,
|
|
951
|
+
* []
|
|
952
|
+
* )) {
|
|
953
|
+
* yield result.rows?._array.map((r) => r.id) ?? [];
|
|
954
|
+
* }
|
|
955
|
+
* }
|
|
956
|
+
* ```
|
|
957
|
+
*/
|
|
958
|
+
watch(sql: string, parameters?: any[], options?: SQLWatchOptions): AsyncIterable<QueryResult>;
|
|
959
|
+
/**
|
|
960
|
+
* See {@link watchWithCallback}.
|
|
961
|
+
*
|
|
962
|
+
* @example
|
|
963
|
+
* ```javascript
|
|
964
|
+
* onAttachmentIdsChange(onResult) {
|
|
965
|
+
* this.powersync.watch(
|
|
966
|
+
* `SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL`,
|
|
967
|
+
* [],
|
|
968
|
+
* {
|
|
969
|
+
* onResult: (result) => onResult(result.rows?._array.map((r) => r.id) ?? [])
|
|
970
|
+
* }
|
|
971
|
+
* );
|
|
972
|
+
* }
|
|
973
|
+
* ```
|
|
974
|
+
*/
|
|
975
|
+
watch(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void;
|
|
976
|
+
|
|
977
|
+
watch(
|
|
978
|
+
sql: string,
|
|
979
|
+
parameters?: any[],
|
|
980
|
+
handlerOrOptions?: WatchHandler | SQLWatchOptions,
|
|
981
|
+
maybeOptions?: SQLWatchOptions
|
|
982
|
+
): void | AsyncIterable<QueryResult> {
|
|
983
|
+
if (handlerOrOptions && typeof handlerOrOptions === 'object' && 'onResult' in handlerOrOptions) {
|
|
984
|
+
const handler = handlerOrOptions as WatchHandler;
|
|
985
|
+
const options = maybeOptions;
|
|
986
|
+
|
|
987
|
+
return this.watchWithCallback(sql, parameters, handler, options);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const options = handlerOrOptions as SQLWatchOptions | undefined;
|
|
991
|
+
return this.watchWithAsyncGenerator(sql, parameters, options);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Allows defining a query which can be used to build a {@link WatchedQuery}.
|
|
996
|
+
* The defined query will be executed with {@link AbstractPowerSyncDatabase#getAll}.
|
|
997
|
+
* An optional mapper function can be provided to transform the results.
|
|
998
|
+
*
|
|
999
|
+
* @example
|
|
1000
|
+
* ```javascript
|
|
1001
|
+
* const watchedTodos = powersync.query({
|
|
1002
|
+
* sql: `SELECT photo_id as id FROM todos WHERE photo_id IS NOT NULL`,
|
|
1003
|
+
* parameters: [],
|
|
1004
|
+
* mapper: (row) => ({
|
|
1005
|
+
* ...row,
|
|
1006
|
+
* created_at: new Date(row.created_at as string)
|
|
1007
|
+
* })
|
|
1008
|
+
* })
|
|
1009
|
+
* .watch()
|
|
1010
|
+
* // OR use .differentialWatch() for fine-grained watches.
|
|
1011
|
+
* ```
|
|
1012
|
+
*/
|
|
1013
|
+
query<RowType>(query: ArrayQueryDefinition<RowType>): Query<RowType> {
|
|
1014
|
+
const { sql, parameters = [], mapper } = query;
|
|
1015
|
+
const compatibleQuery: WatchCompatibleQuery<RowType[]> = {
|
|
1016
|
+
compile: () => ({
|
|
1017
|
+
sql,
|
|
1018
|
+
parameters
|
|
1019
|
+
}),
|
|
1020
|
+
execute: async ({ sql, parameters }) => {
|
|
1021
|
+
const result = await this.getAll(sql, parameters);
|
|
1022
|
+
return mapper ? result.map(mapper) : (result as RowType[]);
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
return this.customQuery(compatibleQuery);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Allows building a {@link WatchedQuery} using an existing {@link WatchCompatibleQuery}.
|
|
1030
|
+
* The watched query will use the provided {@link WatchCompatibleQuery.execute} method to query results.
|
|
1031
|
+
*
|
|
1032
|
+
* @example
|
|
1033
|
+
* ```javascript
|
|
1034
|
+
*
|
|
1035
|
+
* // Potentially a query from an ORM like Drizzle
|
|
1036
|
+
* const query = db.select().from(lists);
|
|
1037
|
+
*
|
|
1038
|
+
* const watchedTodos = powersync.customQuery(query)
|
|
1039
|
+
* .watch()
|
|
1040
|
+
* // OR use .differentialWatch() for fine-grained watches.
|
|
1041
|
+
* ```
|
|
1042
|
+
*/
|
|
1043
|
+
customQuery<RowType>(query: WatchCompatibleQuery<RowType[]>): Query<RowType> {
|
|
1044
|
+
return new CustomQuery({
|
|
1045
|
+
db: this,
|
|
1046
|
+
query
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Execute a read query every time the source tables are modified.
|
|
1052
|
+
* Use {@link SQLWatchOptions.throttleMs} to specify the minimum interval between queries.
|
|
1053
|
+
* Source tables are automatically detected using `EXPLAIN QUERY PLAN`.
|
|
1054
|
+
*
|
|
1055
|
+
* Note that the `onChange` callback member of the handler is required.
|
|
1056
|
+
*
|
|
1057
|
+
* @param sql The SQL query to execute
|
|
1058
|
+
* @param parameters Optional array of parameters to bind to the query
|
|
1059
|
+
* @param handler Callbacks for handling results and errors
|
|
1060
|
+
* @param options Options for configuring watch behavior
|
|
1061
|
+
*/
|
|
1062
|
+
watchWithCallback(sql: string, parameters?: any[], handler?: WatchHandler, options?: SQLWatchOptions): void {
|
|
1063
|
+
const { onResult, onError = (e: Error) => this.logger.error(e) } = handler ?? {};
|
|
1064
|
+
if (!onResult) {
|
|
1065
|
+
throw new Error('onResult is required');
|
|
1066
|
+
}
|
|
1067
|
+
const { comparator } = options ?? {};
|
|
1068
|
+
|
|
1069
|
+
// This API yields a QueryResult type.
|
|
1070
|
+
// This is not a standard Array result, which makes it incompatible with the .query API.
|
|
1071
|
+
const watchedQuery = new OnChangeQueryProcessor({
|
|
1072
|
+
db: this,
|
|
1073
|
+
comparator,
|
|
1074
|
+
placeholderData: null,
|
|
1075
|
+
watchOptions: {
|
|
1076
|
+
query: {
|
|
1077
|
+
compile: () => ({
|
|
1078
|
+
sql: sql,
|
|
1079
|
+
parameters: parameters ?? []
|
|
1080
|
+
}),
|
|
1081
|
+
execute: () => this.executeReadOnly(sql, parameters)
|
|
1082
|
+
},
|
|
1083
|
+
reportFetching: false,
|
|
1084
|
+
throttleMs: options?.throttleMs ?? DEFAULT_WATCH_THROTTLE_MS,
|
|
1085
|
+
triggerOnTables: options?.tables
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
const dispose = watchedQuery.registerListener({
|
|
1090
|
+
onData: (data) => {
|
|
1091
|
+
if (!data) {
|
|
1092
|
+
// This should not happen. We only use null for the initial data.
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
onResult(data);
|
|
1096
|
+
},
|
|
1097
|
+
onError: (error) => {
|
|
1098
|
+
onError(error);
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
options?.signal?.addEventListener('abort', () => {
|
|
1103
|
+
dispose();
|
|
1104
|
+
watchedQuery.close();
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Execute a read query every time the source tables are modified.
|
|
1110
|
+
* Use {@link SQLWatchOptions.throttleMs} to specify the minimum interval between queries.
|
|
1111
|
+
* Source tables are automatically detected using `EXPLAIN QUERY PLAN`.
|
|
1112
|
+
*
|
|
1113
|
+
* @param sql The SQL query to execute
|
|
1114
|
+
* @param parameters Optional array of parameters to bind to the query
|
|
1115
|
+
* @param options Options for configuring watch behavior
|
|
1116
|
+
* @returns An AsyncIterable that yields QueryResults whenever the data changes
|
|
1117
|
+
*/
|
|
1118
|
+
watchWithAsyncGenerator(sql: string, parameters?: any[], options?: SQLWatchOptions): AsyncIterable<QueryResult> {
|
|
1119
|
+
return new EventIterator<QueryResult>((eventOptions) => {
|
|
1120
|
+
const handler: WatchHandler = {
|
|
1121
|
+
onResult: (result) => {
|
|
1122
|
+
eventOptions.push(result);
|
|
1123
|
+
},
|
|
1124
|
+
onError: (error) => {
|
|
1125
|
+
eventOptions.fail(error);
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
this.watchWithCallback(sql, parameters, handler, options);
|
|
1130
|
+
|
|
1131
|
+
options?.signal?.addEventListener('abort', () => {
|
|
1132
|
+
eventOptions.stop();
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Resolves the list of tables that are used in a SQL query.
|
|
1139
|
+
* If tables are specified in the options, those are used directly.
|
|
1140
|
+
* Otherwise, analyzes the query using EXPLAIN to determine which tables are accessed.
|
|
1141
|
+
*
|
|
1142
|
+
* @param sql The SQL query to analyze
|
|
1143
|
+
* @param parameters Optional parameters for the SQL query
|
|
1144
|
+
* @param options Optional watch options that may contain explicit table list
|
|
1145
|
+
* @returns Array of table names that the query depends on
|
|
1146
|
+
*/
|
|
1147
|
+
async resolveTables(sql: string, parameters?: any[], options?: SQLWatchOptions): Promise<string[]> {
|
|
1148
|
+
const resolvedTables = options?.tables ? [...options.tables] : [];
|
|
1149
|
+
if (!options?.tables) {
|
|
1150
|
+
const explained = await this.getAll<{ opcode: string; p3: number; p2: number }>(`EXPLAIN ${sql}`, parameters);
|
|
1151
|
+
const rootPages = explained
|
|
1152
|
+
.filter((row) => row.opcode == 'OpenRead' && row.p3 == 0 && typeof row.p2 == 'number')
|
|
1153
|
+
.map((row) => row.p2);
|
|
1154
|
+
const tables = await this.getAll<{ tbl_name: string }>(
|
|
1155
|
+
`SELECT DISTINCT tbl_name FROM sqlite_master WHERE rootpage IN (SELECT json_each.value FROM json_each(?))`,
|
|
1156
|
+
[JSON.stringify(rootPages)]
|
|
1157
|
+
);
|
|
1158
|
+
for (const table of tables) {
|
|
1159
|
+
resolvedTables.push(table.tbl_name.replace(POWERSYNC_TABLE_MATCH, ''));
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return resolvedTables;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* This version of `onChange` uses {@link AsyncGenerator}, for documentation see {@link onChangeWithAsyncGenerator}.
|
|
1168
|
+
* Can be overloaded to use a callback handler instead, for documentation see {@link onChangeWithCallback}.
|
|
1169
|
+
*
|
|
1170
|
+
* @example
|
|
1171
|
+
* ```javascript
|
|
1172
|
+
* async monitorChanges() {
|
|
1173
|
+
* for await (const event of this.powersync.onChange({tables: ['todos']})) {
|
|
1174
|
+
* console.log('Detected change event:', event);
|
|
1175
|
+
* }
|
|
1176
|
+
* }
|
|
1177
|
+
* ```
|
|
1178
|
+
*/
|
|
1179
|
+
onChange(options?: SQLOnChangeOptions): AsyncIterable<WatchOnChangeEvent>;
|
|
1180
|
+
/**
|
|
1181
|
+
* See {@link onChangeWithCallback}.
|
|
1182
|
+
*
|
|
1183
|
+
* @example
|
|
1184
|
+
* ```javascript
|
|
1185
|
+
* monitorChanges() {
|
|
1186
|
+
* this.powersync.onChange({
|
|
1187
|
+
* onChange: (event) => {
|
|
1188
|
+
* console.log('Change detected:', event);
|
|
1189
|
+
* }
|
|
1190
|
+
* }, { tables: ['todos'] });
|
|
1191
|
+
* }
|
|
1192
|
+
* ```
|
|
1193
|
+
*/
|
|
1194
|
+
onChange(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void;
|
|
1195
|
+
|
|
1196
|
+
onChange(
|
|
1197
|
+
handlerOrOptions?: WatchOnChangeHandler | SQLOnChangeOptions,
|
|
1198
|
+
maybeOptions?: SQLOnChangeOptions
|
|
1199
|
+
): (() => void) | AsyncIterable<WatchOnChangeEvent> {
|
|
1200
|
+
if (handlerOrOptions && typeof handlerOrOptions === 'object' && 'onChange' in handlerOrOptions) {
|
|
1201
|
+
const handler = handlerOrOptions as WatchOnChangeHandler;
|
|
1202
|
+
const options = maybeOptions;
|
|
1203
|
+
|
|
1204
|
+
return this.onChangeWithCallback(handler, options);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const options = handlerOrOptions as SQLWatchOptions | undefined;
|
|
1208
|
+
return this.onChangeWithAsyncGenerator(options);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Invoke the provided callback on any changes to any of the specified tables.
|
|
1213
|
+
*
|
|
1214
|
+
* This is preferred over {@link watchWithCallback} when multiple queries need to be performed
|
|
1215
|
+
* together when data is changed.
|
|
1216
|
+
*
|
|
1217
|
+
* Note that the `onChange` callback member of the handler is required.
|
|
1218
|
+
*
|
|
1219
|
+
* @param handler Callbacks for handling change events and errors
|
|
1220
|
+
* @param options Options for configuring watch behavior
|
|
1221
|
+
* @returns A dispose function to stop watching for changes
|
|
1222
|
+
*/
|
|
1223
|
+
onChangeWithCallback(handler?: WatchOnChangeHandler, options?: SQLOnChangeOptions): () => void {
|
|
1224
|
+
const { onChange, onError = (e: Error) => this.logger.error(e) } = handler ?? {};
|
|
1225
|
+
if (!onChange) {
|
|
1226
|
+
throw new Error('onChange is required');
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const resolvedOptions = options ?? {};
|
|
1230
|
+
const watchedTables = new Set<string>(
|
|
1231
|
+
(resolvedOptions?.tables ?? []).flatMap((table) => [table, `ps_data__${table}`, `ps_data_local__${table}`])
|
|
1232
|
+
);
|
|
1233
|
+
|
|
1234
|
+
const changedTables = new Set<string>();
|
|
1235
|
+
const throttleMs = resolvedOptions.throttleMs ?? DEFAULT_WATCH_THROTTLE_MS;
|
|
1236
|
+
|
|
1237
|
+
const executor = new ControlledExecutor(async (e: WatchOnChangeEvent) => {
|
|
1238
|
+
await onChange(e);
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
const flushTableUpdates = throttleTrailing(
|
|
1242
|
+
() =>
|
|
1243
|
+
this.handleTableChanges(changedTables, watchedTables, (intersection) => {
|
|
1244
|
+
if (resolvedOptions?.signal?.aborted) return;
|
|
1245
|
+
executor.schedule({ changedTables: intersection });
|
|
1246
|
+
}),
|
|
1247
|
+
throttleMs
|
|
1248
|
+
);
|
|
1249
|
+
|
|
1250
|
+
if (options?.triggerImmediate) {
|
|
1251
|
+
executor.schedule({ changedTables: [] });
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const dispose = this.database.registerListener({
|
|
1255
|
+
tablesUpdated: async (update) => {
|
|
1256
|
+
try {
|
|
1257
|
+
this.processTableUpdates(update, changedTables);
|
|
1258
|
+
flushTableUpdates();
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
onError?.(error);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
resolvedOptions.signal?.addEventListener('abort', () => {
|
|
1266
|
+
executor.dispose();
|
|
1267
|
+
dispose();
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
return () => dispose();
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Create a Stream of changes to any of the specified tables.
|
|
1275
|
+
*
|
|
1276
|
+
* This is preferred over {@link watchWithAsyncGenerator} when multiple queries need to be performed
|
|
1277
|
+
* together when data is changed.
|
|
1278
|
+
*
|
|
1279
|
+
* Note: do not declare this as `async *onChange` as it will not work in React Native.
|
|
1280
|
+
*
|
|
1281
|
+
* @param options Options for configuring watch behavior
|
|
1282
|
+
* @returns An AsyncIterable that yields change events whenever the specified tables change
|
|
1283
|
+
*/
|
|
1284
|
+
onChangeWithAsyncGenerator(options?: SQLWatchOptions): AsyncIterable<WatchOnChangeEvent> {
|
|
1285
|
+
const resolvedOptions = options ?? {};
|
|
1286
|
+
|
|
1287
|
+
return new EventIterator<WatchOnChangeEvent>((eventOptions) => {
|
|
1288
|
+
const dispose = this.onChangeWithCallback(
|
|
1289
|
+
{
|
|
1290
|
+
onChange: (event): void => {
|
|
1291
|
+
eventOptions.push(event);
|
|
1292
|
+
},
|
|
1293
|
+
onError: (error) => {
|
|
1294
|
+
eventOptions.fail(error);
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1297
|
+
options
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
resolvedOptions.signal?.addEventListener('abort', () => {
|
|
1301
|
+
eventOptions.stop();
|
|
1302
|
+
// Maybe fail?
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
return () => dispose();
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
private handleTableChanges(
|
|
1310
|
+
changedTables: Set<string>,
|
|
1311
|
+
watchedTables: Set<string>,
|
|
1312
|
+
onDetectedChanges: (changedTables: string[]) => void
|
|
1313
|
+
): void {
|
|
1314
|
+
if (changedTables.size > 0) {
|
|
1315
|
+
const intersection = Array.from(changedTables.values()).filter((change) => watchedTables.has(change));
|
|
1316
|
+
if (intersection.length) {
|
|
1317
|
+
onDetectedChanges(intersection);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
changedTables.clear();
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
private processTableUpdates(
|
|
1324
|
+
updateNotification: BatchedUpdateNotification | UpdateNotification,
|
|
1325
|
+
changedTables: Set<string>
|
|
1326
|
+
): void {
|
|
1327
|
+
const tables = isBatchedUpdateNotification(updateNotification)
|
|
1328
|
+
? updateNotification.tables
|
|
1329
|
+
: [updateNotification.table];
|
|
1330
|
+
|
|
1331
|
+
for (const table of tables) {
|
|
1332
|
+
changedTables.add(table);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* @ignore
|
|
1338
|
+
*/
|
|
1339
|
+
private async executeReadOnly(sql: string, params?: any[]) {
|
|
1340
|
+
await this.waitForReady();
|
|
1341
|
+
return this.database.readLock((tx) => tx.execute(sql, params));
|
|
1342
|
+
}
|
|
1343
|
+
}
|