|
import { |
|
assign, |
|
createMachine, |
|
pure, |
|
raise, |
|
type ActorBehavior, |
|
type AnyActorRef, |
|
type AnyEventObject, |
|
type AnyStateMachine, |
|
type BaseActionObject, |
|
type MachineContext, |
|
} from 'xstate'; |
|
import { z } from 'zod'; |
|
|
|
const ActorEventAdd = z.object({ |
|
type: z.literal('actor.add'), |
|
params: z.object({ |
|
actor: z.any(), |
|
id: z.string(), |
|
systemId: z.string().optional(), |
|
autoStart: z.boolean().optional(), |
|
input: z.any().optional(), |
|
}), |
|
}); |
|
export type ActorEventAdd = z.infer<typeof ActorEventAdd>; |
|
|
|
const ActorEventStart = z.object({ |
|
type: z.literal('actor.start'), |
|
params: z.object({ |
|
id: z.string(), |
|
}), |
|
}); |
|
export type ActorEventStart = z.infer<typeof ActorEventStart>; |
|
|
|
const ActorEventStop = z.object({ |
|
type: z.literal('actor.stop'), |
|
params: z.object({ |
|
id: z.string(), |
|
}), |
|
}); |
|
export type ActorEventStop = z.infer<typeof ActorEventStop>; |
|
|
|
const ActorEventStarted = z.object({ |
|
type: z.literal('actorManager.started'), |
|
params: z.object({ |
|
id: z.string(), |
|
}), |
|
}); |
|
export type ActorEventStarted = z.infer<typeof ActorEventStarted>; |
|
|
|
const ActorEventStopped = z.object({ |
|
type: z.literal('actorManager.stopped'), |
|
params: z.object({ |
|
id: z.string(), |
|
}), |
|
}); |
|
export type ActorEventStopped = z.infer<typeof ActorEventStopped>; |
|
|
|
const ActorProcessEvents = z.object({ |
|
type: z.literal('actorManager.processEvents'), |
|
}); |
|
export type ActorProcessEvents = z.infer<typeof ActorProcessEvents>; |
|
|
|
const ActorEventsProcessed = z.object({ |
|
type: z.literal('actorManager.eventsProcessed'), |
|
}); |
|
export type ActorEventsProcessed = z.infer<typeof ActorEventsProcessed>; |
|
|
|
export interface ActorManagerActor { |
|
id: string; |
|
systemId: string; |
|
src: AnyStateMachine | ActorBehavior<AnyEventObject>; |
|
actorRef: AnyActorRef; |
|
input?: unknown; |
|
} |
|
|
|
export interface ActorManagerContext extends MachineContext { |
|
actors: ActorManagerActor[]; |
|
events: AnyEventObject[]; |
|
isWorking: boolean; |
|
currentEvent: AnyEventObject | null; |
|
} |
|
|
|
export const actorManagerMachine = createMachine( |
|
{ |
|
id: 'Actor Manager', |
|
context: { |
|
actors: [], |
|
events: [], |
|
isWorking: false, |
|
currentEvent: null, |
|
}, |
|
initial: 'ACTORS IDLE', |
|
states: { |
|
'ACTORS IDLE': { |
|
entry: ['setNotWorking'], |
|
always: [ |
|
{ |
|
target: 'ACTORS PROCESSING', |
|
guard: 'hasEvents', |
|
}, |
|
], |
|
on: { |
|
'actorManager.processEvents': { |
|
target: 'ACTORS PROCESSING', |
|
guard: 'hasEvents', |
|
}, |
|
}, |
|
}, |
|
'ACTORS PROCESSING': { |
|
entry: ['setWorking', 'eventStackWorker'], |
|
always: [ |
|
{ |
|
target: 'ACTORS STARTING', |
|
guard: 'currentIsStart', |
|
}, |
|
{ |
|
target: 'ACTORS STOPPING', |
|
guard: 'currentIsStop', |
|
}, |
|
], |
|
on: { |
|
'actorManager.processEvents': { |
|
target: 'ACTORS PROCESSING', |
|
reenter: true, |
|
}, |
|
'actorManager.eventsProcessed': { |
|
target: 'ACTORS IDLE', |
|
}, |
|
}, |
|
}, |
|
'ACTORS STARTING': { |
|
entry: ['startActor'], |
|
on: { |
|
'actorManager.processEvents': { |
|
target: 'ACTORS PROCESSING', |
|
}, |
|
}, |
|
}, |
|
'ACTORS STOPPING': { |
|
entry: ['stopActor'], |
|
on: { |
|
'actorManager.processEvents': { |
|
target: 'ACTORS PROCESSING', |
|
}, |
|
}, |
|
}, |
|
}, |
|
on: { |
|
'actor.add': { |
|
actions: ['addActor'], |
|
}, |
|
'actor.*': { |
|
actions: ['addEvent', 'startProcess'], |
|
}, |
|
}, |
|
|
|
types: { |
|
context: {} as ActorManagerContext, |
|
events: {} as |
|
| ActorEventAdd |
|
| ActorEventStart |
|
| ActorEventStop |
|
| ActorProcessEvents |
|
| ActorEventsProcessed |
|
| { type: 'actor.*'; params: unknown }, |
|
}, |
|
}, |
|
{ |
|
guards: { |
|
hasEvents: ({ context }) => context.events.length > 0, |
|
currentIsStart: ({ context }) => |
|
context.currentEvent?.type === 'actor.start', |
|
currentIsStop: ({ context }) => |
|
context.currentEvent?.type === 'actor.stop', |
|
}, |
|
actions: { |
|
startProcess: ({ self }) => |
|
self.send({ type: 'actorManager.processEvents' }), |
|
addEvent: assign({ |
|
events: ({ context, event }) => { |
|
const parsed = z |
|
.union([ActorEventAdd, ActorEventStart, ActorEventStop]) |
|
.safeParse(event); |
|
if (!parsed.success) { |
|
if (process.env.NODE_ENV === 'development') { |
|
console.error( |
|
'Event is not a valid ActorEventAdd, ActorEventStart, or ActorEventStop', |
|
parsed.error |
|
); |
|
} |
|
return context.events; |
|
} |
|
return [...context.events, event]; |
|
}, |
|
}), |
|
addActor: pure(({ event }) => { |
|
const parsed = ActorEventAdd.safeParse(event); |
|
if (!parsed.success) { |
|
if (process.env.NODE_ENV === 'development') { |
|
console.error('Event is not a valid ActorEventAdd', parsed.error); |
|
} |
|
return []; |
|
} |
|
|
|
const finalActions: BaseActionObject[] = []; |
|
|
|
finalActions.push( |
|
assign({ |
|
actors: ({ context }) => { |
|
const { actor, id, systemId, input } = parsed.data.params; |
|
const newActor = { |
|
id, |
|
systemId: systemId || id, |
|
src: actor, |
|
actorRef: null, |
|
input, |
|
}; |
|
return [...context.actors, newActor]; |
|
}, |
|
}) |
|
); |
|
|
|
if (parsed.data.params.autoStart) { |
|
raise({ |
|
type: 'actor.start', |
|
params: { id: parsed.data.params.id }, |
|
} as never); |
|
} |
|
return finalActions; |
|
}), |
|
startActor: pure(({ context }) => { |
|
const actorId = context.currentEvent?.params.id; |
|
const actorData = context.actors.find( |
|
(a) => actorId && a.id === actorId |
|
); |
|
if (!actorData || actorData.actorRef !== null) { |
|
if (process.env.NODE_ENV === 'development') { |
|
console.warn( |
|
!actorData |
|
? `Actor with id ${actorId} not found in actor manager` |
|
: `Actor with id ${actorId} has previously been started` |
|
); |
|
} |
|
return [raise({ type: 'actorManager.processEvents' })]; |
|
} |
|
return [ |
|
assign({ |
|
actors: ({ context, spawn }) => |
|
context.actors.map((a: ActorManagerActor) => |
|
a.id === actorId |
|
? { |
|
...a, |
|
actorRef: spawn(a.src, { id: a.id, input: a.input }), |
|
input: undefined, // Clear for GC |
|
} |
|
: a |
|
), |
|
}), |
|
raise({ type: 'actorManager.processEvents' }), |
|
]; |
|
}), |
|
stopActor: pure(({ context }) => { |
|
const actorId = context.currentEvent?.params.id; |
|
const actorData = context.actors.find( |
|
(a) => actorId && a.id === actorId |
|
); |
|
if (!actorData || actorData.actorRef === null) { |
|
if (process.env.NODE_ENV === 'development') { |
|
console.warn( |
|
!actorData |
|
? `Actor with id ${actorId} not found in actor manager` |
|
: `Actor with id ${actorId} is not running` |
|
); |
|
} |
|
return [raise({ type: 'actorManager.processEvents' })]; |
|
} |
|
return [ |
|
assign({ |
|
actors: ({ context }) => |
|
context.actors.map((a: ActorManagerActor) => { |
|
if (a.id === actorId) { |
|
a.actorRef.stop(); |
|
return { |
|
...a, |
|
actorRef: null, |
|
}; |
|
} |
|
return a; |
|
}), |
|
}), |
|
raise({ type: 'actorManager.processEvents' }), |
|
]; |
|
}), |
|
eventStackWorker: pure(({ context }) => { |
|
if (context.currentEvent) { |
|
return []; // Already processing an event |
|
} |
|
const events = context.events || []; |
|
if (events.length === 0) { |
|
return [raise({ type: 'actorManager.eventsProcessed' })]; |
|
} |
|
return [ |
|
assign({ |
|
currentEvent: () => events[0], |
|
events: () => events.slice(1), |
|
}), |
|
]; |
|
}), |
|
setWorking: assign({ |
|
isWorking: () => true, |
|
}), |
|
setNotWorking: assign({ |
|
isWorking: () => false, |
|
}), |
|
}, |
|
} |
|
); |