@pgflow/client 0.0.0-update-supabase-ba45e13a-20251119080026 → 0.0.0-worker-management-a4f969d1-20251208095931

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.
@@ -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"}
@@ -0,0 +1,332 @@
1
+ import { createNanoEvents } from 'nanoevents';
2
+ /**
3
+ * Adapter to handle realtime communication with Supabase
4
+ */
5
+ export class SupabaseBroadcastAdapter {
6
+ #supabase;
7
+ #channels = new Map();
8
+ #emitter = createNanoEvents();
9
+ #reconnectionDelay;
10
+ #stabilizationDelay;
11
+ #schedule;
12
+ /**
13
+ * Creates a new instance of SupabaseBroadcastAdapter
14
+ *
15
+ * @param supabase - Supabase client instance
16
+ */
17
+ constructor(supabase, opts = {}) {
18
+ this.#supabase = supabase;
19
+ this.#reconnectionDelay = opts.reconnectDelayMs ?? 2000;
20
+ this.#stabilizationDelay = opts.stabilizationDelayMs ?? 300;
21
+ this.#schedule = opts.schedule ?? setTimeout;
22
+ }
23
+ /**
24
+ * Handle broadcast messages from Supabase
25
+ * @param payload - The message payload
26
+ */
27
+ #handleBroadcastMessage(msg) {
28
+ const { event, payload } = msg;
29
+ // run_id is already inside the payload coming from the database trigger
30
+ // so just preserve it without overwriting
31
+ const eventData = payload;
32
+ // Auto-parse JSON strings in broadcast data (realtime sends JSONB as strings)
33
+ this.#parseJsonFields(eventData);
34
+ if (event.startsWith('run:')) {
35
+ // Handle run events
36
+ this.#emitter.emit('runEvent', eventData);
37
+ }
38
+ else if (event.startsWith('step:')) {
39
+ // Handle step events
40
+ this.#emitter.emit('stepEvent', eventData);
41
+ }
42
+ }
43
+ /**
44
+ * Parse JSON string fields in broadcast event data
45
+ * @param eventData - The event data object to parse
46
+ */
47
+ #parseJsonFields(eventData) {
48
+ // Parse output field if it's a JSON string
49
+ if ('output' in eventData && typeof eventData.output === 'string') {
50
+ try {
51
+ eventData.output = JSON.parse(eventData.output);
52
+ }
53
+ catch {
54
+ // Keep as string if not valid JSON
55
+ }
56
+ }
57
+ // Parse input field if it's a JSON string
58
+ if ('input' in eventData && typeof eventData.input === 'string') {
59
+ try {
60
+ eventData.input = JSON.parse(eventData.input);
61
+ }
62
+ catch {
63
+ // Keep as string if not valid JSON
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * Handle channel errors and reconnection
69
+ * @param run_id - The run ID
70
+ * @param channelName - The channel name
71
+ * @param channel - The RealtimeChannel instance
72
+ * @param error - The error object
73
+ */
74
+ async #handleChannelError(run_id, channelName, channel, error) {
75
+ console.error(`Channel ${channelName} error:`, error);
76
+ // Schedule reconnection attempt
77
+ this.#schedule(async () => {
78
+ if (this.#channels.has(run_id)) {
79
+ await this.#reconnectChannel(run_id, channelName);
80
+ }
81
+ }, this.#reconnectionDelay);
82
+ }
83
+ /**
84
+ * Creates and configures a channel for a run
85
+ * @param run_id - The run ID
86
+ * @param channelName - The channel name
87
+ * @returns The configured RealtimeChannel
88
+ */
89
+ #createAndConfigureChannel(run_id, channelName) {
90
+ const channel = this.#supabase.channel(channelName);
91
+ // Listen to *all* broadcast messages; filter inside the handler.
92
+ // Using the 3-arg overload with event filter for proper Supabase v2 client compatibility.
93
+ channel.on('broadcast', { event: '*' }, this.#handleBroadcastMessage.bind(this));
94
+ // Note: Lifecycle event listeners (subscribed, closed, error) are handled
95
+ // by the calling code to avoid conflicts when multiple listeners try to
96
+ // handle the same events.
97
+ return channel;
98
+ }
99
+ /**
100
+ * Reconnect to a channel and refresh state
101
+ * @param run_id - The run ID
102
+ * @param channelName - The channel name
103
+ */
104
+ async #reconnectChannel(run_id, channelName) {
105
+ console.log(`Attempting to reconnect to ${channelName}`);
106
+ try {
107
+ // Fetch current state to avoid missing events during disconnection
108
+ const currentState = await this.getRunWithStates(run_id);
109
+ // Update state based on current data
110
+ this.#refreshStateFromSnapshot(run_id, currentState);
111
+ // Create a new channel as the old one can't be reused
112
+ const newChannel = this.#createAndConfigureChannel(run_id, channelName);
113
+ // Set up lifecycle event handlers for reconnection
114
+ newChannel.on('system', { event: 'subscribed' }, () => {
115
+ console.log(`Reconnected and subscribed to channel ${channelName}`);
116
+ });
117
+ newChannel.on('system', { event: 'closed' }, () => {
118
+ console.log(`Reconnected channel ${channelName} closed`);
119
+ });
120
+ newChannel.on('system', { event: 'error' }, (payload) => this.#handleChannelError(run_id, channelName, newChannel, payload.error));
121
+ // Subscribe and update the channels map
122
+ newChannel.subscribe();
123
+ this.#channels.set(run_id, newChannel);
124
+ }
125
+ catch (e) {
126
+ console.error(`Failed to reconnect to ${channelName}:`, e);
127
+ }
128
+ }
129
+ /**
130
+ * Refresh client state from a state snapshot
131
+ * @param run_id - The run ID
132
+ * @param state - The state snapshot
133
+ */
134
+ #refreshStateFromSnapshot(run_id, state) {
135
+ if (!state || !state.run)
136
+ return;
137
+ // Create proper run event with correct event_type
138
+ const runEvent = {
139
+ event_type: `run:${state.run.status}`,
140
+ ...state.run
141
+ };
142
+ // Emit run event
143
+ this.#emitter.emit('runEvent', runEvent);
144
+ // Emit events for each step state
145
+ if (state.steps && Array.isArray(state.steps)) {
146
+ for (const step of state.steps) {
147
+ // Create proper step event with correct event_type
148
+ const stepEvent = {
149
+ event_type: `step:${step.status}`,
150
+ ...step
151
+ };
152
+ // Emit step event
153
+ this.#emitter.emit('stepEvent', stepEvent);
154
+ }
155
+ }
156
+ }
157
+ /**
158
+ * Fetches flow definition metadata from the database
159
+ *
160
+ * @param flow_slug - Flow slug to fetch
161
+ */
162
+ async fetchFlowDefinition(flow_slug) {
163
+ // Fetch flow details and steps in parallel
164
+ const [flowResult, stepsResult] = await Promise.all([
165
+ this.#supabase
166
+ .schema('pgflow')
167
+ .from('flows')
168
+ .select('*')
169
+ .eq('flow_slug', flow_slug)
170
+ .single(),
171
+ this.#supabase
172
+ .schema('pgflow')
173
+ .from('steps')
174
+ .select('*')
175
+ .eq('flow_slug', flow_slug)
176
+ .order('step_index', { ascending: true })
177
+ ]);
178
+ // Handle flow result
179
+ if (flowResult.error)
180
+ throw flowResult.error;
181
+ if (!flowResult.data)
182
+ throw new Error(`Flow "${flow_slug}" not found`);
183
+ // Handle steps result
184
+ if (stepsResult.error)
185
+ throw stepsResult.error;
186
+ // Ensure steps is always an array, even if it's null or undefined
187
+ const stepsArray = Array.isArray(stepsResult.data) ? stepsResult.data : [];
188
+ return {
189
+ flow: flowResult.data,
190
+ steps: stepsArray,
191
+ };
192
+ }
193
+ /**
194
+ * Registers a callback for run events
195
+ *
196
+ * @param callback - Function to call when run events are received
197
+ * @returns Function to unsubscribe from the event
198
+ */
199
+ onRunEvent(callback) {
200
+ // Add a guard to prevent errors if called after emitter is deleted
201
+ const unsubscribe = this.#emitter.on('runEvent', callback);
202
+ return () => {
203
+ try {
204
+ unsubscribe();
205
+ }
206
+ catch (e) {
207
+ console.warn('Could not unsubscribe from run event - emitter may have been disposed', e);
208
+ }
209
+ };
210
+ }
211
+ /**
212
+ * Registers a callback for step events
213
+ *
214
+ * @param callback - Function to call when step events are received
215
+ * @returns Function to unsubscribe from the event
216
+ */
217
+ onStepEvent(callback) {
218
+ // Add a guard to prevent errors if called after emitter is deleted
219
+ const unsubscribe = this.#emitter.on('stepEvent', callback);
220
+ return () => {
221
+ try {
222
+ unsubscribe();
223
+ }
224
+ catch (e) {
225
+ console.warn('Could not unsubscribe from step event - emitter may have been disposed', e);
226
+ }
227
+ };
228
+ }
229
+ // Store unsubscribe functions per run ID for reference equality
230
+ #unsubscribeFunctions = new Map();
231
+ /**
232
+ * Subscribes to a flow run's events
233
+ *
234
+ * @param run_id - Run ID to subscribe to
235
+ * @returns Function to unsubscribe
236
+ */
237
+ async subscribeToRun(run_id) {
238
+ const channelName = `pgflow:run:${run_id}`;
239
+ // If already subscribed, return the existing unsubscribe function
240
+ if (this.#channels.has(run_id)) {
241
+ const existingUnsubscribe = this.#unsubscribeFunctions.get(run_id);
242
+ if (existingUnsubscribe) {
243
+ return existingUnsubscribe;
244
+ }
245
+ // If channel exists but no unsubscribe function, something went wrong
246
+ // Let's clean up and recreate
247
+ this.#unsubscribe(run_id);
248
+ }
249
+ const channel = this.#supabase.channel(channelName);
250
+ // Listen to *all* broadcast messages; filter inside the handler.
251
+ // Using the 3-arg overload with event filter for proper Supabase v2 client compatibility.
252
+ channel.on('broadcast', { event: '*' }, this.#handleBroadcastMessage.bind(this));
253
+ // Set up error handling
254
+ channel.on('system', { event: 'closed' }, () => {
255
+ console.log(`Channel ${channelName} closed`);
256
+ });
257
+ channel.on('system', { event: 'error' }, (payload) => {
258
+ console.log(`Channel ${channelName} error:`, payload);
259
+ this.#handleChannelError(run_id, channelName, channel, payload.error);
260
+ });
261
+ // Subscribe to channel and wait for confirmation (like the working realtime-send test)
262
+ console.log(`Subscribing to channel ${channelName}...`);
263
+ const subscriptionPromise = new Promise((resolve, reject) => {
264
+ const timeout = setTimeout(() => {
265
+ reject(new Error(`Subscription timeout for channel ${channelName}`));
266
+ }, 20000); // Increased from 5s to 20s for slower CI environments
267
+ channel.subscribe((status) => {
268
+ console.log(`Channel ${channelName} subscription status:`, status);
269
+ if (status === 'SUBSCRIBED') {
270
+ clearTimeout(timeout);
271
+ resolve();
272
+ }
273
+ // Don't reject on CHANNEL_ERROR - it's a transient state
274
+ // Only reject on timeout
275
+ });
276
+ });
277
+ // Wait for the 'SUBSCRIBED' acknowledgment to avoid race conditions
278
+ await subscriptionPromise;
279
+ // Stabilization delay - known Supabase Realtime limitation
280
+ // The SUBSCRIBED event is emitted before backend routing is fully established.
281
+ // This delay ensures the backend can receive messages sent immediately after subscription.
282
+ // See: https://github.com/supabase/supabase-js/issues/1599
283
+ await new Promise(resolve => this.#schedule(resolve, this.#stabilizationDelay));
284
+ this.#channels.set(run_id, channel);
285
+ const unsubscribe = () => this.unsubscribe(run_id);
286
+ this.#unsubscribeFunctions.set(run_id, unsubscribe);
287
+ return unsubscribe;
288
+ }
289
+ /**
290
+ * Unsubscribes from a run's events
291
+ *
292
+ * @param run_id - Run ID to unsubscribe from
293
+ */
294
+ unsubscribe(run_id) {
295
+ this.#unsubscribe(run_id);
296
+ }
297
+ /**
298
+ * Fetches current state of a run and its steps
299
+ *
300
+ * @param run_id - Run ID to fetch
301
+ */
302
+ async getRunWithStates(run_id) {
303
+ // Call the RPC function which returns a JSONB object
304
+ const { data, error } = await this.#supabase
305
+ .schema('pgflow')
306
+ .rpc('get_run_with_states', { run_id });
307
+ if (error)
308
+ throw error;
309
+ if (!data)
310
+ throw new Error(`No data returned for run ${run_id}`);
311
+ return data;
312
+ }
313
+ /**
314
+ * Unsubscribes from a run's events
315
+ *
316
+ * @param run_id - Run ID to unsubscribe from
317
+ */
318
+ #unsubscribe(run_id) {
319
+ const channel = this.#channels.get(run_id);
320
+ if (channel) {
321
+ // Close the channel
322
+ channel.unsubscribe();
323
+ this.#channels.delete(run_id);
324
+ // Also clean up the unsubscribe function reference
325
+ this.#unsubscribeFunctions.delete(run_id);
326
+ // We don't need to explicitly remove event listeners from the emitter
327
+ // as they will be garbage collected when no longer referenced.
328
+ // The event listeners are bound to specific callbacks provided by the client,
329
+ // which will retain references if they're still in use.
330
+ }
331
+ }
332
+ }
@@ -0,0 +1,20 @@
1
+ import type { AnyFlow, ExtractFlowSteps } from '@pgflow/dsl';
2
+ import type { RunRow, StepStateRow } from '@pgflow/core';
3
+ import type { BroadcastRunEvent, BroadcastStepEvent, FlowRunEvent, StepEvent } from './types.js';
4
+ /**
5
+ * Convert a broadcast run event to a typed run event
6
+ */
7
+ export declare function toTypedRunEvent<TFlow extends AnyFlow>(evt: BroadcastRunEvent): FlowRunEvent<TFlow>;
8
+ /**
9
+ * Convert a broadcast step event to a typed step event
10
+ */
11
+ export declare function toTypedStepEvent<TFlow extends AnyFlow, TStepSlug extends keyof ExtractFlowSteps<TFlow> & string>(evt: BroadcastStepEvent): StepEvent<TFlow, TStepSlug>;
12
+ /**
13
+ * Convert a database run row to a typed run event
14
+ */
15
+ export declare function runRowToTypedEvent<TFlow extends AnyFlow>(row: RunRow): FlowRunEvent<TFlow>;
16
+ /**
17
+ * Convert a database step state row to a typed step event
18
+ */
19
+ export declare function stepStateRowToTypedEvent<TFlow extends AnyFlow, TStepSlug extends keyof ExtractFlowSteps<TFlow> & string>(row: StepStateRow): StepEvent<TFlow, TStepSlug>;
20
+ //# sourceMappingURL=eventAdapters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventAdapters.d.ts","sourceRoot":"","sources":["../../../src/lib/eventAdapters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAGP,gBAAgB,EAEjB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKzD,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,SAAS,EACV,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,SAAS,OAAO,EACnD,GAAG,EAAE,iBAAiB,GACrB,YAAY,CAAC,KAAK,CAAC,CA+BrB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,SAAS,OAAO,EACrB,SAAS,SAAS,MAAM,gBAAgB,CAAC,KAAK,CAAC,GAAG,MAAM,EACxD,GAAG,EAAE,kBAAkB,GAAG,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CA6BtD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,SAAS,OAAO,EACtD,GAAG,EAAE,MAAM,GACV,YAAY,CAAC,KAAK,CAAC,CAiCrB;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,SAAS,OAAO,EACrB,SAAS,SAAS,MAAM,gBAAgB,CAAC,KAAK,CAAC,GAAG,MAAM,EACxD,GAAG,EAAE,YAAY,GAAG,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CAgChD"}
@@ -0,0 +1,142 @@
1
+ import { FlowStepStatus, FlowRunStatus, } from './types.js';
2
+ /**
3
+ * Convert a broadcast run event to a typed run event
4
+ */
5
+ export function toTypedRunEvent(evt) {
6
+ switch (evt.status) {
7
+ case FlowRunStatus.Started:
8
+ return {
9
+ event_type: 'run:started',
10
+ run_id: evt.run_id,
11
+ flow_slug: evt.flow_slug,
12
+ status: FlowRunStatus.Started,
13
+ started_at: evt.started_at,
14
+ remaining_steps: evt.remaining_steps,
15
+ input: evt.input,
16
+ };
17
+ case FlowRunStatus.Completed:
18
+ return {
19
+ event_type: 'run:completed',
20
+ run_id: evt.run_id,
21
+ flow_slug: evt.flow_slug,
22
+ status: FlowRunStatus.Completed,
23
+ completed_at: evt.completed_at,
24
+ output: evt.output,
25
+ };
26
+ case FlowRunStatus.Failed:
27
+ return {
28
+ event_type: 'run:failed',
29
+ run_id: evt.run_id,
30
+ flow_slug: evt.flow_slug,
31
+ status: FlowRunStatus.Failed,
32
+ failed_at: evt.failed_at,
33
+ error_message: evt.error_message,
34
+ };
35
+ }
36
+ }
37
+ /**
38
+ * Convert a broadcast step event to a typed step event
39
+ */
40
+ export function toTypedStepEvent(evt) {
41
+ switch (evt.status) {
42
+ case FlowStepStatus.Started:
43
+ return {
44
+ event_type: 'step:started',
45
+ run_id: evt.run_id,
46
+ step_slug: evt.step_slug,
47
+ status: FlowStepStatus.Started,
48
+ started_at: evt.started_at,
49
+ };
50
+ case FlowStepStatus.Completed:
51
+ return {
52
+ event_type: 'step:completed',
53
+ run_id: evt.run_id,
54
+ step_slug: evt.step_slug,
55
+ status: FlowStepStatus.Completed,
56
+ completed_at: evt.completed_at,
57
+ output: evt.output,
58
+ };
59
+ case FlowStepStatus.Failed:
60
+ return {
61
+ event_type: 'step:failed',
62
+ run_id: evt.run_id,
63
+ step_slug: evt.step_slug,
64
+ status: FlowStepStatus.Failed,
65
+ failed_at: evt.failed_at,
66
+ error_message: evt.error_message,
67
+ };
68
+ }
69
+ }
70
+ /**
71
+ * Convert a database run row to a typed run event
72
+ */
73
+ export function runRowToTypedEvent(row) {
74
+ switch (row.status) {
75
+ case 'started':
76
+ return {
77
+ event_type: 'run:started',
78
+ run_id: row.run_id,
79
+ flow_slug: row.flow_slug,
80
+ status: FlowRunStatus.Started,
81
+ started_at: row.started_at,
82
+ remaining_steps: row.remaining_steps,
83
+ input: row.input,
84
+ };
85
+ case 'completed':
86
+ return {
87
+ event_type: 'run:completed',
88
+ run_id: row.run_id,
89
+ flow_slug: row.flow_slug,
90
+ status: FlowRunStatus.Completed,
91
+ completed_at: row.completed_at,
92
+ output: row.output,
93
+ };
94
+ case 'failed':
95
+ return {
96
+ event_type: 'run:failed',
97
+ run_id: row.run_id,
98
+ flow_slug: row.flow_slug,
99
+ status: FlowRunStatus.Failed,
100
+ failed_at: row.failed_at,
101
+ error_message: 'Flow failed', // Database doesn't have error_message for runs
102
+ };
103
+ default:
104
+ throw new Error(`Unknown run status: ${row.status}`);
105
+ }
106
+ }
107
+ /**
108
+ * Convert a database step state row to a typed step event
109
+ */
110
+ export function stepStateRowToTypedEvent(row) {
111
+ switch (row.status) {
112
+ case 'created':
113
+ case 'started':
114
+ return {
115
+ event_type: 'step:started',
116
+ run_id: row.run_id,
117
+ step_slug: row.step_slug,
118
+ status: FlowStepStatus.Started,
119
+ started_at: row.started_at,
120
+ };
121
+ case 'completed':
122
+ return {
123
+ event_type: 'step:completed',
124
+ run_id: row.run_id,
125
+ step_slug: row.step_slug,
126
+ status: FlowStepStatus.Completed,
127
+ completed_at: row.completed_at,
128
+ output: {}, // Database doesn't have output in step_states
129
+ };
130
+ case 'failed':
131
+ return {
132
+ event_type: 'step:failed',
133
+ run_id: row.run_id,
134
+ step_slug: row.step_slug,
135
+ status: FlowStepStatus.Failed,
136
+ failed_at: row.failed_at,
137
+ error_message: row.error_message || 'Step failed',
138
+ };
139
+ default:
140
+ throw new Error(`Unknown step status: ${row.status}`);
141
+ }
142
+ }