@xstate/effect 0.1.0-alpha.2 → 0.1.0-alpha.5
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 +165 -511
- package/dist/declarations/src/actor.d.ts +7 -6
- package/dist/declarations/src/actorScope.d.ts +11 -0
- package/dist/declarations/src/atom.d.ts +4 -3
- package/dist/declarations/src/createEffectActor.d.ts +4 -8
- package/dist/declarations/src/effectActor.d.ts +1 -1
- package/dist/declarations/src/fromEffect.d.ts +7 -2
- package/dist/declarations/src/index.d.ts +1 -0
- package/dist/declarations/src/types.d.ts +6 -4
- package/dist/{state-9a718be3.js → state-14a1fecf.js} +38 -22
- package/dist/xstate-effect-atom.js +5 -4
- package/dist/xstate-effect.js +22 -12
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# @xstate/effect
|
|
2
2
|
|
|
3
|
-
Effect
|
|
3
|
+
Combine XState's event-driven workflows with Effect services, scopes, clocks and tracing.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- Model approval, cancellation, deadlines and retry as explicit states and events.
|
|
6
|
+
- Invoke Effect tasks when their result determines the next state.
|
|
7
|
+
- Provide services once for the actor and its child logic.
|
|
8
|
+
- Keep actor lifetime inside your Effect application.
|
|
9
|
+
|
|
10
|
+
This package is experimental. It targets XState v6 alpha and Effect 4 RC.
|
|
6
11
|
|
|
7
12
|
## Installation
|
|
8
13
|
|
|
@@ -12,600 +17,249 @@ This package is experimental and targets XState v6 alpha and Effect 4 RC.
|
|
|
12
17
|
npm install @xstate/effect@alpha xstate@alpha effect@rc
|
|
13
18
|
```
|
|
14
19
|
|
|
15
|
-
## Quick start
|
|
20
|
+
## XState Effect: Quick start
|
|
16
21
|
|
|
17
22
|
<!-- public API from src/index.ts; actor lifetime from src/createEffectActor.ts -->
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
A release waits for approval, expires after 30 seconds, and allows cancellation during deployment. A failed deployment waits for an explicit retry:
|
|
25
|
+
|
|
26
|
+
<!-- example from examples/effect-workflows/src/approval.ts -->
|
|
20
27
|
|
|
21
28
|
```ts
|
|
22
29
|
import { Context, Effect, Schema } from 'effect';
|
|
23
|
-
import {
|
|
24
|
-
|
|
30
|
+
import {
|
|
31
|
+
createEffectActor,
|
|
32
|
+
fromEffect,
|
|
33
|
+
send,
|
|
34
|
+
setupEffect,
|
|
35
|
+
waitFor
|
|
36
|
+
} from '@xstate/effect';
|
|
25
37
|
|
|
26
|
-
class
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
schemas: {
|
|
35
|
-
input: Schema.Struct({ id: Schema.String }),
|
|
36
|
-
output: Schema.Struct({ id: Schema.String })
|
|
37
|
-
},
|
|
38
|
-
effect: ({ input }) => Api.use((api) => api.fetchUser(input.id))
|
|
38
|
+
export class Deployments extends Context.Service<
|
|
39
|
+
Deployments,
|
|
40
|
+
{ readonly deploy: (release: string) => Effect.Effect<string, Error> }
|
|
41
|
+
>()('@app/Deployments') {}
|
|
42
|
+
|
|
43
|
+
const deploy = fromEffect({
|
|
44
|
+
schemas: { input: Schema.Struct({ release: Schema.String }) },
|
|
45
|
+
effect: ({ input }) => Deployments.use((api) => api.deploy(input.release))
|
|
39
46
|
});
|
|
40
47
|
|
|
41
|
-
const
|
|
42
|
-
|
|
48
|
+
export const approvalMachine = setupEffect({
|
|
49
|
+
schemas: { input: Schema.Struct({ release: Schema.String }) },
|
|
50
|
+
actors: { deploy }
|
|
51
|
+
}).createMachine({
|
|
52
|
+
context: ({ input }) => ({ release: input.release, url: '' }),
|
|
53
|
+
initial: 'awaitingApproval',
|
|
43
54
|
states: {
|
|
44
|
-
|
|
55
|
+
awaitingApproval: {
|
|
56
|
+
after: { 30000: { target: 'expired' } },
|
|
57
|
+
on: {
|
|
58
|
+
APPROVE: { target: 'deploying' },
|
|
59
|
+
CANCEL: { target: 'cancelled' }
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
deploying: {
|
|
45
63
|
invoke: {
|
|
46
|
-
src: '
|
|
47
|
-
input: {
|
|
48
|
-
onDone: {
|
|
49
|
-
|
|
64
|
+
src: 'deploy',
|
|
65
|
+
input: ({ context }) => ({ release: context.release }),
|
|
66
|
+
onDone: ({ context, event }) => ({
|
|
67
|
+
target: 'deployed',
|
|
68
|
+
context: { ...context, url: event.output }
|
|
69
|
+
}),
|
|
70
|
+
onError: { target: 'failed' }
|
|
71
|
+
},
|
|
72
|
+
on: { CANCEL: { target: 'cancelled' } }
|
|
73
|
+
},
|
|
74
|
+
failed: {
|
|
75
|
+
on: {
|
|
76
|
+
RETRY: { target: 'deploying' },
|
|
77
|
+
CANCEL: { target: 'cancelled' }
|
|
50
78
|
}
|
|
51
79
|
},
|
|
52
|
-
|
|
53
|
-
|
|
80
|
+
deployed: { type: 'final' },
|
|
81
|
+
expired: { type: 'final' },
|
|
82
|
+
cancelled: { type: 'final' }
|
|
54
83
|
}
|
|
55
84
|
});
|
|
56
85
|
|
|
57
|
-
const program = Effect.gen(function* () {
|
|
58
|
-
const actor = yield* createEffectActor(
|
|
59
|
-
|
|
60
|
-
|
|
86
|
+
export const program = Effect.gen(function* () {
|
|
87
|
+
const actor = yield* createEffectActor(approvalMachine, {
|
|
88
|
+
input: { release: 'v1.2.0' }
|
|
89
|
+
});
|
|
90
|
+
// A UI, webhook or CLI can send this event after a person approves.
|
|
91
|
+
yield* send(actor, { type: 'APPROVE' });
|
|
92
|
+
const snapshot = yield* waitFor(actor, (s) => s.matches('deployed'), {
|
|
93
|
+
timeout: '5 seconds'
|
|
94
|
+
});
|
|
95
|
+
return snapshot.context.url;
|
|
61
96
|
});
|
|
62
97
|
|
|
63
|
-
await Effect.runPromise(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
### How it runs
|
|
73
|
-
|
|
74
|
-
XState's transition function is pure: `transition(snapshot, event)` returns the next snapshot and the actions to run, without running them. `createEffectActor` drives that function through XState's `createDurable` loop from an Effect fiber and interprets the actions:
|
|
75
|
-
|
|
76
|
-
- the mailbox is an Effect `Queue`, so `actor.send` enqueues and the fiber processes events in order;
|
|
77
|
-
- timers for `after` transitions and delayed events are `Effect.sleep` fibers in the actor's `Scope`, on the Effect `Clock`;
|
|
78
|
-
- declared Effect actions run as forked Effects with the services captured at creation, without blocking the loop;
|
|
79
|
-
- built-in actions (spawn, send, emit, stop) run as XState defines them, and child actors run as live XState actors whose Effects use the same host.
|
|
80
|
-
|
|
81
|
-
The handle it returns is an `EffectActor`. It implements XState's `ActorRef` contract (`send`, `getSnapshot`, `subscribe`, `on`) plus `inspect`, `getPersistedSnapshot` and `stop`, so `useSelector`, this package's actor functions and the inspection APIs accept it. Because the loop runs on a fiber, `send` returns before the event is processed; read the outcome with `waitFor`, `join` or `snapshots` rather than `getSnapshot` right after a send.
|
|
82
|
-
|
|
83
|
-
## Actors
|
|
84
|
-
|
|
85
|
-
### Lifetime
|
|
86
|
-
|
|
87
|
-
`createEffectActor(logic, options?)` returns `Effect<Actor, never, R | Scope>`, where `R` is the union of service requirements of the logic (see [Requirements](#requirements)). The actor stops, and every Effect it hosts is interrupted, when the scope closes. Use `Effect.scoped` to close the scope when the program finishes, or run the actor inside a Layer to tie it to the application's lifetime.
|
|
88
|
-
|
|
89
|
-
Effects hosted by the actor see a `Scope` that closes when the actor stops. `Effect.addFinalizer` and `Effect.acquireRelease` inside an actor's Effects release with the actor, not with the Effect that registered them.
|
|
90
|
-
|
|
91
|
-
Provide Layers outside `Effect.scoped`, so their resources are released after the actor has stopped:
|
|
92
|
-
|
|
93
|
-
```ts
|
|
94
|
-
await Effect.runPromise(program.pipe(Effect.scoped, Effect.provide(AppLayer)));
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
`actor.stop()` still works. It stops the actor and closes the actor's scope synchronously; the release step of `createEffectActor` waits for the actor scope's finalizers before the enclosing scope continues closing.
|
|
98
|
-
|
|
99
|
-
### Providing an actor as a service
|
|
100
|
-
|
|
101
|
-
An actor built by `createEffectActor` is a scoped Effect, so `Layer.effect` turns it into a service. The actor starts when the Layer is built and stops when the Layer's scope closes.
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
import { Context, Effect, Layer, ManagedRuntime } from 'effect';
|
|
105
|
-
import {
|
|
106
|
-
createEffectActor,
|
|
107
|
-
send,
|
|
108
|
-
waitFor,
|
|
109
|
-
type EffectActor
|
|
110
|
-
} from '@xstate/effect';
|
|
111
|
-
|
|
112
|
-
class CheckoutActor extends Context.Service<
|
|
113
|
-
CheckoutActor,
|
|
114
|
-
EffectActor<typeof checkoutMachine>
|
|
115
|
-
>()('@app/CheckoutActor') {}
|
|
116
|
-
|
|
117
|
-
const CheckoutActorLayer = Layer.effect(
|
|
118
|
-
CheckoutActor,
|
|
119
|
-
createEffectActor(checkoutMachine)
|
|
98
|
+
export const result = await Effect.runPromise(
|
|
99
|
+
program.pipe(
|
|
100
|
+
Effect.scoped,
|
|
101
|
+
Effect.provideService(Deployments, {
|
|
102
|
+
// Replace this demo service with your deployment API.
|
|
103
|
+
deploy: (release) => Effect.succeed(`https://example.com/${release}`)
|
|
104
|
+
})
|
|
105
|
+
)
|
|
120
106
|
);
|
|
107
|
+
console.log(result); // 'https://example.com/v1.2.0'
|
|
121
108
|
```
|
|
122
109
|
|
|
123
|
-
The
|
|
110
|
+
The demo service runs locally. Replace it with your deployment API. The machine defines which events are valid in each state; Effect provides the deployment service and owns the actor's lifetime.
|
|
124
111
|
|
|
125
|
-
|
|
126
|
-
const AppLayer = CheckoutActorLayer.pipe(Layer.provide(PaymentsLayer));
|
|
127
|
-
```
|
|
112
|
+
Start Effect-backed logic with `createEffectActor` inside an Effect scope.
|
|
128
113
|
|
|
129
|
-
|
|
114
|
+
<details>
|
|
115
|
+
<summary>Runtime and cancellation details</summary>
|
|
130
116
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Effect.gen(function* () {
|
|
136
|
-
const actor = yield* CheckoutActor;
|
|
137
|
-
yield* send(actor, { type: 'PAY' });
|
|
138
|
-
return yield* waitFor(actor, (s) => s.matches('paid'));
|
|
139
|
-
})
|
|
140
|
-
);
|
|
117
|
+
- `createEffectActor` runs pure XState transitions from an Effect fiber. Its mailbox processes events in order; timers use Effect's `Clock`.
|
|
118
|
+
- Events are processed asynchronously. Read their outcomes with `waitFor`, `join` or `snapshots`.
|
|
119
|
+
- Starting Effect-backed logic with XState's `createActor` produces an error because the Effect host is missing.
|
|
120
|
+
- Leaving an invoking state interrupts its local Effect. It does not undo work already accepted by an external API; model cancellation or rollback explicitly when needed.
|
|
141
121
|
|
|
142
|
-
|
|
143
|
-
await runtime.dispose();
|
|
144
|
-
```
|
|
122
|
+
</details>
|
|
145
123
|
|
|
146
|
-
|
|
124
|
+
## Guides
|
|
147
125
|
|
|
148
|
-
|
|
126
|
+
| Guide | Examples |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| [Quick start](docs/quick-start.md) | Release approval, expiry, cancellation and retry. |
|
|
129
|
+
| [Actors](docs/actors.md) | Share an actor through a Layer and ManagedRuntime. |
|
|
130
|
+
| [Observing actors](docs/observing-actors.md) | Snapshot history, output and streamed reminders. |
|
|
131
|
+
| [Matching states](docs/matching-states.md) | Exhaustive matching and per-state context. |
|
|
132
|
+
| [Effect actor logic](docs/effect-logic.md) | Report tasks, upload progress and rollout health events. |
|
|
133
|
+
| [Schemas and actions](docs/schemas-and-actions.md) | Runtime event validation and background audit entries. |
|
|
134
|
+
| [Atoms and React](docs/atoms-and-react.md) | Registry ownership, approval controls and useSelector. |
|
|
135
|
+
| [Testing and errors](docs/testing-and-errors.md) | TestClock, task retries, supervision and typed failures. |
|
|
149
136
|
|
|
150
|
-
|
|
137
|
+
Runnable copies live in [examples/effect-workflows](../../examples/effect-workflows). Their tests verify the docs use the same code.
|
|
151
138
|
|
|
152
|
-
|
|
153
|
-
import { TestClock } from 'effect/testing';
|
|
139
|
+
## Actors
|
|
154
140
|
|
|
155
|
-
|
|
156
|
-
const actor = yield* createEffectActor(machine);
|
|
157
|
-
yield* TestClock.adjust('5 seconds');
|
|
158
|
-
return actor.getSnapshot().value;
|
|
159
|
-
});
|
|
141
|
+
The actor stops and interrupts its hosted Effects when its enclosing scope closes. Use `Effect.scoped` for a bounded program, or `Layer.effect` to share an actor for an application's lifetime.
|
|
160
142
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
```
|
|
143
|
+
- Invoked Effect tasks and streams own their resources and release them on completion, failure or cancellation.
|
|
144
|
+
- Background actions use the owning actor's scope.
|
|
145
|
+
- Use `withActorScope` around resource acquisition to keep a resource until the owning actor stops.
|
|
165
146
|
|
|
166
|
-
|
|
147
|
+
Provide Layers outside the actor's scope, so services remain available during cleanup. Dispose a `ManagedRuntime` when its owner shuts down. See [actors](docs/actors.md).
|
|
167
148
|
|
|
168
149
|
### Requirements
|
|
169
150
|
|
|
170
|
-
`RequirementsFrom<
|
|
151
|
+
`RequirementsFrom<TLogic>` collects services from declared Effect actions, declared actors and inline invocations, including child machines up to 10 levels deep. `createEffectActor` returns `Effect<EffectActor<TLogic>, never, R | Scope>`. Provided action and actor overrides update that service union. Actor and invocation scopes are supplied automatically.
|
|
171
152
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
- logic used inline as `invoke.src`, at the root or in any state,
|
|
175
|
-
- all of the above inside child machines, whether registered or invoked inline, up to 10 levels of machine nesting.
|
|
153
|
+
<details>
|
|
154
|
+
<summary>Requirement limits</summary>
|
|
176
155
|
|
|
177
|
-
|
|
156
|
+
Deeper requirements are not inferred. Provide them explicitly or flatten the machine tree. Inline spawned Effect logic is rejected; register it in `actors` before spawning it.
|
|
178
157
|
|
|
179
|
-
|
|
158
|
+
The actor's later errors are observed through `join` or snapshots. Invalid input rejected during creation is a defect.
|
|
180
159
|
|
|
181
|
-
|
|
160
|
+
</details>
|
|
182
161
|
|
|
183
162
|
### Observing actors
|
|
184
163
|
|
|
185
164
|
<!-- actor surface from src/actor.ts -->
|
|
186
165
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `emitted
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `join(actor)` | `Effect<Output, ErrorFrom<Logic> \| ActorStoppedError>` |
|
|
197
|
-
| `inspect(actor)` | `Stream<InspectionEvent>` |
|
|
198
|
-
| `deadLetters(actor)` | `Stream<DeadLetterInspectionEvent>` |
|
|
199
|
-
|
|
200
|
-
`send` and `waitFor` are dual: each takes the actor first, or returns a function of the actor so it can be piped.
|
|
166
|
+
| Function | Use |
|
|
167
|
+
| --- | --- |
|
|
168
|
+
| `send` | Enqueue an event. |
|
|
169
|
+
| `snapshots` | Stream the current snapshot and changes. |
|
|
170
|
+
| `waitFor` | Await a snapshot predicate, optionally with a timeout. |
|
|
171
|
+
| `join` | Await final output or failure. |
|
|
172
|
+
| `emitted` | Stream notifications emitted by the actor. |
|
|
173
|
+
| `inspect` | Stream system inspection events. |
|
|
174
|
+
| `deadLetters` | Observe delivery failures. |
|
|
201
175
|
|
|
202
|
-
|
|
203
|
-
yield* send(actor, { type: 'PAY' });
|
|
204
|
-
yield* pipe(actor, send({ type: 'PAY' }));
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
When `predicate` is a type predicate, `waitFor` narrows its result to the asserted snapshot type:
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
const snapshot = yield* waitFor(
|
|
211
|
-
actor,
|
|
212
|
-
(s): s is typeof s & { status: 'done' } => s.status === 'done'
|
|
213
|
-
);
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
The `timeout` failure is Effect's `Cause.TimeoutError`, not the `TimeoutError` that `xstate` exports for its own delayed-transition errors.
|
|
217
|
-
|
|
218
|
-
`snapshots` ends when the actor completes. If the actor errors, the stream emits the error snapshot and ends. `waitFor` and `join` fail with `ActorStoppedError` when the actor stops or errors before the awaited result; `join` fails with the actor's typed error when the actor's status is `error`.
|
|
219
|
-
|
|
220
|
-
```ts
|
|
221
|
-
const program = Effect.gen(function* () {
|
|
222
|
-
const actor = yield* createEffectActor(fetchUser, { input: { id: '42' } });
|
|
223
|
-
const user = yield* join(actor);
|
|
224
|
-
return user.id;
|
|
225
|
-
});
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
`send` returns `Effect<void>` and never fails. It enqueues the event, like `actor.send(event)`, and XState reports an event it could not deliver as a **dead letter** with a reason: `'stopped'` for a send to a stopped actor, `'invalidEvent'` for a payload the target's schema rejects, and `'internalEvent'` for an internal event type sent from outside its owning actor. A failing `send` could not carry that reason, and a dead letter is not an actor error. Observe them instead:
|
|
229
|
-
|
|
230
|
-
```ts
|
|
231
|
-
const program = Effect.gen(function* () {
|
|
232
|
-
const actor = yield* createEffectActor(machine);
|
|
233
|
-
yield* Effect.forkScoped(
|
|
234
|
-
Stream.runForEach(deadLetters(actor), (event) =>
|
|
235
|
-
Effect.logWarning(`undelivered ${event.event.type}: ${event.reason}`)
|
|
236
|
-
)
|
|
237
|
-
);
|
|
238
|
-
});
|
|
239
|
-
```
|
|
176
|
+
`send` and `waitFor` support direct and pipeable usage. See [complete observation examples](docs/observing-actors.md).
|
|
240
177
|
|
|
241
178
|
### Matching states
|
|
242
179
|
|
|
243
180
|
<!-- tagged state view from src/state.ts -->
|
|
244
181
|
|
|
245
|
-
`taggedState(snapshot)`
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
import { Match, Stream } from 'effect';
|
|
249
|
-
import { snapshots, taggedState, type TaggedState } from '@xstate/effect';
|
|
250
|
-
|
|
251
|
-
const describe = Match.type<TaggedState<typeof checkoutMachine>>().pipe(
|
|
252
|
-
Match.tag('cart', ({ context }) => `${context.items.length} items`),
|
|
253
|
-
Match.tag('paying', ({ context }) => `paying ${context.paymentId}`),
|
|
254
|
-
Match.tag('done.paid', 'done.declined', ({ _tag }) => _tag),
|
|
255
|
-
Match.exhaustive
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
const labels = snapshots(actor).pipe(Stream.map(taggedState), Stream.map(describe));
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
A parallel state has no single path. Its tag stops at the parallel state, or is `'(machine)'` when the machine itself is parallel; match on `value` or `snapshot.matches` there. The `state` atom from `createActorAtoms` exposes the same view.
|
|
262
|
-
|
|
263
|
-
## Atoms
|
|
264
|
-
|
|
265
|
-
<!-- atom surface from src/atom.ts -->
|
|
266
|
-
|
|
267
|
-
`@xstate/effect/atom` exposes an actor through `effect/unstable/reactivity`, so a reactive UI reads it the way it reads any other Effect state. `createActorAtoms(runtime, logic, options?)` takes an `Atom.runtime` whose Layer provides the logic's services and returns:
|
|
268
|
-
|
|
269
|
-
| Atom | Type |
|
|
270
|
-
| ----------- | ------------------------------------------------------------------------------- |
|
|
271
|
-
| `actor` | `Atom<AsyncResult<Actor>>` |
|
|
272
|
-
| `snapshot` | `Atom<AsyncResult<Snapshot>>` |
|
|
273
|
-
| `result` | `Atom<AsyncResult<Snapshot, ErrorFrom<Logic>>>`: a `Failure` once the actor errors. The runtime's own error type is also in every atom's error channel |
|
|
274
|
-
| `send` | `Writable<AsyncResult<void, NotReadyError>, Event>`: set it with an event; `NotReadyError` is exported from `@xstate/effect/atom` |
|
|
275
|
-
| `select(f)` | `Atom<AsyncResult<T>>` derived from `snapshot` |
|
|
276
|
-
| `state` | `Atom<AsyncResult<TaggedState>>`: the snapshot as a tagged union, see [Matching states](#matching-states) |
|
|
277
|
-
|
|
278
|
-
```ts
|
|
279
|
-
import { Effect, Layer } from 'effect';
|
|
280
|
-
import { Atom, AtomRegistry, AsyncResult } from 'effect/unstable/reactivity';
|
|
281
|
-
import { createActorAtoms } from '@xstate/effect/atom';
|
|
282
|
-
|
|
283
|
-
const runtime = Atom.runtime(
|
|
284
|
-
Layer.succeed(Api, { fetchUser: (id: string) => Effect.succeed({ id }) })
|
|
285
|
-
);
|
|
286
|
-
const user = createActorAtoms(runtime, machine);
|
|
287
|
-
const status = user.select((snapshot) => snapshot.value);
|
|
288
|
-
|
|
289
|
-
const registry = AtomRegistry.make();
|
|
290
|
-
registry.subscribe(
|
|
291
|
-
status,
|
|
292
|
-
(result) => {
|
|
293
|
-
if (AsyncResult.isSuccess(result)) {
|
|
294
|
-
console.log(result.value);
|
|
295
|
-
}
|
|
296
|
-
},
|
|
297
|
-
{ immediate: true }
|
|
298
|
-
);
|
|
299
|
-
registry.set(user.send, { type: 'RETRY' });
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
The actor starts when one of its atoms is first read and stops when nothing reads or mounts them anymore. Results are `AsyncResult` values because the runtime's Layer builds asynchronously. `send` enqueues the event, like `actor.send`; setting it before the runtime is ready records a `NotReadyError` failure instead. `result` is `snapshot` with an errored actor reported as a `Failure`, for error boundaries. Wrap an atom with `Atom.keepAlive` to keep the actor for the registry's lifetime, or build the atoms inside `Atom.family` to get one actor per input.
|
|
303
|
-
|
|
304
|
-
A runtime that does not provide a service the logic requires is a type error on the `runtime` argument.
|
|
305
|
-
|
|
306
|
-
`effect/unstable/reactivity` is an unstable Effect module. This entry point follows it and may change independently of the rest of the package.
|
|
307
|
-
|
|
308
|
-
## React
|
|
309
|
-
|
|
310
|
-
`useMachine`, `useActor` and `useActorRef` call `createActor` internally, so they cannot start Effect-backed logic. In an Effect application the actor lives in the runtime and React reads it through atoms: `@xstate/effect/atom` builds them and `@effect/atom-react` provides the hooks.
|
|
311
|
-
|
|
312
|
-
```tsx
|
|
313
|
-
import { Suspense } from 'react';
|
|
314
|
-
import { Atom } from 'effect/unstable/reactivity';
|
|
315
|
-
import { useAtomSet, useAtomSuspense } from '@effect/atom-react';
|
|
316
|
-
import { createActorAtoms } from '@xstate/effect/atom';
|
|
317
|
-
|
|
318
|
-
const runtime = Atom.runtime(AppLayer);
|
|
319
|
-
const checkout = createActorAtoms(runtime, checkoutMachine);
|
|
320
|
-
const status = checkout.select((snapshot) => snapshot.value);
|
|
321
|
-
|
|
322
|
-
function Checkout() {
|
|
323
|
-
const { value } = useAtomSuspense(status);
|
|
324
|
-
const send = useAtomSet(checkout.send);
|
|
325
|
-
|
|
326
|
-
return (
|
|
327
|
-
<button onClick={() => send({ type: 'PAY' })}>{String(value)}</button>
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export function App() {
|
|
332
|
-
return (
|
|
333
|
-
<Suspense fallback={null}>
|
|
334
|
-
<Checkout />
|
|
335
|
-
</Suspense>
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
`useAtomSuspense` suspends until the runtime and the actor are ready. `useAtomValue` returns the `AsyncResult` instead, for components that render their own loading state. The actor starts when the first component reads one of its atoms and stops when the last one unmounts. An owner component can hold it with `useAtomMount(checkout.actor)` while children read selectors; pin it with `Atom.keepAlive` when it must outlive the components.
|
|
341
|
-
|
|
342
|
-
Without atoms, create the actor through a `ManagedRuntime` and read it with `useSelector` from `@xstate/react`, which takes an existing actor reference. `actor.send` works directly there.
|
|
182
|
+
`taggedState(snapshot)` exposes `_tag`, `value`, `context` and `snapshot`. Use `TaggedState<typeof machine>` with Effect's `Match.tag` and `Match.exhaustive`. Per-state context narrows with the tag. See [matching states](docs/matching-states.md).
|
|
343
183
|
|
|
344
184
|
## Effect actor logic
|
|
345
185
|
|
|
346
186
|
<!-- fromEffect, fromEffectStream, fromEffectEventStream from src/fromEffect.ts -->
|
|
347
187
|
|
|
348
|
-
|
|
188
|
+
| Function | Behavior |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `fromEffect` | Run a task; its success value is the actor's output. |
|
|
191
|
+
| `fromEffectStream` | Expose the latest stream item as snapshot context. |
|
|
192
|
+
| `fromEffectEventStream` | Relay stream items to the parent as events. |
|
|
349
193
|
|
|
350
|
-
|
|
194
|
+
For example, a progress stream can feed an upload UI:
|
|
351
195
|
|
|
352
|
-
|
|
353
|
-
fromEffect(Effect.succeed('done'));
|
|
354
|
-
|
|
355
|
-
fromEffect(({ input }: { input: string }) => Effect.succeed(input.length));
|
|
356
|
-
|
|
357
|
-
fromEffect({
|
|
358
|
-
id: 'loadUser',
|
|
359
|
-
schemas: {
|
|
360
|
-
input: Schema.Struct({ id: Schema.String }),
|
|
361
|
-
output: Schema.Struct({ id: Schema.String })
|
|
362
|
-
},
|
|
363
|
-
effect: ({ input }) => Api.use((api) => api.fetchUser(input.id))
|
|
364
|
-
});
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
The function form receives `{ input, self, system, emit }`. `emit` publishes an event that `actor.on(...)` and `emitted(actor)` observe. Either schema may be omitted; the missing type is inferred from the Effect.
|
|
368
|
-
|
|
369
|
-
The actor's result maps from the Effect's exit:
|
|
370
|
-
|
|
371
|
-
- Success: status `done` with the value as `output`.
|
|
372
|
-
- Failure: status `error` with the `E` value as `error`. `invoke.onError` receives it typed through `ErrorFrom`.
|
|
373
|
-
- Defect: status `error` with the squashed cause. Defects are not part of the typed error.
|
|
374
|
-
- Interruption caused by the actor stopping or by the invoking state exiting: no error. The actor is stopped.
|
|
375
|
-
- Interruption from inside the Effect, such as `Effect.interrupt`: status `error` with an `EffectInterruptedError`. An `Effect.timeout` is a failure with `Cause.TimeoutError`, not an interruption; a lost `Effect.race` inside the Effect interrupts only the loser and the actor completes with the winner.
|
|
376
|
-
|
|
377
|
-
Use `Effect.timeout`, `Effect.retry` and the other Effect combinators inside the Effect. `fromEffect` does not add its own options for them.
|
|
378
|
-
|
|
379
|
-
### `fromEffectStream`
|
|
380
|
-
|
|
381
|
-
`fromEffectStream` exposes the latest stream item as the actor's `context` and reaches `done` when the stream completes. A stream failure puts the actor in the `error` status.
|
|
196
|
+
<!-- example from examples/effect-workflows/src/latest-stream.ts -->
|
|
382
197
|
|
|
383
198
|
```ts
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
fromEffectStream({
|
|
387
|
-
schemas: { input: Schema.Struct({ topic: Schema.String }) },
|
|
388
|
-
stream: ({ input }) => Stream.fromPubSub(topicPubSub(input.topic))
|
|
389
|
-
});
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### `fromEffectEventStream`
|
|
199
|
+
import { Effect, Option, Stream } from 'effect';
|
|
200
|
+
import { createEffectActor, fromEffectStream, snapshots } from '@xstate/effect';
|
|
393
201
|
|
|
394
|
-
|
|
202
|
+
// Demo upload progress. Replace with your upload SDK's progress stream.
|
|
203
|
+
const uploadProgress = fromEffectStream(Stream.make(0, 25, 60, 100));
|
|
395
204
|
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
src: fromEffectEventStream(
|
|
405
|
-
Stream.make({ type: 'VALUE', value: 1 }, { type: 'VALUE', value: 2 })
|
|
406
|
-
)
|
|
407
|
-
},
|
|
408
|
-
on: {
|
|
409
|
-
VALUE: ({ context, event }) => ({
|
|
410
|
-
context: { seen: context.seen + event.value }
|
|
411
|
-
})
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
205
|
+
const program = Effect.gen(function* () {
|
|
206
|
+
const actor = yield* createEffectActor(uploadProgress);
|
|
207
|
+
return yield* snapshots(actor).pipe(
|
|
208
|
+
Stream.filter((s) => s.context !== undefined),
|
|
209
|
+
Stream.map((s) => s.context),
|
|
210
|
+
Stream.runLast,
|
|
211
|
+
Effect.map(Option.getOrThrow)
|
|
212
|
+
);
|
|
415
213
|
});
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
## Effect schemas
|
|
419
|
-
|
|
420
|
-
<!-- schema conversion from src/schema.ts -->
|
|
421
214
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
```ts
|
|
425
|
-
import { Effect, Schema } from 'effect';
|
|
426
|
-
import { createEffectActor, setupEffect } from '@xstate/effect';
|
|
427
|
-
|
|
428
|
-
const machine = setupEffect({
|
|
429
|
-
schemas: {
|
|
430
|
-
context: Schema.Struct({ count: Schema.Number }),
|
|
431
|
-
events: {
|
|
432
|
-
ADD: Schema.Struct({ value: Schema.Number })
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}).createMachine({
|
|
436
|
-
context: { count: 0 },
|
|
437
|
-
on: {
|
|
438
|
-
ADD: ({ context, event }) => ({
|
|
439
|
-
context: { count: context.count + event.value }
|
|
440
|
-
})
|
|
441
|
-
}
|
|
442
|
-
});
|
|
215
|
+
export const result = await Effect.runPromise(Effect.scoped(program));
|
|
216
|
+
console.log(result); // 100
|
|
443
217
|
```
|
|
444
218
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
XState validation checks a value but does not replace it with a transformed value. When runtime validation is enabled, TypeScript rejects schemas whose encoded and decoded types differ, such as `Schema.NumberFromString`. Effect schemas used at this validation seam must decode synchronously and without service requirements.
|
|
448
|
-
|
|
449
|
-
## Effect actions
|
|
450
|
-
|
|
451
|
-
<!-- effect action contract from src/setupEffect.ts; spawn guard from src/internal.ts -->
|
|
452
|
-
|
|
453
|
-
`setupEffect({ actions })` declares actions that return an Effect. An Effect action is fire-and-forget: the transition enqueues it, the transition commits, and the Effect then runs in the actor's Effect context without blocking the actor. It is interrupted when the actor stops. A failure or defect routes to the state's `onError`.
|
|
219
|
+
See [Effect actor logic](docs/effect-logic.md) for tasks and a stream that drives rollout transitions.
|
|
454
220
|
|
|
455
|
-
|
|
221
|
+
## Schemas and actions
|
|
456
222
|
|
|
457
|
-
|
|
458
|
-
class Audit extends Context.Service<
|
|
459
|
-
Audit,
|
|
460
|
-
{ readonly record: (count: number) => Effect.Effect<void> }
|
|
461
|
-
>()('@app/Audit') {}
|
|
462
|
-
|
|
463
|
-
const machine = setupEffect({
|
|
464
|
-
actions: {
|
|
465
|
-
audit: ({ context }) => Audit.use((audit) => audit.record(context.count))
|
|
466
|
-
}
|
|
467
|
-
}).createMachine({
|
|
468
|
-
context: { count: 1 },
|
|
469
|
-
initial: 'active',
|
|
470
|
-
states: {
|
|
471
|
-
active: {
|
|
472
|
-
on: {
|
|
473
|
-
AUDIT: (args, enq) => enq(args.actions.audit, args)
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
});
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
Use Effect actions for work whose result the machine does not need: logging, telemetry, notifications, cache writes. When the result matters, invoke the Effect as an actor with `fromEffect`, so `onDone` and `onError` receive it typed and the state models the wait.
|
|
223
|
+
<!-- schema conversion from src/schema.ts -->
|
|
481
224
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
```ts
|
|
485
|
-
const machine = setupEffect({
|
|
486
|
-
schemas: {
|
|
487
|
-
events: {
|
|
488
|
-
SAVE: Schema.Struct({}),
|
|
489
|
-
SYNCED: Schema.Struct({ version: Schema.Number })
|
|
490
|
-
}
|
|
491
|
-
},
|
|
492
|
-
actions: {
|
|
493
|
-
sync: ({ context, self }) =>
|
|
494
|
-
Effect.gen(function* () {
|
|
495
|
-
const version = yield* Api.use((api) => api.sync(context.draft));
|
|
496
|
-
yield* Effect.sync(() => self.send({ type: 'SYNCED', version }));
|
|
497
|
-
})
|
|
498
|
-
}
|
|
499
|
-
}).createMachine({
|
|
500
|
-
context: { draft: '', version: 0 },
|
|
501
|
-
initial: 'active',
|
|
502
|
-
states: {
|
|
503
|
-
active: {
|
|
504
|
-
on: {
|
|
505
|
-
SAVE: (args, enq) => enq(args.actions.sync, args),
|
|
506
|
-
SYNCED: ({ context, event }) => ({
|
|
507
|
-
context: { ...context, version: event.version }
|
|
508
|
-
})
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
```
|
|
225
|
+
`setupEffect` accepts Effect schemas for input, context, events, output and per-state context. Types are inferred without manual conversion. Add `standardSchemaValidator()` from `xstate/validation` for runtime checks.
|
|
514
226
|
|
|
515
227
|
### Declared only
|
|
516
228
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
- Register Effect actions with `setupEffect({ actions })` and run them with `enq(args.actions.name, args)`.
|
|
520
|
-
- Register spawned Effect logic with `setup({ actors })` or `setupEffect({ actors })` and spawn it with `enq.spawn(args.actors.name)`. Spawning inline Effect logic is an error at runtime.
|
|
521
|
-
- Effect logic used inline as `invoke.src` runs and contributes to `RequirementsFrom`. Registering it in `actors` gives it a name.
|
|
522
|
-
|
|
523
|
-
> **Warning:** An inline action that returns an Effect does not run it. `enq(() => Effect.log('saved'))` creates the Effect and discards it, because XState only awaits returned promises. Register the action with `setupEffect({ actions })` instead.
|
|
524
|
-
|
|
525
|
-
Effect actions do not block the actor. The actor processes the next event while the Effect runs.
|
|
526
|
-
|
|
527
|
-
## Persistence
|
|
528
|
-
|
|
529
|
-
`actor.getPersistedSnapshot()` returns a serializable snapshot of the actor's state. It records state, not the progress of a running Effect. Restoring a snapshot into a new interpreter is not supported yet; the durable execution loop this package is built on is the intended path for that, and it is the next piece of work.
|
|
530
|
-
|
|
531
|
-
## Testing
|
|
532
|
-
|
|
533
|
-
Machines keep their normal test surface, and this package adds Effect-native ways to drive and observe them.
|
|
534
|
-
|
|
535
|
-
Delays run on the Effect `Clock`, so `TestClock` advances `after` transitions and delayed sends without real time passing:
|
|
536
|
-
|
|
537
|
-
```ts
|
|
538
|
-
import { Effect } from 'effect';
|
|
539
|
-
import { TestClock } from 'effect/testing';
|
|
540
|
-
import { createEffectActor, waitFor } from '@xstate/effect';
|
|
541
|
-
|
|
542
|
-
const test = Effect.gen(function* () {
|
|
543
|
-
const actor = yield* createEffectActor(machine);
|
|
544
|
-
yield* TestClock.adjust('30 seconds');
|
|
545
|
-
yield* waitFor(actor, (s) => s.matches('timedOut'));
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
await Effect.runPromise(
|
|
549
|
-
test.pipe(Effect.scoped, Effect.provide(TestClock.layer()))
|
|
550
|
-
);
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
Assert with `waitFor` for a state the actor should reach, and with `join` for the actor's final output. Both fail rather than hang when the actor stops first, and `waitFor`'s `timeout` option bounds a test that would otherwise wait forever.
|
|
554
|
-
|
|
555
|
-
Observe with `inspect` for every inspection event and `deadLetters` for events the system could not deliver. A test that ends with no dead letters confirms that every event it sent was accepted.
|
|
229
|
+
<!-- effect action contract from src/setupEffect.ts; spawn guard from src/internal.ts -->
|
|
556
230
|
|
|
557
|
-
|
|
231
|
+
- Register background Effect actions in `setupEffect({ actions })`; enqueue them with explicit arguments.
|
|
232
|
+
- Invoke `fromEffect` logic when the machine needs to wait for a result.
|
|
233
|
+
- Register spawned Effect logic in `actors`, then use `enq.spawn(args.actors.worker)`.
|
|
558
234
|
|
|
559
|
-
|
|
235
|
+
See [schemas and actions](docs/schemas-and-actions.md) for a complete audit example, runtime schema constraints and inline-source pitfalls.
|
|
560
236
|
|
|
561
|
-
|
|
237
|
+
## Atoms and React
|
|
562
238
|
|
|
563
|
-
|
|
564
|
-
import { Effect, Schedule } from 'effect';
|
|
565
|
-
import { createEffectActor, join } from '@xstate/effect';
|
|
239
|
+
<!-- atom surface from src/atom.ts -->
|
|
566
240
|
|
|
567
|
-
|
|
568
|
-
const actor = yield* createEffectActor(machine);
|
|
569
|
-
return yield* join(actor);
|
|
570
|
-
});
|
|
241
|
+
`createActorAtoms` from `@xstate/effect/atom` exposes `actor`, `snapshot`, `result`, `send`, `select` and `state`. Its `Atom.runtime` must provide the logic's services. Pass `{ input }` when the logic requires input; missing or mismatched input is a type error.
|
|
571
242
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
});
|
|
576
|
-
```
|
|
243
|
+
- Read atoms with `@effect/atom-react` in React, or an `AtomRegistry` elsewhere.
|
|
244
|
+
- The registry owns their lifetime; dispose it when its owner shuts down.
|
|
245
|
+
- `useSelector` from `@xstate/react` can also read an existing `EffectActor` directly.
|
|
577
246
|
|
|
578
|
-
|
|
247
|
+
See [atoms and React](docs/atoms-and-react.md) for complete examples with imports, runtime setup and cleanup.
|
|
579
248
|
|
|
580
|
-
##
|
|
249
|
+
## Testing and errors
|
|
581
250
|
|
|
582
251
|
<!-- error types from src/errors.ts; timeout failure from src/actor.ts -->
|
|
583
252
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
Both package errors are `Data.TaggedError` classes, so `Effect.catchTag('ActorStoppedError', …)` matches them.
|
|
591
|
-
|
|
592
|
-
The actor's own failures are not in this table. A `fromEffect` actor reports the Effect's `E` value as `snapshot.error`, typed through `ErrorFrom`, which this package re-exports from `xstate`.
|
|
253
|
+
- Use `TestClock` to drive deadlines without waiting for real time.
|
|
254
|
+
- `ActorStoppedError` reports a stop before an awaited result.
|
|
255
|
+
- `EffectInterruptedError` reports self-interruption inside Effect logic.
|
|
256
|
+
- Timed waits fail with Effect's `Cause.TimeoutError`.
|
|
257
|
+
- `join` preserves a task's typed failure. Unexpected machine errors have type `unknown`.
|
|
593
258
|
|
|
594
|
-
|
|
259
|
+
See [testing and errors](docs/testing-and-errors.md) for executable deadline, retry and failure examples. Persisted snapshots record actor state; restoration into a new Effect interpreter is not supported yet.
|
|
595
260
|
|
|
596
261
|
## Tracing
|
|
597
262
|
|
|
598
263
|
<!-- span names and attributes from src/internal.ts and src/fromEffect.ts -->
|
|
599
264
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
| Span | Covers |
|
|
603
|
-
| ----------------------- | ---------------------------------------- |
|
|
604
|
-
| `fromEffect` | a `fromEffect` actor's Effect |
|
|
605
|
-
| `fromEffectStream` | a `fromEffectStream` actor's stream |
|
|
606
|
-
| `fromEffectEventStream` | a `fromEffectEventStream` actor's stream |
|
|
607
|
-
| `action.<name>` | an Effect action registered as `<name>` |
|
|
608
|
-
|
|
609
|
-
Each span carries the attributes `xstate.actor.id` and `xstate.actor.address`. `id` is the actor's own name. `address` is its `/`-joined path of ids from the root actor, which is stable across persistence and restore, so it identifies the same logical actor across runs.
|
|
610
|
-
|
|
611
|
-
Spans are recorded only when a `Tracer` is provided. Without one they cost nothing and export nothing.
|
|
265
|
+
Hosted work uses spans named `fromEffect`, `fromEffectStream`, `fromEffectEventStream` and `action.<name>`. Each carries `xstate.actor.id` and `xstate.actor.address`. Provide a `Tracer` to record and export them.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Cause, Duration, Effect, Stream } from 'effect';
|
|
2
|
-
import type { Actor, ActorRef, AnyActorRef, AnyEventObject,
|
|
2
|
+
import type { Actor, ActorRef, AnyActorRef, AnyEventObject, EmittedFrom, EventRejection, ErrorFrom, InspectionEvent, OutputFrom, SnapshotFrom } from 'xstate';
|
|
3
3
|
import type { EffectActor } from "./effectActor.js";
|
|
4
4
|
import { ActorStoppedError } from "./errors.js";
|
|
5
5
|
/** The event type accepted by an actor's `send` method. */
|
|
@@ -68,9 +68,10 @@ export declare function join<TActor extends AnyActorRef>(actor: TActor): Effect.
|
|
|
68
68
|
*/
|
|
69
69
|
export declare function inspect(actor: AnyActorRef): Stream.Stream<InspectionEvent>;
|
|
70
70
|
/**
|
|
71
|
-
* Streams the events the actor's system could not deliver
|
|
72
|
-
* stopped actor, invalid external
|
|
73
|
-
* outside their owner. A dead letter is
|
|
74
|
-
* until it is interrupted or its scope
|
|
71
|
+
* Streams the events the actor's system could not deliver, as reported by
|
|
72
|
+
* `system.onRejectedEvent`: sends to a stopped actor, invalid external
|
|
73
|
+
* events and internal events sent from outside their owner. A dead letter is
|
|
74
|
+
* not an actor error. The stream runs until it is interrupted or its scope
|
|
75
|
+
* closes.
|
|
75
76
|
*/
|
|
76
|
-
export declare function deadLetters(actor: AnyActorRef): Stream.Stream<
|
|
77
|
+
export declare function deadLetters(actor: AnyActorRef): Stream.Stream<EventRejection>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Context, Effect, Scope } from 'effect';
|
|
2
|
+
declare const ActorScope_base: Context.ServiceClass<ActorScope, "@xstate/effect/ActorScope", Scope.Scope>;
|
|
3
|
+
/** The owning Effect actor's scope, supplied by `createEffectActor`. */
|
|
4
|
+
export declare class ActorScope extends ActorScope_base {
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Acquires resources in the owning actor's scope. Their finalizers run when
|
|
8
|
+
* that actor stops, even if the invoking task finishes or is cancelled first.
|
|
9
|
+
*/
|
|
10
|
+
export declare function withActorScope<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, Exclude<R, Scope.Scope> | ActorScope>;
|
|
11
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncResult, Atom } from 'effect/unstable/reactivity';
|
|
2
2
|
import type { AnyActorLogic, ErrorFrom, EventFromLogic, SnapshotFrom } from 'xstate';
|
|
3
|
-
import { type
|
|
3
|
+
import { type EffectActorOptionsArgs } from "./createEffectActor.js";
|
|
4
4
|
import type { EffectActor } from "./effectActor.js";
|
|
5
5
|
import { NotReadyError } from "./errors.js";
|
|
6
6
|
import { type TaggedStateFrom } from "./state.js";
|
|
@@ -45,7 +45,8 @@ interface MissingRequirements<T> {
|
|
|
45
45
|
/**
|
|
46
46
|
* Creates atoms for an actor that runs in an `Atom.runtime`. The runtime's
|
|
47
47
|
* Layer must provide every service the logic requires; a missing service is
|
|
48
|
-
* a type error on the `runtime` argument.
|
|
48
|
+
* a type error on the `runtime` argument. Logic with required input also
|
|
49
|
+
* requires an options object containing that input.
|
|
49
50
|
*
|
|
50
51
|
* The actor is created with `createEffectActor` inside the `actor` atom's
|
|
51
52
|
* scope, so its lifetime follows the atom: it starts on first read and stops
|
|
@@ -53,4 +54,4 @@ interface MissingRequirements<T> {
|
|
|
53
54
|
* actor for the registry's lifetime, or with `Atom.family` to create one
|
|
54
55
|
* actor per input.
|
|
55
56
|
*/
|
|
56
|
-
export declare function createActorAtoms<TLogic extends AnyActorLogic, R, ER = never>(runtime: Atom.AtomRuntime<R, ER> & ([RequirementsFrom<TLogic>] extends [R] ? unknown : MissingRequirements<Exclude<RequirementsFrom<TLogic>, R>>), logic: TLogic, options
|
|
57
|
+
export declare function createActorAtoms<TLogic extends AnyActorLogic, R, ER = never>(runtime: Atom.AtomRuntime<R, ER> & ([RequirementsFrom<TLogic>] extends [R] ? unknown : MissingRequirements<Exclude<RequirementsFrom<TLogic>, R>>), logic: TLogic, ...[options]: EffectActorOptionsArgs<TLogic>): ActorAtoms<TLogic, ER>;
|
|
@@ -3,15 +3,12 @@ import { type AnyActorLogic, type InputFrom } from 'xstate';
|
|
|
3
3
|
import { EffectActor } from "./effectActor.js";
|
|
4
4
|
import type { RequirementsFrom } from "./types.js";
|
|
5
5
|
/** Options for {@link createEffectActor}. */
|
|
6
|
-
export
|
|
7
|
-
/** The actor's input. */
|
|
6
|
+
export type EffectActorOptions<TLogic extends AnyActorLogic> = undefined extends InputFrom<TLogic> ? {
|
|
8
7
|
readonly input?: InputFrom<TLogic>;
|
|
9
|
-
}
|
|
10
|
-
type RequiredInput<TLogic extends AnyActorLogic> = undefined extends InputFrom<TLogic> ? {
|
|
11
|
-
input?: InputFrom<TLogic>;
|
|
12
8
|
} : {
|
|
13
|
-
input: InputFrom<TLogic>;
|
|
9
|
+
readonly input: InputFrom<TLogic>;
|
|
14
10
|
};
|
|
11
|
+
export type EffectActorOptionsArgs<TLogic extends AnyActorLogic> = undefined extends InputFrom<TLogic> ? [options?: EffectActorOptions<TLogic>] : [options: EffectActorOptions<TLogic>];
|
|
15
12
|
/**
|
|
16
13
|
* Creates and starts an actor as an Effect interpreter over pure transitions.
|
|
17
14
|
*
|
|
@@ -26,5 +23,4 @@ type RequiredInput<TLogic extends AnyActorLogic> = undefined extends InputFrom<T
|
|
|
26
23
|
* interrupted, when the enclosing `Scope` closes. The returned Effect never
|
|
27
24
|
* fails; the actor's own outcome is its snapshot status, read with `join`.
|
|
28
25
|
*/
|
|
29
|
-
export declare function createEffectActor<TLogic extends AnyActorLogic>(logic: TLogic, ...[options]:
|
|
30
|
-
export {};
|
|
26
|
+
export declare function createEffectActor<TLogic extends AnyActorLogic>(logic: TLogic, ...[options]: EffectActorOptionsArgs<TLogic>): Effect.Effect<EffectActor<TLogic>, never, RequirementsFrom<TLogic> | Scope.Scope>;
|
|
@@ -48,7 +48,7 @@ export declare class EffectActor<TLogic extends AnyActorLogic> implements ActorR
|
|
|
48
48
|
})) => void): Subscription;
|
|
49
49
|
/**
|
|
50
50
|
* Observes the inspection events of this actor and its children: every
|
|
51
|
-
*
|
|
51
|
+
* actor creation and transition of the execution.
|
|
52
52
|
*/
|
|
53
53
|
inspect(observer: Observer<InspectionEvent> | ((event: InspectionEvent) => void)): Subscription;
|
|
54
54
|
[symbolObservable](): this;
|
|
@@ -101,6 +101,9 @@ export type EffectStreamActorLogic<TItem, TError, TInput, TRequirements> = Actor
|
|
|
101
101
|
* an Effect, a function of {@link EffectSourceArgs} returning one, or a config
|
|
102
102
|
* object with `id`, `schemas`, `validator` and `effect`. The logic must be run
|
|
103
103
|
* under `createEffectActor`, which provides the Effect services it declares.
|
|
104
|
+
* Resources belong to this task's scope and close on completion, failure or
|
|
105
|
+
* interruption. Use `withActorScope` to retain a resource until the owning
|
|
106
|
+
* Effect actor stops.
|
|
104
107
|
*/
|
|
105
108
|
export declare function fromEffect<const TInputSchema extends EffectSchemaLike, const TOutputSchema extends EffectSchemaLike, TError = unknown, TRequirements = never>(config: Omit<EffectSourceConfig<SchemaOutput<TOutputSchema>, TError, SchemaOutput<TInputSchema>, TRequirements>, 'schemas' | 'validator'> & EffectSchemaValidationConfig<{
|
|
106
109
|
input: TInputSchema;
|
|
@@ -147,7 +150,8 @@ type StreamRequirements<T> = T extends Stream.Stream<any, any, infer R> ? R : ne
|
|
|
147
150
|
* the actor's `context`. The actor reaches `done` when the stream completes
|
|
148
151
|
* and `error` when it fails. Accepts a Stream, a function of
|
|
149
152
|
* {@link EffectSourceArgs} returning one, or a config object with `id`,
|
|
150
|
-
* `schemas`, `validator` and `stream`.
|
|
153
|
+
* `schemas`, `validator` and `stream`. Its resources close when the stream
|
|
154
|
+
* completes, fails or is interrupted.
|
|
151
155
|
*/
|
|
152
156
|
export declare function fromEffectStream<const TInputSchema extends EffectSchemaLike, TStream extends Stream.Stream<any, any, any>>(config: Omit<EffectStreamConfig<StreamItem<TStream>, StreamError<TStream>, SchemaOutput<TInputSchema>, StreamRequirements<TStream>>, 'stream' | 'schemas' | 'validator'> & EffectSchemaValidationConfig<{
|
|
153
157
|
input: TInputSchema;
|
|
@@ -162,7 +166,8 @@ export declare function fromEffectStream<TItem, TError = unknown, TInput = undef
|
|
|
162
166
|
* Creates actor logic that runs a Stream of events and relays each item to the
|
|
163
167
|
* parent machine as an event, the way `fromEventObservable` does. The actor
|
|
164
168
|
* has no output: it reaches `done` when the stream completes and `error` when
|
|
165
|
-
* it fails. Accepts the same forms as `fromEffectStream`.
|
|
169
|
+
* it fails. Accepts the same forms as `fromEffectStream`. Its resources close
|
|
170
|
+
* when the stream completes, fails or is interrupted.
|
|
166
171
|
*/
|
|
167
172
|
export declare function fromEffectEventStream<const TInputSchema extends EffectSchemaLike, TStream extends Stream.Stream<EventObject, any, any>>(config: Omit<EffectStreamConfig<StreamItem<TStream>, StreamError<TStream>, SchemaOutput<TInputSchema>, StreamRequirements<TStream>>, 'stream' | 'schemas' | 'validator'> & EffectSchemaValidationConfig<{
|
|
168
173
|
input: TInputSchema;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { fromEffect, fromEffectEventStream, fromEffectStream, type EffectActorLogic, type EffectLogicBrand, type EffectSnapshot, type EffectSource, type EffectSourceArgs, type EffectStreamSource, type EffectStreamActorLogic, type EffectStreamSnapshot } from "./fromEffect.js";
|
|
2
2
|
export { createEffectActor, type EffectActorOptions } from "./createEffectActor.js";
|
|
3
3
|
export { EffectActor } from "./effectActor.js";
|
|
4
|
+
export { ActorScope, withActorScope } from "./actorScope.js";
|
|
4
5
|
export { deadLetters, emitted, inspect, join, send, snapshots, waitFor, type EmittedEventFrom, type SendableEventFrom, type WaitForOptions } from "./actor.js";
|
|
5
6
|
export { ActorStoppedError, EffectInterruptedError } from "./errors.js";
|
|
6
7
|
export { setupEffect, type EffectAction, type EffectActionArgs, type EffectSetupReturn } from "./setupEffect.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { Effect } from 'effect';
|
|
1
|
+
import type { Effect, Scope } from 'effect';
|
|
2
2
|
import type { EffectLogicBrand } from "./fromEffect.js";
|
|
3
3
|
import { effectActionBrand } from "./brands.js";
|
|
4
|
+
import type { ActorScope } from "./actorScope.js";
|
|
4
5
|
/** The `R` channel of an Effect type, or `never` for anything else. */
|
|
5
6
|
export type EffectRequirements<T> = T extends Effect.Effect<any, any, infer R> ? R : never;
|
|
6
7
|
type RequirementsFromActions<T> = T extends Record<string, (...args: any[]) => any> ? T[keyof T] extends infer TAction ? TAction extends {
|
|
@@ -38,15 +39,16 @@ type RequirementsFromStateSchema<TSchema, TDepth extends Depth> = (TSchema exten
|
|
|
38
39
|
/**
|
|
39
40
|
* Collects the Effect service requirements of an actor logic.
|
|
40
41
|
*
|
|
41
|
-
*
|
|
42
|
+
* Actor and invocation scopes are supplied by the host and excluded here.
|
|
43
|
+
* For Effect logic this is the remaining `R`. For a state machine it is the
|
|
42
44
|
* union of the requirements of its registered Effect actions, its registered
|
|
43
45
|
* Effect actors, the Effect logic passed inline as `invoke.src` anywhere in
|
|
44
46
|
* its state tree, and — recursively — the requirements of any child machine
|
|
45
47
|
* reached either way (capped at {@link MaxDepth} levels of machine nesting).
|
|
46
48
|
*/
|
|
47
|
-
export type RequirementsFrom<T, TDepth extends Depth = MaxDepth> = T extends EffectLogicBrand<any, infer R> ? R : T extends {
|
|
49
|
+
export type RequirementsFrom<T, TDepth extends Depth = MaxDepth> = T extends EffectLogicBrand<any, infer R> ? Exclude<R, ActorScope | Scope.Scope> : T extends {
|
|
48
50
|
readonly _actionMap: infer TActionMap;
|
|
49
51
|
readonly _actorMap: infer TActorMap;
|
|
50
52
|
readonly _stateSchema: infer TStateSchema;
|
|
51
|
-
} ? RequirementsFromActions<TActionMap> | ([TDepth] extends [0] ? never : RequirementsFromActors<TActorMap, PrevDepth[TDepth]> | RequirementsFromStateSchema<TStateSchema, PrevDepth[TDepth]>) : never;
|
|
53
|
+
} ? Exclude<RequirementsFromActions<TActionMap>, ActorScope | Scope.Scope> | ([TDepth] extends [0] ? never : RequirementsFromActors<TActorMap, PrevDepth[TDepth]> | RequirementsFromStateSchema<TStateSchema, PrevDepth[TDepth]>) : never;
|
|
52
54
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Queue, Effect, Scope, Exit, Cause, Context,
|
|
1
|
+
import { Queue, Effect, Fiber, Scope, Exit, Cause, Context, Duration, Data } from 'effect';
|
|
2
2
|
import { stopActor, terminateActor, deliverEvent } from 'xstate';
|
|
3
3
|
import { createDurable } from 'xstate/durable';
|
|
4
4
|
|
|
@@ -130,7 +130,7 @@ class EffectActor {
|
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
132
|
* Observes the inspection events of this actor and its children: every
|
|
133
|
-
*
|
|
133
|
+
* actor creation and transition of the execution.
|
|
134
134
|
*/
|
|
135
135
|
inspect(observer) {
|
|
136
136
|
const handler = typeof observer === 'function' ? observer : event => observer.next?.(event);
|
|
@@ -216,7 +216,8 @@ function createEffectHost(context, scope) {
|
|
|
216
216
|
context,
|
|
217
217
|
scope,
|
|
218
218
|
interruptors: new Map(),
|
|
219
|
-
subscriptions: new Map()
|
|
219
|
+
subscriptions: new Map(),
|
|
220
|
+
fibers: new Set()
|
|
220
221
|
};
|
|
221
222
|
}
|
|
222
223
|
function bindEffectHost(target, host) {
|
|
@@ -311,9 +312,8 @@ function trackEffect(actor, host, interrupt) {
|
|
|
311
312
|
* the actor stops or when the returned function is called; `onExit` is not
|
|
312
313
|
* called after that.
|
|
313
314
|
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* interruptor kept per actor.
|
|
315
|
+
* The fiber belongs to the host scope, so closing the owning actor also
|
|
316
|
+
* waits for asynchronous invocation finalizers.
|
|
317
317
|
*/
|
|
318
318
|
function startHostedEffect(actor, effect, spanName, onExit) {
|
|
319
319
|
const host = requireEffectHost(actor);
|
|
@@ -334,15 +334,17 @@ function startHostedEffect(actor, effect, spanName, onExit) {
|
|
|
334
334
|
untrackEffect(actor, host, cancel);
|
|
335
335
|
interrupt();
|
|
336
336
|
};
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
onExit(exit);
|
|
337
|
+
const fiber = Fiber.runIn(Effect.runForkWith(host.context)(traced), host.scope);
|
|
338
|
+
const interrupt = () => fiber.interruptUnsafe();
|
|
339
|
+
host.fibers.add(fiber);
|
|
340
|
+
fiber.addObserver(exit => {
|
|
341
|
+
host.fibers.delete(fiber);
|
|
342
|
+
if (!active) {
|
|
343
|
+
return;
|
|
345
344
|
}
|
|
345
|
+
active = false;
|
|
346
|
+
untrackEffect(actor, host, cancel);
|
|
347
|
+
onExit(exit);
|
|
346
348
|
});
|
|
347
349
|
if (active) {
|
|
348
350
|
trackEffect(actor, host, cancel);
|
|
@@ -370,10 +372,13 @@ function runHostedEffect(actor, effect, spanName) {
|
|
|
370
372
|
}
|
|
371
373
|
|
|
372
374
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
+
* Finishes hosted task cleanup before closing the owning actor's resources.
|
|
376
|
+
* `createEffectActor`'s release awaits this fiber before its parent scope closes.
|
|
375
377
|
*/
|
|
376
378
|
function closeEffectHost(host) {
|
|
379
|
+
if (host.closing) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
377
382
|
for (const [actor, interruptors] of host.interruptors) {
|
|
378
383
|
host.interruptors.delete(actor);
|
|
379
384
|
for (const interrupt of interruptors) {
|
|
@@ -382,10 +387,10 @@ function closeEffectHost(host) {
|
|
|
382
387
|
host.subscriptions.get(actor)?.unsubscribe();
|
|
383
388
|
host.subscriptions.delete(actor);
|
|
384
389
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
390
|
+
host.closing = Effect.runForkWith(host.context)(Effect.forEach([...host.fibers], fiber => Fiber.interrupt(fiber), {
|
|
391
|
+
concurrency: 'unbounded',
|
|
392
|
+
discard: true
|
|
393
|
+
}).pipe(Effect.andThen(Scope.close(host.scope, Exit.void))));
|
|
389
394
|
}
|
|
390
395
|
|
|
391
396
|
/**
|
|
@@ -399,6 +404,17 @@ function relayToParent(actor, event) {
|
|
|
399
404
|
}
|
|
400
405
|
}
|
|
401
406
|
|
|
407
|
+
/** The owning Effect actor's scope, supplied by `createEffectActor`. */
|
|
408
|
+
class ActorScope extends Context.Service()('@xstate/effect/ActorScope') {}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Acquires resources in the owning actor's scope. Their finalizers run when
|
|
412
|
+
* that actor stops, even if the invoking task finishes or is cancelled first.
|
|
413
|
+
*/
|
|
414
|
+
function withActorScope(effect) {
|
|
415
|
+
return ActorScope.use(scope => Scope.provide(effect, scope));
|
|
416
|
+
}
|
|
417
|
+
|
|
402
418
|
const XSTATE_TIMER = 'xstate.timer';
|
|
403
419
|
|
|
404
420
|
/** Options for {@link createEffectActor}. */
|
|
@@ -422,7 +438,7 @@ function createEffectActor(logic, ...[options]) {
|
|
|
422
438
|
const parentScope = yield* Effect.scope;
|
|
423
439
|
const actorScope = yield* Scope.fork(parentScope);
|
|
424
440
|
const baseContext = yield* Effect.context();
|
|
425
|
-
const context = Context.add(baseContext, Scope.Scope, actorScope);
|
|
441
|
+
const context = Context.add(Context.add(baseContext, Scope.Scope, actorScope), ActorScope, actorScope);
|
|
426
442
|
const host = createEffectHost(context, actorScope);
|
|
427
443
|
const runFork = Effect.runForkWith(context);
|
|
428
444
|
const runPromise = Effect.runPromiseWith(context);
|
|
@@ -730,4 +746,4 @@ function taggedState(snapshot) {
|
|
|
730
746
|
};
|
|
731
747
|
}
|
|
732
748
|
|
|
733
|
-
export { ActorStoppedError as A, EffectInterruptedError as E, NotReadyError as N, runHostedEffect as a, EffectActor as b, createEffectActor as c, relayToParent as r, startHostedEffect as s, taggedState as t };
|
|
749
|
+
export { ActorStoppedError as A, EffectInterruptedError as E, NotReadyError as N, runHostedEffect as a, EffectActor as b, createEffectActor as c, ActorScope as d, relayToParent as r, startHostedEffect as s, taggedState as t, withActorScope as w };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Cause } from 'effect';
|
|
2
2
|
import { Atom, AsyncResult } from 'effect/unstable/reactivity';
|
|
3
|
-
import { c as createEffectActor, N as NotReadyError, t as taggedState } from './state-
|
|
4
|
-
export { N as NotReadyError } from './state-
|
|
3
|
+
import { c as createEffectActor, N as NotReadyError, t as taggedState } from './state-14a1fecf.js';
|
|
4
|
+
export { N as NotReadyError } from './state-14a1fecf.js';
|
|
5
5
|
import 'xstate';
|
|
6
6
|
import 'xstate/durable';
|
|
7
7
|
|
|
@@ -14,7 +14,8 @@ import 'xstate/durable';
|
|
|
14
14
|
/**
|
|
15
15
|
* Creates atoms for an actor that runs in an `Atom.runtime`. The runtime's
|
|
16
16
|
* Layer must provide every service the logic requires; a missing service is
|
|
17
|
-
* a type error on the `runtime` argument.
|
|
17
|
+
* a type error on the `runtime` argument. Logic with required input also
|
|
18
|
+
* requires an options object containing that input.
|
|
18
19
|
*
|
|
19
20
|
* The actor is created with `createEffectActor` inside the `actor` atom's
|
|
20
21
|
* scope, so its lifetime follows the atom: it starts on first read and stops
|
|
@@ -22,7 +23,7 @@ import 'xstate/durable';
|
|
|
22
23
|
* actor for the registry's lifetime, or with `Atom.family` to create one
|
|
23
24
|
* actor per input.
|
|
24
25
|
*/
|
|
25
|
-
function createActorAtoms(runtime, logic, options) {
|
|
26
|
+
function createActorAtoms(runtime, logic, ...[options]) {
|
|
26
27
|
const host = runtime;
|
|
27
28
|
const actor = host.atom(createEffectActor(logic, options));
|
|
28
29
|
const snapshot = Atom.make(get => {
|
package/dist/xstate-effect.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Schema, Effect, Stream, Exit, Cause, Queue } from 'effect';
|
|
2
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-
|
|
4
|
-
export { A as ActorStoppedError, b as EffectActor, E as EffectInterruptedError, c as createEffectActor, t as taggedState } from './state-
|
|
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';
|
|
5
5
|
import { dual } from 'effect/Function';
|
|
6
6
|
import 'xstate/durable';
|
|
7
7
|
|
|
@@ -169,6 +169,9 @@ function brandLogic(logic, error, requirements) {
|
|
|
169
169
|
* an Effect, a function of {@link EffectSourceArgs} returning one, or a config
|
|
170
170
|
* object with `id`, `schemas`, `validator` and `effect`. The logic must be run
|
|
171
171
|
* under `createEffectActor`, which provides the Effect services it declares.
|
|
172
|
+
* Resources belong to this task's scope and close on completion, failure or
|
|
173
|
+
* interruption. Use `withActorScope` to retain a resource until the owning
|
|
174
|
+
* Effect actor stops.
|
|
172
175
|
*/
|
|
173
176
|
|
|
174
177
|
function fromEffect(sourceOrConfig) {
|
|
@@ -213,7 +216,7 @@ function fromEffect(sourceOrConfig) {
|
|
|
213
216
|
return;
|
|
214
217
|
}
|
|
215
218
|
const effect = typeof source === 'function' ? source(createSourceArgs(input, self, system)) : source;
|
|
216
|
-
enq.effect(() => startHostedEffect(self, effect, 'fromEffect', exit => {
|
|
219
|
+
enq.effect(() => startHostedEffect(self, Effect.scoped(effect), 'fromEffect', exit => {
|
|
217
220
|
if (self.getSnapshot().status !== 'active') {
|
|
218
221
|
return;
|
|
219
222
|
}
|
|
@@ -256,7 +259,8 @@ function toLogicSchemas(schemas) {
|
|
|
256
259
|
* the actor's `context`. The actor reaches `done` when the stream completes
|
|
257
260
|
* and `error` when it fails. Accepts a Stream, a function of
|
|
258
261
|
* {@link EffectSourceArgs} returning one, or a config object with `id`,
|
|
259
|
-
* `schemas`, `validator` and `stream`.
|
|
262
|
+
* `schemas`, `validator` and `stream`. Its resources close when the stream
|
|
263
|
+
* completes, fails or is interrupted.
|
|
260
264
|
*/
|
|
261
265
|
|
|
262
266
|
function fromEffectStream(sourceOrConfig) {
|
|
@@ -303,7 +307,7 @@ function fromEffectStream(sourceOrConfig) {
|
|
|
303
307
|
});
|
|
304
308
|
}
|
|
305
309
|
}));
|
|
306
|
-
enq.effect(() => startHostedEffect(self, consume, 'fromEffectStream', exit => {
|
|
310
|
+
enq.effect(() => startHostedEffect(self, Effect.scoped(consume), 'fromEffectStream', exit => {
|
|
307
311
|
if (self.getSnapshot().status !== 'active') {
|
|
308
312
|
return;
|
|
309
313
|
}
|
|
@@ -327,7 +331,8 @@ function fromEffectStream(sourceOrConfig) {
|
|
|
327
331
|
* Creates actor logic that runs a Stream of events and relays each item to the
|
|
328
332
|
* parent machine as an event, the way `fromEventObservable` does. The actor
|
|
329
333
|
* has no output: it reaches `done` when the stream completes and `error` when
|
|
330
|
-
* it fails. Accepts the same forms as `fromEffectStream`.
|
|
334
|
+
* it fails. Accepts the same forms as `fromEffectStream`. Its resources close
|
|
335
|
+
* when the stream completes, fails or is interrupted.
|
|
331
336
|
*/
|
|
332
337
|
|
|
333
338
|
function fromEffectEventStream(sourceOrConfig) {
|
|
@@ -362,7 +367,7 @@ function fromEffectEventStream(sourceOrConfig) {
|
|
|
362
367
|
}
|
|
363
368
|
const streamValue = typeof stream === 'function' ? stream(createSourceArgs(input, self, system)) : stream;
|
|
364
369
|
const consume = Stream.runForEach(streamValue, value => Effect.sync(() => relayToParent(self, value)));
|
|
365
|
-
enq.effect(() => startHostedEffect(self, consume, 'fromEffectEventStream', exit => {
|
|
370
|
+
enq.effect(() => startHostedEffect(self, Effect.scoped(consume), 'fromEffectEventStream', exit => {
|
|
366
371
|
if (self.getSnapshot().status !== 'active') {
|
|
367
372
|
return;
|
|
368
373
|
}
|
|
@@ -608,13 +613,18 @@ function inspect(actor) {
|
|
|
608
613
|
}
|
|
609
614
|
|
|
610
615
|
/**
|
|
611
|
-
* Streams the events the actor's system could not deliver
|
|
612
|
-
* stopped actor, invalid external
|
|
613
|
-
* outside their owner. A dead letter is
|
|
614
|
-
* until it is interrupted or its scope
|
|
616
|
+
* Streams the events the actor's system could not deliver, as reported by
|
|
617
|
+
* `system.onRejectedEvent`: sends to a stopped actor, invalid external
|
|
618
|
+
* events and internal events sent from outside their owner. A dead letter is
|
|
619
|
+
* not an actor error. The stream runs until it is interrupted or its scope
|
|
620
|
+
* closes.
|
|
615
621
|
*/
|
|
616
622
|
function deadLetters(actor) {
|
|
617
|
-
return Stream.
|
|
623
|
+
return Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => actor.system.onRejectedEvent(rejection => {
|
|
624
|
+
Queue.offerUnsafe(queue, rejection);
|
|
625
|
+
})), subscription => Effect.sync(() => {
|
|
626
|
+
subscription.unsubscribe();
|
|
627
|
+
})));
|
|
618
628
|
}
|
|
619
629
|
|
|
620
630
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xstate/effect",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Effect integration for XState",
|
|
6
6
|
"keywords": [
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"effect": "^4.0.0-rc.115",
|
|
33
|
-
"xstate": "^6.0.0-alpha.
|
|
33
|
+
"xstate": "^6.0.0-alpha.62"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"effect": "4.0.0-rc.115",
|
|
37
|
-
"xstate": "^6.0.0-alpha.
|
|
37
|
+
"xstate": "^6.0.0-alpha.62"
|
|
38
38
|
},
|
|
39
39
|
"preconstruct": {
|
|
40
40
|
"entrypoints": [
|