@xstate/effect 1.0.0-alpha.8 → 1.0.0-beta.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 +20 -9
- package/dist/declarations/src/actor.d.ts +33 -7
- package/dist/declarations/src/actorScope.d.ts +7 -1
- package/dist/declarations/src/atom.d.ts +11 -9
- package/dist/declarations/src/createEffectActor.d.ts +16 -5
- package/dist/declarations/src/effectActor.d.ts +20 -11
- package/dist/declarations/src/effectLogic.d.ts +30 -10
- package/dist/declarations/src/errors.d.ts +37 -4
- package/dist/declarations/src/index.d.ts +3 -2
- package/dist/declarations/src/oxlint.d.ts +3 -0
- package/dist/declarations/src/persistence.d.ts +45 -0
- package/dist/declarations/src/schema.d.ts +10 -0
- package/dist/declarations/src/setupEffect.d.ts +56 -13
- package/dist/declarations/src/state.d.ts +19 -8
- package/dist/declarations/src/test.d.ts +13 -0
- package/dist/declarations/src/types.d.ts +3 -2
- package/dist/{state-ea62b88e.js → state-a2154dce.js} +247 -82
- package/dist/xstate-effect-atom.js +14 -8
- package/dist/xstate-effect-oxlint.d.ts +3 -0
- package/dist/xstate-effect-oxlint.js +126 -0
- package/dist/xstate-effect-test.d.ts +2 -0
- package/dist/xstate-effect-test.js +13 -0
- package/dist/xstate-effect.js +286 -35
- package/package.json +17 -5
- package/dist/declarations/src/brands.d.ts +0 -1
package/dist/xstate-effect.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Schema, Effect, Stream,
|
|
1
|
+
import { Schema, Effect, Stream, Cause, Exit, Queue, Data } from 'effect';
|
|
2
2
|
import { createStepLogic, setup } from 'xstate';
|
|
3
|
-
import { s as startHostedEffect, r as relayToParent, E as
|
|
4
|
-
export {
|
|
3
|
+
import { s as startHostedEffect, r as relayToParent, E as EffectFailure, A as ActorStoppedError, a as causeFromError, b as runHostedEffect } from './state-a2154dce.js';
|
|
4
|
+
export { e as ActorScope, A as ActorStoppedError, d as EffectActor, E as EffectFailure, g as EffectInterruptedError, f as EventRejectedError, c as createEffectActor, t as taggedState, w as withActorScope } from './state-a2154dce.js';
|
|
5
5
|
import { dual } from 'effect/Function';
|
|
6
6
|
import 'xstate/durable';
|
|
7
7
|
|
|
@@ -10,12 +10,16 @@ import 'xstate/durable';
|
|
|
10
10
|
* `createEffectStreamLogic` and `createEffectEventStreamLogic` convert it to a Standard
|
|
11
11
|
* Schema, so XState infers the decoded `Schema.Type` without an explicit call
|
|
12
12
|
* to `Schema.toStandardSchemaV1`.
|
|
13
|
+
*
|
|
14
|
+
* @experimental
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Either an Effect {@link EffectSchema} or a Standard Schema. Every schema
|
|
17
19
|
* position in this package accepts both, so Effect schemas and schemas from
|
|
18
20
|
* other libraries can be mixed in one machine.
|
|
21
|
+
*
|
|
22
|
+
* @experimental
|
|
19
23
|
*/
|
|
20
24
|
|
|
21
25
|
/**
|
|
@@ -23,15 +27,23 @@ import 'xstate/durable';
|
|
|
23
27
|
* schema position widened to {@link EffectSchemaLike}. Covers `context`,
|
|
24
28
|
* `input`, `output` and the per-key records for `events`, `internalEvents`,
|
|
25
29
|
* `emitted` and `children`.
|
|
30
|
+
*
|
|
31
|
+
* @experimental
|
|
26
32
|
*/
|
|
27
33
|
|
|
34
|
+
/** @experimental */
|
|
35
|
+
|
|
28
36
|
/**
|
|
29
37
|
* One node of the `states` option of `setupEffect`: the schemas declared for
|
|
30
38
|
* that state, and the same shape recursively for its child states. It is
|
|
31
39
|
* XState's `SetupStateSchema` with Effect schemas allowed in every schema
|
|
32
40
|
* position.
|
|
41
|
+
*
|
|
42
|
+
* @experimental
|
|
33
43
|
*/
|
|
34
44
|
|
|
45
|
+
/** @experimental */
|
|
46
|
+
|
|
35
47
|
function isStandardSchema(value) {
|
|
36
48
|
return !!value && (typeof value === 'object' || typeof value === 'function') && '~standard' in value;
|
|
37
49
|
}
|
|
@@ -87,13 +99,16 @@ const effectLogicBrand = Symbol.for('@xstate/effect/logic');
|
|
|
87
99
|
* failure type and its Effect service requirements, which is how
|
|
88
100
|
* `RequirementsFrom` collects the `R` channel of `createEffectActor` without
|
|
89
101
|
* inspecting the logic at runtime.
|
|
102
|
+
*
|
|
103
|
+
* @experimental
|
|
90
104
|
*/
|
|
91
105
|
|
|
92
106
|
/**
|
|
93
107
|
* Snapshot of a `createEffectLogic` actor. The actor holds no context of its own: it
|
|
94
108
|
* is `active` while the Effect runs, `done` with the Effect's success value as
|
|
95
|
-
* `output`, or `error` with the
|
|
96
|
-
*
|
|
109
|
+
* `output`, or `error` with the original `Cause` wrapped in `EffectFailure` as `error`.
|
|
110
|
+
*
|
|
111
|
+
* @experimental
|
|
97
112
|
*/
|
|
98
113
|
|
|
99
114
|
/**
|
|
@@ -101,12 +116,16 @@ const effectLogicBrand = Symbol.for('@xstate/effect/logic');
|
|
|
101
116
|
* source the actor's `input`, its own reference (`self`), the actor `system`,
|
|
102
117
|
* and `emit` for publishing events that `actor.on(...)` and `emitted(actor)`
|
|
103
118
|
* observe.
|
|
119
|
+
*
|
|
120
|
+
* @experimental
|
|
104
121
|
*/
|
|
105
122
|
|
|
106
123
|
/**
|
|
107
124
|
* What `createEffectLogic` accepts as its Effect: either an Effect value, or a
|
|
108
125
|
* function of {@link EffectSourceArgs} returning one. The function form is
|
|
109
126
|
* called once per actor start, so it sees that actor's input.
|
|
127
|
+
*
|
|
128
|
+
* @experimental
|
|
110
129
|
*/
|
|
111
130
|
|
|
112
131
|
/**
|
|
@@ -114,30 +133,28 @@ const effectLogicBrand = Symbol.for('@xstate/effect/logic');
|
|
|
114
133
|
* ordinary XState actor logic carrying {@link EffectLogicBrand}, so a machine
|
|
115
134
|
* can invoke or spawn it, and `createEffectActor` can collect its
|
|
116
135
|
* requirements.
|
|
136
|
+
*
|
|
137
|
+
* @experimental
|
|
117
138
|
*/
|
|
118
139
|
|
|
119
140
|
/**
|
|
120
141
|
* Snapshot of a `createEffectStreamLogic` actor. `context` holds the most recent
|
|
121
142
|
* stream item, or `undefined` before the first one. The actor reaches `done`
|
|
122
143
|
* with no output when the stream completes, and `error` when it fails.
|
|
144
|
+
*
|
|
145
|
+
* @experimental
|
|
123
146
|
*/
|
|
124
147
|
|
|
125
148
|
/**
|
|
126
149
|
* Actor logic produced by `createEffectStreamLogic`. Its snapshot is an
|
|
127
150
|
* {@link EffectStreamSnapshot}, so the parent reads the latest item from the
|
|
128
151
|
* child's `context` rather than from events.
|
|
152
|
+
*
|
|
153
|
+
* @experimental
|
|
129
154
|
*/
|
|
130
155
|
|
|
131
|
-
function
|
|
132
|
-
|
|
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);
|
|
156
|
+
function preserveFailure(error) {
|
|
157
|
+
return EffectFailure.is(error) ? error : new EffectFailure(Cause.die(error));
|
|
141
158
|
}
|
|
142
159
|
function createSourceArgs(input, self, system) {
|
|
143
160
|
return {
|
|
@@ -150,6 +167,32 @@ function createSourceArgs(input, self, system) {
|
|
|
150
167
|
};
|
|
151
168
|
}
|
|
152
169
|
function brandLogic(logic, error, requirements) {
|
|
170
|
+
const transition = logic.transition.bind(logic);
|
|
171
|
+
logic.transition = (snapshot, event, scope) => {
|
|
172
|
+
try {
|
|
173
|
+
return transition(snapshot, event, scope);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
throw preserveFailure(error);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const initialTransition = logic.initialTransition.bind(logic);
|
|
179
|
+
logic.initialTransition = (input, scope) => {
|
|
180
|
+
try {
|
|
181
|
+
return initialTransition(input, scope);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw preserveFailure(error);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const restoreSnapshot = logic.restoreSnapshot?.bind(logic);
|
|
187
|
+
if (restoreSnapshot) {
|
|
188
|
+
logic.restoreSnapshot = (persisted, scope) => {
|
|
189
|
+
const snapshot = restoreSnapshot(persisted, scope);
|
|
190
|
+
if (snapshot.status === 'error' && !EffectFailure.is(snapshot.error)) {
|
|
191
|
+
throw new Error('Cannot restore a serialized Effect failure. Recover the error before persisting the actor.');
|
|
192
|
+
}
|
|
193
|
+
return snapshot;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
153
196
|
Object.defineProperty(logic, effectLogicBrand, {
|
|
154
197
|
configurable: false,
|
|
155
198
|
enumerable: false,
|
|
@@ -163,15 +206,15 @@ function brandLogic(logic, error, requirements) {
|
|
|
163
206
|
|
|
164
207
|
/**
|
|
165
208
|
* 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
|
|
167
|
-
* squashed defect, or an `EffectInterruptedError` when the Effect interrupts
|
|
168
|
-
* itself. Stopping the actor interrupts the Effect without an error. Accepts
|
|
209
|
+
* Effect's success value as its output and fails with the original `Cause` wrapped in `EffectFailure`. Stopping the actor interrupts the Effect without an error. Accepts
|
|
169
210
|
* an Effect, a function of {@link EffectSourceArgs} returning one, or a config
|
|
170
211
|
* object with `id`, `schemas`, `validator` and `effect`. The logic must be run
|
|
171
212
|
* under `createEffectActor`, which provides the Effect services it declares.
|
|
172
213
|
* Resources belong to this task's scope and close on completion, failure or
|
|
173
214
|
* interruption. Use `withActorScope` to retain a resource until the owning
|
|
174
215
|
* Effect actor stops.
|
|
216
|
+
*
|
|
217
|
+
* @experimental
|
|
175
218
|
*/
|
|
176
219
|
|
|
177
220
|
function createEffectLogic(sourceOrConfig) {
|
|
@@ -208,14 +251,14 @@ function createEffectLogic(sourceOrConfig) {
|
|
|
208
251
|
if (event.type === EFFECT_REJECT) {
|
|
209
252
|
return {
|
|
210
253
|
status: 'error',
|
|
211
|
-
error: event.error,
|
|
254
|
+
error: preserveFailure(event.error),
|
|
212
255
|
input: undefined
|
|
213
256
|
};
|
|
214
257
|
}
|
|
215
258
|
if (event.type !== EFFECT_INIT) {
|
|
216
259
|
return;
|
|
217
260
|
}
|
|
218
|
-
const effect = typeof source === 'function' ? source(createSourceArgs(input, self, system)) : source;
|
|
261
|
+
const effect = Effect.suspend(() => typeof source === 'function' ? source(createSourceArgs(input, self, system)) : source);
|
|
219
262
|
enq.effect(() => startHostedEffect(self, Effect.scoped(effect), 'createEffectLogic', exit => {
|
|
220
263
|
if (self.getSnapshot().status !== 'active') {
|
|
221
264
|
return;
|
|
@@ -228,7 +271,7 @@ function createEffectLogic(sourceOrConfig) {
|
|
|
228
271
|
} else {
|
|
229
272
|
self.send({
|
|
230
273
|
type: EFFECT_REJECT,
|
|
231
|
-
error:
|
|
274
|
+
error: new EffectFailure(exit.cause)
|
|
232
275
|
});
|
|
233
276
|
}
|
|
234
277
|
}));
|
|
@@ -241,6 +284,8 @@ function createEffectLogic(sourceOrConfig) {
|
|
|
241
284
|
* What `createEffectStreamLogic` and `createEffectEventStreamLogic` accept as their stream:
|
|
242
285
|
* either a Stream value, or a function of {@link EffectSourceArgs} returning
|
|
243
286
|
* one. The function form is called once per actor start.
|
|
287
|
+
*
|
|
288
|
+
* @experimental
|
|
244
289
|
*/
|
|
245
290
|
|
|
246
291
|
function resolveStreamConfig(sourceOrConfig) {
|
|
@@ -261,6 +306,8 @@ function toLogicSchemas(schemas) {
|
|
|
261
306
|
* {@link EffectSourceArgs} returning one, or a config object with `id`,
|
|
262
307
|
* `schemas`, `validator` and `stream`. Its resources close when the stream
|
|
263
308
|
* completes, fails or is interrupted.
|
|
309
|
+
*
|
|
310
|
+
* @experimental
|
|
264
311
|
*/
|
|
265
312
|
|
|
266
313
|
function createEffectStreamLogic(sourceOrConfig) {
|
|
@@ -291,14 +338,14 @@ function createEffectStreamLogic(sourceOrConfig) {
|
|
|
291
338
|
if (event.type === EFFECT_REJECT) {
|
|
292
339
|
return {
|
|
293
340
|
status: 'error',
|
|
294
|
-
error: event.error,
|
|
341
|
+
error: preserveFailure(event.error),
|
|
295
342
|
input: undefined
|
|
296
343
|
};
|
|
297
344
|
}
|
|
298
345
|
if (event.type !== EFFECT_INIT) {
|
|
299
346
|
return;
|
|
300
347
|
}
|
|
301
|
-
const streamValue = typeof stream === 'function' ? stream(createSourceArgs(input, self, system)) : stream;
|
|
348
|
+
const streamValue = Stream.suspend(() => typeof stream === 'function' ? stream(createSourceArgs(input, self, system)) : stream);
|
|
302
349
|
const consume = Stream.runForEach(streamValue, value => Effect.sync(() => {
|
|
303
350
|
if (self.getSnapshot().status === 'active') {
|
|
304
351
|
self.send({
|
|
@@ -318,7 +365,7 @@ function createEffectStreamLogic(sourceOrConfig) {
|
|
|
318
365
|
} else {
|
|
319
366
|
self.send({
|
|
320
367
|
type: EFFECT_REJECT,
|
|
321
|
-
error:
|
|
368
|
+
error: new EffectFailure(exit.cause)
|
|
322
369
|
});
|
|
323
370
|
}
|
|
324
371
|
}));
|
|
@@ -333,6 +380,8 @@ function createEffectStreamLogic(sourceOrConfig) {
|
|
|
333
380
|
* has no output: it reaches `done` when the stream completes and `error` when
|
|
334
381
|
* it fails. Accepts the same forms as `createEffectStreamLogic`. Its resources close
|
|
335
382
|
* when the stream completes, fails or is interrupted.
|
|
383
|
+
*
|
|
384
|
+
* @experimental
|
|
336
385
|
*/
|
|
337
386
|
|
|
338
387
|
function createEffectEventStreamLogic(sourceOrConfig) {
|
|
@@ -358,14 +407,14 @@ function createEffectEventStreamLogic(sourceOrConfig) {
|
|
|
358
407
|
if (event.type === EFFECT_REJECT) {
|
|
359
408
|
return {
|
|
360
409
|
status: 'error',
|
|
361
|
-
error: event.error,
|
|
410
|
+
error: preserveFailure(event.error),
|
|
362
411
|
input: undefined
|
|
363
412
|
};
|
|
364
413
|
}
|
|
365
414
|
if (event.type !== EFFECT_INIT) {
|
|
366
415
|
return;
|
|
367
416
|
}
|
|
368
|
-
const streamValue = typeof stream === 'function' ? stream(createSourceArgs(input, self, system)) : stream;
|
|
417
|
+
const streamValue = Stream.suspend(() => typeof stream === 'function' ? stream(createSourceArgs(input, self, system)) : stream);
|
|
369
418
|
const consume = Stream.runForEach(streamValue, value => Effect.sync(() => relayToParent(self, value)));
|
|
370
419
|
enq.effect(() => startHostedEffect(self, Effect.scoped(consume), 'createEffectEventStreamLogic', exit => {
|
|
371
420
|
if (self.getSnapshot().status !== 'active') {
|
|
@@ -378,7 +427,7 @@ function createEffectEventStreamLogic(sourceOrConfig) {
|
|
|
378
427
|
} else {
|
|
379
428
|
self.send({
|
|
380
429
|
type: EFFECT_REJECT,
|
|
381
|
-
error:
|
|
430
|
+
error: new EffectFailure(exit.cause)
|
|
382
431
|
});
|
|
383
432
|
}
|
|
384
433
|
}));
|
|
@@ -387,11 +436,23 @@ function createEffectEventStreamLogic(sourceOrConfig) {
|
|
|
387
436
|
return brandLogic(logic, undefined, undefined);
|
|
388
437
|
}
|
|
389
438
|
|
|
390
|
-
/**
|
|
439
|
+
/**
|
|
440
|
+
* The event type accepted by an actor's `send` method.
|
|
441
|
+
*
|
|
442
|
+
* @experimental
|
|
443
|
+
*/
|
|
391
444
|
|
|
392
|
-
/**
|
|
445
|
+
/**
|
|
446
|
+
* The event type an actor emits through `actor.on(...)`.
|
|
447
|
+
*
|
|
448
|
+
* @experimental
|
|
449
|
+
*/
|
|
393
450
|
|
|
394
|
-
/**
|
|
451
|
+
/**
|
|
452
|
+
* Options for {@link waitFor}.
|
|
453
|
+
*
|
|
454
|
+
* @experimental
|
|
455
|
+
*/
|
|
395
456
|
|
|
396
457
|
const noopSubscription = {
|
|
397
458
|
unsubscribe: () => {}
|
|
@@ -424,6 +485,8 @@ function isActorRef(value) {
|
|
|
424
485
|
* is enqueued, like `actor.send(event)`, and processed on the actor's own
|
|
425
486
|
* fiber. An event that cannot be delivered (for example to a stopped actor)
|
|
426
487
|
* is reported as a dead letter, observable with {@link deadLetters}.
|
|
488
|
+
*
|
|
489
|
+
* @experimental
|
|
427
490
|
*/
|
|
428
491
|
const send = dual(2, (actor, event) => Effect.sync(() => {
|
|
429
492
|
actor.send(event);
|
|
@@ -434,6 +497,8 @@ const send = dual(2, (actor, event) => Effect.sync(() => {
|
|
|
434
497
|
* when the actor completes or stops, and emits the error snapshot before
|
|
435
498
|
* ending when the actor errors. Interrupting the stream unsubscribes from the
|
|
436
499
|
* actor.
|
|
500
|
+
*
|
|
501
|
+
* @experimental
|
|
437
502
|
*/
|
|
438
503
|
function snapshots(actor) {
|
|
439
504
|
return Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => {
|
|
@@ -465,6 +530,8 @@ function snapshots(actor) {
|
|
|
465
530
|
* Streams every event an actor emits, as delivered to `actor.on('*', …)`. The
|
|
466
531
|
* stream ends when the actor completes, errors or stops. Interrupting the
|
|
467
532
|
* stream removes the listener.
|
|
533
|
+
*
|
|
534
|
+
* @experimental
|
|
468
535
|
*/
|
|
469
536
|
function emitted(actor) {
|
|
470
537
|
return Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => {
|
|
@@ -496,6 +563,8 @@ function emitted(actor) {
|
|
|
496
563
|
* `ActorStoppedError` if the actor stops or errors first, and with
|
|
497
564
|
* `Cause.TimeoutError` when `options.timeout` elapses. Interrupting the Effect
|
|
498
565
|
* unsubscribes from the actor.
|
|
566
|
+
*
|
|
567
|
+
* @experimental
|
|
499
568
|
*/
|
|
500
569
|
const waitFor = dual(args => isActorRef(args[0]), (actor, predicate, options) => {
|
|
501
570
|
const waiting = Effect.callback(resume => {
|
|
@@ -549,9 +618,11 @@ const waitFor = dual(args => isActorRef(args[0]), (actor, predicate, options) =>
|
|
|
549
618
|
|
|
550
619
|
/**
|
|
551
620
|
* Joins an actor's final result, like `Fiber.join`: succeeds with its `output`
|
|
552
|
-
* when it is done,
|
|
621
|
+
* when it is done, replays an Effect actor's original Cause when it errors, and fails with
|
|
553
622
|
* `ActorStoppedError` when it stops without output. Waits for a still-active
|
|
554
|
-
* actor to settle.
|
|
623
|
+
* actor to settle. Ordinary XState errors remain typed failures.
|
|
624
|
+
*
|
|
625
|
+
* @experimental
|
|
555
626
|
*/
|
|
556
627
|
function join(actor) {
|
|
557
628
|
return Effect.callback(resume => {
|
|
@@ -560,7 +631,7 @@ function join(actor) {
|
|
|
560
631
|
if (snapshot.status === 'done') {
|
|
561
632
|
resume(Effect.succeed(snapshot.output));
|
|
562
633
|
} else if (snapshot.status === 'error') {
|
|
563
|
-
resume(Effect.
|
|
634
|
+
resume(Effect.failCause(causeFromError(snapshot.error)));
|
|
564
635
|
} else {
|
|
565
636
|
resume(Effect.fail(stoppedError(actor)));
|
|
566
637
|
}
|
|
@@ -599,6 +670,8 @@ function join(actor) {
|
|
|
599
670
|
* Streams the inspection events of the actor's system, as delivered to
|
|
600
671
|
* `system.inspect(…)`. The stream runs until it is interrupted or its scope
|
|
601
672
|
* closes.
|
|
673
|
+
*
|
|
674
|
+
* @experimental
|
|
602
675
|
*/
|
|
603
676
|
function inspect(actor) {
|
|
604
677
|
return Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => {
|
|
@@ -618,6 +691,8 @@ function inspect(actor) {
|
|
|
618
691
|
* events and internal events sent from outside their owner. A dead letter is
|
|
619
692
|
* not an actor error. The stream runs until it is interrupted or its scope
|
|
620
693
|
* closes.
|
|
694
|
+
*
|
|
695
|
+
* @experimental
|
|
621
696
|
*/
|
|
622
697
|
function deadLetters(actor) {
|
|
623
698
|
return Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => actor.system.onRejectedEvent(rejection => {
|
|
@@ -633,6 +708,8 @@ function deadLetters(actor) {
|
|
|
633
708
|
* the actor itself and its family (`self`, `parent`, `children`), the
|
|
634
709
|
* registered `actions`, `actors`, `guards` and `delays`, the actor `system`,
|
|
635
710
|
* and the `params` and `output` the transition passed along.
|
|
711
|
+
*
|
|
712
|
+
* @experimental
|
|
636
713
|
*/
|
|
637
714
|
|
|
638
715
|
/**
|
|
@@ -641,6 +718,8 @@ function deadLetters(actor) {
|
|
|
641
718
|
* enqueued, and returns an Effect that runs afterwards in the actor's Effect
|
|
642
719
|
* context. The Effect is interrupted when the actor stops; its failures and
|
|
643
720
|
* defects route to the state's `onError`.
|
|
721
|
+
*
|
|
722
|
+
* @experimental
|
|
644
723
|
*/
|
|
645
724
|
|
|
646
725
|
/**
|
|
@@ -648,6 +727,8 @@ function deadLetters(actor) {
|
|
|
648
727
|
* that also accepts Effect schemas and Effect-returning actions. Call
|
|
649
728
|
* `createMachine` on it to build a machine whose registered actions and actors
|
|
650
729
|
* contribute to `RequirementsFrom`.
|
|
730
|
+
*
|
|
731
|
+
* @experimental
|
|
651
732
|
*/
|
|
652
733
|
|
|
653
734
|
function wrapActions(actions) {
|
|
@@ -659,7 +740,12 @@ function wrapActions(actions) {
|
|
|
659
740
|
const action = actions[key];
|
|
660
741
|
wrapped[key] = args => {
|
|
661
742
|
// machine.provide can replace Effect actions with plain actions.
|
|
662
|
-
|
|
743
|
+
let result;
|
|
744
|
+
try {
|
|
745
|
+
result = action(args);
|
|
746
|
+
} catch (error) {
|
|
747
|
+
result = Effect.die(error);
|
|
748
|
+
}
|
|
663
749
|
return Effect.isEffect(result) ? runHostedEffect(args.self, result, `action.${key}`) : result;
|
|
664
750
|
};
|
|
665
751
|
}
|
|
@@ -699,6 +785,8 @@ function decorateEffectSetup(effectSetup) {
|
|
|
699
785
|
* runs in the actor's Effect context. Everything else, including `actors`,
|
|
700
786
|
* `guards`, `delays` and `extend`, behaves as in `setup`. Machines built from
|
|
701
787
|
* it must be started with `createEffectActor`.
|
|
788
|
+
*
|
|
789
|
+
* @experimental
|
|
702
790
|
*/
|
|
703
791
|
|
|
704
792
|
function setupEffect(config = {}) {
|
|
@@ -710,4 +798,167 @@ function setupEffect(config = {}) {
|
|
|
710
798
|
}));
|
|
711
799
|
}
|
|
712
800
|
|
|
713
|
-
|
|
801
|
+
/**
|
|
802
|
+
* A malformed transport, unsupported JSON value or missing persistence metadata.
|
|
803
|
+
*
|
|
804
|
+
* @experimental
|
|
805
|
+
*/
|
|
806
|
+
class SnapshotCodecError extends Data.TaggedError('SnapshotCodecError') {
|
|
807
|
+
get message() {
|
|
808
|
+
return `${this.stage} at ${this.path}: ${this.reason}`;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function record(value) {
|
|
812
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
813
|
+
}
|
|
814
|
+
function invalid(stage, path, reason) {
|
|
815
|
+
throw new SnapshotCodecError({
|
|
816
|
+
stage,
|
|
817
|
+
path,
|
|
818
|
+
reason
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
function checkJson(value, stage, path = '$', ancestors = new Set()) {
|
|
822
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
|
|
823
|
+
if (typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0)) return;
|
|
824
|
+
if (typeof value !== 'object') invalid(stage, path, 'Expected lossless JSON; encode this value with a schema');
|
|
825
|
+
if (ancestors.has(value)) invalid(stage, path, 'Cyclic value');
|
|
826
|
+
if (Array.isArray(value) && Object.getPrototypeOf(value) !== Array.prototype) invalid(stage, path, 'Unsupported array prototype');
|
|
827
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
828
|
+
invalid(stage, path, 'Unsupported object prototype; supply a reversible schema');
|
|
829
|
+
}
|
|
830
|
+
if (Object.getOwnPropertySymbols(value).length) invalid(stage, path, 'Symbol properties are not JSON');
|
|
831
|
+
ancestors.add(value);
|
|
832
|
+
if (Array.isArray(value)) {
|
|
833
|
+
if (Object.keys(value).length !== value.length) invalid(stage, path, 'Array holes or extra properties are not JSON');
|
|
834
|
+
for (let index = 0; index < value.length; index++) {
|
|
835
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
836
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) invalid(stage, `${path}[${index}]`, 'Array accessor or hole is not JSON');
|
|
837
|
+
checkJson(descriptor.value, stage, `${path}[${index}]`, ancestors);
|
|
838
|
+
}
|
|
839
|
+
if (Object.getOwnPropertyNames(value).length !== value.length + 1) invalid(stage, path, 'Extra array properties are not JSON');
|
|
840
|
+
} else {
|
|
841
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
842
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
843
|
+
if (!descriptor.enumerable || !('value' in descriptor)) invalid(stage, `${path}.${key}`, 'Non-enumerable or accessor property is not JSON');
|
|
844
|
+
checkJson(descriptor.value, stage, `${path}.${key}`, ancestors);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
ancestors.delete(value);
|
|
848
|
+
}
|
|
849
|
+
function checkSnapshot(value, stage, path = '$.snapshot', machineId) {
|
|
850
|
+
if (!record(value)) invalid(stage, path, 'Expected a persisted snapshot');
|
|
851
|
+
if (!['active', 'done', 'stopped', 'error'].includes(String(value.status))) invalid(stage, path, 'Invalid snapshot status');
|
|
852
|
+
if (value.status === 'error') invalid(stage, path, 'Errored snapshots require an explicit error recovery policy before persistence');
|
|
853
|
+
if (machineId !== undefined || 'machine' in value) {
|
|
854
|
+
if (!record(value.machine) || typeof value.machine.id !== 'string' || typeof value.machine.version !== 'string' || !value.machine.version) invalid(stage, `${path}.machine`, 'Expected a versioned machine identity');
|
|
855
|
+
if (machineId !== undefined && value.machine.id !== machineId) invalid(stage, `${path}.machine.id`, 'Machine identity mismatch');
|
|
856
|
+
}
|
|
857
|
+
if ('value' in value || 'machine' in value) {
|
|
858
|
+
for (const key of ['context', 'value', 'children', 'historyValue', 'timers']) {
|
|
859
|
+
if (!(key in value)) invalid(stage, `${path}.${key}`, 'Missing machine persistence metadata');
|
|
860
|
+
}
|
|
861
|
+
if (!record(value.children) || !record(value.historyValue) || !Array.isArray(value.timers)) invalid(stage, path, 'Invalid machine persistence metadata');
|
|
862
|
+
}
|
|
863
|
+
if (record(value.children)) {
|
|
864
|
+
for (const [id, child] of Object.entries(value.children)) {
|
|
865
|
+
if (!record(child) || child.src !== undefined && typeof child.src !== 'string') invalid(stage, `${path}.children.${id}`, 'Invalid persisted child');
|
|
866
|
+
if (child.remote === true) {
|
|
867
|
+
if (child.snapshot !== undefined) invalid(stage, `${path}.children.${id}`, 'Remote children cannot contain snapshots');
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
checkSnapshot(child.snapshot, stage, `${path}.children.${id}.snapshot`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function sameData(left, right) {
|
|
875
|
+
if (Object.is(left, right)) return true;
|
|
876
|
+
if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((item, index) => sameData(item, right[index]));
|
|
877
|
+
if (record(left) && record(right)) {
|
|
878
|
+
const keys = Object.keys(left);
|
|
879
|
+
return keys.length === Object.keys(right).length && keys.every(key => Object.hasOwn(right, key) && sameData(left[key], right[key]));
|
|
880
|
+
}
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
function checkMetadata(before, after, stage, path = '$.snapshot') {
|
|
884
|
+
for (const [key, value] of Object.entries(before)) {
|
|
885
|
+
if (['context', 'input', 'output', 'error', 'stateInputs'].includes(key) || value === undefined) continue;
|
|
886
|
+
if (key === 'children' && record(value) && record(after.children)) {
|
|
887
|
+
if (!sameData(Object.keys(value).sort(), Object.keys(after.children).sort())) invalid(stage, `${path}.children`, 'Schema changed persisted children');
|
|
888
|
+
for (const [id, child] of Object.entries(value)) {
|
|
889
|
+
const next = after.children[id];
|
|
890
|
+
if (!record(child) || !record(next)) invalid(stage, `${path}.children.${id}`, 'Schema changed child metadata');
|
|
891
|
+
for (const childKey of Object.keys(child)) {
|
|
892
|
+
if (childKey !== 'snapshot' && child[childKey] !== undefined && !sameData(child[childKey], next[childKey])) invalid(stage, `${path}.children.${id}.${childKey}`, 'Schema changed child metadata');
|
|
893
|
+
}
|
|
894
|
+
if (record(child.snapshot) && record(next.snapshot)) checkMetadata(child.snapshot, next.snapshot, stage, `${path}.children.${id}.snapshot`);
|
|
895
|
+
}
|
|
896
|
+
} else if (!sameData(value, after[key])) {
|
|
897
|
+
invalid(stage, `${path}.${key}`, 'Schema changed machine persistence metadata');
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (before.input !== undefined && !('input' in after)) invalid(stage, `${path}.input`, 'Schema removed input');
|
|
901
|
+
if (before.output !== undefined && !('output' in after)) invalid(stage, `${path}.output`, 'Schema removed output');
|
|
902
|
+
if (record(before.stateInputs) && (!record(after.stateInputs) || !sameData(Object.keys(before.stateInputs).sort(), Object.keys(after.stateInputs).sort()))) invalid(stage, `${path}.stateInputs`, 'Schema changed state input owners');
|
|
903
|
+
if ('stateInputs' in before && !('stateInputs' in after)) invalid(stage, `${path}.stateInputs`, 'Schema removed state inputs');
|
|
904
|
+
}
|
|
905
|
+
function checked(stage, run) {
|
|
906
|
+
return Effect.try({
|
|
907
|
+
try: run,
|
|
908
|
+
catch: error => error instanceof SnapshotCodecError ? error : new SnapshotCodecError({
|
|
909
|
+
stage,
|
|
910
|
+
path: '$',
|
|
911
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
912
|
+
})
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Binds a full bidirectional snapshot schema to a versioned machine. The schema
|
|
918
|
+
* owns rich values and supported historical versions. Decoding never migrates;
|
|
919
|
+
* restore or `machineVersions` remains responsible for migration.
|
|
920
|
+
*
|
|
921
|
+
* @experimental
|
|
922
|
+
*/
|
|
923
|
+
function createSnapshotCodec(machine, schema) {
|
|
924
|
+
const format = '@xstate/effect/snapshot';
|
|
925
|
+
return {
|
|
926
|
+
encode(snapshot) {
|
|
927
|
+
return Effect.gen(function* () {
|
|
928
|
+
yield* checked('encode', () => checkSnapshot(snapshot, 'encode', '$.snapshot', machine.id));
|
|
929
|
+
const encoded = yield* Schema.encodeUnknownEffect(schema)(snapshot);
|
|
930
|
+
return yield* checked('encode', () => {
|
|
931
|
+
checkJson(encoded, 'encode');
|
|
932
|
+
checkSnapshot(encoded, 'encode', '$.snapshot', machine.id);
|
|
933
|
+
checkMetadata(snapshot, encoded, 'encode');
|
|
934
|
+
return JSON.stringify({
|
|
935
|
+
format,
|
|
936
|
+
version: 1,
|
|
937
|
+
logicId: machine.id,
|
|
938
|
+
snapshot: encoded
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
},
|
|
943
|
+
decode(json) {
|
|
944
|
+
return Effect.gen(function* () {
|
|
945
|
+
const encoded = yield* checked('decode', () => {
|
|
946
|
+
const envelope = JSON.parse(json);
|
|
947
|
+
checkJson(envelope, 'decode');
|
|
948
|
+
if (!record(envelope) || envelope.format !== format || envelope.version !== 1) invalid('decode', '$', 'Unsupported snapshot format or version');
|
|
949
|
+
if (envelope.logicId !== machine.id) invalid('decode', '$.logicId', 'Machine identity mismatch');
|
|
950
|
+
checkSnapshot(envelope.snapshot, 'decode', '$.snapshot', machine.id);
|
|
951
|
+
return envelope.snapshot;
|
|
952
|
+
});
|
|
953
|
+
const snapshot = yield* Schema.decodeUnknownEffect(schema)(encoded);
|
|
954
|
+
yield* checked('decode', () => {
|
|
955
|
+
checkSnapshot(snapshot, 'decode', '$.snapshot', machine.id);
|
|
956
|
+
checkMetadata(encoded, snapshot, 'decode');
|
|
957
|
+
});
|
|
958
|
+
return snapshot;
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
export { SnapshotCodecError, createEffectEventStreamLogic, createEffectLogic, createEffectStreamLogic, createSnapshotCodec, deadLetters, emitted, inspect, join, send, setupEffect, snapshots, waitFor };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xstate/effect",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-beta.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Effect integration for XState",
|
|
6
6
|
"keywords": [
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"exports": {
|
|
16
16
|
"./atom": "./dist/xstate-effect-atom.js",
|
|
17
|
+
"./test": "./dist/xstate-effect-test.js",
|
|
17
18
|
".": "./dist/xstate-effect.js",
|
|
19
|
+
"./oxlint": "./dist/xstate-effect-oxlint.js",
|
|
18
20
|
"./package.json": "./package.json"
|
|
19
21
|
},
|
|
20
22
|
"sideEffects": false,
|
|
@@ -29,19 +31,29 @@
|
|
|
29
31
|
"url": "https://github.com/statelyai/xstate/issues"
|
|
30
32
|
},
|
|
31
33
|
"peerDependencies": {
|
|
34
|
+
"@oxlint/plugins": "^1.76.0",
|
|
32
35
|
"effect": "^4.0.0",
|
|
33
|
-
"xstate": "^6.0.0-
|
|
36
|
+
"xstate": "^6.0.0-beta.0"
|
|
34
37
|
},
|
|
35
38
|
"devDependencies": {
|
|
36
|
-
"
|
|
39
|
+
"@oxlint/plugins": "^1.76.0",
|
|
40
|
+
"effect": "4.0.6",
|
|
37
41
|
"fast-check": "4.10.2",
|
|
38
|
-
"xstate": "^
|
|
42
|
+
"@xstate/test": "^2.0.0-beta.0",
|
|
43
|
+
"xstate": "^6.0.0-beta.0"
|
|
39
44
|
},
|
|
40
45
|
"preconstruct": {
|
|
41
46
|
"entrypoints": [
|
|
42
47
|
"./index.ts",
|
|
43
|
-
"./atom.ts"
|
|
48
|
+
"./atom.ts",
|
|
49
|
+
"./oxlint.ts",
|
|
50
|
+
"./test.ts"
|
|
44
51
|
]
|
|
45
52
|
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@oxlint/plugins": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
46
58
|
"scripts": {}
|
|
47
59
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const effectActionBrand: unique symbol;
|