@pgflow/client 0.0.0-array-map-steps-fixed-types-38a198ae-20251011160533 → 0.0.0-control-plane-a947cb71-20251121164755

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 (46) hide show
  1. package/CHANGELOG.md +75 -3
  2. package/README.md +5 -1
  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 -1
  20. package/dist/index.js +401 -446
  21. package/dist/index.js.map +1 -1
  22. package/dist/package.json +3 -3
  23. package/dist/pgflow-client.browser.js +1 -1
  24. package/dist/pgflow-client.browser.js.map +1 -1
  25. package/dist/src/browser.d.ts +3 -3
  26. package/dist/src/browser.d.ts.map +1 -1
  27. package/dist/src/browser.js +10 -0
  28. package/dist/src/index.js +7 -0
  29. package/dist/src/lib/FlowRun.d.ts +11 -3
  30. package/dist/src/lib/FlowRun.d.ts.map +1 -1
  31. package/dist/src/lib/FlowRun.js +368 -0
  32. package/dist/src/lib/FlowStep.d.ts +11 -3
  33. package/dist/src/lib/FlowStep.d.ts.map +1 -1
  34. package/dist/src/lib/FlowStep.js +244 -0
  35. package/dist/src/lib/PgflowClient.d.ts +12 -10
  36. package/dist/src/lib/PgflowClient.d.ts.map +1 -1
  37. package/dist/src/lib/PgflowClient.js +227 -0
  38. package/dist/src/lib/SupabaseBroadcastAdapter.d.ts +4 -4
  39. package/dist/src/lib/SupabaseBroadcastAdapter.d.ts.map +1 -1
  40. package/dist/src/lib/SupabaseBroadcastAdapter.js +332 -0
  41. package/dist/src/lib/eventAdapters.d.ts +3 -4
  42. package/dist/src/lib/eventAdapters.js +142 -0
  43. package/dist/src/lib/types.d.ts +3 -4
  44. package/dist/src/lib/types.js +91 -0
  45. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  46. package/package.json +5 -5
