@powersync/common 1.48.0 → 1.50.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/dist/bundle.cjs +270 -46
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.mjs +265 -44
- package/dist/bundle.mjs.map +1 -1
- package/dist/bundle.node.cjs +270 -45
- package/dist/bundle.node.cjs.map +1 -1
- package/dist/bundle.node.mjs +265 -43
- package/dist/bundle.node.mjs.map +1 -1
- package/dist/index.d.cts +108 -26
- package/lib/attachments/AttachmentQueue.d.ts +10 -4
- package/lib/attachments/AttachmentQueue.js +10 -4
- package/lib/attachments/AttachmentQueue.js.map +1 -1
- package/lib/attachments/AttachmentService.js +2 -3
- package/lib/attachments/AttachmentService.js.map +1 -1
- package/lib/attachments/SyncingService.d.ts +2 -1
- package/lib/attachments/SyncingService.js +4 -5
- package/lib/attachments/SyncingService.js.map +1 -1
- package/lib/client/AbstractPowerSyncDatabase.d.ts +5 -1
- package/lib/client/AbstractPowerSyncDatabase.js +6 -2
- package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
- package/lib/client/triggers/TriggerManager.d.ts +13 -1
- package/lib/client/triggers/TriggerManagerImpl.d.ts +2 -2
- package/lib/client/triggers/TriggerManagerImpl.js +19 -7
- package/lib/client/triggers/TriggerManagerImpl.js.map +1 -1
- package/lib/db/DBAdapter.d.ts +55 -9
- package/lib/db/DBAdapter.js +126 -0
- package/lib/db/DBAdapter.js.map +1 -1
- package/lib/utils/mutex.d.ts +18 -5
- package/lib/utils/mutex.js +97 -21
- package/lib/utils/mutex.js.map +1 -1
- package/package.json +1 -2
- package/src/attachments/AttachmentQueue.ts +10 -4
- package/src/attachments/AttachmentService.ts +2 -3
- package/src/attachments/README.md +6 -4
- package/src/attachments/SyncingService.ts +4 -5
- package/src/client/AbstractPowerSyncDatabase.ts +6 -2
- package/src/client/triggers/TriggerManager.ts +15 -2
- package/src/client/triggers/TriggerManagerImpl.ts +18 -6
- package/src/db/DBAdapter.ts +167 -9
- package/src/utils/mutex.ts +121 -26
package/lib/utils/mutex.js
CHANGED
|
@@ -1,29 +1,105 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* An asynchronous mutex implementation.
|
|
3
|
+
*
|
|
4
|
+
* @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
|
|
3
5
|
*/
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
export class Mutex {
|
|
7
|
+
inCriticalSection = false;
|
|
8
|
+
// Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
|
|
9
|
+
// aborted waiters from the middle of the list efficiently.
|
|
10
|
+
firstWaiter;
|
|
11
|
+
lastWaiter;
|
|
12
|
+
addWaiter(onAcquire) {
|
|
13
|
+
const node = {
|
|
14
|
+
isActive: true,
|
|
15
|
+
onAcquire,
|
|
16
|
+
prev: this.lastWaiter
|
|
17
|
+
};
|
|
18
|
+
if (this.lastWaiter) {
|
|
19
|
+
this.lastWaiter.next = node;
|
|
20
|
+
this.lastWaiter = node;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
// First waiter
|
|
24
|
+
this.lastWaiter = this.firstWaiter = node;
|
|
25
|
+
}
|
|
26
|
+
return node;
|
|
27
|
+
}
|
|
28
|
+
deactivateWaiter(waiter) {
|
|
29
|
+
const { prev, next } = waiter;
|
|
30
|
+
waiter.isActive = false;
|
|
31
|
+
if (prev)
|
|
32
|
+
prev.next = next;
|
|
33
|
+
if (next)
|
|
34
|
+
next.prev = prev;
|
|
35
|
+
if (waiter == this.firstWaiter)
|
|
36
|
+
this.firstWaiter = next;
|
|
37
|
+
if (waiter == this.lastWaiter)
|
|
38
|
+
this.lastWaiter = prev;
|
|
39
|
+
}
|
|
40
|
+
acquire(abort) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
function rejectAborted() {
|
|
43
|
+
reject(abort?.reason ?? new Error('Mutex acquire aborted'));
|
|
17
44
|
}
|
|
18
|
-
if (
|
|
19
|
-
return;
|
|
20
|
-
try {
|
|
21
|
-
resolve(await callback());
|
|
45
|
+
if (abort?.aborted) {
|
|
46
|
+
return rejectAborted();
|
|
22
47
|
}
|
|
23
|
-
|
|
24
|
-
|
|
48
|
+
let holdsMutex = false;
|
|
49
|
+
const markCompleted = () => {
|
|
50
|
+
if (!holdsMutex)
|
|
51
|
+
return;
|
|
52
|
+
holdsMutex = false;
|
|
53
|
+
const waiter = this.firstWaiter;
|
|
54
|
+
if (waiter) {
|
|
55
|
+
this.deactivateWaiter(waiter);
|
|
56
|
+
// Still in critical section, but owned by next waiter now.
|
|
57
|
+
waiter.onAcquire();
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.inCriticalSection = false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
if (!this.inCriticalSection) {
|
|
64
|
+
this.inCriticalSection = true;
|
|
65
|
+
holdsMutex = true;
|
|
66
|
+
return resolve(markCompleted);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
let node;
|
|
70
|
+
const onAbort = () => {
|
|
71
|
+
abort?.removeEventListener('abort', onAbort);
|
|
72
|
+
if (node.isActive) {
|
|
73
|
+
this.deactivateWaiter(node);
|
|
74
|
+
rejectAborted();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
node = this.addWaiter(() => {
|
|
78
|
+
abort?.removeEventListener('abort', onAbort);
|
|
79
|
+
holdsMutex = true;
|
|
80
|
+
resolve(markCompleted);
|
|
81
|
+
});
|
|
82
|
+
abort?.addEventListener('abort', onAbort);
|
|
25
83
|
}
|
|
26
84
|
});
|
|
27
|
-
}
|
|
85
|
+
}
|
|
86
|
+
async runExclusive(fn, abort) {
|
|
87
|
+
const returnMutex = await this.acquire(abort);
|
|
88
|
+
try {
|
|
89
|
+
return await fn();
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
returnMutex();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function timeoutSignal(timeout) {
|
|
97
|
+
if (timeout == null)
|
|
98
|
+
return;
|
|
99
|
+
if ('timeout' in AbortSignal)
|
|
100
|
+
return AbortSignal.timeout(timeout);
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
setTimeout(() => controller.abort(new Error('Timeout waiting for lock')), timeout);
|
|
103
|
+
return controller.signal;
|
|
28
104
|
}
|
|
29
105
|
//# sourceMappingURL=mutex.js.map
|
package/lib/utils/mutex.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutex.js","sourceRoot":"","sources":["../../src/utils/mutex.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"mutex.js","sourceRoot":"","sources":["../../src/utils/mutex.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powersync/common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.50.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/",
|
|
6
6
|
"access": "public"
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
},
|
|
49
49
|
"homepage": "https://docs.powersync.com",
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"async-mutex": "^0.5.0",
|
|
52
51
|
"event-iterator": "^2.0.0"
|
|
53
52
|
},
|
|
54
53
|
"devDependencies": {
|
|
@@ -51,10 +51,16 @@ export class AttachmentQueue {
|
|
|
51
51
|
/** Logger instance for diagnostic information */
|
|
52
52
|
readonly logger: ILogger;
|
|
53
53
|
|
|
54
|
-
/** Interval in milliseconds between periodic sync operations.
|
|
54
|
+
/** Interval in milliseconds between periodic sync operations. Acts as a polling timer to retry
|
|
55
|
+
* failed uploads/downloads, especially after the app goes offline. Default: 30000 (30 seconds) */
|
|
55
56
|
readonly syncIntervalMs: number = 30 * 1000;
|
|
56
57
|
|
|
57
|
-
/**
|
|
58
|
+
/** Throttle duration in milliseconds for the reactive watch query on the attachments table.
|
|
59
|
+
* When attachment records change, a watch query detects the change and triggers a sync.
|
|
60
|
+
* This throttle prevents the sync from firing too rapidly when many changes happen in
|
|
61
|
+
* quick succession (e.g., bulk inserts). This is distinct from syncIntervalMs — it controls
|
|
62
|
+
* how quickly the queue reacts to changes, while syncIntervalMs controls how often it polls
|
|
63
|
+
* for retries. Default: 30 (from DEFAULT_WATCH_THROTTLE_MS) */
|
|
58
64
|
readonly syncThrottleDuration: number;
|
|
59
65
|
|
|
60
66
|
/** Whether to automatically download remote attachments. Default: true */
|
|
@@ -86,8 +92,8 @@ export class AttachmentQueue {
|
|
|
86
92
|
* @param options.watchAttachments - Callback for monitoring attachment changes in your data model
|
|
87
93
|
* @param options.tableName - Name of the table to store attachment records. Default: 'ps_attachment_queue'
|
|
88
94
|
* @param options.logger - Logger instance. Defaults to db.logger
|
|
89
|
-
* @param options.syncIntervalMs -
|
|
90
|
-
* @param options.syncThrottleDuration - Throttle duration for
|
|
95
|
+
* @param options.syncIntervalMs - Periodic polling interval in milliseconds for retrying failed uploads/downloads. Default: 30000
|
|
96
|
+
* @param options.syncThrottleDuration - Throttle duration in milliseconds for the reactive watch query that detects attachment changes. Prevents rapid-fire syncs during bulk changes. Default: 30
|
|
91
97
|
* @param options.downloadAttachments - Whether to automatically download remote attachments. Default: true
|
|
92
98
|
* @param options.archivedCacheLimit - Maximum archived attachments before cleanup. Default: 100
|
|
93
99
|
*/
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { Mutex } from 'async-mutex';
|
|
2
1
|
import { AbstractPowerSyncDatabase } from '../client/AbstractPowerSyncDatabase.js';
|
|
3
2
|
import { DifferentialWatchedQuery } from '../client/watched/processors/DifferentialQueryProcessor.js';
|
|
4
3
|
import { ILogger } from '../utils/Logger.js';
|
|
5
|
-
import {
|
|
4
|
+
import { Mutex } from '../utils/mutex.js';
|
|
6
5
|
import { AttachmentContext } from './AttachmentContext.js';
|
|
7
6
|
import { AttachmentRecord, AttachmentState } from './Schema.js';
|
|
8
7
|
|
|
@@ -55,7 +54,7 @@ export class AttachmentService {
|
|
|
55
54
|
* Executes a callback with exclusive access to the attachment context.
|
|
56
55
|
*/
|
|
57
56
|
async withContext<T>(callback: (context: AttachmentContext) => Promise<T>): Promise<T> {
|
|
58
|
-
return
|
|
57
|
+
return this.mutex.runExclusive(async () => {
|
|
59
58
|
return callback(this.context);
|
|
60
59
|
});
|
|
61
60
|
}
|
|
@@ -289,8 +289,8 @@ new AttachmentQueue(options: AttachmentQueueOptions)
|
|
|
289
289
|
| `watchAttachments` | `(onUpdate: (attachments: WatchedAttachmentItem[]) => Promise<void>) => void` | Yes | - | Callback to determine which attachments to handle by the queue from your user defined query |
|
|
290
290
|
| `tableName` | `string` | No | `'attachments'` | Name of the attachments table |
|
|
291
291
|
| `logger` | `ILogger` | No | `db.logger` | Logger instance for diagnostic output |
|
|
292
|
-
| `syncIntervalMs` | `number` | No | `30000` |
|
|
293
|
-
| `syncThrottleDuration` | `number` | No | `30` | Throttle duration for sync
|
|
292
|
+
| `syncIntervalMs` | `number` | No | `30000` | Periodic polling interval (in milliseconds) for retrying failed uploads/downloads. A `setInterval` timer that calls `syncStorage()` on this cadence, ensuring operations are retried even if no database changes occur (e.g., after coming back online). |
|
|
293
|
+
| `syncThrottleDuration` | `number` | No | `30` | Throttle duration (in milliseconds) for the reactive watch query on the attachments table. When attachment records change (e.g., a new file is queued), a watch query detects the change and triggers a sync. This throttle prevents the sync from firing too rapidly when many changes happen in quick succession (e.g., bulk inserts). This is distinct from `syncIntervalMs` — it controls how quickly the queue *reacts* to changes, while `syncIntervalMs` controls how often it *polls* for retries. |
|
|
294
294
|
| `downloadAttachments` | `boolean` | No | `true` | Whether to automatically download remote attachments |
|
|
295
295
|
| `archivedCacheLimit` | `number` | No | `100` | Maximum number of archived attachments before cleanup |
|
|
296
296
|
| `errorHandler` | `AttachmentErrorHandler` | No | `undefined` | Custom error handler for upload/download/delete operations |
|
|
@@ -676,11 +676,13 @@ Adjust sync frequency based on your needs:
|
|
|
676
676
|
```typescript
|
|
677
677
|
const queue = new AttachmentQueue({
|
|
678
678
|
// ... other options
|
|
679
|
-
syncIntervalMs: 60000, //
|
|
679
|
+
syncIntervalMs: 60000, // Poll for retries every 60 seconds instead of 30
|
|
680
|
+
syncThrottleDuration: 100, // React to attachment changes within 100ms (default: 30ms)
|
|
680
681
|
});
|
|
681
682
|
```
|
|
682
683
|
|
|
683
|
-
|
|
684
|
+
- **`syncIntervalMs`** controls the periodic polling timer — how often the queue retries failed operations.
|
|
685
|
+
- **`syncThrottleDuration`** controls how quickly the queue reacts to attachment table changes. The default (30ms) is fast enough for most use cases. Increase it if you see performance issues during bulk attachment operations.
|
|
684
686
|
|
|
685
687
|
### Archive and Cache Management
|
|
686
688
|
|
|
@@ -54,7 +54,7 @@ export class SyncingService {
|
|
|
54
54
|
updatedAttachments.push(downloaded);
|
|
55
55
|
break;
|
|
56
56
|
case AttachmentState.QUEUED_DELETE:
|
|
57
|
-
const deleted = await this.deleteAttachment(attachment);
|
|
57
|
+
const deleted = await this.deleteAttachment(attachment, context);
|
|
58
58
|
updatedAttachments.push(deleted);
|
|
59
59
|
break;
|
|
60
60
|
|
|
@@ -143,18 +143,17 @@ export class SyncingService {
|
|
|
143
143
|
* On failure, defers to error handler or archives.
|
|
144
144
|
*
|
|
145
145
|
* @param attachment - The attachment record to delete
|
|
146
|
+
* @param context - Attachment context for database operations
|
|
146
147
|
* @returns Updated attachment record
|
|
147
148
|
*/
|
|
148
|
-
async deleteAttachment(attachment: AttachmentRecord): Promise<AttachmentRecord> {
|
|
149
|
+
async deleteAttachment(attachment: AttachmentRecord, context: AttachmentContext): Promise<AttachmentRecord> {
|
|
149
150
|
try {
|
|
150
151
|
await this.remoteStorage.deleteFile(attachment);
|
|
151
152
|
if (attachment.localUri) {
|
|
152
153
|
await this.localStorage.deleteFile(attachment.localUri);
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
await
|
|
156
|
-
await ctx.deleteAttachment(attachment.id);
|
|
157
|
-
});
|
|
156
|
+
await context.deleteAttachment(attachment.id);
|
|
158
157
|
|
|
159
158
|
return {
|
|
160
159
|
...attachment,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Mutex } from 'async-mutex';
|
|
2
1
|
import { EventIterator } from 'event-iterator';
|
|
3
2
|
import Logger, { ILogger } from 'js-logger';
|
|
4
3
|
import {
|
|
@@ -46,6 +45,7 @@ import { TriggerManagerImpl } from './triggers/TriggerManagerImpl.js';
|
|
|
46
45
|
import { DEFAULT_WATCH_THROTTLE_MS, WatchCompatibleQuery } from './watched/WatchedQuery.js';
|
|
47
46
|
import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js';
|
|
48
47
|
import { WatchedQueryComparator } from './watched/processors/comparators.js';
|
|
48
|
+
import { Mutex } from '../utils/mutex.js';
|
|
49
49
|
|
|
50
50
|
export interface DisconnectAndClearOptions {
|
|
51
51
|
/** When set to false, data in local-only tables is preserved. */
|
|
@@ -813,6 +813,10 @@ SELECT * FROM crud_entries;
|
|
|
813
813
|
* Execute a SQL write (INSERT/UPDATE/DELETE) query
|
|
814
814
|
* and optionally return results.
|
|
815
815
|
*
|
|
816
|
+
* When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure),
|
|
817
|
+
* the returned result's `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements.
|
|
818
|
+
* Use a `RETURNING` clause and inspect `result.rows` when you need to confirm which rows changed.
|
|
819
|
+
*
|
|
816
820
|
* @param sql The SQL query to execute
|
|
817
821
|
* @param parameters Optional array of parameters to bind to the query
|
|
818
822
|
* @returns The query result as an object with structured key-value pairs
|
|
@@ -921,7 +925,7 @@ SELECT * FROM crud_entries;
|
|
|
921
925
|
await this.waitForReady();
|
|
922
926
|
return this.database.readTransaction(
|
|
923
927
|
async (tx) => {
|
|
924
|
-
const res = await callback(
|
|
928
|
+
const res = await callback(tx);
|
|
925
929
|
await tx.rollback();
|
|
926
930
|
return res;
|
|
927
931
|
},
|
|
@@ -223,14 +223,27 @@ export interface CreateDiffTriggerOptions extends BaseCreateDiffTriggerOptions {
|
|
|
223
223
|
* This table will be dropped once the trigger is removed.
|
|
224
224
|
*/
|
|
225
225
|
destination: string;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Context to use for the setup operation.
|
|
229
|
+
* This is useful for when the setup operation needs to be executed in a specific context.
|
|
230
|
+
*/
|
|
231
|
+
setupContext?: LockContext;
|
|
226
232
|
}
|
|
227
233
|
|
|
228
234
|
/**
|
|
229
235
|
* @experimental
|
|
230
|
-
*
|
|
236
|
+
* Options for {@link TriggerRemoveCallback}.
|
|
231
237
|
*/
|
|
232
|
-
export
|
|
238
|
+
export interface TriggerRemoveCallbackOptions {
|
|
239
|
+
context?: LockContext;
|
|
240
|
+
}
|
|
233
241
|
|
|
242
|
+
/**
|
|
243
|
+
* @experimental
|
|
244
|
+
* Callback to drop a trigger after it has been created.
|
|
245
|
+
*/
|
|
246
|
+
export type TriggerRemoveCallback = (options?: TriggerRemoveCallbackOptions) => Promise<void>;
|
|
234
247
|
/**
|
|
235
248
|
* @experimental
|
|
236
249
|
* Options for {@link TriggerDiffHandlerContext#withDiff}.
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
TriggerManager,
|
|
10
10
|
TriggerManagerConfig,
|
|
11
11
|
TriggerRemoveCallback,
|
|
12
|
+
TriggerRemoveCallbackOptions,
|
|
12
13
|
WithDiffOptions
|
|
13
14
|
} from './TriggerManager.js';
|
|
14
15
|
|
|
@@ -201,6 +202,7 @@ export class TriggerManagerImpl implements TriggerManager {
|
|
|
201
202
|
columns,
|
|
202
203
|
when,
|
|
203
204
|
hooks,
|
|
205
|
+
setupContext,
|
|
204
206
|
// Fall back to the provided default if not given on this level
|
|
205
207
|
useStorage = this.defaultConfig.useStorageByDefault
|
|
206
208
|
} = options;
|
|
@@ -268,13 +270,19 @@ export class TriggerManagerImpl implements TriggerManager {
|
|
|
268
270
|
* we need to ensure we can cleanup the created resources.
|
|
269
271
|
* We unfortunately cannot rely on transaction rollback.
|
|
270
272
|
*/
|
|
271
|
-
const cleanup = async () => {
|
|
273
|
+
const cleanup = async (options?: TriggerRemoveCallbackOptions) => {
|
|
274
|
+
const { context } = options ?? {};
|
|
272
275
|
disposeWarningListener();
|
|
273
|
-
|
|
276
|
+
const doCleanup = async (tx: LockContext) => {
|
|
274
277
|
await this.removeTriggers(tx, triggerIds);
|
|
275
|
-
await tx.execute(
|
|
278
|
+
await tx.execute(`DROP TABLE IF EXISTS ${destination};`);
|
|
276
279
|
await releaseStorageClaim?.();
|
|
277
|
-
}
|
|
280
|
+
};
|
|
281
|
+
if (context) {
|
|
282
|
+
await doCleanup(context);
|
|
283
|
+
} else {
|
|
284
|
+
await this.db.writeLock(doCleanup);
|
|
285
|
+
}
|
|
278
286
|
};
|
|
279
287
|
|
|
280
288
|
const setup = async (tx: LockContext) => {
|
|
@@ -360,11 +368,15 @@ export class TriggerManagerImpl implements TriggerManager {
|
|
|
360
368
|
};
|
|
361
369
|
|
|
362
370
|
try {
|
|
363
|
-
|
|
371
|
+
if (setupContext) {
|
|
372
|
+
await setup(setupContext);
|
|
373
|
+
} else {
|
|
374
|
+
await this.db.writeLock(setup);
|
|
375
|
+
}
|
|
364
376
|
return cleanup;
|
|
365
377
|
} catch (error) {
|
|
366
378
|
try {
|
|
367
|
-
await cleanup();
|
|
379
|
+
await cleanup(setupContext ? { context: setupContext } : undefined);
|
|
368
380
|
} catch (cleanupError) {
|
|
369
381
|
throw new AggregateError([error, cleanupError], 'Error during operation and cleanup');
|
|
370
382
|
}
|
package/src/db/DBAdapter.ts
CHANGED
|
@@ -16,7 +16,13 @@ import { BaseListener, BaseObserverInterface } from '../utils/BaseObserver.js';
|
|
|
16
16
|
export type QueryResult = {
|
|
17
17
|
/** Represents the auto-generated row id if applicable. */
|
|
18
18
|
insertId?: number;
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Number of affected rows reported by SQLite for a write query.
|
|
21
|
+
*
|
|
22
|
+
* When using the default client-side [JSON-based view system](https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure),
|
|
23
|
+
* `rowsAffected` may be `0` for successful `UPDATE` and `DELETE` statements.
|
|
24
|
+
* Use a `RETURNING` clause and inspect `rows` when you need to confirm which rows changed.
|
|
25
|
+
*/
|
|
20
26
|
rowsAffected: number;
|
|
21
27
|
/** if status is undefined or 0 this object will contain the query results */
|
|
22
28
|
rows?: {
|
|
@@ -41,7 +47,7 @@ export interface DBGetUtils {
|
|
|
41
47
|
get<T>(sql: string, parameters?: any[]): Promise<T>;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
export interface
|
|
50
|
+
export interface SqlExecutor {
|
|
45
51
|
/** Execute a single write statement. */
|
|
46
52
|
execute: (query: string, params?: any[] | undefined) => Promise<QueryResult>;
|
|
47
53
|
/**
|
|
@@ -59,6 +65,61 @@ export interface LockContext extends DBGetUtils {
|
|
|
59
65
|
* ```[ { id: '33', name: 'list 1', content: 'Post content', list_id: '1' } ]```
|
|
60
66
|
*/
|
|
61
67
|
executeRaw: (query: string, params?: any[] | undefined) => Promise<any[][]>;
|
|
68
|
+
|
|
69
|
+
executeBatch: (query: string, params?: any[][]) => Promise<QueryResult>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface LockContext extends SqlExecutor, DBGetUtils {}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Implements {@link DBGetUtils} on a {@link SqlRunner}.
|
|
76
|
+
*/
|
|
77
|
+
export function DBGetUtilsDefaultMixin<TBase extends new (...args: any[]) => Omit<SqlExecutor, 'executeBatch'>>(
|
|
78
|
+
Base: TBase
|
|
79
|
+
) {
|
|
80
|
+
return class extends Base implements DBGetUtils, SqlExecutor {
|
|
81
|
+
async getAll<T>(sql: string, parameters?: any[]): Promise<T[]> {
|
|
82
|
+
const res = await this.execute(sql, parameters);
|
|
83
|
+
return res.rows?._array ?? [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getOptional<T>(sql: string, parameters?: any[]): Promise<T | null> {
|
|
87
|
+
const res = await this.execute(sql, parameters);
|
|
88
|
+
return res.rows?.item(0) ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async get<T>(sql: string, parameters?: any[]): Promise<T> {
|
|
92
|
+
const res = await this.execute(sql, parameters);
|
|
93
|
+
const first = res.rows?.item(0);
|
|
94
|
+
if (!first) {
|
|
95
|
+
throw new Error('Result set is empty');
|
|
96
|
+
}
|
|
97
|
+
return first;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async executeBatch(query: string, params: any[][] = []): Promise<QueryResult> {
|
|
101
|
+
// If this context can run batch statements natively, use that.
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
if (super.executeBatch) {
|
|
104
|
+
// @ts-ignore
|
|
105
|
+
return super.executeBatch(query, params);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Emulate executeBatch by running statements individually.
|
|
109
|
+
let lastInsertId: number | undefined;
|
|
110
|
+
let rowsAffected = 0;
|
|
111
|
+
for (const set of params) {
|
|
112
|
+
const result = await this.execute(query, set);
|
|
113
|
+
lastInsertId = result.insertId;
|
|
114
|
+
rowsAffected += result.rowsAffected;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
rowsAffected,
|
|
119
|
+
insertId: lastInsertId
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
};
|
|
62
123
|
}
|
|
63
124
|
|
|
64
125
|
export interface Transaction extends LockContext {
|
|
@@ -107,22 +168,119 @@ export interface DBLockOptions {
|
|
|
107
168
|
timeoutMs?: number;
|
|
108
169
|
}
|
|
109
170
|
|
|
110
|
-
export interface
|
|
111
|
-
close: () => void | Promise<void>;
|
|
112
|
-
execute: (query: string, params?: any[]) => Promise<QueryResult>;
|
|
113
|
-
executeRaw: (query: string, params?: any[]) => Promise<any[][]>;
|
|
114
|
-
executeBatch: (query: string, params?: any[][]) => Promise<QueryResult>;
|
|
171
|
+
export interface ConnectionPool extends BaseObserverInterface<DBAdapterListener> {
|
|
115
172
|
name: string;
|
|
173
|
+
close: () => void | Promise<void>;
|
|
116
174
|
readLock: <T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) => Promise<T>;
|
|
117
|
-
readTransaction: <T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) => Promise<T>;
|
|
118
175
|
writeLock: <T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions) => Promise<T>;
|
|
119
|
-
|
|
176
|
+
|
|
120
177
|
/**
|
|
121
178
|
* This method refreshes the schema information across all connections. This is for advanced use cases, and should generally not be needed.
|
|
122
179
|
*/
|
|
123
180
|
refreshSchema: () => Promise<void>;
|
|
124
181
|
}
|
|
125
182
|
|
|
183
|
+
export interface DBAdapter extends ConnectionPool, SqlExecutor, DBGetUtils {
|
|
184
|
+
readTransaction: <T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) => Promise<T>;
|
|
185
|
+
writeTransaction: <T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions) => Promise<T>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A mixin to implement {@link DBAdapter} by delegating to {@link ConnectionPool.readLock} and
|
|
190
|
+
* {@link ConnectionPool.writeLock}.
|
|
191
|
+
*/
|
|
192
|
+
export function DBAdapterDefaultMixin<TBase extends new (...args: any[]) => ConnectionPool>(Base: TBase) {
|
|
193
|
+
return class extends Base implements DBAdapter {
|
|
194
|
+
readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
195
|
+
return this.readLock((ctx) => TransactionImplementation.runWith(ctx, fn), options);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
writeTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
199
|
+
return this.writeLock((ctx) => TransactionImplementation.runWith(ctx, fn), options);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
getAll<T>(sql: string, parameters?: any[]): Promise<T[]> {
|
|
203
|
+
return this.readLock((ctx) => ctx.getAll(sql, parameters));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getOptional<T>(sql: string, parameters?: any[]): Promise<T | null> {
|
|
207
|
+
return this.readLock((ctx) => ctx.getOptional(sql, parameters));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
get<T>(sql: string, parameters?: any[]): Promise<T> {
|
|
211
|
+
return this.readLock((ctx) => ctx.get(sql, parameters));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
execute(query: string, params?: any[]): Promise<QueryResult> {
|
|
215
|
+
return this.writeLock((ctx) => ctx.execute(query, params));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
executeRaw(query: string, params?: any[]): Promise<any[][]> {
|
|
219
|
+
return this.writeLock((ctx) => ctx.executeRaw(query, params));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
executeBatch(query: string, params?: any[][]): Promise<QueryResult> {
|
|
223
|
+
return this.writeTransaction((tx) => tx.executeBatch(query, params));
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
class BaseTransaction implements SqlExecutor {
|
|
229
|
+
private finalized = false;
|
|
230
|
+
|
|
231
|
+
constructor(private inner: SqlExecutor) {}
|
|
232
|
+
|
|
233
|
+
async commit(): Promise<QueryResult> {
|
|
234
|
+
if (this.finalized) {
|
|
235
|
+
return { rowsAffected: 0 };
|
|
236
|
+
}
|
|
237
|
+
this.finalized = true;
|
|
238
|
+
return this.inner.execute('COMMIT');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async rollback(): Promise<QueryResult> {
|
|
242
|
+
if (this.finalized) {
|
|
243
|
+
return { rowsAffected: 0 };
|
|
244
|
+
}
|
|
245
|
+
this.finalized = true;
|
|
246
|
+
return this.inner.execute('ROLLBACK');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
execute(query: string, params?: any[] | undefined): Promise<QueryResult> {
|
|
250
|
+
return this.inner.execute(query, params);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
executeRaw(query: string, params?: any[] | undefined): Promise<any[][]> {
|
|
254
|
+
return this.inner.executeRaw(query, params);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
executeBatch(query: string, params?: any[][]): Promise<QueryResult> {
|
|
258
|
+
return this.inner.executeBatch(query, params);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
class TransactionImplementation extends DBGetUtilsDefaultMixin(BaseTransaction) {
|
|
263
|
+
static async runWith<T>(ctx: LockContext, fn: (tx: Transaction) => Promise<T>): Promise<T> {
|
|
264
|
+
let tx = new TransactionImplementation(ctx);
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
await ctx.execute('BEGIN IMMEDIATE');
|
|
268
|
+
|
|
269
|
+
const result = await fn(tx);
|
|
270
|
+
await tx.commit();
|
|
271
|
+
return result;
|
|
272
|
+
} catch (ex) {
|
|
273
|
+
try {
|
|
274
|
+
await tx.rollback();
|
|
275
|
+
} catch (ex2) {
|
|
276
|
+
// In rare cases, a rollback may fail.
|
|
277
|
+
// Safe to ignore.
|
|
278
|
+
}
|
|
279
|
+
throw ex;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
126
284
|
export function isBatchedUpdateNotification(
|
|
127
285
|
update: BatchedUpdateNotification | UpdateNotification
|
|
128
286
|
): update is BatchedUpdateNotification {
|