effect-machine 0.3.1 → 0.4.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.
Files changed (62) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/_rolldown/runtime.js +18 -0
  3. package/dist/actor.d.ts +256 -0
  4. package/dist/actor.js +402 -0
  5. package/dist/cluster/entity-machine.d.ts +90 -0
  6. package/dist/cluster/entity-machine.js +80 -0
  7. package/dist/cluster/index.d.ts +3 -0
  8. package/dist/cluster/index.js +4 -0
  9. package/dist/cluster/to-entity.d.ts +64 -0
  10. package/dist/cluster/to-entity.js +53 -0
  11. package/dist/errors.d.ts +61 -0
  12. package/dist/errors.js +38 -0
  13. package/dist/index.d.ts +13 -0
  14. package/dist/index.js +14 -0
  15. package/dist/inspection.d.ts +125 -0
  16. package/dist/inspection.js +50 -0
  17. package/dist/internal/brands.d.ts +40 -0
  18. package/dist/internal/brands.js +0 -0
  19. package/dist/internal/inspection.d.ts +11 -0
  20. package/dist/internal/inspection.js +15 -0
  21. package/dist/internal/transition.d.ts +160 -0
  22. package/dist/internal/transition.js +238 -0
  23. package/dist/internal/utils.d.ts +60 -0
  24. package/dist/internal/utils.js +46 -0
  25. package/dist/machine.d.ts +278 -0
  26. package/dist/machine.js +317 -0
  27. package/{src/persistence/adapter.ts → dist/persistence/adapter.d.ts} +40 -72
  28. package/dist/persistence/adapter.js +27 -0
  29. package/dist/persistence/adapters/in-memory.d.ts +32 -0
  30. package/dist/persistence/adapters/in-memory.js +176 -0
  31. package/dist/persistence/index.d.ts +5 -0
  32. package/dist/persistence/index.js +6 -0
  33. package/dist/persistence/persistent-actor.d.ts +50 -0
  34. package/dist/persistence/persistent-actor.js +358 -0
  35. package/{src/persistence/persistent-machine.ts → dist/persistence/persistent-machine.d.ts} +28 -54
  36. package/dist/persistence/persistent-machine.js +24 -0
  37. package/dist/schema.d.ts +141 -0
  38. package/dist/schema.js +165 -0
  39. package/dist/slot.d.ts +130 -0
  40. package/dist/slot.js +99 -0
  41. package/dist/testing.d.ts +142 -0
  42. package/dist/testing.js +138 -0
  43. package/package.json +28 -14
  44. package/src/actor.ts +0 -1058
  45. package/src/cluster/entity-machine.ts +0 -201
  46. package/src/cluster/index.ts +0 -43
  47. package/src/cluster/to-entity.ts +0 -99
  48. package/src/errors.ts +0 -64
  49. package/src/index.ts +0 -105
  50. package/src/inspection.ts +0 -178
  51. package/src/internal/brands.ts +0 -51
  52. package/src/internal/inspection.ts +0 -18
  53. package/src/internal/transition.ts +0 -489
  54. package/src/internal/utils.ts +0 -80
  55. package/src/machine.ts +0 -836
  56. package/src/persistence/adapters/in-memory.ts +0 -294
  57. package/src/persistence/index.ts +0 -24
  58. package/src/persistence/persistent-actor.ts +0 -791
  59. package/src/schema.ts +0 -362
  60. package/src/slot.ts +0 -281
  61. package/src/testing.ts +0 -284
  62. package/tsconfig.json +0 -65
