@tanstack/powersync-db-collection 0.1.69 → 0.2.2
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/cjs/PowerSyncTransactor.cjs +31 -18
- package/dist/cjs/PowerSyncTransactor.cjs.map +1 -1
- package/dist/cjs/PowerSyncTransactor.d.cts +5 -5
- package/dist/cjs/definitions.cjs.map +1 -1
- package/dist/cjs/definitions.d.cts +4 -4
- package/dist/cjs/helpers.cjs.map +1 -1
- package/dist/cjs/helpers.d.cts +6 -9
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/powersync.cjs +214 -53
- package/dist/cjs/powersync.cjs.map +1 -1
- package/dist/cjs/serialization.cjs.map +1 -1
- package/dist/cjs/serialization.d.cts +2 -6
- package/dist/esm/PowerSyncTransactor.d.ts +5 -5
- package/dist/esm/PowerSyncTransactor.js +32 -19
- package/dist/esm/PowerSyncTransactor.js.map +1 -1
- package/dist/esm/definitions.d.ts +4 -4
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/helpers.d.ts +6 -9
- package/dist/esm/helpers.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/powersync.js +215 -54
- package/dist/esm/powersync.js.map +1 -1
- package/dist/esm/serialization.d.ts +2 -6
- package/dist/esm/serialization.js.map +1 -1
- package/package.json +7 -6
- package/src/PowerSyncTransactor.ts +47 -26
- package/src/definitions.ts +11 -5
- package/src/helpers.ts +10 -18
- package/src/index.ts +1 -0
- package/src/powersync.ts +258 -62
- package/src/serialization.ts +8 -17
package/dist/cjs/powersync.cjs
CHANGED
|
@@ -9,6 +9,9 @@ const definitions = require("./definitions.cjs");
|
|
|
9
9
|
const helpers = require("./helpers.cjs");
|
|
10
10
|
const schema = require("./schema.cjs");
|
|
11
11
|
const serialization = require("./serialization.cjs");
|
|
12
|
+
function isPromiseLike(value) {
|
|
13
|
+
return !!value && (typeof value === `object` || typeof value === `function`) && typeof value.then === `function`;
|
|
14
|
+
}
|
|
12
15
|
function powerSyncCollectionOptions(config) {
|
|
13
16
|
const outputConfig = createPowerSyncCollectionConfig(config);
|
|
14
17
|
return db.withCollectionConfigFactory(
|
|
@@ -36,12 +39,15 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
36
39
|
return validation.value;
|
|
37
40
|
} else if (`issues` in validation) {
|
|
38
41
|
const issueMessage = `Failed to validate incoming data for ${viewName}. Issues: ${validation.issues.map((issue) => `${issue.path} - ${issue.message}`)}`;
|
|
39
|
-
database.logger.error
|
|
42
|
+
database.logger.log({ level: common.LogLevels.error, message: issueMessage });
|
|
40
43
|
onDeserializationError(validation);
|
|
41
44
|
throw new Error(issueMessage);
|
|
42
45
|
} else {
|
|
43
46
|
const unknownErrorMessage = `Unknown deserialization error for ${viewName}`;
|
|
44
|
-
database.logger.
|
|
47
|
+
database.logger.log({
|
|
48
|
+
level: common.LogLevels.error,
|
|
49
|
+
message: unknownErrorMessage
|
|
50
|
+
});
|
|
45
51
|
onDeserializationError({ issues: [{ message: unknownErrorMessage }] });
|
|
46
52
|
throw new Error(unknownErrorMessage);
|
|
47
53
|
}
|
|
@@ -60,6 +66,8 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
60
66
|
const abortController = new AbortController();
|
|
61
67
|
let disposeTracking = null;
|
|
62
68
|
let trackingSetup = null;
|
|
69
|
+
let activeTrackingDisposal = null;
|
|
70
|
+
let abortTrackingDisposal = null;
|
|
63
71
|
if (syncMode === `eager`) {
|
|
64
72
|
return runEagerSync();
|
|
65
73
|
} else {
|
|
@@ -72,10 +80,29 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
72
80
|
}
|
|
73
81
|
const dispose = disposeTracking;
|
|
74
82
|
if (!dispose) {
|
|
83
|
+
await activeTrackingDisposal;
|
|
75
84
|
return;
|
|
76
85
|
}
|
|
77
86
|
disposeTracking = null;
|
|
78
|
-
|
|
87
|
+
let disposal;
|
|
88
|
+
try {
|
|
89
|
+
disposal = Promise.resolve(dispose(context ? { context } : void 0));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
disposal = Promise.reject(error);
|
|
92
|
+
}
|
|
93
|
+
activeTrackingDisposal = disposal;
|
|
94
|
+
try {
|
|
95
|
+
await disposal;
|
|
96
|
+
} finally {
|
|
97
|
+
if (activeTrackingDisposal === disposal) {
|
|
98
|
+
activeTrackingDisposal = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function disposeTrackingAfterAbort() {
|
|
103
|
+
const disposal = abortTrackingDisposal ??= safelyDisposeTracking();
|
|
104
|
+
void disposal.catch(() => void 0);
|
|
105
|
+
return disposal;
|
|
79
106
|
}
|
|
80
107
|
async function establishTracking(options, appliedReceipts) {
|
|
81
108
|
const setup = (async () => {
|
|
@@ -96,6 +123,7 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
96
123
|
return await database.triggers.createDiffTrigger({
|
|
97
124
|
source: viewName,
|
|
98
125
|
destination: trackedTableName,
|
|
126
|
+
columns: table.columns.map((column) => column.name),
|
|
99
127
|
setupContext,
|
|
100
128
|
when,
|
|
101
129
|
hooks: {
|
|
@@ -119,9 +147,10 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
119
147
|
}
|
|
120
148
|
appliedReceipts.push(commit());
|
|
121
149
|
}
|
|
122
|
-
database.logger.
|
|
123
|
-
|
|
124
|
-
|
|
150
|
+
database.logger.log({
|
|
151
|
+
level: common.LogLevels.info,
|
|
152
|
+
message: `Sync is ready for ${viewName} into ${trackedTableName}`
|
|
153
|
+
});
|
|
125
154
|
}
|
|
126
155
|
}
|
|
127
156
|
});
|
|
@@ -138,10 +167,11 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
138
167
|
await database.writeTransaction(async (context) => {
|
|
139
168
|
await flushDiffRecordsWithContext(context, ignoredReceipts);
|
|
140
169
|
}).catch((error) => {
|
|
141
|
-
database.logger.
|
|
142
|
-
|
|
170
|
+
database.logger.log({
|
|
171
|
+
level: common.LogLevels.error,
|
|
172
|
+
message: `An error has been detected in the sync handler`,
|
|
143
173
|
error
|
|
144
|
-
);
|
|
174
|
+
});
|
|
145
175
|
});
|
|
146
176
|
}
|
|
147
177
|
async function flushDiffRecordsWithContext(context, appliedReceipts) {
|
|
@@ -181,16 +211,18 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
181
211
|
appliedReceipts.push(applied);
|
|
182
212
|
pendingOperationStore.resolvePendingFor(pendingOperations);
|
|
183
213
|
} catch (error) {
|
|
184
|
-
database.logger.
|
|
185
|
-
|
|
214
|
+
database.logger.log({
|
|
215
|
+
level: common.LogLevels.error,
|
|
216
|
+
message: `An error has been detected in the sync handler`,
|
|
186
217
|
error
|
|
187
|
-
);
|
|
218
|
+
});
|
|
188
219
|
}
|
|
189
220
|
}
|
|
190
221
|
async function start(afterOnChangeRegistered) {
|
|
191
|
-
database.logger.
|
|
192
|
-
|
|
193
|
-
|
|
222
|
+
database.logger.log({
|
|
223
|
+
level: common.LogLevels.info,
|
|
224
|
+
message: `Sync is starting for ${viewName} into ${trackedTableName}`
|
|
225
|
+
});
|
|
194
226
|
database.onChangeWithCallback(
|
|
195
227
|
{
|
|
196
228
|
onChange: async () => {
|
|
@@ -205,12 +237,12 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
205
237
|
);
|
|
206
238
|
await afterOnChangeRegistered?.();
|
|
207
239
|
if (abortController.signal.aborted) {
|
|
208
|
-
await
|
|
240
|
+
await disposeTrackingAfterAbort();
|
|
209
241
|
} else {
|
|
210
242
|
abortController.signal.addEventListener(
|
|
211
243
|
`abort`,
|
|
212
|
-
|
|
213
|
-
|
|
244
|
+
() => {
|
|
245
|
+
void disposeTrackingAfterAbort();
|
|
214
246
|
},
|
|
215
247
|
{ once: true }
|
|
216
248
|
);
|
|
@@ -218,13 +250,13 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
218
250
|
}
|
|
219
251
|
function runEagerSync() {
|
|
220
252
|
let onUnload = null;
|
|
221
|
-
|
|
253
|
+
let onUnloadStarted = false;
|
|
254
|
+
const startup = start(async () => {
|
|
222
255
|
const cleanup = await restConfig.onLoad?.();
|
|
256
|
+
onUnload = cleanup;
|
|
223
257
|
if (abortController.signal.aborted) {
|
|
224
|
-
cleanup?.();
|
|
225
258
|
return;
|
|
226
259
|
}
|
|
227
|
-
onUnload = cleanup;
|
|
228
260
|
const appliedReceipts = [];
|
|
229
261
|
await establishTracking(
|
|
230
262
|
{
|
|
@@ -247,38 +279,77 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
247
279
|
await Promise.all(appliedReceipts);
|
|
248
280
|
markReady();
|
|
249
281
|
}).catch((error) => {
|
|
250
|
-
database.logger.
|
|
251
|
-
|
|
282
|
+
database.logger.log({
|
|
283
|
+
level: common.LogLevels.error,
|
|
284
|
+
message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,
|
|
252
285
|
error
|
|
253
|
-
);
|
|
286
|
+
});
|
|
254
287
|
if (collection.status === `loading`) {
|
|
255
288
|
markError(error);
|
|
256
289
|
}
|
|
257
290
|
});
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
291
|
+
const invokeOnUnload = () => {
|
|
292
|
+
if (onUnloadStarted || !onUnload) return;
|
|
293
|
+
onUnloadStarted = true;
|
|
294
|
+
const cleanup = onUnload;
|
|
295
|
+
onUnload = null;
|
|
296
|
+
return cleanup();
|
|
297
|
+
};
|
|
298
|
+
return async () => {
|
|
299
|
+
database.logger.log({
|
|
300
|
+
level: common.LogLevels.info,
|
|
301
|
+
message: `Sync has been stopped for ${viewName} into ${trackedTableName}`
|
|
302
|
+
});
|
|
262
303
|
abortController.abort();
|
|
263
|
-
|
|
304
|
+
const trackingDisposal = disposeTrackingAfterAbort();
|
|
305
|
+
let onUnloadFailure;
|
|
306
|
+
const settleOnUnload = async () => {
|
|
307
|
+
try {
|
|
308
|
+
await invokeOnUnload();
|
|
309
|
+
} catch (error) {
|
|
310
|
+
onUnloadFailure ??= { error };
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
const settlements = await Promise.allSettled([
|
|
314
|
+
startup,
|
|
315
|
+
trackingDisposal,
|
|
316
|
+
settleOnUnload()
|
|
317
|
+
]);
|
|
318
|
+
const disposalSettlement = settlements[1];
|
|
319
|
+
await settleOnUnload();
|
|
320
|
+
if (disposalSettlement.status === `rejected` && onUnloadFailure) {
|
|
321
|
+
throw new AggregateError(
|
|
322
|
+
[disposalSettlement.reason, onUnloadFailure.error],
|
|
323
|
+
`PowerSync tracking disposal and load-hook cleanup both failed`,
|
|
324
|
+
{ cause: disposalSettlement.reason }
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (disposalSettlement.status === `rejected`) {
|
|
328
|
+
throw disposalSettlement.reason;
|
|
329
|
+
}
|
|
330
|
+
if (onUnloadFailure) throw onUnloadFailure.error;
|
|
264
331
|
};
|
|
265
332
|
}
|
|
266
333
|
function runOnDemandSync() {
|
|
267
334
|
const demands = /* @__PURE__ */ new Map();
|
|
268
335
|
const releasedSubsets = /* @__PURE__ */ new WeakSet();
|
|
269
336
|
const pendingReleases = [];
|
|
337
|
+
const pendingDemandLoads = /* @__PURE__ */ new Set();
|
|
338
|
+
const pendingDemandCleanups = /* @__PURE__ */ new Set();
|
|
270
339
|
let stopped = false;
|
|
271
340
|
let trackingRevision = 0;
|
|
272
341
|
let reconciledTrackingRevision = 0;
|
|
273
342
|
let rebuildPromise = null;
|
|
274
343
|
let drainingReleases = false;
|
|
275
344
|
let releaseRetryTimer;
|
|
345
|
+
let demandCleanupCollector = null;
|
|
276
346
|
const startup = start();
|
|
277
347
|
void startup.catch(
|
|
278
|
-
(error) => database.logger.
|
|
279
|
-
|
|
348
|
+
(error) => database.logger.log({
|
|
349
|
+
level: common.LogLevels.error,
|
|
350
|
+
message: `Could not start syncing process for ${viewName} into ${trackedTableName}`,
|
|
280
351
|
error
|
|
281
|
-
)
|
|
352
|
+
})
|
|
282
353
|
);
|
|
283
354
|
const activeWhereExpressions = () => Array.from(demands.values()).filter((demand) => demand.active).map((demand) => demand.options.where);
|
|
284
355
|
const reconcileTracking = async () => {
|
|
@@ -369,6 +440,13 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
369
440
|
if (cleanup) demand.cleanup = cleanup;
|
|
370
441
|
} catch (error) {
|
|
371
442
|
demands.delete(options);
|
|
443
|
+
const collector = demandCleanupCollector;
|
|
444
|
+
if (collector) {
|
|
445
|
+
collector.failures.push({
|
|
446
|
+
order: collector.nextOrder++,
|
|
447
|
+
error
|
|
448
|
+
});
|
|
449
|
+
}
|
|
372
450
|
throw error;
|
|
373
451
|
}
|
|
374
452
|
if (
|
|
@@ -376,14 +454,20 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
376
454
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
377
455
|
stopped || releasedSubsets.has(options) || options.signal?.aborted || demands.get(options) !== demand
|
|
378
456
|
) {
|
|
379
|
-
|
|
380
|
-
demand.cleanup?.();
|
|
457
|
+
cleanupDemand(demand);
|
|
381
458
|
return;
|
|
382
459
|
}
|
|
383
460
|
demand.active = true;
|
|
384
461
|
trackingRevision++;
|
|
385
462
|
await rebuildTracking();
|
|
386
463
|
};
|
|
464
|
+
const trackLoadSubset = (options) => {
|
|
465
|
+
const task = loadSubset(options);
|
|
466
|
+
pendingDemandLoads.add(task);
|
|
467
|
+
const finish = () => pendingDemandLoads.delete(task);
|
|
468
|
+
void task.then(finish, finish);
|
|
469
|
+
return task;
|
|
470
|
+
};
|
|
387
471
|
const toInlinedWhereClause = (compiled) => {
|
|
388
472
|
if (!compiled.where) return "TRUE";
|
|
389
473
|
const sqlParts = compiled.where.split("?");
|
|
@@ -392,15 +476,55 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
392
476
|
...compiled.params
|
|
393
477
|
);
|
|
394
478
|
};
|
|
479
|
+
const reportDemandCleanupFailure = (error) => {
|
|
480
|
+
database.logger.log({
|
|
481
|
+
level: common.LogLevels.error,
|
|
482
|
+
message: `Could not clean up subset hook for ${viewName}`,
|
|
483
|
+
error
|
|
484
|
+
});
|
|
485
|
+
};
|
|
395
486
|
const cleanupDemand = (demand) => {
|
|
396
487
|
demands.delete(demand.options);
|
|
488
|
+
const collector = demandCleanupCollector;
|
|
489
|
+
const order = collector ? collector.nextOrder++ : void 0;
|
|
397
490
|
try {
|
|
398
|
-
demand.cleanup
|
|
491
|
+
const cleanup = demand.cleanup;
|
|
492
|
+
demand.cleanup = void 0;
|
|
493
|
+
const result = cleanup?.();
|
|
494
|
+
if (!isPromiseLike(result)) return;
|
|
495
|
+
const task = Promise.resolve(result);
|
|
496
|
+
pendingDemandCleanups.add(task);
|
|
497
|
+
const finish = () => pendingDemandCleanups.delete(task);
|
|
498
|
+
void task.then(finish, finish);
|
|
499
|
+
if (collector) {
|
|
500
|
+
collector.tasks.push({ order, promise: task });
|
|
501
|
+
collector.ownedTasks.add(task);
|
|
502
|
+
void task.catch(() => void 0);
|
|
503
|
+
} else {
|
|
504
|
+
void task.catch((error) => {
|
|
505
|
+
if (!demandCleanupCollector?.ownedTasks.has(task)) {
|
|
506
|
+
reportDemandCleanupFailure(error);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
399
510
|
} catch (error) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
511
|
+
if (collector) {
|
|
512
|
+
collector.failures.push({ order, error });
|
|
513
|
+
} else {
|
|
514
|
+
reportDemandCleanupFailure(error);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const throwDemandCleanupFailures = (collector, primaryFailures = []) => {
|
|
519
|
+
const errors = [
|
|
520
|
+
...primaryFailures,
|
|
521
|
+
...collector.failures.slice().sort((left, right) => left.order - right.order).map(({ error }) => error)
|
|
522
|
+
];
|
|
523
|
+
if (errors.length === 1) throw errors[0];
|
|
524
|
+
if (errors.length > 1) {
|
|
525
|
+
throw new AggregateError(errors, `PowerSync cleanup failed`, {
|
|
526
|
+
cause: errors[0]
|
|
527
|
+
});
|
|
404
528
|
}
|
|
405
529
|
};
|
|
406
530
|
const performPhysicalRelease = async (options) => {
|
|
@@ -464,10 +588,11 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
464
588
|
3e4
|
|
465
589
|
);
|
|
466
590
|
retryDelay = retryDelay === 0 ? delay : Math.min(retryDelay, delay);
|
|
467
|
-
database.logger.
|
|
468
|
-
|
|
591
|
+
database.logger.log({
|
|
592
|
+
level: common.LogLevels.error,
|
|
593
|
+
message: `Could not release subset tracking for ${viewName}; retrying`,
|
|
469
594
|
error
|
|
470
|
-
);
|
|
595
|
+
});
|
|
471
596
|
}
|
|
472
597
|
}
|
|
473
598
|
} finally {
|
|
@@ -495,16 +620,58 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
495
620
|
stopped = true;
|
|
496
621
|
clearTimeout(releaseRetryTimer);
|
|
497
622
|
releaseRetryTimer = void 0;
|
|
498
|
-
database.logger.
|
|
499
|
-
|
|
500
|
-
|
|
623
|
+
database.logger.log({
|
|
624
|
+
level: common.LogLevels.info,
|
|
625
|
+
message: `Sync has been stopped for ${viewName} into ${trackedTableName}`
|
|
626
|
+
});
|
|
501
627
|
abortController.abort();
|
|
628
|
+
const trackingDisposal = disposeTrackingAfterAbort();
|
|
629
|
+
const pendingLoads = [...pendingDemandLoads];
|
|
630
|
+
const pendingTasks = [...pendingDemandCleanups];
|
|
631
|
+
const collector = {
|
|
632
|
+
tasks: pendingTasks.map((promise, order) => ({ order, promise })),
|
|
633
|
+
ownedTasks: new Set(pendingTasks),
|
|
634
|
+
failures: [],
|
|
635
|
+
nextOrder: pendingTasks.length
|
|
636
|
+
};
|
|
637
|
+
demandCleanupCollector = collector;
|
|
502
638
|
for (const demand of demands.values()) {
|
|
503
639
|
cleanupDemand(demand);
|
|
504
640
|
}
|
|
505
641
|
pendingReleases.length = 0;
|
|
642
|
+
return (async () => {
|
|
643
|
+
const ownershipSettlements = await Promise.allSettled([
|
|
644
|
+
trackingDisposal,
|
|
645
|
+
...pendingLoads
|
|
646
|
+
]);
|
|
647
|
+
const disposalSettlement = ownershipSettlements[0];
|
|
648
|
+
let settledTasks = 0;
|
|
649
|
+
while (settledTasks < collector.tasks.length) {
|
|
650
|
+
const tasks = collector.tasks.slice(settledTasks);
|
|
651
|
+
settledTasks = collector.tasks.length;
|
|
652
|
+
const outcomes = await Promise.allSettled(
|
|
653
|
+
tasks.map(({ promise }) => promise)
|
|
654
|
+
);
|
|
655
|
+
for (const [index, outcome] of outcomes.entries()) {
|
|
656
|
+
if (outcome.status === `rejected`) {
|
|
657
|
+
collector.failures.push({
|
|
658
|
+
order: tasks[index].order,
|
|
659
|
+
error: outcome.reason
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
throwDemandCleanupFailures(
|
|
665
|
+
collector,
|
|
666
|
+
disposalSettlement.status === `rejected` ? [disposalSettlement.reason] : []
|
|
667
|
+
);
|
|
668
|
+
})().finally(() => {
|
|
669
|
+
if (demandCleanupCollector === collector) {
|
|
670
|
+
demandCleanupCollector = null;
|
|
671
|
+
}
|
|
672
|
+
});
|
|
506
673
|
},
|
|
507
|
-
loadSubset:
|
|
674
|
+
loadSubset: trackLoadSubset,
|
|
508
675
|
unloadSubset
|
|
509
676
|
};
|
|
510
677
|
}
|
|
@@ -535,13 +702,7 @@ function createPowerSyncCollectionConfig(config) {
|
|
|
535
702
|
tableName: viewName,
|
|
536
703
|
trackedTableName,
|
|
537
704
|
metadataIsTracked,
|
|
538
|
-
serializeValue: (value) => serialization.serializeForSQLite(
|
|
539
|
-
value,
|
|
540
|
-
// This is required by the input generic
|
|
541
|
-
table,
|
|
542
|
-
// Coerce serializer to the shape that corresponds to the Table constructed from OutputType
|
|
543
|
-
serializer
|
|
544
|
-
)
|
|
705
|
+
serializeValue: (value) => serialization.serializeForSQLite(value, table, serializer)
|
|
545
706
|
})
|
|
546
707
|
}
|
|
547
708
|
};
|