effect-machine 0.10.0 → 0.12.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 (81) hide show
  1. package/README.md +65 -62
  2. package/dist/actor.d.ts +35 -97
  3. package/dist/actor.js +69 -98
  4. package/dist/cluster/adapters/in-memory.d.ts +28 -0
  5. package/dist/cluster/adapters/in-memory.js +79 -0
  6. package/dist/cluster/entity-actor-ref.d.ts +56 -0
  7. package/dist/cluster/entity-actor-ref.js +33 -0
  8. package/dist/cluster/entity-machine.d.ts +31 -49
  9. package/dist/cluster/entity-machine.js +167 -52
  10. package/dist/cluster/index.d.ts +5 -2
  11. package/dist/cluster/index.js +4 -1
  12. package/dist/cluster/persistence.d.ts +49 -0
  13. package/dist/cluster/persistence.js +18 -0
  14. package/dist/cluster/to-entity.d.ts +9 -3
  15. package/dist/cluster/to-entity.js +16 -4
  16. package/dist/errors.d.ts +12 -1
  17. package/dist/errors.js +8 -1
  18. package/dist/index.d.ts +5 -8
  19. package/dist/index.js +1 -6
  20. package/dist/internal/brands.d.ts +14 -1
  21. package/dist/internal/runtime.d.ts +67 -0
  22. package/dist/internal/runtime.js +248 -0
  23. package/dist/internal/transition.d.ts +6 -1
  24. package/dist/internal/transition.js +16 -4
  25. package/dist/internal/utils.d.ts +42 -6
  26. package/dist/internal/utils.js +28 -6
  27. package/dist/machine.d.ts +55 -46
  28. package/dist/machine.js +74 -13
  29. package/dist/schema.d.ts +35 -34
  30. package/dist/schema.js +32 -3
  31. package/dist/testing.js +4 -2
  32. package/package.json +4 -4
  33. package/v3/dist/actor.d.ts +29 -96
  34. package/v3/dist/actor.js +52 -97
  35. package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
  36. package/v3/dist/cluster/adapters/in-memory.js +62 -0
  37. package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
  38. package/v3/dist/cluster/entity-actor-ref.js +19 -0
  39. package/v3/dist/cluster/entity-machine.d.ts +34 -49
  40. package/v3/dist/cluster/entity-machine.js +134 -50
  41. package/v3/dist/cluster/index.d.ts +5 -2
  42. package/v3/dist/cluster/index.js +4 -1
  43. package/v3/dist/cluster/persistence.d.ts +48 -0
  44. package/v3/dist/cluster/persistence.js +14 -0
  45. package/v3/dist/cluster/to-entity.d.ts +5 -2
  46. package/v3/dist/cluster/to-entity.js +12 -4
  47. package/v3/dist/errors.d.ts +16 -1
  48. package/v3/dist/errors.js +8 -1
  49. package/v3/dist/index.d.ts +5 -8
  50. package/v3/dist/index.js +1 -6
  51. package/v3/dist/internal/brands.d.ts +15 -1
  52. package/v3/dist/internal/runtime.d.ts +65 -0
  53. package/v3/dist/internal/runtime.js +236 -0
  54. package/v3/dist/internal/transition.d.ts +5 -0
  55. package/v3/dist/internal/transition.js +15 -3
  56. package/v3/dist/internal/utils.d.ts +42 -6
  57. package/v3/dist/internal/utils.js +28 -6
  58. package/v3/dist/machine.d.ts +48 -46
  59. package/v3/dist/machine.js +71 -13
  60. package/v3/dist/schema.d.ts +35 -34
  61. package/v3/dist/schema.js +29 -3
  62. package/dist/persistence/adapter.d.ts +0 -135
  63. package/dist/persistence/adapter.js +0 -25
  64. package/dist/persistence/adapters/in-memory.d.ts +0 -32
  65. package/dist/persistence/adapters/in-memory.js +0 -174
  66. package/dist/persistence/index.d.ts +0 -5
  67. package/dist/persistence/index.js +0 -5
  68. package/dist/persistence/persistent-actor.d.ts +0 -50
  69. package/dist/persistence/persistent-actor.js +0 -404
  70. package/dist/persistence/persistent-machine.d.ts +0 -105
  71. package/dist/persistence/persistent-machine.js +0 -22
  72. package/v3/dist/persistence/adapter.d.ts +0 -138
  73. package/v3/dist/persistence/adapter.js +0 -25
  74. package/v3/dist/persistence/adapters/in-memory.d.ts +0 -32
  75. package/v3/dist/persistence/adapters/in-memory.js +0 -174
  76. package/v3/dist/persistence/index.d.ts +0 -5
  77. package/v3/dist/persistence/index.js +0 -5
  78. package/v3/dist/persistence/persistent-actor.d.ts +0 -50
  79. package/v3/dist/persistence/persistent-actor.js +0 -404
  80. package/v3/dist/persistence/persistent-machine.d.ts +0 -105
  81. package/v3/dist/persistence/persistent-machine.js +0 -22
