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/v3/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, 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";
|
|
10
7
|
//#region src/actor.ts
|
|
11
8
|
/**
|
|
12
9
|
* Actor system: spawning, lifecycle, and event processing.
|
|
@@ -31,7 +28,7 @@ const notifyListeners = (listeners, state) => {
|
|
|
31
28
|
/**
|
|
32
29
|
* Build core ActorRef methods shared between regular and persistent actors.
|
|
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, {
|
|
@@ -121,6 +118,7 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
121
118
|
matches,
|
|
122
119
|
can,
|
|
123
120
|
changes: stateRef.changes,
|
|
121
|
+
transitions: transitionsPubSub !== void 0 ? Stream.fromPubSub(transitionsPubSub) : Stream.empty,
|
|
124
122
|
waitFor,
|
|
125
123
|
awaitFinal,
|
|
126
124
|
sendAndWait,
|
|
@@ -151,7 +149,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
151
149
|
/**
|
|
152
150
|
* Create and start an actor for a machine
|
|
153
151
|
*/
|
|
154
|
-
const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine) {
|
|
152
|
+
const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
|
|
153
|
+
const initial = options?.initialState ?? machine.initial;
|
|
155
154
|
yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
|
|
156
155
|
const existingSystem = yield* Effect.serviceOption(ActorSystem);
|
|
157
156
|
let system;
|
|
@@ -186,20 +185,20 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
186
185
|
return child;
|
|
187
186
|
})
|
|
188
187
|
};
|
|
189
|
-
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state",
|
|
188
|
+
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
|
|
190
189
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
191
190
|
type: "@machine.spawn",
|
|
192
191
|
actorId: id,
|
|
193
|
-
initialState:
|
|
192
|
+
initialState: initial,
|
|
194
193
|
timestamp
|
|
195
194
|
}));
|
|
196
|
-
const stateRef = yield* SubscriptionRef.make(
|
|
195
|
+
const stateRef = yield* SubscriptionRef.make(initial);
|
|
197
196
|
const listeners = /* @__PURE__ */ new Set();
|
|
198
197
|
const backgroundFibers = [];
|
|
199
198
|
const initEvent = { _tag: INTERNAL_INIT_EVENT };
|
|
200
199
|
const ctx = {
|
|
201
200
|
actorId: id,
|
|
202
|
-
state:
|
|
201
|
+
state: initial,
|
|
203
202
|
event: initEvent,
|
|
204
203
|
self,
|
|
205
204
|
system
|
|
@@ -208,7 +207,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
208
207
|
for (const bg of machine.backgroundEffects) {
|
|
209
208
|
const fiber = yield* Effect.forkDaemon(bg.handler({
|
|
210
209
|
actorId: id,
|
|
211
|
-
state:
|
|
210
|
+
state: initial,
|
|
212
211
|
event: initEvent,
|
|
213
212
|
self,
|
|
214
213
|
effects: effectSlots,
|
|
@@ -217,14 +216,14 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
217
216
|
backgroundFibers.push(fiber);
|
|
218
217
|
}
|
|
219
218
|
const stateScopeRef = { current: yield* Scope.make() };
|
|
220
|
-
yield* runSpawnEffectsWithInspection(machine,
|
|
221
|
-
if (machine.finalStates.has(
|
|
219
|
+
yield* runSpawnEffectsWithInspection(machine, initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
|
|
220
|
+
if (machine.finalStates.has(initial._tag)) {
|
|
222
221
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
223
222
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
224
223
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
225
224
|
type: "@machine.stop",
|
|
226
225
|
actorId: id,
|
|
227
|
-
finalState:
|
|
226
|
+
finalState: initial,
|
|
228
227
|
timestamp
|
|
229
228
|
}));
|
|
230
229
|
yield* Ref.set(stoppedRef, true);
|
|
@@ -232,7 +231,8 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
232
231
|
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());
|
|
233
232
|
}
|
|
234
233
|
const pendingReplies = /* @__PURE__ */ new Set();
|
|
235
|
-
const
|
|
234
|
+
const transitionsPubSub = yield* PubSub.unbounded();
|
|
235
|
+
const loopFiber = yield* Effect.forkDaemon(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies, transitionsPubSub));
|
|
236
236
|
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
|
|
237
237
|
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
238
238
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
@@ -247,7 +247,7 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
247
247
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
248
248
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
249
249
|
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
250
|
-
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies);
|
|
250
|
+
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub);
|
|
251
251
|
});
|
|
252
252
|
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
253
253
|
const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
|
|
@@ -260,7 +260,7 @@ const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
|
|
|
260
260
|
* Includes postpone buffer — events matching postpone rules are buffered
|
|
261
261
|
* and drained after state tag changes (gen_statem semantics).
|
|
262
262
|
*/
|
|
263
|
-
const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies) {
|
|
263
|
+
const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies, transitionsPubSub) {
|
|
264
264
|
const postponed = [];
|
|
265
265
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
266
266
|
const processQueued = Effect.fn("effect-machine.actor.processQueued")(function* (queued) {
|
|
@@ -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,13 +297,29 @@ 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)
|
|
300
|
-
|
|
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
|
}));
|
|
304
316
|
break;
|
|
305
317
|
}
|
|
318
|
+
if (result.transitioned) yield* PubSub.publish(transitionsPubSub, {
|
|
319
|
+
fromState: result.previousState,
|
|
320
|
+
toState: result.newState,
|
|
321
|
+
event
|
|
322
|
+
});
|
|
306
323
|
return {
|
|
307
324
|
shouldStop,
|
|
308
325
|
stateChanged: result.lifecycleRan
|
|
@@ -318,15 +335,21 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
|
|
|
318
335
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
319
336
|
return;
|
|
320
337
|
}
|
|
321
|
-
|
|
338
|
+
let drainTriggered = stateChanged;
|
|
339
|
+
while (drainTriggered && postponed.length > 0) {
|
|
340
|
+
drainTriggered = false;
|
|
322
341
|
const drained = postponed.splice(0);
|
|
323
|
-
for (const entry of drained)
|
|
324
|
-
yield*
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
342
|
+
for (const entry of drained) {
|
|
343
|
+
const drain = yield* processQueued(entry);
|
|
344
|
+
if (drain.shouldStop) {
|
|
345
|
+
yield* Ref.set(stoppedRef, true);
|
|
346
|
+
settlePostponedBuffer(postponed, pendingReplies, actorId);
|
|
347
|
+
yield* settlePendingReplies(pendingReplies, actorId);
|
|
348
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
349
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (drain.stateChanged) drainTriggered = true;
|
|
330
353
|
}
|
|
331
354
|
}
|
|
332
355
|
}
|
|
@@ -482,25 +505,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
482
505
|
if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
|
|
483
506
|
return yield* registerActor(id, yield* createActor(id, built._inner));
|
|
484
507
|
});
|
|
485
|
-
const
|
|
486
|
-
if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
|
|
487
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
488
|
-
const maybeSnapshot = yield* adapter.loadSnapshot(id, persistentMachine.persistence.stateSchema);
|
|
489
|
-
return yield* registerActor(id, yield* createPersistentActor(id, persistentMachine, maybeSnapshot, yield* adapter.loadEvents(id, persistentMachine.persistence.eventSchema, Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : void 0)));
|
|
490
|
-
});
|
|
491
|
-
const spawnImpl = Effect.fn("effect-machine.actorSystem.spawn")(function* (id, machine) {
|
|
492
|
-
if (isPersistentMachine(machine)) return yield* spawnPersistent(id, machine);
|
|
493
|
-
return yield* spawnRegular(id, machine);
|
|
494
|
-
});
|
|
495
|
-
function spawn(id, machine) {
|
|
496
|
-
return withSpawnGate(spawnImpl(id, machine));
|
|
497
|
-
}
|
|
498
|
-
const restoreImpl = Effect.fn("effect-machine.actorSystem.restore")(function* (id, persistentMachine) {
|
|
499
|
-
const maybeActor = yield* restorePersistentActor(id, persistentMachine);
|
|
500
|
-
if (Option.isSome(maybeActor)) yield* registerActor(id, maybeActor.value);
|
|
501
|
-
return maybeActor;
|
|
502
|
-
});
|
|
503
|
-
const restore = (id, persistentMachine) => withSpawnGate(restoreImpl(id, persistentMachine));
|
|
508
|
+
const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
|
|
504
509
|
const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
|
|
505
510
|
return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
|
|
506
511
|
});
|
|
@@ -517,55 +522,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
517
522
|
yield* actor.stop;
|
|
518
523
|
return true;
|
|
519
524
|
});
|
|
520
|
-
const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
|
|
521
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
522
|
-
if (adapter.listActors === void 0) return [];
|
|
523
|
-
return yield* adapter.listActors();
|
|
524
|
-
});
|
|
525
|
-
const restoreMany = Effect.fn("effect-machine.actorSystem.restoreMany")(function* (ids, persistentMachine) {
|
|
526
|
-
const restored = [];
|
|
527
|
-
const failed = [];
|
|
528
|
-
for (const id of ids) {
|
|
529
|
-
if (MutableHashMap.has(actorsMap, id)) continue;
|
|
530
|
-
const result = yield* Effect.either(restore(id, persistentMachine));
|
|
531
|
-
if (result._tag === "Left") failed.push({
|
|
532
|
-
id,
|
|
533
|
-
error: result.left
|
|
534
|
-
});
|
|
535
|
-
else if (Option.isSome(result.right)) restored.push(result.right.value);
|
|
536
|
-
else failed.push({
|
|
537
|
-
id,
|
|
538
|
-
error: new PersistenceError({
|
|
539
|
-
operation: "restore",
|
|
540
|
-
actorId: id,
|
|
541
|
-
message: "No persisted state found"
|
|
542
|
-
})
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
return {
|
|
546
|
-
restored,
|
|
547
|
-
failed
|
|
548
|
-
};
|
|
549
|
-
});
|
|
550
|
-
const restoreAll = Effect.fn("effect-machine.actorSystem.restoreAll")(function* (persistentMachine, options) {
|
|
551
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
552
|
-
if (adapter.listActors === void 0) return {
|
|
553
|
-
restored: [],
|
|
554
|
-
failed: []
|
|
555
|
-
};
|
|
556
|
-
const machineType = persistentMachine.persistence.machineType;
|
|
557
|
-
if (machineType === void 0) return yield* new PersistenceError({
|
|
558
|
-
operation: "restoreAll",
|
|
559
|
-
actorId: "*",
|
|
560
|
-
message: "restoreAll requires explicit machineType in persistence config"
|
|
561
|
-
});
|
|
562
|
-
let filtered = (yield* adapter.listActors()).filter((meta) => meta.machineType === machineType);
|
|
563
|
-
if (options?.filter !== void 0) filtered = filtered.filter(options.filter);
|
|
564
|
-
return yield* restoreMany(filtered.map((meta) => meta.id), persistentMachine);
|
|
565
|
-
});
|
|
566
525
|
return ActorSystem.of({
|
|
567
526
|
spawn,
|
|
568
|
-
restore,
|
|
569
527
|
get,
|
|
570
528
|
stop,
|
|
571
529
|
events: Stream.fromPubSub(eventPubSub),
|
|
@@ -581,10 +539,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
581
539
|
return () => {
|
|
582
540
|
eventListeners.delete(fn);
|
|
583
541
|
};
|
|
584
|
-
}
|
|
585
|
-
listPersisted,
|
|
586
|
-
restoreMany,
|
|
587
|
-
restoreAll
|
|
542
|
+
}
|
|
588
543
|
});
|
|
589
544
|
});
|
|
590
545
|
/**
|
|
@@ -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 {
|
|
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
|
-
*
|
|
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,
|
|
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 };
|