@powersync/common 1.44.0 → 1.46.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/dist/bundle.cjs +442 -2057
- package/dist/bundle.cjs.map +1 -1
- package/dist/bundle.mjs +439 -2058
- package/dist/bundle.mjs.map +1 -1
- package/dist/bundle.node.cjs +341 -127
- package/dist/bundle.node.cjs.map +1 -1
- package/dist/bundle.node.mjs +338 -128
- package/dist/bundle.node.mjs.map +1 -1
- package/dist/index.d.cts +263 -164
- 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/ConnectionManager.d.ts +1 -1
- package/lib/client/ConnectionManager.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/db/ConnectionClosedError.d.ts +10 -0
- package/lib/db/ConnectionClosedError.js +21 -0
- package/lib/db/ConnectionClosedError.js.map +1 -0
- package/lib/db/crud/SyncStatus.d.ts +11 -2
- package/lib/db/crud/SyncStatus.js +19 -1
- package/lib/db/crud/SyncStatus.js.map +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/utils/DataStream.js +11 -2
- package/lib/utils/DataStream.js.map +1 -1
- package/package.json +4 -3
- package/src/client/AbstractPowerSyncDatabase.ts +21 -6
- package/src/client/ConnectionManager.ts +1 -1
- package/src/client/sync/stream/AbstractRemote.ts +47 -35
- package/src/client/sync/stream/AbstractStreamingSyncImplementation.ts +11 -15
- 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/db/ConnectionClosedError.ts +23 -0
- package/src/db/crud/SyncStatus.ts +22 -3
- package/src/index.ts +4 -0
- package/src/utils/DataStream.ts +13 -2
|
@@ -40,7 +40,8 @@ import {
|
|
|
40
40
|
} from './sync/stream/AbstractStreamingSyncImplementation.js';
|
|
41
41
|
import { CoreSyncStatus, coreStatusToJs } from './sync/stream/core-instruction.js';
|
|
42
42
|
import { SyncStream } from './sync/sync-streams.js';
|
|
43
|
-
import {
|
|
43
|
+
import { MEMORY_TRIGGER_CLAIM_MANAGER } from './triggers/MemoryTriggerClaimManager.js';
|
|
44
|
+
import { TriggerManager, TriggerManagerConfig } from './triggers/TriggerManager.js';
|
|
44
45
|
import { TriggerManagerImpl } from './triggers/TriggerManagerImpl.js';
|
|
45
46
|
import { DEFAULT_WATCH_THROTTLE_MS, WatchCompatibleQuery } from './watched/WatchedQuery.js';
|
|
46
47
|
import { OnChangeQueryProcessor } from './watched/processors/OnChangeQueryProcessor.js';
|
|
@@ -222,6 +223,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
|
|
|
222
223
|
* Allows creating SQLite triggers which can be used to track various operations on SQLite tables.
|
|
223
224
|
*/
|
|
224
225
|
readonly triggers: TriggerManager;
|
|
226
|
+
protected triggersImpl: TriggerManagerImpl;
|
|
225
227
|
|
|
226
228
|
logger: ILogger;
|
|
227
229
|
|
|
@@ -296,9 +298,10 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
|
|
|
296
298
|
|
|
297
299
|
this._isReadyPromise = this.initialize();
|
|
298
300
|
|
|
299
|
-
this.triggers = new TriggerManagerImpl({
|
|
301
|
+
this.triggers = this.triggersImpl = new TriggerManagerImpl({
|
|
300
302
|
db: this,
|
|
301
|
-
schema: this.schema
|
|
303
|
+
schema: this.schema,
|
|
304
|
+
...this.generateTriggerManagerConfig()
|
|
302
305
|
});
|
|
303
306
|
}
|
|
304
307
|
|
|
@@ -334,6 +337,16 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
|
|
|
334
337
|
*/
|
|
335
338
|
protected abstract openDBAdapter(options: PowerSyncDatabaseOptionsWithSettings): DBAdapter;
|
|
336
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Generates a base configuration for {@link TriggerManagerImpl}.
|
|
342
|
+
* Implementations should override this if necessary.
|
|
343
|
+
*/
|
|
344
|
+
protected generateTriggerManagerConfig(): TriggerManagerConfig {
|
|
345
|
+
return {
|
|
346
|
+
claimManager: MEMORY_TRIGGER_CLAIM_MANAGER
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
337
350
|
protected abstract generateSyncStreamImplementation(
|
|
338
351
|
connector: PowerSyncBackendConnector,
|
|
339
352
|
options: CreateSyncImplementationOptions & RequiredAdditionalConnectionOptions
|
|
@@ -416,15 +429,16 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
|
|
|
416
429
|
protected async initialize() {
|
|
417
430
|
await this._initialize();
|
|
418
431
|
await this.bucketStorageAdapter.init();
|
|
419
|
-
await this.
|
|
432
|
+
await this.loadVersion();
|
|
420
433
|
await this.updateSchema(this.options.schema);
|
|
421
434
|
await this.resolveOfflineSyncStatus();
|
|
422
435
|
await this.database.execute('PRAGMA RECURSIVE_TRIGGERS=TRUE');
|
|
436
|
+
await this.triggersImpl.cleanupResources();
|
|
423
437
|
this.ready = true;
|
|
424
438
|
this.iterateListeners((cb) => cb.initialized?.());
|
|
425
439
|
}
|
|
426
440
|
|
|
427
|
-
|
|
441
|
+
protected async loadVersion() {
|
|
428
442
|
try {
|
|
429
443
|
const { version } = await this.database.get<{ version: string }>('SELECT powersync_rs_version() as version');
|
|
430
444
|
this.sdkVersion = version;
|
|
@@ -560,7 +574,6 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
|
|
|
560
574
|
|
|
561
575
|
const { clearLocal } = options;
|
|
562
576
|
|
|
563
|
-
// TODO DB name, verify this is necessary with extension
|
|
564
577
|
await this.database.writeTransaction(async (tx) => {
|
|
565
578
|
await tx.execute('SELECT powersync_clear(?)', [clearLocal ? 1 : 0]);
|
|
566
579
|
});
|
|
@@ -597,6 +610,8 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
|
|
|
597
610
|
return;
|
|
598
611
|
}
|
|
599
612
|
|
|
613
|
+
this.triggersImpl.dispose();
|
|
614
|
+
|
|
600
615
|
await this.iterateAsyncListeners(async (cb) => cb.closing?.());
|
|
601
616
|
|
|
602
617
|
const { disconnect } = options;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ILogger } from 'js-logger';
|
|
2
|
+
import { SyncStatus } from '../db/crud/SyncStatus.js';
|
|
2
3
|
import { BaseListener, BaseObserver } from '../utils/BaseObserver.js';
|
|
3
4
|
import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js';
|
|
4
5
|
import {
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
SyncStreamSubscribeOptions,
|
|
14
15
|
SyncStreamSubscription
|
|
15
16
|
} from './sync/sync-streams.js';
|
|
16
|
-
import { SyncStatus } from '../db/crud/SyncStatus.js';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @internal
|
|
@@ -573,7 +573,11 @@ export abstract class AbstractRemote {
|
|
|
573
573
|
|
|
574
574
|
const stream = new DataStream<T, string>({
|
|
575
575
|
logger: this.logger,
|
|
576
|
-
mapLine: mapLine
|
|
576
|
+
mapLine: mapLine,
|
|
577
|
+
pressure: {
|
|
578
|
+
highWaterMark: 20,
|
|
579
|
+
lowWaterMark: 10
|
|
580
|
+
}
|
|
577
581
|
});
|
|
578
582
|
|
|
579
583
|
abortSignal?.addEventListener('abort', () => {
|
|
@@ -585,46 +589,54 @@ export abstract class AbstractRemote {
|
|
|
585
589
|
let buffer = '';
|
|
586
590
|
|
|
587
591
|
|
|
592
|
+
const consumeStream = async () => {
|
|
593
|
+
while (!stream.closed && !abortSignal?.aborted && !readerReleased) {
|
|
594
|
+
const { done, value } = await reader.read();
|
|
595
|
+
if (done) {
|
|
596
|
+
const remaining = buffer.trim();
|
|
597
|
+
if (remaining.length != 0) {
|
|
598
|
+
stream.enqueueData(remaining);
|
|
599
|
+
}
|
|
588
600
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
try {
|
|
595
|
-
let didCompleteLine = false;
|
|
596
|
-
while (!didCompleteLine) {
|
|
597
|
-
const { done, value } = await reader.read();
|
|
598
|
-
if (done) {
|
|
599
|
-
const remaining = buffer.trim();
|
|
600
|
-
if (remaining.length != 0) {
|
|
601
|
-
stream.enqueueData(remaining);
|
|
602
|
-
}
|
|
601
|
+
stream.close();
|
|
602
|
+
await closeReader();
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
603
605
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
606
|
+
const data = decoder.decode(value, { stream: true });
|
|
607
|
+
buffer += data;
|
|
608
608
|
|
|
609
|
-
|
|
610
|
-
|
|
609
|
+
const lines = buffer.split('\n');
|
|
610
|
+
for (var i = 0; i < lines.length - 1; i++) {
|
|
611
|
+
var l = lines[i].trim();
|
|
612
|
+
if (l.length > 0) {
|
|
613
|
+
stream.enqueueData(l);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
611
616
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
617
|
+
buffer = lines[lines.length - 1];
|
|
618
|
+
|
|
619
|
+
// Implement backpressure by waiting for the low water mark to be reached
|
|
620
|
+
if (stream.dataQueue.length > stream.highWatermark) {
|
|
621
|
+
await new Promise<void>((resolve) => {
|
|
622
|
+
const dispose = stream.registerListener({
|
|
623
|
+
lowWater: async () => {
|
|
624
|
+
resolve();
|
|
625
|
+
dispose();
|
|
626
|
+
},
|
|
627
|
+
closed: () => {
|
|
628
|
+
resolve();
|
|
629
|
+
dispose();
|
|
618
630
|
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
buffer = lines[lines.length - 1];
|
|
622
|
-
}
|
|
623
|
-
} catch (ex) {
|
|
624
|
-
stream.close();
|
|
625
|
-
throw ex;
|
|
631
|
+
})
|
|
632
|
+
})
|
|
626
633
|
}
|
|
627
|
-
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
consumeStream().catch(ex => this.logger.error('Error consuming stream', ex));
|
|
638
|
+
|
|
639
|
+
const l = stream.registerListener({
|
|
628
640
|
closed: () => {
|
|
629
641
|
closeReader();
|
|
630
642
|
l?.();
|
|
@@ -47,18 +47,17 @@ export enum SyncClientImplementation {
|
|
|
47
47
|
*
|
|
48
48
|
* This is the default option.
|
|
49
49
|
*
|
|
50
|
-
* @deprecated
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* @deprecated We recommend the {@link RUST} client implementation for all apps. If you have issues with
|
|
51
|
+
* the Rust client, please file an issue or reach out to us. The JavaScript client will be removed in a future
|
|
52
|
+
* version of the PowerSync SDK.
|
|
53
53
|
*/
|
|
54
54
|
JAVASCRIPT = 'js',
|
|
55
55
|
/**
|
|
56
56
|
* This implementation offloads the sync line decoding and handling into the PowerSync
|
|
57
57
|
* core extension.
|
|
58
58
|
*
|
|
59
|
-
* @
|
|
60
|
-
*
|
|
61
|
-
* it has seen less real-world testing and is marked as __experimental__ at the moment.
|
|
59
|
+
* This option is more performant than the {@link JAVASCRIPT} client, enabled by default and the
|
|
60
|
+
* recommended client implementation for all apps.
|
|
62
61
|
*
|
|
63
62
|
* ## Compatibility warning
|
|
64
63
|
*
|
|
@@ -77,13 +76,9 @@ export enum SyncClientImplementation {
|
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
/**
|
|
80
|
-
* The default {@link SyncClientImplementation} to use.
|
|
81
|
-
*
|
|
82
|
-
* Please use this field instead of {@link SyncClientImplementation.JAVASCRIPT} directly. A future version
|
|
83
|
-
* of the PowerSync SDK will enable {@link SyncClientImplementation.RUST} by default and remove the JavaScript
|
|
84
|
-
* option.
|
|
79
|
+
* The default {@link SyncClientImplementation} to use, {@link SyncClientImplementation.RUST}.
|
|
85
80
|
*/
|
|
86
|
-
export const DEFAULT_SYNC_CLIENT_IMPLEMENTATION = SyncClientImplementation.
|
|
81
|
+
export const DEFAULT_SYNC_CLIENT_IMPLEMENTATION = SyncClientImplementation.RUST;
|
|
87
82
|
|
|
88
83
|
/**
|
|
89
84
|
* Abstract Lock to be implemented by various JS environments
|
|
@@ -194,8 +189,7 @@ export interface RequiredAdditionalConnectionOptions extends Required<Additional
|
|
|
194
189
|
}
|
|
195
190
|
|
|
196
191
|
export interface StreamingSyncImplementation
|
|
197
|
-
extends BaseObserverInterface<StreamingSyncImplementationListener>,
|
|
198
|
-
Disposable {
|
|
192
|
+
extends BaseObserverInterface<StreamingSyncImplementationListener>, Disposable {
|
|
199
193
|
/**
|
|
200
194
|
* Connects to the sync service
|
|
201
195
|
*/
|
|
@@ -693,6 +687,9 @@ The next upload iteration will be delayed.`);
|
|
|
693
687
|
if (rawTables != null && rawTables.length) {
|
|
694
688
|
this.logger.warn('Raw tables require the Rust-based sync client. The JS client will ignore them.');
|
|
695
689
|
}
|
|
690
|
+
if (this.activeStreams.length) {
|
|
691
|
+
this.logger.error('Sync streams require `clientImplementation: SyncClientImplementation.RUST` when connecting.');
|
|
692
|
+
}
|
|
696
693
|
|
|
697
694
|
this.logger.debug('Streaming sync iteration started');
|
|
698
695
|
this.options.adapter.startSession();
|
|
@@ -1241,7 +1238,6 @@ The next upload iteration will be delayed.`);
|
|
|
1241
1238
|
resolve();
|
|
1242
1239
|
return;
|
|
1243
1240
|
}
|
|
1244
|
-
|
|
1245
1241
|
const { retryDelayMs } = this.options;
|
|
1246
1242
|
|
|
1247
1243
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TriggerClaimManager } from './TriggerManager.js';
|
|
2
|
+
|
|
3
|
+
const CLAIM_STORE = new Map<string, () => Promise<void>>();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @internal
|
|
7
|
+
* @experimental
|
|
8
|
+
*/
|
|
9
|
+
export const MEMORY_TRIGGER_CLAIM_MANAGER: TriggerClaimManager = {
|
|
10
|
+
async obtainClaim(identifier: string): Promise<() => Promise<void>> {
|
|
11
|
+
if (CLAIM_STORE.has(identifier)) {
|
|
12
|
+
throw new Error(`A claim is already present for ${identifier}`);
|
|
13
|
+
}
|
|
14
|
+
const release = async () => {
|
|
15
|
+
CLAIM_STORE.delete(identifier);
|
|
16
|
+
};
|
|
17
|
+
CLAIM_STORE.set(identifier, release);
|
|
18
|
+
|
|
19
|
+
return release;
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
async checkClaim(identifier: string): Promise<boolean> {
|
|
23
|
+
return CLAIM_STORE.has(identifier);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -47,8 +47,9 @@ export interface BaseTriggerDiffRecord<TOperationId extends string | number = nu
|
|
|
47
47
|
* This record contains the new value and optionally the previous value.
|
|
48
48
|
* Values are stored as JSON strings.
|
|
49
49
|
*/
|
|
50
|
-
export interface TriggerDiffUpdateRecord<
|
|
51
|
-
extends
|
|
50
|
+
export interface TriggerDiffUpdateRecord<
|
|
51
|
+
TOperationId extends string | number = number
|
|
52
|
+
> extends BaseTriggerDiffRecord<TOperationId> {
|
|
52
53
|
operation: DiffTriggerOperation.UPDATE;
|
|
53
54
|
/**
|
|
54
55
|
* The updated state of the row in JSON string format.
|
|
@@ -65,8 +66,9 @@ export interface TriggerDiffUpdateRecord<TOperationId extends string | number =
|
|
|
65
66
|
* Represents a diff record for a SQLite INSERT operation.
|
|
66
67
|
* This record contains the new value represented as a JSON string.
|
|
67
68
|
*/
|
|
68
|
-
export interface TriggerDiffInsertRecord<
|
|
69
|
-
extends
|
|
69
|
+
export interface TriggerDiffInsertRecord<
|
|
70
|
+
TOperationId extends string | number = number
|
|
71
|
+
> extends BaseTriggerDiffRecord<TOperationId> {
|
|
70
72
|
operation: DiffTriggerOperation.INSERT;
|
|
71
73
|
/**
|
|
72
74
|
* The value of the row, at the time of INSERT, in JSON string format.
|
|
@@ -79,8 +81,9 @@ export interface TriggerDiffInsertRecord<TOperationId extends string | number =
|
|
|
79
81
|
* Represents a diff record for a SQLite DELETE operation.
|
|
80
82
|
* This record contains the new value represented as a JSON string.
|
|
81
83
|
*/
|
|
82
|
-
export interface TriggerDiffDeleteRecord<
|
|
83
|
-
extends
|
|
84
|
+
export interface TriggerDiffDeleteRecord<
|
|
85
|
+
TOperationId extends string | number = number
|
|
86
|
+
> extends BaseTriggerDiffRecord<TOperationId> {
|
|
84
87
|
operation: DiffTriggerOperation.DELETE;
|
|
85
88
|
/**
|
|
86
89
|
* The value of the row, before the DELETE operation, in JSON string format.
|
|
@@ -201,6 +204,12 @@ interface BaseCreateDiffTriggerOptions {
|
|
|
201
204
|
* Hooks which allow execution during the trigger creation process.
|
|
202
205
|
*/
|
|
203
206
|
hooks?: TriggerCreationHooks;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Use storage-backed (non-TEMP) tables and triggers that persist across sessions.
|
|
210
|
+
* These resources are still automatically disposed when no longer claimed.
|
|
211
|
+
*/
|
|
212
|
+
useStorage?: boolean;
|
|
204
213
|
}
|
|
205
214
|
|
|
206
215
|
/**
|
|
@@ -449,3 +458,38 @@ export interface TriggerManager {
|
|
|
449
458
|
*/
|
|
450
459
|
trackTableDiff(options: TrackDiffOptions): Promise<TriggerRemoveCallback>;
|
|
451
460
|
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* @experimental
|
|
464
|
+
* @internal
|
|
465
|
+
* Manages claims on persisted SQLite triggers and destination tables to enable proper cleanup
|
|
466
|
+
* when they are no longer actively in use.
|
|
467
|
+
*
|
|
468
|
+
* When using persisted triggers (especially for OPFS multi-tab scenarios), we need a reliable way to determine which resources are still actively in use across different connections/tabs so stale resources can be safely cleaned up without interfering with active triggers.
|
|
469
|
+
*
|
|
470
|
+
* A cleanup process runs
|
|
471
|
+
* on database creation (and every 2 minutes) that:
|
|
472
|
+
* 1. Queries for existing managed persisted resources
|
|
473
|
+
* 2. Checks with the claim manager if any consumer is actively using those resources
|
|
474
|
+
* 3. Deletes unused resources
|
|
475
|
+
*/
|
|
476
|
+
|
|
477
|
+
export interface TriggerClaimManager {
|
|
478
|
+
/**
|
|
479
|
+
* Obtains or marks a claim on a certain identifier.
|
|
480
|
+
* @returns a callback to release the claim.
|
|
481
|
+
*/
|
|
482
|
+
obtainClaim: (identifier: string) => Promise<() => Promise<void>>;
|
|
483
|
+
/**
|
|
484
|
+
* Checks if a claim is present for an identifier.
|
|
485
|
+
*/
|
|
486
|
+
checkClaim: (identifier: string) => Promise<boolean>;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* @experimental
|
|
491
|
+
* @internal
|
|
492
|
+
*/
|
|
493
|
+
export interface TriggerManagerConfig {
|
|
494
|
+
claimManager: TriggerClaimManager;
|
|
495
|
+
}
|
|
@@ -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();
|