Skip to content

Commit 1dd8450

Browse files
wip
1 parent a308b27 commit 1dd8450

File tree

5 files changed

+556
-162
lines changed

5 files changed

+556
-162
lines changed

packages/common/src/client/AbstractPowerSyncDatabase.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import {
3636
} from './sync/stream/AbstractStreamingSyncImplementation.js';
3737
import { TriggerManager } from './triggers/TriggerManager.js';
3838
import { TriggerManagerImpl } from './triggers/TriggerManagerImpl.js';
39-
import { WhenReadyTriggerManager } from './triggers/WhenReadyTriggerManager.js';
4039

4140
export interface DisconnectAndClearOptions {
4241
/** When set to false, data in local-only tables is preserved. */
@@ -185,7 +184,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
185184

186185
protected runExclusiveMutex: Mutex;
187186

188-
protected triggerManager: WhenReadyTriggerManager;
187+
protected triggerManager: TriggerManagerImpl;
189188

190189
get triggers(): TriggerManager {
191190
return this.triggerManager;
@@ -252,11 +251,8 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
252251

253252
this._isReadyPromise = this.initialize();
254253

255-
this.triggerManager = new WhenReadyTriggerManager({
256-
manager: new TriggerManagerImpl({
257-
db: this._database
258-
}),
259-
readyPromise: this.waitForReady()
254+
this.triggerManager = new TriggerManagerImpl({
255+
db: this
260256
});
261257
}
262258

packages/common/src/client/triggers/TriggerManager.ts

Lines changed: 126 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,45 @@
1+
import { LockContext } from 'src/db/DBAdapter.js';
2+
13
export enum DiffTriggerOperation {
24
INSERT = 'INSERT',
35
UPDATE = 'UPDATE',
46
DELETE = 'DELETE'
57
}
68

7-
export type TriggerResultUpdate = {
8-
operation: 'UPDATE';
9+
export interface BaseTriggerDiffRecord {
910
id: string;
11+
operation: DiffTriggerOperation;
12+
/**
13+
* Time the change operation was recorded.
14+
* This is in ISO 8601 format, e.g. `2023-10-01T12:00:00.000Z`.
15+
*/
16+
timestamp: string;
17+
}
18+
19+
export interface TriggerDiffUpdateRecord extends BaseTriggerDiffRecord {
20+
operation: DiffTriggerOperation.UPDATE;
1021
change: string;
11-
};
22+
}
1223

13-
export type TriggerResultInsert = {
14-
operation: 'INSERT';
15-
id: string;
24+
export interface TriggerDiffInsertRecord extends BaseTriggerDiffRecord {
25+
operation: DiffTriggerOperation.INSERT;
1626
change: string;
17-
};
27+
}
1828

19-
export type TriggerResultDelete = {
20-
id: string;
21-
operation: 'DELETE';
22-
};
29+
export interface TriggerDiffDeleteRecord extends BaseTriggerDiffRecord {
30+
operation: DiffTriggerOperation.DELETE;
31+
}
2332

24-
export type TriggerDiffResult = TriggerResultUpdate | TriggerResultInsert | TriggerResultDelete;
33+
export type TriggerDiffRecord = TriggerDiffUpdateRecord | TriggerDiffInsertRecord | TriggerDiffDeleteRecord;
2534

26-
export type CreateDiffTriggerOptions = {
35+
export interface TriggerCreationHooks {
36+
/**
37+
* Executed inside the write lock before the trigger is created.
38+
*/
39+
beforeCreate?: (context: LockContext) => Promise<void>;
40+
}
41+
42+
export interface CreateDiffTriggerOptions {
2743
/**
2844
* Source table to trigger and track changes from.
2945
*/
@@ -51,13 +67,59 @@ export type CreateDiffTriggerOptions = {
5167
* This is useful for only triggering on specific conditions.
5268
* For example, you can use it to only trigger on certain values in the NEW row.
5369
* Note that for PowerSync the data is stored in a JSON column named `data`.
54-
* @example `NEW.data.status = 'active' AND length(NEW.data.name) > 3`
70+
* @example
71+
* [`NEW.data.status = 'active' AND length(NEW.data.name) > 3`]
5572
*/
5673
when?: string;
57-
};
74+
75+
/**
76+
* Optional context to create the triggers in.
77+
* This can be useful to synchronize the current state and fetch all changes after the current state.
78+
*/
79+
hooks?: TriggerCreationHooks;
80+
}
5881

5982
export type TriggerRemoveCallback = () => Promise<void>;
6083

84+
export interface TriggerDiffHandlerContext {
85+
/**
86+
* Allows querying the database with access to the table containing diff records.
87+
* The diff table is accessible via the `diff` accessor.
88+
* The `DIFF` table is of the form:
89+
*
90+
* ```sql
91+
* CREATE TEMP TABLE ${destination} (
92+
* id TEXT,
93+
* operation TEXT,
94+
* change TEXT,
95+
* timestamp TEXT
96+
* );
97+
* ```
98+
* The `change` column contains the JSON representation of the change. This column is NULL for
99+
* {@link DiffTriggerOperation#DELETE} operations.
100+
* For {@link DiffTriggerOperation#UPDATE} operations the `change` column is a JSON object containing both the `new` and `old` values:
101+
*
102+
* @example
103+
* ```sql
104+
* SELECT
105+
* todos.*
106+
* FROM
107+
* DIFF
108+
* JOIN todos ON DIFF.id = todos.id
109+
* ```
110+
*/
111+
getAll: <T = any>(query: string, params?: any[]) => Promise<T[]>;
112+
}
113+
114+
export interface TrackDiffOptions {
115+
source: string;
116+
filter?: string;
117+
columns?: string[];
118+
operations: DiffTriggerOperation[];
119+
onChange: (context: TriggerDiffHandlerContext) => Promise<void>;
120+
hooks?: TriggerCreationHooks;
121+
}
122+
61123
export interface TriggerManager {
62124
/**
63125
* Creates a temporary trigger which tracks changes to a source table
@@ -66,4 +128,53 @@ export interface TriggerManager {
66128
* @returns A callback to remove the trigger and drop the destination table.
67129
*/
68130
createDiffTrigger(options: CreateDiffTriggerOptions): Promise<TriggerRemoveCallback>;
131+
132+
/**
133+
* Tracks changes for a table. Triggering a provided handler on changes.
134+
* Uses {@link createDiffTrigger} internally to create a temporary destination table.
135+
* @returns A callback to cleanup the trigger and stop tracking changes.
136+
*
137+
* @example
138+
* ```javascript
139+
* database.triggers.trackTableDiff({
140+
* source: 'ps_data__todos',
141+
* columns: ['list_id'],
142+
* operations: [DiffTriggerOperation.INSERT],
143+
* onChange: async (context) => {
144+
* console.log('Change detected, fetching new todos');
145+
* // Fetches the todo records that were inserted during this diff
146+
* const newTodos = await context.getAll<Database['todos']>("
147+
* SELECT
148+
* todos.*
149+
* FROM
150+
* DIFF
151+
* JOIN todos ON DIFF.id = todos.id
152+
* ");
153+
* todos.push(...newTodos);
154+
* },
155+
* hooks: {
156+
* beforeCreate: async (lockContext) => {
157+
* // This hook is executed inside the write lock before the trigger is created.
158+
* // It can be used to synchronize the current state and fetch all changes after the current state.
159+
* // Read the current state of the todos table
160+
* const currentTodos = await lockContext.getAll<Database['todos']>(
161+
* "
162+
* SELECT
163+
* *
164+
* FROM
165+
* todos
166+
* WHERE
167+
* list_id = ?
168+
* ",
169+
* [firstList.id]
170+
* );
171+
*
172+
* // Example code could process the current todos if necessary
173+
* todos.push(...currentTodos);
174+
* }
175+
* }
176+
* });
177+
* ```
178+
*/
179+
trackTableDiff(options: TrackDiffOptions): Promise<TriggerRemoveCallback>;
69180
}

0 commit comments

Comments
 (0)