effect-machine 0.11.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 (58) hide show
  1. package/dist/actor.d.ts +10 -4
  2. package/dist/actor.js +33 -5
  3. package/dist/cluster/adapters/in-memory.d.ts +28 -0
  4. package/dist/cluster/adapters/in-memory.js +79 -0
  5. package/dist/cluster/entity-actor-ref.d.ts +56 -0
  6. package/dist/cluster/entity-actor-ref.js +33 -0
  7. package/dist/cluster/entity-machine.d.ts +31 -49
  8. package/dist/cluster/entity-machine.js +167 -52
  9. package/dist/cluster/index.d.ts +5 -2
  10. package/dist/cluster/index.js +4 -1
  11. package/dist/cluster/persistence.d.ts +49 -0
  12. package/dist/cluster/persistence.js +18 -0
  13. package/dist/cluster/to-entity.d.ts +9 -3
  14. package/dist/cluster/to-entity.js +16 -4
  15. package/dist/errors.d.ts +12 -1
  16. package/dist/errors.js +8 -1
  17. package/dist/index.d.ts +3 -2
  18. package/dist/internal/brands.d.ts +14 -1
  19. package/dist/internal/runtime.d.ts +67 -0
  20. package/dist/internal/runtime.js +248 -0
  21. package/dist/internal/transition.d.ts +5 -0
  22. package/dist/internal/transition.js +15 -3
  23. package/dist/internal/utils.d.ts +42 -6
  24. package/dist/internal/utils.js +27 -1
  25. package/dist/machine.d.ts +26 -13
  26. package/dist/machine.js +14 -3
  27. package/dist/schema.d.ts +35 -34
  28. package/dist/schema.js +32 -3
  29. package/dist/testing.js +4 -2
  30. package/package.json +3 -3
  31. package/v3/dist/actor.d.ts +4 -3
  32. package/v3/dist/actor.js +15 -3
  33. package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
  34. package/v3/dist/cluster/adapters/in-memory.js +62 -0
  35. package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
  36. package/v3/dist/cluster/entity-actor-ref.js +19 -0
  37. package/v3/dist/cluster/entity-machine.d.ts +34 -49
  38. package/v3/dist/cluster/entity-machine.js +134 -50
  39. package/v3/dist/cluster/index.d.ts +5 -2
  40. package/v3/dist/cluster/index.js +4 -1
  41. package/v3/dist/cluster/persistence.d.ts +48 -0
  42. package/v3/dist/cluster/persistence.js +14 -0
  43. package/v3/dist/cluster/to-entity.d.ts +5 -2
  44. package/v3/dist/cluster/to-entity.js +12 -4
  45. package/v3/dist/errors.d.ts +16 -1
  46. package/v3/dist/errors.js +8 -1
  47. package/v3/dist/index.d.ts +3 -2
  48. package/v3/dist/internal/brands.d.ts +15 -1
  49. package/v3/dist/internal/runtime.d.ts +65 -0
  50. package/v3/dist/internal/runtime.js +236 -0
  51. package/v3/dist/internal/transition.d.ts +5 -0
  52. package/v3/dist/internal/transition.js +15 -3
  53. package/v3/dist/internal/utils.d.ts +42 -6
  54. package/v3/dist/internal/utils.js +27 -1
  55. package/v3/dist/machine.d.ts +19 -13
  56. package/v3/dist/machine.js +10 -2
  57. package/v3/dist/schema.d.ts +35 -34
  58. package/v3/dist/schema.js +29 -3
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.11.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",
@@ -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,4 +1,5 @@
1
1
  import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
2
+ import { ExtractReply, ReplyTypeBrand } from "./internal/brands.js";
2
3
  import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
3
4
  import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
4
5
  import { BuiltMachine, Machine, MachineRef } from "./machine.js";
@@ -59,11 +60,11 @@ interface ActorRef<State extends {
59
60
  */
60
61
  readonly call: (event: Event) => Effect.Effect<ProcessEventResult<State>>;
61
62
  /**
62
- * Typed request-reply. Event is processed through the queue; caller gets
63
- * 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.
64
65
  * Fails with NoReplyError if the handler doesn't provide a reply.
65
66
  */
66
- 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>;
67
68
  /** Observable state. */
68
69
  readonly state: SubscriptionRef.SubscriptionRef<State>;
69
70
  /** Stop the actor gracefully. */
package/v3/dist/actor.js CHANGED
@@ -3,7 +3,7 @@ import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
3
3
  import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
4
4
  import { emitWithTimestamp } from "./internal/inspection.js";
5
5
  import { processEventCore, resolveTransition, runSpawnEffects, shouldPostpone } from "./internal/transition.js";
6
- import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect";
6
+ import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Runtime, Schema, Scope, Stream, SubscriptionRef } from "effect";
7
7
  //#region src/actor.ts
