Skip to content

Commit 37734e5

Browse files
committed
refactor: fix all remaining type issues
improve type safety and interface consistency across FlowRun, FlowStep, and client types - Updated FlowRun to implement FlowRunBase with more precise step storage - Enhanced FlowStep to implement FlowStepBase with generic event handling - Added non-generic base interfaces for flow runs and steps for better invariance - Improved event type definitions to match nanoevents expectations - Modified PgflowClient to use specific flow types and correct startFlow return types - Updated type guards and event data handling for safer state updates - Minor Vite config adjustment for better build stability and type referencing
1 parent d0ae28d commit 37734e5

File tree

6 files changed

+110
-65
lines changed

6 files changed

+110
-65
lines changed

pkgs/client/src/lib/FlowRun.ts

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { createNanoEvents } from 'nanoevents';
22
import type { AnyFlow, ExtractFlowInput, ExtractFlowOutput, ExtractFlowSteps } from '@pgflow/dsl';
3-
import type { FlowRunState, FlowRunEvents, Unsubscribe, StepEvents } from './types';
3+
import type { FlowRunState, FlowRunEvents, FlowRunEventData, Unsubscribe, StepEventData, FlowRunBase, FlowStepBase } from './types';
44
import { FlowStep } from './FlowStep';
55

66
/**
77
* Represents a single execution of a flow
88
*/
9-
export class FlowRun<TFlow extends AnyFlow> {
9+
export class FlowRun<TFlow extends AnyFlow> implements FlowRunBase {
1010
#state: FlowRunState<TFlow>;
1111
#events = createNanoEvents<FlowRunEvents<TFlow>>();
12-
#steps = new Map<string, FlowStep<TFlow, keyof ExtractFlowSteps<TFlow> & string>>();
12+
#steps = new Map<string, FlowStepBase>();
1313
#statusPrecedence: Record<string, number> = {
1414
'queued': 0,
1515
'started': 1,
@@ -113,7 +113,7 @@ export class FlowRun<TFlow extends AnyFlow> {
113113
*/
114114
on<E extends keyof FlowRunEvents<TFlow>>(
115115
event: E,
116-
callback: (event: FlowRunEvents<TFlow>[E]) => void
116+
callback: FlowRunEvents<TFlow>[E]
117117
): Unsubscribe {
118118
return this.#events.on(event, callback);
119119
}
@@ -130,7 +130,8 @@ export class FlowRun<TFlow extends AnyFlow> {
130130
// Look up if we already have this step cached
131131
const existingStep = this.#steps.get(stepSlug as string);
132132
if (existingStep) {
133-
return existingStep as FlowStep<TFlow, TStepSlug>;
133+
// Safe to cast since we only store steps with matching slugs
134+
return existingStep as unknown as FlowStep<TFlow, TStepSlug>;
134135
}
135136

136137
// Create a new step instance with default state
@@ -147,7 +148,7 @@ export class FlowRun<TFlow extends AnyFlow> {
147148
});
148149

149150
// Cache the step
150-
this.#steps.set(stepSlug as string, step);
151+
this.#steps.set(stepSlug as string, step as FlowStepBase);
151152

152153
return step;
153154
}
@@ -216,38 +217,41 @@ export class FlowRun<TFlow extends AnyFlow> {
216217
* @param event - Event data to update the state with
217218
* @returns true if the state was updated, false otherwise
218219
*/
219-
updateState(event: FlowRunEvents<TFlow>['*']): boolean {
220-
// Ensure this event is for this run
221-
if (event.run_id !== this.#state.run_id) {
220+
updateState(event: any): boolean {
221+
// Type guard to validate the event shape
222+
if (!event || typeof event !== 'object' || event.run_id !== this.#state.run_id) {
222223
return false;
223224
}
225+
226+
// Use properly typed event for the rest of the function
227+
const typedEvent = event as FlowRunEventData<TFlow>['*'];
224228

225229
// Check if the event status has higher precedence than current status
226-
if (!this.#shouldUpdateStatus(this.#state.status, event.status)) {
230+
if (!this.#shouldUpdateStatus(this.#state.status, typedEvent.status)) {
227231
return false;
228232
}
229233

230234
// Update state based on event type
231-
switch (event.status) {
235+
switch (typedEvent.status) {
232236
case 'started':
233237
this.#state = {
234238
...this.#state,
235239
status: 'started',
236-
started_at: event.started_at ? new Date(event.started_at) : new Date(),
237-
remaining_steps: 'remaining_steps' in event ? Number(event.remaining_steps) : this.#state.remaining_steps,
240+
started_at: typeof typedEvent.started_at === 'string' ? new Date(typedEvent.started_at) : new Date(),
241+
remaining_steps: 'remaining_steps' in typedEvent ? Number(typedEvent.remaining_steps) : this.#state.remaining_steps,
238242
};
239-
this.#events.emit('started', event as FlowRunEvents<TFlow>['started']);
243+
this.#events.emit('started', typedEvent as FlowRunEventData<TFlow>['started']);
240244
break;
241245

242246
case 'completed':
243247
this.#state = {
244248
...this.#state,
245249
status: 'completed',
246-
completed_at: event.completed_at ? new Date(event.completed_at) : new Date(),
247-
output: event.output as ExtractFlowOutput<TFlow>,
250+
completed_at: typeof typedEvent.completed_at === 'string' ? new Date(typedEvent.completed_at) : new Date(),
251+
output: typedEvent.output as ExtractFlowOutput<TFlow>,
248252
remaining_steps: 0,
249253
};
250-
this.#events.emit('completed', event as FlowRunEvents<TFlow>['completed']);
254+
this.#events.emit('completed', typedEvent as FlowRunEventData<TFlow>['completed']);
251255

252256
// Check for auto-dispose
253257
this.#checkAutoDispose();
@@ -257,11 +261,11 @@ export class FlowRun<TFlow extends AnyFlow> {
257261
this.#state = {
258262
...this.#state,
259263
status: 'failed',
260-
failed_at: event.failed_at ? new Date(event.failed_at) : new Date(),
261-
error_message: event.error_message || 'Unknown error',
262-
error: new Error(event.error_message || 'Unknown error'),
264+
failed_at: typeof typedEvent.failed_at === 'string' ? new Date(typedEvent.failed_at) : new Date(),
265+
error_message: typeof typedEvent.error_message === 'string' ? typedEvent.error_message : 'Unknown error',
266+
error: new Error(typeof typedEvent.error_message === 'string' ? typedEvent.error_message : 'Unknown error'),
263267
};
264-
this.#events.emit('failed', event as FlowRunEvents<TFlow>['failed']);
268+
this.#events.emit('failed', typedEvent as FlowRunEventData<TFlow>['failed']);
265269

266270
// Check for auto-dispose
267271
this.#checkAutoDispose();
@@ -272,7 +276,7 @@ export class FlowRun<TFlow extends AnyFlow> {
272276
}
273277

274278
// Also emit to the catch-all listener
275-
this.#events.emit('*', event);
279+
this.#events.emit('*', typedEvent);
276280

277281
return true;
278282
}
@@ -286,7 +290,7 @@ export class FlowRun<TFlow extends AnyFlow> {
286290
*/
287291
updateStepState<TStepSlug extends keyof ExtractFlowSteps<TFlow> & string>(
288292
stepSlug: TStepSlug,
289-
event: StepEvents<TFlow, TStepSlug>['*']
293+
event: StepEventData<TFlow, TStepSlug>['*']
290294
): boolean {
291295
const step = this.step(stepSlug);
292296
return step.updateState(event);

pkgs/client/src/lib/FlowStep.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { createNanoEvents } from 'nanoevents';
22
import type { AnyFlow, ExtractFlowSteps, StepOutput } from '@pgflow/dsl';
3-
import type { FlowStepState, StepEvents, Unsubscribe } from './types';
3+
import type { FlowStepState, StepEvents, StepEventData, Unsubscribe, FlowStepBase } from './types';
44

55
/**
66
* Represents a single step in a flow run
77
*/
88
export class FlowStep<
99
TFlow extends AnyFlow,
1010
TStepSlug extends keyof ExtractFlowSteps<TFlow> & string
11-
> {
11+
> implements FlowStepBase {
1212
#state: FlowStepState<TFlow, TStepSlug>;
1313
#events = createNanoEvents<StepEvents<TFlow, TStepSlug>>();
1414
#statusPrecedence: Record<string, number> = {
@@ -92,7 +92,7 @@ export class FlowStep<
9292
*/
9393
on<E extends keyof StepEvents<TFlow, TStepSlug>>(
9494
event: E,
95-
callback: (event: StepEvents<TFlow, TStepSlug>[E]) => void
95+
callback: StepEvents<TFlow, TStepSlug>[E]
9696
): Unsubscribe {
9797
return this.#events.on(event, callback);
9898
}
@@ -161,55 +161,58 @@ export class FlowStep<
161161
* @param event - Event data to update the state with
162162
* @returns true if the state was updated, false otherwise
163163
*/
164-
updateState(event: StepEvents<TFlow, TStepSlug>['*']): boolean {
165-
// Ensure this event is for this step
166-
if (event.step_slug !== this.#state.step_slug) {
164+
updateState(event: any): boolean {
165+
// Use type guard to validate the step slug shape
166+
if (!event || typeof event !== 'object' || event.step_slug !== this.#state.step_slug) {
167167
return false;
168168
}
169+
170+
// Use the properly typed event for the rest of the function
171+
const typedEvent = event as StepEventData<TFlow, TStepSlug>['*'];
169172

170173
// Check if the event status has higher precedence than current status
171-
if (!this.#shouldUpdateStatus(this.#state.status, event.status)) {
174+
if (!this.#shouldUpdateStatus(this.#state.status, typedEvent.status)) {
172175
return false;
173176
}
174177

175178
// Update state based on event type
176-
switch (event.status) {
179+
switch (typedEvent.status) {
177180
case 'started':
178181
this.#state = {
179182
...this.#state,
180183
status: 'started',
181-
started_at: event.started_at ? new Date(event.started_at) : new Date(),
184+
started_at: typeof typedEvent.started_at === 'string' ? new Date(typedEvent.started_at) : new Date(),
182185
};
183-
this.#events.emit('started', event as StepEvents<TFlow, TStepSlug>['started']);
186+
this.#events.emit('started', typedEvent as StepEventData<TFlow, TStepSlug>['started']);
184187
break;
185188

186189
case 'completed':
187190
this.#state = {
188191
...this.#state,
189192
status: 'completed',
190-
completed_at: event.completed_at ? new Date(event.completed_at) : new Date(),
191-
output: event.output as StepOutput<TFlow, TStepSlug>,
193+
completed_at: typeof typedEvent.completed_at === 'string' ? new Date(typedEvent.completed_at) : new Date(),
194+
output: typedEvent.output as StepOutput<TFlow, TStepSlug>,
192195
};
193-
this.#events.emit('completed', event as StepEvents<TFlow, TStepSlug>['completed']);
196+
this.#events.emit('completed', typedEvent as StepEventData<TFlow, TStepSlug>['completed']);
194197
break;
195198

196199
case 'failed':
197200
this.#state = {
198201
...this.#state,
199202
status: 'failed',
200-
failed_at: event.failed_at ? new Date(event.failed_at) : new Date(),
201-
error_message: event.error_message || 'Unknown error',
202-
error: new Error(event.error_message || 'Unknown error'),
203+
failed_at: typeof typedEvent.failed_at === 'string' ? new Date(typedEvent.failed_at) : new Date(),
204+
error_message: typeof typedEvent.error_message === 'string' ? typedEvent.error_message : 'Unknown error',
205+
error: new Error(typeof typedEvent.error_message === 'string' ? typedEvent.error_message : 'Unknown error'),
203206
};
204-
this.#events.emit('failed', event as StepEvents<TFlow, TStepSlug>['failed']);
207+
this.#events.emit('failed', typedEvent as StepEventData<TFlow, TStepSlug>['failed']);
205208
break;
206209

207210
default:
208211
return false;
209212
}
210213

211214
// Also emit to the catch-all listener
212-
this.#events.emit('*', event);
215+
this.#events.emit('*', typedEvent);
213216

214217
return true;
215218
}

pkgs/client/src/lib/PgflowClient.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import { v4 as uuidv4 } from 'uuid';
22
import type { SupabaseClient } from '@supabase/supabase-js';
33
import type { AnyFlow, ExtractFlowInput } from '@pgflow/dsl';
4-
import type { IFlowClient, FlowRunState, BroadcastRunEvent, BroadcastStepEvent, Unsubscribe } from './types';
4+
import type { IFlowClient, FlowRunState, BroadcastRunEvent, BroadcastStepEvent, Unsubscribe, FlowRunBase } from './types';
55
import { SupabaseBroadcastAdapter } from './SupabaseBroadcastAdapter';
66
import { FlowRun } from './FlowRun';
77

88
/**
99
* Client for interacting with pgflow
1010
*/
11-
export class PgflowClient implements IFlowClient {
11+
export class PgflowClient<TFlow extends AnyFlow = AnyFlow> implements IFlowClient<TFlow> {
1212
#supabase: SupabaseClient;
1313
#realtimeAdapter: SupabaseBroadcastAdapter;
14-
#runs = new Map<string, FlowRun<AnyFlow>>();
14+
#runs = new Map<string, FlowRunBase>();
1515

1616
/**
1717
* Creates a new PgflowClient instance
@@ -48,20 +48,20 @@ export class PgflowClient implements IFlowClient {
4848
* @param run_id - Optional run ID (will be generated if not provided)
4949
* @returns Promise that resolves with the FlowRun instance
5050
*/
51-
async startFlow<TFlow extends AnyFlow>(
51+
async startFlow<TSpecificFlow extends TFlow>(
5252
flow_slug: string,
53-
input: ExtractFlowInput<TFlow>,
53+
input: ExtractFlowInput<TSpecificFlow>,
5454
run_id?: string
55-
): Promise<FlowRun<TFlow>> {
55+
): Promise<FlowRun<TSpecificFlow>> {
5656
// Generate a run_id if not provided
5757
const id = run_id || uuidv4();
5858

5959
// Create initial state for the flow run
60-
const initialState: FlowRunState<TFlow> = {
60+
const initialState: FlowRunState<TSpecificFlow> = {
6161
run_id: id,
6262
flow_slug,
6363
status: 'queued',
64-
input: input as ExtractFlowInput<TFlow>,
64+
input: input as ExtractFlowInput<TSpecificFlow>,
6565
output: null,
6666
error: null,
6767
error_message: null,
@@ -72,7 +72,7 @@ export class PgflowClient implements IFlowClient {
7272
};
7373

7474
// Create the flow run instance
75-
const run = new FlowRun<TFlow>(initialState);
75+
const run = new FlowRun<TSpecificFlow>(initialState);
7676

7777
// Store the run
7878
this.#runs.set(id, run);

pkgs/client/src/lib/client.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { client } from './client';
1+
// This is a stub test to be replaced with real tests
2+
// Import a real export to prevent TypeScript errors
3+
import { PgflowClient } from './PgflowClient';
24

3-
describe('client', () => {
4-
it('should work', () => {
5-
expect(client()).toEqual('client');
5+
describe('PgflowClient', () => {
6+
it('should be defined', () => {
7+
expect(PgflowClient).toBeDefined();
68
});
79
});

pkgs/client/src/lib/types.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import type { Json, RunRow, StepStateRow, FlowRow, StepRow } from '@pgflow/core'
33
import type { FlowRun } from './FlowRun';
44

55
/**
6-
* Flow run event types
6+
* Flow run event data types
77
*/
8-
export type FlowRunEvents<TFlow extends AnyFlow> = {
8+
export type FlowRunEventData<TFlow extends AnyFlow> = {
99
started: {
1010
run_id: string;
1111
flow_slug: string;
@@ -31,9 +31,16 @@ export type FlowRunEvents<TFlow extends AnyFlow> = {
3131
};
3232

3333
/**
34-
* Step event types
34+
* Flow run event types matching nanoevents expectations
3535
*/
36-
export type StepEvents<
36+
export type FlowRunEvents<TFlow extends AnyFlow> = {
37+
[K in keyof FlowRunEventData<TFlow>]: (event: FlowRunEventData<TFlow>[K]) => void;
38+
};
39+
40+
/**
41+
* Step event data types
42+
*/
43+
export type StepEventData<
3744
TFlow extends AnyFlow,
3845
TStepSlug extends keyof ExtractFlowSteps<TFlow> & string
3946
> = {
@@ -66,6 +73,17 @@ export type StepEvents<
6673
};
6774
};
6875

76+
/**
77+
* Step event types matching nanoevents expectations
78+
*/
79+
export type StepEvents<
80+
TFlow extends AnyFlow,
81+
TStepSlug extends keyof ExtractFlowSteps<TFlow> & string
82+
> = {
83+
[K in keyof StepEventData<TFlow, TStepSlug>]:
84+
(event: StepEventData<TFlow, TStepSlug>[K]) => void;
85+
};
86+
6987
/**
7088
* Function returned by event subscriptions to remove the listener
7189
*/
@@ -211,6 +229,23 @@ export interface IFlowRealtime<TFlow = unknown> {
211229
getRunWithStates(run_id: string): Promise<{ run: RunRow; steps: StepStateRow[] }>;
212230
}
213231

232+
/**
233+
* Non-generic base interface for flow runs - what the client needs
234+
*/
235+
export interface FlowRunBase {
236+
readonly run_id: string;
237+
updateState(event: any): boolean; // Using any here to solve invariance
238+
step(stepSlug: string): FlowStepBase;
239+
dispose(): void;
240+
}
241+
242+
/**
243+
* Non-generic base interface for flow steps - what the client needs
244+
*/
245+
export interface FlowStepBase {
246+
updateState(event: any): boolean; // Using any here to solve invariance
247+
}
248+
214249
/**
215250
* Composite interface for client
216251
*/
@@ -223,9 +258,9 @@ export interface IFlowClient<TFlow extends AnyFlow = AnyFlow> extends IFlowRealt
223258
* @param run_id - Optional run ID (will be generated if not provided)
224259
* @returns Promise that resolves with the FlowRun instance
225260
*/
226-
startFlow<TFlow extends AnyFlow>(
261+
startFlow<TSpecificFlow extends TFlow>(
227262
flow_slug: string,
228-
input: ExtractFlowInput<TFlow>,
263+
input: ExtractFlowInput<TSpecificFlow>,
229264
run_id?: string
230-
): Promise<FlowRun<TFlow>>;
265+
): Promise<FlowRun<TSpecificFlow>>;
231266
}

0 commit comments

Comments
 (0)