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.
- package/dist/actor.d.ts +10 -4
- package/dist/actor.js +33 -5
- package/dist/cluster/adapters/in-memory.d.ts +28 -0
- package/dist/cluster/adapters/in-memory.js +79 -0
- package/dist/cluster/entity-actor-ref.d.ts +56 -0
- package/dist/cluster/entity-actor-ref.js +33 -0
- package/dist/cluster/entity-machine.d.ts +31 -49
- package/dist/cluster/entity-machine.js +167 -52
- package/dist/cluster/index.d.ts +5 -2
- package/dist/cluster/index.js +4 -1
- package/dist/cluster/persistence.d.ts +49 -0
- package/dist/cluster/persistence.js +18 -0
- package/dist/cluster/to-entity.d.ts +9 -3
- package/dist/cluster/to-entity.js +16 -4
- package/dist/errors.d.ts +12 -1
- package/dist/errors.js +8 -1
- package/dist/index.d.ts +3 -2
- package/dist/internal/brands.d.ts +14 -1
- package/dist/internal/runtime.d.ts +67 -0
- package/dist/internal/runtime.js +248 -0
- package/dist/internal/transition.d.ts +5 -0
- package/dist/internal/transition.js +15 -3
- package/dist/internal/utils.d.ts +42 -6
- package/dist/internal/utils.js +27 -1
- package/dist/machine.d.ts +26 -13
- package/dist/machine.js +14 -3
- package/dist/schema.d.ts +35 -34
- package/dist/schema.js +32 -3
- package/dist/testing.js +4 -2
- package/package.json +3 -3
- package/v3/dist/actor.d.ts +4 -3
- package/v3/dist/actor.js +15 -3
- package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
- package/v3/dist/cluster/adapters/in-memory.js +62 -0
- package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
- package/v3/dist/cluster/entity-actor-ref.js +19 -0
- package/v3/dist/cluster/entity-machine.d.ts +34 -49
- package/v3/dist/cluster/entity-machine.js +134 -50
- package/v3/dist/cluster/index.d.ts +5 -2
- package/v3/dist/cluster/index.js +4 -1
- package/v3/dist/cluster/persistence.d.ts +48 -0
- package/v3/dist/cluster/persistence.js +14 -0
- package/v3/dist/cluster/to-entity.d.ts +5 -2
- package/v3/dist/cluster/to-entity.js +12 -4
- package/v3/dist/errors.d.ts +16 -1
- package/v3/dist/errors.js +8 -1
- package/v3/dist/index.d.ts +3 -2
- package/v3/dist/internal/brands.d.ts +15 -1
- package/v3/dist/internal/runtime.d.ts +65 -0
- package/v3/dist/internal/runtime.js +236 -0
- package/v3/dist/internal/transition.d.ts +5 -0
- package/v3/dist/internal/transition.js +15 -3
- package/v3/dist/internal/utils.d.ts +42 -6
- package/v3/dist/internal/utils.js +27 -1
- package/v3/dist/machine.d.ts +19 -13
- package/v3/dist/machine.js +10 -2
- package/v3/dist/schema.d.ts +35 -34
- package/v3/dist/schema.js +29 -3
package/dist/actor.d.ts
CHANGED
|
@@ -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.
|
|
63
|
-
*
|
|
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: <
|
|
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. */
|
|
@@ -202,9 +203,14 @@ declare const createActor: <S extends {
|
|
|
202
203
|
} | undefined) => Effect.Effect<ActorRef<S, E>, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, effect_Tracer0.ParentSpan> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope> | Exclude<Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>, effect_Tracer0.ParentSpan>>;
|
|
203
204
|
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
204
205
|
declare const settlePendingReplies: (pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, actorId: string) => Effect.Effect<void, never, never>;
|
|
206
|
+
/**
|
|
207
|
+
* Create an ActorSystem instance. Must be run in a Scope.
|
|
208
|
+
* @internal — use Default layer for normal usage
|
|
209
|
+
*/
|
|
210
|
+
declare const makeSystem: () => Effect.Effect<ActorSystem, never, Scope.Scope>;
|
|
205
211
|
/**
|
|
206
212
|
* Default ActorSystem layer
|
|
207
213
|
*/
|
|
208
214
|
declare const Default: Layer.Layer<ActorSystem, never, never>;
|
|
209
215
|
//#endregion
|
|
210
|
-
export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, TransitionInfo, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
|
216
|
+
export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, TransitionInfo, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
package/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, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
|
|
6
|
+
import { Cause, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schema, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
|
|
7
7
|
//#region src/actor.ts
|
|
8
8
|
/**
|
|
9
9
|
* Actor system: spawning, lifecycle, and event processing.
|
|
@@ -163,6 +163,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
163
163
|
const eventQueue = yield* Queue.unbounded();
|
|
164
164
|
const stoppedRef = yield* Ref.make(false);
|
|
165
165
|
const childrenMap = /* @__PURE__ */ new Map();
|
|
166
|
+
const deferredReplyRef = { current: void 0 };
|
|
166
167
|
const selfSend = Effect.fn("effect-machine.actor.self.send")(function* (event) {
|
|
167
168
|
if (yield* Ref.get(stoppedRef)) return;
|
|
168
169
|
yield* Queue.offer(eventQueue, {
|
|
@@ -181,6 +182,15 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
181
182
|
childrenMap.delete(childId);
|
|
182
183
|
}));
|
|
183
184
|
return child;
|
|
185
|
+
}),
|
|
186
|
+
reply: (value) => Effect.sync(() => {
|
|
187
|
+
const deferred = deferredReplyRef.current;
|
|
188
|
+
if (deferred !== void 0) {
|
|
189
|
+
deferredReplyRef.current = void 0;
|
|
190
|
+
Effect.runFork(Deferred.succeed(deferred, value));
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
184
194
|
})
|
|
185
195
|
};
|
|
186
196
|
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
|
|
@@ -230,7 +240,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
230
240
|
}
|
|
231
241
|
const pendingReplies = /* @__PURE__ */ new Set();
|
|
232
242
|
const transitionsPubSub = yield* PubSub.unbounded();
|
|
233
|
-
const loopFiber = yield* Effect.forkDetach(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies, transitionsPubSub));
|
|
243
|
+
const loopFiber = yield* Effect.forkDetach(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies, transitionsPubSub, deferredReplyRef));
|
|
234
244
|
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
|
|
235
245
|
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
236
246
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
@@ -258,7 +268,7 @@ const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
|
|
|
258
268
|
* Includes postpone buffer — events matching postpone rules are buffered
|
|
259
269
|
* and drained after state tag changes (gen_statem semantics).
|
|
260
270
|
*/
|
|
261
|
-
const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies, transitionsPubSub) {
|
|
271
|
+
const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies, transitionsPubSub, deferredReplyRef) {
|
|
262
272
|
const postponed = [];
|
|
263
273
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
264
274
|
const processQueued = Effect.fn("effect-machine.actor.processQueued")(function* (queued) {
|
|
@@ -274,6 +284,7 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
|
|
|
274
284
|
lifecycleRan: false,
|
|
275
285
|
isFinal: false,
|
|
276
286
|
hasReply: false,
|
|
287
|
+
deferReply: false,
|
|
277
288
|
reply: void 0,
|
|
278
289
|
postponed: true
|
|
279
290
|
};
|
|
@@ -294,7 +305,19 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
|
|
|
294
305
|
yield* Deferred.succeed(queued.reply, result);
|
|
295
306
|
break;
|
|
296
307
|
case "ask":
|
|
297
|
-
if (result.hasReply)
|
|
308
|
+
if (result.hasReply) {
|
|
309
|
+
const replySchema = machine._replySchemas?.get(event._tag);
|
|
310
|
+
if (replySchema !== void 0) {
|
|
311
|
+
let decoded;
|
|
312
|
+
try {
|
|
313
|
+
decoded = Schema.decodeUnknownSync(replySchema)(result.reply);
|
|
314
|
+
} catch (decodeError) {
|
|
315
|
+
yield* Deferred.die(queued.reply, decodeError);
|
|
316
|
+
return yield* Effect.die(decodeError);
|
|
317
|
+
}
|
|
318
|
+
yield* Deferred.succeed(queued.reply, decoded);
|
|
319
|
+
} else yield* Deferred.succeed(queued.reply, result.reply);
|
|
320
|
+
} else if (result.deferReply) deferredReplyRef.current = queued.reply;
|
|
298
321
|
else yield* Deferred.fail(queued.reply, new NoReplyError({
|
|
299
322
|
actorId,
|
|
300
323
|
eventTag: event._tag
|
|
@@ -529,8 +552,13 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
529
552
|
});
|
|
530
553
|
});
|
|
531
554
|
/**
|
|
555
|
+
* Create an ActorSystem instance. Must be run in a Scope.
|
|
556
|
+
* @internal — use Default layer for normal usage
|
|
557
|
+
*/
|
|
558
|
+
const makeSystem = make;
|
|
559
|
+
/**
|
|
532
560
|
* Default ActorSystem layer
|
|
533
561
|
*/
|
|
534
562
|
const Default = Layer.effect(ActorSystem, make());
|
|
535
563
|
//#endregion
|
|
536
|
-
export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
|
564
|
+
export { ActorSystem, Default, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Create an in-memory persistence adapter.
|
|
11
|
+
*
|
|
12
|
+
* Returns a Layer providing `PersistenceAdapter` and a ref
|
|
13
|
+
* to the backing store for test assertions.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const { layer, storeRef } = yield* makeInMemoryPersistenceAdapter
|
|
18
|
+
* // Use layer to provide PersistenceAdapter
|
|
19
|
+
* // Inspect storeRef for test assertions
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
declare const makeInMemoryPersistenceAdapter: Effect.Effect<{
|
|
23
|
+
adapter: PersistenceAdapter;
|
|
24
|
+
storeRef: Ref.Ref<Map<string, EntityStore>>;
|
|
25
|
+
layer: Layer.Layer<PersistenceAdapter, never, never>;
|
|
26
|
+
}, never, never>;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { makeInMemoryPersistenceAdapter };
|
|
@@ -0,0 +1,79 @@
|
|
|
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.
|
|
7
|
+
*
|
|
8
|
+
* Backed by a simple Map — state is lost on process exit.
|
|
9
|
+
* Supports both snapshot and journal strategies with proper
|
|
10
|
+
* version checking (CAS on appends, monotonic on snapshots).
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
const makeKey = (key) => `${key.entityType}/${key.entityId}`;
|
|
15
|
+
const getOrCreate = (store, key) => {
|
|
16
|
+
let entry = store.get(key);
|
|
17
|
+
if (entry === void 0) {
|
|
18
|
+
entry = {
|
|
19
|
+
snapshot: void 0,
|
|
20
|
+
events: []
|
|
21
|
+
};
|
|
22
|
+
store.set(key, entry);
|
|
23
|
+
}
|
|
24
|
+
return entry;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Create an in-memory persistence adapter.
|
|
28
|
+
*
|
|
29
|
+
* Returns a Layer providing `PersistenceAdapter` and a ref
|
|
30
|
+
* to the backing store for test assertions.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const { layer, storeRef } = yield* makeInMemoryPersistenceAdapter
|
|
35
|
+
* // Use layer to provide PersistenceAdapter
|
|
36
|
+
* // Inspect storeRef for test assertions
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
const makeInMemoryPersistenceAdapter = Effect.gen(function* () {
|
|
40
|
+
const store = /* @__PURE__ */ new Map();
|
|
41
|
+
const storeRef = yield* Ref.make(store);
|
|
42
|
+
const adapter = {
|
|
43
|
+
saveSnapshot: (key, snapshot) => Effect.gen(function* () {
|
|
44
|
+
const entry = getOrCreate(yield* Ref.get(storeRef), makeKey(key));
|
|
45
|
+
if (entry.snapshot !== void 0 && snapshot.version < entry.snapshot.version) return yield* new VersionConflictError({
|
|
46
|
+
expected: snapshot.version,
|
|
47
|
+
actual: entry.snapshot.version
|
|
48
|
+
});
|
|
49
|
+
entry.snapshot = snapshot;
|
|
50
|
+
}),
|
|
51
|
+
loadSnapshot: (key) => Effect.gen(function* () {
|
|
52
|
+
const entry = (yield* Ref.get(storeRef)).get(makeKey(key));
|
|
53
|
+
return Option.fromNullishOr(entry?.snapshot);
|
|
54
|
+
}),
|
|
55
|
+
appendEvents: (key, events, expectedVersion) => Effect.gen(function* () {
|
|
56
|
+
const entry = getOrCreate(yield* Ref.get(storeRef), makeKey(key));
|
|
57
|
+
const lastEvent = entry.events[entry.events.length - 1];
|
|
58
|
+
const currentVersion = lastEvent !== void 0 ? lastEvent.version : 0;
|
|
59
|
+
if (currentVersion !== expectedVersion) return yield* new VersionConflictError({
|
|
60
|
+
expected: expectedVersion,
|
|
61
|
+
actual: currentVersion
|
|
62
|
+
});
|
|
63
|
+
for (const event of events) entry.events.push(event);
|
|
64
|
+
}),
|
|
65
|
+
loadEvents: (key, afterVersion) => Effect.gen(function* () {
|
|
66
|
+
const entry = (yield* Ref.get(storeRef)).get(makeKey(key));
|
|
67
|
+
if (entry === void 0) return [];
|
|
68
|
+
if (afterVersion === void 0) return entry.events;
|
|
69
|
+
return entry.events.filter((e) => e.version > afterVersion);
|
|
70
|
+
})
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
adapter,
|
|
74
|
+
storeRef,
|
|
75
|
+
layer: Layer.succeed(PersistenceAdapter, adapter)
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
//#endregion
|
|
79
|
+
export { makeInMemoryPersistenceAdapter };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ExtractReply, ReplyTypeBrand } from "../internal/brands.js";
|
|
2
|
+
import { ActorStoppedError, NoReplyError } from "../errors.js";
|
|
3
|
+
import { EntityRpcs } from "./to-entity.js";
|
|
4
|
+
import { Effect, Stream } from "effect";
|
|
5
|
+
import { RpcClient } from "effect/unstable/rpc";
|
|
6
|
+
|
|
7
|
+
//#region src/cluster/entity-actor-ref.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Typed client wrapper for remote entity machines.
|
|
10
|
+
*
|
|
11
|
+
* Unlike local `ActorRef`, this communicates over cluster RPCs.
|
|
12
|
+
* Only operations that make sense over the network are exposed.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const ref = makeEntityActorRef(client, "order-123")
|
|
17
|
+
* yield* ref.send(OrderEvent.Ship({ trackingId: "abc" }))
|
|
18
|
+
* const state = yield* ref.snapshot
|
|
19
|
+
* yield* ref.waitFor((s) => s._tag === "Shipped")
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
interface EntityActorRef<State extends {
|
|
23
|
+
readonly _tag: string;
|
|
24
|
+
}, Event extends {
|
|
25
|
+
readonly _tag: string;
|
|
26
|
+
}> {
|
|
27
|
+
readonly entityId: string;
|
|
28
|
+
/** Send event. Returns new state after processing. */
|
|
29
|
+
readonly send: (event: Event) => Effect.Effect<State>;
|
|
30
|
+
/** Send event and get typed domain reply (via Event.reply() schema). */
|
|
31
|
+
readonly ask: <E extends Event & ReplyTypeBrand<unknown>>(event: E) => Effect.Effect<ExtractReply<E>, NoReplyError>;
|
|
32
|
+
/** Get current state. */
|
|
33
|
+
readonly snapshot: Effect.Effect<State>;
|
|
34
|
+
/** Stream of state changes (via WatchState streaming RPC). */
|
|
35
|
+
readonly watch: Stream.Stream<State>;
|
|
36
|
+
/** Wait for a state matching the predicate. Snapshots first, then watches stream. */
|
|
37
|
+
readonly waitFor: (predicate: (state: State) => boolean) => Effect.Effect<State, ActorStoppedError>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create an EntityActorRef from a RPC client.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const makeClient = yield* Entity.makeTestClient(entity, entityLayer)
|
|
45
|
+
* const client = yield* makeClient("order-123")
|
|
46
|
+
* const ref = makeEntityActorRef(client, "order-123")
|
|
47
|
+
* yield* ref.send(OrderEvent.Process)
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
declare const makeEntityActorRef: <State extends {
|
|
51
|
+
readonly _tag: string;
|
|
52
|
+
}, Event extends {
|
|
53
|
+
readonly _tag: string;
|
|
54
|
+
}, Rpcs extends EntityRpcs<any, any>[number]>(client: RpcClient.RpcClient<Rpcs>, entityId: string) => EntityActorRef<State, Event>;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { EntityActorRef, makeEntityActorRef };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ActorStoppedError } from "../errors.js";
|
|
2
|
+
import { Effect, Option, Stream } from "effect";
|
|
3
|
+
//#region src/cluster/entity-actor-ref.ts
|
|
4
|
+
/**
|
|
5
|
+
* Create an EntityActorRef from a RPC client.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const makeClient = yield* Entity.makeTestClient(entity, entityLayer)
|
|
10
|
+
* const client = yield* makeClient("order-123")
|
|
11
|
+
* const ref = makeEntityActorRef(client, "order-123")
|
|
12
|
+
* yield* ref.send(OrderEvent.Process)
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
const makeEntityActorRef = (client, entityId) => {
|
|
16
|
+
const c = client;
|
|
17
|
+
return {
|
|
18
|
+
entityId,
|
|
19
|
+
send: (event) => c.Send({ event }),
|
|
20
|
+
ask: ((event) => c.Ask({ event })),
|
|
21
|
+
snapshot: c.GetState(),
|
|
22
|
+
watch: c.WatchState(),
|
|
23
|
+
waitFor: (predicate) => Effect.gen(function* () {
|
|
24
|
+
const current = yield* c.GetState();
|
|
25
|
+
if (predicate(current)) return current;
|
|
26
|
+
const result = yield* c.WatchState().pipe(Stream.filter(predicate), Stream.take(1), Stream.runHead);
|
|
27
|
+
if (Option.isSome(result)) return result.value;
|
|
28
|
+
return yield* new ActorStoppedError({ actorId: entityId });
|
|
29
|
+
})
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
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 {
|
|
3
|
+
import { EntityPersistenceConfig } from "./persistence.js";
|
|
4
|
+
import { Duration, Layer, Schedule } from "effect";
|
|
5
5
|
import { Entity } from "effect/unstable/cluster";
|
|
6
6
|
import { Rpc } from "effect/unstable/rpc";
|
|
7
7
|
|
|
@@ -13,77 +13,59 @@ 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.toLayerQueue.
|
|
25
|
+
*/
|
|
26
|
+
readonly maxIdleTime?: Duration.Input;
|
|
27
|
+
/**
|
|
28
|
+
* Mailbox capacity. Default: "unbounded".
|
|
29
|
+
* Forwarded to Entity.toLayerQueue.
|
|
30
|
+
*/
|
|
31
|
+
readonly mailboxCapacity?: number | "unbounded";
|
|
32
|
+
/**
|
|
33
|
+
* Disable fatal defects (defects won't crash the entity activation).
|
|
34
|
+
* Forwarded to Entity.toLayerQueue.
|
|
35
|
+
*/
|
|
36
|
+
readonly disableFatalDefects?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Retry policy for defects (schedule for restarting after defect).
|
|
39
|
+
* Forwarded to Entity.toLayerQueue.
|
|
40
|
+
*/
|
|
41
|
+
readonly defectRetryPolicy?: Schedule.Schedule<any, unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* Persistence configuration. When set, requires PersistenceAdapter in R.
|
|
44
|
+
*/
|
|
45
|
+
readonly persistence?: EntityPersistenceConfig;
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
46
48
|
* Create an Entity layer that wires a machine to handle RPC calls.
|
|
47
49
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* - Evaluates guards in registration order
|
|
52
|
-
* - Runs lifecycle effects (onEnter/spawn)
|
|
53
|
-
* - Processes internal events from spawn effects
|
|
50
|
+
* Uses `Entity.toLayerQueue` for a single serialized mailbox per entity.
|
|
51
|
+
* The runtime kernel handles event processing, postpone, background effects,
|
|
52
|
+
* spawn effects, and final state detection.
|
|
54
53
|
*
|
|
55
54
|
* @example
|
|
56
55
|
* ```ts
|
|
57
|
-
* const OrderEntity = toEntity(orderMachine, {
|
|
58
|
-
* type: "Order",
|
|
59
|
-
* stateSchema: OrderState,
|
|
60
|
-
* eventSchema: OrderEvent,
|
|
61
|
-
* })
|
|
56
|
+
* const OrderEntity = toEntity(orderMachine, { type: "Order" })
|
|
62
57
|
*
|
|
63
58
|
* const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine, {
|
|
64
59
|
* initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
|
|
65
60
|
* })
|
|
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
61
|
* ```
|
|
73
62
|
*/
|
|
74
63
|
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
64
|
layer: <S extends {
|
|
83
65
|
readonly _tag: string;
|
|
84
66
|
}, E extends {
|
|
85
67
|
readonly _tag: string;
|
|
86
|
-
}, R,
|
|
68
|
+
}, 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
69
|
};
|
|
88
70
|
//#endregion
|
|
89
71
|
export { EntityMachine, EntityMachineOptions };
|