package/dist/machine.js CHANGED
@@ -1,19 +1,21 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
2
  import { Inspector } from "./inspection.js";
3
- import { getTag } from "./internal/utils.js";
3
+ import { getTag, makeDeferReply, makeReply, stubSystem } from "./internal/utils.js";
4
4
  import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
5
- import { persist } from "./persistence/persistent-machine.js";
6
5
  import { emitWithTimestamp } from "./internal/inspection.js";
7
6
  import { MachineContextTag } from "./slot.js";
8
- import { findTransitions, invalidateIndex } from "./internal/transition.js";
7
+ import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
9
8
  import { createActor } from "./actor.js";
10
9
  import { Cause, Effect, Exit, Option, Scope } from "effect";
11
10
  //#region src/machine.ts
12
11
  var machine_exports = /* @__PURE__ */ __exportAll({
13
12
  BuiltMachine: () => BuiltMachine,
14
13
  Machine: () => Machine,
14
+ deferReply: () => deferReply,
15
15
  findTransitions: () => findTransitions,
16
16
  make: () => make,
17
+ replay: () => replay,
18
+ reply: () => reply,
17
19
  spawn: () => spawn
18
20
  });
19
21
  const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOption(Inspector), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
@@ -42,9 +44,6 @@ var BuiltMachine = class {
42
44
  get initial() {
43
45
  return this._inner.initial;
44
46
  }
45
- persist(config) {
46
- return this._inner.persist(config);
47
- }
48
47
  };
49
48
  /**
50
49
  * Machine definition with fluent builder API.
@@ -72,6 +71,7 @@ var Machine = class Machine {
72
71
  /** @internal */ _slots;
73
72
  stateSchema;
74
73
  eventSchema;
74
+ /** @internal */ _replySchemas;
75
75
  /**
76
76
  * Context tag for accessing machine state/event/self in slot handlers.
77
77
  * Uses shared module-level tag for all machines.
@@ -98,6 +98,9 @@ var Machine = class Machine {
98
98
  get effectsSchema() {
99
99
  return this._effectsSchema;
100
100
  }
101
+ get replySchemas() {
102
+ return this._replySchemas;
103
+ }
101
104
  /** @internal */