8
8
  /**
9
9
  * Actor system: spawning, lifecycle, and event processing.
@@ -276,6 +276,7 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
276
276
  lifecycleRan: false,
277
277
  isFinal: false,
278
278
  hasReply: false,
279
+ deferReply: false,
279
280
  reply: void 0,
280
281
  postponed: true
281
282
  };
@@ -296,8 +297,19 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
296
297
  yield* Deferred.succeed(queued.reply, result);
297
298
  break;
298
299
  case "ask":
299
- if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
300
- else yield* Deferred.fail(queued.reply, new NoReplyError({
300
+ if (result.hasReply) {
301
+ const replySchema = machine._replySchemas?.get(event._tag);
302
+ if (replySchema !== void 0) {
303
+ let decoded;
304
+ try {
305
+ decoded = Schema.decodeUnknownSync(replySchema)(result.reply);
306
+ } catch (decodeError) {
307
+ yield* Deferred.die(queued.reply, decodeError);
308
+ return yield* Effect.die(decodeError);
309
+ }
310
+ yield* Deferred.succeed(queued.reply, decoded);
311
+ } else yield* Deferred.succeed(queued.reply, result.reply);
312
+ } else yield* Deferred.fail(queued.reply, new NoReplyError({
301
313
  actorId,
302
314
  eventTag: event._tag
303
315
  }));
@@ -0,0 +1,15 @@
1
+ import { PersistedEvent, PersistenceAdapter, Snapshot } from "../persistence.js";
2
+ import { Effect, Layer, Ref } from "effect";
3
+
4
+ //#region src/cluster/adapters/in-memory.d.ts
5
+ interface EntityStore {
6
+ snapshot: Snapshot<unknown> | undefined;
7
+ events: Array<PersistedEvent<unknown>>;
8
+ }
9
+ declare const makeInMemoryPersistenceAdapter: Effect.Effect<{
10
+ adapter: PersistenceAdapter;
11
+ storeRef: Ref.Ref<Map<string, EntityStore>>;
12
+ layer: Layer.Layer<PersistenceAdapter, never, never>;
13
+ }, never, never>;
14
+ //#endregion
15
+ export { makeInMemoryPersistenceAdapter };
@@ -0,0 +1,62 @@
1
+ import { VersionConflictError } from "../../errors.js";
2
+ import { PersistenceAdapter } from "../persistence.js";
3
+ import { Effect, Layer, Option, Ref } from "effect";
4
+ //#region src/cluster/adapters/in-memory.ts
5
+ /**
6
+ * In-memory persistence adapter for testing and development (v3).
7
+ *
8
+ * @module
9
+ */
10
+ const makeKey = (key) => `${key.entityType}/${key.entityId}`;
11
+ const getOrCreate = (store, key) => {
12
+ let entry = store.get(key);
13
+ if (entry === void 0) {
14
+ entry = {
15
+ snapshot: void 0,
16
+ events: []
17
+ };
18
+ store.set(key, entry);
19
+ }
20
+ return entry;
21
+ };
22
+ const makeInMemoryPersistenceAdapter = Effect.gen(function* () {
23
+ const store = /* @__PURE__ */ new Map();
24
+ const storeRef = yield* Ref.make(store);
25
+ const adapter = {
26
+ saveSnapshot: (key, snapshot) => Effect.gen(function* () {
27
+ const entry = getOrCreate(yield* Ref.get(storeRef), makeKey(key));
28
+ if (entry.snapshot !== void 0 && snapshot.version < entry.snapshot.version) return yield* new VersionConflictError({
29
+ expected: snapshot.version,
30
+ actual: entry.snapshot.version
31
+ });
32
+ entry.snapshot = snapshot;
33
+ }),
34
+ loadSnapshot: (key) => Effect.gen(function* () {
35
+ const entry = (yield* Ref.get(storeRef)).get(makeKey(key));
36
+ return Option.fromNullable(entry?.snapshot);
37
+ }),
38
+ appendEvents: (key, events, expectedVersion) => Effect.gen(function* () {
39
+ const entry = getOrCreate(yield* Ref.get(storeRef), makeKey(key));
40
+ const lastEvent = entry.events[entry.events.length - 1];
41
+ const currentVersion = lastEvent !== void 0 ? lastEvent.version : 0;
42
+ if (currentVersion !== expectedVersion) return yield* new VersionConflictError({
43
+ expected: expectedVersion,
44
+ actual: currentVersion
45
+ });
46
+ for (const event of events) entry.events.push(event);
47
+ }),
48
+ loadEvents: (key, afterVersion) => Effect.gen(function* () {
49
+ const entry = (yield* Ref.get(storeRef)).get(makeKey(key));
50
+ if (entry === void 0) return [];
51
+ if (afterVersion === void 0) return entry.events;
52
+ return entry.events.filter((e) => e.version > afterVersion);
53
+ })
54
+ };
55
+ return {
56
+ adapter,
57
+ storeRef,
58
+ layer: Layer.succeed(PersistenceAdapter, adapter)
59
+ };
60
+ });
61
+ //#endregion
62
+ export { makeInMemoryPersistenceAdapter };
@@ -0,0 +1,49 @@
1
+ import { ExtractReply, ReplyTypeBrand } from "../internal/brands.js";
2
+ import { NoReplyError } from "../errors.js";
3
+ import { Effect } from "effect";
4
+ import { RpcClient } from "effect/unstable/rpc";
5
+
6
+ //#region src/cluster/entity-actor-ref.d.ts
7
+ /**
8
+ * Typed client wrapper for remote entity machines.
9
+ *
10
+ * Unlike local `ActorRef`, this communicates over cluster RPCs.
11
+ * Only operations that make sense over the network are exposed.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const ref = yield* EntityActorRef.make(OrderEntity, OrderEntityLayer, "order-123")
16
+ * yield* ref.send(OrderEvent.Ship({ trackingId: "abc" }))
17
+ * const state = yield* ref.snapshot
18
+ * ```
19
+ */
20
+ interface EntityActorRef<State extends {
21
+ readonly _tag: string;
22
+ }, Event extends {
23
+ readonly _tag: string;
24
+ }> {
25
+ readonly entityId: string;
26
+ /** Send event (fire-and-forget). Returns new state after processing. */
27
+ readonly send: (event: Event) => Effect.Effect<State>;
28
+ /** Send event and get typed domain reply (via Event.reply() schema). */
29
+ readonly ask: <E extends Event & ReplyTypeBrand<unknown>>(event: E) => Effect.Effect<ExtractReply<E>, NoReplyError>;
30
+ /** Get current state. */
31
+ readonly snapshot: Effect.Effect<State>;
32
+ }
33
+ /**
34
+ * Create an EntityActorRef from a test client factory and entity ID.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const makeClient = yield* Entity.makeTestClient(entity, entityLayer)
39
+ * const ref = yield* makeEntityActorRef(makeClient, "order-123")
40
+ * yield* ref.send(OrderEvent.Process)
41
+ * ```
42
+ */
43
+ declare const makeEntityActorRef: <State extends {
44
+ readonly _tag: string;
45
+ }, Event extends {
46
+ readonly _tag: string;
47
+ }>(client: RpcClient.RpcClient<any>, entityId: string) => EntityActorRef<State, Event>;
48
+ //#endregion
49
+ export { EntityActorRef, makeEntityActorRef };
@@ -0,0 +1,19 @@
1
+ //#region src/cluster/entity-actor-ref.ts
2
+ /**
3
+ * Create an EntityActorRef from a test client factory and entity ID.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const makeClient = yield* Entity.makeTestClient(entity, entityLayer)
8
+ * const ref = yield* makeEntityActorRef(makeClient, "order-123")
9
+ * yield* ref.send(OrderEvent.Process)
10
+ * ```
11
+ */
12
+ const makeEntityActorRef = (client, entityId) => ({
13
+ entityId,
14
+ send: (event) => client.Send({ event }),
15
+ ask: ((event) => client.Ask({ event })),
16
+ snapshot: client.GetState()
17
+ });
18
+ //#endregion
19
+ export { makeEntityActorRef };
@@ -1,7 +1,7 @@
1
- import { EffectsDef, GuardsDef } from "../slot.js";
2
1
  import { ProcessEventHooks } from "../internal/transition.js";
