@systemfsoftware/xstate-effect 0.0.0-stage → 0.1.0-beta.8
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/CHANGELOG.md +7 -0
- package/LICENSE +22 -0
- package/README.md +262 -2
- package/dist/atom.d.ts +53 -0
- package/dist/atom.mjs +64 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.mjs +429 -0
- package/dist/state-CNrCEIpU.mjs +607 -0
- package/dist/state-Ch1-WxXd.d.ts +447 -0
- package/package.json +60 -3
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import { Cause, Clock, Context, Data, Duration, Effect, Exit, Fiber, Queue, Scope } from "effect";
|
|
2
|
+
import { dual } from "effect/Function";
|
|
3
|
+
import { deliverEvent, isMachineSnapshot, stopActor, terminateActor } from "@systemfsoftware/xstate";
|
|
4
|
+
import { createDurable } from "@systemfsoftware/xstate/durable";
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
/**
|
|
7
|
+
* The failure an Effect-backed actor reports when its Effect was interrupted
|
|
8
|
+
* by something other than the actor being stopped, such as `Effect.interrupt`,
|
|
9
|
+
* losing an `Effect.race`, or an `Effect.timeout` that interrupts.
|
|
10
|
+
*/
|
|
11
|
+
var EffectInterruptedError = class extends Data.TaggedError("EffectInterruptedError") {
|
|
12
|
+
get message() {
|
|
13
|
+
return "Effect was interrupted before the actor completed";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Reported by `waitFor` when the actor stops or errors before a snapshot
|
|
18
|
+
* matches, and by `join` when the actor stops without output. `join` reports
|
|
19
|
+
* an errored actor's own `snapshot.error` instead.
|
|
20
|
+
*/
|
|
21
|
+
var ActorStoppedError = class extends Data.TaggedError("ActorStoppedError") {
|
|
22
|
+
get message() {
|
|
23
|
+
return `Actor "${this.actorId}" ${this.snapshot.status === "error" ? "errored" : "stopped"} before completing`;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var ActorFailedError = class extends Data.TaggedError("@systemfsoftware/xstate-effect/errors/ActorFailedError") {
|
|
27
|
+
get message() {
|
|
28
|
+
return "The actor failed";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Reported by the `send` atom of `createActorAtoms` when an event is sent
|
|
33
|
+
* before the actor's runtime has finished building.
|
|
34
|
+
*/
|
|
35
|
+
var NotReadyError = class extends Data.TaggedError("NotReadyError") {
|
|
36
|
+
get message() {
|
|
37
|
+
return "The actor is not ready yet";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
// src/actorScope.ts
|
|
41
|
+
const ActorScopeService = Context.Service()("@systemfsoftware/xstate-effect/actorScope");
|
|
42
|
+
/** The owning Effect actor's scope, supplied by `createEffectActor`. */
|
|
43
|
+
var ActorScope = class extends ActorScopeService {};
|
|
44
|
+
/**
|
|
45
|
+
* Acquires resources in the owning actor's scope. Their finalizers run when
|
|
46
|
+
* that actor stops, even if the invoking task finishes or is cancelled first.
|
|
47
|
+
*/
|
|
48
|
+
function withActorScope(effect) {
|
|
49
|
+
return ActorScope.use((scope) => Scope.provide(effect, scope));
|
|
50
|
+
}
|
|
51
|
+
// src/reportUnhandledError.ts
|
|
52
|
+
const reportUnhandledError = (error) => {
|
|
53
|
+
Effect.runCallback(Effect.sleep(Duration.millis(1)), { onExit: () => {
|
|
54
|
+
throw error;
|
|
55
|
+
} });
|
|
56
|
+
};
|
|
57
|
+
// src/effectActor.ts
|
|
58
|
+
const actionFailure = Symbol.for("@xstate/effect/actionFailure");
|
|
59
|
+
function isActionFailure(value) {
|
|
60
|
+
return typeof value === "object" && value !== null && actionFailure in value;
|
|
61
|
+
}
|
|
62
|
+
const symbolObservable = (() => typeof Symbol === "function" && Symbol.observable || "@@observable")();
|
|
63
|
+
/**
|
|
64
|
+
* Calls a listener and reports an exception it throws without letting it
|
|
65
|
+
* escape into the interpreter, matching core's `safeCall`.
|
|
66
|
+
*/
|
|
67
|
+
const safeCall = dual(2, (fn, arg) => {
|
|
68
|
+
try {
|
|
69
|
+
fn?.(arg);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
reportUnhandledError(err);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
function toObserver(nextHandler, errorHandler, completionHandler) {
|
|
75
|
+
if (typeof nextHandler === "object") return nextHandler;
|
|
76
|
+
return {
|
|
77
|
+
next: nextHandler,
|
|
78
|
+
error: errorHandler,
|
|
79
|
+
complete: completionHandler
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The handle `createEffectActor` returns. It implements XState's `ActorRef`
|
|
84
|
+
* contract (`send`, `getSnapshot`, `subscribe`, `on`), so it works with
|
|
85
|
+
* `useSelector`, this package's actor functions and the inspection APIs, while
|
|
86
|
+
* the actor itself is driven by an Effect fiber over pure transitions.
|
|
87
|
+
*/
|
|
88
|
+
var EffectActor = class {
|
|
89
|
+
logic;
|
|
90
|
+
_mailbox;
|
|
91
|
+
_stopExecution;
|
|
92
|
+
_inspectors;
|
|
93
|
+
id;
|
|
94
|
+
address;
|
|
95
|
+
sessionId;
|
|
96
|
+
/** The actor system that hosts this actor and its children. */
|
|
97
|
+
system;
|
|
98
|
+
/** @internal */
|
|
99
|
+
_root;
|
|
100
|
+
_snapshot;
|
|
101
|
+
_observers = /* @__PURE__ */ new Set();
|
|
102
|
+
_listeners = /* @__PURE__ */ new Map();
|
|
103
|
+
_settled = false;
|
|
104
|
+
constructor(logic, root, snapshot, _mailbox, _stopExecution, _inspectors) {
|
|
105
|
+
this.logic = logic;
|
|
106
|
+
this._mailbox = _mailbox;
|
|
107
|
+
this._stopExecution = _stopExecution;
|
|
108
|
+
this._inspectors = _inspectors;
|
|
109
|
+
this._root = root;
|
|
110
|
+
this.id = root.id;
|
|
111
|
+
this.address = root.address;
|
|
112
|
+
this.sessionId = root.sessionId;
|
|
113
|
+
this.system = root.system;
|
|
114
|
+
this._snapshot = snapshot;
|
|
115
|
+
}
|
|
116
|
+
getSnapshot() {
|
|
117
|
+
return this._snapshot;
|
|
118
|
+
}
|
|
119
|
+
getPersistedSnapshot() {
|
|
120
|
+
return this.logic.getPersistedSnapshot(this._snapshot);
|
|
121
|
+
}
|
|
122
|
+
/** Sends an event; the actor processes it on its own fiber. */
|
|
123
|
+
send(event) {
|
|
124
|
+
if (this._settled) {
|
|
125
|
+
this.system.deadLetter(void 0, this._root, event, "stopped");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
Queue.offerUnsafe(this._mailbox, event);
|
|
129
|
+
}
|
|
130
|
+
/** Stops the actor, its children and every Effect it hosts. */
|
|
131
|
+
stop() {
|
|
132
|
+
this._stopExecution();
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
subscribe(nextListenerOrObserver, errorListener, completeListener) {
|
|
136
|
+
const observer = toObserver(nextListenerOrObserver, errorListener, completeListener);
|
|
137
|
+
if (this._settled) {
|
|
138
|
+
const snapshot = this._snapshot;
|
|
139
|
+
if (snapshot.status === "error") safeCall(observer.error, snapshot.error);
|
|
140
|
+
else safeCall(observer.complete, void 0);
|
|
141
|
+
return { unsubscribe: () => {} };
|
|
142
|
+
}
|
|
143
|
+
this._observers.add(observer);
|
|
144
|
+
return { unsubscribe: () => {
|
|
145
|
+
this._observers.delete(observer);
|
|
146
|
+
} };
|
|
147
|
+
}
|
|
148
|
+
on(type, handler) {
|
|
149
|
+
let listeners = this._listeners.get(type);
|
|
150
|
+
if (listeners === void 0) {
|
|
151
|
+
listeners = /* @__PURE__ */ new Set();
|
|
152
|
+
this._listeners.set(type, listeners);
|
|
153
|
+
}
|
|
154
|
+
const listener = handler;
|
|
155
|
+
listeners.add(listener);
|
|
156
|
+
return { unsubscribe: () => {
|
|
157
|
+
listeners.delete(listener);
|
|
158
|
+
} };
|
|
159
|
+
}
|
|
160
|
+
/** @internal Delivers an emitted event to `on` listeners. */
|
|
161
|
+
_emit(event) {
|
|
162
|
+
const listeners = [...this._listeners.get(event.type) ?? [], ...this._listeners.get("*") ?? []];
|
|
163
|
+
for (const listener of listeners) safeCall(listener, event);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Observes the inspection events of this actor and its children: every
|
|
167
|
+
* actor creation and transition of the execution.
|
|
168
|
+
*/
|
|
169
|
+
inspect(observer) {
|
|
170
|
+
const handler = typeof observer === "function" ? observer : (event) => observer.next?.(event);
|
|
171
|
+
this._inspectors.add(handler);
|
|
172
|
+
return { unsubscribe: () => {
|
|
173
|
+
this._inspectors.delete(handler);
|
|
174
|
+
} };
|
|
175
|
+
}
|
|
176
|
+
[symbolObservable]() {
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
toJSON() {
|
|
180
|
+
return {
|
|
181
|
+
xstate$$type: 1,
|
|
182
|
+
id: this.id
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** @internal Publishes a snapshot produced by the execution loop. */
|
|
186
|
+
_publish(snapshot) {
|
|
187
|
+
this._snapshot = snapshot;
|
|
188
|
+
if (snapshot.status === "active") {
|
|
189
|
+
for (const observer of this._observers) safeCall(observer.next, snapshot);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this._settle(snapshot);
|
|
193
|
+
}
|
|
194
|
+
/** @internal Marks the actor stopped and notifies observers. */
|
|
195
|
+
_settle(snapshot) {
|
|
196
|
+
if (this._settled) return;
|
|
197
|
+
this._snapshot = snapshot;
|
|
198
|
+
this._settled = true;
|
|
199
|
+
const observers = [...this._observers];
|
|
200
|
+
this._observers.clear();
|
|
201
|
+
const status = snapshot.status;
|
|
202
|
+
if (status === "done") for (const observer of observers) safeCall(observer.next, snapshot);
|
|
203
|
+
for (const observer of observers) if (status === "error") safeCall(observer.error, snapshot.error);
|
|
204
|
+
else safeCall(observer.complete, void 0);
|
|
205
|
+
}
|
|
206
|
+
/** @internal */
|
|
207
|
+
get _isSettled() {
|
|
208
|
+
return this._settled;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
// src/internal.ts
|
|
212
|
+
const effectHosts = /* @__PURE__ */ new WeakMap();
|
|
213
|
+
let ambientHost;
|
|
214
|
+
/**
|
|
215
|
+
* Runs `fn` with `host` as the ambient host, so Effects started synchronously
|
|
216
|
+
* inside it (declared actions executed by an execution loop) resolve their
|
|
217
|
+
* host without an identity binding.
|
|
218
|
+
*/
|
|
219
|
+
const withEffectHost = dual(2, (host, fn) => {
|
|
220
|
+
const previous = ambientHost;
|
|
221
|
+
ambientHost = host;
|
|
222
|
+
try {
|
|
223
|
+
return fn();
|
|
224
|
+
} finally {
|
|
225
|
+
ambientHost = previous;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
const createEffectHost = dual(2, (context, scope) => ({
|
|
229
|
+
context,
|
|
230
|
+
scope,
|
|
231
|
+
interruptors: /* @__PURE__ */ new Map(),
|
|
232
|
+
subscriptions: /* @__PURE__ */ new Map(),
|
|
233
|
+
fibers: /* @__PURE__ */ new Set()
|
|
234
|
+
}));
|
|
235
|
+
const bindEffectHost = dual(2, (target, host) => {
|
|
236
|
+
effectHosts.set(target, host);
|
|
237
|
+
});
|
|
238
|
+
function findEffectHost(actor) {
|
|
239
|
+
let current = actor;
|
|
240
|
+
while (current !== void 0) {
|
|
241
|
+
const host = effectHosts.get(current);
|
|
242
|
+
if (host !== void 0) return host;
|
|
243
|
+
current = current._parent;
|
|
244
|
+
}
|
|
245
|
+
return ambientHost;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Rejects Effect logic that a machine spawned inline. Only declared actors
|
|
249
|
+
* (`setup({ actors })`) and `invoke` sources are visible to
|
|
250
|
+
* `RequirementsFrom`, so anything else would infer `R = never` and fail
|
|
251
|
+
* later with a missing service.
|
|
252
|
+
*
|
|
253
|
+
* A spawned declared actor carries its registered key as `src`; an invoked
|
|
254
|
+
* child carries the id of the `invoke` entry that created it.
|
|
255
|
+
*/
|
|
256
|
+
function assertDeclaredLogic(actor) {
|
|
257
|
+
const self = actor;
|
|
258
|
+
const parent = self._parent;
|
|
259
|
+
const machine = parent?.logic;
|
|
260
|
+
if (machine?.sources === void 0 || machine.idMap === void 0 || typeof self.src === "string") return;
|
|
261
|
+
if (Object.values(machine.sources.actors ?? {}).includes(self.logic)) return;
|
|
262
|
+
for (const stateNode of machine.idMap.values()) for (const definition of stateNode.invoke ?? []) if (definition.id === self.id) return;
|
|
263
|
+
throw new Error(`Effect logic spawned by "${parent.id}" must be declared in setup({ actors }). Spawn a declared actor instead, for example enq.spawn(args.actors.name).`);
|
|
264
|
+
}
|
|
265
|
+
function requireEffectHost(actor) {
|
|
266
|
+
assertDeclaredLogic(actor);
|
|
267
|
+
const host = findEffectHost(actor);
|
|
268
|
+
if (host === void 0) throw new Error("Effect-backed actor logic must be created with createEffectActor().");
|
|
269
|
+
return host;
|
|
270
|
+
}
|
|
271
|
+
function untrackEffect(actor, host, interrupt) {
|
|
272
|
+
const interruptors = host.interruptors.get(actor);
|
|
273
|
+
if (interruptors === void 0) return;
|
|
274
|
+
interruptors.delete(interrupt);
|
|
275
|
+
if (interruptors.size === 0) {
|
|
276
|
+
host.interruptors.delete(actor);
|
|
277
|
+
host.subscriptions.get(actor)?.unsubscribe();
|
|
278
|
+
host.subscriptions.delete(actor);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function cleanupActorEffects(actor, host) {
|
|
282
|
+
const interruptors = host.interruptors.get(actor);
|
|
283
|
+
if (interruptors !== void 0) {
|
|
284
|
+
host.interruptors.delete(actor);
|
|
285
|
+
for (const interrupt of interruptors) interrupt();
|
|
286
|
+
}
|
|
287
|
+
host.subscriptions.get(actor)?.unsubscribe();
|
|
288
|
+
host.subscriptions.delete(actor);
|
|
289
|
+
}
|
|
290
|
+
function trackEffect(actor, host, interrupt) {
|
|
291
|
+
let interruptors = host.interruptors.get(actor);
|
|
292
|
+
if (interruptors === void 0) {
|
|
293
|
+
interruptors = /* @__PURE__ */ new Set();
|
|
294
|
+
host.interruptors.set(actor, interruptors);
|
|
295
|
+
host.subscriptions.set(actor, actor.subscribe({
|
|
296
|
+
passive: true,
|
|
297
|
+
error: () => cleanupActorEffects(actor, host),
|
|
298
|
+
complete: () => cleanupActorEffects(actor, host)
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
interruptors.add(interrupt);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Runs an Effect in the actor's host context. The Effect is interrupted when
|
|
305
|
+
* the actor stops or when the returned function is called; `onExit` is not
|
|
306
|
+
* called after that.
|
|
307
|
+
*
|
|
308
|
+
* The fiber belongs to the host scope, so closing the owning actor also
|
|
309
|
+
* waits for asynchronous invocation finalizers.
|
|
310
|
+
*/
|
|
311
|
+
const startHostedEffect = dual(4, (actor, effect, spanName, onExit) => {
|
|
312
|
+
const host = requireEffectHost(actor);
|
|
313
|
+
let active = true;
|
|
314
|
+
const traced = Effect.withSpan(spanName, { attributes: {
|
|
315
|
+
"xstate.actor.id": actor.id,
|
|
316
|
+
"xstate.actor.address": actor.address
|
|
317
|
+
} }, { captureStackTrace: false })(effect);
|
|
318
|
+
const cancel = () => {
|
|
319
|
+
if (!active) return;
|
|
320
|
+
active = false;
|
|
321
|
+
untrackEffect(actor, host, cancel);
|
|
322
|
+
interrupt();
|
|
323
|
+
};
|
|
324
|
+
const fiber = Fiber.runIn(Effect.runForkWith(host.context)(traced), host.scope);
|
|
325
|
+
const interrupt = () => fiber.interruptUnsafe();
|
|
326
|
+
host.fibers.add(fiber);
|
|
327
|
+
fiber.addObserver((exit) => {
|
|
328
|
+
host.fibers.delete(fiber);
|
|
329
|
+
if (!active) return;
|
|
330
|
+
active = false;
|
|
331
|
+
untrackEffect(actor, host, cancel);
|
|
332
|
+
onExit(exit);
|
|
333
|
+
});
|
|
334
|
+
if (active) trackEffect(actor, host, cancel);
|
|
335
|
+
return cancel;
|
|
336
|
+
});
|
|
337
|
+
/**
|
|
338
|
+
* Runs an Effect in the actor's host context and settles when it exits.
|
|
339
|
+
* Interruption settles without error; failures and defects reject with the
|
|
340
|
+
* squashed cause so the actor's error handling can observe them.
|
|
341
|
+
*/
|
|
342
|
+
const runHostedEffect = dual(3, (actor, effect, spanName) => Effect.runPromise(Effect.callback((resume) => {
|
|
343
|
+
startHostedEffect(actor, effect, spanName, (exit) => {
|
|
344
|
+
if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) resume(Effect.void);
|
|
345
|
+
else resume(Effect.failCause(exit.cause));
|
|
346
|
+
});
|
|
347
|
+
})));
|
|
348
|
+
/**
|
|
349
|
+
* Finishes hosted task cleanup before closing the owning actor's resources.
|
|
350
|
+
* `createEffectActor`'s release awaits this fiber before its parent scope closes.
|
|
351
|
+
*/
|
|
352
|
+
function closeEffectHost(host) {
|
|
353
|
+
if (host.closing !== void 0) return;
|
|
354
|
+
for (const [actor, interruptors] of host.interruptors) {
|
|
355
|
+
host.interruptors.delete(actor);
|
|
356
|
+
for (const interrupt of interruptors) interrupt();
|
|
357
|
+
host.subscriptions.get(actor)?.unsubscribe();
|
|
358
|
+
host.subscriptions.delete(actor);
|
|
359
|
+
}
|
|
360
|
+
host.closing = Effect.runForkWith(host.context)(Effect.forEach([...host.fibers], (fiber) => Fiber.interrupt(fiber), {
|
|
361
|
+
concurrency: "unbounded",
|
|
362
|
+
discard: true
|
|
363
|
+
}).pipe(Effect.andThen(Scope.close(host.scope, Exit.void))));
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Delivers a stream item to the parent as an event. This runs outside a
|
|
367
|
+
* transition, so it uses the system relay like core's observable logic does.
|
|
368
|
+
*/
|
|
369
|
+
const relayToParent = dual(2, (actor, event) => {
|
|
370
|
+
const actorWithParent = actor;
|
|
371
|
+
if (actorWithParent._parent !== void 0) actor.system._relay(actorWithParent, actorWithParent._parent, event);
|
|
372
|
+
});
|
|
373
|
+
// src/createEffectActor.ts
|
|
374
|
+
const XSTATE_TIMER = "xstate.timer";
|
|
375
|
+
const XSTATE_INIT = "@xstate.init";
|
|
376
|
+
/**
|
|
377
|
+
* Creates and starts an actor as an Effect interpreter over pure transitions.
|
|
378
|
+
*
|
|
379
|
+
* Each step is `transition(snapshot, event)`, a pure function that returns
|
|
380
|
+
* the next snapshot and the actions to run. An Effect fiber owns the loop:
|
|
381
|
+
* the mailbox is a `Queue`, timers are `Effect.sleep` fibers on the Effect
|
|
382
|
+
* `Clock`, and declared Effect actions run as forked Effects in the actor's
|
|
383
|
+
* `Scope` with the services captured here. Child actors are started as live
|
|
384
|
+
* XState actors whose Effects run in the same host.
|
|
385
|
+
*
|
|
386
|
+
* The actor is a scoped resource: it stops, and every Effect it hosts is
|
|
387
|
+
* interrupted, when the enclosing `Scope` closes. The returned Effect never
|
|
388
|
+
* fails; the actor's own outcome is its snapshot status, read with `join`.
|
|
389
|
+
*/
|
|
390
|
+
function createEffectActor(logic, ...[options]) {
|
|
391
|
+
return Effect.acquireRelease(Effect.gen(function* () {
|
|
392
|
+
const parentScope = yield* Effect.scope;
|
|
393
|
+
const actorScope = yield* Scope.fork(parentScope);
|
|
394
|
+
const baseContext = yield* Effect.context();
|
|
395
|
+
const context = Context.add(Context.add(baseContext, Scope.Scope, actorScope), ActorScope, actorScope);
|
|
396
|
+
const host = createEffectHost(context, actorScope);
|
|
397
|
+
const runFork = Effect.runForkWith(context);
|
|
398
|
+
const runPromise = Effect.runPromiseWith(context);
|
|
399
|
+
const mailbox = yield* Queue.unbounded();
|
|
400
|
+
const timers = /* @__PURE__ */ new Map();
|
|
401
|
+
let stopped = false;
|
|
402
|
+
const isRoot = (candidate) => candidate.address === durable.rootAddress;
|
|
403
|
+
const offer = (item) => {
|
|
404
|
+
if (!stopped) Queue.offerUnsafe(mailbox, item);
|
|
405
|
+
};
|
|
406
|
+
const timerKey = (source, id) => `${source.sessionId}:${id}`;
|
|
407
|
+
const inspectors = /* @__PURE__ */ new Set();
|
|
408
|
+
let rootAnnounced = false;
|
|
409
|
+
const clock = Context.get(context, Clock.Clock);
|
|
410
|
+
const durable = createDurable(logic, {
|
|
411
|
+
now: () => clock.currentTimeMillisUnsafe(),
|
|
412
|
+
executeAction: (action, _metadata, runtime) => {
|
|
413
|
+
try {
|
|
414
|
+
const result = withEffectHost(host, () => action.exec(runtime));
|
|
415
|
+
if (result !== void 0 && typeof result.then === "function") Promise.resolve(result).catch((error) => {
|
|
416
|
+
offer({
|
|
417
|
+
[actionFailure]: true,
|
|
418
|
+
error
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
} catch (error) {
|
|
422
|
+
offer({
|
|
423
|
+
[actionFailure]: true,
|
|
424
|
+
error
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
spawnActor: (_source, child) => {
|
|
429
|
+
bindEffectHost(child, host);
|
|
430
|
+
},
|
|
431
|
+
startActor: (child) => {
|
|
432
|
+
child.start();
|
|
433
|
+
},
|
|
434
|
+
stopActor: (child) => {
|
|
435
|
+
if (!isRoot(child)) stopActor(child);
|
|
436
|
+
},
|
|
437
|
+
terminateActor: (child, termination) => {
|
|
438
|
+
if (!isRoot(child)) terminateActor(child, termination);
|
|
439
|
+
},
|
|
440
|
+
sendEvent: (source, target, event) => {
|
|
441
|
+
if (isRoot(target)) {
|
|
442
|
+
offer(event);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
deliverEvent(source, target, event);
|
|
446
|
+
},
|
|
447
|
+
emitEvent: (source, event) => {
|
|
448
|
+
if (isRoot(source)) {
|
|
449
|
+
actor._emit(event);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
source._emit(event);
|
|
453
|
+
},
|
|
454
|
+
scheduleTimer: (source, id, delay) => {
|
|
455
|
+
const key = timerKey(source, id);
|
|
456
|
+
timers.get(key)?.interruptUnsafe();
|
|
457
|
+
const fiber = Fiber.runIn(runFork(Effect.andThen(Effect.sleep(Duration.millis(delay)), Effect.sync(() => {
|
|
458
|
+
timers.delete(key);
|
|
459
|
+
const timerEvent = {
|
|
460
|
+
type: XSTATE_TIMER,
|
|
461
|
+
id
|
|
462
|
+
};
|
|
463
|
+
if (isRoot(source)) offer(timerEvent);
|
|
464
|
+
else deliverEvent(source, source, timerEvent);
|
|
465
|
+
}))), actorScope);
|
|
466
|
+
timers.set(key, fiber);
|
|
467
|
+
},
|
|
468
|
+
cancelTimer: (source, id) => {
|
|
469
|
+
const key = timerKey(source, id);
|
|
470
|
+
timers.get(key)?.interruptUnsafe();
|
|
471
|
+
timers.delete(key);
|
|
472
|
+
},
|
|
473
|
+
cancelAllTimers: (source) => {
|
|
474
|
+
for (const [key, fiber] of timers) if (key.startsWith(`${source.sessionId}:`)) {
|
|
475
|
+
fiber.interruptUnsafe();
|
|
476
|
+
timers.delete(key);
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
waitForEvent: () => runPromise(Queue.take(mailbox))
|
|
480
|
+
}, { inspect: (event) => {
|
|
481
|
+
if (event.type === "@xstate.actor" && event.actorRef.address === durable.rootAddress) {
|
|
482
|
+
if (rootAnnounced) return;
|
|
483
|
+
rootAnnounced = true;
|
|
484
|
+
}
|
|
485
|
+
for (const inspector of inspectors) safeCall(inspector, event);
|
|
486
|
+
} });
|
|
487
|
+
const errorSnapshot = (snapshot, error) => ({
|
|
488
|
+
...snapshot,
|
|
489
|
+
status: "error",
|
|
490
|
+
error
|
|
491
|
+
});
|
|
492
|
+
const stopChildren = (snapshot) => {
|
|
493
|
+
const children = snapshot.children;
|
|
494
|
+
for (const child of Object.values(children ?? {})) if (child !== void 0 && !isRoot(child)) stopActor(child);
|
|
495
|
+
};
|
|
496
|
+
const stop = () => {
|
|
497
|
+
if (stopped) return;
|
|
498
|
+
stopped = true;
|
|
499
|
+
for (const fiber of timers.values()) fiber.interruptUnsafe();
|
|
500
|
+
timers.clear();
|
|
501
|
+
const current = actor.getSnapshot();
|
|
502
|
+
if (current) stopChildren(current);
|
|
503
|
+
runFork(Queue.shutdown(mailbox));
|
|
504
|
+
if (!actor._isSettled) actor._settle({
|
|
505
|
+
...actor.getSnapshot(),
|
|
506
|
+
status: "stopped"
|
|
507
|
+
});
|
|
508
|
+
closeEffectHost(host);
|
|
509
|
+
};
|
|
510
|
+
let [snapshot, effects] = options?.snapshot !== void 0 ? durable.restore(options.snapshot) : durable.initialTransition(options?.input);
|
|
511
|
+
const initial = snapshot;
|
|
512
|
+
if (options?.snapshot !== void 0 && !isMachineSnapshot(initial) && initial.status === "active") {
|
|
513
|
+
const [started, startEffects] = durable.transition(snapshot, {
|
|
514
|
+
type: XSTATE_INIT,
|
|
515
|
+
input: "input" in initial ? initial.input : void 0
|
|
516
|
+
});
|
|
517
|
+
snapshot = started;
|
|
518
|
+
effects = [...effects, ...startEffects];
|
|
519
|
+
}
|
|
520
|
+
const root = durable.getActorRef(snapshot);
|
|
521
|
+
bindEffectHost(root, host);
|
|
522
|
+
rootAnnounced = true;
|
|
523
|
+
const actor = new EffectActor(logic, root, snapshot, mailbox, stop, inspectors);
|
|
524
|
+
bindEffectHost(actor, host);
|
|
525
|
+
const executeEffects = (batch) => Effect.promise(() => durable.executeEffects(batch).then(() => void 0, (error) => {
|
|
526
|
+
offer({
|
|
527
|
+
[actionFailure]: true,
|
|
528
|
+
error
|
|
529
|
+
});
|
|
530
|
+
}));
|
|
531
|
+
const loop = Effect.gen(function* () {
|
|
532
|
+
yield* executeEffects(effects);
|
|
533
|
+
while (snapshot.status === "active" && !stopped) {
|
|
534
|
+
const item = yield* Effect.promise(() => durable.waitForEvent().then((event) => event, () => void 0));
|
|
535
|
+
if (item === void 0 || stopped) break;
|
|
536
|
+
let event;
|
|
537
|
+
if (isActionFailure(item)) {
|
|
538
|
+
const errorEvent = logic.getExecutionErrorEvent?.(snapshot, item.error);
|
|
539
|
+
if (errorEvent === void 0) {
|
|
540
|
+
snapshot = errorSnapshot(snapshot, item.error);
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
event = errorEvent;
|
|
544
|
+
} else event = item;
|
|
545
|
+
const transitioned = yield* Effect.exit(Effect.sync(() => durable.transition(snapshot, event)));
|
|
546
|
+
if (Exit.isFailure(transitioned)) {
|
|
547
|
+
snapshot = errorSnapshot(snapshot, Cause.squash(transitioned.cause));
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
[snapshot, effects] = transitioned.value;
|
|
551
|
+
actor._publish(snapshot);
|
|
552
|
+
yield* executeEffects(effects);
|
|
553
|
+
}
|
|
554
|
+
if (!stopped) {
|
|
555
|
+
actor._publish(snapshot);
|
|
556
|
+
if (snapshot.status !== "active") {
|
|
557
|
+
stopChildren(snapshot);
|
|
558
|
+
closeEffectHost(host);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
yield* Effect.forkIn(loop, actorScope);
|
|
563
|
+
return {
|
|
564
|
+
actor,
|
|
565
|
+
host
|
|
566
|
+
};
|
|
567
|
+
}), ({ actor, host }) => Effect.gen(function* () {
|
|
568
|
+
actor.stop();
|
|
569
|
+
if (host.closing !== void 0) yield* Fiber.join(host.closing);
|
|
570
|
+
else yield* Scope.close(host.scope, Exit.void);
|
|
571
|
+
})).pipe(Effect.map(({ actor }) => actor));
|
|
572
|
+
}
|
|
573
|
+
// src/state.ts
|
|
574
|
+
/** The tag of the machine itself, used when the root state is parallel. */
|
|
575
|
+
const MACHINE_TAG = "(machine)";
|
|
576
|
+
/** Computes the dotted path tag of a state value. */
|
|
577
|
+
function stateTag(value, prefix = "") {
|
|
578
|
+
if (typeof value === "string") return prefix !== "" ? `${prefix}.${value}` : value;
|
|
579
|
+
const keys = Object.keys(value);
|
|
580
|
+
if (keys.length !== 1) return prefix !== "" ? prefix : MACHINE_TAG;
|
|
581
|
+
const key = keys[0];
|
|
582
|
+
return stateTag(value[key], prefix !== "" ? `${prefix}.${key}` : key);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Views a machine snapshot as a tagged union member for `Match.tag`,
|
|
586
|
+
* `Match.tags` or a `switch` on `_tag`.
|
|
587
|
+
*
|
|
588
|
+
* @example
|
|
589
|
+
*
|
|
590
|
+
* ```ts
|
|
591
|
+
* const view = Match.type<TaggedStateFrom<typeof snapshot>>().pipe(
|
|
592
|
+
* Match.tag('loading', ({ context }) => `Loading ${context.id}`),
|
|
593
|
+
* Match.tag('done', () => 'Done'),
|
|
594
|
+
* Match.exhaustive
|
|
595
|
+
* );
|
|
596
|
+
* view(taggedState(snapshot));
|
|
597
|
+
* ```
|
|
598
|
+
*/
|
|
599
|
+
function taggedState(snapshot) {
|
|
600
|
+
return {
|
|
601
|
+
_tag: stateTag(snapshot.value),
|
|
602
|
+
value: snapshot.value,
|
|
603
|
+
context: snapshot.context,
|
|
604
|
+
snapshot
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
export { startHostedEffect as a, withActorScope as c, EffectInterruptedError as d, NotReadyError as f, runHostedEffect as i, ActorFailedError as l, createEffectActor as n, EffectActor as o, relayToParent as r, ActorScope as s, taggedState as t, ActorStoppedError as u };
|