102
105
  constructor(initial, stateSchema, eventSchema, guardsSchema, effectsSchema) {
103
106
  this.initial = initial;
@@ -108,6 +111,7 @@ var Machine = class Machine {
108
111
  this._postponeRules = [];
109
112
  this._guardsSchema = guardsSchema;
110
113
  this._effectsSchema = effectsSchema;
114
+ this._replySchemas = eventSchema?._replySchemas ?? /* @__PURE__ */ new Map();
111
115
  this._guardHandlers = /* @__PURE__ */ new Map();
112
116
  this._effectHandlers = /* @__PURE__ */ new Map();
113
117
  this.stateSchema = stateSchema;
@@ -378,6 +382,7 @@ var Machine = class Machine {
378
382
  result._spawnEffects = [...this._spawnEffects];
379
383
  result._backgroundEffects = [...this._backgroundEffects];
380
384
  result._postponeRules = [...this._postponeRules];
385
+ result._replySchemas = this._replySchemas;
381
386
  const anyHandlers = handlers;
382
387
  if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
383
388
  if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
@@ -385,10 +390,6 @@ var Machine = class Machine {
385
390
  }
386
391
  return new BuiltMachine(this);
387
392
  }
388
- /** @internal Persist from raw Machine — prefer BuiltMachine.persist() */
389
- persist(config) {
390
- return persist(config)(this);
391
- }
392
393
  static make(config) {
393
394
  return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
394
395
  }
@@ -408,11 +409,71 @@ var TransitionScope = class {
408
409
  }
409
410
  };
410
411
  const make = Machine.make;
411
- const spawn = Effect.fn("effect-machine.spawn")(function* (built, id) {
412
- const actor = yield* createActor(id ?? `actor-${Math.random().toString(36).slice(2)}`, built._inner);
412
+ /**
413
+ * Spawn an actor from a built machine.
414
+ *
415
+ * Options:
416
+ * - `id` — custom actor ID (default: random)
417
+ * - `hydrate` — restore from a previously-saved state snapshot.
418
+ * The actor starts in the hydrated state and re-runs spawn effects
419
+ * for that state (timers, scoped resources, etc.). Transition history
420
+ * is not replayed — only the current state's entry effects run.
421
+ *
422
+ * Persistence is composed in userland by observing `actor.changes`
423
+ * and saving snapshots to your own storage.
424
+ */
425
+ const spawn = Effect.fn("effect-machine.spawn")(function* (built, idOrOptions) {
426
+ const opts = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
427
+ const actor = yield* createActor(opts?.id ?? `actor-${Math.random().toString(36).slice(2)}`, built._inner, { initialState: opts?.hydrate });
413
428
  const maybeScope = yield* Effect.serviceOption(Scope.Scope);
414
429
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
415
430
  return actor;
416
431
  });
432
+ const replay = Effect.fn("effect-machine.replay")(function* (built, events, options) {
433
+ const machine = built._inner;
434
+ let state = options?.from ?? machine.initial;
435
+ const hasPostponeRules = machine.postponeRules.length > 0;
436
+ const postponed = [];
437
+ const dummySend = Effect.fn("effect-machine.replay.send")((_event) => Effect.void);
438
+ const self = {
439
+ send: dummySend,
440
+ cast: dummySend,
441
+ spawn: () => Effect.die("spawn not supported in replay"),
442
+ reply: () => Effect.succeed(false)
443
+ };
444
+ for (const event of events) {
445
+ if (machine.finalStates.has(state._tag)) break;
446
+ if (hasPostponeRules && shouldPostpone(machine, state._tag, event._tag)) {
447
+ postponed.push(event);
448
+ continue;
449
+ }
450
+ const transition = resolveTransition(machine, state, event);
451
+ if (transition !== void 0) {
452
+ const result = yield* runTransitionHandler(machine, transition, state, event, self, stubSystem, "replay");
453
+ const previousTag = state._tag;
454
+ state = result.newState;
455
+ if ((state._tag !== previousTag || transition.reenter === true) && postponed.length > 0) {
456
+ let drainTag = previousTag;
457
+ while (state._tag !== drainTag && postponed.length > 0) {
458
+ if (machine.finalStates.has(state._tag)) break;
459
+ drainTag = state._tag;
460
+ const drained = postponed.splice(0);
461
+ for (const postponedEvent of drained) {
462
+ if (machine.finalStates.has(state._tag)) break;
463
+ if (shouldPostpone(machine, state._tag, postponedEvent._tag)) {
464
+ postponed.push(postponedEvent);
465
+ continue;
466
+ }
467
+ const pTransition = resolveTransition(machine, state, postponedEvent);
468
+ if (pTransition !== void 0) state = (yield* runTransitionHandler(machine, pTransition, state, postponedEvent, self, stubSystem, "replay")).newState;
469
+ }
470
+ }
471
+ }
472
+ }
473
+ }
474
+ return state;
475
+ });
476
+ const reply = makeReply;
477
+ const deferReply = makeDeferReply;
417
478
  //#endregion
418
- export { BuiltMachine, Machine, findTransitions, machine_exports, make, spawn };
479
+ export { BuiltMachine, Machine, deferReply, findTransitions, machine_exports, make, replay, reply, spawn };
package/dist/schema.d.ts CHANGED
@@ -1,7 +1,17 @@
1
- import { FullEventBrand, FullStateBrand } from "./internal/brands.js";
1
+ import { FullEventBrand, FullStateBrand, ReplyTypeBrand } from "./internal/brands.js";
2
2
  import { Schema } from "effect";
3
3
 
4
4
  //#region src/schema.d.ts
5
+ declare const ReplySchemaSymbol: unique symbol;
6
+ type ReplySchemaSymbol = typeof ReplySchemaSymbol;
7
+ /**
8
+ * Fields annotated with a reply schema.
9
+ * Structurally identical to Schema.Struct.Fields at runtime,
10
+ * but carries the reply schema type at compile time.
11
+ */
12
+ type ReplyFields<F extends Schema.Struct.Fields, RS extends Schema.Schema<unknown>> = F & {
13
+ readonly [ReplySchemaSymbol]: RS;
14
+ };
5
15
  /**
6
16
  * Extract the TypeScript type from a TaggedStruct schema
7
17
  */
@@ -12,20 +22,23 @@ type TaggedStructType<Tag extends string, Fields extends Schema.Struct.Fields> =
12
22
  type VariantSchemas<D extends Record<string, Schema.Struct.Fields>> = { readonly [K in keyof D & string]: Schema.TaggedStruct<K, D[K]> };
13
23
  /**
14
24
  * Build union type from variant schemas.
15
- * Used for constraining fluent method type params.
25
+ * Reply-bearing variants carry ReplyTypeBrand<R> for ask() inference.
16
26
  */
17
- type VariantsUnion<D extends Record<string, Schema.Struct.Fields>> = { [K in keyof D & string]: TaggedStructType<K, D[K]> }[keyof D & string];
27
+ type VariantsUnion<D extends Record<string, Schema.Struct.Fields>> = { [K in keyof D & string]: TaggedStructType<K, D[K]> & (D[K] extends {
28
+ readonly [ReplySchemaSymbol]: Schema.Schema<infer R>;
29
+ } ? ReplyTypeBrand<R> : unknown) }[keyof D & string];
18
30
  /**
19
- * Check if fields are empty (no required properties)
31
+ * Check if fields are empty (no required string properties).
32
+ * Symbol keys (like ReplySchemaSymbol) are metadata, not payload fields.
20
33
  */
21
- type IsEmptyFields<Fields extends Schema.Struct.Fields> = keyof Fields extends never ? true : false;
34
+ type IsEmptyFields<Fields extends Schema.Struct.Fields> = string & keyof Fields extends never ? true : false;
22
35
  /**
23
- * Constructor functions for each variant.
24
- * Empty structs: plain values with `_tag`: `State.Idle`
25
- * Non-empty structs require args: `State.Loading({ url })`
26
- *
27
- * Each variant also has a `derive` method for constructing from a source object.
36
+ * Resolve the reply brand for a variant's fields.
37
+ * If fields carry ReplySchemaSymbol, adds ReplyTypeBrand<R>.
28
38
  */
39
+ type VariantReplyBrand<Fields extends Schema.Struct.Fields> = Fields extends {
40
+ readonly [ReplySchemaSymbol]: Schema.Schema<infer R>;
41
+ } ? ReplyTypeBrand<R> : unknown;
29
42
  /**
30
43
  * Constructor functions for each variant.
31
44
  * Empty structs: plain values with `_tag`: `State.Idle`
@@ -33,10 +46,11 @@ type IsEmptyFields<Fields extends Schema.Struct.Fields> = keyof Fields extends n
33
46
  *
34
47
  * Each variant also has a `derive` method for constructing from a source object.
35
48
  * The source type uses `object` to accept branded state types without index signature issues.
49
+ * Reply-bearing variants carry ReplyTypeBrand<R> for ask() type inference.
36
50
  */
37
- type VariantConstructors<D extends Record<string, Schema.Struct.Fields>, Brand> = { readonly [K in keyof D & string]: IsEmptyFields<D[K]> extends true ? TaggedStructType<K, D[K]> & Brand & {
51
+ type VariantConstructors<D extends Record<string, Schema.Struct.Fields>, Brand> = { readonly [K in keyof D & string]: IsEmptyFields<D[K]> extends true ? TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]> & {
38
52
  readonly derive: (source: object) => TaggedStructType<K, D[K]> & Brand;
39
- } : ((args: Schema.Struct.Type<D[K]>) => TaggedStructType<K, D[K]> & Brand) & {
53
+ } : ((args: Schema.Struct.Type<D[K]>) => TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]>) & {
40
54
  readonly derive: (source: object, partial?: Partial<Schema.Struct.Type<D[K]>>) => TaggedStructType<K, D[K]> & Brand;
41
55
  readonly _tag: K;
42
56
  } };
@@ -67,6 +81,11 @@ interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Bran
67
81
  <R>(cases: MatchCases<D, R>): (value: VariantsUnion<D> & Brand) => R;
68
82
  <R>(value: VariantsUnion<D> & Brand, cases: MatchCases<D, R>): R;
69
83
  };
84
+ /**
85
+ * Reply schemas per variant tag. Only populated for event schemas
86
+ * with variants defined via `Event.reply()`.
87
+ */
88
+ readonly _replySchemas: ReadonlyMap<string, Schema.Decoder<unknown>>;
70
89
  }
71
90
  /**
72
91
  * Schema-first state definition that provides:
@@ -116,26 +135,8 @@ type MachineEventSchema<D extends Record<string, Schema.Struct.Fields>> = Schema
116
135
  * ```
117
136
  */
118
137
  declare const State: <const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineStateSchema<D>;
119
- /**
120
- * Create a schema-first Event definition.
121
- *
122
- * The schema's definition type D creates a unique brand, preventing
123
- * accidental use of constructors from different event schemas
124
- * (unless they have identical definitions).
125
- *
126
- * @example
127
- * ```ts
128
- * const OrderEvent = MachineSchema.Event({
129
- * Ship: { trackingId: Schema.String },
130
- * Cancel: {},
131
- * })
132
- *
133
- * type OrderEvent = typeof OrderEvent.Type
134
- *
135
- * // Construct
136
- * const e = OrderEvent.Ship({ trackingId: "abc" })
137
- * ```
138
- */
139
- declare const Event: <const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineEventSchema<D>;
138
+ declare const Event: (<const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineEventSchema<D>) & {
139
+ reply: <F extends Schema.Struct.Fields, RS extends Schema.Schema<unknown>>(fields: F, replySchema: RS) => ReplyFields<F, RS>;
140
+ };
140
141
  //#endregion
141
- export { Event, MachineEventSchema, MachineStateSchema, State, VariantsUnion };
142
+ export { Event, MachineEventSchema, MachineStateSchema, ReplyFields, ReplySchemaSymbol, State, VariantsUnion };
package/dist/schema.js CHANGED
@@ -38,6 +38,7 @@ import { Schema } from "effect";
38
38
  *
39
39
  * @module
40
40
  */
41
+ const ReplySchemaSymbol = Symbol.for("effect-machine/ReplySchema");
41
42
  /**
42
43
  * Build a schema-first definition from a record of tag -> fields
43
44
  */
@@ -45,9 +46,14 @@ const RESERVED_DERIVE_KEYS = new Set(["_tag"]);
45
46
  const buildMachineSchema = (definition) => {
46
47
  const variants = {};
47
48
  const constructors = {};
49
+ const replySchemas = /* @__PURE__ */ new Map();
48
50
  for (const tag of Object.keys(definition)) {
49
51
  const fields = definition[tag];
50
52
  if (fields === void 0) continue;
53
+ if (ReplySchemaSymbol in fields) {
54
+ const rs = fields[ReplySchemaSymbol];
55
+ if (rs !== void 0) replySchemas.set(tag, rs);
56
+ }
51
57
  variants[tag] = Schema.TaggedStruct(tag, fields);
52
58
  const fieldNames = new Set(Object.keys(fields));
53
59
  if (fieldNames.size > 0) {
@@ -94,6 +100,7 @@ const buildMachineSchema = (definition) => {
94
100
  variants,
95
101
  constructors,
96
102
  _definition: definition,
103
+ replySchemas,
97
104
  $is,
98
105
  $match
99
106
  };
@@ -103,10 +110,11 @@ const buildMachineSchema = (definition) => {
103
110
  * Builds the schema object with variants, constructors, $is, and $match.
104
111
  */
105
112
  const createMachineSchema = (definition) => {
106
- const { schema, variants, constructors, _definition, $is, $match } = buildMachineSchema(definition);
113
+ const { schema, variants, constructors, _definition, replySchemas, $is, $match } = buildMachineSchema(definition);
107
114
  return Object.assign(Object.create(schema), {
108
115
  variants,
109
116
  _definition,
117
+ _replySchemas: replySchemas,
110
118
  $is,
111
119
  $match,
112
120
  ...constructors
@@ -149,19 +157,40 @@ const State = (definition) => createMachineSchema(definition);
149
157
  * accidental use of constructors from different event schemas
150
158
  * (unless they have identical definitions).
151
159
  *
160
+ * Use `Event.reply(fields, replySchema)` to define events that support
161
+ * typed `ask()` replies.
162
+ *
152
163
  * @example
153
164
  * ```ts
154
- * const OrderEvent = MachineSchema.Event({
165
+ * const OrderEvent = Event({
155
166
  * Ship: { trackingId: Schema.String },
156
167
  * Cancel: {},
168
+ * GetTotal: Event.reply({}, Schema.Number),
157
169
  * })
158
170
  *
159
171
  * type OrderEvent = typeof OrderEvent.Type
160
172
  *
161
173
  * // Construct
162
174
  * const e = OrderEvent.Ship({ trackingId: "abc" })
175
+ *
176
+ * // Typed ask
177
+ * const total = yield* actor.ask(OrderEvent.GetTotal) // number
163
178
  * ```
164
179
  */
165
- const Event = (definition) => createMachineSchema(definition);
180
+ const EventImpl = (definition) => createMachineSchema(definition);
181
+ /**
182
+ * Annotate event fields with a reply schema.
183
+ * Events defined with `Event.reply(fields, replySchema)` enable typed `ask()`.
184
+ */
185
+ const replyFieldsFn = (fields, replySchema) => {
186
+ const annotated = { ...fields };
187
+ Object.defineProperty(annotated, ReplySchemaSymbol, {
188
+ value: replySchema,
189
+ enumerable: false,
190
+ writable: false
191
+ });
192
+ return annotated;
193
+ };
194
+ const Event = Object.assign(EventImpl, { reply: replyFieldsFn });
166
195
  //#endregion
167
196
  export { Event, State };
package/dist/testing.js CHANGED
@@ -30,7 +30,8 @@ const simulate = Effect.fn("effect-machine.simulate")(function* (input, events)
30
30
  const dummySelf = {
31
31
  send: dummySend,
32
32
  cast: dummySend,
33
- spawn: () => Effect.die("spawn not supported in simulation")
33
+ spawn: () => Effect.die("spawn not supported in simulation"),
34
+ reply: () => Effect.succeed(false)
34
35
  };
35
36
  let currentState = machine.initial;
36
37
  const states = [currentState];
@@ -143,7 +144,8 @@ const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function
143
144
  const dummySelf = {
144
145
  send: dummySend,
145
146
  cast: dummySend,
146
- spawn: () => Effect.die("spawn not supported in test harness")
147
+ spawn: () => Effect.die("spawn not supported in test harness"),
148
+ reply: () => Effect.succeed(false)
147
149
  };
148
150
  const stateRef = yield* SubscriptionRef.make(machine.initial);
149
151
  const hasPostponeRules = machine.postponeRules.length > 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-machine",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cevr/effect-machine.git"
@@ -56,7 +56,7 @@
56
56
  "release": "bun run build && changeset publish"
57
57
  },
58
58
  "dependencies": {
59
- "effect": "4.0.0-beta.35"
59
+ "effect": "4.0.0-beta.42"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@changesets/changelog-github": "^0.6.0",
@@ -64,7 +64,7 @@
64
64
  "@effect/language-service": "^0.82.0",
65
65
  "@types/bun": "1.3.11",
66
66
  "concurrently": "^9.2.1",
67
- "effect-bun-test": "0.2.1",
67
+ "effect-bun-test": "0.3.0",
68
68
  "effect-v3": "npm:effect@^3.21.0",
69
69
  "lefthook": "^2.1.4",
70
70
  "oxfmt": "^0.41.0",
@@ -81,6 +81,6 @@
81
81
  }
82
82
  },
83
83
  "overrides": {
84
- "effect": "4.0.0-beta.35"
84
+ "effect": "4.0.0-beta.42"
85
85
  }
86
86
  }
@@ -1,11 +1,9 @@
1
1
  import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
2
- import { PersistentMachine } from "./persistence/persistent-machine.js";
2
+ import { ExtractReply, ReplyTypeBrand } from "./internal/brands.js";
3
3
  import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
4
4
  import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
5
- import { PersistentActorRef } from "./persistence/persistent-actor.js";
6
- import { ActorMetadata, PersistenceAdapterTag, PersistenceError, RestoreResult, VersionConflictError } from "./persistence/adapter.js";
7
5
  import { BuiltMachine, Machine, MachineRef } from "./machine.js";
8
- import { Context, Deferred, Effect, Layer, Option, Queue, Ref, Scope, Stream, SubscriptionRef } from "effect";
6
+ import { Context, Deferred, Effect, Layer, Option, PubSub, Queue, Ref, Scope, Stream, SubscriptionRef } from "effect";
9
7
  import * as effect_dist_dts_Tracer_js0 from "effect/dist/dts/Tracer.js";
10
8
 
11
9
  //#region src/actor.d.ts
@@ -39,6 +37,15 @@ interface ActorRefSync<State extends {
39
37
  readonly matches: (tag: State["_tag"]) => boolean;
40
38
  readonly can: (event: Event) => boolean;
41
39
  }
40
+ /**
41
+ * Information about a successful transition.
42
+ * Emitted on the `transitions` stream after each accepted event.
43
+ */
44
+ interface TransitionInfo<State, Event> {
45
+ readonly fromState: State;
46
+ readonly toState: State;
47
+ readonly event: Event;
48
+ }
42
49
  interface ActorRef<State extends {
43
50
  readonly _tag: string;
44
51
  }, Event> {
@@ -53,11 +60,11 @@ interface ActorRef<State extends {
53
60
  */
54
61
  readonly call: (event: Event) => Effect.Effect<ProcessEventResult<State>>;
55
62
  /**
56
- * Typed request-reply. Event is processed through the queue; caller gets
57
- * the domain value returned by the handler's `reply` field.
63
+ * Typed request-reply. Accepts only events with a reply schema
64
+ * (defined via `Event.reply()`). Return type is inferred from the schema.
58
65
  * Fails with NoReplyError if the handler doesn't provide a reply.
59
66
  */
60
- readonly ask: <R>(event: Event) => Effect.Effect<R, NoReplyError | ActorStoppedError>;
67
+ readonly ask: <E extends Event & ReplyTypeBrand<unknown>>(event: E) => Effect.Effect<ExtractReply<E>, NoReplyError | ActorStoppedError>;
61
68
  /** Observable state. */
62
69
  readonly state: SubscriptionRef.SubscriptionRef<State>;
63
70
  /** Stop the actor gracefully. */
@@ -70,6 +77,14 @@ interface ActorRef<State extends {
70
77
  readonly can: (event: Event) => Effect.Effect<boolean>;
71
78
  /** Stream of state changes. */
72
79
  readonly changes: Stream.Stream<State>;
80
+ /**
81
+ * Stream of accepted transitions (edge stream).
82
+ *
83
+ * Emits `{ fromState, toState, event }` on every successful transition,
84
+ * including same-state reenters. PubSub-backed — late subscribers miss
85
+ * past edges. This is observational, not a durability guarantee.
86
+ */
87
+ readonly transitions: Stream.Stream<TransitionInfo<State, Event>>;
73
88
  /** Wait for a state matching predicate or variant (includes current snapshot). */
74
89
  readonly waitFor: {
75
90
  (predicate: (state: State) => boolean): Effect.Effect<State>;
@@ -123,54 +138,17 @@ interface ActorSystem {
123
138
  /**
124
139
  * Spawn a new actor with the given machine.
125
140
  *
126
- * For regular machines, returns ActorRef.
127
- * For persistent machines (created with Machine.persist), returns PersistentActorRef.
128
- *
129
- * All effect slots must be provided via `.build()` before spawning.
130
- *
131
141
  * @example
132
142
  * ```ts
133
- * // Regular machine (built)
134
143
  * const built = machine.build({ fetchData: ... })
135
144
  * const actor = yield* system.spawn("my-actor", built);
136
- *
137
- * // Persistent machine (auto-detected)
138
- * const persistentActor = yield* system.spawn("my-actor", persistentMachine);
139
- * persistentActor.persist; // available
140
- * persistentActor.version; // available
141
- * ```
142
- */
143
- readonly spawn: {
144
- <S extends {
145
- readonly _tag: string;
146
- }, E extends {
147
- readonly _tag: string;
148
- }, R>(id: string, machine: BuiltMachine<S, E, R>): Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
149
- <S extends {
150
- readonly _tag: string;
151
- }, E extends {
152
- readonly _tag: string;
153
- }, R>(id: string, machine: PersistentMachine<S, E, R>): Effect.Effect<PersistentActorRef<S, E, R>, PersistenceError | VersionConflictError | DuplicateActorError, R | PersistenceAdapterTag>;
154
- };
155
- /**
156
- * Restore an actor from persistence.
157
- * Returns None if no persisted state exists for the given ID.
158
- *
159
- * @example
160
- * ```ts
161
- * const maybeActor = yield* system.restore("order-1", persistentMachine);
162
- * if (Option.isSome(maybeActor)) {
163
- * const actor = maybeActor.value;
164
- * const state = yield* actor.snapshot;
165
- * console.log(`Restored to state: ${state._tag}`);
166
- * }
167
145
  * ```
168
146
  */
169
- readonly restore: <S extends {
147
+ readonly spawn: <S extends {
170
148
  readonly _tag: string;
171
149
  }, E extends {
172
150
  readonly _tag: string;
173
- }, R>(id: string, machine: PersistentMachine<S, E, R>) => Effect.Effect<Option.Option<PersistentActorRef<S, E, R>>, PersistenceError | DuplicateActorError, R | PersistenceAdapterTag>;
151
+ }, R>(id: string, machine: BuiltMachine<S, E, R>) => Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
174
152
  /**
175
153
  * Get an existing actor by ID
176
154
  */
@@ -194,53 +172,6 @@ interface ActorSystem {
194
172
  * Returns an unsubscribe function.
195
173
  */
196
174
  readonly subscribe: (fn: SystemEventListener) => () => void;
197
- /**
198
- * List all persisted actor metadata.
199
- * Returns empty array if adapter doesn't support registry.
200
- *
201
- * @example
202
- * ```ts
203
- * const actors = yield* system.listPersisted();
204
- * for (const meta of actors) {
205
- * console.log(`${meta.id}: ${meta.stateTag} (v${meta.version})`);
206
- * }
207
- * ```
208
- */
209
- readonly listPersisted: () => Effect.Effect<ReadonlyArray<ActorMetadata>, PersistenceError, PersistenceAdapterTag>;
210
- /**
211
- * Restore multiple actors by ID.
212
- * Returns both successfully restored actors and failures.
213
- *
214
- * @example
215
- * ```ts
216
- * const result = yield* system.restoreMany(["order-1", "order-2"], orderMachine);
217
- * console.log(`Restored: ${result.restored.length}, Failed: ${result.failed.length}`);
218
- * ```
219
- */
220
- readonly restoreMany: <S extends {
221
- readonly _tag: string;
222
- }, E extends {
223
- readonly _tag: string;
224
- }, R>(ids: ReadonlyArray<string>, machine: PersistentMachine<S, E, R>) => Effect.Effect<RestoreResult<S, E, R>, never, R | PersistenceAdapterTag>;
225
- /**
226
- * Restore all persisted actors for a machine type.
227
- * Uses adapter registry if available, otherwise returns empty result.
228
- *
229
- * @example
230
- * ```ts
231
- * const result = yield* system.restoreAll(orderMachine, {
232
- * filter: (meta) => meta.stateTag !== "Done"
233
- * });
234
- * console.log(`Restored ${result.restored.length} active orders`);
235
- * ```
236
- */
237
- readonly restoreAll: <S extends {
238
- readonly _tag: string;
239
- }, E extends {
240
- readonly _tag: string;
241
- }, R>(machine: PersistentMachine<S, E, R>, options?: {
242
- filter?: (meta: ActorMetadata) => boolean;
243
- }) => Effect.Effect<RestoreResult<S, E, R>, PersistenceError, R | PersistenceAdapterTag>;
244
175
  }
245
176
  /**
246
177
  * ActorSystem service tag
@@ -259,7 +190,7 @@ declare const buildActorRefCore: <S extends {
259
190
  readonly _tag: string;
260
191
  }, E extends {
261
192
  readonly _tag: string;
262
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueue: Queue.Queue<QueuedEvent<E>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>) => ActorRef<S, E>;
193
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueue: Queue.Queue<QueuedEvent<E>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, transitionsPubSub?: PubSub.PubSub<TransitionInfo<S, E>>) => ActorRef<S, E>;
263
194
  /**
264
195
  * Create and start an actor for a machine
265
196
  */
@@ -267,7 +198,9 @@ declare const createActor: <S extends {
267
198
  readonly _tag: string;
268
199
  }, E extends {
269
200
  readonly _tag: string;
270
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>) => Effect.Effect<ActorRef<S, E>, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, effect_dist_dts_Tracer_js0.ParentSpan> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope> | Exclude<Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>, effect_dist_dts_Tracer_js0.ParentSpan>>;
201
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, options?: {
202
+ initialState?: S;
203
+ } | undefined) => Effect.Effect<ActorRef<S, E>, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, effect_dist_dts_Tracer_js0.ParentSpan> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope> | Exclude<Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>, effect_dist_dts_Tracer_js0.ParentSpan>>;
271
204
  /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
272
205
  declare const settlePendingReplies: (pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, actorId: string) => Effect.Effect<void, never, never>;
273
206
  /**
@@ -275,4 +208,4 @@ declare const settlePendingReplies: (pendingReplies: Set<Deferred.Deferred<unkno
275
208
  */
276
209
  declare const Default: Layer.Layer<ActorSystem, never, never>;
277
210
  //#endregion
278
- export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
211
+ export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, TransitionInfo, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };