Skip to content

Commit 2c26270

Browse files
committed
feat: error handling while in automation execution
1 parent 3e12d72 commit 2c26270

File tree

5 files changed

+137
-47
lines changed

5 files changed

+137
-47
lines changed

packages/automation/src/lib/listeners/listener.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { EventEmitter } from 'events';
22

3+
import { onError } from '../types';
4+
35
export interface ListenerParams {
46
start?: () => Promise<void>;
57
stop?: () => Promise<void>;
8+
onError?: onError;
69
}
710

811
/**
@@ -23,16 +26,23 @@ export class Listener<T = unknown> {
2326
*/
2427
public stop: () => Promise<void>;
2528

29+
/**
30+
* The error handling function to call when an error occurs.
31+
*/
32+
public onError?: onError;
33+
2634
/**
2735
* Constructor for the Listener class.
2836
* @param params The parameters object containing start and stop functions.
2937
*/
3038
constructor({
3139
start = async () => {},
3240
stop = async () => {},
41+
onError,
3342
}: ListenerParams = {}) {
3443
this.start = start;
3544
this.stop = stop;
45+
this.onError = onError;
3646
}
3747

3848
/**

packages/automation/src/lib/state-machine.ts

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { ethers } from 'ethers';
22

3-
import { LIT_RPC } from '@lit-protocol/constants';
3+
import {
4+
AutomationError,
5+
UnknownError,
6+
LIT_RPC,
7+
} from '@lit-protocol/constants';
48
import { LitContracts } from '@lit-protocol/contracts-sdk';
59
import { LitNodeClient } from '@lit-protocol/lit-node-client';
610

@@ -34,6 +38,7 @@ export type MachineStatus = 'running' | 'stopped';
3438
*/
3539
export class StateMachine {
3640
private readonly debug;
41+
private readonly onError?: (error: unknown, context?: string) => void;
3742
private context: MachineContext;
3843

3944
private litNodeClient: LitNodeClient;
@@ -51,6 +56,7 @@ export class StateMachine {
5156
constructor(params: BaseStateMachineParams) {
5257
this.id = this.generateId();
5358
this.debug = params.debug ?? false;
59+
this.onError = params.onError;
5460
this.context = new MachineContext(params.context);
5561

5662
this.litNodeClient = params.litNodeClient;
@@ -62,6 +68,7 @@ export class StateMachine {
6268
static fromDefinition(machineConfig: StateMachineDefinition): StateMachine {
6369
const {
6470
debug = false,
71+
onError,
6572
litNodeClient,
6673
litContracts = {},
6774
privateKey,
@@ -94,6 +101,7 @@ export class StateMachine {
94101

95102
const stateMachine = new StateMachine({
96103
debug,
104+
onError,
97105
litNodeClient: litNodeClientInstance,
98106
litContracts: litContractsInstance,
99107
privateKey,
@@ -360,16 +368,18 @@ export class StateMachine {
360368
transitionConfig.listeners = listeners;
361369
// Aggregate (AND) all listener checks to a single function result
362370
transitionConfig.check = async (values) => {
363-
console.log(
364-
`${transition.fromState} -> ${transition.toState} values`,
365-
values
366-
);
371+
debug &&
372+
console.log(
373+
`${transition.fromState} -> ${transition.toState} values`,
374+
values
375+
);
367376
return Promise.all(checks.map((check) => check(values))).then(
368377
(results) => {
369-
console.log(
370-
`${transition.fromState} -> ${transition.toState} results`,
371-
results
372-
);
378+
debug &&
379+
console.log(
380+
`${transition.fromState} -> ${transition.toState} results`,
381+
results
382+
);
373383
return results.every((result) => result);
374384
}
375385
);
@@ -421,10 +431,15 @@ export class StateMachine {
421431
await this.transitionTo(toState);
422432
};
423433

434+
const onTransitionError = async (error: unknown) => {
435+
this.handleError(error, `Error at ${fromState} -> ${toState} transition`);
436+
};
437+
424438
const transition = new Transition({
425439
debug: this.debug,
426440
listeners,
427441
check,
442+
onError: onTransitionError,
428443
onMatch: transitioningOnMatch,
429444
onMismatch,
430445
});
@@ -461,9 +476,9 @@ export class StateMachine {
461476
async stopMachine() {
462477
this.debug && console.log('Stopping state machine...');
463478

479+
this.status = 'stopped';
464480
await this.exitCurrentState();
465481
await this.onStopCallback?.();
466-
this.status = 'stopped';
467482

468483
this.debug && console.log('State machine stopped');
469484
}
@@ -504,10 +519,6 @@ export class StateMachine {
504519
* Stops listening on the current state's transitions and exits the current state.
505520
*/
506521
private async exitCurrentState() {
507-
if (!this.isRunning) {
508-
return;
509-
}
510-
511522
this.debug && console.log('exitCurrentState', this.currentState?.key);
512523

513524
const currentTransitions =
@@ -547,7 +558,13 @@ export class StateMachine {
547558
const nextState = this.states.get(stateKey);
548559

549560
if (!nextState) {
550-
throw new Error(`State ${stateKey} not found`);
561+
throw new UnknownError(
562+
{
563+
currentState: this.currentState,
564+
nextState: stateKey,
565+
},
566+
`Machine next state not found`
567+
);
551568
}
552569
if (this.currentState === nextState) {
553570
console.warn(`State ${stateKey} is already active. Skipping transition.`);
@@ -560,8 +577,33 @@ export class StateMachine {
560577
this.isRunning && (await this.enterState(stateKey));
561578
} catch (e) {
562579
this.currentState = undefined;
563-
console.error(e);
564-
throw new Error(`Could not enter state ${stateKey}`);
580+
this.handleError(e, `Could not enter state ${stateKey}`);
581+
}
582+
}
583+
584+
private handleError(error: unknown, context: string) {
585+
// Try to halt machine if it is still running
586+
if (this.isRunning) {
587+
const publicError = new AutomationError(
588+
{
589+
info: {
590+
stateMachineId: this.id,
591+
status: this.status,
592+
currentState: this.currentState,
593+
},
594+
cause: error,
595+
},
596+
context ?? 'Error running state machine'
597+
);
598+
if (this.onError) {
599+
this.onError(publicError);
600+
} else {
601+
// This throw will likely crash the server
602+
throw publicError;
603+
}
604+
605+
// Throwing when stopping could hide above error
606+
this.stopMachine().catch(console.error);
565607
}
566608
}
567609

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Listener } from '../listeners';
2+
import { onError } from '../types';
23

34
export type CheckFn = (values: (unknown | undefined)[]) => Promise<boolean>;
45
export type resultFn = (values: (unknown | undefined)[]) => Promise<void>;
@@ -13,6 +14,7 @@ export interface BaseTransitionParams {
1314
check?: CheckFn;
1415
onMatch: resultFn;
1516
onMismatch?: resultFn;
17+
onError?: onError;
1618
}
1719

1820
export class Transition {
@@ -22,6 +24,7 @@ export class Transition {
2224
private readonly check?: CheckFn;
2325
private readonly onMatch: resultFn;
2426
private readonly onMismatch?: resultFn;
27+
private readonly onError?: onError;
2528
private readonly queue: Values[] = [];
2629
private isProcessingQueue = false;
2730

@@ -36,12 +39,14 @@ export class Transition {
3639
check,
3740
onMatch,
3841
onMismatch,
42+
onError,
3943
}: BaseTransitionParams) {
4044
this.debug = debug ?? false;
4145
this.listeners = listeners;
4246
this.check = check;
4347
this.onMatch = onMatch;
4448
this.onMismatch = onMismatch;
49+
this.onError = onError;
4550
this.values = new Array(listeners.length).fill(undefined);
4651
this.setupListeners();
4752
}
@@ -60,59 +65,84 @@ export class Transition {
6065
// Process the queue
6166
await this.processQueue();
6267
});
68+
listener.onError?.(this.onError);
6369
});
6470
}
6571

6672
/**
6773
* Starts all listeners for this transition.
6874
*/
6975
async startListening() {
70-
this.debug && console.log('startListening');
71-
await Promise.all(this.listeners.map((listener) => listener.start()));
72-
73-
if (!this.listeners.length) {
74-
// If the transition does not have any listeners it will never emit. Therefore, we "match" automatically on next event loop
75-
setTimeout(() => {
76-
this.debug && console.log('Transition without listeners: auto match');
77-
this.onMatch([]);
78-
}, 0);
76+
try {
77+
this.debug && console.log('startListening');
78+
await Promise.all(this.listeners.map((listener) => listener.start()));
79+
80+
if (!this.listeners.length) {
81+
// If the transition does not have any listeners it will never emit. Therefore, we "match" automatically on next event loop
82+
setTimeout(() => {
83+
this.debug && console.log('Transition without listeners: auto match');
84+
this.onMatch([]);
85+
}, 0);
86+
}
87+
} catch (e) {
88+
if (this.onError) {
89+
this.onError(e);
90+
} else {
91+
throw e;
92+
}
7993
}
8094
}
8195

8296
/**
8397
* Stops all listeners for this transition.
8498
*/
8599
async stopListening() {
86-
this.debug && console.log('stopListening');
87-
this.queue.length = 0; // Flush the queue as there might be more value arrays to check
88-
await Promise.all(this.listeners.map((listener) => listener.stop()));
100+
try {
101+
this.debug && console.log('stopListening');
102+
this.queue.length = 0; // Flush the queue as there might be more value arrays to check
103+
await Promise.all(this.listeners.map((listener) => listener.stop()));
104+
} catch (e) {
105+
if (this.onError) {
106+
this.onError(e);
107+
} else {
108+
throw e;
109+
}
110+
}
89111
}
90112

91113
private async processQueue() {
92-
// Prevent concurrent queue processing
93-
if (this.isProcessingQueue) {
94-
return;
95-
}
96-
this.isProcessingQueue = true;
114+
try {
115+
// Prevent concurrent queue processing
116+
if (this.isProcessingQueue) {
117+
return;
118+
}
119+
this.isProcessingQueue = true;
97120

98-
while (this.queue.length > 0) {
99-
const currentValues = this.queue.shift();
121+
while (this.queue.length > 0) {
122+
const currentValues = this.queue.shift();
100123

101-
if (!currentValues) {
102-
continue;
103-
}
124+
if (!currentValues) {
125+
continue;
126+
}
104127

105-
const isMatch = this.check ? await this.check(currentValues) : true;
128+
const isMatch = this.check ? await this.check(currentValues) : true;
106129

107-
if (isMatch) {
108-
this.debug && console.log('match', currentValues);
109-
await this.onMatch?.(currentValues);
130+
if (isMatch) {
131+
this.debug && console.log('match', currentValues);
132+
await this.onMatch?.(currentValues);
133+
} else {
134+
this.debug && console.log('mismatch', currentValues);
135+
await this.onMismatch?.(currentValues);
136+
}
137+
}
138+
139+
this.isProcessingQueue = false; // Allow new queue processing
140+
} catch (e) {
141+
if (this.onError) {
142+
this.onError(e);
110143
} else {
111-
this.debug && console.log('mismatch', currentValues);
112-
await this.onMismatch?.(currentValues);
144+
throw e;
113145
}
114146
}
115-
116-
this.isProcessingQueue = false; // Allow new queue processing
117147
}
118148
}

packages/automation/src/lib/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { LitNodeClient } from '@lit-protocol/lit-node-client';
66
import { BaseTransitionParams } from './transitions';
77

88
export type Address = `0x${string}`;
9+
export type onError = (error: unknown) => void;
910

1011
export type PKPInfo = {
1112
tokenId: string;
@@ -125,6 +126,7 @@ export interface BaseStateMachineParams {
125126
debug?: boolean;
126127
litContracts: LitContracts;
127128
litNodeClient: LitNodeClient;
129+
onError?: (error: unknown, context?: string) => void;
128130
privateKey?: string;
129131
pkp?: PKPInfo;
130132
}

packages/constants/src/lib/errors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ export const LIT_ERROR: Record<string, ErrorConfig> = {
217217
code: 'transaction_error',
218218
kind: LitErrorKind.Unexpected,
219219
},
220+
AUTOMATION_ERROR: {
221+
name: 'AutomationError',
222+
code: 'automation_error',
223+
kind: LitErrorKind.Unexpected,
224+
},
220225
};
221226

222227
export const LIT_ERROR_CODE = {
@@ -292,6 +297,7 @@ const MultiError = VError.MultiError;
292297
export { MultiError };
293298

294299
export const {
300+
AutomationError,
295301
InitError,
296302
InvalidAccessControlConditions,
297303
InvalidArgumentException,

0 commit comments

Comments
 (0)