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