effect-machine 0.1.0 → 0.2.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 +17 -0
- package/package.json +1 -1
- package/src/actor.ts +544 -455
- package/src/cluster/entity-machine.ts +81 -82
- package/src/index.ts +1 -0
- package/src/inspection.ts +17 -0
- package/src/internal/inspection.ts +18 -0
- package/src/internal/transition.ts +150 -94
- package/src/machine.ts +139 -71
- package/src/persistence/adapter.ts +4 -3
- package/src/persistence/adapters/in-memory.ts +201 -182
- package/src/persistence/persistent-actor.ts +582 -386
- package/src/testing.ts +92 -98
|
@@ -23,26 +23,29 @@ const make = Effect.gen(function* () {
|
|
|
23
23
|
const storage = yield* Ref.make(new Map<string, ActorStorage>());
|
|
24
24
|
const registry = yield* Ref.make(new Map<string, ActorMetadata>());
|
|
25
25
|
|
|
26
|
-
const getOrCreateStorage = (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
const getOrCreateStorage = Effect.fn("effect-machine.persistence.inMemory.getOrCreateStorage")(
|
|
27
|
+
function* (id: string) {
|
|
28
|
+
return yield* Ref.modify(storage, (map) => {
|
|
29
|
+
const existing = map.get(id);
|
|
30
|
+
if (existing !== undefined) {
|
|
31
|
+
return [existing, map];
|
|
32
|
+
}
|
|
33
|
+
const newStorage: ActorStorage = {
|
|
34
|
+
snapshot: Option.none(),
|
|
35
|
+
events: [],
|
|
36
|
+
};
|
|
37
|
+
const newMap = new Map(map);
|
|
38
|
+
newMap.set(id, newStorage);
|
|
39
|
+
return [newStorage, newMap];
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
);
|
|
40
43
|
|
|
41
|
-
const updateStorage = (
|
|
44
|
+
const updateStorage = Effect.fn("effect-machine.persistence.inMemory.updateStorage")(function* (
|
|
42
45
|
id: string,
|
|
43
46
|
update: (storage: ActorStorage) => ActorStorage,
|
|
44
|
-
)
|
|
45
|
-
Ref.update(storage, (map) => {
|
|
47
|
+
) {
|
|
48
|
+
yield* Ref.update(storage, (map) => {
|
|
46
49
|
const existing = map.get(id);
|
|
47
50
|
if (existing === undefined) {
|
|
48
51
|
return map;
|
|
@@ -51,197 +54,213 @@ const make = Effect.gen(function* () {
|
|
|
51
54
|
newMap.set(id, update(existing));
|
|
52
55
|
return newMap;
|
|
53
56
|
});
|
|
57
|
+
});
|
|
54
58
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
): Effect.Effect<void, PersistenceError | VersionConflictError> =>
|
|
61
|
-
Effect.gen(function* () {
|
|
62
|
-
const actorStorage = yield* getOrCreateStorage(id);
|
|
59
|
+
const saveSnapshot = Effect.fn("effect-machine.persistence.inMemory.saveSnapshot")(function* <
|
|
60
|
+
S,
|
|
61
|
+
SI,
|
|
62
|
+
>(id: string, snapshot: Snapshot<S>, schema: Schema.Schema<S, SI, never>) {
|
|
63
|
+
const actorStorage = yield* getOrCreateStorage(id);
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// Encode state using schema
|
|
79
|
-
const encoded = yield* Schema.encode(schema)(snapshot.state).pipe(
|
|
80
|
-
Effect.mapError(
|
|
81
|
-
(cause) =>
|
|
82
|
-
new PersistenceError({
|
|
83
|
-
operation: "saveSnapshot",
|
|
84
|
-
actorId: id,
|
|
85
|
-
cause,
|
|
86
|
-
message: "Failed to encode state",
|
|
87
|
-
}),
|
|
88
|
-
),
|
|
89
|
-
);
|
|
65
|
+
// Optimistic locking: check version
|
|
66
|
+
// Reject only if trying to save an older version (strict <)
|
|
67
|
+
// Same-version saves are idempotent (allow retries/multiple callers)
|
|
68
|
+
if (Option.isSome(actorStorage.snapshot)) {
|
|
69
|
+
const existingVersion = actorStorage.snapshot.value.version;
|
|
70
|
+
if (snapshot.version < existingVersion) {
|
|
71
|
+
return yield* new VersionConflictError({
|
|
72
|
+
actorId: id,
|
|
73
|
+
expectedVersion: existingVersion,
|
|
74
|
+
actualVersion: snapshot.version,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
90
78
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
79
|
+
// Encode state using schema
|
|
80
|
+
const encoded = yield* Schema.encode(schema)(snapshot.state).pipe(
|
|
81
|
+
Effect.mapError(
|
|
82
|
+
(cause) =>
|
|
83
|
+
new PersistenceError({
|
|
84
|
+
operation: "saveSnapshot",
|
|
85
|
+
actorId: id,
|
|
86
|
+
cause,
|
|
87
|
+
message: "Failed to encode state",
|
|
97
88
|
}),
|
|
98
|
-
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
yield* updateStorage(id, (s) => ({
|
|
93
|
+
...s,
|
|
94
|
+
snapshot: Option.some({
|
|
95
|
+
data: encoded,
|
|
96
|
+
version: snapshot.version,
|
|
97
|
+
timestamp: snapshot.timestamp,
|
|
99
98
|
}),
|
|
99
|
+
}));
|
|
100
|
+
});
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const actorStorage = yield* getOrCreateStorage(id);
|
|
102
|
+
const loadSnapshot = Effect.fn("effect-machine.persistence.inMemory.loadSnapshot")(function* <
|
|
103
|
+
S,
|
|
104
|
+
SI,
|
|
105
|
+
>(id: string, schema: Schema.Schema<S, SI, never>) {
|
|
106
|
+
const actorStorage = yield* getOrCreateStorage(id);
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
if (Option.isNone(actorStorage.snapshot)) {
|
|
109
|
+
return Option.none();
|
|
110
|
+
}
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
const stored = actorStorage.snapshot.value;
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
// Decode state using schema
|
|
115
|
+
const decoded = yield* Schema.decode(schema)(stored.data as SI).pipe(
|
|
116
|
+
Effect.mapError(
|
|
117
|
+
(cause) =>
|
|
118
|
+
new PersistenceError({
|
|
119
|
+
operation: "loadSnapshot",
|
|
120
|
+
actorId: id,
|
|
121
|
+
cause,
|
|
122
|
+
message: "Failed to decode state",
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
return Option.some({
|
|
128
|
+
state: decoded,
|
|
129
|
+
version: stored.version,
|
|
130
|
+
timestamp: stored.timestamp,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
Effect.gen(function* () {
|
|
140
|
-
yield* getOrCreateStorage(id);
|
|
134
|
+
const appendEvent = Effect.fn("effect-machine.persistence.inMemory.appendEvent")(function* <
|
|
135
|
+
E,
|
|
136
|
+
EI,
|
|
137
|
+
>(id: string, event: PersistedEvent<E>, schema: Schema.Schema<E, EI, never>) {
|
|
138
|
+
yield* getOrCreateStorage(id);
|
|
141
139
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
140
|
+
// Encode event using schema
|
|
141
|
+
const encoded = yield* Schema.encode(schema)(event.event).pipe(
|
|
142
|
+
Effect.mapError(
|
|
143
|
+
(cause) =>
|
|
144
|
+
new PersistenceError({
|
|
145
|
+
operation: "appendEvent",
|
|
146
|
+
actorId: id,
|
|
147
|
+
cause,
|
|
148
|
+
message: "Failed to encode event",
|
|
149
|
+
}),
|
|
150
|
+
),
|
|
151
|
+
);
|
|
154
152
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
153
|
+
yield* updateStorage(id, (s) => ({
|
|
154
|
+
...s,
|
|
155
|
+
events: [
|
|
156
|
+
...s.events,
|
|
157
|
+
{
|
|
158
|
+
data: encoded,
|
|
159
|
+
version: event.version,
|
|
160
|
+
timestamp: event.timestamp,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
}));
|
|
164
|
+
});
|
|
167
165
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const actorStorage = yield* getOrCreateStorage(id);
|
|
166
|
+
const loadEvents = Effect.fn("effect-machine.persistence.inMemory.loadEvents")(function* <E, EI>(
|
|
167
|
+
id: string,
|
|
168
|
+
schema: Schema.Schema<E, EI, never>,
|
|
169
|
+
afterVersion?: number,
|
|
170
|
+
) {
|
|
171
|
+
const actorStorage = yield* getOrCreateStorage(id);
|
|
175
172
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
173
|
+
// Single pass - skip filtered events inline instead of creating intermediate array
|
|
174
|
+
const decoded: PersistedEvent<E>[] = [];
|
|
175
|
+
for (const stored of actorStorage.events) {
|
|
176
|
+
if (afterVersion !== undefined && stored.version <= afterVersion) continue;
|
|
180
177
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
178
|
+
const event = yield* Schema.decode(schema)(stored.data as EI).pipe(
|
|
179
|
+
Effect.mapError(
|
|
180
|
+
(cause) =>
|
|
181
|
+
new PersistenceError({
|
|
182
|
+
operation: "loadEvents",
|
|
183
|
+
actorId: id,
|
|
184
|
+
cause,
|
|
185
|
+
message: "Failed to decode event",
|
|
186
|
+
}),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
decoded.push({
|
|
190
|
+
event,
|
|
191
|
+
version: stored.version,
|
|
192
|
+
timestamp: stored.timestamp,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
198
195
|
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
return decoded;
|
|
197
|
+
});
|
|
201
198
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
199
|
+
const deleteActor = Effect.fn("effect-machine.persistence.inMemory.deleteActor")(function* (
|
|
200
|
+
id: string,
|
|
201
|
+
) {
|
|
202
|
+
yield* Ref.update(storage, (map) => {
|
|
203
|
+
const newMap = new Map(map);
|
|
204
|
+
newMap.delete(id);
|
|
205
|
+
return newMap;
|
|
206
|
+
});
|
|
207
|
+
// Also delete metadata
|
|
208
|
+
yield* Ref.update(registry, (map) => {
|
|
209
|
+
const newMap = new Map(map);
|
|
210
|
+
newMap.delete(id);
|
|
211
|
+
return newMap;
|
|
212
|
+
});
|
|
213
|
+
});
|
|
216
214
|
|
|
217
|
-
|
|
215
|
+
const listActors = Effect.fn("effect-machine.persistence.inMemory.listActors")(function* () {
|
|
216
|
+
const map = yield* Ref.get(registry);
|
|
217
|
+
return Array.from(map.values());
|
|
218
|
+
});
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
const saveMetadata = Effect.fn("effect-machine.persistence.inMemory.saveMetadata")(function* (
|
|
221
|
+
metadata: ActorMetadata,
|
|
222
|
+
) {
|
|
223
|
+
yield* Ref.update(registry, (map) => {
|
|
224
|
+
const newMap = new Map(map);
|
|
225
|
+
newMap.set(metadata.id, metadata);
|
|
226
|
+
return newMap;
|
|
227
|
+
});
|
|
228
|
+
});
|
|
221
229
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
const deleteMetadata = Effect.fn("effect-machine.persistence.inMemory.deleteMetadata")(function* (
|
|
231
|
+
id: string,
|
|
232
|
+
) {
|
|
233
|
+
yield* Ref.update(registry, (map) => {
|
|
234
|
+
const newMap = new Map(map);
|
|
235
|
+
newMap.delete(id);
|
|
236
|
+
return newMap;
|
|
237
|
+
});
|
|
238
|
+
});
|
|
228
239
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
const loadMetadata = Effect.fn("effect-machine.persistence.inMemory.loadMetadata")(function* (
|
|
241
|
+
id: string,
|
|
242
|
+
) {
|
|
243
|
+
const map = yield* Ref.get(registry);
|
|
244
|
+
const meta = map.get(id);
|
|
245
|
+
return meta !== undefined ? Option.some(meta) : Option.none();
|
|
246
|
+
});
|
|
235
247
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
248
|
+
const adapter: PersistenceAdapter = {
|
|
249
|
+
saveSnapshot,
|
|
250
|
+
loadSnapshot,
|
|
251
|
+
appendEvent,
|
|
252
|
+
loadEvents,
|
|
253
|
+
deleteActor,
|
|
254
|
+
|
|
255
|
+
// Registry methods for actor discovery
|
|
256
|
+
listActors,
|
|
257
|
+
saveMetadata,
|
|
258
|
+
deleteMetadata,
|
|
259
|
+
loadMetadata,
|
|
241
260
|
};
|
|
242
261
|
|
|
243
262
|
return adapter;
|
|
244
|
-
});
|
|
263
|
+
}).pipe(Effect.withSpan("effect-machine.persistence.inMemory.make"));
|
|
245
264
|
|
|
246
265
|
/**
|
|
247
266
|
* Create an in-memory persistence adapter effect.
|