@@ -0,0 +1,368 @@
1
+ import { createNanoEvents } from 'nanoevents';
2
+ import { FlowRunStatus, FlowStepStatus } from './types.js';
3
+ import { FlowStep } from './FlowStep.js';
4
+ /**
5
+ * Represents a single execution of a flow
6
+ */
7
+ export class FlowRun {
8
+ #state;
9
+ #events = createNanoEvents();
10
+ #steps = new Map();
11
+ #statusPrecedence = {
12
+ [FlowRunStatus.Started]: 0,
13
+ [FlowRunStatus.Completed]: 1,
14
+ [FlowRunStatus.Failed]: 2,
15
+ };
16
+ #disposed = false;
17
+ /**
18
+ * Creates a new FlowRun instance
19
+ *
20
+ * @param initialState - Initial state for the run
21
+ */
22
+ constructor(initialState) {
23
+ this.#state = initialState;
24
+ }
25
+ /**
26
+ * Get the run ID
27
+ */
28
+ get run_id() {
29
+ return this.#state.run_id;
30
+ }
31
+ /**
32
+ * Get the flow slug
33
+ */
34
+ get flow_slug() {
35
+ return this.#state.flow_slug;
36
+ }
37
+ /**
38
+ * Get the current status
39
+ */
40
+ get status() {
41
+ return this.#state.status;
42
+ }
43
+ /**
44
+ * Get the started_at timestamp
45
+ */
46
+ get started_at() {
47
+ return this.#state.started_at;
48
+ }
49
+ /**
50
+ * Get the completed_at timestamp
51
+ */
52
+ get completed_at() {
53
+ return this.#state.completed_at;
54
+ }
55
+ /**
56
+ * Get the failed_at timestamp
57
+ */
58
+ get failed_at() {
59
+ return this.#state.failed_at;
60
+ }
61
+ /**
62
+ * Get the flow input
63
+ */
64
+ get input() {
65
+ return this.#state.input;
66
+ }
67
+ /**
68
+ * Get the flow output
69
+ */
70
+ get output() {
71
+ return this.#state.output;
72
+ }
73
+ /**
74
+ * Get the error object
75
+ */
76
+ get error() {
77
+ return this.#state.error;
78
+ }
79
+ /**
80
+ * Get the error message
81
+ */
82
+ get error_message() {
83
+ return this.#state.error_message;
84
+ }
85
+ /**
86
+ * Get the number of remaining steps
87
+ */
88
+ get remaining_steps() {
89
+ return this.#state.remaining_steps;
90
+ }
91
+ /**
92
+ * Register an event handler for a run event
93
+ *
94
+ * @param event - Event type to listen for
95
+ * @param callback - Callback function to execute when event is emitted
96
+ * @returns Function to unsubscribe from the event
97
+ */
98
+ on(event, callback) {
99
+ this.#listenerCount++;
100
+ // Wrap the unsubscribe function to track listener count
101
+ const unsubscribe = this.#events.on(event, callback);
102
+ return () => {
103
+ unsubscribe();
104
+ this.#listenerCount--;
105
+ this.#checkAutoDispose();
106
+ };
107
+ }
108
+ /**
109
+ * Get a FlowStep instance for a specific step
110
+ *
111
+ * @param stepSlug - Step slug to get
112
+ * @returns FlowStep instance for the specified step
113
+ */
114
+ step(stepSlug) {
115
+ // Look up if we already have this step cached
116
+ const existingStep = this.#steps.get(stepSlug);
117
+ if (existingStep) {
118
+ // Safe to cast since we only store steps with matching slugs
119
+ return existingStep;
120
+ }
121
+ // Create a new step instance with default state
122
+ const step = new FlowStep({
123
+ run_id: this.run_id,
124
+ step_slug: stepSlug,
125
+ status: FlowStepStatus.Created,
126
+ output: null,
127
+ error: null,
128
+ error_message: null,
129
+ started_at: null,
130
+ completed_at: null,
131
+ failed_at: null,
132
+ });
133
+ // Cache the step
134
+ this.#steps.set(stepSlug, step);
135
+ return step;
136
+ }
137
+ /**
138
+ * Check if this run has a specific step
139
+ *
140
+ * @param stepSlug - Step slug to check
141
+ * @returns true if the step exists, false otherwise
142
+ */
143
+ hasStep(stepSlug) {
144
+ // Check if we have this step cached
145
+ return this.#steps.has(stepSlug);
146
+ }
147
+ /**
148
+ * Wait for the run to reach a specific status
149
+ *
150
+ * @param targetStatus - The status to wait for
151
+ * @param options - Optional timeout and abort signal
152
+ * @returns Promise that resolves with the run instance when the status is reached
153
+ */
154
+ waitForStatus(targetStatus, options) {
155
+ const timeoutMs = options?.timeoutMs ?? 5 * 60 * 1000; // Default 5 minutes
156
+ const { signal } = options || {};
157
+ // If we already have the target status, resolve immediately
158
+ if (this.status === targetStatus) {
159
+ return Promise.resolve(this);
160
+ }
161
+ // Otherwise, wait for the status to change
162
+ return new Promise((resolve, reject) => {
163
+ let timeoutId;
164
+ let cleanedUp = false;
165
+ // Set up timeout if provided
166
+ if (timeoutMs > 0) {
167
+ timeoutId = setTimeout(() => {
168
+ if (cleanedUp)
169
+ return; // Prevent firing if already cleaned up
170
+ cleanedUp = true;
171
+ unbind();
172
+ reject(new Error(`Timeout waiting for run ${this.run_id} to reach status '${targetStatus}'`));
173
+ }, timeoutMs);
174
+ }
175
+ // Set up abort signal if provided
176
+ let abortCleanup;
177
+ if (signal) {
178
+ const abortHandler = () => {
179
+ if (cleanedUp)
180
+ return; // Prevent double cleanup
181
+ cleanedUp = true;
182
+ if (timeoutId)
183
+ clearTimeout(timeoutId);
184
+ unbind();
185
+ reject(new Error(`Aborted waiting for run ${this.run_id} to reach status '${targetStatus}'`));
186
+ };
187
+ signal.addEventListener('abort', abortHandler);
188
+ abortCleanup = () => {
189
+ signal.removeEventListener('abort', abortHandler);
190
+ };
191
+ }
192
+ // Subscribe to all events
193
+ const unbind = this.on('*', (event) => {
194
+ if (event.status === targetStatus) {
195
+ if (cleanedUp)
196
+ return; // Prevent double cleanup
197
+ cleanedUp = true;
198
+ if (timeoutId)
199
+ clearTimeout(timeoutId);
200
+ if (abortCleanup)
201
+ abortCleanup();
202
+ unbind();
203
+ resolve(this);
204
+ }
205
+ });
206
+ });
207
+ }
208
+ /**
209
+ * Apply state from database snapshot (no events emitted)
210
+ * Used when initializing state from start_flow_with_states() or get_run_with_states()
211
+ *
212
+ * @internal This method is only intended for use by PgflowClient.
213
+ * Applications should not call this directly.
214
+ */
215
+ applySnapshot(row) {
216
+ // Direct state assignment from database row (no event conversion)
217
+ this.#state.status = row.status;
218
+ this.#state.input = row.input;
219
+ this.#state.output = row.output;
220
+ this.#state.started_at = row.started_at ? new Date(row.started_at) : null;
221
+ this.#state.completed_at = row.completed_at ? new Date(row.completed_at) : null;
222
+ this.#state.failed_at = row.failed_at ? new Date(row.failed_at) : null;
223
+ this.#state.remaining_steps = row.remaining_steps;
224
+ this.#state.error_message = null; // Database doesn't have error_message for runs
225
+ this.#state.error = null;
226
+ }
227
+ /**
228
+ * Updates the run state based on an event
229
+ *
230
+ * @internal This method is only intended for use by PgflowClient and tests.
231
+ * Applications should not call this directly - state updates should come from
232
+ * database events through the PgflowClient.
233
+ *
234
+ * TODO: After v1.0, make this method private and refactor tests to use PgflowClient
235
+ * with event emission instead of direct state manipulation.
236
+ */
237
+ updateState(event) {
238
+ // Validate the event is for this run
239
+ if (event.run_id !== this.#state.run_id) {
240
+ return false;
241
+ }
242
+ // Check if the event status has higher precedence than current status
243
+ if (!this.#shouldUpdateStatus(this.#state.status, event.status)) {
244
+ return false;
245
+ }
246
+ // Update state based on event type using narrowing type guards
247
+ switch (event.status) {
248
+ case FlowRunStatus.Started:
249
+ this.#state = {
250
+ ...this.#state,
251
+ status: FlowRunStatus.Started,
252
+ started_at: typeof event.started_at === 'string'
253
+ ? new Date(event.started_at)
254
+ : new Date(),
255
+ remaining_steps: 'remaining_steps' in event
256
+ ? Number(event.remaining_steps)
257
+ : this.#state.remaining_steps,
258
+ };
259
+ this.#events.emit('started', event);
260
+ break;
261
+ case FlowRunStatus.Completed:
262
+ this.#state = {
263
+ ...this.#state,
264
+ status: FlowRunStatus.Completed,
265
+ completed_at: typeof event.completed_at === 'string'
266
+ ? new Date(event.completed_at)
267
+ : new Date(),
268
+ output: event.output,
269
+ remaining_steps: 0,
270
+ };
271
+ this.#events.emit('completed', event);
272
+ // Check for auto-dispose
273
+ this.#checkAutoDispose();
274
+ break;
275
+ case FlowRunStatus.Failed:
276
+ this.#state = {
277
+ ...this.#state,
278
+ status: FlowRunStatus.Failed,
279
+ failed_at: typeof event.failed_at === 'string'
280
+ ? new Date(event.failed_at)
281
+ : new Date(),
282
+ error_message: typeof event.error_message === 'string'
283
+ ? event.error_message
284
+ : 'Unknown error',
285
+ error: new Error(typeof event.error_message === 'string'
286
+ ? event.error_message
287
+ : 'Unknown error'),
288
+ };
289
+ this.#events.emit('failed', event);
290
+ // Check for auto-dispose
291
+ this.#checkAutoDispose();
292
+ break;
293
+ default: {
294
+ // Exhaustiveness check - ensures all event statuses are handled
295
+ event;
296
+ return false;
297
+ }
298
+ }
299
+ // Also emit to the catch-all listener
300
+ this.#events.emit('*', event);
301
+ return true;
302
+ }
303
+ /**
304
+ * Updates a step state based on an event
305
+ *
306
+ * @param stepSlug - Step slug to update
307
+ * @param event - Event data to update the step with
308
+ * @returns true if the state was updated, false otherwise
309
+ */
310
+ updateStepState(stepSlug, event) {
311
+ const step = this.step(stepSlug);
312
+ return step.updateState(event);
313
+ }
314
+ // Track number of listeners
315
+ #listenerCount = 0;
316
+ /**
317
+ * Checks if auto-dispose should be triggered (when in terminal state with no listeners)
318
+ */
319
+ #checkAutoDispose() {
320
+ // Don't auto-dispose multiple times
321
+ if (this.#disposed) {
322
+ return;
323
+ }
324
+ // Only auto-dispose in terminal states
325
+ if (this.status !== FlowRunStatus.Completed &&
326
+ this.status !== FlowRunStatus.Failed) {
327
+ return;
328
+ }
329
+ // If there are no listeners, auto-dispose
330
+ if (this.#listenerCount === 0) {
331
+ this.dispose();
332
+ }
333
+ }
334
+ /**
335
+ * Determines if a status should be updated based on precedence
336
+ *
337
+ * @param currentStatus - Current status
338
+ * @param newStatus - New status
339
+ * @returns true if the status should be updated, false otherwise
340
+ */
341
+ #shouldUpdateStatus(currentStatus, newStatus) {
342
+ // Don't allow changes to terminal states
343
+ if (currentStatus === FlowRunStatus.Completed ||
344
+ currentStatus === FlowRunStatus.Failed) {
345
+ return false; // Terminal states should never change
346
+ }
347
+ const currentPrecedence = this.#statusPrecedence[currentStatus];
348
+ const newPrecedence = this.#statusPrecedence[newStatus];
349
+ // Only allow transitions to higher precedence non-terminal status
350
+ return newPrecedence > currentPrecedence;
351
+ }
352
+ /**
353
+ * Clean up all resources held by this run
354
+ */
355
+ dispose() {
356
+ if (this.#disposed) {
357
+ return;
358
+ }
359
+ // Clear the map to allow garbage collection of steps
360
+ this.#steps.clear();
361
+ // Create a new events object - this effectively clears all listeners
362
+ // without accessing the private internals of nanoevents
363
+ this.#events = createNanoEvents();
364
+ this.#listenerCount = 0;
365
+ // Mark as disposed
366
+ this.#disposed = true;
367
+ }
368
+ }
@@ -1,6 +1,6 @@
1
- import { FlowStepStatus, FlowStepState, StepEvents, Unsubscribe, FlowStepBase, StepEvent } from './types.js';
2
- import { AnyFlow, ExtractFlowSteps, StepOutput } from '../../../dsl/src/index.ts';
3
-
1
+ import type { AnyFlow, ExtractFlowSteps, StepOutput } from '@pgflow/dsl';
2
+ import { FlowStepStatus } from './types.js';
3
+ import type { FlowStepState, StepEvents, Unsubscribe, FlowStepBase, StepEvent } from './types.js';
4
4
  /**
5
5
  * Represents a single step in a flow run
6
6
  */
