@xstate/effect 0.1.0-alpha.6 → 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.
@@ -0,0 +1,124 @@
1
+ import { defineRule, definePlugin } from '@oxlint/plugins';
2
+
3
+ const ENQUEUE_NAMES = new Set(['enq', 'enqueue']);
4
+ const EFFECT_LOGIC_CONSTRUCTORS = new Set(['createEffectLogic', 'createEffectStreamLogic', 'createEffectEventStreamLogic']);
5
+ const isNode = value => typeof value === 'object' && value !== null && 'type' in value && typeof value.type === 'string';
6
+ const rootIdentifier = node => {
7
+ let current = node;
8
+ for (;;) {
9
+ if (current.type === 'CallExpression') {
10
+ current = current.callee;
11
+ } else if (current.type === 'MemberExpression') {
12
+ current = current.object;
13
+ } else if (current.type === 'Identifier') {
14
+ return current.name;
15
+ } else {
16
+ return undefined;
17
+ }
18
+ }
19
+ };
20
+ const returnedExpressions = fn => {
21
+ if (fn.body === null) {
22
+ return [];
23
+ }
24
+ if (fn.body.type !== 'BlockStatement') {
25
+ return [fn.body];
26
+ }
27
+ const found = [];
28
+ const visit = node => {
29
+ if (node.type === 'ReturnStatement') {
30
+ if (node.argument) {
31
+ found.push(node.argument);
32
+ }
33
+ return;
34
+ }
35
+ if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression' || node.type === 'FunctionDeclaration') {
36
+ return;
37
+ }
38
+ for (const [key, value] of Object.entries(node)) {
39
+ if (key === 'parent') {
40
+ continue;
41
+ }
42
+ if (Array.isArray(value)) {
43
+ for (const item of value) {
44
+ if (isNode(item)) {
45
+ visit(item);
46
+ }
47
+ }
48
+ } else if (isNode(value)) {
49
+ visit(value);
50
+ }
51
+ }
52
+ };
53
+ visit(fn.body);
54
+ return found;
55
+ };
56
+ const isPromiseResolve = node => node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name === 'Promise' && node.callee.property.type === 'Identifier' && node.callee.property.name === 'resolve';
57
+ const isEffectExpression = node => {
58
+ if (node.type !== 'CallExpression') {
59
+ return false;
60
+ }
61
+ if (isPromiseResolve(node)) {
62
+ const [argument] = node.arguments;
63
+ return argument !== undefined && isEffectExpression(argument);
64
+ }
65
+ return rootIdentifier(node) === 'Effect';
66
+ };
67
+ const isInlineEffectLogic = node => node.type === 'CallExpression' && node.callee.type === 'Identifier' && EFFECT_LOGIC_CONSTRUCTORS.has(node.callee.name);
68
+ const isFunctionNode = node => node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression';
69
+ const noInlineEffect = defineRule({
70
+ create(context) {
71
+ return {
72
+ CallExpression(node) {
73
+ const {
74
+ callee
75
+ } = node;
76
+ if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier' && ENQUEUE_NAMES.has(callee.object.name) && callee.property.type === 'Identifier' && callee.property.name === 'spawn') {
77
+ const [logic] = node.arguments;
78
+ if (logic !== undefined && isInlineEffectLogic(logic)) {
79
+ context.report({
80
+ messageId: 'inlineSpawn',
81
+ node: logic
82
+ });
83
+ }
84
+ return;
85
+ }
86
+ if (callee.type !== 'Identifier' || !ENQUEUE_NAMES.has(callee.name)) {
87
+ return;
88
+ }
89
+ const [action] = node.arguments;
90
+ if (action === undefined || !isFunctionNode(action)) {
91
+ return;
92
+ }
93
+ for (const expression of returnedExpressions(action)) {
94
+ if (isEffectExpression(expression)) {
95
+ context.report({
96
+ messageId: 'inlineAction',
97
+ node: expression
98
+ });
99
+ }
100
+ }
101
+ }
102
+ };
103
+ },
104
+ meta: {
105
+ docs: {
106
+ description: 'Disallow Effects in inline enqueue callbacks and inline Effect logic in enq.spawn; declare them in setupEffect({ actions }) or actors.'
107
+ },
108
+ messages: {
109
+ inlineAction: 'An Effect returned from an inline action is discarded. Declare it in setupEffect({ actions }) and run it with enq(args.actions.name, args).',
110
+ inlineSpawn: 'Inline Effect logic passed to enq.spawn is invisible to RequirementsFrom and rejected at runtime. Declare it in actors and spawn args.actors.name.'
111
+ },
112
+ type: 'problem'
113
+ }
114
+ });
115
+ var oxlint = definePlugin({
116
+ meta: {
117
+ name: 'xstate-effect'
118
+ },
119
+ rules: {
120
+ 'no-inline-effect': noInlineEffect
121
+ }
122
+ });
123
+
124
+ export { oxlint as default };
@@ -0,0 +1,2 @@
1
+ export * from "./declarations/src/test.js";
2
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoieHN0YXRlLWVmZmVjdC10ZXN0LmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuL2RlY2xhcmF0aW9ucy9zcmMvdGVzdC5kLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBIn0=
@@ -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 };
@@ -1,13 +1,13 @@
1
- import { Schema, Effect, Stream, Exit, Cause, Queue } from 'effect';
2
- import { createLogic, setup } from 'xstate';
3
- import { s as startHostedEffect, r as relayToParent, E as EffectInterruptedError, A as ActorStoppedError, a as runHostedEffect } from './state-14a1fecf.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-14a1fecf.js';
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`, `fromEffect`,
10
- * `fromEffectStream` and `fromEffectEventStream` convert it to a Standard
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 = '@xstate.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 `fromEffect`,
86
- * `fromEffectStream` and `fromEffectEventStream`. It carries the logic's
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 `fromEffect` actor. The actor holds no context of its own: it
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 Effect's failure, defect or
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 `fromEffect` accepts as its Effect: either an Effect value, or a
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 `fromEffect` and `fromEffectEventStream`. It is
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 `fromEffectStream` actor. `context` holds the most recent
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 `fromEffectStream`. Its snapshot is an
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 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);
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'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
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 fromEffect(sourceOrConfig) {
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 = createLogic({
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), 'fromEffect', exit => {
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: toError(exit)
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 `fromEffectStream` and `fromEffectEventStream` accept as their stream:
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 fromEffectStream(sourceOrConfig) {
281
+ function createEffectStreamLogic(sourceOrConfig) {
267
282
  const config = resolveStreamConfig(sourceOrConfig);
268
283
  const stream = config.stream;
269
- const logic = createLogic({
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), 'fromEffectStream', exit => {
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: toError(exit)
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 `fromEffectStream`. Its resources close
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 fromEffectEventStream(sourceOrConfig) {
353
+ function createEffectEventStreamLogic(sourceOrConfig) {
339
354
  const config = resolveStreamConfig(sourceOrConfig);
340
355
  const stream = config.stream;
341
- const logic = createLogic({
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), 'fromEffectEventStream', exit => {
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: toError(exit)
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, fails with `snapshot.error` when it errors, and fails with
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.fail(snapshot.error));
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
- const result = action(args);
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
- export { deadLetters, emitted, fromEffect, fromEffectEventStream, fromEffectStream, inspect, join, send, setupEffect, snapshots, waitFor };
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": "0.1.0-alpha.6",
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,18 +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.63"
36
+ "xstate": "^6.0.0-alpha.71"
34
37
  },
35
38
  "devDependencies": {
36
- "effect": "4.0.0",
37
- "xstate": "^6.0.0-alpha.63"
39
+ "@oxlint/plugins": "^1.76.0",
40
+ "effect": "4.0.6",
41
+ "fast-check": "4.10.2",
42
+ "@xstate/test": "^2.0.0-alpha.4",
43
+ "xstate": "^6.0.0-alpha.71"
38
44
  },
39
45
  "preconstruct": {
40
46
  "entrypoints": [
41
47
  "./index.ts",
42
- "./atom.ts"
48
+ "./atom.ts",
49
+ "./oxlint.ts",
50
+ "./test.ts"
43
51
  ]
44
52
  },
53
+ "peerDependenciesMeta": {
54
+ "@oxlint/plugins": {
55
+ "optional": true
56
+ }
57
+ },
45
58
  "scripts": {}
46
59
  }