@powersync/common 1.45.0 → 1.47.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/README.md +5 -1
- package/dist/bundle.cjs +4165 -4914
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.mjs +4155 -4915
- package/dist/bundle.mjs.map +1 -1
- package/dist/bundle.node.cjs +1502 -422
- package/dist/bundle.node.cjs.map +1 -1
- package/dist/bundle.node.mjs +1492 -423
- package/dist/bundle.node.mjs.map +1 -1
- package/dist/index.d.cts +622 -42
- package/lib/attachments/AttachmentContext.d.ts +86 -0
- package/lib/attachments/AttachmentContext.js +229 -0
- package/lib/attachments/AttachmentContext.js.map +1 -0
- package/lib/attachments/AttachmentErrorHandler.d.ts +31 -0
- package/lib/attachments/AttachmentErrorHandler.js +2 -0
- package/lib/attachments/AttachmentErrorHandler.js.map +1 -0
- package/lib/attachments/AttachmentQueue.d.ts +149 -0
- package/lib/attachments/AttachmentQueue.js +362 -0
- package/lib/attachments/AttachmentQueue.js.map +1 -0
- package/lib/attachments/AttachmentService.d.ts +29 -0
- package/lib/attachments/AttachmentService.js +56 -0
- package/lib/attachments/AttachmentService.js.map +1 -0
- package/lib/attachments/LocalStorageAdapter.d.ts +62 -0
- package/lib/attachments/LocalStorageAdapter.js +6 -0
- package/lib/attachments/LocalStorageAdapter.js.map +1 -0
- package/lib/attachments/RemoteStorageAdapter.d.ts +27 -0
- package/lib/attachments/RemoteStorageAdapter.js +2 -0
- package/lib/attachments/RemoteStorageAdapter.js.map +1 -0
- package/lib/attachments/Schema.d.ts +50 -0
- package/lib/attachments/Schema.js +62 -0
- package/lib/attachments/Schema.js.map +1 -0
- package/lib/attachments/SyncingService.d.ts +62 -0
- package/lib/attachments/SyncingService.js +168 -0
- package/lib/attachments/SyncingService.js.map +1 -0
- package/lib/attachments/WatchedAttachmentItem.d.ts +17 -0
- package/lib/attachments/WatchedAttachmentItem.js +2 -0
- package/lib/attachments/WatchedAttachmentItem.js.map +1 -0
- package/lib/client/AbstractPowerSyncDatabase.d.ts +9 -2
- package/lib/client/AbstractPowerSyncDatabase.js +18 -5
- package/lib/client/AbstractPowerSyncDatabase.js.map +1 -1
- package/lib/client/sync/stream/AbstractRemote.js +41 -32
- package/lib/client/sync/stream/AbstractRemote.js.map +1 -1
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.d.ts +7 -12
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js +10 -12
- package/lib/client/sync/stream/AbstractStreamingSyncImplementation.js.map +1 -1
- package/lib/client/triggers/MemoryTriggerClaimManager.d.ts +6 -0
- package/lib/client/triggers/MemoryTriggerClaimManager.js +21 -0
- package/lib/client/triggers/MemoryTriggerClaimManager.js.map +1 -0
- package/lib/client/triggers/TriggerManager.d.ts +37 -0
- package/lib/client/triggers/TriggerManagerImpl.d.ts +24 -3
- package/lib/client/triggers/TriggerManagerImpl.js +133 -11
- package/lib/client/triggers/TriggerManagerImpl.js.map +1 -1
- package/lib/index.d.ts +13 -0
- package/lib/index.js +13 -0
- package/lib/index.js.map +1 -1
- package/lib/utils/DataStream.js +11 -2
- package/lib/utils/DataStream.js.map +1 -1
- package/lib/utils/mutex.d.ts +1 -1
- package/lib/utils/mutex.js.map +1 -1
- package/package.json +4 -3
- package/src/attachments/AttachmentContext.ts +279 -0
- package/src/attachments/AttachmentErrorHandler.ts +34 -0
- package/src/attachments/AttachmentQueue.ts +472 -0
- package/src/attachments/AttachmentService.ts +62 -0
- package/src/attachments/LocalStorageAdapter.ts +72 -0
- package/src/attachments/README.md +718 -0
- package/src/attachments/RemoteStorageAdapter.ts +30 -0
- package/src/attachments/Schema.ts +87 -0
- package/src/attachments/SyncingService.ts +193 -0
- package/src/attachments/WatchedAttachmentItem.ts +19 -0
- package/src/client/AbstractPowerSyncDatabase.ts +21 -6
- package/src/client/sync/stream/AbstractRemote.ts +47 -35
- package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +11 -14
- package/src/client/triggers/MemoryTriggerClaimManager.ts +25 -0
- package/src/client/triggers/TriggerManager.ts +50 -6
- package/src/client/triggers/TriggerManagerImpl.ts +177 -13
- package/src/index.ts +14 -0
- package/src/utils/DataStream.ts +13 -2
- package/src/utils/mutex.ts +1 -1
|
@@ -1,24 +1,62 @@
|
|
|
1
1
|
import { LockContext } from '../../db/DBAdapter.js';
|
|
2
2
|
import { Schema } from '../../db/schema/Schema.js';
|
|
3
|
-
import {
|
|
3
|
+
import type { AbstractPowerSyncDatabase } from '../AbstractPowerSyncDatabase.js';
|
|
4
4
|
import { DEFAULT_WATCH_THROTTLE_MS } from '../watched/WatchedQuery.js';
|
|
5
5
|
import {
|
|
6
6
|
CreateDiffTriggerOptions,
|
|
7
7
|
DiffTriggerOperation,
|
|
8
8
|
TrackDiffOptions,
|
|
9
9
|
TriggerManager,
|
|
10
|
+
TriggerManagerConfig,
|
|
10
11
|
TriggerRemoveCallback,
|
|
11
12
|
WithDiffOptions
|
|
12
13
|
} from './TriggerManager.js';
|
|
13
14
|
|
|
14
|
-
export type TriggerManagerImplOptions = {
|
|
15
|
+
export type TriggerManagerImplOptions = TriggerManagerConfig & {
|
|
15
16
|
db: AbstractPowerSyncDatabase;
|
|
16
17
|
schema: Schema;
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
export type TriggerManagerImplConfiguration = {
|
|
21
|
+
useStorageByDefault: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_TRIGGER_MANAGER_CONFIGURATION: TriggerManagerImplConfiguration = {
|
|
25
|
+
useStorageByDefault: false
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A record of persisted table/trigger information.
|
|
30
|
+
* This is used for fail-safe cleanup.
|
|
31
|
+
*/
|
|
32
|
+
type TrackedTableRecord = {
|
|
33
|
+
/**
|
|
34
|
+
* The id of the trigger. This is used in the SQLite trigger name
|
|
35
|
+
*/
|
|
36
|
+
id: string;
|
|
37
|
+
/**
|
|
38
|
+
* The destination table name for the trigger
|
|
39
|
+
*/
|
|
40
|
+
table: string;
|
|
41
|
+
/**
|
|
42
|
+
* Array of actual trigger names found for this table/id combo
|
|
43
|
+
*/
|
|
44
|
+
triggerNames: string[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const TRIGGER_CLEANUP_INTERVAL_MS = 120_000; // 2 minutes
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @internal
|
|
51
|
+
* @experimental
|
|
52
|
+
*/
|
|
19
53
|
export class TriggerManagerImpl implements TriggerManager {
|
|
20
54
|
protected schema: Schema;
|
|
21
55
|
|
|
56
|
+
protected defaultConfig: TriggerManagerImplConfiguration;
|
|
57
|
+
protected cleanupTimeout: ReturnType<typeof setTimeout> | null;
|
|
58
|
+
protected isDisposed: boolean;
|
|
59
|
+
|
|
22
60
|
constructor(protected options: TriggerManagerImplOptions) {
|
|
23
61
|
this.schema = options.schema;
|
|
24
62
|
options.db.registerListener({
|
|
@@ -26,6 +64,32 @@ export class TriggerManagerImpl implements TriggerManager {
|
|
|
26
64
|
this.schema = schema;
|
|
27
65
|
}
|
|
28
66
|
});
|
|
67
|
+
this.isDisposed = false;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Configure a cleanup to run on an interval.
|
|
71
|
+
* The interval is configured using setTimeout to take the async
|
|
72
|
+
* execution time of the callback into account.
|
|
73
|
+
*/
|
|
74
|
+
this.defaultConfig = DEFAULT_TRIGGER_MANAGER_CONFIGURATION;
|
|
75
|
+
const cleanupCallback = async () => {
|
|
76
|
+
this.cleanupTimeout = null;
|
|
77
|
+
if (this.isDisposed) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await this.cleanupResources();
|
|
82
|
+
} catch (ex) {
|
|
83
|
+
this.db.logger.error(`Caught error while attempting to cleanup triggers`, ex);
|
|
84
|
+
} finally {
|
|
85
|
+
// if not closed, set another timeout
|
|
86
|
+
if (this.isDisposed) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.cleanupTimeout = setTimeout(cleanupCallback, TRIGGER_CLEANUP_INTERVAL_MS);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
this.cleanupTimeout = setTimeout(cleanupCallback, TRIGGER_CLEANUP_INTERVAL_MS);
|
|
29
93
|
}
|
|
30
94
|
|
|
31
95
|
protected get db() {
|
|
@@ -48,14 +112,111 @@ export class TriggerManagerImpl implements TriggerManager {
|
|
|
48
112
|
}
|
|
49
113
|
}
|
|
50
114
|
|
|
115
|
+
dispose() {
|
|
116
|
+
this.isDisposed = true;
|
|
117
|
+
if (this.cleanupTimeout) {
|
|
118
|
+
clearTimeout(this.cleanupTimeout);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Updates default config settings for platform specific use-cases.
|
|
124
|
+
*/
|
|
125
|
+
updateDefaults(config: TriggerManagerImplConfiguration) {
|
|
126
|
+
this.defaultConfig = {
|
|
127
|
+
...this.defaultConfig,
|
|
128
|
+
...config
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protected generateTriggerName(operation: DiffTriggerOperation, destinationTable: string, triggerId: string) {
|
|
133
|
+
return `__ps_temp_trigger_${operation.toLowerCase()}__${destinationTable}__${triggerId}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Cleanup any SQLite triggers or tables that are no longer in use.
|
|
138
|
+
*/
|
|
139
|
+
async cleanupResources() {
|
|
140
|
+
// we use the database here since cleanupResources is called during the PowerSyncDatabase initialization
|
|
141
|
+
await this.db.database.writeLock(async (ctx) => {
|
|
142
|
+
/**
|
|
143
|
+
* Note: We only cleanup persisted triggers. These are tracked in the sqlite_master table.
|
|
144
|
+
* temporary triggers will not be affected by this.
|
|
145
|
+
* Query all triggers that match our naming pattern
|
|
146
|
+
*/
|
|
147
|
+
const triggers = await ctx.getAll<{ name: string }>(/* sql */ `
|
|
148
|
+
SELECT
|
|
149
|
+
name
|
|
150
|
+
FROM
|
|
151
|
+
sqlite_master
|
|
152
|
+
WHERE
|
|
153
|
+
type = 'trigger'
|
|
154
|
+
AND name LIKE '__ps_temp_trigger_%'
|
|
155
|
+
`);
|
|
156
|
+
|
|
157
|
+
/** Use regex to extract table names and IDs from trigger names
|
|
158
|
+
* Trigger naming convention: __ps_temp_trigger_<operation>__<destination_table>__<id>
|
|
159
|
+
*/
|
|
160
|
+
const triggerPattern = /^__ps_temp_trigger_(?:insert|update|delete)__(.+)__([a-f0-9_]{36})$/i;
|
|
161
|
+
const trackedItems = new Map<string, TrackedTableRecord>();
|
|
162
|
+
|
|
163
|
+
for (const trigger of triggers) {
|
|
164
|
+
const match = trigger.name.match(triggerPattern);
|
|
165
|
+
if (match) {
|
|
166
|
+
const [, table, id] = match;
|
|
167
|
+
// Collect all trigger names for each id combo
|
|
168
|
+
const existing = trackedItems.get(id);
|
|
169
|
+
if (existing) {
|
|
170
|
+
existing.triggerNames.push(trigger.name);
|
|
171
|
+
} else {
|
|
172
|
+
trackedItems.set(id, { table, id, triggerNames: [trigger.name] });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const trackedItem of trackedItems.values()) {
|
|
178
|
+
// check if there is anything holding on to this item
|
|
179
|
+
const hasClaim = await this.options.claimManager.checkClaim(trackedItem.id);
|
|
180
|
+
if (hasClaim) {
|
|
181
|
+
// This does not require cleanup
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.db.logger.debug(`Clearing resources for trigger ${trackedItem.id} with table ${trackedItem.table}`);
|
|
186
|
+
|
|
187
|
+
// We need to delete the triggers and table
|
|
188
|
+
for (const triggerName of trackedItem.triggerNames) {
|
|
189
|
+
await ctx.execute(`DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
190
|
+
}
|
|
191
|
+
await ctx.execute(`DROP TABLE IF EXISTS ${trackedItem.table}`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
51
196
|
async createDiffTrigger(options: CreateDiffTriggerOptions) {
|
|
52
197
|
await this.db.waitForReady();
|
|
53
|
-
const {
|
|
198
|
+
const {
|
|
199
|
+
source,
|
|
200
|
+
destination,
|
|
201
|
+
columns,
|
|
202
|
+
when,
|
|
203
|
+
hooks,
|
|
204
|
+
// Fall back to the provided default if not given on this level
|
|
205
|
+
useStorage = this.defaultConfig.useStorageByDefault
|
|
206
|
+
} = options;
|
|
54
207
|
const operations = Object.keys(when) as DiffTriggerOperation[];
|
|
55
208
|
if (operations.length == 0) {
|
|
56
209
|
throw new Error('At least one WHEN operation must be specified for the trigger.');
|
|
57
210
|
}
|
|
58
211
|
|
|
212
|
+
/**
|
|
213
|
+
* The clause to use when executing
|
|
214
|
+
* CREATE ${tableTriggerTypeClause} TABLE
|
|
215
|
+
* OR
|
|
216
|
+
* CREATE ${tableTriggerTypeClause} TRIGGER
|
|
217
|
+
*/
|
|
218
|
+
const tableTriggerTypeClause = !useStorage ? 'TEMP' : '';
|
|
219
|
+
|
|
59
220
|
const whenClauses = Object.fromEntries(
|
|
60
221
|
Object.entries(when).map(([operation, filter]) => [operation, `WHEN ${filter}`])
|
|
61
222
|
);
|
|
@@ -76,6 +237,8 @@ export class TriggerManagerImpl implements TriggerManager {
|
|
|
76
237
|
|
|
77
238
|
const id = await this.getUUID();
|
|
78
239
|
|
|
240
|
+
const releaseStorageClaim = useStorage ? await this.options.claimManager.obtainClaim(id) : null;
|
|
241
|
+
|
|
79
242
|
/**
|
|
80
243
|
* We default to replicating all columns if no columns array is provided.
|
|
81
244
|
*/
|
|
@@ -110,6 +273,7 @@ export class TriggerManagerImpl implements TriggerManager {
|
|
|
110
273
|
return this.db.writeLock(async (tx) => {
|
|
111
274
|
await this.removeTriggers(tx, triggerIds);
|
|
112
275
|
await tx.execute(/* sql */ `DROP TABLE IF EXISTS ${destination};`);
|
|
276
|
+
await releaseStorageClaim?.();
|
|
113
277
|
});
|
|
114
278
|
};
|
|
115
279
|
|
|
@@ -117,22 +281,22 @@ export class TriggerManagerImpl implements TriggerManager {
|
|
|
117
281
|
// Allow user code to execute in this lock context before the trigger is created.
|
|
118
282
|
await hooks?.beforeCreate?.(tx);
|
|
119
283
|
await tx.execute(/* sql */ `
|
|
120
|
-
CREATE
|
|
284
|
+
CREATE ${tableTriggerTypeClause} TABLE ${destination} (
|
|
121
285
|
operation_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
122
286
|
id TEXT,
|
|
123
287
|
operation TEXT,
|
|
124
288
|
timestamp TEXT,
|
|
125
289
|
value TEXT,
|
|
126
290
|
previous_value TEXT
|
|
127
|
-
)
|
|
291
|
+
)
|
|
128
292
|
`);
|
|
129
293
|
|
|
130
294
|
if (operations.includes(DiffTriggerOperation.INSERT)) {
|
|
131
|
-
const insertTriggerId =
|
|
295
|
+
const insertTriggerId = this.generateTriggerName(DiffTriggerOperation.INSERT, destination, id);
|
|
132
296
|
triggerIds.push(insertTriggerId);
|
|
133
297
|
|
|
134
298
|
await tx.execute(/* sql */ `
|
|
135
|
-
CREATE
|
|
299
|
+
CREATE ${tableTriggerTypeClause} TRIGGER ${insertTriggerId} AFTER INSERT ON ${internalSource} ${whenClauses[
|
|
136
300
|
DiffTriggerOperation.INSERT
|
|
137
301
|
]} BEGIN
|
|
138
302
|
INSERT INTO
|
|
@@ -145,16 +309,16 @@ export class TriggerManagerImpl implements TriggerManager {
|
|
|
145
309
|
${jsonFragment('NEW')}
|
|
146
310
|
);
|
|
147
311
|
|
|
148
|
-
END
|
|
312
|
+
END
|
|
149
313
|
`);
|
|
150
314
|
}
|
|
151
315
|
|
|
152
316
|
if (operations.includes(DiffTriggerOperation.UPDATE)) {
|
|
153
|
-
const updateTriggerId =
|
|
317
|
+
const updateTriggerId = this.generateTriggerName(DiffTriggerOperation.UPDATE, destination, id);
|
|
154
318
|
triggerIds.push(updateTriggerId);
|
|
155
319
|
|
|
156
320
|
await tx.execute(/* sql */ `
|
|
157
|
-
CREATE
|
|
321
|
+
CREATE ${tableTriggerTypeClause} TRIGGER ${updateTriggerId} AFTER
|
|
158
322
|
UPDATE ON ${internalSource} ${whenClauses[DiffTriggerOperation.UPDATE]} BEGIN
|
|
159
323
|
INSERT INTO
|
|
160
324
|
${destination} (id, operation, timestamp, value, previous_value)
|
|
@@ -172,12 +336,12 @@ export class TriggerManagerImpl implements TriggerManager {
|
|
|
172
336
|
}
|
|
173
337
|
|
|
174
338
|
if (operations.includes(DiffTriggerOperation.DELETE)) {
|
|
175
|
-
const deleteTriggerId =
|
|
339
|
+
const deleteTriggerId = this.generateTriggerName(DiffTriggerOperation.DELETE, destination, id);
|
|
176
340
|
triggerIds.push(deleteTriggerId);
|
|
177
341
|
|
|
178
342
|
// Create delete trigger for basic JSON
|
|
179
343
|
await tx.execute(/* sql */ `
|
|
180
|
-
CREATE
|
|
344
|
+
CREATE ${tableTriggerTypeClause} TRIGGER ${deleteTriggerId} AFTER DELETE ON ${internalSource} ${whenClauses[
|
|
181
345
|
DiffTriggerOperation.DELETE
|
|
182
346
|
]} BEGIN
|
|
183
347
|
INSERT INTO
|
|
@@ -227,7 +391,7 @@ export class TriggerManagerImpl implements TriggerManager {
|
|
|
227
391
|
const contextColumns = columns ?? sourceDefinition.columns.map((col) => col.name);
|
|
228
392
|
|
|
229
393
|
const id = await this.getUUID();
|
|
230
|
-
const destination = `
|
|
394
|
+
const destination = `__ps_temp_track_${source}_${id}`;
|
|
231
395
|
|
|
232
396
|
// register an onChange before the trigger is created
|
|
233
397
|
const abortController = new AbortController();
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
export * from './attachments/AttachmentContext.js';
|
|
2
|
+
export * from './attachments/AttachmentErrorHandler.js';
|
|
3
|
+
export * from './attachments/AttachmentQueue.js';
|
|
4
|
+
export * from './attachments/AttachmentService.js';
|
|
5
|
+
export * from './attachments/LocalStorageAdapter.js';
|
|
6
|
+
export * from './attachments/RemoteStorageAdapter.js';
|
|
7
|
+
export * from './attachments/Schema.js';
|
|
8
|
+
export * from './attachments/SyncingService.js';
|
|
9
|
+
export * from './attachments/WatchedAttachmentItem.js';
|
|
10
|
+
|
|
1
11
|
export * from './client/AbstractPowerSyncDatabase.js';
|
|
2
12
|
export * from './client/AbstractPowerSyncOpenFactory.js';
|
|
3
13
|
export { compilableQueryWatch, CompilableQueryWatchHandler } from './client/compilableQueryWatch.js';
|
|
@@ -29,13 +39,16 @@ export * from './db/DBAdapter.js';
|
|
|
29
39
|
export * from './db/schema/Column.js';
|
|
30
40
|
export * from './db/schema/Index.js';
|
|
31
41
|
export * from './db/schema/IndexedColumn.js';
|
|
42
|
+
export * from './db/schema/RawTable.js';
|
|
32
43
|
export * from './db/schema/Schema.js';
|
|
33
44
|
export * from './db/schema/Table.js';
|
|
34
45
|
export * from './db/schema/TableV2.js';
|
|
35
46
|
|
|
36
47
|
export * from './client/Query.js';
|
|
48
|
+
export { MEMORY_TRIGGER_CLAIM_MANAGER } from './client/triggers/MemoryTriggerClaimManager.js';
|
|
37
49
|
export * from './client/triggers/sanitizeSQL.js';
|
|
38
50
|
export * from './client/triggers/TriggerManager.js';
|
|
51
|
+
export { TriggerManagerImpl } from './client/triggers/TriggerManagerImpl.js';
|
|
39
52
|
export * from './client/watched/GetAllQuery.js';
|
|
40
53
|
export * from './client/watched/processors/AbstractQueryProcessor.js';
|
|
41
54
|
export * from './client/watched/processors/comparators.js';
|
|
@@ -48,6 +61,7 @@ export * from './utils/BaseObserver.js';
|
|
|
48
61
|
export * from './utils/ControlledExecutor.js';
|
|
49
62
|
export * from './utils/DataStream.js';
|
|
50
63
|
export * from './utils/Logger.js';
|
|
64
|
+
export * from './utils/mutex.js';
|
|
51
65
|
export * from './utils/parseQuery.js';
|
|
52
66
|
|
|
53
67
|
export * from './types/types.js';
|
package/src/utils/DataStream.ts
CHANGED
|
@@ -110,6 +110,18 @@ export class DataStream<ParsedData, SourceData = any> extends BaseObserver<DataS
|
|
|
110
110
|
return null;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// Wait for any pending processing to complete first.
|
|
114
|
+
// This ensures we register our listener before calling processQueue(),
|
|
115
|
+
// avoiding a race where processQueue() sees no reader and returns early.
|
|
116
|
+
if (this.processingPromise) {
|
|
117
|
+
await this.processingPromise;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Re-check after await - stream may have closed while we were waiting
|
|
121
|
+
if (this.closed) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
113
125
|
return new Promise((resolve, reject) => {
|
|
114
126
|
const l = this.registerListener({
|
|
115
127
|
data: async (data) => {
|
|
@@ -151,7 +163,7 @@ export class DataStream<ParsedData, SourceData = any> extends BaseObserver<DataS
|
|
|
151
163
|
|
|
152
164
|
const promise = (this.processingPromise = this._processQueue());
|
|
153
165
|
promise.finally(() => {
|
|
154
|
-
|
|
166
|
+
this.processingPromise = null;
|
|
155
167
|
});
|
|
156
168
|
return promise;
|
|
157
169
|
}
|
|
@@ -190,7 +202,6 @@ export class DataStream<ParsedData, SourceData = any> extends BaseObserver<DataS
|
|
|
190
202
|
}
|
|
191
203
|
|
|
192
204
|
if (this.dataQueue.length > 0) {
|
|
193
|
-
// Next tick
|
|
194
205
|
setTimeout(() => this.processQueue());
|
|
195
206
|
}
|
|
196
207
|
}
|
package/src/utils/mutex.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Mutex } from 'async-mutex';
|
|
|
6
6
|
export async function mutexRunExclusive<T>(
|
|
7
7
|
mutex: Mutex,
|
|
8
8
|
callback: () => Promise<T>,
|
|
9
|
-
options?: { timeoutMs
|
|
9
|
+
options?: { timeoutMs?: number }
|
|
10
10
|
): Promise<T> {
|
|
11
11
|
return new Promise((resolve, reject) => {
|
|
12
12
|
const timeout = options?.timeoutMs;
|