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