@xstate/effect 1.0.0-beta.0 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -266,7 +266,7 @@ See [atoms and React](docs/atoms-and-react.md) for complete examples with import
266
266
 
267
267
  <!-- error types from src/errors.ts; timeout failure from src/actor.ts -->
268
268
 
269
- - Use `sendAndAwait` from `@xstate/effect/test` to acknowledge a specific event, including ignored events.
269
+ - Use `sendAndAwait` from `@xstate/effect` to acknowledge one event after its transition and immediate effects. It does not wait for child replies, invoked work, background actions or future timers.
270
270
  - Use `TestClock` to drive deadlines without waiting for real time.
271
271
  - `ActorStoppedError` reports a stop before an awaited result.
272
272
  - `EffectFailure<E>` carries the original `Cause<E>` in task and stream error snapshots, including defects and self-interruption.
@@ -41,6 +41,9 @@ export declare class EffectActor<TLogic extends AnyActorLogic> implements ActorR
41
41
  private readonly _listeners;
42
42
  private _settled;
43
43
  private readonly _pending;
44
+ private _inStep;
45
+ private _deliveryDepth;
46
+ private _stopHeld;
44
47
  constructor(logic: TLogic, root: AnyActor, snapshot: SnapshotFrom<TLogic>, _mailbox: Queue.Queue<MailboxItem<EventFromLogic<TLogic>, SnapshotFrom<TLogic>>>, _stopExecution: () => void, _inspectors: Set<(event: InspectionEvent) => void>);
45
48
  getSnapshot(): SnapshotFrom<TLogic>;
46
49
  getPersistedSnapshot(): PersistedSnapshotOf<TLogic>;
@@ -48,13 +51,18 @@ export declare class EffectActor<TLogic extends AnyActorLogic> implements ActorR
48
51
  send(event: EventFromLogic<TLogic>): void;
49
52
  private _deliveryError;
50
53
  private _stoppedError;
51
- /** Stops the actor, its children and every Effect it hosts. */
54
+ /**
55
+ * Stops the actor, its children and every Effect it hosts. If a listener or
56
+ * an observer calls `stop()` while the actor delivers a macrostep to it, the
57
+ * actor stops when that macrostep ends. `createActor` does the same.
58
+ */
52
59
  stop(): this;
53
60
  subscribe(observer: Observer<SnapshotFrom<TLogic>>): Subscription;
54
61
  subscribe(nextListener?: (snapshot: SnapshotFrom<TLogic>) => void, errorListener?: (error: any) => void, completeListener?: () => void): Subscription;
55
62
  on<TType extends EmittedFrom<TLogic>['type'] | '*'>(type: TType, handler: (emitted: EmittedFrom<TLogic> & (TType extends '*' ? unknown : {
56
63
  type: TType;
57
64
  })) => void): Subscription;
