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
|
@@ -1,80 +1,195 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ActorSystem } from "../actor.js";
|
|
3
|
-
import {
|
|
1
|
+
import { BuiltMachine, replay } from "../machine.js";
|
|
2
|
+
import { ActorSystem, makeSystem } from "../actor.js";
|
|
3
|
+
import { createRuntime } from "../internal/runtime.js";
|
|
4
|
+
import { PersistenceAdapter } from "./persistence.js";
|
|
5
|
+
import { Effect, Option, Queue, Ref, Stream, SubscriptionRef } from "effect";
|
|
4
6
|
import { Entity } from "effect/unstable/cluster";
|
|
5
7
|
//#region src/cluster/entity-machine.ts
|
|
6
8
|
/**
|
|
7
9
|
* EntityMachine adapter - wires a machine to a cluster Entity layer.
|
|
8
10
|
*
|
|
11
|
+
* Uses Entity.toLayerQueue for a single serialized mailbox per entity.
|
|
12
|
+
* All events (external RPCs + internal self.send) go through the
|
|
13
|
+
* runtime kernel's single queue — no split-mailbox race.
|
|
14
|
+
*
|
|
15
|
+
* Supports opt-in persistence (snapshot or journal strategy) for
|
|
16
|
+
* state survival across entity deactivation/reactivation cycles.
|
|
17
|
+
*
|
|
9
18
|
* @module
|
|
10
19
|
*/
|
|
11
20
|
/**
|
|
12
|
-
* Process a single event through the machine using shared core.
|
|
13
|
-
* Returns the new state after processing.
|
|
14
|
-
*/
|
|
15
|
-
const processEvent = Effect.fn("effect-machine.cluster.processEvent")(function* (machine, stateRef, event, self, stateScopeRef, system, hooks) {
|
|
16
|
-
const result = yield* processEventCore(machine, yield* Ref.get(stateRef), event, self, stateScopeRef, system, "*", hooks);
|
|
17
|
-
if (result.transitioned) yield* Ref.set(stateRef, result.newState);
|
|
18
|
-
return result.newState;
|
|
19
|
-
});
|
|
20
|
-
/**
|
|
21
21
|
* Create an Entity layer that wires a machine to handle RPC calls.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* - Evaluates guards in registration order
|
|
27
|
-
* - Runs lifecycle effects (onEnter/spawn)
|
|
28
|
-
* - Processes internal events from spawn effects
|
|
23
|
+
* Uses `Entity.toLayerQueue` for a single serialized mailbox per entity.
|
|
24
|
+
* The runtime kernel handles event processing, postpone, background effects,
|
|
25
|
+
* spawn effects, and final state detection.
|
|
29
26
|
*
|
|
30
27
|
* @example
|
|
31
28
|
* ```ts
|
|
32
|
-
* const OrderEntity = toEntity(orderMachine, {
|
|
33
|
-
* type: "Order",
|
|
34
|
-
* stateSchema: OrderState,
|
|
35
|
-
* eventSchema: OrderEvent,
|
|
36
|
-
* })
|
|
29
|
+
* const OrderEntity = toEntity(orderMachine, { type: "Order" })
|
|
37
30
|
*
|
|
38
31
|
* const OrderEntityLayer = EntityMachine.layer(OrderEntity, orderMachine, {
|
|
39
32
|
* initializeState: (entityId) => OrderState.Pending({ orderId: entityId }),
|
|
40
33
|
* })
|
|
41
|
-
*
|
|
42
|
-
* // Use in cluster
|
|
43
|
-
* const program = Effect.gen(function* () {
|
|
44
|
-
* const client = yield* ShardingClient.client(OrderEntity)
|
|
45
|
-
* yield* client.Send("order-123", { event: OrderEvent.Ship({ trackingId: "abc" }) })
|
|
46
|
-
* })
|
|
47
34
|
* ```
|
|
48
35
|
*/
|
|
49
36
|
const EntityMachine = { layer: (entity, machine, options) => {
|
|
50
|
-
const
|
|
37
|
+
const persistence = options?.persistence;
|
|
38
|
+
const build = Effect.gen(function* () {
|
|
51
39
|
const entityId = yield* Effect.serviceOption(Entity.CurrentAddress).pipe(Effect.map((opt) => opt._tag === "Some" ? opt.value.entityId : ""));
|
|
52
|
-
const initialState = options?.initializeState !== void 0 ? options.initializeState(entityId) : machine.initial;
|
|
53
40
|
const existingSystem = yield* Effect.serviceOption(ActorSystem);
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
41
|
+
const system = Option.isSome(existingSystem) ? existingSystem.value : yield* makeSystem();
|
|
42
|
+
const persistCtx = yield* hydratePersistence(persistence, entity, entityId, machine, options?.initializeState);
|
|
43
|
+
const initialState = persistCtx.hydratedState ?? (options?.initializeState !== void 0 ? options.initializeState(entityId) : void 0);
|
|
44
|
+
const machineWithState = initialState !== void 0 ? Object.create(machine, { initial: {
|
|
45
|
+
value: initialState,
|
|
46
|
+
enumerable: true
|
|
47
|
+
} }) : machine;
|
|
48
|
+
const versionRef = yield* Ref.make(persistCtx.initialVersion);
|
|
49
|
+
const runtime = yield* createRuntime(machineWithState, system, {
|
|
50
|
+
actorId: entityId,
|
|
51
|
+
hooks: options?.hooks
|
|
59
52
|
});
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
53
|
+
if (persistCtx.adapter !== void 0) {
|
|
54
|
+
const { adapter: pAdapter, key } = persistCtx;
|
|
55
|
+
const strategy = persistence?.strategy ?? "snapshot";
|
|
56
|
+
const schedule = persistence?.snapshotSchedule;
|
|
57
|
+
if (strategy === "snapshot") yield* SubscriptionRef.changes(runtime.stateRef).pipe(schedule !== void 0 ? Stream.schedule(schedule) : (s) => s, Stream.runForEach((state) => Effect.gen(function* () {
|
|
58
|
+
const version = yield* Ref.get(versionRef);
|
|
59
|
+
yield* pAdapter.saveSnapshot(key, {
|
|
60
|
+
state,
|
|
61
|
+
version,
|
|
62
|
+
timestamp: Date.now()
|
|
63
|
+
});
|
|
64
|
+
}).pipe(Effect.catch(() => Effect.void))), Effect.forkScoped);
|
|
65
|
+
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
|
66
|
+
const state = yield* SubscriptionRef.get(runtime.stateRef);
|
|
67
|
+
const version = yield* Ref.get(versionRef);
|
|
68
|
+
yield* pAdapter.saveSnapshot(key, {
|
|
69
|
+
state,
|
|
70
|
+
version,
|
|
71
|
+
timestamp: Date.now()
|
|
72
|
+
});
|
|
73
|
+
}).pipe(Effect.catch(() => Effect.void)));
|
|
74
|
+
}
|
|
75
|
+
return (mailbox, replier) => Effect.gen(function* () {
|
|
76
|
+
const hasPersistence = persistCtx.adapter !== void 0;
|
|
77
|
+
const journalCtx = hasPersistence && (persistence?.strategy ?? "snapshot") === "journal" ? {
|
|
78
|
+
adapter: persistCtx.adapter,
|
|
79
|
+
key: persistCtx.key
|
|
80
|
+
} : void 0;
|
|
81
|
+
while (true) {
|
|
82
|
+
const request = yield* Queue.take(mailbox);
|
|
83
|
+
switch (request.tag) {
|
|
84
|
+
case "Send": {
|
|
85
|
+
const event = request.payload.event;
|
|
86
|
+
yield* runtime.sendWait(event).pipe(Effect.orDie);
|
|
87
|
+
if (journalCtx !== void 0) yield* persistEvent(journalCtx.adapter, journalCtx.key, versionRef, event);
|
|
88
|
+
else if (hasPersistence) yield* Ref.update(versionRef, (v) => v + 1);
|
|
89
|
+
const state = yield* runtime.getState;
|
|
90
|
+
yield* replier.succeed(request, state);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "Ask": {
|
|
94
|
+
const event = request.payload.event;
|
|
95
|
+
const reply = yield* runtime.ask(event);
|
|
96
|
+
if (journalCtx !== void 0) yield* persistEvent(journalCtx.adapter, journalCtx.key, versionRef, event);
|
|
97
|
+
else if (hasPersistence) yield* Ref.update(versionRef, (v) => v + 1);
|
|
98
|
+
yield* replier.succeed(request, reply);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case "GetState": {
|
|
102
|
+
const state = yield* runtime.getState;
|
|
103
|
+
yield* replier.succeed(request, state);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "WatchState":
|
|
107
|
+
yield* replier.succeed(request, SubscriptionRef.changes(runtime.stateRef));
|
|
108
|
+
break;
|
|
109
|
+
default: break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
75
112
|
});
|
|
76
113
|
});
|
|
77
|
-
|
|
114
|
+
const clusterOptions = {};
|
|
115
|
+
if (options?.maxIdleTime !== void 0) clusterOptions.maxIdleTime = options.maxIdleTime;
|
|
116
|
+
if (options?.mailboxCapacity !== void 0) clusterOptions.mailboxCapacity = options.mailboxCapacity;
|
|
117
|
+
if (options?.disableFatalDefects !== void 0) clusterOptions.disableFatalDefects = options.disableFatalDefects;
|
|
118
|
+
if (options?.defectRetryPolicy !== void 0) clusterOptions.defectRetryPolicy = options.defectRetryPolicy;
|
|
119
|
+
return entity.toLayerQueue(build.pipe(Effect.orDie), Object.keys(clusterOptions).length > 0 ? clusterOptions : void 0);
|
|
78
120
|
} };
|
|
121
|
+
const noPersistence = {
|
|
122
|
+
adapter: void 0,
|
|
123
|
+
key: void 0,
|
|
124
|
+
hydratedState: void 0,
|
|
125
|
+
initialVersion: 0
|
|
126
|
+
};
|
|
127
|
+
/** Load snapshot/journal and compute hydrated state. */
|
|
128
|
+
const hydratePersistence = (persistence, entityDef, entityId, machine, initializeState) => Effect.gen(function* () {
|
|
129
|
+
if (persistence === void 0) return noPersistence;
|
|
130
|
+
const adapter = yield* PersistenceAdapter;
|
|
131
|
+
const key = {
|
|
132
|
+
entityType: persistence.machineType ?? entityDef.type,
|
|
133
|
+
entityId
|
|
134
|
+
};
|
|
135
|
+
const maybeSnapshot = yield* adapter.loadSnapshot(key);
|
|
136
|
+
if ((persistence.strategy ?? "snapshot") === "journal") {
|
|
137
|
+
const baseState = Option.isSome(maybeSnapshot) ? maybeSnapshot.value.state : initializeState !== void 0 ? initializeState(entityId) : machine.initial;
|
|
138
|
+
const snapshotVersion = Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : 0;
|
|
139
|
+
const events = yield* adapter.loadEvents(key, snapshotVersion);
|
|
140
|
+
if (events.length > 0) {
|
|
141
|
+
const eventValues = events.map((e) => e.event);
|
|
142
|
+
const hydratedState = yield* replay(new BuiltMachine(machine), eventValues, { from: baseState });
|
|
143
|
+
const lastEvent = events[events.length - 1];
|
|
144
|
+
return {
|
|
145
|
+
adapter,
|
|
146
|
+
key,
|
|
147
|
+
hydratedState,
|
|
148
|
+
initialVersion: lastEvent !== void 0 ? lastEvent.version : snapshotVersion
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
adapter,
|
|
153
|
+
key,
|
|
154
|
+
hydratedState: Option.isSome(maybeSnapshot) ? maybeSnapshot.value.state : void 0,
|
|
155
|
+
initialVersion: snapshotVersion
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (Option.isSome(maybeSnapshot)) return {
|
|
159
|
+
adapter,
|
|
160
|
+
key,
|
|
161
|
+
hydratedState: maybeSnapshot.value.state,
|
|
162
|
+
initialVersion: maybeSnapshot.value.version
|
|
163
|
+
};
|
|
164
|
+
return {
|
|
165
|
+
adapter,
|
|
166
|
+
key,
|
|
167
|
+
hydratedState: void 0,
|
|
168
|
+
initialVersion: 0
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
/**
|
|
172
|
+
* Append a single event to the journal, incrementing version.
|
|
173
|
+
*
|
|
174
|
+
* On failure: defects the entity activation. The cluster's defectRetryPolicy
|
|
175
|
+
* restarts the entity, which rehydrates from the last consistent snapshot +
|
|
176
|
+
* whatever events made it to the journal. This is correct because the in-memory
|
|
177
|
+
* state has already advanced — we can't un-ring that bell — so the activation
|
|
178
|
+
* is now unreliable and must restart.
|
|
179
|
+
*/
|
|
180
|
+
const persistEvent = (adapter, key, versionRef, event) => Effect.gen(function* () {
|
|
181
|
+
const expectedVersion = yield* Ref.get(versionRef);
|
|
182
|
+
const newVersion = expectedVersion + 1;
|
|
183
|
+
const persisted = {
|
|
184
|
+
event,
|
|
185
|
+
version: newVersion,
|
|
186
|
+
timestamp: Date.now()
|
|
187
|
+
};
|
|
188
|
+
yield* adapter.appendEvents(key, [persisted], expectedVersion);
|
|
189
|
+
yield* Ref.set(versionRef, newVersion);
|
|
190
|
+
}).pipe(Effect.tapError((error) => Effect.logWarning("Journal append failed, defecting entity", {
|
|
191
|
+
key,
|
|
192
|
+
error
|
|
193
|
+
})), Effect.orDie);
|
|
79
194
|
//#endregion
|
|
80
195
|
export { EntityMachine };
|
package/dist/cluster/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { EntityPersistenceConfig, PersistedEvent, PersistenceAdapter, PersistenceKey, Snapshot } from "./persistence.js";
|
|
2
|
+
import { makeInMemoryPersistenceAdapter } from "./adapters/in-memory.js";
|
|
3
|
+
import { EntityRpcs, ToEntityOptions, toEntity } from "./to-entity.js";
|
|
4
|
+
import { EntityActorRef, makeEntityActorRef } from "./entity-actor-ref.js";
|
|
1
5
|
import { EntityMachine, EntityMachineOptions } from "./entity-machine.js";
|
|
2
|
-
|
|
3
|
-
export { EntityMachine, type EntityMachineOptions, type ToEntityOptions, toEntity };
|
|
6
|
+
export { type EntityActorRef, EntityMachine, type EntityMachineOptions, type EntityPersistenceConfig, type EntityRpcs, type PersistedEvent, PersistenceAdapter, type PersistenceAdapter as PersistenceAdapterInterface, type PersistenceKey, type Snapshot, type ToEntityOptions, makeEntityActorRef, makeInMemoryPersistenceAdapter, toEntity };
|
package/dist/cluster/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { makeEntityActorRef } from "./entity-actor-ref.js";
|
|
2
|
+
import { PersistenceAdapter } from "./persistence.js";
|
|
1
3
|
import { EntityMachine } from "./entity-machine.js";
|
|
2
4
|
import { toEntity } from "./to-entity.js";
|
|
3
|
-
|
|
5
|
+
import { makeInMemoryPersistenceAdapter } from "./adapters/in-memory.js";
|
|
6
|
+
export { EntityMachine, PersistenceAdapter, makeEntityActorRef, makeInMemoryPersistenceAdapter, toEntity };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { PersistenceError, VersionConflictError } from "../errors.js";
|
|
2
|
+
import { Effect, Option, Schedule, ServiceMap } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/cluster/persistence.d.ts
|
|
5
|
+
/** Namespaced key preventing cross-type collisions (e.g. Order/123 vs User/123). */
|
|
6
|
+
interface PersistenceKey {
|
|
7
|
+
readonly entityType: string;
|
|
8
|
+
readonly entityId: string;
|
|
9
|
+
}
|
|
10
|
+
/** Stored state snapshot with version and timestamp. */
|
|
11
|
+
interface Snapshot<S> {
|
|
12
|
+
readonly state: S;
|
|
13
|
+
readonly version: number;
|
|
14
|
+
readonly timestamp: number;
|
|
15
|
+
}
|
|
16
|
+
/** Stored event with version and timestamp. */
|
|
17
|
+
interface PersistedEvent<E> {
|
|
18
|
+
readonly event: E;
|
|
19
|
+
readonly version: number;
|
|
20
|
+
readonly timestamp: number;
|
|
21
|
+
}
|
|
22
|
+
/** Persistence configuration for EntityMachineOptions. */
|
|
23
|
+
interface EntityPersistenceConfig {
|
|
24
|
+
/** Persistence strategy. Default: "snapshot". */
|
|
25
|
+
readonly strategy?: "snapshot" | "journal";
|
|
26
|
+
/**
|
|
27
|
+
* Schedule controlling snapshot frequency.
|
|
28
|
+
* Only applies to snapshot strategy (or journal strategy's periodic snapshots).
|
|
29
|
+
* Default: save on every state change.
|
|
30
|
+
*/
|
|
31
|
+
readonly snapshotSchedule?: Schedule.Schedule<any, any>;
|
|
32
|
+
/** Override entityType in the persistence key (defaults to Entity.type). */
|
|
33
|
+
readonly machineType?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Storage backend for entity state persistence. */
|
|
36
|
+
interface PersistenceAdapter {
|
|
37
|
+
/** Save a state snapshot. Fails with VersionConflictError if version is stale. */
|
|
38
|
+
readonly saveSnapshot: (key: PersistenceKey, snapshot: Snapshot<unknown>) => Effect.Effect<void, PersistenceError | VersionConflictError>;
|
|
39
|
+
/** Load the latest snapshot, or None if no snapshot exists. */
|
|
40
|
+
readonly loadSnapshot: (key: PersistenceKey) => Effect.Effect<Option.Option<Snapshot<unknown>>, PersistenceError>;
|
|
41
|
+
/** Append events to the journal. Fails with VersionConflictError if expectedVersion doesn't match. */
|
|
42
|
+
readonly appendEvents: (key: PersistenceKey, events: ReadonlyArray<PersistedEvent<unknown>>, expectedVersion: number) => Effect.Effect<void, PersistenceError | VersionConflictError>;
|
|
43
|
+
/** Load events from the journal, optionally after a given version. */
|
|
44
|
+
readonly loadEvents: (key: PersistenceKey, afterVersion?: number) => Effect.Effect<ReadonlyArray<PersistedEvent<unknown>>, PersistenceError>;
|
|
45
|
+
}
|
|
46
|
+
/** Service tag for PersistenceAdapter — resolve from context for shared infra. */
|
|
47
|
+
declare const PersistenceAdapter: ServiceMap.Service<PersistenceAdapter, PersistenceAdapter>;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { EntityPersistenceConfig, PersistedEvent, PersistenceAdapter, PersistenceKey, Snapshot };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ServiceMap } from "effect";
|
|
2
|
+
//#region src/cluster/persistence.ts
|
|
3
|
+
/**
|
|
4
|
+
* Entity persistence types and adapter interface.
|
|
5
|
+
*
|
|
6
|
+
* Provides snapshot and event journal persistence for entity-machine
|
|
7
|
+
* state across deactivation/reactivation cycles.
|
|
8
|
+
*
|
|
9
|
+
* Two strategies:
|
|
10
|
+
* - **snapshot**: Save full state periodically. Simple, fast.
|
|
11
|
+
* - **journal**: Append events inline, replay on reactivation. Full audit trail.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
/** Service tag for PersistenceAdapter — resolve from context for shared infra. */
|
|
16
|
+
const PersistenceAdapter = ServiceMap.Service("@effect-machine/cluster/PersistenceAdapter");
|
|
17
|
+
//#endregion
|
|
18
|
+
export { PersistenceAdapter };
|
|
@@ -2,6 +2,7 @@ import { Machine } from "../machine.js";
|
|
|
2
2
|
import { Schema } from "effect";
|
|
3
3
|
import { Entity } from "effect/unstable/cluster";
|
|
4
4
|
import { Rpc } from "effect/unstable/rpc";
|
|
5
|
+
import * as effect_unstable_rpc_RpcSchema0 from "effect/unstable/rpc/RpcSchema";
|
|
5
6
|
|
|
6
7
|
//#region src/cluster/to-entity.d.ts
|
|
7
8
|
/**
|
|
@@ -16,12 +17,15 @@ interface ToEntityOptions {
|
|
|
16
17
|
/**
|
|
17
18
|
* Default RPC protocol for entity machines.
|
|
18
19
|
*
|
|
19
|
-
* - `Send` - Send event to machine, returns new state
|
|
20
|
+
* - `Send` - Send event to machine (fire-and-forget), returns new state
|
|
21
|
+
* - `Ask` - Send event and get domain reply (typed via Event.reply() schemas)
|
|
20
22
|
* - `GetState` - Get current state
|
|
21
23
|
*/
|
|
22
24
|
type EntityRpcs<StateSchema extends Schema.Top, EventSchema extends Schema.Top> = readonly [Rpc.Rpc<"Send", Schema.Struct<{
|
|
23
25
|
readonly event: EventSchema;
|
|
24
|
-
}>, StateSchema, typeof Schema.Never, never>, Rpc.Rpc<"
|
|
26
|
+
}>, StateSchema, typeof Schema.Never, never>, Rpc.Rpc<"Ask", Schema.Struct<{
|
|
27
|
+
readonly event: EventSchema;
|
|
28
|
+
}>, typeof Schema.Unknown, typeof Schema.Never, never>, Rpc.Rpc<"GetState", typeof Schema.Void, StateSchema, typeof Schema.Never, never>];
|
|
25
29
|
/**
|
|
26
30
|
* Generate an Entity definition from a machine.
|
|
27
31
|
*
|
|
@@ -59,6 +63,8 @@ declare const toEntity: <S extends {
|
|
|
59
63
|
readonly _tag: string;
|
|
60
64
|
}, R>(machine: Machine<S, E, R, any, any, any, any>, options: ToEntityOptions) => Entity.Entity<string, Rpc.Rpc<"Send", Schema.Struct<{
|
|
61
65
|
event: Schema.Schema<E>;
|
|
62
|
-
}>, Schema.Schema<S>, Schema.Never, never, never> | Rpc.Rpc<"
|
|
66
|
+
}>, Schema.Schema<S>, Schema.Never, never, never> | Rpc.Rpc<"Ask", Schema.Struct<{
|
|
67
|
+
event: Schema.Schema<E>;
|
|
68
|
+
}>, Schema.Unknown, Schema.Never, never, never> | Rpc.Rpc<"GetState", Schema.Void, Schema.Schema<S>, Schema.Never, never, never> | Rpc.Rpc<"WatchState", Schema.Void, effect_unstable_rpc_RpcSchema0.Stream<Schema.Schema<S>, Schema.Never>, Schema.Never, never, never>>;
|
|
63
69
|
//#endregion
|
|
64
70
|
export { EntityRpcs, ToEntityOptions, toEntity };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MissingSchemaError } from "../errors.js";
|
|
2
|
+
import { Schema } from "effect";
|
|
2
3
|
import { Entity } from "effect/unstable/cluster";
|
|
3
4
|
import { Rpc } from "effect/unstable/rpc";
|
|
4
5
|
//#region src/cluster/to-entity.ts
|
|
@@ -42,10 +43,21 @@ const toEntity = (machine, options) => {
|
|
|
42
43
|
const stateSchema = machine.stateSchema;
|
|
43
44
|
const eventSchema = machine.eventSchema;
|
|
44
45
|
if (stateSchema === void 0 || eventSchema === void 0) throw new MissingSchemaError({ operation: "toEntity" });
|
|
45
|
-
return Entity.make(options.type, [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
return Entity.make(options.type, [
|
|
47
|
+
Rpc.make("Send", {
|
|
48
|
+
payload: { event: eventSchema },
|
|
49
|
+
success: stateSchema
|
|
50
|
+
}),
|
|
51
|
+
Rpc.make("Ask", {
|
|
52
|
+
payload: { event: eventSchema },
|
|
53
|
+
success: Schema.Unknown
|
|
54
|
+
}),
|
|
55
|
+
Rpc.make("GetState", { success: stateSchema }),
|
|
56
|
+
Rpc.make("WatchState", {
|
|
57
|
+
success: stateSchema,
|
|
58
|
+
stream: true
|
|
59
|
+
})
|
|
60
|
+
]);
|
|
49
61
|
};
|
|
50
62
|
//#endregion
|
|
51
63
|
export { toEntity };
|
package/dist/errors.d.ts
CHANGED
|
@@ -53,5 +53,16 @@ declare const NoReplyError_base: Schema.ErrorClass<NoReplyError, Schema.TaggedSt
|
|
|
53
53
|
}>, effect_Cause0.YieldableError>;
|
|
54
54
|
/** ask() was used but the transition handler did not call reply */
|
|
55
55
|
declare class NoReplyError extends NoReplyError_base {}
|
|
56
|
+
declare const PersistenceError_base: Schema.ErrorClass<PersistenceError, Schema.TaggedStruct<"PersistenceError", {
|
|
57
|
+
readonly message: Schema.String;
|
|
58
|
+
}>, effect_Cause0.YieldableError>;
|
|
59
|
+
/** Persistence adapter operation failed */
|
|
60
|
+
declare class PersistenceError extends PersistenceError_base {}
|
|
61
|
+
declare const VersionConflictError_base: Schema.ErrorClass<VersionConflictError, Schema.TaggedStruct<"VersionConflictError", {
|
|
62
|
+
readonly expected: Schema.Number;
|
|
63
|
+
readonly actual: Schema.Number;
|
|
64
|
+
}>, effect_Cause0.YieldableError>;
|
|
65
|
+
/** Optimistic locking failure — stored version doesn't match expected */
|
|
66
|
+
declare class VersionConflictError extends VersionConflictError_base {}
|
|
56
67
|
//#endregion
|
|
57
|
-
export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError };
|
|
68
|
+
export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError, VersionConflictError };
|
package/dist/errors.js
CHANGED
|
@@ -39,5 +39,12 @@ var NoReplyError = class extends Schema.TaggedErrorClass()("NoReplyError", {
|
|
|
39
39
|
actorId: Schema.String,
|
|
40
40
|
eventTag: Schema.String
|
|
41
41
|
}) {};
|
|
42
|
+
/** Persistence adapter operation failed */
|
|
43
|
+
var PersistenceError = class extends Schema.TaggedErrorClass()("PersistenceError", { message: Schema.String }) {};
|
|
44
|
+
/** Optimistic locking failure — stored version doesn't match expected */
|
|
45
|
+
var VersionConflictError = class extends Schema.TaggedErrorClass()("VersionConflictError", {
|
|
46
|
+
expected: Schema.Number,
|
|
47
|
+
actual: Schema.Number
|
|
48
|
+
}) {};
|
|
42
49
|
//#endregion
|
|
43
|
-
export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError };
|
|
50
|
+
export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError, VersionConflictError };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { EffectHandlers, EffectSlot, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlot, GuardSlots, GuardsDef, GuardsSchema, MachineContext, Slot } from "./slot.js";
|
|
2
|
-
import {
|
|
2
|
+
import { DeferReplyResult, ReplyResult } from "./internal/utils.js";
|
|
3
|
+
import { Event, MachineEventSchema, MachineStateSchema, ReplyFields, State } from "./schema.js";
|
|
3
4
|
import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
|
|
4
5
|
import { ProcessEventResult } from "./internal/transition.js";
|
|
5
6
|
import { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SpawnEffect, StateHandlerContext, TaskOptions, Transition, machine_d_exports } from "./machine.js";
|
|
6
7
|
import { ActorRef, ActorRefSync, ActorSystem, Default, SystemEvent, SystemEventListener, TransitionInfo } from "./actor.js";
|
|
7
8
|
import { SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
|
|
8
9
|
import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, InspectionEvent, Inspector, InspectorHandler, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
|
|
9
|
-
export { type ActorRef, type ActorRefSync, ActorStoppedError, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type BuiltMachine, DuplicateActorError, type EffectEvent, type EffectSlots, type EffectsDef, type EffectsSchema, type ErrorEvent, Event, type EventReceivedEvent, type GuardHandlers, type GuardSlot, type GuardSlots, type GuardsDef, type GuardsSchema, type HandlerContext, type InspectionEvent, type Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, NoReplyError, type ProcessEventResult, type ProvideHandlers, ProvisionValidationError, type SimulationResult, Slot, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TracingInspectorOptions, type Transition, type TransitionEvent, type TransitionInfo, UnprovidedSlotsError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
|
|
10
|
+
export { type ActorRef, type ActorRefSync, ActorStoppedError, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type BuiltMachine, type DeferReplyResult, DuplicateActorError, type EffectEvent, type EffectSlots, type EffectsDef, type EffectsSchema, type ErrorEvent, Event, type EventReceivedEvent, type GuardHandlers, type GuardSlot, type GuardSlots, type GuardsDef, type GuardsSchema, type HandlerContext, type InspectionEvent, type Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, NoReplyError, type ProcessEventResult, type ProvideHandlers, ProvisionValidationError, type ReplyFields, type ReplyResult, type SimulationResult, Slot, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TracingInspectorOptions, type Transition, type TransitionEvent, type TransitionInfo, UnprovidedSlotsError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
|
|
@@ -26,6 +26,19 @@ type FullStateBrand<D extends Record<string, unknown>> = StateBrand & SchemaIdBr
|
|
|
26
26
|
* Full event brand: combines base event brand with schema-specific brand
|
|
27
27
|
*/
|
|
28
28
|
type FullEventBrand<D extends Record<string, unknown>> = EventBrand & SchemaIdBrand<D>;
|
|
29
|
+
/**
|
|
30
|
+
* Brand that carries the reply type for an event variant.
|
|
31
|
+
* Present only on events defined with Event.reply().
|
|
32
|
+
*/
|
|
33
|
+
type ReplyTypeId = "effect-machine/ReplyTypeId";
|
|
34
|
+
interface ReplyTypeBrand<R> extends Brand.Brand<ReplyTypeId> {
|
|
35
|
+
readonly _ReplyType: R;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Extract the reply type from a branded event value.
|
|
39
|
+
* Returns `never` if the event has no reply schema.
|
|
40
|
+
*/
|
|
41
|
+
type ExtractReply<E> = E extends ReplyTypeBrand<infer R> ? R : never;
|
|
29
42
|
/**
|
|
30
43
|
* Value or constructor for a tagged type.
|
|
31
44
|
* Accepts both plain values (empty structs) and constructor functions (non-empty structs).
|
|
@@ -34,4 +47,4 @@ type TaggedOrConstructor<T extends {
|
|
|
34
47
|
readonly _tag: string;
|
|
35
48
|
}> = T | ((...args: never[]) => T);
|
|
36
49
|
//#endregion
|
|
37
|
-
export { BrandedEvent, BrandedState, EventBrand, EventTypeId, FullEventBrand, FullStateBrand, SchemaIdBrand, StateBrand, StateTypeId, TaggedOrConstructor };
|
|
50
|
+
export { BrandedEvent, BrandedState, EventBrand, EventTypeId, ExtractReply, FullEventBrand, FullStateBrand, ReplyTypeBrand, ReplyTypeId, SchemaIdBrand, StateBrand, StateTypeId, TaggedOrConstructor };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
|
|
2
|
+
import { NoReplyError } from "../errors.js";
|
|
3
|
+
import { ProcessEventHooks } from "./transition.js";
|
|
4
|
+
import { Machine, MachineRef } from "../machine.js";
|
|
5
|
+
import { ActorSystem } from "../actor.js";
|
|
6
|
+
import { Deferred, Effect, Queue, Scope, SubscriptionRef } from "effect";
|
|
7
|
+
|
|
8
|
+
//#region src/internal/runtime.d.ts
|
|
9
|
+
/** @internal */
|
|
10
|
+
type RuntimeQueuedEvent<E> = {
|
|
11
|
+
readonly _tag: "send";
|
|
12
|
+
readonly event: E;
|
|
13
|
+
} | {
|
|
14
|
+
readonly _tag: "sendWait";
|
|
15
|
+
readonly event: E;
|
|
16
|
+
readonly done: Deferred.Deferred<void, unknown>;
|
|
17
|
+
} | {
|
|
18
|
+
readonly _tag: "ask";
|
|
19
|
+
readonly event: E;
|
|
20
|
+
readonly reply: Deferred.Deferred<unknown, NoReplyError>;
|
|
21
|
+
};
|
|
22
|
+
/** @internal */
|
|
23
|
+
interface RuntimeHandle<S, E> {
|
|
24
|
+
/** Enqueue a fire-and-forget event */
|
|
25
|
+
readonly send: (event: E) => Effect.Effect<void>;
|
|
26
|
+
/** Enqueue event and wait for processing to complete (for RPC Send). Fails on defect. */
|
|
27
|
+
readonly sendWait: (event: E) => Effect.Effect<void, unknown>;
|
|
28
|
+
/** Enqueue an ask event, returns the reply value */
|
|
29
|
+
readonly ask: (event: E) => Effect.Effect<unknown, NoReplyError>;
|
|
30
|
+
/** Get current state */
|
|
31
|
+
readonly getState: Effect.Effect<S>;
|
|
32
|
+
/** SubscriptionRef for state observation (WatchState streaming) */
|
|
33
|
+
readonly stateRef: SubscriptionRef.SubscriptionRef<S>;
|
|
34
|
+
/** Whether the runtime has stopped (final state reached) */
|
|
35
|
+
readonly isStopped: Effect.Effect<boolean>;
|
|
36
|
+
/** Stop the runtime (interrupt event loop, clean up) */
|
|
37
|
+
readonly stop: Effect.Effect<void>;
|
|
38
|
+
}
|
|
39
|
+
/** @internal */
|
|
40
|
+
interface RuntimeConfig<S, E> {
|
|
41
|
+
readonly actorId: string;
|
|
42
|
+
readonly hooks?: ProcessEventHooks<S, E>;
|
|
43
|
+
/**
|
|
44
|
+
* Custom queue factory. Default: `Queue.unbounded()`.
|
|
45
|
+
* Use `Queue.sliding(n)` or `Queue.dropping(n)` for bounded queues.
|
|
46
|
+
*/
|
|
47
|
+
readonly queueFactory?: Effect.Effect<Queue.Queue<RuntimeQueuedEvent<E>>>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create a runtime for a machine. Returns a handle for sending events
|
|
51
|
+
* and querying state. The runtime owns:
|
|
52
|
+
* - Single event queue (all events serialized)
|
|
53
|
+
* - Event loop fiber
|
|
54
|
+
* - Postpone buffer
|
|
55
|
+
* - Background effects
|
|
56
|
+
* - State scope (spawn effects)
|
|
57
|
+
* - Final state detection
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
declare const createRuntime: <S extends {
|
|
62
|
+
readonly _tag: string;
|
|
63
|
+
}, E extends {
|
|
64
|
+
readonly _tag: string;
|
|
65
|
+
}, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, any, any, GD, EFD>, system: ActorSystem, config: RuntimeConfig<S, E>) => Effect.Effect<RuntimeHandle<S, E>, never, Scope.Scope | Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
|
|
66
|
+
//#endregion
|
|
67
|
+
export { RuntimeConfig, RuntimeHandle, RuntimeQueuedEvent, createRuntime };
|