@pgflow/client 0.0.0-test-snapshot-releases-8d5d9bc1-20250922101013 → 0.0.0-testsnap-9294d743-20251207205914
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/CHANGELOG.md +104 -4
- package/README.md +6 -2
- package/dist/client/src/browser.d.ts +7 -0
- package/dist/client/src/browser.d.ts.map +1 -0
- package/dist/client/src/index.d.ts +6 -0
- package/dist/client/src/index.d.ts.map +1 -0
- package/dist/client/src/lib/FlowRun.d.ts +125 -0
- package/dist/client/src/lib/FlowRun.d.ts.map +1 -0
- package/dist/client/src/lib/FlowStep.d.ts +90 -0
- package/dist/client/src/lib/FlowStep.d.ts.map +1 -0
- package/dist/client/src/lib/PgflowClient.d.ts +76 -0
- package/dist/client/src/lib/PgflowClient.d.ts.map +1 -0
- package/dist/client/src/lib/SupabaseBroadcastAdapter.d.ts +66 -0
- package/dist/client/src/lib/SupabaseBroadcastAdapter.d.ts.map +1 -0
- package/dist/client/src/lib/eventAdapters.d.ts +21 -0
- package/dist/client/src/lib/eventAdapters.d.ts.map +1 -0
- package/dist/client/src/lib/types.d.ts +308 -0
- package/dist/client/src/lib/types.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +928 -0
- package/dist/index.js.map +1 -0
- package/dist/package.json +51 -0
- package/dist/pgflow-client.browser.js +2 -0
- package/dist/pgflow-client.browser.js.map +1 -0
- package/dist/src/browser.d.ts +7 -0
- package/dist/src/browser.d.ts.map +1 -0
- package/dist/src/browser.js +10 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +7 -0
- package/dist/src/lib/FlowRun.d.ts +125 -0
- package/dist/src/lib/FlowRun.d.ts.map +1 -0
- package/dist/src/lib/FlowRun.js +368 -0
- package/dist/src/lib/FlowStep.d.ts +90 -0
- package/dist/src/lib/FlowStep.d.ts.map +1 -0
- package/dist/src/lib/FlowStep.js +244 -0
- package/dist/src/lib/PgflowClient.d.ts +75 -0
- package/dist/src/lib/PgflowClient.d.ts.map +1 -0
- package/dist/src/lib/PgflowClient.js +227 -0
- package/dist/src/lib/SupabaseBroadcastAdapter.d.ts +65 -0
- package/dist/src/lib/SupabaseBroadcastAdapter.d.ts.map +1 -0
- package/dist/src/lib/SupabaseBroadcastAdapter.js +332 -0
- package/dist/src/lib/eventAdapters.d.ts +20 -0
- package/dist/src/lib/eventAdapters.d.ts.map +1 -0
- package/dist/src/lib/eventAdapters.js +142 -0
- package/dist/src/lib/types.d.ts +307 -0
- package/dist/src/lib/types.d.ts.map +1 -0
- package/dist/src/lib/types.js +91 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { createNanoEvents } from 'nanoevents';
|
|
2
|
+
import { FlowStepStatus } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Represents a single step in a flow run
|
|
5
|
+
*/
|
|
6
|
+
export class FlowStep {
|
|
7
|
+
#state;
|
|
8
|
+
#events = createNanoEvents();
|
|
9
|
+
#statusPrecedence = {
|
|
10
|
+
[FlowStepStatus.Created]: 0,
|
|
11
|
+
[FlowStepStatus.Started]: 1,
|
|
12
|
+
[FlowStepStatus.Completed]: 2,
|
|
13
|
+
[FlowStepStatus.Failed]: 3,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new FlowStep instance
|
|
17
|
+
*
|
|
18
|
+
* @param initialState - Initial state for the step
|
|
19
|
+
*/
|
|
20
|
+
constructor(initialState) {
|
|
21
|
+
this.#state = initialState;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get the run ID this step belongs to
|
|
25
|
+
*/
|
|
26
|
+
get run_id() {
|
|
27
|
+
return this.#state.run_id;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get the step slug
|
|
31
|
+
*/
|
|
32
|
+
get step_slug() {
|
|
33
|
+
return this.#state.step_slug;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get the current status
|
|
37
|
+
*/
|
|
38
|
+
get status() {
|
|
39
|
+
return this.#state.status;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get the started_at timestamp
|
|
43
|
+
*/
|
|
44
|
+
get started_at() {
|
|
45
|
+
return this.#state.started_at;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the completed_at timestamp
|
|
49
|
+
*/
|
|
50
|
+
get completed_at() {
|
|
51
|
+
return this.#state.completed_at;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get the failed_at timestamp
|
|
55
|
+
*/
|
|
56
|
+
get failed_at() {
|
|
57
|
+
return this.#state.failed_at;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get the step output
|
|
61
|
+
*/
|
|
62
|
+
get output() {
|
|
63
|
+
return this.#state.output;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get the error object
|
|
67
|
+
*/
|
|
68
|
+
get error() {
|
|
69
|
+
return this.#state.error;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get the error message
|
|
73
|
+
*/
|
|
74
|
+
get error_message() {
|
|
75
|
+
return this.#state.error_message;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Register an event handler for a step event
|
|
79
|
+
*
|
|
80
|
+
* @param event - Event type to listen for
|
|
81
|
+
* @param callback - Callback function to execute when event is emitted
|
|
82
|
+
* @returns Function to unsubscribe from the event
|
|
83
|
+
*/
|
|
84
|
+
on(event, callback) {
|
|
85
|
+
return this.#events.on(event, callback);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Wait for the step to reach a specific status
|
|
89
|
+
*
|
|
90
|
+
* @param targetStatus - The status to wait for
|
|
91
|
+
* @param options - Optional timeout and abort signal
|
|
92
|
+
* @returns Promise that resolves with the step instance when the status is reached
|
|
93
|
+
*/
|
|
94
|
+
waitForStatus(targetStatus, options) {
|
|
95
|
+
const timeoutMs = options?.timeoutMs ?? 5 * 60 * 1000; // Default 5 minutes
|
|
96
|
+
const { signal } = options || {};
|
|
97
|
+
// If we already have the target status, resolve immediately
|
|
98
|
+
if (this.status === targetStatus) {
|
|
99
|
+
return Promise.resolve(this);
|
|
100
|
+
}
|
|
101
|
+
// Otherwise, wait for the status to change
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
let timeoutId;
|
|
104
|
+
let cleanedUp = false;
|
|
105
|
+
// Set up timeout if provided
|
|
106
|
+
if (timeoutMs > 0) {
|
|
107
|
+
timeoutId = setTimeout(() => {
|
|
108
|
+
if (cleanedUp)
|
|
109
|
+
return; // Prevent firing if already cleaned up
|
|
110
|
+
cleanedUp = true;
|
|
111
|
+
unbind();
|
|
112
|
+
reject(new Error(`Timeout waiting for step ${this.step_slug} to reach status '${targetStatus}'`));
|
|
113
|
+
}, timeoutMs);
|
|
114
|
+
}
|
|
115
|
+
// Set up abort signal if provided
|
|
116
|
+
let abortCleanup;
|
|
117
|
+
if (signal) {
|
|
118
|
+
const abortHandler = () => {
|
|
119
|
+
if (cleanedUp)
|
|
120
|
+
return; // Prevent double cleanup
|
|
121
|
+
cleanedUp = true;
|
|
122
|
+
if (timeoutId)
|
|
123
|
+
clearTimeout(timeoutId);
|
|
124
|
+
unbind();
|
|
125
|
+
reject(new Error(`Aborted waiting for step ${this.step_slug} to reach status '${targetStatus}'`));
|
|
126
|
+
};
|
|
127
|
+
signal.addEventListener('abort', abortHandler);
|
|
128
|
+
abortCleanup = () => {
|
|
129
|
+
signal.removeEventListener('abort', abortHandler);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Subscribe to all events
|
|
133
|
+
const unbind = this.on('*', (event) => {
|
|
134
|
+
if (event.status === targetStatus) {
|
|
135
|
+
if (cleanedUp)
|
|
136
|
+
return; // Prevent double cleanup
|
|
137
|
+
cleanedUp = true;
|
|
138
|
+
if (timeoutId)
|
|
139
|
+
clearTimeout(timeoutId);
|
|
140
|
+
if (abortCleanup)
|
|
141
|
+
abortCleanup();
|
|
142
|
+
unbind();
|
|
143
|
+
resolve(this);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Apply state from database snapshot (no events emitted)
|
|
150
|
+
* Used when initializing state from start_flow_with_states() or get_run_with_states()
|
|
151
|
+
*
|
|
152
|
+
* @internal This method is only intended for use by PgflowClient.
|
|
153
|
+
* Applications should not call this directly.
|
|
154
|
+
*/
|
|
155
|
+
applySnapshot(row) {
|
|
156
|
+
// Direct state assignment from database row (no event conversion)
|
|
157
|
+
this.#state.status = row.status;
|
|
158
|
+
this.#state.started_at = row.started_at ? new Date(row.started_at) : null;
|
|
159
|
+
this.#state.completed_at = row.completed_at ? new Date(row.completed_at) : null;
|
|
160
|
+
this.#state.failed_at = row.failed_at ? new Date(row.failed_at) : null;
|
|
161
|
+
this.#state.error_message = row.error_message;
|
|
162
|
+
this.#state.error = row.error_message ? new Error(row.error_message) : null;
|
|
163
|
+
// Note: output is not stored in step_states table, remains null
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Updates the step state based on an event
|
|
167
|
+
*
|
|
168
|
+
* @internal This method is only intended for use by FlowRun and tests.
|
|
169
|
+
* Applications should not call this directly - state updates should come from
|
|
170
|
+
* database events through the PgflowClient.
|
|
171
|
+
*
|
|
172
|
+
* TODO: After v1.0, make this method private and refactor tests to use PgflowClient
|
|
173
|
+
* with event emission instead of direct state manipulation.
|
|
174
|
+
*/
|
|
175
|
+
updateState(event) {
|
|
176
|
+
// Validate event is for this step
|
|
177
|
+
if (event.step_slug !== this.#state.step_slug) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
// Validate event is for this run
|
|
181
|
+
if (event.run_id !== this.#state.run_id) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
// Check if the event status has higher precedence than current status
|
|
185
|
+
if (!this.#shouldUpdateStatus(this.#state.status, event.status)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
// Update state based on event type using narrowing type guards
|
|
189
|
+
switch (event.status) {
|
|
190
|
+
case FlowStepStatus.Started:
|
|
191
|
+
this.#state = {
|
|
192
|
+
...this.#state,
|
|
193
|
+
status: FlowStepStatus.Started,
|
|
194
|
+
started_at: typeof event.started_at === 'string' ? new Date(event.started_at) : new Date(),
|
|
195
|
+
};
|
|
196
|
+
this.#events.emit('started', event);
|
|
197
|
+
break;
|
|
198
|
+
case FlowStepStatus.Completed:
|
|
199
|
+
this.#state = {
|
|
200
|
+
...this.#state,
|
|
201
|
+
status: FlowStepStatus.Completed,
|
|
202
|
+
completed_at: typeof event.completed_at === 'string' ? new Date(event.completed_at) : new Date(),
|
|
203
|
+
output: event.output,
|
|
204
|
+
};
|
|
205
|
+
this.#events.emit('completed', event);
|
|
206
|
+
break;
|
|
207
|
+
case FlowStepStatus.Failed:
|
|
208
|
+
this.#state = {
|
|
209
|
+
...this.#state,
|
|
210
|
+
status: FlowStepStatus.Failed,
|
|
211
|
+
failed_at: typeof event.failed_at === 'string' ? new Date(event.failed_at) : new Date(),
|
|
212
|
+
error_message: typeof event.error_message === 'string' ? event.error_message : 'Unknown error',
|
|
213
|
+
error: new Error(typeof event.error_message === 'string' ? event.error_message : 'Unknown error'),
|
|
214
|
+
};
|
|
215
|
+
this.#events.emit('failed', event);
|
|
216
|
+
break;
|
|
217
|
+
default: {
|
|
218
|
+
// Exhaustiveness check - ensures all event statuses are handled
|
|
219
|
+
event;
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Also emit to the catch-all listener
|
|
224
|
+
this.#events.emit('*', event);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Determines if a status should be updated based on precedence
|
|
229
|
+
*
|
|
230
|
+
* @param currentStatus - Current status
|
|
231
|
+
* @param newStatus - New status
|
|
232
|
+
* @returns true if the status should be updated, false otherwise
|
|
233
|
+
*/
|
|
234
|
+
#shouldUpdateStatus(currentStatus, newStatus) {
|
|
235
|
+
// Don't allow changes to terminal states
|
|
236
|
+
if (currentStatus === FlowStepStatus.Completed || currentStatus === FlowStepStatus.Failed) {
|
|
237
|
+
return false; // Terminal states should never change
|
|
238
|
+
}
|
|
239
|
+
const currentPrecedence = this.#statusPrecedence[currentStatus];
|
|
240
|
+
const newPrecedence = this.#statusPrecedence[newStatus];
|
|
241
|
+
// Only allow transitions to higher precedence non-terminal status
|
|
242
|
+
return newPrecedence > currentPrecedence;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import type { AnyFlow, ExtractFlowInput } from '@pgflow/dsl';
|
|
3
|
+
import type { IFlowClient, BroadcastRunEvent, BroadcastStepEvent, Unsubscribe } from './types.js';
|
|
4
|
+
import { FlowRun } from './FlowRun.js';
|
|
5
|
+
/**
|
|
6
|
+
* Client for interacting with pgflow
|
|
7
|
+
*/
|
|
8
|
+
export declare class PgflowClient<TFlow extends AnyFlow = AnyFlow> implements IFlowClient<TFlow> {
|
|
9
|
+
#private;
|
|
10
|
+
/**
|
|
11
|
+
* Creates a new PgflowClient instance
|
|
12
|
+
*
|
|
13
|
+
* @param supabaseClient - Supabase client instance
|
|
14
|
+
* @param opts - Optional configuration
|
|
15
|
+
*/
|
|
16
|
+
constructor(supabaseClient: SupabaseClient, opts?: {
|
|
17
|
+
realtimeStabilizationDelayMs?: number;
|
|
18
|
+
schedule?: typeof setTimeout;
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Start a flow with optional run_id
|
|
22
|
+
*
|
|
23
|
+
* @param flow_slug - Flow slug to start
|
|
24
|
+
* @param input - Input data for the flow
|
|
25
|
+
* @param run_id - Optional run ID (will be generated if not provided)
|
|
26
|
+
* @returns Promise that resolves with the FlowRun instance
|
|
27
|
+
*/
|
|
28
|
+
startFlow<TSpecificFlow extends TFlow>(flow_slug: string, input: ExtractFlowInput<TSpecificFlow>, run_id?: string): Promise<FlowRun<TSpecificFlow>>;
|
|
29
|
+
/**
|
|
30
|
+
* Dispose a specific flow run
|
|
31
|
+
*
|
|
32
|
+
* @param runId - Run ID to dispose
|
|
33
|
+
*/
|
|
34
|
+
dispose(runId: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Dispose all flow runs
|
|
37
|
+
*/
|
|
38
|
+
disposeAll(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Fetch flow definition metadata
|
|
41
|
+
*/
|
|
42
|
+
fetchFlowDefinition(flow_slug: string): Promise<{
|
|
43
|
+
flow: import("pkgs/core/dist/types.js").FlowRow;
|
|
44
|
+
steps: import("pkgs/core/dist/types.js").StepRow[];
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Register a callback for run events
|
|
48
|
+
* @returns Function to unsubscribe from the event
|
|
49
|
+
*/
|
|
50
|
+
onRunEvent(callback: (event: BroadcastRunEvent) => void): Unsubscribe;
|
|
51
|
+
/**
|
|
52
|
+
* Register a callback for step events
|
|
53
|
+
* @returns Function to unsubscribe from the event
|
|
54
|
+
*/
|
|
55
|
+
onStepEvent(callback: (event: BroadcastStepEvent) => void): Unsubscribe;
|
|
56
|
+
/**
|
|
57
|
+
* Subscribe to a flow run's events
|
|
58
|
+
*/
|
|
59
|
+
subscribeToRun(run_id: string): Promise<() => void>;
|
|
60
|
+
/**
|
|
61
|
+
* Fetch current state of a run and its steps
|
|
62
|
+
*/
|
|
63
|
+
getRunWithStates(run_id: string): Promise<{
|
|
64
|
+
run: import("pkgs/core/dist/types.js").RunRow;
|
|
65
|
+
steps: import("pkgs/core/dist/types.js").StepStateRow[];
|
|
66
|
+
}>;
|
|
67
|
+
/**
|
|
68
|
+
* Get a flow run by ID
|
|
69
|
+
*
|
|
70
|
+
* @param run_id - ID of the run to get
|
|
71
|
+
* @returns Promise that resolves with the FlowRun instance or null if not found
|
|
72
|
+
*/
|
|
73
|
+
getRun<TSpecificFlow extends TFlow = TFlow>(run_id: string): Promise<FlowRun<TSpecificFlow> | null>;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=PgflowClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PgflowClient.d.ts","sourceRoot":"","sources":["../../../src/lib/PgflowClient.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,KAAK,EACV,WAAW,EAEX,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EAEZ,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC;;GAEG;AACH,qBAAa,YAAY,CAAC,KAAK,SAAS,OAAO,GAAG,OAAO,CAAE,YAAW,WAAW,CAAC,KAAK,CAAC;;IAOtF;;;;;OAKG;gBAED,cAAc,EAAE,cAAc,EAC9B,IAAI,GAAE;QACJ,4BAA4B,CAAC,EAAE,MAAM,CAAC;QACtC,QAAQ,CAAC,EAAE,OAAO,UAAU,CAAC;KACzB;IA4BR;;;;;;;OAOG;IACG,SAAS,CAAC,aAAa,SAAS,KAAK,EACzC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,gBAAgB,CAAC,aAAa,CAAC,EACtC,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAwDlC;;;;OAIG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAc5B;;OAEG;IACH,UAAU,IAAI,IAAI;IAQlB;;OAEG;IACG,mBAAmB,CAAC,SAAS,EAAE,MAAM;;;;IAI3C;;;OAGG;IACH,UAAU,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,WAAW;IAIrE;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAAG,WAAW;IAIvE;;OAEG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC;IAIzD;;OAEG;IACG,gBAAgB,CAAC,MAAM,EAAE,MAAM;;;;IAIrC;;;;;OAKG;IACG,MAAM,CAAC,aAAa,SAAS,KAAK,GAAG,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC;CA6E1G"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { FlowRunStatus } from './types.js';
|
|
3
|
+
import { SupabaseBroadcastAdapter } from './SupabaseBroadcastAdapter.js';
|
|
4
|
+
import { FlowRun } from './FlowRun.js';
|
|
5
|
+
import { toTypedRunEvent, toTypedStepEvent } from './eventAdapters.js';
|
|
6
|
+
/**
|
|
7
|
+
* Client for interacting with pgflow
|
|
8
|
+
*/
|
|
9
|
+
export class PgflowClient {
|
|
10
|
+
#supabase;
|
|
11
|
+
#realtimeAdapter;
|
|
12
|
+
// Use the widest event type - keeps the compiler happy but
|
|
13
|
+
// still provides the structural API we need (updateState/step/...)
|
|
14
|
+
#runs = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new PgflowClient instance
|
|
17
|
+
*
|
|
18
|
+
* @param supabaseClient - Supabase client instance
|
|
19
|
+
* @param opts - Optional configuration
|
|
20
|
+
*/
|
|
21
|
+
constructor(supabaseClient, opts = {}) {
|
|
22
|
+
this.#supabase = supabaseClient;
|
|
23
|
+
this.#realtimeAdapter = new SupabaseBroadcastAdapter(supabaseClient, {
|
|
24
|
+
stabilizationDelayMs: opts.realtimeStabilizationDelayMs,
|
|
25
|
+
schedule: opts.schedule,
|
|
26
|
+
});
|
|
27
|
+
// Set up global event listeners - properly typed
|
|
28
|
+
this.#realtimeAdapter.onRunEvent((event) => {
|
|
29
|
+
const run = this.#runs.get(event.run_id);
|
|
30
|
+
if (run) {
|
|
31
|
+
// Convert broadcast event to typed event before updating state
|
|
32
|
+
run.updateState(toTypedRunEvent(event));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
this.#realtimeAdapter.onStepEvent((event) => {
|
|
36
|
+
const run = this.#runs.get(event.run_id);
|
|
37
|
+
if (run) {
|
|
38
|
+
// Always materialize the step before updating to avoid event loss
|
|
39
|
+
// This ensures we cache all steps even if they were never explicitly requested
|
|
40
|
+
const stepSlug = event.step_slug;
|
|
41
|
+
run.step(stepSlug).updateState(toTypedStepEvent(event));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Start a flow with optional run_id
|
|
47
|
+
*
|
|
48
|
+
* @param flow_slug - Flow slug to start
|
|
49
|
+
* @param input - Input data for the flow
|
|
50
|
+
* @param run_id - Optional run ID (will be generated if not provided)
|
|
51
|
+
* @returns Promise that resolves with the FlowRun instance
|
|
52
|
+
*/
|
|
53
|
+
async startFlow(flow_slug, input, run_id) {
|
|
54
|
+
// Generate a run_id if not provided
|
|
55
|
+
const id = run_id || uuidv4();
|
|
56
|
+
// Create initial state for the flow run
|
|
57
|
+
const initialState = {
|
|
58
|
+
run_id: id,
|
|
59
|
+
flow_slug,
|
|
60
|
+
status: FlowRunStatus.Started,
|
|
61
|
+
input: input,
|
|
62
|
+
output: null,
|
|
63
|
+
error: null,
|
|
64
|
+
error_message: null,
|
|
65
|
+
started_at: null,
|
|
66
|
+
completed_at: null,
|
|
67
|
+
failed_at: null,
|
|
68
|
+
remaining_steps: -1, // Use -1 to indicate unknown until first snapshot arrives
|
|
69
|
+
};
|
|
70
|
+
// Create the flow run instance
|
|
71
|
+
const run = new FlowRun(initialState);
|
|
72
|
+
// Store the run
|
|
73
|
+
this.#runs.set(id, run);
|
|
74
|
+
// Set up subscription for run and step events (wait for subscription confirmation)
|
|
75
|
+
await this.#realtimeAdapter.subscribeToRun(id);
|
|
76
|
+
// Start the flow with the predetermined run_id (only after subscription is ready)
|
|
77
|
+
const { data, error } = await this.#supabase.schema('pgflow').rpc('start_flow_with_states', {
|
|
78
|
+
flow_slug: flow_slug,
|
|
79
|
+
input: input,
|
|
80
|
+
run_id: id
|
|
81
|
+
});
|
|
82
|
+
if (error) {
|
|
83
|
+
// Clean up subscription and run instance
|
|
84
|
+
this.dispose(id);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
// Apply the run state snapshot (no events)
|
|
88
|
+
if (data.run) {
|
|
89
|
+
run.applySnapshot(data.run);
|
|
90
|
+
}
|
|
91
|
+
// Apply step state snapshots (no events)
|
|
92
|
+
if (data.steps && Array.isArray(data.steps)) {
|
|
93
|
+
for (const stepState of data.steps) {
|
|
94
|
+
run.step(stepState.step_slug).applySnapshot(stepState);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return run;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Dispose a specific flow run
|
|
101
|
+
*
|
|
102
|
+
* @param runId - Run ID to dispose
|
|
103
|
+
*/
|
|
104
|
+
dispose(runId) {
|
|
105
|
+
const run = this.#runs.get(runId);
|
|
106
|
+
if (run) {
|
|
107
|
+
// First unsubscribe from the realtime adapter
|
|
108
|
+
this.#realtimeAdapter.unsubscribe(runId);
|
|
109
|
+
// Then dispose the run
|
|
110
|
+
run.dispose();
|
|
111
|
+
// Finally remove from the runs map
|
|
112
|
+
this.#runs.delete(runId);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Dispose all flow runs
|
|
117
|
+
*/
|
|
118
|
+
disposeAll() {
|
|
119
|
+
for (const runId of this.#runs.keys()) {
|
|
120
|
+
this.dispose(runId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Delegate IFlowRealtime methods to the adapter
|
|
124
|
+
/**
|
|
125
|
+
* Fetch flow definition metadata
|
|
126
|
+
*/
|
|
127
|
+
async fetchFlowDefinition(flow_slug) {
|
|
128
|
+
return this.#realtimeAdapter.fetchFlowDefinition(flow_slug);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Register a callback for run events
|
|
132
|
+
* @returns Function to unsubscribe from the event
|
|
133
|
+
*/
|
|
134
|
+
onRunEvent(callback) {
|
|
135
|
+
return this.#realtimeAdapter.onRunEvent(callback);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Register a callback for step events
|
|
139
|
+
* @returns Function to unsubscribe from the event
|
|
140
|
+
*/
|
|
141
|
+
onStepEvent(callback) {
|
|
142
|
+
return this.#realtimeAdapter.onStepEvent(callback);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Subscribe to a flow run's events
|
|
146
|
+
*/
|
|
147
|
+
async subscribeToRun(run_id) {
|
|
148
|
+
return await this.#realtimeAdapter.subscribeToRun(run_id);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Fetch current state of a run and its steps
|
|
152
|
+
*/
|
|
153
|
+
async getRunWithStates(run_id) {
|
|
154
|
+
return this.#realtimeAdapter.getRunWithStates(run_id);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get a flow run by ID
|
|
158
|
+
*
|
|
159
|
+
* @param run_id - ID of the run to get
|
|
160
|
+
* @returns Promise that resolves with the FlowRun instance or null if not found
|
|
161
|
+
*/
|
|
162
|
+
async getRun(run_id) {
|
|
163
|
+
// Check if we already have this run cached
|
|
164
|
+
const existingRun = this.#runs.get(run_id);
|
|
165
|
+
if (existingRun) {
|
|
166
|
+
return existingRun;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
// Fetch the run state from the database
|
|
170
|
+
const { run, steps } = await this.getRunWithStates(run_id);
|
|
171
|
+
if (!run) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
// Validate required fields
|
|
175
|
+
if (!run.run_id || !run.flow_slug || !run.status) {
|
|
176
|
+
throw new Error('Invalid run data: missing required fields');
|
|
177
|
+
}
|
|
178
|
+
// Validate status is a valid FlowRunStatus
|
|
179
|
+
const validStatuses = Object.values(FlowRunStatus);
|
|
180
|
+
if (!validStatuses.includes(run.status)) {
|
|
181
|
+
throw new Error(`Invalid run data: invalid status '${run.status}'`);
|
|
182
|
+
}
|
|
183
|
+
// Create flow run with minimal initial state
|
|
184
|
+
const initialState = {
|
|
185
|
+
run_id: run.run_id,
|
|
186
|
+
flow_slug: run.flow_slug,
|
|
187
|
+
status: run.status,
|
|
188
|
+
input: run.input,
|
|
189
|
+
output: null,
|
|
190
|
+
error: null,
|
|
191
|
+
error_message: null,
|
|
192
|
+
started_at: null,
|
|
193
|
+
completed_at: null,
|
|
194
|
+
failed_at: null,
|
|
195
|
+
remaining_steps: 0,
|
|
196
|
+
};
|
|
197
|
+
// Create the flow run instance
|
|
198
|
+
const flowRun = new FlowRun(initialState);
|
|
199
|
+
// Apply the complete state from database snapshot
|
|
200
|
+
flowRun.applySnapshot(run);
|
|
201
|
+
// Store the run
|
|
202
|
+
this.#runs.set(run_id, flowRun);
|
|
203
|
+
// Set up subscription for run and step events
|
|
204
|
+
await this.#realtimeAdapter.subscribeToRun(run_id);
|
|
205
|
+
// Initialize steps from snapshot
|
|
206
|
+
if (steps && Array.isArray(steps)) {
|
|
207
|
+
for (const stepState of steps) {
|
|
208
|
+
// Validate step has required fields
|
|
209
|
+
if (!stepState.step_slug || !stepState.status) {
|
|
210
|
+
throw new Error('Invalid step data: missing required fields');
|
|
211
|
+
}
|
|
212
|
+
// Apply snapshot state directly (no events)
|
|
213
|
+
flowRun.step(stepState.step_slug).applySnapshot(stepState);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return flowRun;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.error('Error getting run:', error);
|
|
220
|
+
// Re-throw if it's a validation error
|
|
221
|
+
if (error instanceof Error && (error.message.includes('Invalid run data') || error.message.includes('Invalid step data'))) {
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import type { FlowRow, StepRow, RunRow, StepStateRow } from '@pgflow/core';
|
|
3
|
+
import type { IFlowRealtime, BroadcastRunEvent, BroadcastStepEvent, Unsubscribe } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Adapter to handle realtime communication with Supabase
|
|
6
|
+
*/
|
|
7
|
+
export declare class SupabaseBroadcastAdapter implements IFlowRealtime {
|
|
8
|
+
#private;
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new instance of SupabaseBroadcastAdapter
|
|
11
|
+
*
|
|
12
|
+
* @param supabase - Supabase client instance
|
|
13
|
+
*/
|
|
14
|
+
constructor(supabase: SupabaseClient, opts?: {
|
|
15
|
+
reconnectDelayMs?: number;
|
|
16
|
+
stabilizationDelayMs?: number;
|
|
17
|
+
schedule?: typeof setTimeout;
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Fetches flow definition metadata from the database
|
|
21
|
+
*
|
|
22
|
+
* @param flow_slug - Flow slug to fetch
|
|
23
|
+
*/
|
|
24
|
+
fetchFlowDefinition(flow_slug: string): Promise<{
|
|
25
|
+
flow: FlowRow;
|
|
26
|
+
steps: StepRow[];
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Registers a callback for run events
|
|
30
|
+
*
|
|
31
|
+
* @param callback - Function to call when run events are received
|
|
32
|
+
* @returns Function to unsubscribe from the event
|
|
33
|
+
*/
|
|
34
|
+
onRunEvent(callback: (event: BroadcastRunEvent) => void): Unsubscribe;
|
|
35
|
+
/**
|
|
36
|
+
* Registers a callback for step events
|
|
37
|
+
*
|
|
38
|
+
* @param callback - Function to call when step events are received
|
|
39
|
+
* @returns Function to unsubscribe from the event
|
|
40
|
+
*/
|
|
41
|
+
onStepEvent(callback: (event: BroadcastStepEvent) => void): Unsubscribe;
|
|
42
|
+
/**
|
|
43
|
+
* Subscribes to a flow run's events
|
|
44
|
+
*
|
|
45
|
+
* @param run_id - Run ID to subscribe to
|
|
46
|
+
* @returns Function to unsubscribe
|
|
47
|
+
*/
|
|
48
|
+
subscribeToRun(run_id: string): Promise<() => void>;
|
|
49
|
+
/**
|
|
50
|
+
* Unsubscribes from a run's events
|
|
51
|
+
*
|
|
52
|
+
* @param run_id - Run ID to unsubscribe from
|
|
53
|
+
*/
|
|
54
|
+
unsubscribe(run_id: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Fetches current state of a run and its steps
|
|
57
|
+
*
|
|
58
|
+
* @param run_id - Run ID to fetch
|
|
59
|
+
*/
|
|
60
|
+
getRunWithStates(run_id: string): Promise<{
|
|
61
|
+
run: RunRow;
|
|
62
|
+
steps: StepStateRow[];
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=SupabaseBroadcastAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SupabaseBroadcastAdapter.d.ts","sourceRoot":"","sources":["../../../src/lib/SupabaseBroadcastAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE3E,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACZ,MAAM,YAAY,CAAC;AAQpB;;GAEG;AACH,qBAAa,wBAAyB,YAAW,aAAa;;IAS5D;;;;OAIG;gBAED,QAAQ,EAAE,cAAc,EACxB,IAAI,GAAE;QACJ,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,QAAQ,CAAC,EAAE,OAAO,UAAU,CAAC;KACzB;IAmLR;;;;OAIG;IACG,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QACpD,IAAI,EAAE,OAAO,CAAC;QACd,KAAK,EAAE,OAAO,EAAE,CAAC;KAClB,CAAC;IAiCF;;;;;OAKG;IACH,UAAU,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,WAAW;IAYrE;;;;;OAKG;IACH,WAAW,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAAG,WAAW;IAevE;;;;;OAKG;IACG,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC;IAgEzD;;;;OAIG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIjC;;;;OAIG;IACG,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAC9C,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,YAAY,EAAE,CAAC;KACvB,CAAC;CAiCH"}
|