@@ -67,6 +67,14 @@ export declare class FlowStep<TFlow extends AnyFlow, TStepSlug extends keyof Ext
67
67
  timeoutMs?: number;
68
68
  signal?: AbortSignal;
69
69
  }): Promise<this>;
70
+ /**
71
+ * Apply state from database snapshot (no events emitted)
72
+ * Used when initializing state from start_flow_with_states() or get_run_with_states()
73
+ *
74
+ * @internal This method is only intended for use by PgflowClient.
75
+ * Applications should not call this directly.
76
+ */
77
+ applySnapshot(row: import('@pgflow/core').StepStateRow): void;
70
78
  /**
71
79
  * Updates the step state based on an event
72
80
  *
@@ -1 +1 @@
1
- {"version":3,"file":"FlowStep.d.ts","sourceRoot":"","sources":["../../../src/lib/FlowStep.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EACV,aAAa,EACb,UAAU,EACV,WAAW,EACX,YAAY,EACZ,SAAS,EACV,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,qBAAa,QAAQ,CACnB,KAAK,SAAS,OAAO,EACrB,SAAS,SAAS,MAAM,gBAAgB,CAAC,KAAK,CAAC,GAAG,MAAM,CACxD,YAAW,YAAY,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;;IAUpD;;;;OAIG;gBACS,YAAY,EAAE,aAAa,CAAC,KAAK,EAAE,SAAS,CAAC;IAIzD;;OAEG;IACH,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,SAAS,CAEzB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,cAAc,CAE3B;IAED;;OAEG;IACH,IAAI,UAAU,IAAI,IAAI,GAAG,IAAI,CAE5B;IAED;;OAEG;IACH,IAAI,YAAY,IAAI,IAAI,GAAG,IAAI,CAE9B;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,IAAI,GAAG,IAAI,CAE3B;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,UAAU,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,IAAI,CAEhD;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,KAAK,GAAG,IAAI,CAExB;IAED;;OAEG;IACH,IAAI,aAAa,IAAI,MAAM,GAAG,IAAI,CAEjC;IAED;;;;;;OAMG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,UAAU,CAAC,KAAK,EAAE,SAAS,CAAC,EAC7C,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,UAAU,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,GACxC,WAAW;IAId;;;;;;OAMG;IACH,aAAa,CACX,YAAY,EAAE,cAAc,CAAC,OAAO,GAAG,cAAc,CAAC,SAAS,GAAG,cAAc,CAAC,MAAM,EACvF,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACrD,OAAO,CAAC,IAAI,CAAC;IAuDhB;;;;;;;;;OASG;IACH,WAAW,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,OAAO;CAgFzD"}
1
+ {"version":3,"file":"FlowStep.d.ts","sourceRoot":"","sources":["../../../src/lib/FlowStep.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EACV,aAAa,EACb,UAAU,EACV,WAAW,EACX,YAAY,EACZ,SAAS,EACV,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,qBAAa,QAAQ,CACnB,KAAK,SAAS,OAAO,EACrB,SAAS,SAAS,MAAM,gBAAgB,CAAC,KAAK,CAAC,GAAG,MAAM,CACxD,YAAW,YAAY,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;;IAUpD;;;;OAIG;gBACS,YAAY,EAAE,aAAa,CAAC,KAAK,EAAE,SAAS,CAAC;IAIzD;;OAEG;IACH,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,SAAS,CAEzB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,cAAc,CAE3B;IAED;;OAEG;IACH,IAAI,UAAU,IAAI,IAAI,GAAG,IAAI,CAE5B;IAED;;OAEG;IACH,IAAI,YAAY,IAAI,IAAI,GAAG,IAAI,CAE9B;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,IAAI,GAAG,IAAI,CAE3B;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,UAAU,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,IAAI,CAEhD;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,KAAK,GAAG,IAAI,CAExB;IAED;;OAEG;IACH,IAAI,aAAa,IAAI,MAAM,GAAG,IAAI,CAEjC;IAED;;;;;;OAMG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,UAAU,CAAC,KAAK,EAAE,SAAS,CAAC,EAC7C,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,UAAU,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,GACxC,WAAW;IAId;;;;;;OAMG;IACH,aAAa,CACX,YAAY,EAAE,cAAc,CAAC,OAAO,GAAG,cAAc,CAAC,SAAS,GAAG,cAAc,CAAC,MAAM,EACvF,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACrD,OAAO,CAAC,IAAI,CAAC;IAuDhB;;;;;;OAMG;IACH,aAAa,CAAC,GAAG,EAAE,OAAO,cAAc,EAAE,YAAY,GAAG,IAAI;IAW7D;;;;;;;;;OASG;IACH,WAAW,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,OAAO;CAgFzD"}
@@ -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
+ }
@@ -1,9 +1,7 @@
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';
1
4
  import { FlowRun } from './FlowRun.js';
2
- import { IFlowClient, BroadcastRunEvent, BroadcastStepEvent, Unsubscribe } from './types.js';
3
- import { RunRow } from '../../../core/src/index.ts';
4
- import { AnyFlow, ExtractFlowInput } from '../../../dsl/src/index.ts';
5
- import { SupabaseClient } from '@supabase/supabase-js';
6
-
7
5
  /**
8
6
  * Client for interacting with pgflow
9
7
  */
