@powersync/common 1.54.0 → 1.55.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.
@@ -1,3 +1,6 @@
1
+ import { symbolAsyncIterator } from './compatibility.js';
2
+ import { doneResult, valueResult } from './stream_transform.js';
3
+
1
4
  /**
2
5
  * Throttle a function to be called at most once every "wait" milliseconds,
3
6
  * on the trailing edge.
@@ -34,44 +37,149 @@ export interface AsyncNotifier {
34
37
  }
35
38
 
36
39
  export function asyncNotifier(): AsyncNotifier {
37
- let waitingConsumer: (() => void) | null = null;
38
- let hasPendingNotification = false;
40
+ const queue = new EventQueue<void>();
39
41
 
40
42
  return {
41
43
  notify() {
42
- if (waitingConsumer != null) {
43
- waitingConsumer();
44
- waitingConsumer = null;
44
+ if (queue.countOutstandingEvents > 0) {
45
+ // Already has an outstanding event, no need to buffer another one.
45
46
  } else {
46
- hasPendingNotification = true;
47
+ queue.notify();
47
48
  }
48
49
  },
49
50
  waitForNotification(signal: AbortSignal) {
50
- return new Promise((resolve) => {
51
- if (waitingConsumer != null) {
52
- throw new Error('Illegal call to waitForNotification, already has a waiter.');
51
+ return queue.waitForEvent(signal);
52
+ }
53
+ };
54
+ }
55
+
56
+ type QueueWaiter<T> = { resolve: (event: T) => void; reject: (error: unknown) => void };
57
+
58
+ export interface QueueOptions {
59
+ eventDelivered?: () => void;
60
+ }
61
+
62
+ export class EventQueue<T> {
63
+ private waitingConsumer: QueueWaiter<T> | undefined;
64
+ private readonly outstandingEvents: Array<(waiter: QueueWaiter<T>) => void>;
65
+
66
+ constructor(private readonly options: QueueOptions = {}) {
67
+ this.outstandingEvents = [];
68
+ }
69
+
70
+ /**
71
+ * The amount of buffered events not yet dispatched to listeners.
72
+ */
73
+ get countOutstandingEvents(): number {
74
+ return this.outstandingEvents.length;
75
+ }
76
+
77
+ private notifyInner(dispatch: (waiter: QueueWaiter<T>) => void) {
78
+ const existing = this.waitingConsumer;
79
+ this.waitingConsumer = undefined;
80
+
81
+ const dispatchAndNotifyListeners = (waiter: QueueWaiter<T>) => {
82
+ dispatch(waiter);
83
+ this.options.eventDelivered?.();
84
+ };
85
+
86
+ if (existing) {
87
+ dispatchAndNotifyListeners(existing);
88
+ } else {
89
+ this.outstandingEvents.push(dispatchAndNotifyListeners);
90
+ }
91
+ }
92
+
93
+ notify(value: T) {
94
+ this.notifyInner((l) => l.resolve(value));
95
+ }
96
+
97
+ notifyError(error: unknown) {
98
+ this.notifyInner((l) => l.reject(error));
99
+ }
100
+
101
+ waitForEvent(signal: AbortSignal): Promise<T | undefined> {
102
+ return new Promise((resolve, reject) => {
103
+ if (this.waitingConsumer != null) {
104
+ throw new Error('Illegal call to waitForEvent, already has a waiter.');
105
+ }
106
+
107
+ const complete = () => {
108
+ signal?.removeEventListener('abort', onAbort);
109
+ };
110
+
111
+ const onAbort = () => {
112
+ complete();
113
+ this.waitingConsumer = undefined;
114
+ resolve(undefined);
115
+ };
116
+
117
+ const waiter: QueueWaiter<T> = {
118
+ resolve: (value) => {
119
+ complete();
120
+ resolve(value);
121
+ },
122
+ reject: (error) => {
123
+ complete();
124
+ reject(error);
53
125
  }
126
+ };
54
127
 
55
- if (signal.aborted) {
56
- resolve();
57
- } else if (hasPendingNotification) {
58
- resolve();
59
- hasPendingNotification = false;
60
- } else {
61
- function complete() {
62
- signal.removeEventListener('abort', onAbort);
63
- resolve();
64
- }
128
+ if (signal.aborted) {
129
+ resolve(undefined);
130
+ } else if (this.countOutstandingEvents > 0) {
131
+ const [event] = this.outstandingEvents.splice(0, 1);
132
+ event(waiter);
133
+ } else {
134
+ this.waitingConsumer = waiter;
135
+ signal.addEventListener('abort', onAbort);
136
+ }
137
+ });
138
+ }
65
139
 
66
- function onAbort() {
67
- waitingConsumer = null;
68
- resolve();
69
- }
140
+ /**
141
+ * Creates an async iterable backed by event queues.
142
+ *
143
+ * @param run A function invoked for every new listener. It receives a queue backing the async iterator.
144
+ * @param abort An additional abort signal. The `run` callback will also be aborted when `AsyncIterator.return` is
145
+ * called.
146
+ * @returns An object conforming to the async iterable protocol.
147
+ */
148
+ static queueBasedAsyncIterable<T>(
149
+ run: (queue: EventQueue<T>, abort: AbortSignal) => void,
150
+ abort?: AbortSignal
151
+ ): AsyncIterable<T> {
152
+ return {
153
+ [symbolAsyncIterator]: () => {
154
+ const queue = new EventQueue<T>();
155
+ const controller = new AbortController();
70
156
 
71
- waitingConsumer = complete;
72
- signal.addEventListener('abort', onAbort);
157
+ function dispose() {
158
+ controller.abort();
159
+ abort?.removeEventListener('abort', dispose);
73
160
  }
74
- });
75
- }
76
- };
161
+
162
+ if (abort) {
163
+ if (abort.aborted) {
164
+ controller.abort();
165
+ } else {
166
+ abort.addEventListener('abort', dispose);
167
+ }
168
+ }
169
+
170
+ run(queue, controller.signal);
171
+
172
+ return {
173
+ async next(): Promise<IteratorResult<T>> {
174
+ const event = await queue.waitForEvent(controller.signal);
175
+ return event == null ? doneResult : valueResult(event);
176
+ },
177
+ async return(): Promise<IteratorResult<T>> {
178
+ dispose();
179
+ return doneResult;
180
+ }
181
+ };
182
+ }
183
+ };
184
+ }
77
185
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Some JavaScript engines, in particular older versions of React Native, don't support Symbol.asyncIterator.
3
+ *
4
+ * For those, users relying on async generators typically lower them with a transpiler and [this polyfill](https://github.com/Azure/azure-sdk-for-js/blob/%40azure/core-asynciterator-polyfill_1.0.2/sdk/core/core-asynciterator-polyfill/src/index.ts#L4-L6).
5
+ * This definition is compatible with that polyfill, so transpiled apps can use async iterables created by the PowerSync
6
+ * SDK.
7
+ */
8
+ export const symbolAsyncIterator: typeof Symbol.asyncIterator =
9
+ Symbol.asyncIterator ?? Symbol.for('Symbol.asyncIterator');