3
2
  import { Machine } from "../machine.js";
4
- import { Layer } from "effect";
3
+ import { EntityPersistenceConfig } from "./persistence.js";
4
+ import { Duration, Layer, Schedule } from "effect";
5
5
  import { Entity } from "@effect/cluster";
6
6
  import { Rpc } from "@effect/rpc";
7
7
 
@@ -13,77 +13,62 @@ interface EntityMachineOptions<S, E> {
13
13
  /**
14
14
  * Initialize state from entity ID.
15
15
  * Called once when entity is first activated.
16
- *
17
- * @example
18
- * ```ts
19
- * EntityMachine.layer(OrderEntity, orderMachine, {
20
- * initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
21
- * })
22
- * ```
23
16
  */
24
17
  readonly initializeState?: (entityId: string) => S;
25
18
  /**
26
19
  * Optional hooks for inspection/tracing.
27
- * Called at specific points during event processing.
28
- *
29
- * @example
30
- * ```ts
31
- * EntityMachine.layer(OrderEntity, orderMachine, {
32
- * hooks: {
33
- * onTransition: (from, to, event) =>
34
- * Effect.log(`Transition: ${from._tag} -> ${to._tag}`),
35
- * onSpawnEffect: (state) =>
36
- * Effect.log(`Running spawn effects for ${state._tag}`),
37
- * onError: ({ phase, state }) =>
38
- * Effect.log(`Defect in ${phase} at ${state._tag}`),
39
- * },
40
- * })
41
- * ```
42
20
  */
43
21
  readonly hooks?: ProcessEventHooks<S, E>;
22
+ /**
23
+ * Maximum idle time before entity deactivation.
24
+ * Forwarded to Entity.toLayer.
25
+ */
26
+ readonly maxIdleTime?: Duration.DurationInput;
27
+ /**
28
+ * Concurrency for handler execution.
29
+ * Forwarded to Entity.toLayer.
30
+ */
31
+ readonly concurrency?: number | "unbounded";
32
+ /**
33
+ * Mailbox capacity. Default: "unbounded".
34
+ * Forwarded to Entity.toLayer.
35
+ */
36
+ readonly mailboxCapacity?: number | "unbounded";
37
+ /**
38
+ * Disable fatal defects (defects won't crash the entity activation).
39
+ * Forwarded to Entity.toLayer.
40
+ */
41
+ readonly disableFatalDefects?: boolean;
42
+ /**
43
+ * Retry policy for defects (schedule for restarting after defect).
44
+ * Forwarded to Entity.toLayer.
45
+ */
46
+ readonly defectRetryPolicy?: Schedule.Schedule<any, unknown>;
47
+ /**
48
+ * Persistence configuration. When set, requires PersistenceAdapter in R.
49
+ */
50
+ readonly persistence?: EntityPersistenceConfig;
44
51
  }
