@xstate/effect 1.0.0-alpha.9 → 1.0.0-beta.0

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