package/dist/actor.js ADDED
@@ -0,0 +1,402 @@
1
+ import { Inspector } from "./inspection.js";
2
+ import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
3
+ import { DuplicateActorError } from "./errors.js";
4
+ import { isPersistentMachine } from "./persistence/persistent-machine.js";
5
+ import { processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
6
+ import { emitWithTimestamp } from "./internal/inspection.js";
7
+ import { PersistenceAdapterTag, PersistenceError } from "./persistence/adapter.js";
8
+ import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
9
+ import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, Queue, Ref, Runtime, Scope, SubscriptionRef } from "effect";
10
+
11
+ //#region src/actor.ts
12
+ /**
13
+ * ActorSystem service tag
14
+ */
15
+ const ActorSystem = Context.GenericTag("@effect/machine/ActorSystem");
16
+ /**
17
+ * Notify all listeners of state change.
18
+ */
19
+ const notifyListeners = (listeners, state) => {
20
+ for (const listener of listeners) try {
21
+ listener(state);
22
+ } catch {}
23
+ };
24
+ /**
25
+ * Build core ActorRef methods shared between regular and persistent actors.
26
+ */
27
+ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system) => {
28
+ const send = Effect.fn("effect-machine.actor.send")(function* (event) {
29
+ if (yield* Ref.get(stoppedRef)) return;
30
+ yield* Queue.offer(eventQueue, event);
31
+ });
32
+ const snapshot = SubscriptionRef.get(stateRef).pipe(Effect.withSpan("effect-machine.actor.snapshot"));
33
+ const matches = Effect.fn("effect-machine.actor.matches")(function* (tag) {
34
+ return (yield* SubscriptionRef.get(stateRef))._tag === tag;
35
+ });
36
+ const can = Effect.fn("effect-machine.actor.can")(function* (event) {
37
+ return resolveTransition(machine, yield* SubscriptionRef.get(stateRef), event) !== void 0;
38
+ });
39
+ const waitFor = Effect.fn("effect-machine.actor.waitFor")(function* (predicateOrState) {
40
+ const predicate = typeof predicateOrState === "function" && !("_tag" in predicateOrState) ? predicateOrState : (s) => s._tag === predicateOrState._tag;
41
+ const current = yield* SubscriptionRef.get(stateRef);
42
+ if (predicate(current)) return current;
43
+ const done = yield* Deferred.make();
44
+ const rt = yield* Effect.runtime();
45
+ const runFork = Runtime.runFork(rt);
46
+ const listener = (state) => {
47
+ if (predicate(state)) runFork(Deferred.succeed(done, state));
48
+ };
49
+ listeners.add(listener);
50
+ const afterSubscribe = yield* SubscriptionRef.get(stateRef);
51
+ if (predicate(afterSubscribe)) {
52
+ listeners.delete(listener);
53
+ return afterSubscribe;
54
+ }
55
+ const result = yield* Deferred.await(done);
56
+ listeners.delete(listener);
57
+ return result;
58
+ });
59
+ const awaitFinal = waitFor((state) => machine.finalStates.has(state._tag)).pipe(Effect.withSpan("effect-machine.actor.awaitFinal"));
60
+ const sendAndWait = Effect.fn("effect-machine.actor.sendAndWait")(function* (event, predicateOrState) {
61
+ yield* send(event);
62
+ if (predicateOrState !== void 0) return yield* waitFor(predicateOrState);
63
+ return yield* awaitFinal;
64
+ });
65
+ return {
66
+ id,
67
+ send,
68
+ state: stateRef,
69
+ stop,
70
+ stopSync: () => Effect.runFork(stop),
71
+ snapshot,
72
+ snapshotSync: () => Effect.runSync(SubscriptionRef.get(stateRef)),
73
+ matches,
74
+ matchesSync: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
75
+ can,
76
+ canSync: (event) => {
77
+ return resolveTransition(machine, Effect.runSync(SubscriptionRef.get(stateRef)), event) !== void 0;
78
+ },
79
+ changes: stateRef.changes,
80
+ waitFor,
81
+ awaitFinal,
82
+ sendAndWait,
83
+ sendSync: (event) => {
84
+ if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, event));
85
+ },
86
+ subscribe: (fn) => {
87
+ listeners.add(fn);
88
+ return () => {
89
+ listeners.delete(fn);
90
+ };
91
+ },
92
+ system
93
+ };
94
+ };
95
+ /**
96
+ * Create and start an actor for a machine
97
+ */
98
+ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine) {
99
+ yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
100
+ const existingSystem = yield* Effect.serviceOption(ActorSystem);
101
+ let system;
102
+ let implicitSystemScope;
103
+ if (Option.isSome(existingSystem)) system = existingSystem.value;
104
+ else {
105
+ const scope = yield* Scope.make();
106
+ system = yield* make().pipe(Effect.provideService(Scope.Scope, scope));
107
+ implicitSystemScope = scope;
108
+ }
109
+ const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
110
+ const eventQueue = yield* Queue.unbounded();
111
+ const stoppedRef = yield* Ref.make(false);
112
+ const self = {
113
+ send: Effect.fn("effect-machine.actor.self.send")(function* (event) {
114
+ if (yield* Ref.get(stoppedRef)) return;
115
+ yield* Queue.offer(eventQueue, event);
116
+ }),
117
+ spawn: (childId, childMachine) => system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system))
118
+ };
119
+ yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", machine.initial._tag);
120
+ yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
121
+ type: "@machine.spawn",
122
+ actorId: id,
123
+ initialState: machine.initial,
124
+ timestamp
125
+ }));
126
+ const stateRef = yield* SubscriptionRef.make(machine.initial);
127
+ const listeners = /* @__PURE__ */ new Set();
128
+ const backgroundFibers = [];
129
+ const initEvent = { _tag: INTERNAL_INIT_EVENT };
130
+ const ctx = {
131
+ state: machine.initial,
132
+ event: initEvent,
133
+ self,
134
+ system
135
+ };
136
+ const { effects: effectSlots } = machine._slots;
137
+ for (const bg of machine.backgroundEffects) {
138
+ const fiber = yield* Effect.forkDaemon(bg.handler({
139
+ state: machine.initial,
140
+ event: initEvent,
141
+ self,
142
+ effects: effectSlots,
143
+ system
144
+ }).pipe(Effect.provideService(machine.Context, ctx)));
145
+ backgroundFibers.push(fiber);
146
+ }
147
+ const stateScopeRef = { current: yield* Scope.make() };
148
+ yield* runSpawnEffectsWithInspection(machine, machine.initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
149
+ if (machine.finalStates.has(machine.initial._tag)) {
150
+ yield* Scope.close(stateScopeRef.current, Exit.void);
151
+ yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
152
+ yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
153
+ type: "@machine.stop",
154
+ actorId: id,
155
+ finalState: machine.initial,
156
+ timestamp
157
+ }));
158
+ yield* Ref.set(stoppedRef, true);
159
+ if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
160
+ return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system);
161
+ }
162
+ const loopFiber = yield* Effect.forkDaemon(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system));
163
+ return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
164
+ const finalState = yield* SubscriptionRef.get(stateRef);
165
+ yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
166
+ type: "@machine.stop",
167
+ actorId: id,
168
+ finalState,
169
+ timestamp
170
+ }));
171
+ yield* Ref.set(stoppedRef, true);
172
+ yield* Fiber.interrupt(loopFiber);
173
+ yield* Scope.close(stateScopeRef.current, Exit.void);
174
+ yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
175
+ if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
176
+ }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system);
177
+ });
178
+ /**
179
+ * Main event loop for the actor
180
+ */
181
+ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system) {
182
+ while (true) {
183
+ const event = yield* Queue.take(eventQueue);
184
+ const currentState = yield* SubscriptionRef.get(stateRef);
185
+ if (yield* Effect.withSpan("effect-machine.event.process", { attributes: {
186
+ "effect_machine.actor.id": actorId,
187
+ "effect_machine.state.current": currentState._tag,
188
+ "effect_machine.event.type": event._tag
189
+ } })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system))) {
190
+ yield* Ref.set(stoppedRef, true);
191
+ yield* Scope.close(stateScopeRef.current, Exit.void);
192
+ yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
193
+ return;
194
+ }
195
+ }
196
+ });
197
+ /**
198
+ * Process a single event, returning true if the actor should stop.
199
+ * Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
200
+ */
201
+ const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system) {
202
+ yield* emitWithTimestamp(inspector, (timestamp) => ({
203
+ type: "@machine.event",
204
+ actorId,
205
+ state: currentState,
206
+ event,
207
+ timestamp
208
+ }));
209
+ const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, inspector === void 0 ? void 0 : {
210
+ onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
211
+ type: "@machine.effect",
212
+ actorId,
213
+ effectType: "spawn",
214
+ state,
215
+ timestamp
216
+ })),
217
+ onTransition: (from, to, ev) => emitWithTimestamp(inspector, (timestamp) => ({
218
+ type: "@machine.transition",
219
+ actorId,
220
+ fromState: from,
221
+ toState: to,
222
+ event: ev,
223
+ timestamp
224
+ })),
225
+ onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
226
+ type: "@machine.error",
227
+ actorId,
228
+ phase: info.phase,
229
+ state: info.state,
230
+ event: info.event,
231
+ error: Cause.pretty(info.cause),
232
+ timestamp
233
+ }))
234
+ });
235
+ if (!result.transitioned) {
236
+ yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
237
+ return false;
238
+ }
239
+ yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
240
+ yield* SubscriptionRef.set(stateRef, result.newState);
241
+ notifyListeners(listeners, result.newState);
242
+ if (result.lifecycleRan) {
243
+ yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
244
+ yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
245
+ if (result.isFinal) {
246
+ yield* emitWithTimestamp(inspector, (timestamp) => ({
247
+ type: "@machine.stop",
248
+ actorId,
249
+ finalState: result.newState,
250
+ timestamp
251
+ }));
252
+ return true;
253
+ }
254
+ }
255
+ return false;
256
+ });
257
+ /**
258
+ * Run spawn effects with actor-specific inspection and tracing.
259
+ * Wraps the core runSpawnEffects with inspection events and spans.
260
+ * @internal
261
+ */
262
+ const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffects")(function* (machine, state, event, self, stateScope, actorId, inspector, system) {
263
+ yield* emitWithTimestamp(inspector, (timestamp) => ({
264
+ type: "@machine.effect",
265
+ actorId,
266
+ effectType: "spawn",
267
+ state,
268
+ timestamp
269
+ }));
270
+ yield* runSpawnEffects(machine, state, event, self, stateScope, system, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
271
+ type: "@machine.error",
272
+ actorId,
273
+ phase: info.phase,
274
+ state: info.state,
275
+ event: info.event,
276
+ error: Cause.pretty(info.cause),
277
+ timestamp
278
+ })));
279
+ });
280
+ /**
281
+ * Internal implementation
282
+ */
283
+ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
284
+ const actors = MutableHashMap.empty();
285
+ const withSpawnGate = (yield* Effect.makeSemaphore(1)).withPermits(1);
286
+ yield* Effect.addFinalizer(() => {
287
+ const stops = [];
288
+ MutableHashMap.forEach(actors, (actor) => {
289
+ stops.push(actor.stop);
290
+ });
291
+ return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.asVoid);
292
+ });
293
+ /** Check for duplicate ID, register actor, attach scope cleanup if available */
294
+ const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* (id, actor) {
295
+ if (MutableHashMap.has(actors, id)) {
296
+ yield* actor.stop;
297
+ return yield* new DuplicateActorError({ actorId: id });
298
+ }
299
+ MutableHashMap.set(actors, id, actor);
300
+ const maybeScope = yield* Effect.serviceOption(Scope.Scope);
301
+ if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.gen(function* () {
302
+ yield* actor.stop;
303
+ MutableHashMap.remove(actors, id);
304
+ }));
305
+ return actor;
306
+ });
307
+ const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, built) {
308
+ if (MutableHashMap.has(actors, id)) return yield* new DuplicateActorError({ actorId: id });
309
+ return yield* registerActor(id, yield* createActor(id, built._inner));
310
+ });
311
+ const spawnPersistent = Effect.fn("effect-machine.actorSystem.spawnPersistent")(function* (id, persistentMachine) {
312
+ if (MutableHashMap.has(actors, id)) return yield* new DuplicateActorError({ actorId: id });
313
+ const adapter = yield* PersistenceAdapterTag;
314
+ const maybeSnapshot = yield* adapter.loadSnapshot(id, persistentMachine.persistence.stateSchema);
315
+ return yield* registerActor(id, yield* createPersistentActor(id, persistentMachine, maybeSnapshot, yield* adapter.loadEvents(id, persistentMachine.persistence.eventSchema, Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : void 0)));
316
+ });
317
+ const spawnImpl = Effect.fn("effect-machine.actorSystem.spawn")(function* (id, machine) {
318
+ if (isPersistentMachine(machine)) return yield* spawnPersistent(id, machine);
319
+ return yield* spawnRegular(id, machine);
320
+ });
321
+ function spawn(id, machine) {
322
+ return withSpawnGate(spawnImpl(id, machine));
323
+ }
324
+ const restoreImpl = Effect.fn("effect-machine.actorSystem.restore")(function* (id, persistentMachine) {
325
+ const maybeActor = yield* restorePersistentActor(id, persistentMachine);
326
+ if (Option.isSome(maybeActor)) yield* registerActor(id, maybeActor.value);
327
+ return maybeActor;
328
+ });
329
+ const restore = (id, persistentMachine) => withSpawnGate(restoreImpl(id, persistentMachine));
330
+ const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
331
+ return yield* Effect.sync(() => MutableHashMap.get(actors, id));
332
+ });
333
+ const stop = Effect.fn("effect-machine.actorSystem.stop")(function* (id) {
334
+ const maybeActor = MutableHashMap.get(actors, id);
335
+ if (Option.isNone(maybeActor)) return false;
336
+ yield* maybeActor.value.stop;
337
+ MutableHashMap.remove(actors, id);
338
+ return true;
339
+ });
340
+ const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
341
+ const adapter = yield* PersistenceAdapterTag;
342
+ if (adapter.listActors === void 0) return [];
343
+ return yield* adapter.listActors();
344
+ });
345
+ const restoreMany = Effect.fn("effect-machine.actorSystem.restoreMany")(function* (ids, persistentMachine) {
346
+ const restored = [];
347
+ const failed = [];
348
+ for (const id of ids) {
349
+ if (MutableHashMap.has(actors, id)) continue;
350
+ const result = yield* Effect.either(restore(id, persistentMachine));
351
+ if (result._tag === "Left") failed.push({
352
+ id,
353
+ error: result.left
354
+ });
355
+ else if (Option.isSome(result.right)) restored.push(result.right.value);
356
+ else failed.push({
357
+ id,
358
+ error: new PersistenceError({
359
+ operation: "restore",
360
+ actorId: id,
361
+ message: "No persisted state found"
362
+ })
363
+ });
364
+ }
365
+ return {
366
+ restored,
367
+ failed
368
+ };
369
+ });
370
+ const restoreAll = Effect.fn("effect-machine.actorSystem.restoreAll")(function* (persistentMachine, options) {
371
+ const adapter = yield* PersistenceAdapterTag;
372
+ if (adapter.listActors === void 0) return {
373
+ restored: [],
374
+ failed: []
375
+ };
376
+ const machineType = persistentMachine.persistence.machineType;
377
+ if (machineType === void 0) return yield* new PersistenceError({
378
+ operation: "restoreAll",
379
+ actorId: "*",
380
+ message: "restoreAll requires explicit machineType in persistence config"
381
+ });
382
+ let filtered = (yield* adapter.listActors()).filter((meta) => meta.machineType === machineType);
383
+ if (options?.filter !== void 0) filtered = filtered.filter(options.filter);
384
+ return yield* restoreMany(filtered.map((meta) => meta.id), persistentMachine);
385
+ });
386
+ return ActorSystem.of({
387
+ spawn,
388
+ restore,
389
+ get,
390
+ stop,
391
+ listPersisted,
392
+ restoreMany,
393
+ restoreAll
394
+ });
395
+ });
396
+ /**
397
+ * Default ActorSystem layer
398
+ */
399
+ const Default = Layer.scoped(ActorSystem, make());
400
+
401
+ //#endregion
402
+ export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects };
@@ -0,0 +1,90 @@
1
+ import { EffectsDef, GuardsDef } from "../slot.js";
2
+ import { ProcessEventHooks } from "../internal/transition.js";
3
+ import { Machine } from "../machine.js";
4
+ import "../actor.js";
5
+ import { Layer } from "effect";
6
+ import { Entity } from "@effect/cluster";
7
+ import { Rpc } from "@effect/rpc";
8
+
9
+ //#region src/cluster/entity-machine.d.ts
10
+ /**
11
+ * Options for EntityMachine.layer
12
+ */
13
+ interface EntityMachineOptions<S, E> {
14
+ /**
15
+ * Initialize state from entity ID.
16
+ * Called once when entity is first activated.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * EntityMachine.layer(OrderEntity, orderMachine, {
21
+ * initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
22
+ * })
23
+ * ```
24
+ */
25
+ readonly initializeState?: (entityId: string) => S;
26
+ /**
27
+ * Optional hooks for inspection/tracing.
28
+ * Called at specific points during event processing.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * EntityMachine.layer(OrderEntity, orderMachine, {
33
+ * hooks: {
34
+ * onTransition: (from, to, event) =>
35
+ * Effect.log(`Transition: ${from._tag} -> ${to._tag}`),
36
+ * onSpawnEffect: (state) =>
37
+ * Effect.log(`Running spawn effects for ${state._tag}`),
38
+ * onError: ({ phase, state }) =>
39
+ * Effect.log(`Defect in ${phase} at ${state._tag}`),
40
+ * },
41
+ * })
42
+ * ```
43
+ */
44
+ readonly hooks?: ProcessEventHooks<S, E>;
45
+ }
46
+ /**
47
+ * Create an Entity layer that wires a machine to handle RPC calls.
48
+ *
49
+ * The layer:
50
+ * - Maintains state via Ref per entity instance
51
+ * - Resolves transitions using the indexed lookup
52
+ * - Evaluates guards in registration order
53
+ * - Runs lifecycle effects (onEnter/spawn)
54
+ * - Processes internal events from spawn effects
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const OrderEntity = toEntity(orderMachine, {
59
+ * type: "Order",
60
+ * stateSchema: OrderState,
61
+ * eventSchema: OrderEvent,
62
+ * })
63
+ *
64
+ * const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine, {
65
+ * initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
66
+ * })
67
+ *
68
+ * // Use in cluster
69
+ * const program = Effect.gen(function* () {
70
+ * const client = yield* ShardingClient.client(OrderEntity)
71
+ * yield* client.Send("order-123", { event: OrderEvent.Ship({ trackingId: "abc" }) })
72
+ * })
73
+ * ```
74
+ */
75
+ declare const EntityMachine: {
76
+ /**
77
+ * Create a layer that wires a machine to an Entity.
78
+ *
79
+ * @param entity - Entity created via toEntity()
80
+ * @param machine - Machine with all effects provided
81
+ * @param options - Optional configuration (state initializer, inspection hooks)
82
+ */
83
+ layer: <S extends {
84
+ readonly _tag: string;
85
+ }, E extends {
86
+ readonly _tag: string;
87
+ }, R, GD extends GuardsDef, EFD extends EffectsDef, EntityType extends string, Rpcs extends Rpc.Any>(entity: Entity.Entity<EntityType, Rpcs>, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, options?: EntityMachineOptions<S, E>) => Layer.Layer<never, never, R>;
88
+ };
89
+ //#endregion
90
+ export { EntityMachine, EntityMachineOptions };
@@ -0,0 +1,80 @@
1
+ import { processEventCore, runSpawnEffects } from "../internal/transition.js";
2
+ import { ActorSystem } from "../actor.js";
3
+ import { Effect, Option, Queue, Ref, Scope } from "effect";
4
+ import { Entity } from "@effect/cluster";
5
+
6
+ //#region src/cluster/entity-machine.ts
7
+ /**
8
+ * EntityMachine adapter - wires a machine to a cluster Entity layer.
9
+ *
10
+ * @module
11
+ */
12
+ /**
13
+ * Process a single event through the machine using shared core.
14
+ * Returns the new state after processing.
15
+ */
16
+ const processEvent = Effect.fn("effect-machine.cluster.processEvent")(function* (machine, stateRef, event, self, stateScopeRef, system, hooks) {
17
+ const result = yield* processEventCore(machine, yield* Ref.get(stateRef), event, self, stateScopeRef, system, hooks);
18
+ if (result.transitioned) yield* Ref.set(stateRef, result.newState);
19
+ return result.newState;
20
+ });
21
+ /**
22
+ * Create an Entity layer that wires a machine to handle RPC calls.
23
+ *
24
+ * The layer:
25
+ * - Maintains state via Ref per entity instance
26
+ * - Resolves transitions using the indexed lookup
27
+ * - Evaluates guards in registration order
28
+ * - Runs lifecycle effects (onEnter/spawn)
29
+ * - Processes internal events from spawn effects
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const OrderEntity = toEntity(orderMachine, {
34
+ * type: "Order",
35
+ * stateSchema: OrderState,
36
+ * eventSchema: OrderEvent,
37
+ * })
38
+ *
39
+ * const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine, {
40
+ * initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
41
+ * })
42
+ *
43
+ * // Use in cluster
44
+ * const program = Effect.gen(function* () {
45
+ * const client = yield* ShardingClient.client(OrderEntity)
46
+ * yield* client.Send("order-123", { event: OrderEvent.Ship({ trackingId: "abc" }) })
47
+ * })
48
+ * ```
49
+ */
50
+ const EntityMachine = { layer: (entity, machine, options) => {
51
+ const layer = Effect.fn("effect-machine.cluster.layer")(function* () {
52
+ const entityId = yield* Effect.serviceOption(Entity.CurrentAddress).pipe(Effect.map((opt) => opt._tag === "Some" ? opt.value.entityId : ""));
53
+ const initialState = options?.initializeState !== void 0 ? options.initializeState(entityId) : machine.initial;
54
+ const existingSystem = yield* Effect.serviceOption(ActorSystem);
55
+ if (Option.isNone(existingSystem)) return yield* Effect.die("EntityMachine requires ActorSystem in context");
56
+ const system = existingSystem.value;
57
+ const internalQueue = yield* Queue.unbounded();
58
+ const self = {
59
+ send: Effect.fn("effect-machine.cluster.self.send")(function* (event) {
60
+ yield* Queue.offer(internalQueue, event);
61
+ }),
62
+ spawn: (childId, childMachine) => system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system))
63
+ };
64
+ const stateRef = yield* Ref.make(initialState);
65
+ const stateScopeRef = { current: yield* Scope.make() };
66
+ yield* runSpawnEffects(machine, initialState, { _tag: "$init" }, self, stateScopeRef.current, system, options?.hooks?.onError);
67
+ const runInternalEvent = Effect.fn("effect-machine.cluster.internalEvent")(function* () {
68
+ yield* processEvent(machine, stateRef, yield* Queue.take(internalQueue), self, stateScopeRef, system, options?.hooks);
69
+ });
70
+ yield* Effect.forkScoped(Effect.forever(runInternalEvent()));
71
+ return entity.of({
72
+ Send: (envelope) => processEvent(machine, stateRef, envelope.payload.event, self, stateScopeRef, system, options?.hooks),
73
+ GetState: () => Ref.get(stateRef)
74
+ });
75
+ });
76
+ return entity.toLayer(layer());
77
+ } };
78
+
79
+ //#endregion
80
+ export { EntityMachine };
@@ -0,0 +1,3 @@
1
+ import { EntityMachine, EntityMachineOptions } from "./entity-machine.js";
2
+ import { ToEntityOptions, toEntity } from "./to-entity.js";
3
+ export { EntityMachine, type EntityMachineOptions, type ToEntityOptions, toEntity };
@@ -0,0 +1,4 @@
1
+ import { EntityMachine } from "./entity-machine.js";
2
+ import { toEntity } from "./to-entity.js";
3
+
4
+ export { EntityMachine, toEntity };
@@ -0,0 +1,64 @@
1
+ import { Machine } from "../machine.js";
2
+ import { Schema } from "effect";
3
+ import { Entity } from "@effect/cluster";
4
+ import { Rpc } from "@effect/rpc";
5
+
6
+ //#region src/cluster/to-entity.d.ts
7
+ /**
8
+ * Options for toEntity.
9
+ */
10
+ interface ToEntityOptions {
11
+ /**
12
+ * Entity type name (e.g., "Order", "User")
13
+ */
14
+ readonly type: string;
15
+ }
16
+ /**
17
+ * Default RPC protocol for entity machines.
18
+ *
19
+ * - `Send` - Send event to machine, returns new state
20
+ * - `GetState` - Get current state
21
+ */
22
+ type EntityRpcs<StateSchema extends Schema.Schema.Any, EventSchema extends Schema.Schema.Any> = readonly [Rpc.Rpc<"Send", Schema.Struct<{
23
+ readonly event: EventSchema;
24
+ }>, StateSchema, typeof Schema.Never, never>, Rpc.Rpc<"GetState", typeof Schema.Void, StateSchema, typeof Schema.Never, never>];
25
+ /**
26
+ * Generate an Entity definition from a machine.
27
+ *
28
+ * Creates an Entity with a standard RPC protocol:
29
+ * - `Send(event)` - Process event through machine, returns new state
30
+ * - `GetState()` - Returns current state
31
+ *
32
+ * Schemas are read from the machine - must use `Machine.make({ state, event, initial })`.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const OrderState = State({
37
+ * Pending: { orderId: Schema.String },
38
+ * Shipped: { trackingId: Schema.String },
39
+ * })
40
+ *
41
+ * const OrderEvent = Event({
42
+ * Ship: { trackingId: Schema.String },
43
+ * })
44
+ *
45
+ * const orderMachine = Machine.make({
46
+ * state: OrderState,
47
+ * event: OrderEvent,
48
+ * initial: OrderState.Pending({ orderId: "" }),
49
+ * }).pipe(
50
+ * Machine.on(OrderState.Pending, OrderEvent.Ship, ...),
51
+ * )
52
+ *
53
+ * const OrderEntity = toEntity(orderMachine, { type: "Order" })
54
+ * ```
55
+ */
56
+ declare const toEntity: <S extends {
57
+ readonly _tag: string;
58
+ }, E extends {
59
+ readonly _tag: string;
60
+ }, R>(machine: Machine<S, E, R, any, any, any, any>, options: ToEntityOptions) => Entity.Entity<string, Rpc.Rpc<"Send", Schema.Struct<{
61
+ event: Schema.Schema<E, unknown, never>;
62
+ }>, Schema.Schema<S, unknown, never>, typeof Schema.Never, never> | Rpc.Rpc<"GetState", typeof Schema.Void, Schema.Schema<S, unknown, never>, typeof Schema.Never, never>>;
63
+ //#endregion
64
+ export { EntityRpcs, ToEntityOptions, toEntity };