@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,702 @@
|
|
|
1
|
+
import { Schema, Effect, Stream, Exit, Cause, Queue } from 'effect';
|
|
2
|
+
import { createLogic, setup } from 'xstate';
|
|
3
|
+
import { s as startHostedEffect, r as relayToParent, E as EffectInterruptedError, A as ActorStoppedError, a as runHostedEffect } from './state-9a718be3.js';
|
|
4
|
+
export { A as ActorStoppedError, b as EffectActor, E as EffectInterruptedError, c as createEffectActor, t as taggedState } from './state-9a718be3.js';
|
|
5
|
+
import { dual } from 'effect/Function';
|
|
6
|
+
import 'xstate/durable';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* An Effect `Schema` accepted by this package. `setupEffect`, `fromEffect`,
|
|
10
|
+
* `fromEffectStream` and `fromEffectEventStream` convert it to a Standard
|
|
11
|
+
* Schema, so XState infers the decoded `Schema.Type` without an explicit call
|
|
12
|
+
* to `Schema.toStandardSchemaV1`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Either an Effect {@link EffectSchema} or a Standard Schema. Every schema
|
|
17
|
+
* position in this package accepts both, so Effect schemas and schemas from
|
|
18
|
+
* other libraries can be mixed in one machine.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The `schemas` option of `setupEffect`: XState's `SetupSchemas` with every
|
|
23
|
+
* schema position widened to {@link EffectSchemaLike}. Covers `context`,
|
|
24
|
+
* `input`, `output` and the per-key records for `events`, `internalEvents`,
|
|
25
|
+
* `emitted` and `children`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One node of the `states` option of `setupEffect`: the schemas declared for
|
|
30
|
+
* that state, and the same shape recursively for its child states. It is
|
|
31
|
+
* XState's `SetupStateSchema` with Effect schemas allowed in every schema
|
|
32
|
+
* position.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
function isStandardSchema(value) {
|
|
36
|
+
return !!value && (typeof value === 'object' || typeof value === 'function') && '~standard' in value;
|
|
37
|
+
}
|
|
38
|
+
function toStandardSchema(schema) {
|
|
39
|
+
if (Schema.isSchema(schema)) {
|
|
40
|
+
return Schema.toStandardSchemaV1(schema);
|
|
41
|
+
}
|
|
42
|
+
return schema;
|
|
43
|
+
}
|
|
44
|
+
function mapSchemaRecord(value) {
|
|
45
|
+
return Object.fromEntries(Object.keys(value).map(key => {
|
|
46
|
+
const entry = value[key];
|
|
47
|
+
if (Schema.isSchema(entry) || isStandardSchema(entry)) {
|
|
48
|
+
return [key, toStandardSchema(entry)];
|
|
49
|
+
}
|
|
50
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
51
|
+
return [key, mapSchemaRecord(entry)];
|
|
52
|
+
}
|
|
53
|
+
return [key, entry];
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
function toStandardSetupSchemas(schemas) {
|
|
57
|
+
return schemas ? mapSchemaRecord(schemas) : undefined;
|
|
58
|
+
}
|
|
59
|
+
function toStandardSetupStates(states) {
|
|
60
|
+
if (!states) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return Object.fromEntries(Object.keys(states).map(key => {
|
|
64
|
+
const state = states[key];
|
|
65
|
+
return [key, {
|
|
66
|
+
...state,
|
|
67
|
+
...(state.schemas ? {
|
|
68
|
+
schemas: mapSchemaRecord(state.schemas)
|
|
69
|
+
} : undefined),
|
|
70
|
+
...(state.states ? {
|
|
71
|
+
states: toStandardSetupStates(state.states)
|
|
72
|
+
} : undefined)
|
|
73
|
+
}];
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const EFFECT_INIT = '@xstate.init';
|
|
78
|
+
const EFFECT_RESOLVE = 'xstate.effect.resolve';
|
|
79
|
+
const EFFECT_REJECT = 'xstate.effect.reject';
|
|
80
|
+
const EFFECT_NEXT = 'xstate.effect.next';
|
|
81
|
+
const EFFECT_COMPLETE = 'xstate.effect.complete';
|
|
82
|
+
const effectLogicBrand = Symbol.for('@xstate/effect/logic');
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Type-level marker attached to logic created by `fromEffect`,
|
|
86
|
+
* `fromEffectStream` and `fromEffectEventStream`. It carries the logic's
|
|
87
|
+
* failure type and its Effect service requirements, which is how
|
|
88
|
+
* `RequirementsFrom` collects the `R` channel of `createEffectActor` without
|
|
89
|
+
* inspecting the logic at runtime.
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Snapshot of a `fromEffect` actor. The actor holds no context of its own: it
|
|
94
|
+
* is `active` while the Effect runs, `done` with the Effect's success value as
|
|
95
|
+
* `output`, or `error` with the Effect's failure, defect or
|
|
96
|
+
* `EffectInterruptedError` as `error`.
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Argument passed to the function form of an Effect source. It gives the
|
|
101
|
+
* source the actor's `input`, its own reference (`self`), the actor `system`,
|
|
102
|
+
* and `emit` for publishing events that `actor.on(...)` and `emitted(actor)`
|
|
103
|
+
* observe.
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* What `fromEffect` accepts as its Effect: either an Effect value, or a
|
|
108
|
+
* function of {@link EffectSourceArgs} returning one. The function form is
|
|
109
|
+
* called once per actor start, so it sees that actor's input.
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Actor logic produced by `fromEffect` and `fromEffectEventStream`. It is
|
|
114
|
+
* ordinary XState actor logic carrying {@link EffectLogicBrand}, so a machine
|
|
115
|
+
* can invoke or spawn it, and `createEffectActor` can collect its
|
|
116
|
+
* requirements.
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Snapshot of a `fromEffectStream` actor. `context` holds the most recent
|
|
121
|
+
* stream item, or `undefined` before the first one. The actor reaches `done`
|
|
122
|
+
* with no output when the stream completes, and `error` when it fails.
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Actor logic produced by `fromEffectStream`. Its snapshot is an
|
|
127
|
+
* {@link EffectStreamSnapshot}, so the parent reads the latest item from the
|
|
128
|
+
* child's `context` rather than from events.
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
function toError(exit) {
|
|
132
|
+
if (Exit.isSuccess(exit)) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
if (Cause.hasInterruptsOnly(exit.cause)) {
|
|
136
|
+
return new EffectInterruptedError({
|
|
137
|
+
cause: exit.cause
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return Cause.squash(exit.cause);
|
|
141
|
+
}
|
|
142
|
+
function createSourceArgs(input, self, system) {
|
|
143
|
+
return {
|
|
144
|
+
input,
|
|
145
|
+
self,
|
|
146
|
+
system,
|
|
147
|
+
emit: event => {
|
|
148
|
+
void system.emitEvent(self, event);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function brandLogic(logic, error, requirements) {
|
|
153
|
+
Object.defineProperty(logic, effectLogicBrand, {
|
|
154
|
+
configurable: false,
|
|
155
|
+
enumerable: false,
|
|
156
|
+
value: {
|
|
157
|
+
error,
|
|
158
|
+
requirements
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return logic;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Creates actor logic that runs an Effect. The actor completes with the
|
|
166
|
+
* Effect's success value as its output and fails with the Effect's error, its
|
|
167
|
+
* squashed defect, or an `EffectInterruptedError` when the Effect interrupts
|
|
168
|
+
* itself. Stopping the actor interrupts the Effect without an error. Accepts
|
|
169
|
+
* an Effect, a function of {@link EffectSourceArgs} returning one, or a config
|
|
170
|
+
* object with `id`, `schemas`, `validator` and `effect`. The logic must be run
|
|
171
|
+
* under `createEffectActor`, which provides the Effect services it declares.
|
|
172
|
+
*/
|
|
173
|
+
|
|
174
|
+
function fromEffect(sourceOrConfig) {
|
|
175
|
+
const config = typeof sourceOrConfig === 'function' || Effect.isEffect(sourceOrConfig) ? {
|
|
176
|
+
effect: sourceOrConfig
|
|
177
|
+
} : sourceOrConfig;
|
|
178
|
+
const source = config.effect;
|
|
179
|
+
const schemas = config.schemas ? {
|
|
180
|
+
...(config.schemas.input ? {
|
|
181
|
+
input: toStandardSchema(config.schemas.input)
|
|
182
|
+
} : {}),
|
|
183
|
+
...(config.schemas.output ? {
|
|
184
|
+
output: toStandardSchema(config.schemas.output)
|
|
185
|
+
} : {})
|
|
186
|
+
} : undefined;
|
|
187
|
+
const logic = createLogic({
|
|
188
|
+
id: config.id,
|
|
189
|
+
validator: config.validator,
|
|
190
|
+
schemas,
|
|
191
|
+
context: undefined,
|
|
192
|
+
run: ({
|
|
193
|
+
event,
|
|
194
|
+
input,
|
|
195
|
+
self,
|
|
196
|
+
system
|
|
197
|
+
}, enq) => {
|
|
198
|
+
if (event.type === EFFECT_RESOLVE) {
|
|
199
|
+
return {
|
|
200
|
+
status: 'done',
|
|
201
|
+
output: event.output,
|
|
202
|
+
input: undefined
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (event.type === EFFECT_REJECT) {
|
|
206
|
+
return {
|
|
207
|
+
status: 'error',
|
|
208
|
+
error: event.error,
|
|
209
|
+
input: undefined
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (event.type !== EFFECT_INIT) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const effect = typeof source === 'function' ? source(createSourceArgs(input, self, system)) : source;
|
|
216
|
+
enq.effect(() => startHostedEffect(self, effect, 'fromEffect', exit => {
|
|
217
|
+
if (self.getSnapshot().status !== 'active') {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (Exit.isSuccess(exit)) {
|
|
221
|
+
self.send({
|
|
222
|
+
type: EFFECT_RESOLVE,
|
|
223
|
+
output: exit.value
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
self.send({
|
|
227
|
+
type: EFFECT_REJECT,
|
|
228
|
+
error: toError(exit)
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
return brandLogic(logic, undefined, undefined);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* What `fromEffectStream` and `fromEffectEventStream` accept as their stream:
|
|
239
|
+
* either a Stream value, or a function of {@link EffectSourceArgs} returning
|
|
240
|
+
* one. The function form is called once per actor start.
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
function resolveStreamConfig(sourceOrConfig) {
|
|
244
|
+
return typeof sourceOrConfig === 'function' || Stream.isStream(sourceOrConfig) ? {
|
|
245
|
+
stream: sourceOrConfig
|
|
246
|
+
} : sourceOrConfig;
|
|
247
|
+
}
|
|
248
|
+
function toLogicSchemas(schemas) {
|
|
249
|
+
return schemas?.input ? {
|
|
250
|
+
input: toStandardSchema(schemas.input)
|
|
251
|
+
} : undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Creates actor logic that runs a Stream and exposes its most recent item as
|
|
256
|
+
* the actor's `context`. The actor reaches `done` when the stream completes
|
|
257
|
+
* and `error` when it fails. Accepts a Stream, a function of
|
|
258
|
+
* {@link EffectSourceArgs} returning one, or a config object with `id`,
|
|
259
|
+
* `schemas`, `validator` and `stream`.
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
function fromEffectStream(sourceOrConfig) {
|
|
263
|
+
const config = resolveStreamConfig(sourceOrConfig);
|
|
264
|
+
const stream = config.stream;
|
|
265
|
+
const logic = createLogic({
|
|
266
|
+
id: config.id,
|
|
267
|
+
validator: config.validator,
|
|
268
|
+
schemas: toLogicSchemas(config.schemas),
|
|
269
|
+
context: undefined,
|
|
270
|
+
run: ({
|
|
271
|
+
event,
|
|
272
|
+
input,
|
|
273
|
+
self,
|
|
274
|
+
system
|
|
275
|
+
}, enq) => {
|
|
276
|
+
if (event.type === EFFECT_NEXT) {
|
|
277
|
+
return {
|
|
278
|
+
context: event.value
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (event.type === EFFECT_COMPLETE) {
|
|
282
|
+
return {
|
|
283
|
+
status: 'done',
|
|
284
|
+
input: undefined
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (event.type === EFFECT_REJECT) {
|
|
288
|
+
return {
|
|
289
|
+
status: 'error',
|
|
290
|
+
error: event.error,
|
|
291
|
+
input: undefined
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (event.type !== EFFECT_INIT) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const streamValue = typeof stream === 'function' ? stream(createSourceArgs(input, self, system)) : stream;
|
|
298
|
+
const consume = Stream.runForEach(streamValue, value => Effect.sync(() => {
|
|
299
|
+
if (self.getSnapshot().status === 'active') {
|
|
300
|
+
self.send({
|
|
301
|
+
type: EFFECT_NEXT,
|
|
302
|
+
value
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}));
|
|
306
|
+
enq.effect(() => startHostedEffect(self, consume, 'fromEffectStream', exit => {
|
|
307
|
+
if (self.getSnapshot().status !== 'active') {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (Exit.isSuccess(exit)) {
|
|
311
|
+
self.send({
|
|
312
|
+
type: EFFECT_COMPLETE
|
|
313
|
+
});
|
|
314
|
+
} else {
|
|
315
|
+
self.send({
|
|
316
|
+
type: EFFECT_REJECT,
|
|
317
|
+
error: toError(exit)
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
return brandLogic(logic, undefined, undefined);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Creates actor logic that runs a Stream of events and relays each item to the
|
|
328
|
+
* parent machine as an event, the way `fromEventObservable` does. The actor
|
|
329
|
+
* has no output: it reaches `done` when the stream completes and `error` when
|
|
330
|
+
* it fails. Accepts the same forms as `fromEffectStream`.
|
|
331
|
+
*/
|
|
332
|
+
|
|
333
|
+
function fromEffectEventStream(sourceOrConfig) {
|
|
334
|
+
const config = resolveStreamConfig(sourceOrConfig);
|
|
335
|
+
const stream = config.stream;
|
|
336
|
+
const logic = createLogic({
|
|
337
|
+
id: config.id,
|
|
338
|
+
validator: config.validator,
|
|
339
|
+
schemas: toLogicSchemas(config.schemas),
|
|
340
|
+
context: undefined,
|
|
341
|
+
run: ({
|
|
342
|
+
event,
|
|
343
|
+
input,
|
|
344
|
+
self,
|
|
345
|
+
system
|
|
346
|
+
}, enq) => {
|
|
347
|
+
if (event.type === EFFECT_COMPLETE) {
|
|
348
|
+
return {
|
|
349
|
+
status: 'done',
|
|
350
|
+
input: undefined
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (event.type === EFFECT_REJECT) {
|
|
354
|
+
return {
|
|
355
|
+
status: 'error',
|
|
356
|
+
error: event.error,
|
|
357
|
+
input: undefined
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (event.type !== EFFECT_INIT) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const streamValue = typeof stream === 'function' ? stream(createSourceArgs(input, self, system)) : stream;
|
|
364
|
+
const consume = Stream.runForEach(streamValue, value => Effect.sync(() => relayToParent(self, value)));
|
|
365
|
+
enq.effect(() => startHostedEffect(self, consume, 'fromEffectEventStream', exit => {
|
|
366
|
+
if (self.getSnapshot().status !== 'active') {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (Exit.isSuccess(exit)) {
|
|
370
|
+
self.send({
|
|
371
|
+
type: EFFECT_COMPLETE
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
self.send({
|
|
375
|
+
type: EFFECT_REJECT,
|
|
376
|
+
error: toError(exit)
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
return brandLogic(logic, undefined, undefined);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** The event type accepted by an actor's `send` method. */
|
|
386
|
+
|
|
387
|
+
/** The event type an actor emits through `actor.on(...)`. */
|
|
388
|
+
|
|
389
|
+
/** Options for {@link waitFor}. */
|
|
390
|
+
|
|
391
|
+
const noopSubscription = {
|
|
392
|
+
unsubscribe: () => {}
|
|
393
|
+
};
|
|
394
|
+
function actorId(actor) {
|
|
395
|
+
return actor.id ?? '(unknown)';
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Marks an already-errored actor's error as observed, so XState does not
|
|
400
|
+
* report it as unhandled: the caller consumes it as a typed failure.
|
|
401
|
+
*/
|
|
402
|
+
function observeError(actor) {
|
|
403
|
+
actor.subscribe({
|
|
404
|
+
error: () => {}
|
|
405
|
+
}).unsubscribe();
|
|
406
|
+
}
|
|
407
|
+
function stoppedError(actor) {
|
|
408
|
+
return new ActorStoppedError({
|
|
409
|
+
actorId: actorId(actor),
|
|
410
|
+
snapshot: actor.getSnapshot()
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function isActorRef(value) {
|
|
414
|
+
return typeof value === 'object' && value !== null && typeof value.send === 'function' && typeof value.getSnapshot === 'function';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Sends an event to an actor. The returned Effect always succeeds: the event
|
|
419
|
+
* is enqueued, like `actor.send(event)`, and processed on the actor's own
|
|
420
|
+
* fiber. An event that cannot be delivered (for example to a stopped actor)
|
|
421
|
+
* is reported as a dead letter, observable with {@link deadLetters}.
|
|
422
|
+
*/
|
|
423
|
+
const send = dual(2, (actor, event) => Effect.sync(() => {
|
|
424
|
+
actor.send(event);
|
|
425
|
+
}));
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Streams an actor's snapshots, starting with the current one. The stream ends
|
|
429
|
+
* when the actor completes or stops, and emits the error snapshot before
|
|
430
|
+
* ending when the actor errors. Interrupting the stream unsubscribes from the
|
|
431
|
+
* actor.
|
|
432
|
+
*/
|
|
433
|
+
function snapshots(actor) {
|
|
434
|
+
return Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => {
|
|
435
|
+
const current = actor.getSnapshot();
|
|
436
|
+
Queue.offerUnsafe(queue, current);
|
|
437
|
+
if (current.status !== 'active') {
|
|
438
|
+
observeError(actor);
|
|
439
|
+
Queue.endUnsafe(queue);
|
|
440
|
+
return noopSubscription;
|
|
441
|
+
}
|
|
442
|
+
return actor.subscribe({
|
|
443
|
+
next: snapshot => {
|
|
444
|
+
Queue.offerUnsafe(queue, snapshot);
|
|
445
|
+
},
|
|
446
|
+
error: () => {
|
|
447
|
+
Queue.offerUnsafe(queue, actor.getSnapshot());
|
|
448
|
+
Queue.endUnsafe(queue);
|
|
449
|
+
},
|
|
450
|
+
complete: () => {
|
|
451
|
+
Queue.endUnsafe(queue);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}), subscription => Effect.sync(() => {
|
|
455
|
+
subscription.unsubscribe();
|
|
456
|
+
})));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Streams every event an actor emits, as delivered to `actor.on('*', …)`. The
|
|
461
|
+
* stream ends when the actor completes, errors or stops. Interrupting the
|
|
462
|
+
* stream removes the listener.
|
|
463
|
+
*/
|
|
464
|
+
function emitted(actor) {
|
|
465
|
+
return Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => {
|
|
466
|
+
const listener = actor.on('*', event => {
|
|
467
|
+
Queue.offerUnsafe(queue, event);
|
|
468
|
+
});
|
|
469
|
+
if (actor.getSnapshot().status !== 'active') {
|
|
470
|
+
Queue.endUnsafe(queue);
|
|
471
|
+
return [listener, noopSubscription];
|
|
472
|
+
}
|
|
473
|
+
const subscription = actor.subscribe({
|
|
474
|
+
error: () => {
|
|
475
|
+
Queue.endUnsafe(queue);
|
|
476
|
+
},
|
|
477
|
+
complete: () => {
|
|
478
|
+
Queue.endUnsafe(queue);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
return [listener, subscription];
|
|
482
|
+
}), ([listener, subscription]) => Effect.sync(() => {
|
|
483
|
+
listener.unsubscribe();
|
|
484
|
+
subscription.unsubscribe();
|
|
485
|
+
})));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Waits for the first actor snapshot that satisfies `predicate`, succeeding
|
|
490
|
+
* immediately when the current snapshot already does. Fails with
|
|
491
|
+
* `ActorStoppedError` if the actor stops or errors first, and with
|
|
492
|
+
* `Cause.TimeoutError` when `options.timeout` elapses. Interrupting the Effect
|
|
493
|
+
* unsubscribes from the actor.
|
|
494
|
+
*/
|
|
495
|
+
const waitFor = dual(args => isActorRef(args[0]), (actor, predicate, options) => {
|
|
496
|
+
const waiting = Effect.callback(resume => {
|
|
497
|
+
const current = actor.getSnapshot();
|
|
498
|
+
if (predicate(current)) {
|
|
499
|
+
resume(Effect.succeed(current));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (current.status !== 'active') {
|
|
503
|
+
observeError(actor);
|
|
504
|
+
resume(Effect.fail(stoppedError(actor)));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
let settled = false;
|
|
508
|
+
// oxlint-disable-next-line prefer-const
|
|
509
|
+
let subscription; // avoid TDZ when settling synchronously
|
|
510
|
+
const dispose = () => {
|
|
511
|
+
settled = true;
|
|
512
|
+
subscription?.unsubscribe();
|
|
513
|
+
};
|
|
514
|
+
subscription = actor.subscribe({
|
|
515
|
+
next: snapshot => {
|
|
516
|
+
if (settled || !predicate(snapshot)) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
dispose();
|
|
520
|
+
resume(Effect.succeed(snapshot));
|
|
521
|
+
},
|
|
522
|
+
error: () => {
|
|
523
|
+
if (settled) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
dispose();
|
|
527
|
+
resume(Effect.fail(stoppedError(actor)));
|
|
528
|
+
},
|
|
529
|
+
complete: () => {
|
|
530
|
+
if (settled) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
dispose();
|
|
534
|
+
resume(Effect.fail(stoppedError(actor)));
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
if (settled) {
|
|
538
|
+
subscription.unsubscribe();
|
|
539
|
+
}
|
|
540
|
+
return Effect.sync(dispose);
|
|
541
|
+
});
|
|
542
|
+
return options === undefined ? waiting : Effect.timeout(waiting, options.timeout);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Joins an actor's final result, like `Fiber.join`: succeeds with its `output`
|
|
547
|
+
* when it is done, fails with `snapshot.error` when it errors, and fails with
|
|
548
|
+
* `ActorStoppedError` when it stops without output. Waits for a still-active
|
|
549
|
+
* actor to settle.
|
|
550
|
+
*/
|
|
551
|
+
function join(actor) {
|
|
552
|
+
return Effect.callback(resume => {
|
|
553
|
+
const settle = () => {
|
|
554
|
+
const snapshot = actor.getSnapshot();
|
|
555
|
+
if (snapshot.status === 'done') {
|
|
556
|
+
resume(Effect.succeed(snapshot.output));
|
|
557
|
+
} else if (snapshot.status === 'error') {
|
|
558
|
+
resume(Effect.fail(snapshot.error));
|
|
559
|
+
} else {
|
|
560
|
+
resume(Effect.fail(stoppedError(actor)));
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
if (actor.getSnapshot().status !== 'active') {
|
|
564
|
+
observeError(actor);
|
|
565
|
+
settle();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
let settled = false;
|
|
569
|
+
// oxlint-disable-next-line prefer-const
|
|
570
|
+
let subscription; // avoid TDZ when settling synchronously
|
|
571
|
+
const dispose = () => {
|
|
572
|
+
settled = true;
|
|
573
|
+
subscription?.unsubscribe();
|
|
574
|
+
};
|
|
575
|
+
const onSettled = () => {
|
|
576
|
+
if (settled) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
dispose();
|
|
580
|
+
settle();
|
|
581
|
+
};
|
|
582
|
+
subscription = actor.subscribe({
|
|
583
|
+
error: onSettled,
|
|
584
|
+
complete: onSettled
|
|
585
|
+
});
|
|
586
|
+
if (settled) {
|
|
587
|
+
subscription.unsubscribe();
|
|
588
|
+
}
|
|
589
|
+
return Effect.sync(dispose);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Streams the inspection events of the actor's system, as delivered to
|
|
595
|
+
* `system.inspect(…)`. The stream runs until it is interrupted or its scope
|
|
596
|
+
* closes.
|
|
597
|
+
*/
|
|
598
|
+
function inspect(actor) {
|
|
599
|
+
return Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => {
|
|
600
|
+
const observer = inspectionEvent => {
|
|
601
|
+
Queue.offerUnsafe(queue, inspectionEvent);
|
|
602
|
+
};
|
|
603
|
+
const inspectable = actor;
|
|
604
|
+
return inspectable.inspect ? inspectable.inspect(observer) : actor.system.inspect(observer);
|
|
605
|
+
}), subscription => Effect.sync(() => {
|
|
606
|
+
subscription.unsubscribe();
|
|
607
|
+
})));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Streams the events the actor's system could not deliver: sends to a
|
|
612
|
+
* stopped actor, invalid external events and internal events sent from
|
|
613
|
+
* outside their owner. A dead letter is not an actor error. The stream runs
|
|
614
|
+
* until it is interrupted or its scope closes.
|
|
615
|
+
*/
|
|
616
|
+
function deadLetters(actor) {
|
|
617
|
+
return Stream.filter(inspect(actor), event => event.type === '@xstate.deadletter');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Argument an Effect action receives. It mirrors the v6 action argument
|
|
622
|
+
* object: the machine's `context` and the `event` that caused the transition,
|
|
623
|
+
* the actor itself and its family (`self`, `parent`, `children`), the
|
|
624
|
+
* registered `actions`, `actors`, `guards` and `delays`, the actor `system`,
|
|
625
|
+
* and the `params` and `output` the transition passed along.
|
|
626
|
+
*/
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* An action registered with `setupEffect({ actions })`. It is called
|
|
630
|
+
* synchronously during the transition with the arguments the transition
|
|
631
|
+
* enqueued, and returns an Effect that runs afterwards in the actor's Effect
|
|
632
|
+
* context. The Effect is interrupted when the actor stops; its failures and
|
|
633
|
+
* defects route to the state's `onError`.
|
|
634
|
+
*/
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* What `setupEffect` returns: the XState `SetupReturn` with an `extend` method
|
|
638
|
+
* that also accepts Effect schemas and Effect-returning actions. Call
|
|
639
|
+
* `createMachine` on it to build a machine whose registered actions and actors
|
|
640
|
+
* contribute to `RequirementsFrom`.
|
|
641
|
+
*/
|
|
642
|
+
|
|
643
|
+
function wrapActions(actions) {
|
|
644
|
+
if (!actions) {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
const wrapped = {};
|
|
648
|
+
for (const key of Object.keys(actions)) {
|
|
649
|
+
const action = actions[key];
|
|
650
|
+
wrapped[key] = args => {
|
|
651
|
+
const result = action(args);
|
|
652
|
+
return Effect.isEffect(result) ? runHostedEffect(args.self, result, `action.${key}`) : result;
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
return wrapped;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Makes `machine.provide` host Effect-returning action overrides the same way
|
|
660
|
+
* `setupEffect` hosts declared actions.
|
|
661
|
+
*/
|
|
662
|
+
function decorateMachine(machine) {
|
|
663
|
+
const provide = machine.provide.bind(machine);
|
|
664
|
+
machine.provide = sources => decorateMachine(provide({
|
|
665
|
+
...sources,
|
|
666
|
+
actions: wrapActions(sources.actions)
|
|
667
|
+
}));
|
|
668
|
+
return machine;
|
|
669
|
+
}
|
|
670
|
+
function decorateEffectSetup(effectSetup) {
|
|
671
|
+
const createMachine = effectSetup.createMachine.bind(effectSetup);
|
|
672
|
+
effectSetup.createMachine = config => decorateMachine(createMachine(config));
|
|
673
|
+
const extend = effectSetup.extend;
|
|
674
|
+
const extendAny = extend;
|
|
675
|
+
effectSetup.extend = extension => decorateEffectSetup(extendAny({
|
|
676
|
+
...extension,
|
|
677
|
+
schemas: toStandardSetupSchemas(extension.schemas),
|
|
678
|
+
states: toStandardSetupStates(extension.states),
|
|
679
|
+
actions: wrapActions(extension.actions)
|
|
680
|
+
}));
|
|
681
|
+
return effectSetup;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* The Effect-aware form of XState's `setup`. It accepts Effect `Schema` values
|
|
686
|
+
* wherever `setup` accepts Standard Schemas, and actions that return an
|
|
687
|
+
* Effect, which it wraps so the transition stays synchronous while the Effect
|
|
688
|
+
* runs in the actor's Effect context. Everything else, including `actors`,
|
|
689
|
+
* `guards`, `delays` and `extend`, behaves as in `setup`. Machines built from
|
|
690
|
+
* it must be started with `createEffectActor`.
|
|
691
|
+
*/
|
|
692
|
+
|
|
693
|
+
function setupEffect(config = {}) {
|
|
694
|
+
return decorateEffectSetup(setup({
|
|
695
|
+
...config,
|
|
696
|
+
schemas: toStandardSetupSchemas(config.schemas),
|
|
697
|
+
states: toStandardSetupStates(config.states),
|
|
698
|
+
actions: wrapActions(config.actions)
|
|
699
|
+
}));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export { deadLetters, emitted, fromEffect, fromEffectEventStream, fromEffectStream, inspect, join, send, setupEffect, snapshots, waitFor };
|