Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quick-needles-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/common': minor
---

Added `setupContext` option to `CreateDiffTriggerOptions` and a lock context option to the cleanup function returned by `createDiffTrigger`, this allows you to create and dispose of a trigger inside of a lock context.
17 changes: 15 additions & 2 deletions packages/common/src/client/triggers/TriggerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,27 @@ export interface CreateDiffTriggerOptions extends BaseCreateDiffTriggerOptions {
* This table will be dropped once the trigger is removed.
*/
destination: string;

/**
* Context to use for the setup operation.
* This is useful for when the setup operation needs to be executed in a specific context.
*/
setupContext?: LockContext;
}

/**
* @experimental
* Callback to drop a trigger after it has been created.
* Options for {@link TriggerRemoveCallback}.
*/
export type TriggerRemoveCallback = () => Promise<void>;
export interface TriggerRemoveCallbackOptions {
context?: LockContext;
}

/**
* @experimental
* Callback to drop a trigger after it has been created.
*/
export type TriggerRemoveCallback = (options?: TriggerRemoveCallbackOptions) => Promise<void>;
/**
* @experimental
* Options for {@link TriggerDiffHandlerContext#withDiff}.
Expand Down
24 changes: 18 additions & 6 deletions packages/common/src/client/triggers/TriggerManagerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
TriggerManager,
TriggerManagerConfig,
TriggerRemoveCallback,
TriggerRemoveCallbackOptions,
WithDiffOptions
} from './TriggerManager.js';

Expand Down Expand Up @@ -201,6 +202,7 @@ export class TriggerManagerImpl implements TriggerManager {
columns,
when,
hooks,
setupContext,
// Fall back to the provided default if not given on this level
useStorage = this.defaultConfig.useStorageByDefault
} = options;
Expand Down Expand Up @@ -268,13 +270,19 @@ export class TriggerManagerImpl implements TriggerManager {
* we need to ensure we can cleanup the created resources.
* We unfortunately cannot rely on transaction rollback.
*/
const cleanup = async () => {
const cleanup = async (options?: TriggerRemoveCallbackOptions) => {
const { context } = options ?? {};
disposeWarningListener();
return this.db.writeLock(async (tx) => {
const doCleanup = async (tx: LockContext) => {
await this.removeTriggers(tx, triggerIds);
await tx.execute(/* sql */ `DROP TABLE IF EXISTS ${destination};`);
await tx.execute(`DROP TABLE IF EXISTS ${destination};`);
await releaseStorageClaim?.();
});
};
if (context) {
await doCleanup(context);
} else {
await this.db.writeLock(doCleanup);
}
};

const setup = async (tx: LockContext) => {
Expand Down Expand Up @@ -360,11 +368,15 @@ export class TriggerManagerImpl implements TriggerManager {
};

try {
await this.db.writeLock(setup);
if (setupContext) {
await setup(setupContext);
} else {
await this.db.writeLock(setup);
}
return cleanup;
} catch (error) {
try {
await cleanup();
await cleanup(setupContext ? { context: setupContext } : undefined);
} catch (cleanupError) {
throw new AggregateError([error, cleanupError], 'Error during operation and cleanup');
}
Expand Down
43 changes: 43 additions & 0 deletions packages/node/tests/trigger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,49 @@ describe('Triggers', () => {
);
});

databaseTest('Recreating trigger with write lock on dispose and re-creation', async ({ database }) => {
const destination = 'temp_recreate';
const columns: Array<keyof Database['todos']> = ['content'];
const when = { [DiffTriggerOperation.INSERT]: 'TRUE' };

let dispose = await database.triggers.createDiffTrigger({
source: 'todos',
destination,
columns,
when
});

// Dispose the trigger (drops destination table + triggers)
await database.writeLock(async (tx) => {
await dispose({ context: tx });

// Create new trigger in the same lock context — no nested lock needed
dispose = await database.triggers.createDiffTrigger({
source: 'todos',
destination,
columns,
when,
setupContext: tx
});
});

// Verify new trigger works after locked context recreation
await database.execute("INSERT INTO todos (id, content) VALUES (uuid(), 'after recreate');");

await vi.waitFor(
async () => {
const rows = await database.writeLock(async (tx) =>
tx.getAll<TriggerDiffRecord>(`SELECT * FROM ${destination}`)
);
expect(rows.length).toBe(1);
expect(JSON.parse(rows[0].value).content).toBe('after recreate');
},
{ timeout: 1000 }
);

await dispose();
});

databaseTest('Should use persisted tables and triggers', async ({ database }) => {
const table = 'temp_remote_lists';

Expand Down
6 changes: 3 additions & 3 deletions packages/web/tests/triggers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('Triggers', () => {
}
});

onTestFinished(disposeTrigger);
onTestFinished(() => disposeTrigger());

await db.execute("INSERT INTO customers (id, name) VALUES (uuid(), 'test')");

Expand Down Expand Up @@ -103,7 +103,7 @@ describe('Triggers', () => {
}
});

onTestFinished(disposeTrigger);
onTestFinished(() => disposeTrigger());

await db.execute("INSERT INTO customers (id, name) VALUES (uuid(), 'test')");

Expand Down Expand Up @@ -221,7 +221,7 @@ describe('Triggers', () => {
}
});

onTestFinished(disposeTrigger);
onTestFinished(() => disposeTrigger());

// Perform an insert from client B
await dbB.execute("INSERT INTO customers (id, name) VALUES (uuid(), 'from-client-b')");
Expand Down
Loading