65
+ private _deliver;
58
66
  /**
59
67
  * Observes the inspection events of this actor and its children: every
60
68
  * actor creation and transition of the execution.
@@ -62,7 +70,7 @@ export declare class EffectActor<TLogic extends AnyActorLogic> implements ActorR
62
70
  inspect(observer: Observer<InspectionEvent> | ((event: InspectionEvent) => void)): Subscription;
63
71
  [symbolObservable](): this;
64
72
  toJSON(): {
65
- $actor: string;
73
+ '@actor': string;
66
74
  };
67
75
  }
68
76
  export {};
@@ -1,6 +1,7 @@
1
1
  export { createEffectLogic, createEffectEventStreamLogic, createEffectStreamLogic, type EffectActorLogic, type EffectLogicBrand, type EffectSnapshot, type EffectSource, type EffectSourceArgs, type EffectStreamSource, type EffectStreamActorLogic, type EffectStreamSnapshot } from "./effectLogic.js";
2
2
  export { createEffectActor, type EffectActorOptions } from "./createEffectActor.js";
3
3
  export { EffectActor } from "./effectActor.js";
4
+ export { sendAndAwait } from "./sendAndAwait.js";
4
5
  export { ActorScope, withActorScope } from "./actorScope.js";
5
6
  export { deadLetters, emitted, inspect, join, send, snapshots, waitFor, type EmittedEventFrom, type SendableEventFrom, type WaitForOptions } from "./actor.js";
6
7
  export { ActorStoppedError, EventRejectedError, EffectFailure, EffectInterruptedError } from "./errors.js";
@@ -0,0 +1,14 @@
1
+ import type { Effect } from 'effect';
2
+ import type { AnyActorLogic, EventFromLogic, SnapshotFrom } from 'xstate';
3
+ import type { EffectActor } from "./effectActor.js";
4
+ import type { ActorStoppedError, EventRejectedError } from "./errors.js";
5
+ /**
6
+ * Sends one event and acknowledges its transition and immediate effects.
7
+ * Ignored events are acknowledged too, as are events whose actions fail; the
8
+ * failure is processed afterwards. Child replies, invoked work, background
9
+ * action bodies and future timers are not awaited. Interrupting the waiter
10
+ * detaches its receipt without retracting the event.
11
+ *
12
+ * @experimental
13
+ */
14
+ export declare function sendAndAwait<TLogic extends AnyActorLogic>(actor: EffectActor<TLogic>, event: EventFromLogic<TLogic>): Effect.Effect<SnapshotFrom<TLogic>, ActorStoppedError | EventRejectedError>;
@@ -1,13 +1 @@
1
- import type { Effect } from 'effect';
2
- import type { AnyActorLogic, EventFromLogic, SnapshotFrom } from 'xstate';
3
- import type { EffectActor } from "./effectActor.js";
4
- import type { ActorStoppedError, EventRejectedError } from "./errors.js";
5
- /**
6
- * Sends one mailbox item and awaits its macrostep publication and immediate
7
- * effect scheduling. Ignored events are acknowledged too. Does not await
8
- * invoked work, background actions or future timers. Interrupting the waiter
9
- * detaches its receipt without retracting the event.
10
- *
11
- * @experimental
12
- */
13
- export declare function sendAndAwait<TLogic extends AnyActorLogic>(actor: EffectActor<TLogic>, event: EventFromLogic<TLogic>): Effect.Effect<SnapshotFrom<TLogic>, ActorStoppedError | EventRejectedError>;
1
+ export { sendAndAwait } from "./sendAndAwait.js";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Sends one event and acknowledges its transition and immediate effects.
3
+ * Ignored events are acknowledged too, as are events whose actions fail; the
4
+ * failure is processed afterwards. Child replies, invoked work, background
5
+ * action bodies and future timers are not awaited. Interrupting the waiter
6
+ * detaches its receipt without retracting the event.
7
+ *
8
+ * @experimental
9
+ */
10
+ function sendAndAwait(actor, event) {
11
+ return actor._sendAndAwait(event);
12
+ }
13
+
14
+ export { sendAndAwait as s };
@@ -90,6 +90,8 @@ function causeFromError(error) {
90
90
  */
91
91
  class EventRejectedError extends Data.TaggedError('EventRejectedError') {}
92
92
 
93
+ /** @internal */
94
+
93
95
  const symbolObservable = (() => typeof Symbol === 'function' && Symbol.observable || '@@observable')();
94
96
 
95
97
  /**
@@ -142,6 +144,9 @@ class EffectActor {
142
144
  this._listeners = new Map();
143
145
  this._settled = false;
144
146
  this._pending = new Set();
147
+ this._inStep = false;
148
+ this._deliveryDepth = 0;
149
+ this._stopHeld = false;
145
150
  this._root = root;
146
151
  this.id = root.id;
147
152
  this.address = root.address;
@@ -203,11 +208,6 @@ class EffectActor {
203
208
  }
204
209
  return undefined;
205
210
  }
206
-
207
- /** @internal */
208
- _rejectReceipts() {
209
- for (const receipt of this._pending) this._completeReceipt(receipt, Exit.fail(this._stoppedError()));
210
- }
211
211
  _stoppedError() {
212
212
  return new ActorStoppedError({
213
213
  actorId: this.id,
@@ -223,8 +223,16 @@ class EffectActor {
223
223
  resume?.(exit);
224
224
  }
225
225
 
226
- /** Stops the actor, its children and every Effect it hosts. */
226
+ /**
227
+ * Stops the actor, its children and every Effect it hosts. If a listener or
228
+ * an observer calls `stop()` while the actor delivers a macrostep to it, the
229
+ * actor stops when that macrostep ends. `createActor` does the same.
230
+ */
227
231
  stop() {
232
+ if (this._inStep && this._deliveryDepth > 0) {
233
+ this._stopHeld = true;
234
+ return this;
235
+ }
228
236
  this._stopExecution();
229
237
  return this;
230
238
  }
@@ -265,9 +273,20 @@ class EffectActor {
265
273
 
266
274
  /** @internal Delivers an emitted event to `on` listeners. */
267
275
  _emit(event) {
276
+ if (this._settled) return;
268
277
  const listeners = [...(this._listeners.get(event.type) ?? []), ...(this._listeners.get('*') ?? [])];
269
- for (const listener of listeners) {
270
- safeCall(listener, event);
278
+ this._deliver(() => {
279
+ for (const listener of listeners) {
280
+ safeCall(listener, event);
281
+ }
282
+ });
283
+ }
284
+ _deliver(notify) {
285
+ this._deliveryDepth++;
286
+ try {
287
+ notify();
288
+ } finally {
289
+ this._deliveryDepth--;
271
290
  }
272
291
  }
273
292
 
@@ -289,48 +308,66 @@ class EffectActor {
289
308
  }
290
309
  toJSON() {
291
310
  return {
292
- $actor: this.address
311
+ '@actor': this.address
293
312
  };
294
313
  }
295
314
 
296
- /** @internal Publishes a snapshot produced by the execution loop. */
297
- _publish(snapshot, receipt) {
298
- if (this._settled) return;
299
- this._snapshot = snapshot;
300
- const status = snapshot.status;
301
- if (status === 'active') {
302
- for (const observer of this._observers) {
303
- safeCall(observer.next, snapshot);
315
+ /** @internal */
316
+ _runStep(step) {
317
+ return Effect.gen({
318
+ self: this
319
+ }, function* () {
320
+ if (this._settled) return;
321
+ this._snapshot = step.snapshot;
322
+ this._inStep = true;
323
+ yield* step.effects;
324
+ if (this._settled) return;
325
+ if (this._snapshot.status !== 'active') {
326
+ this._settle(this._snapshot, step.kind === 'transition' ? step.receipt : undefined);
327
+ } else if (step.kind === 'transition') {
328
+ this._deliver(() => {
329
+ for (const observer of this._observers) {
330
+ safeCall(observer.next, this._snapshot);
331
+ }
332
+ });
304
333
  }
305
- return;
306
- }
307
- this._settle(snapshot, receipt);
334
+ }).pipe(Effect.onExit(exit => Effect.sync(() => {
335
+ this._inStep = false;
336
+ if (this._stopHeld) {
337
+ this._stopExecution();
338
+ } else if (step.kind === 'transition' && Exit.isSuccess(exit)) {
339
+ this._completeReceipt(step.receipt, step.rejection ? Exit.fail(step.rejection) : Exit.succeed(step.snapshot));
340
+ }
341
+ })));
308
342
  }
309
343
 
310
- /** @internal Marks the actor stopped and notifies observers. */
344
+ /** @internal Settles the actor and notifies its observers. */
311
345
  _settle(snapshot, processing) {
312
346
  if (this._settled) {
313
347
  return;
314
348
  }
315
349
  this._snapshot = snapshot;
316
350
  this._settled = true;
317
- for (const receipt of this._pending) {
318
- if (receipt !== processing) this._completeReceipt(receipt, Exit.fail(this._stoppedError()));
319
- }
320
351
  const observers = [...this._observers];
321
352
  this._observers.clear();
322
353
  const status = snapshot.status;
323
- if (status === 'done') {
324
- for (const observer of observers) {
325
- safeCall(observer.next, snapshot);
354
+ this._deliver(() => {
355
+ if (status === 'done') {
356
+ for (const observer of observers) {
357
+ safeCall(observer.next, snapshot);
358
+ }
326
359
  }
327
- }
328
- for (const observer of observers) {
329
- if (status === 'error') {
330
- safeCall(observer.error, snapshot.error);
331
- } else {
332
- safeCall(observer.complete);
360
+ for (const observer of observers) {
361
+ if (status === 'error') {
362
+ safeCall(observer.error, snapshot.error);
363
+ } else {
364
+ safeCall(observer.complete);
365
+ }
333
366
  }
367
+ });
368
+ this._completeReceipt(processing, Exit.succeed(snapshot));
369
+ for (const receipt of this._pending) {
370
+ this._completeReceipt(receipt, Exit.fail(this._stoppedError()));
334
371
  }
335
372
  }
336
373
 
@@ -761,12 +798,12 @@ function createEffectActor(logic, ...[options]) {
761
798
  }
762
799
  runFork(Queue.shutdown(mailbox));
763
800
  if (!actor._isSettled) {
764
- actor._settle({
765
- ...actor.getSnapshot(),
801
+ const current = actor.getSnapshot();
802
+ actor._settle(current.status === 'active' ? {
803
+ ...current,
766
804
  status: 'stopped'
767
- });
805
+ } : current);
768
806
  }
769
- actor._rejectReceipts();
770
807
  closeEffectHost(host);
771
808
  };
772
809
 
@@ -805,7 +842,11 @@ function createEffectActor(logic, ...[options]) {
805
842
  });
806
843
  }));
807
844
  const loop = Effect.gen(function* () {
808
- yield* executeEffects(effects);
845
+ yield* actor._runStep({
846
+ kind: 'start',
847
+ snapshot,
848
+ effects: executeEffects(effects)
849
+ });
809
850
  while (snapshot.status === 'active' && !stopped) {
810
851
  delivered = undefined;
811
852
  const received = yield* Effect.promise(() => durable.waitForEvent().catch(() => undefined));
@@ -820,6 +861,11 @@ function createEffectActor(logic, ...[options]) {
820
861
  const errorEvent = logic.getExecutionErrorEvent?.(snapshot, item.error);
821
862
  if (!errorEvent) {
822
863
  snapshot = errorSnapshot(snapshot, item.error);
864
+ yield* actor._runStep({
865
+ kind: 'transition',
866
+ snapshot,
867
+ effects: Effect.void
868
+ });
823
869
  break;
824
870
  }
825
871
  event = errorEvent;
@@ -830,22 +876,28 @@ function createEffectActor(logic, ...[options]) {
830
876
  [snapshot, effects] = durable.transition(snapshot, event);
831
877
  } catch (error) {
832
878
  snapshot = errorSnapshot(snapshot, error);
833
- actor._publish(snapshot, receipt);
834
- actor._completeReceipt(receipt, Exit.succeed(snapshot));
879
+ yield* actor._runStep({
880
+ kind: 'transition',
881
+ snapshot,
882
+ effects: Effect.void,
883
+ receipt
884
+ });
835
885
  break;
836
886
  }
837
- actor._publish(snapshot, receipt);
838
- if (stopped) break;
839
887
  const rejectedReason = effects.length === 1 && effects[0].effect.type === '@xstate.deadLetter' && 'reason' in effects[0].effect && effects[0].effect.reason === 'invalidEvent' ? effects[0].effect.reason : undefined;
840
- yield* executeEffects(effects);
841
- actor._completeReceipt(receipt, rejectedReason !== undefined ? Exit.fail(new EventRejectedError({
842
- actorId: actor.id,
843
- event,
844
- reason: rejectedReason
845
- })) : Exit.succeed(snapshot));
888
+ yield* actor._runStep({
889
+ kind: 'transition',
890
+ snapshot,
891
+ effects: executeEffects(effects),
892
+ receipt,
893
+ rejection: rejectedReason !== undefined ? new EventRejectedError({
894
+ actorId: actor.id,
895
+ event,
896
+ reason: rejectedReason
897
+ }) : undefined
898
+ });
846
899
  }
847
900
  if (!stopped) {
848
- actor._publish(snapshot);
849
901
  if (snapshot.status !== 'active') {
850
902
  stopChildren(snapshot);
851
903
  closeEffectHost(host);
@@ -855,13 +907,14 @@ function createEffectActor(logic, ...[options]) {
855
907
  yield* Effect.forkIn(loop, actorScope);
856
908
  return {
857
909
  actor,
858
- host
910
+ host,
911
+ stopNow: stop
859
912
  };
860
913
  }), ({
861
- actor,
862
- host
914
+ host,
915
+ stopNow
863
916
  }) => Effect.gen(function* () {
864
- actor.stop();
917
+ stopNow();
865
918
  if (host.closing) {
866
919
  yield* Fiber.join(host.closing);
867
920
  } else {
@@ -1,8 +1,8 @@
1
1
  import { Cause } from 'effect';
2
2
  import { Atom, AsyncResult } from 'effect/reactivity';
3
3
  import { StateMachine } from 'xstate';
4
- import { c as createEffectActor, a as causeFromError, N as NotReadyError, t as taggedState } from './state-a2154dce.js';
5
- export { N as NotReadyError } from './state-a2154dce.js';
4
+ import { c as createEffectActor, a as causeFromError, N as NotReadyError, t as taggedState } from './state-83513b45.js';
5
+ export { N as NotReadyError } from './state-83513b45.js';
6
6
  import 'xstate/durable';
7
7
 
8
8
  /**
@@ -1,13 +1 @@
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
- * @experimental
8
- */
9
- function sendAndAwait(actor, event) {
10
- return actor._sendAndAwait(event);
11
- }
12
-
13
- export { sendAndAwait };
1
+ export { s as sendAndAwait } from './sendAndAwait-bf1d4117.js';
@@ -1,7 +1,8 @@
1
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 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';
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
 
@@ -748,6 +749,12 @@ function wrapActions(actions) {
748
749
  }
749
750
  return Effect.isEffect(result) ? runHostedEffect(args.self, result, `action.${key}`) : result;
750
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
+ }
751
758
  }
752
759
  return wrapped;
753
760
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xstate/effect",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.1",
4
4
  "type": "module",
5
5
  "description": "Effect integration for XState",
6
6
  "keywords": [
@@ -33,14 +33,14 @@
33
33
  "peerDependencies": {
34
34
  "@oxlint/plugins": "^1.76.0",
35
35
  "effect": "^4.0.0",
36
- "xstate": "^6.0.0-beta.0"
36
+ "xstate": "^6.0.0-beta.2"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@oxlint/plugins": "^1.76.0",
40
40
  "effect": "4.0.6",
41
41
  "fast-check": "4.10.2",
42
- "@xstate/test": "^2.0.0-beta.0",
43
- "xstate": "^6.0.0-beta.0"
42
+ "@xstate/test": "^2.0.0-beta.1",
43
+ "xstate": "^6.0.0-beta.2"
44
44
  },
45
45
  "preconstruct": {
46
46
  "entrypoints": [