@@ -13,8 +11,12 @@ export declare class PgflowClient<TFlow extends AnyFlow = AnyFlow> implements IF
13
11
  * Creates a new PgflowClient instance
14
12
  *
15
13
  * @param supabaseClient - Supabase client instance
14
+ * @param opts - Optional configuration
16
15
  */
17
- constructor(supabaseClient: SupabaseClient);
16
+ constructor(supabaseClient: SupabaseClient, opts?: {
17
+ realtimeStabilizationDelayMs?: number;
18
+ schedule?: typeof setTimeout;
19
+ });
18
20
  /**
19
21
  * Start a flow with optional run_id
20
22
  *
@@ -38,8 +40,8 @@ export declare class PgflowClient<TFlow extends AnyFlow = AnyFlow> implements IF
38
40
  * Fetch flow definition metadata
39
41
  */
40
42
  fetchFlowDefinition(flow_slug: string): Promise<{
41
- flow: import('../../../core/src/index.ts').FlowRow;
42
- steps: import('../../../core/src/index.ts').StepRow[];
43
+ flow: import("pkgs/core/dist/types.js").FlowRow;
44
+ steps: import("pkgs/core/dist/types.js").StepRow[];
43
45
  }>;
44
46
  /**
45
47
  * Register a callback for run events
@@ -59,8 +61,8 @@ export declare class PgflowClient<TFlow extends AnyFlow = AnyFlow> implements IF
59
61
  * Fetch current state of a run and its steps
60
62
  */
61
63
  getRunWithStates(run_id: string): Promise<{
62
- run: RunRow;
63
- steps: import('../../../core/src/index.ts').StepStateRow[];
64
+ run: import("pkgs/core/dist/types.js").RunRow;
65
+ steps: import("pkgs/core/dist/types.js").StepStateRow[];
64
66
  }>;
65
67
  /**
66
68
  * Get a flow run by ID