@powersync/common 1.49.0 → 1.51.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.
Files changed (37) hide show
  1. package/dist/bundle.cjs +214 -39
  2. package/dist/bundle.cjs.map +1 -1
  3. package/dist/bundle.mjs +210 -37
  4. package/dist/bundle.mjs.map +1 -1
  5. package/dist/bundle.node.cjs +214 -38
  6. package/dist/bundle.node.cjs.map +1 -1
  7. package/dist/bundle.node.mjs +210 -36
  8. package/dist/bundle.node.mjs.map +1 -1
  9. package/dist/index.d.cts +75 -16
  10. package/lib/attachments/AttachmentQueue.d.ts +10 -4
  11. package/lib/attachments/AttachmentQueue.js +10 -4
  12. package/lib/attachments/AttachmentQueue.js.map +1 -1
  13. package/lib/attachments/AttachmentService.js +2 -3
  14. package/lib/attachments/AttachmentService.js.map +1 -1
  15. package/lib/attachments/SyncingService.d.ts +2 -1
  16. package/lib/attachments/SyncingService.js +4 -5
  17. package/lib/attachments/SyncingService.js.map +1 -1
  18. package/lib/client/AbstractPowerSyncDatabase.d.ts +5 -1
  19. package/lib/client/AbstractPowerSyncDatabase.js +6 -2
  20. package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
  21. package/lib/db/DBAdapter.d.ts +7 -1
  22. package/lib/db/DBAdapter.js.map +1 -1
  23. package/lib/utils/mutex.d.ts +47 -5
  24. package/lib/utils/mutex.js +146 -21
  25. package/lib/utils/mutex.js.map +1 -1
  26. package/lib/utils/queue.d.ts +16 -0
  27. package/lib/utils/queue.js +42 -0
  28. package/lib/utils/queue.js.map +1 -0
  29. package/package.json +1 -2
  30. package/src/attachments/AttachmentQueue.ts +10 -4
  31. package/src/attachments/AttachmentService.ts +2 -3
  32. package/src/attachments/README.md +6 -4
  33. package/src/attachments/SyncingService.ts +4 -5
  34. package/src/client/AbstractPowerSyncDatabase.ts +6 -2
  35. package/src/db/DBAdapter.ts +7 -1
  36. package/src/utils/mutex.ts +184 -26
  37. package/src/utils/queue.ts +48 -0
@@ -1,7 +1,49 @@
1
- import { Mutex } from 'async-mutex';
1
+ export type UnlockFn = () => void;
2
2
  /**
3
- * Wrapper for async-mutex runExclusive, which allows for a timeout on each exclusive lock.
3
+ * An asynchronous semaphore implementation with associated items per lease.
4
+ *
5
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
4
6
  */
5
- export declare function mutexRunExclusive<T>(mutex: Mutex, callback: () => Promise<T>, options?: {
6
- timeoutMs?: number;
7
- }): Promise<T>;
7
+ export declare class Semaphore<T> {
8
+ private readonly available;
9
+ readonly size: number;
10
+ private firstWaiter?;
11
+ private lastWaiter?;
12
+ constructor(elements: Iterable<T>);
13
+ private addWaiter;
14
+ private deactivateWaiter;
15
+ private requestPermits;
16
+ /**
17
+ * Requests a single item from the pool.
18
+ *
19
+ * The returned `release` callback must be invoked to return the item into the pool.
20
+ */
21
+ requestOne(abort?: AbortSignal): Promise<{
22
+ item: T;
23
+ release: UnlockFn;
24
+ }>;
25
+ /**
26
+ * Requests access to all items from the pool.
27
+ *
28
+ * The returned `release` callback must be invoked to return items into the pool.
29
+ */
30
+ requestAll(abort?: AbortSignal): Promise<{
31
+ items: T[];
32
+ release: UnlockFn;
33
+ }>;
34
+ }
35
+ /**
36
+ * An asynchronous mutex implementation.
37
+ *
38
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
39
+ */
40
+ export declare class Mutex {
41
+ private inner;
42
+ acquire(abort?: AbortSignal): Promise<UnlockFn>;
43
+ runExclusive<T>(fn: () => PromiseLike<T> | T, abort?: AbortSignal): Promise<T>;
44
+ }
45
+ /**
46
+ * Creates a signal aborting after the set timeout.
47
+ */
48
+ export declare function timeoutSignal(timeout: number): AbortSignal;
49
+ export declare function timeoutSignal(timeout?: number): AbortSignal | undefined;
@@ -1,29 +1,154 @@
1
+ import { Queue } from './queue.js';
1
2
  /**
2
- * Wrapper for async-mutex runExclusive, which allows for a timeout on each exclusive lock.
3
+ * An asynchronous semaphore implementation with associated items per lease.
4
+ *
5
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
3
6
  */