45
52
  /**
46
53
  * Create an Entity layer that wires a machine to handle RPC calls.
47
54
  *
48
- * The layer:
49
- * - Maintains state via Ref per entity instance
50
- * - Resolves transitions using the indexed lookup
51
- * - Evaluates guards in registration order
52
- * - Runs lifecycle effects (onEnter/spawn)
53
- * - Processes internal events from spawn effects
55
+ * v3: Uses `Entity.toLayer` with handler objects backed by the runtime kernel.
54
56
  *
55
57
  * @example
56
58
  * ```ts
57
- * const OrderEntity = toEntity(orderMachine, {
58
- * type: "Order",
59
- * stateSchema: OrderState,
60
- * eventSchema: OrderEvent,
61
- * })
59
+ * const OrderEntity = toEntity(orderMachine, { type: "Order" })
62
60
  *
63
61
  * const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine, {
64
62
  * initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
65
63
  * })
66
- *
67
- * // Use in cluster
68
- * const program = Effect.gen(function* () {
69
- * const client = yield* ShardingClient.client(OrderEntity)
70
- * yield* client.Send("order-123", { event: OrderEvent.Ship({ trackingId: "abc" }) })
71
- * })
72
64
  * ```
73
65
  */
74
66
  declare const EntityMachine: {
75
- /**
76
- * Create a layer that wires a machine to an Entity.
77
- *
78
- * @param entity - Entity created via toEntity()
79
- * @param machine - Machine with all effects provided
80
- * @param options - Optional configuration (state initializer, inspection hooks)
81
- */
82
67
  layer: <S extends {
83
68
  readonly _tag: string;
84
69
  }, E extends {
85
70
  readonly _tag: string;
86
- }, 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>;
71
+ }, R, EntityType extends string, Rpcs extends Rpc.Any>(entity: Entity.Entity<EntityType, Rpcs>, machine: Machine<S, E, R, any, any, any, any>, options?: EntityMachineOptions<S, E>) => Layer.Layer<never, never, R>;
87
72
  };
88
73
  //#endregion
89
74
  export { EntityMachine, EntityMachineOptions };