effect-machine 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actor.d.ts +10 -4
- package/dist/actor.js +33 -5
- package/dist/cluster/adapters/in-memory.d.ts +28 -0
- package/dist/cluster/adapters/in-memory.js +79 -0
- package/dist/cluster/entity-actor-ref.d.ts +56 -0
- package/dist/cluster/entity-actor-ref.js +33 -0
- package/dist/cluster/entity-machine.d.ts +31 -49
- package/dist/cluster/entity-machine.js +167 -52
- package/dist/cluster/index.d.ts +5 -2
- package/dist/cluster/index.js +4 -1
- package/dist/cluster/persistence.d.ts +49 -0
- package/dist/cluster/persistence.js +18 -0
- package/dist/cluster/to-entity.d.ts +9 -3
- package/dist/cluster/to-entity.js +16 -4
- package/dist/errors.d.ts +12 -1
- package/dist/errors.js +8 -1
- package/dist/index.d.ts +3 -2
- package/dist/internal/brands.d.ts +14 -1
- package/dist/internal/runtime.d.ts +67 -0
- package/dist/internal/runtime.js +248 -0
- package/dist/internal/transition.d.ts +5 -0
- package/dist/internal/transition.js +15 -3
- package/dist/internal/utils.d.ts +42 -6
- package/dist/internal/utils.js +27 -1
- package/dist/machine.d.ts +26 -13
- package/dist/machine.js +14 -3
- package/dist/schema.d.ts +35 -34
- package/dist/schema.js +32 -3
- package/dist/testing.js +4 -2
- package/package.json +3 -3
- package/v3/dist/actor.d.ts +4 -3
- package/v3/dist/actor.js +15 -3
- package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
- package/v3/dist/cluster/adapters/in-memory.js +62 -0
- package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
- package/v3/dist/cluster/entity-actor-ref.js +19 -0
- package/v3/dist/cluster/entity-machine.d.ts +34 -49
- package/v3/dist/cluster/entity-machine.js +134 -50
- package/v3/dist/cluster/index.d.ts +5 -2
- package/v3/dist/cluster/index.js +4 -1
- package/v3/dist/cluster/persistence.d.ts +48 -0
- package/v3/dist/cluster/persistence.js +14 -0
- package/v3/dist/cluster/to-entity.d.ts +5 -2
- package/v3/dist/cluster/to-entity.js +12 -4
- package/v3/dist/errors.d.ts +16 -1
- package/v3/dist/errors.js +8 -1
- package/v3/dist/index.d.ts +3 -2
- package/v3/dist/internal/brands.d.ts +15 -1
- package/v3/dist/internal/runtime.d.ts +65 -0
- package/v3/dist/internal/runtime.js +236 -0
- package/v3/dist/internal/transition.d.ts +5 -0
- package/v3/dist/internal/transition.js +15 -3
- package/v3/dist/internal/utils.d.ts +42 -6
- package/v3/dist/internal/utils.js +27 -1
- package/v3/dist/machine.d.ts +19 -13
- package/v3/dist/machine.js +10 -2
- package/v3/dist/schema.d.ts +35 -34
- package/v3/dist/schema.js +29 -3
package/dist/schema.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import { FullEventBrand, FullStateBrand } from "./internal/brands.js";
|
|
1
|
+
import { FullEventBrand, FullStateBrand, ReplyTypeBrand } from "./internal/brands.js";
|
|
2
2
|
import { Schema } from "effect";
|
|
3
3
|
|
|
4
4
|
//#region src/schema.d.ts
|
|
5
|
+
declare const ReplySchemaSymbol: unique symbol;
|
|
6
|
+
type ReplySchemaSymbol = typeof ReplySchemaSymbol;
|
|
7
|
+
/**
|
|
8
|
+
* Fields annotated with a reply schema.
|
|
9
|
+
* Structurally identical to Schema.Struct.Fields at runtime,
|
|
10
|
+
* but carries the reply schema type at compile time.
|
|
11
|
+
*/
|
|
12
|
+
type ReplyFields<F extends Schema.Struct.Fields, RS extends Schema.Schema<unknown>> = F & {
|
|
13
|
+
readonly [ReplySchemaSymbol]: RS;
|
|
14
|
+
};
|
|
5
15
|
/**
|
|
6
16
|
* Extract the TypeScript type from a TaggedStruct schema
|
|
7
17
|
*/
|
|
@@ -12,20 +22,23 @@ type TaggedStructType<Tag extends string, Fields extends Schema.Struct.Fields> =
|
|
|
12
22
|
type VariantSchemas<D extends Record<string, Schema.Struct.Fields>> = { readonly [K in keyof D & string]: Schema.TaggedStruct<K, D[K]> };
|
|
13
23
|
/**
|
|
14
24
|
* Build union type from variant schemas.
|
|
15
|
-
*
|
|
25
|
+
* Reply-bearing variants carry ReplyTypeBrand<R> for ask() inference.
|
|
16
26
|
*/
|
|
17
|
-
type VariantsUnion<D extends Record<string, Schema.Struct.Fields>> = { [K in keyof D & string]: TaggedStructType<K, D[K]>
|
|
27
|
+
type VariantsUnion<D extends Record<string, Schema.Struct.Fields>> = { [K in keyof D & string]: TaggedStructType<K, D[K]> & (D[K] extends {
|
|
28
|
+
readonly [ReplySchemaSymbol]: Schema.Schema<infer R>;
|
|
29
|
+
} ? ReplyTypeBrand<R> : unknown) }[keyof D & string];
|
|
18
30
|
/**
|
|
19
|
-
* Check if fields are empty (no required properties)
|
|
31
|
+
* Check if fields are empty (no required string properties).
|
|
32
|
+
* Symbol keys (like ReplySchemaSymbol) are metadata, not payload fields.
|
|
20
33
|
*/
|
|
21
|
-
type IsEmptyFields<Fields extends Schema.Struct.Fields> = keyof Fields extends never ? true : false;
|
|
34
|
+
type IsEmptyFields<Fields extends Schema.Struct.Fields> = string & keyof Fields extends never ? true : false;
|
|
22
35
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* Non-empty structs require args: `State.Loading({ url })`
|
|
26
|
-
*
|
|
27
|
-
* Each variant also has a `derive` method for constructing from a source object.
|
|
36
|
+
* Resolve the reply brand for a variant's fields.
|
|
37
|
+
* If fields carry ReplySchemaSymbol, adds ReplyTypeBrand<R>.
|
|
28
38
|
*/
|
|
39
|
+
type VariantReplyBrand<Fields extends Schema.Struct.Fields> = Fields extends {
|
|
40
|
+
readonly [ReplySchemaSymbol]: Schema.Schema<infer R>;
|
|
41
|
+
} ? ReplyTypeBrand<R> : unknown;
|
|
29
42
|
/**
|
|
30
43
|
* Constructor functions for each variant.
|
|
31
44
|
* Empty structs: plain values with `_tag`: `State.Idle`
|
|
@@ -33,10 +46,11 @@ type IsEmptyFields<Fields extends Schema.Struct.Fields> = keyof Fields extends n
|
|
|
33
46
|
*
|
|
34
47
|
* Each variant also has a `derive` method for constructing from a source object.
|
|
35
48
|
* The source type uses `object` to accept branded state types without index signature issues.
|
|
49
|
+
* Reply-bearing variants carry ReplyTypeBrand<R> for ask() type inference.
|
|
36
50
|
*/
|
|
37
|
-
type VariantConstructors<D extends Record<string, Schema.Struct.Fields>, Brand> = { readonly [K in keyof D & string]: IsEmptyFields<D[K]> extends true ? TaggedStructType<K, D[K]> & Brand & {
|
|
51
|
+
type VariantConstructors<D extends Record<string, Schema.Struct.Fields>, Brand> = { readonly [K in keyof D & string]: IsEmptyFields<D[K]> extends true ? TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]> & {
|
|
38
52
|
readonly derive: (source: object) => TaggedStructType<K, D[K]> & Brand;
|
|
39
|
-
} : ((args: Schema.Struct.Type<D[K]>) => TaggedStructType<K, D[K]> & Brand) & {
|
|
53
|
+
} : ((args: Schema.Struct.Type<D[K]>) => TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]>) & {
|
|
40
54
|
readonly derive: (source: object, partial?: Partial<Schema.Struct.Type<D[K]>>) => TaggedStructType<K, D[K]> & Brand;
|
|
41
55
|
readonly _tag: K;
|
|
42
56
|
} };
|
|
@@ -67,6 +81,11 @@ interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Bran
|
|
|
67
81
|
<R>(cases: MatchCases<D, R>): (value: VariantsUnion<D> & Brand) => R;
|
|
68
82
|
<R>(value: VariantsUnion<D> & Brand, cases: MatchCases<D, R>): R;
|
|
69
83
|
};
|
|
84
|
+
/**
|
|
85
|
+
* Reply schemas per variant tag. Only populated for event schemas
|
|
86
|
+
* with variants defined via `Event.reply()`.
|
|
87
|
+
*/
|
|
88
|
+
readonly _replySchemas: ReadonlyMap<string, Schema.Decoder<unknown>>;
|
|
70
89
|
}
|
|
71
90
|
/**
|
|
72
91
|
* Schema-first state definition that provides:
|
|
@@ -116,26 +135,8 @@ type MachineEventSchema<D extends Record<string, Schema.Struct.Fields>> = Schema
|
|
|
116
135
|
* ```
|
|
117
136
|
*/
|
|
118
137
|
declare const State: <const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineStateSchema<D>;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
* The schema's definition type D creates a unique brand, preventing
|
|
123
|
-
* accidental use of constructors from different event schemas
|
|
124
|
-
* (unless they have identical definitions).
|
|
125
|
-
*
|
|
126
|
-
* @example
|
|
127
|
-
* ```ts
|
|
128
|
-
* const OrderEvent = MachineSchema.Event({
|
|
129
|
-
* Ship: { trackingId: Schema.String },
|
|
130
|
-
* Cancel: {},
|
|
131
|
-
* })
|
|
132
|
-
*
|
|
133
|
-
* type OrderEvent = typeof OrderEvent.Type
|
|
134
|
-
*
|
|
135
|
-
* // Construct
|
|
136
|
-
* const e = OrderEvent.Ship({ trackingId: "abc" })
|
|
137
|
-
* ```
|
|
138
|
-
*/
|
|
139
|
-
declare const Event: <const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineEventSchema<D>;
|
|
138
|
+
declare const Event: (<const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineEventSchema<D>) & {
|
|
139
|
+
reply: <F extends Schema.Struct.Fields, RS extends Schema.Schema<unknown>>(fields: F, replySchema: RS) => ReplyFields<F, RS>;
|
|
140
|
+
};
|
|
140
141
|
//#endregion
|
|
141
|
-
export { Event, MachineEventSchema, MachineStateSchema, State, VariantsUnion };
|
|
142
|
+
export { Event, MachineEventSchema, MachineStateSchema, ReplyFields, ReplySchemaSymbol, State, VariantsUnion };
|
package/dist/schema.js
CHANGED
|
@@ -38,6 +38,7 @@ import { Schema } from "effect";
|
|
|
38
38
|
*
|
|
39
39
|
* @module
|
|
40
40
|
*/
|
|
41
|
+
const ReplySchemaSymbol = Symbol.for("effect-machine/ReplySchema");
|
|
41
42
|
/**
|
|
42
43
|
* Build a schema-first definition from a record of tag -> fields
|
|
43
44
|
*/
|
|
@@ -45,9 +46,14 @@ const RESERVED_DERIVE_KEYS = new Set(["_tag"]);
|
|
|
45
46
|
const buildMachineSchema = (definition) => {
|
|
46
47
|
const variants = {};
|
|
47
48
|
const constructors = {};
|
|
49
|
+
const replySchemas = /* @__PURE__ */ new Map();
|
|
48
50
|
for (const tag of Object.keys(definition)) {
|
|
49
51
|
const fields = definition[tag];
|
|
50
52
|
if (fields === void 0) continue;
|
|
53
|
+
if (ReplySchemaSymbol in fields) {
|
|
54
|
+
const rs = fields[ReplySchemaSymbol];
|
|
55
|
+
if (rs !== void 0) replySchemas.set(tag, rs);
|
|
56
|
+
}
|
|
51
57
|
variants[tag] = Schema.TaggedStruct(tag, fields);
|
|
52
58
|
const fieldNames = new Set(Object.keys(fields));
|
|
53
59
|
if (fieldNames.size > 0) {
|
|
@@ -94,6 +100,7 @@ const buildMachineSchema = (definition) => {
|
|
|
94
100
|
variants,
|
|
95
101
|
constructors,
|
|
96
102
|
_definition: definition,
|
|
103
|
+
replySchemas,
|
|
97
104
|
$is,
|
|
98
105
|
$match
|
|
99
106
|
};
|
|
@@ -103,10 +110,11 @@ const buildMachineSchema = (definition) => {
|
|
|
103
110
|
* Builds the schema object with variants, constructors, $is, and $match.
|
|
104
111
|
*/
|
|
105
112
|
const createMachineSchema = (definition) => {
|
|
106
|
-
const { schema, variants, constructors, _definition, $is, $match } = buildMachineSchema(definition);
|
|
113
|
+
const { schema, variants, constructors, _definition, replySchemas, $is, $match } = buildMachineSchema(definition);
|
|
107
114
|
return Object.assign(Object.create(schema), {
|
|
108
115
|
variants,
|
|
109
116
|
_definition,
|
|
117
|
+
_replySchemas: replySchemas,
|
|
110
118
|
$is,
|
|
111
119
|
$match,
|
|
112
120
|
...constructors
|
|
@@ -149,19 +157,40 @@ const State = (definition) => createMachineSchema(definition);
|
|
|
149
157
|
* accidental use of constructors from different event schemas
|
|
150
158
|
* (unless they have identical definitions).
|
|
151
159
|
*
|
|
160
|
+
* Use `Event.reply(fields, replySchema)` to define events that support
|
|
161
|
+
* typed `ask()` replies.
|
|
162
|
+
*
|
|
152
163
|
* @example
|
|
153
164
|
* ```ts
|
|
154
|
-
* const OrderEvent =
|
|
165
|
+
* const OrderEvent = Event({
|
|
155
166
|
* Ship: { trackingId: Schema.String },
|
|
156
167
|
* Cancel: {},
|
|
168
|
+
* GetTotal: Event.reply({}, Schema.Number),
|
|
157
169
|
* })
|
|
158
170
|
*
|
|
159
171
|
* type OrderEvent = typeof OrderEvent.Type
|
|
160
172
|
*
|
|
161
173
|
* // Construct
|
|
162
174
|
* const e = OrderEvent.Ship({ trackingId: "abc" })
|
|
175
|
+
*
|
|
176
|
+
* // Typed ask
|
|
177
|
+
* const total = yield* actor.ask(OrderEvent.GetTotal) // number
|
|
163
178
|
* ```
|
|
164
179
|
*/
|
|
165
|
-
const
|
|
180
|
+
const EventImpl = (definition) => createMachineSchema(definition);
|
|
181
|
+
/**
|
|
182
|
+
* Annotate event fields with a reply schema.
|
|
183
|
+
* Events defined with `Event.reply(fields, replySchema)` enable typed `ask()`.
|
|
184
|
+
*/
|
|
185
|
+
const replyFieldsFn = (fields, replySchema) => {
|
|
186
|
+
const annotated = { ...fields };
|
|
187
|
+
Object.defineProperty(annotated, ReplySchemaSymbol, {
|
|
188
|
+
value: replySchema,
|
|
189
|
+
enumerable: false,
|
|
190
|
+
writable: false
|
|
191
|
+
});
|
|
192
|
+
return annotated;
|
|
193
|
+
};
|
|
194
|
+
const Event = Object.assign(EventImpl, { reply: replyFieldsFn });
|
|
166
195
|
//#endregion
|
|
167
196
|
export { Event, State };
|
package/dist/testing.js
CHANGED
|
@@ -30,7 +30,8 @@ const simulate = Effect.fn("effect-machine.simulate")(function* (input, events)
|
|
|
30
30
|
const dummySelf = {
|
|
31
31
|
send: dummySend,
|
|
32
32
|
cast: dummySend,
|
|
33
|
-
spawn: () => Effect.die("spawn not supported in simulation")
|
|
33
|
+
spawn: () => Effect.die("spawn not supported in simulation"),
|
|
34
|
+
reply: () => Effect.succeed(false)
|
|
34
35
|
};
|
|
35
36
|
let currentState = machine.initial;
|
|
36
37
|
const states = [currentState];
|
|
@@ -143,7 +144,8 @@ const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function
|
|
|
143
144
|
const dummySelf = {
|
|
144
145
|
send: dummySend,
|
|
145
146
|
cast: dummySend,
|
|
146
|
-
spawn: () => Effect.die("spawn not supported in test harness")
|
|
147
|
+
spawn: () => Effect.die("spawn not supported in test harness"),
|
|
148
|
+
reply: () => Effect.succeed(false)
|
|
147
149
|
};
|
|
148
150
|
const stateRef = yield* SubscriptionRef.make(machine.initial);
|
|
149
151
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-machine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/cevr/effect-machine.git"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"release": "bun run build && changeset publish"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"effect": "4.0.0-beta.
|
|
59
|
+
"effect": "4.0.0-beta.42"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@changesets/changelog-github": "^0.6.0",
|
|
@@ -81,6 +81,6 @@
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"overrides": {
|
|
84
|
-
"effect": "4.0.0-beta.
|
|
84
|
+
"effect": "4.0.0-beta.42"
|
|
85
85
|
}
|
|
86
86
|
}
|
package/v3/dist/actor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
|
|
2
|
+
import { ExtractReply, ReplyTypeBrand } from "./internal/brands.js";
|
|
2
3
|
import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
|
|
3
4
|
import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
|
|
4
5
|
import { BuiltMachine, Machine, MachineRef } from "./machine.js";
|
|
@@ -59,11 +60,11 @@ interface ActorRef<State extends {
|
|
|
59
60
|
*/
|
|
60
61
|
readonly call: (event: Event) => Effect.Effect<ProcessEventResult<State>>;
|
|
61
62
|
/**
|
|
62
|
-
* Typed request-reply.
|
|
63
|
-
*
|
|
63
|
+
* Typed request-reply. Accepts only events with a reply schema
|
|
64
|
+
* (defined via `Event.reply()`). Return type is inferred from the schema.
|
|
64
65
|
* Fails with NoReplyError if the handler doesn't provide a reply.
|
|
65
66
|
*/
|
|
66
|
-
readonly ask: <
|
|
67
|
+
readonly ask: <E extends Event & ReplyTypeBrand<unknown>>(event: E) => Effect.Effect<ExtractReply<E>, NoReplyError | ActorStoppedError>;
|
|
67
68
|
/** Observable state. */
|
|
68
69
|
readonly state: SubscriptionRef.SubscriptionRef<State>;
|
|
69
70
|
/** Stop the actor gracefully. */
|
package/v3/dist/actor.js
CHANGED
|
@@ -3,7 +3,7 @@ import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
|
|
|
3
3
|
import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
|
|
4
4
|
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
5
5
|
import { processEventCore, resolveTransition, runSpawnEffects, shouldPostpone } from "./internal/transition.js";
|
|
6
|
-
import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect";
|
|
6
|
+
import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Runtime, Schema, Scope, Stream, SubscriptionRef } from "effect";
|
|
7
7
|
//#region src/actor.ts
|
|
8
8
|
/**
|
|
9
9
|
* Actor system: spawning, lifecycle, and event processing.
|
|
@@ -276,6 +276,7 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
|
|
|
276
276
|
lifecycleRan: false,
|
|
277
277
|
isFinal: false,
|
|
278
278
|
hasReply: false,
|
|
279
|
+
deferReply: false,
|
|
279
280
|
reply: void 0,
|
|
280
281
|
postponed: true
|
|
281
282
|
};
|
|
@@ -296,8 +297,19 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
|
|
|
296
297
|
yield* Deferred.succeed(queued.reply, result);
|
|
297
298
|
break;
|
|
298
299
|
case "ask":
|
|
299
|
-
if (result.hasReply)
|
|
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
|
}));
|
|
@@ -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 };
|