@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +104 -4
  2. package/README.md +6 -2
  3. package/dist/client/src/browser.d.ts +7 -0
  4. package/dist/client/src/browser.d.ts.map +1 -0
  5. package/dist/client/src/index.d.ts +6 -0
  6. package/dist/client/src/index.d.ts.map +1 -0
  7. package/dist/client/src/lib/FlowRun.d.ts +125 -0
  8. package/dist/client/src/lib/FlowRun.d.ts.map +1 -0
  9. package/dist/client/src/lib/FlowStep.d.ts +90 -0
  10. package/dist/client/src/lib/FlowStep.d.ts.map +1 -0
  11. package/dist/client/src/lib/PgflowClient.d.ts +76 -0
  12. package/dist/client/src/lib/PgflowClient.d.ts.map +1 -0
  13. package/dist/client/src/lib/SupabaseBroadcastAdapter.d.ts +66 -0
  14. package/dist/client/src/lib/SupabaseBroadcastAdapter.d.ts.map +1 -0
  15. package/dist/client/src/lib/eventAdapters.d.ts +21 -0
  16. package/dist/client/src/lib/eventAdapters.d.ts.map +1 -0
  17. package/dist/client/src/lib/types.d.ts +308 -0
  18. package/dist/client/src/lib/types.d.ts.map +1 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +928 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/package.json +51 -0
  23. package/dist/pgflow-client.browser.js +2 -0
  24. package/dist/pgflow-client.browser.js.map +1 -0
  25. package/dist/src/browser.d.ts +7 -0
  26. package/dist/src/browser.d.ts.map +1 -0
  27. package/dist/src/browser.js +10 -0
  28. package/dist/src/index.d.ts +6 -0
  29. package/dist/src/index.d.ts.map +1 -0
  30. package/dist/src/index.js +7 -0
  31. package/dist/src/lib/FlowRun.d.ts +125 -0
  32. package/dist/src/lib/FlowRun.d.ts.map +1 -0
  33. package/dist/src/lib/FlowRun.js +368 -0
  34. package/dist/src/lib/FlowStep.d.ts +90 -0
  35. package/dist/src/lib/FlowStep.d.ts.map +1 -0
  36. package/dist/src/lib/FlowStep.js +244 -0
  37. package/dist/src/lib/PgflowClient.d.ts +75 -0
  38. package/dist/src/lib/PgflowClient.d.ts.map +1 -0
  39. package/dist/src/lib/PgflowClient.js +227 -0
  40. package/dist/src/lib/SupabaseBroadcastAdapter.d.ts +65 -0
  41. package/dist/src/lib/SupabaseBroadcastAdapter.d.ts.map +1 -0
  42. package/dist/src/lib/SupabaseBroadcastAdapter.js +332 -0
  43. package/dist/src/lib/eventAdapters.d.ts +20 -0
  44. package/dist/src/lib/eventAdapters.d.ts.map +1 -0
  45. package/dist/src/lib/eventAdapters.js +142 -0
  46. package/dist/src/lib/types.d.ts +307 -0
  47. package/dist/src/lib/types.d.ts.map +1 -0
  48. package/dist/src/lib/types.js +91 -0
  49. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  50. 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"}