@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.
@@ -1,7 +1,8 @@
1
- import { Schema, Effect, Stream, Exit, Cause, Queue } from 'effect';
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 EffectInterruptedError, A as ActorStoppedError, a as runHostedEffect } from './state-a2e5cb31.js';
4
- export { d as ActorScope, A as ActorStoppedError, b as EffectActor, E as EffectInterruptedError, c as createEffectActor, t as taggedState, w as withActorScope } from './state-a2e5cb31.js';
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 Effect's failure, defect or
96
- * `EffectInterruptedError` as `error`.
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 toError(exit) {
132
- if (Exit.isSuccess(exit)) {
133
- return undefined;
134
- }
135
- if (Cause.hasInterruptsOnly(exit.cause)) {
136
- return new EffectInterruptedError({
137
- cause: exit.cause
138
- });
139
- }
140
- return Cause.squash(exit.cause);
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's error, its
167
- * squashed defect, or an `EffectInterruptedError` when the Effect interrupts
168
- * itself. Stopping the actor interrupts the Effect without an error. Accepts
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: toError(exit)
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: toError(exit)
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: toError(exit)
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
- /** The event type accepted by an actor's `send` method. */
440
+ /**
441
+ * The event type accepted by an actor's `send` method.
442
+ *
443
+ * @experimental
444
+ */
391
445
 
392
- /** The event type an actor emits through `actor.on(...)`. */
446
+ /**
447
+ * The event type an actor emits through `actor.on(...)`.
448
+ *
449
+ * @experimental
450
+ */
393
451
 
394
- /** Options for {@link waitFor}. */
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, fails with `snapshot.error` when it errors, and fails with
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.fail(snapshot.error));
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
- const result = action(args);
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
- export { createEffectEventStreamLogic, createEffectLogic, createEffectStreamLogic, deadLetters, emitted, inspect, join, send, setupEffect, snapshots, waitFor };
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-alpha.9",
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-alpha.70"
36
+ "xstate": "^6.0.0-beta.2"
34
37
  },
35
38
  "devDependencies": {
36
- "effect": "4.0.0",
39
+ "@oxlint/plugins": "^1.76.0",
40
+ "effect": "4.0.6",
37
41
  "fast-check": "4.10.2",
38
- "xstate": "^6.0.0-alpha.70"
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;