@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.
Files changed (40) hide show
  1. package/dist/bundle.cjs +270 -46
  2. package/dist/bundle.cjs.map +1 -1
  3. package/dist/bundle.mjs +265 -44
  4. package/dist/bundle.mjs.map +1 -1
  5. package/dist/bundle.node.cjs +270 -45
  6. package/dist/bundle.node.cjs.map +1 -1
  7. package/dist/bundle.node.mjs +265 -43
  8. package/dist/bundle.node.mjs.map +1 -1
  9. package/dist/index.d.cts +108 -26
  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/client/triggers/TriggerManager.d.ts +13 -1
  22. package/lib/client/triggers/TriggerManagerImpl.d.ts +2 -2
  23. package/lib/client/triggers/TriggerManagerImpl.js +19 -7
  24. package/lib/client/triggers/TriggerManagerImpl.js.map +1 -1
  25. package/lib/db/DBAdapter.d.ts +55 -9
  26. package/lib/db/DBAdapter.js +126 -0
  27. package/lib/db/DBAdapter.js.map +1 -1
  28. package/lib/utils/mutex.d.ts +18 -5
  29. package/lib/utils/mutex.js +97 -21
  30. package/lib/utils/mutex.js.map +1 -1
  31. package/package.json +1 -2
  32. package/src/attachments/AttachmentQueue.ts +10 -4
  33. package/src/attachments/AttachmentService.ts +2 -3
  34. package/src/attachments/README.md +6 -4
  35. package/src/attachments/SyncingService.ts +4 -5
  36. package/src/client/AbstractPowerSyncDatabase.ts +6 -2
  37. package/src/client/triggers/TriggerManager.ts +15 -2
  38. package/src/client/triggers/TriggerManagerImpl.ts +18 -6
  39. package/src/db/DBAdapter.ts +167 -9
  40. package/src/utils/mutex.ts +121 -26
@@ -1,34 +1,129 @@
1
- import { Mutex } from 'async-mutex';
1
+ export type UnlockFn = () => void;
2
2
 
3
3
  /**
4
- * Wrapper for async-mutex runExclusive, which allows for a timeout on each exclusive lock.
4
+ * An asynchronous mutex implementation.
5
+ *
6
+ * @internal This class is meant to be used in PowerSync SDKs only, and is not part of the public API.
5
7
  */
6
- export async function mutexRunExclusive<T>(
7
- mutex: Mutex,
8
- callback: () => Promise<T>,
9
- options?: { timeoutMs?: number }
10
- ): Promise<T> {
11
- return new Promise((resolve, reject) => {
12
- const timeout = options?.timeoutMs;
13
- let timedOut = false;
14
- const timeoutId = timeout
15
- ? setTimeout(() => {
16
- timedOut = true;
17
- reject(new Error('Timeout waiting for lock'));
18
- }, timeout)
19
- : undefined;
20
-
21
- mutex.runExclusive(async () => {
22
- if (timeoutId) {
23
- clearTimeout(timeoutId);
8
+ export class Mutex {
9
+ private inCriticalSection = false;
10
+
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
+ private firstWaiter?: MutexWaitNode;
14
+ private lastWaiter?: MutexWaitNode;
15
+
16
+ private addWaiter(onAcquire: () => void): MutexWaitNode {
17
+ const node: MutexWaitNode = {
18
+ isActive: true,
19
+ onAcquire,
20
+ prev: this.lastWaiter
21
+ };
22
+ if (this.lastWaiter) {
23
+ this.lastWaiter.next = node;
24
+ this.lastWaiter = node;
25
+ } else {
26
+ // First waiter
27
+ this.lastWaiter = this.firstWaiter = node;
28
+ }
29
+
30
+ return node;
31
+ }
32
+
33
+ private deactivateWaiter(waiter: MutexWaitNode) {
34
+ const { prev, next } = waiter;
35
+ waiter.isActive = false;
36
+
37
+ if (prev) prev.next = next;
38
+ if (next) next.prev = prev;
39
+ if (waiter == this.firstWaiter) this.firstWaiter = next;
40
+ if (waiter == this.lastWaiter) this.lastWaiter = prev;
41
+ }
42
+
43
+ acquire(abort?: AbortSignal): Promise<UnlockFn> {
44
+ return new Promise((resolve, reject) => {
45
+ function rejectAborted() {
46
+ reject(abort?.reason ?? new Error('Mutex acquire aborted'));
24
47
  }
25
- if (timedOut) return;
48
+ if (abort?.aborted) {
49
+ return rejectAborted();
50
+ }
51
+
52
+ let holdsMutex = false;
53
+
54
+ const markCompleted = () => {
55
+ if (!holdsMutex) return;
56
+ holdsMutex = false;
57
+
58
+ const waiter = this.firstWaiter;
59
+ if (waiter) {
60
+ this.deactivateWaiter(waiter);
61
+ // Still in critical section, but owned by next waiter now.
62
+ waiter.onAcquire();
63
+ } else {
64
+ this.inCriticalSection = false;
65
+ }
66
+ };
67
+
68
+ if (!this.inCriticalSection) {
69
+ this.inCriticalSection = true;
70
+ holdsMutex = true;
71
+ return resolve(markCompleted);
72
+ } else {
73
+ let node: MutexWaitNode;
74
+
75
+ const onAbort = () => {
76
+ abort?.removeEventListener('abort', onAbort);
26
77
 
27
- try {
28
- resolve(await callback());
29
- } catch (ex) {
30
- reject(ex);
78
+ if (node.isActive) {
79
+ this.deactivateWaiter(node);
80
+ rejectAborted();
81
+ }
82
+ };
83
+
84
+ node = this.addWaiter(() => {
85
+ abort?.removeEventListener('abort', onAbort);
86
+ holdsMutex = true;
87
+ resolve(markCompleted);
88
+ });
89
+
90
+ abort?.addEventListener('abort', onAbort);
31
91
  }
32
92
  });
33
- });
93
+ }
94
+
95
+ async runExclusive<T>(fn: () => PromiseLike<T> | T, abort?: AbortSignal): Promise<T> {
96
+ const returnMutex = await this.acquire(abort);
97
+
98
+ try {
99
+ return await fn();
100
+ } finally {
101
+ returnMutex();
102
+ }
103
+ }
104
+ }
105
+
106
+ interface MutexWaitNode {
107
+ /**
108
+ * Whether the waiter is currently active (not aborted and not fullfilled).
109
+ */
110
+ isActive: boolean;
111
+ onAcquire: () => void;
112
+ prev?: MutexWaitNode;
113
+ next?: MutexWaitNode;
114
+ }
115
+
116
+ /**
117
+ * Creates a signal aborting after the set timeout.
118
+ */
119
+ export function timeoutSignal(timeout: number): AbortSignal;
120
+ export function timeoutSignal(timeout?: number): AbortSignal | undefined;
121
+
122
+ export function timeoutSignal(timeout?: number): AbortSignal | undefined {
123
+ if (timeout == null) return;
124
+ if ('timeout' in AbortSignal) return AbortSignal.timeout(timeout);
125
+
126
+ const controller = new AbortController();
127
+ setTimeout(() => controller.abort(new Error('Timeout waiting for lock')), timeout);
128
+ return controller.signal;
34
129
  }