4
- export async function mutexRunExclusive(mutex, callback, options) {
5
- return new Promise((resolve, reject) => {
6
- const timeout = options?.timeoutMs;
7
- let timedOut = false;
8
- const timeoutId = timeout
9
- ? setTimeout(() => {
10
- timedOut = true;
11
- reject(new Error('Timeout waiting for lock'));
12
- }, timeout)
13
- : undefined;
14
- mutex.runExclusive(async () => {
15
- if (timeoutId) {
16
- clearTimeout(timeoutId);
7
+ export class Semaphore {
8
+ // Available items that are not currently assigned to a waiter.
9
+ available;
10
+ size;
11
+ // Linked list of waiters. We don't expect the wait list to become particularly large, and this allows removing
12
+ // aborted waiters from the middle of the list efficiently.
13
+ firstWaiter;
14
+ lastWaiter;
15
+ constructor(elements) {
16
+ this.available = new Queue(elements);
17
+ this.size = this.available.length;
18
+ }
19
+ addWaiter(requestedItems, onAcquire) {
20
+ const node = {
21
+ isActive: true,
22
+ acquiredItems: [],
23
+ remainingItems: requestedItems,
24
+ onAcquire,
25
+ prev: this.lastWaiter
26
+ };
27
+ if (this.lastWaiter) {
28
+ this.lastWaiter.next = node;
29
+ this.lastWaiter = node;
30
+ }
31
+ else {
32
+ // First waiter
33
+ this.lastWaiter = this.firstWaiter = node;
34
+ }
35
+ return node;
36
+ }
37
+ deactivateWaiter(waiter) {
38
+ const { prev, next } = waiter;
39
+ waiter.isActive = false;
40
+ if (prev)
41
+ prev.next = next;
42
+ if (next)
43
+ next.prev = prev;
44
+ if (waiter == this.firstWaiter)
45
+ this.firstWaiter = next;
46
+ if (waiter == this.lastWaiter)
47
+ this.lastWaiter = prev;
48
+ }
49
+ requestPermits(amount, abort) {
50
+ if (amount <= 0 || amount > this.size) {
51
+ throw new Error(`Invalid amount of items requested (${amount}), must be between 1 and ${this.size}`);
52
+ }
53
+ return new Promise((resolve, reject) => {
54
+ function rejectAborted() {
55
+ reject(abort?.reason ?? new Error('Semaphore acquire aborted'));
17
56
  }
18
- if (timedOut)
19
- return;
20
- try {
21
- resolve(await callback());
57
+ if (abort?.aborted) {
58
+ return rejectAborted();
22
59
  }
23
- catch (ex) {
24
- reject(ex);
60
+ let waiter;
61
+ const markCompleted = () => {
62
+ const items = waiter.acquiredItems;
63
+ waiter.acquiredItems = []; // Avoid releasing items twice.
64
+ for (const element of items) {
65
+ // Give to next waiter, if possible.
66
+ const nextWaiter = this.firstWaiter;
67
+ if (nextWaiter) {
68
+ nextWaiter.acquiredItems.push(element);
69
+ nextWaiter.remainingItems--;
70
+ if (nextWaiter.remainingItems == 0) {
71
+ nextWaiter.onAcquire();
72
+ }
73
+ }
74
+ else {
75
+ // No pending waiter, return lease into pool.
76
+ this.available.addLast(element);
77
+ }
78
+ }
79
+ };
80
+ const onAbort = () => {
81
+ abort?.removeEventListener('abort', onAbort);
82
+ if (waiter.isActive) {
83
+ this.deactivateWaiter(waiter);
84
+ rejectAborted();
85
+ }
86
+ };
87
+ const resolvePromise = () => {
88
+ this.deactivateWaiter(waiter);
89
+ abort?.removeEventListener('abort', onAbort);
90
+ const items = waiter.acquiredItems;
91
+ resolve({ items, release: markCompleted });
92
+ };
93
+ waiter = this.addWaiter(amount, resolvePromise);
94
+ // If there are items in the pool that haven't been assigned, we can pull them into this waiter. Note that this is
95
+ // only the case if we're the first waiter (otherwise, items would have been assigned to an earlier waiter).
96
+ while (!this.available.isEmpty && waiter.remainingItems > 0) {
97
+ waiter.acquiredItems.push(this.available.removeFirst());
98
+ waiter.remainingItems--;
25
99
  }
100
+ if (waiter.remainingItems == 0) {
101
+ return resolvePromise();
102
+ }
103
+ abort?.addEventListener('abort', onAbort);
26
104
  });
27
- });
105
+ }
106
+ /**
107
+ * Requests a single item from the pool.
108
+ *
109
+ * The returned `release` callback must be invoked to return the item into the pool.
110
+ */
111
+ async requestOne(abort) {
112
+ const { items, release } = await this.requestPermits(1, abort);
113
+ return { release, item: items[0] };
114
+ }
115
+ /**
116
+ * Requests access to all items from the pool.
117
+ *
118
+ * The returned `release` callback must be invoked to return items into the pool.
119
+ */
120
+ requestAll(abort) {
121
+ return this.requestPermits(this.size, abort);
122
+ }
123
+ }
124
+ /**
125
+ * An asynchronous mutex implementation.
126
+ *
127
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
128
+ */
129
+ export class Mutex {
130
+ inner = new Semaphore([null]);
131
+ async acquire(abort) {
132
+ const { release } = await this.inner.requestOne(abort);
133
+ return release;
134
+ }
135
+ async runExclusive(fn, abort) {
136
+ const returnMutex = await this.acquire(abort);
137
+ try {
138
+ return await fn();
139
+ }
140
+ finally {
141
+ returnMutex();
142
+ }
143
+ }
144
+ }
145
+ export function timeoutSignal(timeout) {
146
+ if (timeout == null)
147
+ return;
148
+ if ('timeout' in AbortSignal)
149
+ return AbortSignal.timeout(timeout);
150
+ const controller = new AbortController();
151
+ setTimeout(() => controller.abort(new Error('Timeout waiting for lock')), timeout);
152
+ return controller.signal;
28
153
  }
29
154
  //# sourceMappingURL=mutex.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mutex.js","sourceRoot":"","sources":["../../src/utils/mutex.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAY,EACZ,QAA0B,EAC1B,OAAgC;IAEhC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,OAAO,GAAG,OAAO,EAAE,SAAS,CAAC;QACnC,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,MAAM,SAAS,GAAG,OAAO;YACvB,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE;gBACd,QAAQ,GAAG,IAAI,CAAC;gBAChB,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC,CAAC;YAChD,CAAC,EAAE,OAAO,CAAC;YACb,CAAC,CAAC,SAAS,CAAC;QAEd,KAAK,CAAC,YAAY,CAAC,KAAK,IAAI,EAAE;YAC5B,IAAI,SAAS,EAAE,CAAC;gBACd,YAAY,CAAC,SAAS,CAAC,CAAC;YAC1B,CAAC;YACD,IAAI,QAAQ;gBAAE,OAAO;YAErB,IAAI,CAAC;gBACH,OAAO,CAAC,MAAM,QAAQ,EAAE,CAAC,CAAC;YAC5B,CAAC;YAAC,OAAO,EAAE,EAAE,CAAC;gBACZ,MAAM,CAAC,EAAE,CAAC,CAAC;YACb,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"mutex.js","sourceRoot":"","sources":["../../src/utils/mutex.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A simple fixed-capacity queue implementation.
3
+ *
4
+ * Unlike a naive queue implemented by `array.push()` and `array.shift()`, this avoids moving array elements around
5
+ * and is `O(1)` for {@link addLast} and {@link removeFirst}.
6
+ */
7
+ export declare class Queue<T> {
8
+ private table;
9
+ private head;
10
+ private _length;
11
+ constructor(initialItems: Iterable<T>);
12
+ get isEmpty(): boolean;
13
+ get length(): number;
14
+ removeFirst(): T;
15
+ addLast(element: T): void;
16
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A simple fixed-capacity queue implementation.
3
+ *
4
+ * Unlike a naive queue implemented by `array.push()` and `array.shift()`, this avoids moving array elements around
5
+ * and is `O(1)` for {@link addLast} and {@link removeFirst}.
6
+ */
7
+ export class Queue {
8
+ table;
9
+ // Index of the first element in the table.
10
+ head;
11
+ // Amount of items currently in the queue.
12
+ _length;
13
+ constructor(initialItems) {
14
+ this.table = [...initialItems];
15
+ this.head = 0;
16
+ this._length = this.table.length;
17
+ }
18
+ get isEmpty() {
19
+ return this.length == 0;
20
+ }
21
+ get length() {
22
+ return this._length;
23
+ }
24
+ removeFirst() {
25
+ if (this.isEmpty) {
26
+ throw new Error('Queue is empty');
27
+ }
28
+ const result = this.table[this.head];
29
+ this._length--;
30
+ this.table[this.head] = undefined;
31
+ this.head = (this.head + 1) % this.table.length;
32
+ return result;
33
+ }
34
+ addLast(element) {
35
+ if (this.length == this.table.length) {
36
+ throw new Error('Queue is full');
37
+ }
38
+ this.table[(this.head + this._length) % this.table.length] = element;
39
+ this._length++;
40
+ }
41
+ }
42
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.js","sourceRoot":"","sources":["../../src/utils/queue.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,OAAO,KAAK;IACR,KAAK,CAAoB;IACjC,2CAA2C;IACnC,IAAI,CAAS;IACrB,0CAA0C;IAClC,OAAO,CAAS;IAExB,YAAY,YAAyB;QACnC,IAAI,CAAC,KAAK,GAAG,CAAC,GAAG,YAAY,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IACnC,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,WAAW;QACT,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;QACpC,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAM,CAAC;QAC1C,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC;QAClC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;QAChD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO,CAAC,OAAU;QAChB,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;QACnC,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;QACrE,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powersync/common",
3
- "version": "1.49.0",
3
+ "version": "1.51.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. Default: 30000 (30 seconds) */
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
- /** Duration in milliseconds to throttle sync operations */
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 - Interval between automatic syncs in milliseconds. Default: 30000
90
- * @param options.syncThrottleDuration - Throttle duration for sync operations in milliseconds. Default: 1000
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 { mutexRunExclusive } from '../utils/mutex.js';
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 mutexRunExclusive(this.mutex, async () => {
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` | Interval between automatic syncs in milliseconds |
293
- | `syncThrottleDuration` | `number` | No | `30` | Throttle duration for sync operations in milliseconds |
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, // Sync every 60 seconds instead of 30
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
- Set to `0` to disable periodic syncing (manual `syncStorage()` calls only).
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 this.attachmentService.withContext(async (ctx) => {
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({ ...tx });
928
+ const res = await callback(tx);
925
929
  await tx.rollback();
926
930
  return res;
927
931
  },
@@ -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
- /** Number of affected rows if result of a update query. */
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?: {