@xstate/effect 0.1.0-alpha.7 → 1.0.0-alpha.10
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 +35 -18
- package/dist/declarations/src/actor.d.ts +4 -4
- package/dist/declarations/src/atom.d.ts +8 -9
- package/dist/declarations/src/effectActor.d.ts +19 -13
- package/dist/declarations/src/{fromEffect.d.ts → effectLogic.d.ts} +28 -30
- package/dist/declarations/src/errors.d.ts +23 -6
- package/dist/declarations/src/index.d.ts +4 -3
- package/dist/declarations/src/oxlint.d.ts +2 -0
- package/dist/declarations/src/persistence.d.ts +39 -0
- package/dist/declarations/src/schema.d.ts +2 -2
- package/dist/declarations/src/setupEffect.d.ts +48 -13
- package/dist/declarations/src/state.d.ts +8 -7
- package/dist/declarations/src/test.d.ts +11 -0
- package/dist/declarations/src/types.d.ts +2 -3
- package/dist/{state-b3d9b3e2.js → state-a3f54e06.js} +210 -88
- package/dist/xstate-effect-atom.js +10 -8
- package/dist/xstate-effect-oxlint.d.ts +3 -0
- package/dist/xstate-effect-oxlint.js +124 -0
- package/dist/xstate-effect-test.d.ts +2 -0
- package/dist/xstate-effect-test.js +11 -0
- package/dist/xstate-effect.js +231 -54
- package/package.json +17 -5
- package/dist/declarations/src/brands.d.ts +0 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sends one mailbox item and awaits its macrostep publication and immediate
|
|
3
|
+
* effect scheduling. Ignored events are acknowledged too. Does not await
|
|
4
|
+
* invoked work, background actions or future timers. Interrupting the waiter
|
|
5
|
+
* detaches its receipt without retracting the event.
|
|
6
|
+
*/
|
|
7
|
+
function sendAndAwait(actor, event) {
|
|
8
|
+
return actor._sendAndAwait(event);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { sendAndAwait };
|
package/dist/xstate-effect.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { Schema, Effect, Stream,
|
|
2
|
-
import {
|
|
3
|
-
import { s as startHostedEffect, r as relayToParent, E as
|
|
4
|
-
export {
|
|
1
|
+
import { Schema, Effect, Stream, Cause, Exit, Queue, Data } from 'effect';
|
|
2
|
+
import { createStepLogic, setup } from 'xstate';
|
|
3
|
+
import { s as startHostedEffect, r as relayToParent, E as EffectFailure, A as ActorStoppedError, a as causeFromError, b as runHostedEffect } from './state-a3f54e06.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-a3f54e06.js';
|
|
5
5
|
import { dual } from 'effect/Function';
|
|
6
6
|
import 'xstate/durable';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* An Effect `Schema` accepted by this package. `setupEffect`, `
|
|
10
|
-
* `
|
|
9
|
+
* An Effect `Schema` accepted by this package. `setupEffect`, `createEffectLogic`,
|
|
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
13
|
*/
|
|
@@ -74,7 +74,7 @@ function toStandardSetupStates(states) {
|
|
|
74
74
|
}));
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const EFFECT_INIT = '
|
|
77
|
+
const EFFECT_INIT = 'xstate.init';
|
|
78
78
|
const EFFECT_RESOLVE = 'xstate.effect.resolve';
|
|
79
79
|
const EFFECT_REJECT = 'xstate.effect.reject';
|
|
80
80
|
const EFFECT_NEXT = 'xstate.effect.next';
|
|
@@ -82,18 +82,17 @@ const EFFECT_COMPLETE = 'xstate.effect.complete';
|
|
|
82
82
|
const effectLogicBrand = Symbol.for('@xstate/effect/logic');
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* Type-level marker attached to logic created by `
|
|
86
|
-
* `
|
|
85
|
+
* Type-level marker attached to logic created by `createEffectLogic`,
|
|
86
|
+
* `createEffectStreamLogic` and `createEffectEventStreamLogic`. It carries the logic's
|
|
87
87
|
* failure type and its Effect service requirements, which is how
|
|
88
88
|
* `RequirementsFrom` collects the `R` channel of `createEffectActor` without
|
|
89
89
|
* inspecting the logic at runtime.
|
|
90
90
|
*/
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
* Snapshot of a `
|
|
93
|
+
* Snapshot of a `createEffectLogic` actor. The actor holds no context of its own: it
|
|
94
94
|
* is `active` while the Effect runs, `done` with the Effect's success value as
|
|
95
|
-
* `output`, or `error` with the
|
|
96
|
-
* `EffectInterruptedError` as `error`.
|
|
95
|
+
* `output`, or `error` with the original `Cause` wrapped in `EffectFailure` as `error`.
|
|
97
96
|
*/
|
|
98
97
|
|
|
99
98
|
/**
|
|
@@ -104,40 +103,32 @@ const effectLogicBrand = Symbol.for('@xstate/effect/logic');
|
|
|
104
103
|
*/
|
|
105
104
|
|
|
106
105
|
/**
|
|
107
|
-
* What `
|
|
106
|
+
* What `createEffectLogic` accepts as its Effect: either an Effect value, or a
|
|
108
107
|
* function of {@link EffectSourceArgs} returning one. The function form is
|
|
109
108
|
* called once per actor start, so it sees that actor's input.
|
|
110
109
|
*/
|
|
111
110
|
|
|
112
111
|
/**
|
|
113
|
-
* Actor logic produced by `
|
|
112
|
+
* Actor logic produced by `createEffectLogic` and `createEffectEventStreamLogic`. It is
|
|
114
113
|
* ordinary XState actor logic carrying {@link EffectLogicBrand}, so a machine
|
|
115
114
|
* can invoke or spawn it, and `createEffectActor` can collect its
|
|
116
115
|
* requirements.
|
|
117
116
|
*/
|
|
118
117
|
|
|
119
118
|
/**
|
|
120
|
-
* Snapshot of a `
|
|
119
|
+
* Snapshot of a `createEffectStreamLogic` actor. `context` holds the most recent
|
|
121
120
|
* stream item, or `undefined` before the first one. The actor reaches `done`
|
|
122
121
|
* with no output when the stream completes, and `error` when it fails.
|
|
123
122
|
*/
|
|
124
123
|
|
|
125
124
|
/**
|
|
126
|
-
* Actor logic produced by `
|
|
125
|
+
* Actor logic produced by `createEffectStreamLogic`. Its snapshot is an
|
|
127
126
|
* {@link EffectStreamSnapshot}, so the parent reads the latest item from the
|
|
128
127
|
* child's `context` rather than from events.
|
|
129
128
|
*/
|
|
130
129
|
|
|
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);
|
|
130
|
+
function preserveFailure(error) {
|
|
131
|
+
return EffectFailure.is(error) ? error : new EffectFailure(Cause.die(error));
|
|
141
132
|
}
|
|
142
133
|
function createSourceArgs(input, self, system) {
|
|
143
134
|
return {
|
|
@@ -150,6 +141,32 @@ function createSourceArgs(input, self, system) {
|
|
|
150
141
|
};
|
|
151
142
|
}
|
|
152
143
|
function brandLogic(logic, error, requirements) {
|
|
144
|
+
const transition = logic.transition.bind(logic);
|
|
145
|
+
logic.transition = (snapshot, event, scope) => {
|
|
146
|
+
try {
|
|
147
|
+
return transition(snapshot, event, scope);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
throw preserveFailure(error);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const initialTransition = logic.initialTransition.bind(logic);
|
|
153
|
+
logic.initialTransition = (input, scope) => {
|
|
154
|
+
try {
|
|
155
|
+
return initialTransition(input, scope);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw preserveFailure(error);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const restoreSnapshot = logic.restoreSnapshot?.bind(logic);
|
|
161
|
+
if (restoreSnapshot) {
|
|
162
|
+
logic.restoreSnapshot = (persisted, scope) => {
|
|
163
|
+
const snapshot = restoreSnapshot(persisted, scope);
|
|
164
|
+
if (snapshot.status === 'error' && !EffectFailure.is(snapshot.error)) {
|
|
165
|
+
throw new Error('Cannot restore a serialized Effect failure. Recover the error before persisting the actor.');
|
|
166
|
+
}
|
|
167
|
+
return snapshot;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
153
170
|
Object.defineProperty(logic, effectLogicBrand, {
|
|
154
171
|
configurable: false,
|
|
155
172
|
enumerable: false,
|
|
@@ -163,9 +180,7 @@ function brandLogic(logic, error, requirements) {
|
|
|
163
180
|
|
|
164
181
|
/**
|
|
165
182
|
* 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
|
|
183
|
+
* 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
184
|
* an Effect, a function of {@link EffectSourceArgs} returning one, or a config
|
|
170
185
|
* object with `id`, `schemas`, `validator` and `effect`. The logic must be run
|
|
171
186
|
* under `createEffectActor`, which provides the Effect services it declares.
|
|
@@ -174,7 +189,7 @@ function brandLogic(logic, error, requirements) {
|
|
|
174
189
|
* Effect actor stops.
|
|
175
190
|
*/
|
|
176
191
|
|
|
177
|
-
function
|
|
192
|
+
function createEffectLogic(sourceOrConfig) {
|
|
178
193
|
const config = typeof sourceOrConfig === 'function' || Effect.isEffect(sourceOrConfig) ? {
|
|
179
194
|
effect: sourceOrConfig
|
|
180
195
|
} : sourceOrConfig;
|
|
@@ -187,7 +202,7 @@ function fromEffect(sourceOrConfig) {
|
|
|
187
202
|
output: toStandardSchema(config.schemas.output)
|
|
188
203
|
} : {})
|
|
189
204
|
} : undefined;
|
|
190
|
-
const logic =
|
|
205
|
+
const logic = createStepLogic({
|
|
191
206
|
id: config.id,
|
|
192
207
|
validator: config.validator,
|
|
193
208
|
schemas,
|
|
@@ -208,15 +223,15 @@ function fromEffect(sourceOrConfig) {
|
|
|
208
223
|
if (event.type === EFFECT_REJECT) {
|
|
209
224
|
return {
|
|
210
225
|
status: 'error',
|
|
211
|
-
error: event.error,
|
|
226
|
+
error: preserveFailure(event.error),
|
|
212
227
|
input: undefined
|
|
213
228
|
};
|
|
214
229
|
}
|
|
215
230
|
if (event.type !== EFFECT_INIT) {
|
|
216
231
|
return;
|
|
217
232
|
}
|
|
218
|
-
const effect = typeof source === 'function' ? source(createSourceArgs(input, self, system)) : source;
|
|
219
|
-
enq.effect(() => startHostedEffect(self, Effect.scoped(effect), '
|
|
233
|
+
const effect = Effect.suspend(() => typeof source === 'function' ? source(createSourceArgs(input, self, system)) : source);
|
|
234
|
+
enq.effect(() => startHostedEffect(self, Effect.scoped(effect), 'createEffectLogic', exit => {
|
|
220
235
|
if (self.getSnapshot().status !== 'active') {
|
|
221
236
|
return;
|
|
222
237
|
}
|
|
@@ -228,7 +243,7 @@ function fromEffect(sourceOrConfig) {
|
|
|
228
243
|
} else {
|
|
229
244
|
self.send({
|
|
230
245
|
type: EFFECT_REJECT,
|
|
231
|
-
error:
|
|
246
|
+
error: new EffectFailure(exit.cause)
|
|
232
247
|
});
|
|
233
248
|
}
|
|
234
249
|
}));
|
|
@@ -238,7 +253,7 @@ function fromEffect(sourceOrConfig) {
|
|
|
238
253
|
}
|
|
239
254
|
|
|
240
255
|
/**
|
|
241
|
-
* What `
|
|
256
|
+
* What `createEffectStreamLogic` and `createEffectEventStreamLogic` accept as their stream:
|
|
242
257
|
* either a Stream value, or a function of {@link EffectSourceArgs} returning
|
|
243
258
|
* one. The function form is called once per actor start.
|
|
244
259
|
*/
|
|
@@ -263,10 +278,10 @@ function toLogicSchemas(schemas) {
|
|
|
263
278
|
* completes, fails or is interrupted.
|
|
264
279
|
*/
|
|
265
280
|
|
|
266
|
-
function
|
|
281
|
+
function createEffectStreamLogic(sourceOrConfig) {
|
|
267
282
|
const config = resolveStreamConfig(sourceOrConfig);
|
|
268
283
|
const stream = config.stream;
|
|
269
|
-
const logic =
|
|
284
|
+
const logic = createStepLogic({
|
|
270
285
|
id: config.id,
|
|
271
286
|
validator: config.validator,
|
|
272
287
|
schemas: toLogicSchemas(config.schemas),
|
|
@@ -291,14 +306,14 @@ function fromEffectStream(sourceOrConfig) {
|
|
|
291
306
|
if (event.type === EFFECT_REJECT) {
|
|
292
307
|
return {
|
|
293
308
|
status: 'error',
|
|
294
|
-
error: event.error,
|
|
309
|
+
error: preserveFailure(event.error),
|
|
295
310
|
input: undefined
|
|
296
311
|
};
|
|
297
312
|
}
|
|
298
313
|
if (event.type !== EFFECT_INIT) {
|
|
299
314
|
return;
|
|
300
315
|
}
|
|
301
|
-
const streamValue = typeof stream === 'function' ? stream(createSourceArgs(input, self, system)) : stream;
|
|
316
|
+
const streamValue = Stream.suspend(() => typeof stream === 'function' ? stream(createSourceArgs(input, self, system)) : stream);
|
|
302
317
|
const consume = Stream.runForEach(streamValue, value => Effect.sync(() => {
|
|
303
318
|
if (self.getSnapshot().status === 'active') {
|
|
304
319
|
self.send({
|
|
@@ -307,7 +322,7 @@ function fromEffectStream(sourceOrConfig) {
|
|
|
307
322
|
});
|
|
308
323
|
}
|
|
309
324
|
}));
|
|
310
|
-
enq.effect(() => startHostedEffect(self, Effect.scoped(consume), '
|
|
325
|
+
enq.effect(() => startHostedEffect(self, Effect.scoped(consume), 'createEffectStreamLogic', exit => {
|
|
311
326
|
if (self.getSnapshot().status !== 'active') {
|
|
312
327
|
return;
|
|
313
328
|
}
|
|
@@ -318,7 +333,7 @@ function fromEffectStream(sourceOrConfig) {
|
|
|
318
333
|
} else {
|
|
319
334
|
self.send({
|
|
320
335
|
type: EFFECT_REJECT,
|
|
321
|
-
error:
|
|
336
|
+
error: new EffectFailure(exit.cause)
|
|
322
337
|
});
|
|
323
338
|
}
|
|
324
339
|
}));
|
|
@@ -331,14 +346,14 @@ function fromEffectStream(sourceOrConfig) {
|
|
|
331
346
|
* Creates actor logic that runs a Stream of events and relays each item to the
|
|
332
347
|
* parent machine as an event, the way `fromEventObservable` does. The actor
|
|
333
348
|
* has no output: it reaches `done` when the stream completes and `error` when
|
|
334
|
-
* it fails. Accepts the same forms as `
|
|
349
|
+
* it fails. Accepts the same forms as `createEffectStreamLogic`. Its resources close
|
|
335
350
|
* when the stream completes, fails or is interrupted.
|
|
336
351
|
*/
|
|
337
352
|
|
|
338
|
-
function
|
|
353
|
+
function createEffectEventStreamLogic(sourceOrConfig) {
|
|
339
354
|
const config = resolveStreamConfig(sourceOrConfig);
|
|
340
355
|
const stream = config.stream;
|
|
341
|
-
const logic =
|
|
356
|
+
const logic = createStepLogic({
|
|
342
357
|
id: config.id,
|
|
343
358
|
validator: config.validator,
|
|
344
359
|
schemas: toLogicSchemas(config.schemas),
|
|
@@ -358,16 +373,16 @@ function fromEffectEventStream(sourceOrConfig) {
|
|
|
358
373
|
if (event.type === EFFECT_REJECT) {
|
|
359
374
|
return {
|
|
360
375
|
status: 'error',
|
|
361
|
-
error: event.error,
|
|
376
|
+
error: preserveFailure(event.error),
|
|
362
377
|
input: undefined
|
|
363
378
|
};
|
|
364
379
|
}
|
|
365
380
|
if (event.type !== EFFECT_INIT) {
|
|
366
381
|
return;
|
|
367
382
|
}
|
|
368
|
-
const streamValue = typeof stream === 'function' ? stream(createSourceArgs(input, self, system)) : stream;
|
|
383
|
+
const streamValue = Stream.suspend(() => typeof stream === 'function' ? stream(createSourceArgs(input, self, system)) : stream);
|
|
369
384
|
const consume = Stream.runForEach(streamValue, value => Effect.sync(() => relayToParent(self, value)));
|
|
370
|
-
enq.effect(() => startHostedEffect(self, Effect.scoped(consume), '
|
|
385
|
+
enq.effect(() => startHostedEffect(self, Effect.scoped(consume), 'createEffectEventStreamLogic', exit => {
|
|
371
386
|
if (self.getSnapshot().status !== 'active') {
|
|
372
387
|
return;
|
|
373
388
|
}
|
|
@@ -378,7 +393,7 @@ function fromEffectEventStream(sourceOrConfig) {
|
|
|
378
393
|
} else {
|
|
379
394
|
self.send({
|
|
380
395
|
type: EFFECT_REJECT,
|
|
381
|
-
error:
|
|
396
|
+
error: new EffectFailure(exit.cause)
|
|
382
397
|
});
|
|
383
398
|
}
|
|
384
399
|
}));
|
|
@@ -549,9 +564,9 @@ const waitFor = dual(args => isActorRef(args[0]), (actor, predicate, options) =>
|
|
|
549
564
|
|
|
550
565
|
/**
|
|
551
566
|
* Joins an actor's final result, like `Fiber.join`: succeeds with its `output`
|
|
552
|
-
* when it is done,
|
|
567
|
+
* when it is done, replays an Effect actor's original Cause when it errors, and fails with
|
|
553
568
|
* `ActorStoppedError` when it stops without output. Waits for a still-active
|
|
554
|
-
* actor to settle.
|
|
569
|
+
* actor to settle. Ordinary XState errors remain typed failures.
|
|
555
570
|
*/
|
|
556
571
|
function join(actor) {
|
|
557
572
|
return Effect.callback(resume => {
|
|
@@ -560,7 +575,7 @@ function join(actor) {
|
|
|
560
575
|
if (snapshot.status === 'done') {
|
|
561
576
|
resume(Effect.succeed(snapshot.output));
|
|
562
577
|
} else if (snapshot.status === 'error') {
|
|
563
|
-
resume(Effect.
|
|
578
|
+
resume(Effect.failCause(causeFromError(snapshot.error)));
|
|
564
579
|
} else {
|
|
565
580
|
resume(Effect.fail(stoppedError(actor)));
|
|
566
581
|
}
|
|
@@ -659,7 +674,12 @@ function wrapActions(actions) {
|
|
|
659
674
|
const action = actions[key];
|
|
660
675
|
wrapped[key] = args => {
|
|
661
676
|
// machine.provide can replace Effect actions with plain actions.
|
|
662
|
-
|
|
677
|
+
let result;
|
|
678
|
+
try {
|
|
679
|
+
result = action(args);
|
|
680
|
+
} catch (error) {
|
|
681
|
+
result = Effect.die(error);
|
|
682
|
+
}
|
|
663
683
|
return Effect.isEffect(result) ? runHostedEffect(args.self, result, `action.${key}`) : result;
|
|
664
684
|
};
|
|
665
685
|
}
|
|
@@ -710,4 +730,161 @@ function setupEffect(config = {}) {
|
|
|
710
730
|
}));
|
|
711
731
|
}
|
|
712
732
|
|
|
713
|
-
|
|
733
|
+
/** A malformed transport, unsupported JSON value or missing persistence metadata. */
|
|
734
|
+
class SnapshotCodecError extends Data.TaggedError('SnapshotCodecError') {
|
|
735
|
+
get message() {
|
|
736
|
+
return `${this.stage} at ${this.path}: ${this.reason}`;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function record(value) {
|
|
740
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
741
|
+
}
|
|
742
|
+
function invalid(stage, path, reason) {
|
|
743
|
+
throw new SnapshotCodecError({
|
|
744
|
+
stage,
|
|
745
|
+
path,
|
|
746
|
+
reason
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
function checkJson(value, stage, path = '$', ancestors = new Set()) {
|
|
750
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
|
|
751
|
+
if (typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0)) return;
|
|
752
|
+
if (typeof value !== 'object') invalid(stage, path, 'Expected lossless JSON; encode this value with a schema');
|
|
753
|
+
if (ancestors.has(value)) invalid(stage, path, 'Cyclic value');
|
|
754
|
+
if (Array.isArray(value) && Object.getPrototypeOf(value) !== Array.prototype) invalid(stage, path, 'Unsupported array prototype');
|
|
755
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
756
|
+
invalid(stage, path, 'Unsupported object prototype; supply a reversible schema');
|
|
757
|
+
}
|
|
758
|
+
if (Object.getOwnPropertySymbols(value).length) invalid(stage, path, 'Symbol properties are not JSON');
|
|
759
|
+
ancestors.add(value);
|
|
760
|
+
if (Array.isArray(value)) {
|
|
761
|
+
if (Object.keys(value).length !== value.length) invalid(stage, path, 'Array holes or extra properties are not JSON');
|
|
762
|
+
for (let index = 0; index < value.length; index++) {
|
|
763
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
764
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) invalid(stage, `${path}[${index}]`, 'Array accessor or hole is not JSON');
|
|
765
|
+
checkJson(descriptor.value, stage, `${path}[${index}]`, ancestors);
|
|
766
|
+
}
|
|
767
|
+
if (Object.getOwnPropertyNames(value).length !== value.length + 1) invalid(stage, path, 'Extra array properties are not JSON');
|
|
768
|
+
} else {
|
|
769
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
770
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
771
|
+
if (!descriptor.enumerable || !('value' in descriptor)) invalid(stage, `${path}.${key}`, 'Non-enumerable or accessor property is not JSON');
|
|
772
|
+
checkJson(descriptor.value, stage, `${path}.${key}`, ancestors);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
ancestors.delete(value);
|
|
776
|
+
}
|
|
777
|
+
function checkSnapshot(value, stage, path = '$.snapshot', machineId) {
|
|
778
|
+
if (!record(value)) invalid(stage, path, 'Expected a persisted snapshot');
|
|
779
|
+
if (!['active', 'done', 'stopped', 'error'].includes(String(value.status))) invalid(stage, path, 'Invalid snapshot status');
|
|
780
|
+
if (value.status === 'error') invalid(stage, path, 'Errored snapshots require an explicit error recovery policy before persistence');
|
|
781
|
+
if (machineId !== undefined || 'machine' in value) {
|
|
782
|
+
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');
|
|
783
|
+
if (machineId !== undefined && value.machine.id !== machineId) invalid(stage, `${path}.machine.id`, 'Machine identity mismatch');
|
|
784
|
+
}
|
|
785
|
+
if ('value' in value || 'machine' in value) {
|
|
786
|
+
for (const key of ['context', 'value', 'children', 'historyValue', 'timers']) {
|
|
787
|
+
if (!(key in value)) invalid(stage, `${path}.${key}`, 'Missing machine persistence metadata');
|
|
788
|
+
}
|
|
789
|
+
if (!record(value.children) || !record(value.historyValue) || !Array.isArray(value.timers)) invalid(stage, path, 'Invalid machine persistence metadata');
|
|
790
|
+
}
|
|
791
|
+
if (record(value.children)) {
|
|
792
|
+
for (const [id, child] of Object.entries(value.children)) {
|
|
793
|
+
if (!record(child) || child.src !== undefined && typeof child.src !== 'string') invalid(stage, `${path}.children.${id}`, 'Invalid persisted child');
|
|
794
|
+
if (child.remote === true) {
|
|
795
|
+
if (child.snapshot !== undefined) invalid(stage, `${path}.children.${id}`, 'Remote children cannot contain snapshots');
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
checkSnapshot(child.snapshot, stage, `${path}.children.${id}.snapshot`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function sameData(left, right) {
|
|
803
|
+
if (Object.is(left, right)) return true;
|
|
804
|
+
if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((item, index) => sameData(item, right[index]));
|
|
805
|
+
if (record(left) && record(right)) {
|
|
806
|
+
const keys = Object.keys(left);
|
|
807
|
+
return keys.length === Object.keys(right).length && keys.every(key => Object.hasOwn(right, key) && sameData(left[key], right[key]));
|
|
808
|
+
}
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
function checkMetadata(before, after, stage, path = '$.snapshot') {
|
|
812
|
+
for (const [key, value] of Object.entries(before)) {
|
|
813
|
+
if (['context', 'input', 'output', 'error', 'stateInputs'].includes(key) || value === undefined) continue;
|
|
814
|
+
if (key === 'children' && record(value) && record(after.children)) {
|
|
815
|
+
if (!sameData(Object.keys(value).sort(), Object.keys(after.children).sort())) invalid(stage, `${path}.children`, 'Schema changed persisted children');
|
|
816
|
+
for (const [id, child] of Object.entries(value)) {
|
|
817
|
+
const next = after.children[id];
|
|
818
|
+
if (!record(child) || !record(next)) invalid(stage, `${path}.children.${id}`, 'Schema changed child metadata');
|
|
819
|
+
for (const childKey of Object.keys(child)) {
|
|
820
|
+
if (childKey !== 'snapshot' && child[childKey] !== undefined && !sameData(child[childKey], next[childKey])) invalid(stage, `${path}.children.${id}.${childKey}`, 'Schema changed child metadata');
|
|
821
|
+
}
|
|
822
|
+
if (record(child.snapshot) && record(next.snapshot)) checkMetadata(child.snapshot, next.snapshot, stage, `${path}.children.${id}.snapshot`);
|
|
823
|
+
}
|
|
824
|
+
} else if (!sameData(value, after[key])) {
|
|
825
|
+
invalid(stage, `${path}.${key}`, 'Schema changed machine persistence metadata');
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (before.input !== undefined && !('input' in after)) invalid(stage, `${path}.input`, 'Schema removed input');
|
|
829
|
+
if (before.output !== undefined && !('output' in after)) invalid(stage, `${path}.output`, 'Schema removed output');
|
|
830
|
+
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');
|
|
831
|
+
if ('stateInputs' in before && !('stateInputs' in after)) invalid(stage, `${path}.stateInputs`, 'Schema removed state inputs');
|
|
832
|
+
}
|
|
833
|
+
function checked(stage, run) {
|
|
834
|
+
return Effect.try({
|
|
835
|
+
try: run,
|
|
836
|
+
catch: error => error instanceof SnapshotCodecError ? error : new SnapshotCodecError({
|
|
837
|
+
stage,
|
|
838
|
+
path: '$',
|
|
839
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
840
|
+
})
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Binds a full bidirectional snapshot schema to a versioned machine. The schema
|
|
846
|
+
* owns rich values and supported historical versions. Decoding never migrates;
|
|
847
|
+
* restore or `machineVersions` remains responsible for migration.
|
|
848
|
+
*/
|
|
849
|
+
function createSnapshotCodec(machine, schema) {
|
|
850
|
+
const format = '@xstate/effect/snapshot';
|
|
851
|
+
return {
|
|
852
|
+
encode(snapshot) {
|
|
853
|
+
return Effect.gen(function* () {
|
|
854
|
+
yield* checked('encode', () => checkSnapshot(snapshot, 'encode', '$.snapshot', machine.id));
|
|
855
|
+
const encoded = yield* Schema.encodeUnknownEffect(schema)(snapshot);
|
|
856
|
+
return yield* checked('encode', () => {
|
|
857
|
+
checkJson(encoded, 'encode');
|
|
858
|
+
checkSnapshot(encoded, 'encode', '$.snapshot', machine.id);
|
|
859
|
+
checkMetadata(snapshot, encoded, 'encode');
|
|
860
|
+
return JSON.stringify({
|
|
861
|
+
format,
|
|
862
|
+
version: 1,
|
|
863
|
+
logicId: machine.id,
|
|
864
|
+
snapshot: encoded
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
},
|
|
869
|
+
decode(json) {
|
|
870
|
+
return Effect.gen(function* () {
|
|
871
|
+
const encoded = yield* checked('decode', () => {
|
|
872
|
+
const envelope = JSON.parse(json);
|
|
873
|
+
checkJson(envelope, 'decode');
|
|
874
|
+
if (!record(envelope) || envelope.format !== format || envelope.version !== 1) invalid('decode', '$', 'Unsupported snapshot format or version');
|
|
875
|
+
if (envelope.logicId !== machine.id) invalid('decode', '$.logicId', 'Machine identity mismatch');
|
|
876
|
+
checkSnapshot(envelope.snapshot, 'decode', '$.snapshot', machine.id);
|
|
877
|
+
return envelope.snapshot;
|
|
878
|
+
});
|
|
879
|
+
const snapshot = yield* Schema.decodeUnknownEffect(schema)(encoded);
|
|
880
|
+
yield* checked('decode', () => {
|
|
881
|
+
checkSnapshot(snapshot, 'decode', '$.snapshot', machine.id);
|
|
882
|
+
checkMetadata(encoded, snapshot, 'decode');
|
|
883
|
+
});
|
|
884
|
+
return snapshot;
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
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": "
|
|
3
|
+
"version": "1.0.0-alpha.10",
|
|
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-alpha.
|
|
36
|
+
"xstate": "^6.0.0-alpha.71"
|
|
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-alpha.4",
|
|
43
|
+
"xstate": "^6.0.0-alpha.71"
|
|
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;
|