@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
@@ -1 +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;AAC7D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C,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;;;;OAIG;gBACS,cAAc,EAAE,cAAc;IAwB1C;;;;;;;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"}
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
+ }
@@ -1,7 +1,6 @@
1
- import { IFlowRealtime, BroadcastRunEvent, BroadcastStepEvent, Unsubscribe } from './types.js';
2
- import { FlowRow, StepRow, RunRow, StepStateRow } from '../../../core/src/index.ts';
3
- import { SupabaseClient } from '@supabase/supabase-js';
4
-
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';
5
4
  /**
6
5
  * Adapter to handle realtime communication with Supabase
7
6
  */
@@ -14,6 +13,7 @@ export declare class SupabaseBroadcastAdapter implements IFlowRealtime {
14
13
  */
15
14
  constructor(supabase: SupabaseClient, opts?: {
16
15
  reconnectDelayMs?: number;
16
+ stabilizationDelayMs?: number;
17
17
  schedule?: typeof setTimeout;
18
18
  });
19
19
  /**
@@ -1 +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;;IAQ5D;;;;OAIG;gBAED,QAAQ,EAAE,cAAc,EACxB,IAAI,GAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,UAAU,CAAA;KAAO;IAkLxE;;;;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;IA0DzD;;;;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"}
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
+ }
@@ -1,7 +1,6 @@
1
- import { BroadcastRunEvent, BroadcastStepEvent, FlowRunEvent, StepEvent } from './types.js';
2
- import { RunRow, StepStateRow } from '../../../core/src/index.ts';
3
- import { AnyFlow, ExtractFlowSteps } from '../../../dsl/src/index.ts';
4
-
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';
5
4
  /**
6
5
  * Convert a broadcast run event to a typed run event
7
6
  */