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.
- package/README.md +65 -62
- package/dist/actor.d.ts +35 -97
- package/dist/actor.js +69 -98
- 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 +5 -8
- package/dist/index.js +1 -6
- 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 +6 -1
- package/dist/internal/transition.js +16 -4
- package/dist/internal/utils.d.ts +42 -6
- package/dist/internal/utils.js +28 -6
- package/dist/machine.d.ts +55 -46
- package/dist/machine.js +74 -13
- package/dist/schema.d.ts +35 -34
- package/dist/schema.js +32 -3
- package/dist/testing.js +4 -2
- package/package.json +4 -4
- package/v3/dist/actor.d.ts +29 -96
- package/v3/dist/actor.js +52 -97
- 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 +5 -8
- package/v3/dist/index.js +1 -6
- 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 +28 -6
- package/v3/dist/machine.d.ts +48 -46
- package/v3/dist/machine.js +71 -13
- package/v3/dist/schema.d.ts +35 -34
- package/v3/dist/schema.js +29 -3
- package/dist/persistence/adapter.d.ts +0 -135
- package/dist/persistence/adapter.js +0 -25
- package/dist/persistence/adapters/in-memory.d.ts +0 -32
- package/dist/persistence/adapters/in-memory.js +0 -174
- package/dist/persistence/index.d.ts +0 -5
- package/dist/persistence/index.js +0 -5
- package/dist/persistence/persistent-actor.d.ts +0 -50
- package/dist/persistence/persistent-actor.js +0 -404
- package/dist/persistence/persistent-machine.d.ts +0 -105
- package/dist/persistence/persistent-machine.js +0 -22
- package/v3/dist/persistence/adapter.d.ts +0 -138
- package/v3/dist/persistence/adapter.js +0 -25
- package/v3/dist/persistence/adapters/in-memory.d.ts +0 -32
- package/v3/dist/persistence/adapters/in-memory.js +0 -174
- package/v3/dist/persistence/index.d.ts +0 -5
- package/v3/dist/persistence/index.js +0 -5
- package/v3/dist/persistence/persistent-actor.d.ts +0 -50
- package/v3/dist/persistence/persistent-actor.js +0 -404
- package/v3/dist/persistence/persistent-machine.d.ts +0 -105
- package/v3/dist/persistence/persistent-machine.js +0 -22
package/dist/actor.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { Inspector } from "./inspection.js";
|
|
2
2
|
import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
|
|
3
3
|
import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
|
|
4
|
-
import { isPersistentMachine } from "./persistence/persistent-machine.js";
|
|
5
4
|
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
6
5
|
import { processEventCore, resolveTransition, runSpawnEffects, shouldPostpone } from "./internal/transition.js";
|
|
7
|
-
import {
|
|
8
|
-
import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
|
|
9
|
-
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";
|
|
10
7
|
//#region src/actor.ts
|
|
11
8
|
/**
|
|
12
9
|
* Actor system: spawning, lifecycle, and event processing.
|
|
@@ -29,9 +26,9 @@ const notifyListeners = (listeners, state) => {
|
|
|
29
26
|
} catch {}
|
|
30
27
|
};
|
|
31
28
|
/**
|
|
32
|
-
* Build core ActorRef methods
|
|
29
|
+
* Build core ActorRef methods.
|
|
33
30
|
*/
|
|
34
|
-
const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies) => {
|
|
31
|
+
const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies, transitionsPubSub) => {
|
|
35
32
|
const send = Effect.fn("effect-machine.actor.send")(function* (event) {
|
|
36
33
|
if (yield* Ref.get(stoppedRef)) return;
|
|
37
34
|
yield* Queue.offer(eventQueue, {
|
|
@@ -119,6 +116,7 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
119
116
|
matches,
|
|
120
117
|
can,
|
|
121
118
|
changes: SubscriptionRef.changes(stateRef),
|
|
119
|
+
transitions: transitionsPubSub !== void 0 ? Stream.fromPubSub(transitionsPubSub) : Stream.empty,
|
|
122
120
|
waitFor,
|
|
123
121
|
awaitFinal,
|
|
124
122
|
sendAndWait,
|
|
@@ -149,7 +147,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
149
147
|
/**
|
|
150
148
|
* Create and start an actor for a machine
|
|
151
149
|
*/
|
|
152
|
-
const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine) {
|
|
150
|
+
const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
|
|
151
|
+
const initial = options?.initialState ?? machine.initial;
|
|
153
152
|
yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
|
|
154
153
|
const existingSystem = yield* Effect.serviceOption(ActorSystem);
|
|
155
154
|
let system;
|
|
@@ -164,6 +163,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
164
163
|
const eventQueue = yield* Queue.unbounded();
|
|
165
164
|
const stoppedRef = yield* Ref.make(false);
|
|
166
165
|
const childrenMap = /* @__PURE__ */ new Map();
|
|
166
|
+
const deferredReplyRef = { current: void 0 };
|
|
167
167
|
const selfSend = Effect.fn("effect-machine.actor.self.send")(function* (event) {
|
|
168
168
|
if (yield* Ref.get(stoppedRef)) return;
|
|
169
169
|
yield* Queue.offer(eventQueue, {
|
|
@@ -182,22 +182,31 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
182
182
|
childrenMap.delete(childId);
|
|
183
183
|
}));
|
|
184
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;
|
|
185
194
|
})
|
|
186
195
|
};
|
|
187
|
-
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state",
|
|
196
|
+
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
|
|
188
197
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
189
198
|
type: "@machine.spawn",
|
|
190
199
|
actorId: id,
|
|
191
|
-
initialState:
|
|
200
|
+
initialState: initial,
|
|
192
201
|
timestamp
|
|
193
202
|
}));
|
|
194
|
-
const stateRef = yield* SubscriptionRef.make(
|
|
203
|
+
const stateRef = yield* SubscriptionRef.make(initial);
|
|
195
204
|
const listeners = /* @__PURE__ */ new Set();
|
|
196
205
|
const backgroundFibers = [];
|
|
197
206
|
const initEvent = { _tag: INTERNAL_INIT_EVENT };
|
|
198
207
|
const ctx = {
|
|
199
208
|
actorId: id,
|
|
200
|
-
state:
|
|
209
|
+
state: initial,
|
|
201
210
|
event: initEvent,
|
|
202
211
|
self,
|
|
203
212
|
system
|
|
@@ -206,7 +215,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
206
215
|
for (const bg of machine.backgroundEffects) {
|
|
207
216
|
const fiber = yield* Effect.forkDetach(bg.handler({
|
|
208
217
|
actorId: id,
|
|
209
|
-
state:
|
|
218
|
+
state: initial,
|
|
210
219
|
event: initEvent,
|
|
211
220
|
self,
|
|
212
221
|
effects: effectSlots,
|
|
@@ -215,14 +224,14 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
215
224
|
backgroundFibers.push(fiber);
|
|
216
225
|
}
|
|
217
226
|
const stateScopeRef = { current: yield* Scope.make() };
|
|
218
|
-
yield* runSpawnEffectsWithInspection(machine,
|
|
219
|
-
if (machine.finalStates.has(
|
|
227
|
+
yield* runSpawnEffectsWithInspection(machine, initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
|
|
228
|
+
if (machine.finalStates.has(initial._tag)) {
|
|
220
229
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
221
230
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
222
231
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
223
232
|
type: "@machine.stop",
|
|
224
233
|
actorId: id,
|
|
225
|
-
finalState:
|
|
234
|
+
finalState: initial,
|
|
226
235
|
timestamp
|
|
227
236
|
}));
|
|
228
237
|
yield* Ref.set(stoppedRef, true);
|
|
@@ -230,7 +239,8 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
230
239
|
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, /* @__PURE__ */ new Set());
|
|
231
240
|
}
|
|
232
241
|
const pendingReplies = /* @__PURE__ */ new Set();
|
|
233
|
-
const
|
|
242
|
+
const transitionsPubSub = yield* PubSub.unbounded();
|
|
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) => ({
|
|
@@ -245,7 +255,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
245
255
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
246
256
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
247
257
|
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
248
|
-
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies);
|
|
258
|
+
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub);
|
|
249
259
|
});
|
|
250
260
|
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
251
261
|
const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
|
|
@@ -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) {
|
|
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,13 +305,30 @@ 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
|
|
301
324
|
}));
|
|
302
325
|
break;
|
|
303
326
|
}
|
|
327
|
+
if (result.transitioned) yield* PubSub.publish(transitionsPubSub, {
|
|
328
|
+
fromState: result.previousState,
|
|
329
|
+
toState: result.newState,
|
|
330
|
+
event
|
|
331
|
+
});
|
|
304
332
|
return {
|
|
305
333
|
shouldStop,
|
|
306
334
|
stateChanged: result.lifecycleRan
|
|
@@ -316,15 +344,21 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
|
|
|
316
344
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
317
345
|
return;
|
|
318
346
|
}
|
|
319
|
-
|
|
347
|
+
let drainTriggered = stateChanged;
|
|
348
|
+
while (drainTriggered && postponed.length > 0) {
|
|
349
|
+
drainTriggered = false;
|
|
320
350
|
const drained = postponed.splice(0);
|
|
321
|
-
for (const entry of drained)
|
|
322
|
-
yield*
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
351
|
+
for (const entry of drained) {
|
|
352
|
+
const drain = yield* processQueued(entry);
|
|
353
|
+
if (drain.shouldStop) {
|
|
354
|
+
yield* Ref.set(stoppedRef, true);
|
|
355
|
+
settlePostponedBuffer(postponed, pendingReplies, actorId);
|
|
356
|
+
yield* settlePendingReplies(pendingReplies, actorId);
|
|
357
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
358
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (drain.stateChanged) drainTriggered = true;
|
|
328
362
|
}
|
|
329
363
|
}
|
|
330
364
|
}
|
|
@@ -480,25 +514,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
480
514
|
if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
|
|
481
515
|
return yield* registerActor(id, yield* createActor(id, built._inner));
|
|
482
516
|
});
|
|
483
|
-
const
|
|
484
|
-
if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
|
|
485
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
486
|
-
const maybeSnapshot = yield* adapter.loadSnapshot(id, persistentMachine.persistence.stateSchema);
|
|
487
|
-
return yield* registerActor(id, yield* createPersistentActor(id, persistentMachine, maybeSnapshot, yield* adapter.loadEvents(id, persistentMachine.persistence.eventSchema, Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : void 0)));
|
|
488
|
-
});
|
|
489
|
-
const spawnImpl = Effect.fn("effect-machine.actorSystem.spawn")(function* (id, machine) {
|
|
490
|
-
if (isPersistentMachine(machine)) return yield* spawnPersistent(id, machine);
|
|
491
|
-
return yield* spawnRegular(id, machine);
|
|
492
|
-
});
|
|
493
|
-
function spawn(id, machine) {
|
|
494
|
-
return withSpawnGate(spawnImpl(id, machine));
|
|
495
|
-
}
|
|
496
|
-
const restoreImpl = Effect.fn("effect-machine.actorSystem.restore")(function* (id, persistentMachine) {
|
|
497
|
-
const maybeActor = yield* restorePersistentActor(id, persistentMachine);
|
|
498
|
-
if (Option.isSome(maybeActor)) yield* registerActor(id, maybeActor.value);
|
|
499
|
-
return maybeActor;
|
|
500
|
-
});
|
|
501
|
-
const restore = (id, persistentMachine) => withSpawnGate(restoreImpl(id, persistentMachine));
|
|
517
|
+
const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
|
|
502
518
|
const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
|
|
503
519
|
return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
|
|
504
520
|
});
|
|
@@ -515,55 +531,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
515
531
|
yield* actor.stop;
|
|
516
532
|
return true;
|
|
517
533
|
});
|
|
518
|
-
const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
|
|
519
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
520
|
-
if (adapter.listActors === void 0) return [];
|
|
521
|
-
return yield* adapter.listActors();
|
|
522
|
-
});
|
|
523
|
-
const restoreMany = Effect.fn("effect-machine.actorSystem.restoreMany")(function* (ids, persistentMachine) {
|
|
524
|
-
const restored = [];
|
|
525
|
-
const failed = [];
|
|
526
|
-
for (const id of ids) {
|
|
527
|
-
if (MutableHashMap.has(actorsMap, id)) continue;
|
|
528
|
-
const result = yield* Effect.result(restore(id, persistentMachine));
|
|
529
|
-
if (result._tag === "Failure") failed.push({
|
|
530
|
-
id,
|
|
531
|
-
error: result.failure
|
|
532
|
-
});
|
|
533
|
-
else if (Option.isSome(result.success)) restored.push(result.success.value);
|
|
534
|
-
else failed.push({
|
|
535
|
-
id,
|
|
536
|
-
error: new PersistenceError({
|
|
537
|
-
operation: "restore",
|
|
538
|
-
actorId: id,
|
|
539
|
-
message: "No persisted state found"
|
|
540
|
-
})
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
return {
|
|
544
|
-
restored,
|
|
545
|
-
failed
|
|
546
|
-
};
|
|
547
|
-
});
|
|
548
|
-
const restoreAll = Effect.fn("effect-machine.actorSystem.restoreAll")(function* (persistentMachine, options) {
|
|
549
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
550
|
-
if (adapter.listActors === void 0) return {
|
|
551
|
-
restored: [],
|
|
552
|
-
failed: []
|
|
553
|
-
};
|
|
554
|
-
const machineType = persistentMachine.persistence.machineType;
|
|
555
|
-
if (machineType === void 0) return yield* new PersistenceError({
|
|
556
|
-
operation: "restoreAll",
|
|
557
|
-
actorId: "*",
|
|
558
|
-
message: "restoreAll requires explicit machineType in persistence config"
|
|
559
|
-
});
|
|
560
|
-
let filtered = (yield* adapter.listActors()).filter((meta) => meta.machineType === machineType);
|
|
561
|
-
if (options?.filter !== void 0) filtered = filtered.filter(options.filter);
|
|
562
|
-
return yield* restoreMany(filtered.map((meta) => meta.id), persistentMachine);
|
|
563
|
-
});
|
|
564
534
|
return ActorSystem.of({
|
|
565
535
|
spawn,
|
|
566
|
-
restore,
|
|
567
536
|
get,
|
|
568
537
|
stop,
|
|
569
538
|
events: Stream.fromPubSub(eventPubSub),
|
|
@@ -579,15 +548,17 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
579
548
|
return () => {
|
|
580
549
|
eventListeners.delete(fn);
|
|
581
550
|
};
|
|
582
|
-
}
|
|
583
|
-
listPersisted,
|
|
584
|
-
restoreMany,
|
|
585
|
-
restoreAll
|
|
551
|
+
}
|
|
586
552
|
});
|
|
587
553
|
});
|
|
588
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
|
+
/**
|
|
589
560
|
* Default ActorSystem layer
|
|
590
561
|
*/
|
|
591
562
|
const Default = Layer.effect(ActorSystem, make());
|
|
592
563
|
//#endregion
|
|
593
|
-
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 };
|