effect-machine 0.1.0
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.
- package/README.md +221 -0
- package/package.json +63 -0
- package/src/actor.ts +942 -0
- package/src/cluster/entity-machine.ts +202 -0
- package/src/cluster/index.ts +43 -0
- package/src/cluster/to-entity.ts +99 -0
- package/src/errors.ts +64 -0
- package/src/index.ts +102 -0
- package/src/inspection.ts +132 -0
- package/src/internal/brands.ts +51 -0
- package/src/internal/transition.ts +427 -0
- package/src/internal/utils.ts +80 -0
- package/src/machine.ts +685 -0
- package/src/persistence/adapter.ts +169 -0
- package/src/persistence/adapters/in-memory.ts +275 -0
- package/src/persistence/index.ts +24 -0
- package/src/persistence/persistent-actor.ts +601 -0
- package/src/persistence/persistent-machine.ts +131 -0
- package/src/schema.ts +316 -0
- package/src/slot.ts +281 -0
- package/src/testing.ts +282 -0
- package/tsconfig.json +68 -0
package/src/machine.ts
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine namespace - fluent builder API for state machines.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { Machine, State, Event, Slot } from "effect-machine"
|
|
7
|
+
*
|
|
8
|
+
* const MyState = State({ Idle: {}, Running: { count: Schema.Number } })
|
|
9
|
+
* const MyEvent = Event({ Start: {}, Stop: {} })
|
|
10
|
+
*
|
|
11
|
+
* const MyGuards = Slot.Guards({
|
|
12
|
+
* canStart: { threshold: Schema.Number },
|
|
13
|
+
* })
|
|
14
|
+
*
|
|
15
|
+
* const MyEffects = Slot.Effects({
|
|
16
|
+
* notify: { message: Schema.String },
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* const machine = Machine.make({
|
|
20
|
+
* state: MyState,
|
|
21
|
+
* event: MyEvent,
|
|
22
|
+
* guards: MyGuards,
|
|
23
|
+
* effects: MyEffects,
|
|
24
|
+
* initial: MyState.Idle,
|
|
25
|
+
* })
|
|
26
|
+
* .on(MyState.Idle, MyEvent.Start, ({ state, guards, effects }) =>
|
|
27
|
+
* Effect.gen(function* () {
|
|
28
|
+
* if (yield* guards.canStart({ threshold: 5 })) {
|
|
29
|
+
* yield* effects.notify({ message: "Starting!" })
|
|
30
|
+
* return MyState.Running({ count: 0 })
|
|
31
|
+
* }
|
|
32
|
+
* return state
|
|
33
|
+
* })
|
|
34
|
+
* )
|
|
35
|
+
* .on(MyState.Running, MyEvent.Stop, () => MyState.Idle)
|
|
36
|
+
* .final(MyState.Idle)
|
|
37
|
+
* .provide({
|
|
38
|
+
* canStart: ({ threshold }) => Effect.succeed(threshold > 0),
|
|
39
|
+
* notify: ({ message }) => Effect.log(message),
|
|
40
|
+
* })
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @module
|
|
44
|
+
*/
|
|
45
|
+
import type { Schema, Schedule, Scope, Context } from "effect";
|
|
46
|
+
import { Effect } from "effect";
|
|
47
|
+
import type { Pipeable } from "effect/Pipeable";
|
|
48
|
+
import { pipeArguments } from "effect/Pipeable";
|
|
49
|
+
|
|
50
|
+
import type { TransitionResult } from "./internal/utils.js";
|
|
51
|
+
import { getTag } from "./internal/utils.js";
|
|
52
|
+
import type { TaggedOrConstructor, BrandedState, BrandedEvent } from "./internal/brands.js";
|
|
53
|
+
import type { MachineStateSchema, MachineEventSchema, VariantsUnion } from "./schema.js";
|
|
54
|
+
import type { PersistentMachine, WithPersistenceConfig } from "./persistence/persistent-machine.js";
|
|
55
|
+
import { persist as persistImpl } from "./persistence/persistent-machine.js";
|
|
56
|
+
import { SlotProvisionError, ProvisionValidationError } from "./errors.js";
|
|
57
|
+
import type {
|
|
58
|
+
GuardsSchema,
|
|
59
|
+
EffectsSchema,
|
|
60
|
+
GuardsDef,
|
|
61
|
+
EffectsDef,
|
|
62
|
+
GuardSlots,
|
|
63
|
+
EffectSlots,
|
|
64
|
+
GuardHandlers,
|
|
65
|
+
EffectHandlers as SlotEffectHandlers,
|
|
66
|
+
MachineContext,
|
|
67
|
+
} from "./slot.js";
|
|
68
|
+
import { MachineContextTag } from "./slot.js";
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Core types
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Self reference for sending events back to the machine
|
|
76
|
+
*/
|
|
77
|
+
export interface MachineRef<Event> {
|
|
78
|
+
readonly send: (event: Event) => Effect.Effect<void>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Handler context passed to transition handlers
|
|
83
|
+
*/
|
|
84
|
+
export interface HandlerContext<State, Event, GD extends GuardsDef, ED extends EffectsDef> {
|
|
85
|
+
readonly state: State;
|
|
86
|
+
readonly event: Event;
|
|
87
|
+
readonly guards: GuardSlots<GD>;
|
|
88
|
+
readonly effects: EffectSlots<ED>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Handler context passed to state effect handlers (onEnter, spawn, background)
|
|
93
|
+
*/
|
|
94
|
+
export interface StateHandlerContext<State, Event, ED extends EffectsDef> {
|
|
95
|
+
readonly state: State;
|
|
96
|
+
readonly event: Event;
|
|
97
|
+
readonly self: MachineRef<Event>;
|
|
98
|
+
readonly effects: EffectSlots<ED>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Transition handler function
|
|
103
|
+
*/
|
|
104
|
+
export type TransitionHandler<S, E, NewState, GD extends GuardsDef, ED extends EffectsDef, R> = (
|
|
105
|
+
ctx: HandlerContext<S, E, GD, ED>,
|
|
106
|
+
) => TransitionResult<NewState, R>;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* State effect handler function
|
|
110
|
+
*/
|
|
111
|
+
export type StateEffectHandler<S, E, ED extends EffectsDef, R> = (
|
|
112
|
+
ctx: StateHandlerContext<S, E, ED>,
|
|
113
|
+
) => Effect.Effect<void, never, R>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Transition definition
|
|
117
|
+
*/
|
|
118
|
+
export interface Transition<State, Event, GD extends GuardsDef, ED extends EffectsDef, R> {
|
|
119
|
+
readonly stateTag: string;
|
|
120
|
+
readonly eventTag: string;
|
|
121
|
+
readonly handler: TransitionHandler<State, Event, State, GD, ED, R>;
|
|
122
|
+
readonly reenter?: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Spawn effect - state-scoped forked effect
|
|
127
|
+
*/
|
|
128
|
+
export interface SpawnEffect<State, Event, ED extends EffectsDef, R> {
|
|
129
|
+
readonly stateTag: string;
|
|
130
|
+
readonly handler: StateEffectHandler<State, Event, ED, R>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Background effect - runs for entire machine lifetime
|
|
135
|
+
*/
|
|
136
|
+
export interface BackgroundEffect<State, Event, ED extends EffectsDef, R> {
|
|
137
|
+
readonly handler: StateEffectHandler<State, Event, ED, R>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Options types
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
/** Options for `persist` */
|
|
145
|
+
export interface PersistOptions {
|
|
146
|
+
readonly snapshotSchedule: Schedule.Schedule<unknown, { readonly _tag: string }>;
|
|
147
|
+
readonly journalEvents: boolean;
|
|
148
|
+
readonly machineType?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Internal helpers
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
156
|
+
type IsUnknown<T> = unknown extends T ? ([T] extends [unknown] ? true : false) : false;
|
|
157
|
+
type NormalizeR<T> = IsAny<T> extends true ? T : IsUnknown<T> extends true ? never : T;
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// MakeConfig
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
export interface MakeConfig<
|
|
164
|
+
SD extends Record<string, Schema.Struct.Fields>,
|
|
165
|
+
ED extends Record<string, Schema.Struct.Fields>,
|
|
166
|
+
S extends BrandedState,
|
|
167
|
+
E extends BrandedEvent,
|
|
168
|
+
GD extends GuardsDef,
|
|
169
|
+
EFD extends EffectsDef,
|
|
170
|
+
> {
|
|
171
|
+
readonly state: MachineStateSchema<SD> & { Type: S };
|
|
172
|
+
readonly event: MachineEventSchema<ED> & { Type: E };
|
|
173
|
+
readonly guards?: GuardsSchema<GD>;
|
|
174
|
+
readonly effects?: EffectsSchema<EFD>;
|
|
175
|
+
readonly initial: S;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// Provide types
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
/** Check if a GuardsDef has any actual keys */
|
|
183
|
+
type HasGuardKeys<GD extends GuardsDef> = [keyof GD] extends [never]
|
|
184
|
+
? false
|
|
185
|
+
: GD extends Record<string, never>
|
|
186
|
+
? false
|
|
187
|
+
: true;
|
|
188
|
+
|
|
189
|
+
/** Check if an EffectsDef has any actual keys */
|
|
190
|
+
type HasEffectKeys<EFD extends EffectsDef> = [keyof EFD] extends [never]
|
|
191
|
+
? false
|
|
192
|
+
: EFD extends Record<string, never>
|
|
193
|
+
? false
|
|
194
|
+
: true;
|
|
195
|
+
|
|
196
|
+
/** Context type passed to guard/effect handlers */
|
|
197
|
+
export type SlotContext<State, Event> = MachineContext<State, Event, MachineRef<Event>>;
|
|
198
|
+
|
|
199
|
+
/** Combined handlers for provide() - guards and effects only */
|
|
200
|
+
export type ProvideHandlers<
|
|
201
|
+
State,
|
|
202
|
+
Event,
|
|
203
|
+
GD extends GuardsDef,
|
|
204
|
+
EFD extends EffectsDef,
|
|
205
|
+
R,
|
|
206
|
+
> = (HasGuardKeys<GD> extends true ? GuardHandlers<GD, SlotContext<State, Event>, R> : object) &
|
|
207
|
+
(HasEffectKeys<EFD> extends true
|
|
208
|
+
? SlotEffectHandlers<EFD, SlotContext<State, Event>, R>
|
|
209
|
+
: object);
|
|
210
|
+
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// Machine class
|
|
213
|
+
// ============================================================================
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Machine definition with fluent builder API.
|
|
217
|
+
*
|
|
218
|
+
* Type parameters:
|
|
219
|
+
* - `State`: The state union type
|
|
220
|
+
* - `Event`: The event union type
|
|
221
|
+
* - `R`: Effect requirements
|
|
222
|
+
* - `_SD`: State schema definition (for compile-time validation)
|
|
223
|
+
* - `_ED`: Event schema definition (for compile-time validation)
|
|
224
|
+
* - `GD`: Guard definitions
|
|
225
|
+
* - `EFD`: Effect definitions
|
|
226
|
+
*/
|
|
227
|
+
export class Machine<
|
|
228
|
+
State,
|
|
229
|
+
Event,
|
|
230
|
+
R = never,
|
|
231
|
+
_SD extends Record<string, Schema.Struct.Fields> = Record<string, Schema.Struct.Fields>,
|
|
232
|
+
_ED extends Record<string, Schema.Struct.Fields> = Record<string, Schema.Struct.Fields>,
|
|
233
|
+
GD extends GuardsDef = Record<string, never>,
|
|
234
|
+
EFD extends EffectsDef = Record<string, never>,
|
|
235
|
+
> implements Pipeable {
|
|
236
|
+
readonly initial: State;
|
|
237
|
+
/** @internal */ readonly _transitions: Array<Transition<State, Event, GD, EFD, R>>;
|
|
238
|
+
/** @internal */ readonly _spawnEffects: Array<SpawnEffect<State, Event, EFD, R>>;
|
|
239
|
+
/** @internal */ readonly _backgroundEffects: Array<BackgroundEffect<State, Event, EFD, R>>;
|
|
240
|
+
/** @internal */ readonly _finalStates: Set<string>;
|
|
241
|
+
/** @internal */ readonly _guardsSchema?: GuardsSchema<GD>;
|
|
242
|
+
/** @internal */ readonly _effectsSchema?: EffectsSchema<EFD>;
|
|
243
|
+
/** @internal */ readonly _guardHandlers: Map<
|
|
244
|
+
string,
|
|
245
|
+
(params: unknown, ctx: SlotContext<State, Event>) => boolean | Effect.Effect<boolean, never, R>
|
|
246
|
+
>;
|
|
247
|
+
/** @internal */ readonly _effectHandlers: Map<
|
|
248
|
+
string,
|
|
249
|
+
(params: unknown, ctx: SlotContext<State, Event>) => Effect.Effect<void, never, R>
|
|
250
|
+
>;
|
|
251
|
+
readonly stateSchema?: Schema.Schema<State, unknown, never>;
|
|
252
|
+
readonly eventSchema?: Schema.Schema<Event, unknown, never>;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Context tag for accessing machine state/event/self in slot handlers.
|
|
256
|
+
* Uses shared module-level tag for all machines.
|
|
257
|
+
*/
|
|
258
|
+
readonly Context: Context.Tag<
|
|
259
|
+
MachineContext<State, Event, MachineRef<Event>>,
|
|
260
|
+
MachineContext<State, Event, MachineRef<Event>>
|
|
261
|
+
> = MachineContextTag as Context.Tag<
|
|
262
|
+
MachineContext<State, Event, MachineRef<Event>>,
|
|
263
|
+
MachineContext<State, Event, MachineRef<Event>>
|
|
264
|
+
>;
|
|
265
|
+
|
|
266
|
+
// Public readonly views
|
|
267
|
+
get transitions(): ReadonlyArray<Transition<State, Event, GD, EFD, R>> {
|
|
268
|
+
return this._transitions;
|
|
269
|
+
}
|
|
270
|
+
get spawnEffects(): ReadonlyArray<SpawnEffect<State, Event, EFD, R>> {
|
|
271
|
+
return this._spawnEffects;
|
|
272
|
+
}
|
|
273
|
+
get backgroundEffects(): ReadonlyArray<BackgroundEffect<State, Event, EFD, R>> {
|
|
274
|
+
return this._backgroundEffects;
|
|
275
|
+
}
|
|
276
|
+
get finalStates(): ReadonlySet<string> {
|
|
277
|
+
return this._finalStates;
|
|
278
|
+
}
|
|
279
|
+
get guardsSchema(): GuardsSchema<GD> | undefined {
|
|
280
|
+
return this._guardsSchema;
|
|
281
|
+
}
|
|
282
|
+
get effectsSchema(): EffectsSchema<EFD> | undefined {
|
|
283
|
+
return this._effectsSchema;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** @internal */
|
|
287
|
+
constructor(
|
|
288
|
+
initial: State,
|
|
289
|
+
stateSchema?: Schema.Schema<State, unknown, never>,
|
|
290
|
+
eventSchema?: Schema.Schema<Event, unknown, never>,
|
|
291
|
+
guardsSchema?: GuardsSchema<GD>,
|
|
292
|
+
effectsSchema?: EffectsSchema<EFD>,
|
|
293
|
+
) {
|
|
294
|
+
this.initial = initial;
|
|
295
|
+
this._transitions = [];
|
|
296
|
+
this._spawnEffects = [];
|
|
297
|
+
this._backgroundEffects = [];
|
|
298
|
+
this._finalStates = new Set();
|
|
299
|
+
this._guardsSchema = guardsSchema;
|
|
300
|
+
this._effectsSchema = effectsSchema;
|
|
301
|
+
this._guardHandlers = new Map();
|
|
302
|
+
this._effectHandlers = new Map();
|
|
303
|
+
this.stateSchema = stateSchema;
|
|
304
|
+
this.eventSchema = eventSchema;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
pipe() {
|
|
308
|
+
// eslint-disable-next-line prefer-rest-params
|
|
309
|
+
return pipeArguments(this, arguments);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---- on ----
|
|
313
|
+
|
|
314
|
+
on<
|
|
315
|
+
NS extends VariantsUnion<_SD> & BrandedState,
|
|
316
|
+
NE extends VariantsUnion<_ED> & BrandedEvent,
|
|
317
|
+
RS extends VariantsUnion<_SD> & BrandedState,
|
|
318
|
+
>(
|
|
319
|
+
state: TaggedOrConstructor<NS>,
|
|
320
|
+
event: TaggedOrConstructor<NE>,
|
|
321
|
+
handler: TransitionHandler<NS, NE, RS, GD, EFD, never>,
|
|
322
|
+
): Machine<State, Event, R, _SD, _ED, GD, EFD> {
|
|
323
|
+
return this.addTransition(state, event, handler, false);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ---- reenter ----
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Like `on()`, but forces onEnter/spawn to run even when transitioning to the same state tag.
|
|
330
|
+
* Use this to restart timers, re-run spawned effects, or reset state-scoped effects.
|
|
331
|
+
*/
|
|
332
|
+
reenter<
|
|
333
|
+
NS extends VariantsUnion<_SD> & BrandedState,
|
|
334
|
+
NE extends VariantsUnion<_ED> & BrandedEvent,
|
|
335
|
+
RS extends VariantsUnion<_SD> & BrandedState,
|
|
336
|
+
>(
|
|
337
|
+
state: TaggedOrConstructor<NS>,
|
|
338
|
+
event: TaggedOrConstructor<NE>,
|
|
339
|
+
handler: TransitionHandler<NS, NE, RS, GD, EFD, never>,
|
|
340
|
+
): Machine<State, Event, R, _SD, _ED, GD, EFD> {
|
|
341
|
+
return this.addTransition(state, event, handler, true);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** @internal */
|
|
345
|
+
private addTransition<NS extends BrandedState, NE extends BrandedEvent>(
|
|
346
|
+
state: TaggedOrConstructor<NS>,
|
|
347
|
+
event: TaggedOrConstructor<NE>,
|
|
348
|
+
handler: TransitionHandler<NS, NE, BrandedState, GD, EFD, never>,
|
|
349
|
+
reenter: boolean,
|
|
350
|
+
): Machine<State, Event, R, _SD, _ED, GD, EFD> {
|
|
351
|
+
const stateTag = getTag(state);
|
|
352
|
+
const eventTag = getTag(event);
|
|
353
|
+
|
|
354
|
+
const transition: Transition<State, Event, GD, EFD, R> = {
|
|
355
|
+
stateTag,
|
|
356
|
+
eventTag,
|
|
357
|
+
handler: handler as unknown as Transition<State, Event, GD, EFD, R>["handler"],
|
|
358
|
+
reenter,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
362
|
+
(this._transitions as any[]).push(transition);
|
|
363
|
+
|
|
364
|
+
return this;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ---- spawn ----
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* State-scoped effect that is forked on state entry and automatically cancelled on state exit.
|
|
371
|
+
* Use effect slots defined via `Slot.Effects` for the actual work.
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* ```ts
|
|
375
|
+
* const MyEffects = Slot.Effects({
|
|
376
|
+
* fetchData: { url: Schema.String },
|
|
377
|
+
* });
|
|
378
|
+
*
|
|
379
|
+
* machine
|
|
380
|
+
* .spawn(State.Loading, ({ effects, state }) => effects.fetchData({ url: state.url }))
|
|
381
|
+
* .provide({
|
|
382
|
+
* fetchData: ({ url }, { self }) =>
|
|
383
|
+
* Effect.gen(function* () {
|
|
384
|
+
* yield* Effect.addFinalizer(() => Effect.log("Leaving Loading"));
|
|
385
|
+
* const data = yield* Http.get(url);
|
|
386
|
+
* yield* self.send(Event.Loaded({ data }));
|
|
387
|
+
* }),
|
|
388
|
+
* });
|
|
389
|
+
* ```
|
|
390
|
+
*/
|
|
391
|
+
spawn<NS extends VariantsUnion<_SD> & BrandedState>(
|
|
392
|
+
state: TaggedOrConstructor<NS>,
|
|
393
|
+
handler: StateEffectHandler<NS, VariantsUnion<_ED> & BrandedEvent, EFD, Scope.Scope>,
|
|
394
|
+
): Machine<State, Event, R, _SD, _ED, GD, EFD> {
|
|
395
|
+
const stateTag = getTag(state);
|
|
396
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
397
|
+
(this._spawnEffects as any[]).push({
|
|
398
|
+
stateTag,
|
|
399
|
+
handler: handler as unknown as SpawnEffect<State, Event, EFD, R>["handler"],
|
|
400
|
+
});
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ---- background ----
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
|
|
408
|
+
* Use effect slots defined via `Slot.Effects` for the actual work.
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```ts
|
|
412
|
+
* const MyEffects = Slot.Effects({
|
|
413
|
+
* heartbeat: {},
|
|
414
|
+
* });
|
|
415
|
+
*
|
|
416
|
+
* machine
|
|
417
|
+
* .background(({ effects }) => effects.heartbeat())
|
|
418
|
+
* .provide({
|
|
419
|
+
* heartbeat: (_, { self }) =>
|
|
420
|
+
* Effect.forever(
|
|
421
|
+
* Effect.sleep("30 seconds").pipe(Effect.andThen(self.send(Event.Ping)))
|
|
422
|
+
* ),
|
|
423
|
+
* });
|
|
424
|
+
* ```
|
|
425
|
+
*/
|
|
426
|
+
background(
|
|
427
|
+
handler: StateEffectHandler<State, Event, EFD, Scope.Scope>,
|
|
428
|
+
): Machine<State, Event, R, _SD, _ED, GD, EFD> {
|
|
429
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
430
|
+
(this._backgroundEffects as any[]).push({
|
|
431
|
+
handler: handler as unknown as BackgroundEffect<State, Event, EFD, R>["handler"],
|
|
432
|
+
});
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ---- final ----
|
|
437
|
+
|
|
438
|
+
final<NS extends VariantsUnion<_SD> & BrandedState>(
|
|
439
|
+
state: TaggedOrConstructor<NS>,
|
|
440
|
+
): Machine<State, Event, R, _SD, _ED, GD, EFD> {
|
|
441
|
+
const stateTag = getTag(state);
|
|
442
|
+
this._finalStates.add(stateTag);
|
|
443
|
+
return this;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---- provide ----
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Provide implementations for guard and effect slots.
|
|
450
|
+
* Creates a new machine instance (the original can be reused with different providers).
|
|
451
|
+
*/
|
|
452
|
+
provide<R2>(
|
|
453
|
+
handlers: ProvideHandlers<State, Event, GD, EFD, R2>,
|
|
454
|
+
): Machine<State, Event, R | NormalizeR<R2>, _SD, _ED, GD, EFD> {
|
|
455
|
+
// Collect all required slot names in a single pass
|
|
456
|
+
const requiredSlots = new Set<string>();
|
|
457
|
+
if (this._guardsSchema !== undefined) {
|
|
458
|
+
for (const name of Object.keys(this._guardsSchema.definitions)) {
|
|
459
|
+
requiredSlots.add(name);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (this._effectsSchema !== undefined) {
|
|
463
|
+
for (const name of Object.keys(this._effectsSchema.definitions)) {
|
|
464
|
+
requiredSlots.add(name);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Single-pass validation: collect all missing and extra handlers
|
|
469
|
+
const providedSlots = new Set(Object.keys(handlers));
|
|
470
|
+
const missing: string[] = [];
|
|
471
|
+
const extra: string[] = [];
|
|
472
|
+
|
|
473
|
+
for (const name of requiredSlots) {
|
|
474
|
+
if (!providedSlots.has(name)) {
|
|
475
|
+
missing.push(name);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
for (const name of providedSlots) {
|
|
479
|
+
if (!requiredSlots.has(name)) {
|
|
480
|
+
extra.push(name);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Report all validation errors at once
|
|
485
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
486
|
+
throw new ProvisionValidationError({ missing, extra });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Create new machine to preserve original for reuse with different providers
|
|
490
|
+
const result = new Machine<State, Event, R | R2, _SD, _ED, GD, EFD>(
|
|
491
|
+
this.initial,
|
|
492
|
+
this.stateSchema as Schema.Schema<State, unknown, never>,
|
|
493
|
+
this.eventSchema as Schema.Schema<Event, unknown, never>,
|
|
494
|
+
this._guardsSchema,
|
|
495
|
+
this._effectsSchema,
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// Share immutable arrays (never mutated after provide)
|
|
499
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
500
|
+
(result as any)._transitions = this._transitions;
|
|
501
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
502
|
+
(result as any)._finalStates = this._finalStates;
|
|
503
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
504
|
+
(result as any)._spawnEffects = this._spawnEffects;
|
|
505
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
506
|
+
(result as any)._backgroundEffects = this._backgroundEffects;
|
|
507
|
+
|
|
508
|
+
// Register handlers from provided object
|
|
509
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
510
|
+
const anyHandlers = handlers as Record<string, any>;
|
|
511
|
+
if (this._guardsSchema !== undefined) {
|
|
512
|
+
for (const name of Object.keys(this._guardsSchema.definitions)) {
|
|
513
|
+
result._guardHandlers.set(name, anyHandlers[name]);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (this._effectsSchema !== undefined) {
|
|
517
|
+
for (const name of Object.keys(this._effectsSchema.definitions)) {
|
|
518
|
+
result._effectHandlers.set(name, anyHandlers[name]);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return result as unknown as Machine<State, Event, R | NormalizeR<R2>, _SD, _ED, GD, EFD>;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ---- persist ----
|
|
526
|
+
|
|
527
|
+
persist(
|
|
528
|
+
config: PersistOptions,
|
|
529
|
+
): PersistentMachine<State & { readonly _tag: string }, Event & { readonly _tag: string }, R> {
|
|
530
|
+
return persistImpl(config as WithPersistenceConfig)(
|
|
531
|
+
this as unknown as Machine<BrandedState, BrandedEvent, R>,
|
|
532
|
+
) as unknown as PersistentMachine<
|
|
533
|
+
State & { readonly _tag: string },
|
|
534
|
+
Event & { readonly _tag: string },
|
|
535
|
+
R
|
|
536
|
+
>;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Create guard and effect slot accessors for use in handlers.
|
|
541
|
+
* @internal Used by the event loop to create typed accessors.
|
|
542
|
+
*/
|
|
543
|
+
_createSlotAccessors(ctx: MachineContext<State, Event, MachineRef<Event>>): {
|
|
544
|
+
guards: GuardSlots<GD>;
|
|
545
|
+
effects: EffectSlots<EFD>;
|
|
546
|
+
} {
|
|
547
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
548
|
+
const machine = this;
|
|
549
|
+
|
|
550
|
+
// Create guard slots that resolve to actual handlers
|
|
551
|
+
const guards =
|
|
552
|
+
this._guardsSchema !== undefined
|
|
553
|
+
? this._guardsSchema._createSlots((name: string, params: unknown) => {
|
|
554
|
+
const handler = machine._guardHandlers.get(name);
|
|
555
|
+
if (handler === undefined) {
|
|
556
|
+
return Effect.die(new SlotProvisionError({ slotName: name, slotType: "guard" }));
|
|
557
|
+
}
|
|
558
|
+
const result = handler(params, ctx);
|
|
559
|
+
// Handler may return boolean or Effect<boolean>
|
|
560
|
+
const normalized = typeof result === "boolean" ? Effect.succeed(result) : result;
|
|
561
|
+
return normalized.pipe(Effect.provideService(machine.Context, ctx)) as Effect.Effect<
|
|
562
|
+
boolean,
|
|
563
|
+
never,
|
|
564
|
+
never
|
|
565
|
+
>;
|
|
566
|
+
})
|
|
567
|
+
: ({} as GuardSlots<GD>);
|
|
568
|
+
|
|
569
|
+
// Create effect slots that resolve to actual handlers
|
|
570
|
+
const effects =
|
|
571
|
+
this._effectsSchema !== undefined
|
|
572
|
+
? this._effectsSchema._createSlots((name: string, params: unknown) => {
|
|
573
|
+
const handler = machine._effectHandlers.get(name);
|
|
574
|
+
if (handler === undefined) {
|
|
575
|
+
return Effect.die(new SlotProvisionError({ slotName: name, slotType: "effect" }));
|
|
576
|
+
}
|
|
577
|
+
return handler(params, ctx).pipe(
|
|
578
|
+
Effect.provideService(machine.Context, ctx),
|
|
579
|
+
) as Effect.Effect<void, never, never>;
|
|
580
|
+
})
|
|
581
|
+
: ({} as EffectSlots<EFD>);
|
|
582
|
+
|
|
583
|
+
return { guards, effects };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ---- Static factory ----
|
|
587
|
+
|
|
588
|
+
static make<
|
|
589
|
+
SD extends Record<string, Schema.Struct.Fields>,
|
|
590
|
+
ED extends Record<string, Schema.Struct.Fields>,
|
|
591
|
+
S extends BrandedState,
|
|
592
|
+
E extends BrandedEvent,
|
|
593
|
+
GD extends GuardsDef = Record<string, never>,
|
|
594
|
+
EFD extends EffectsDef = Record<string, never>,
|
|
595
|
+
>(config: MakeConfig<SD, ED, S, E, GD, EFD>): Machine<S, E, never, SD, ED, GD, EFD> {
|
|
596
|
+
return new Machine<S, E, never, SD, ED, GD, EFD>(
|
|
597
|
+
config.initial,
|
|
598
|
+
config.state as unknown as Schema.Schema<S, unknown, never>,
|
|
599
|
+
config.event as unknown as Schema.Schema<E, unknown, never>,
|
|
600
|
+
config.guards as GuardsSchema<GD> | undefined,
|
|
601
|
+
config.effects as EffectsSchema<EFD> | undefined,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ============================================================================
|
|
607
|
+
// make function (alias for Machine.make)
|
|
608
|
+
// ============================================================================
|
|
609
|
+
|
|
610
|
+
export const make = Machine.make;
|
|
611
|
+
|
|
612
|
+
// ============================================================================
|
|
613
|
+
// spawn function - simple actor creation without ActorSystem
|
|
614
|
+
// ============================================================================
|
|
615
|
+
|
|
616
|
+
import type { ActorRef } from "./actor.js";
|
|
617
|
+
import { createActor } from "./actor.js";
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Spawn an actor directly without ActorSystem ceremony.
|
|
621
|
+
*
|
|
622
|
+
* Use this for simple single-actor cases. For registry, persistence, or
|
|
623
|
+
* multi-actor coordination, use ActorSystemService instead.
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* ```ts
|
|
627
|
+
* const program = Effect.gen(function* () {
|
|
628
|
+
* const actor = yield* Machine.spawn(machine);
|
|
629
|
+
* yield* actor.send(Event.Start);
|
|
630
|
+
* yield* Effect.yieldNow();
|
|
631
|
+
* return yield* actor.snapshot;
|
|
632
|
+
* });
|
|
633
|
+
*
|
|
634
|
+
* Effect.runPromise(Effect.scoped(program));
|
|
635
|
+
* ```
|
|
636
|
+
*/
|
|
637
|
+
export const spawn: {
|
|
638
|
+
<
|
|
639
|
+
S extends { readonly _tag: string },
|
|
640
|
+
E extends { readonly _tag: string },
|
|
641
|
+
R,
|
|
642
|
+
GD extends GuardsDef,
|
|
643
|
+
EFD extends EffectsDef,
|
|
644
|
+
>(
|
|
645
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
646
|
+
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
647
|
+
): Effect.Effect<ActorRef<S, E>, never, R | Scope.Scope>;
|
|
648
|
+
|
|
649
|
+
<
|
|
650
|
+
S extends { readonly _tag: string },
|
|
651
|
+
E extends { readonly _tag: string },
|
|
652
|
+
R,
|
|
653
|
+
GD extends GuardsDef,
|
|
654
|
+
EFD extends EffectsDef,
|
|
655
|
+
>(
|
|
656
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
657
|
+
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
658
|
+
id: string,
|
|
659
|
+
): Effect.Effect<ActorRef<S, E>, never, R | Scope.Scope>;
|
|
660
|
+
} = <
|
|
661
|
+
S extends { readonly _tag: string },
|
|
662
|
+
E extends { readonly _tag: string },
|
|
663
|
+
R,
|
|
664
|
+
GD extends GuardsDef,
|
|
665
|
+
EFD extends EffectsDef,
|
|
666
|
+
>(
|
|
667
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
|
|
668
|
+
machine: Machine<S, E, R, any, any, GD, EFD>,
|
|
669
|
+
id?: string,
|
|
670
|
+
): Effect.Effect<ActorRef<S, E>, never, R | Scope.Scope> =>
|
|
671
|
+
Effect.gen(function* () {
|
|
672
|
+
const actorId = id ?? `actor-${Math.random().toString(36).slice(2)}`;
|
|
673
|
+
const actor = yield* createActor(actorId, machine);
|
|
674
|
+
|
|
675
|
+
// Register cleanup on scope finalization
|
|
676
|
+
yield* Effect.addFinalizer(() => actor.stop);
|
|
677
|
+
|
|
678
|
+
return actor;
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// Transition lookup (introspection)
|
|
682
|
+
export { findTransitions } from "./internal/transition.js";
|
|
683
|
+
|
|
684
|
+
// Persistence types
|
|
685
|
+
export type { PersistenceConfig, PersistentMachine } from "./persistence/index.js";
|