@systemfsoftware/xstate-effect 0.0.0-bootstrap.0 → 0.1.0-beta.8

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/dist/index.mjs ADDED
@@ -0,0 +1,429 @@
1
+ import { a as startHostedEffect, c as withActorScope, d as EffectInterruptedError, i as runHostedEffect, l as ActorFailedError, n as createEffectActor, o as EffectActor, r as relayToParent, s as ActorScope, t as taggedState, u as ActorStoppedError } from "./state-CNrCEIpU.mjs";
2
+ import { Cause, Effect, Exit, Queue, Schema, Stream } from "effect";
3
+ import { dual } from "effect/Function";
4
+ import { createLogic, setup } from "@systemfsoftware/xstate";
5
+ // src/actor.ts
6
+ const noopSubscription = { unsubscribe: () => {} };
7
+ function actorId(actor) {
8
+ return actor.id ?? "(unknown)";
9
+ }
10
+ /**
11
+ * Marks an already-errored actor's error as observed, so XState does not
12
+ * report it as unhandled: the caller consumes it as a typed failure.
13
+ */
14
+ function observeError(actor) {
15
+ actor.subscribe({ error: () => {} }).unsubscribe();
16
+ }
17
+ function stoppedError(actor) {
18
+ return new ActorStoppedError({
19
+ actorId: actorId(actor),
20
+ snapshot: actor.getSnapshot()
21
+ });
22
+ }
23
+ function isActorRef(value) {
24
+ return typeof value === "object" && value !== null && typeof value.send === "function" && typeof value.getSnapshot === "function";
25
+ }
26
+ /**
27
+ * Sends an event to an actor. The returned Effect always succeeds: the event
28
+ * is enqueued, like `actor.send(event)`, and processed on the actor's own
29
+ * fiber. An event that cannot be delivered (for example to a stopped actor)
30
+ * is reported as a dead letter, observable with {@link deadLetters}.
31
+ */
32
+ const send = dual(2, (actor, event) => Effect.sync(() => {
33
+ actor.send(event);
34
+ }));
35
+ /**
36
+ * Streams an actor's snapshots, starting with the current one. The stream ends
37
+ * when the actor completes or stops, and emits the error snapshot before
38
+ * ending when the actor errors. Interrupting the stream unsubscribes from the
39
+ * actor.
40
+ */
41
+ function snapshots(actor) {
42
+ return Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => {
43
+ const current = actor.getSnapshot();
44
+ Queue.offerUnsafe(queue, current);
45
+ if (current.status !== "active") {
46
+ observeError(actor);
47
+ Queue.endUnsafe(queue);
48
+ return noopSubscription;
49
+ }
50
+ return actor.subscribe({
51
+ next: (snapshot) => {
52
+ Queue.offerUnsafe(queue, snapshot);
53
+ },
54
+ error: () => {
55
+ Queue.offerUnsafe(queue, actor.getSnapshot());
56
+ Queue.endUnsafe(queue);
57
+ },
58
+ complete: () => {
59
+ Queue.endUnsafe(queue);
60
+ }
61
+ });
62
+ }), (subscription) => Effect.sync(() => {
63
+ subscription.unsubscribe();
64
+ })));
65
+ }
66
+ /**
67
+ * Streams every event an actor emits, as delivered to `actor.on('*', …)`. The
68
+ * stream ends when the actor completes, errors or stops. Interrupting the
69
+ * stream removes the listener.
70
+ */
71
+ function emitted(actor) {
72
+ return Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => {
73
+ const listener = actor.on("*", (event) => {
74
+ Queue.offerUnsafe(queue, event);
75
+ });
76
+ if (actor.getSnapshot().status !== "active") {
77
+ Queue.endUnsafe(queue);
78
+ return [listener, noopSubscription];
79
+ }
80
+ return [listener, actor.subscribe({
81
+ error: () => {
82
+ Queue.endUnsafe(queue);
83
+ },
84
+ complete: () => {
85
+ Queue.endUnsafe(queue);
86
+ }
87
+ })];
88
+ }), ([listener, subscription]) => Effect.sync(() => {
89
+ listener.unsubscribe();
90
+ subscription.unsubscribe();
91
+ })));
92
+ }
93
+ /**
94
+ * Waits for the first actor snapshot that satisfies `predicate`, succeeding
95
+ * immediately when the current snapshot already does. Fails with
96
+ * `ActorStoppedError` if the actor stops or errors first, and with
97
+ * `Cause.TimeoutError` when `options.timeout` elapses. Interrupting the Effect
98
+ * unsubscribes from the actor.
99
+ */
100
+ const waitFor = dual((args) => isActorRef(args[0]), (actor, predicate, options) => {
101
+ const waiting = Effect.callback((resume) => {
102
+ const current = actor.getSnapshot();
103
+ if (predicate(current)) {
104
+ resume(Effect.succeed(current));
105
+ return;
106
+ }
107
+ if (current.status !== "active") {
108
+ observeError(actor);
109
+ resume(Effect.fail(stoppedError(actor)));
110
+ return;
111
+ }
112
+ let settled = false;
113
+ let subscription;
114
+ const dispose = () => {
115
+ settled = true;
116
+ subscription?.unsubscribe();
117
+ };
118
+ subscription = actor.subscribe({
119
+ next: (snapshot) => {
120
+ if (settled || !predicate(snapshot)) return;
121
+ dispose();
122
+ resume(Effect.succeed(snapshot));
123
+ },
124
+ error: () => {
125
+ if (settled) return;
126
+ dispose();
127
+ resume(Effect.fail(stoppedError(actor)));
128
+ },
129
+ complete: () => {
130
+ if (settled) return;
131
+ dispose();
132
+ resume(Effect.fail(stoppedError(actor)));
133
+ }
134
+ });
135
+ if (settled) subscription.unsubscribe();
136
+ return Effect.sync(dispose);
137
+ });
138
+ return options === void 0 ? waiting : Effect.timeout(waiting, options.timeout);
139
+ });
140
+ /**
141
+ * Joins an actor's final result, like `Fiber.join`: succeeds with its `output`
142
+ * when it is done, fails with `ActorFailedError` carrying `snapshot.error` when
143
+ * it errors, and fails with `ActorStoppedError` when it stops without output.
144
+ * Waits for a still-active actor to settle.
145
+ */
146
+ function join(actor) {
147
+ return Effect.callback((resume) => {
148
+ const settle = () => {
149
+ const snapshot = actor.getSnapshot();
150
+ if (snapshot.status === "done") resume(Effect.succeed(snapshot.output));
151
+ else if (snapshot.status === "error") resume(Effect.fail(new ActorFailedError({ cause: snapshot.error })));
152
+ else resume(Effect.fail(stoppedError(actor)));
153
+ };
154
+ if (actor.getSnapshot().status !== "active") {
155
+ observeError(actor);
156
+ settle();
157
+ return;
158
+ }
159
+ let settled = false;
160
+ let subscription;
161
+ const dispose = () => {
162
+ settled = true;
163
+ subscription?.unsubscribe();
164
+ };
165
+ const onSettled = () => {
166
+ if (settled) return;
167
+ dispose();
168
+ settle();
169
+ };
170
+ subscription = actor.subscribe({
171
+ error: onSettled,
172
+ complete: onSettled
173
+ });
174
+ if (settled) subscription.unsubscribe();
175
+ return Effect.sync(dispose);
176
+ });
177
+ }
178
+ /**
179
+ * Streams the inspection events of the actor's system, as delivered to
180
+ * `system.inspect(…)`. The stream runs until it is interrupted or its scope
181
+ * closes.
182
+ */
183
+ function inspect(actor) {
184
+ return Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => {
185
+ const observer = (inspectionEvent) => {
186
+ Queue.offerUnsafe(queue, inspectionEvent);
187
+ };
188
+ const inspectable = actor;
189
+ return inspectable.inspect !== void 0 ? inspectable.inspect(observer) : actor.system.inspect(observer);
190
+ }), (subscription) => Effect.sync(() => {
191
+ subscription.unsubscribe();
192
+ })));
193
+ }
194
+ /**
195
+ * Streams the events the actor's system could not deliver, as reported by
196
+ * `system.onRejectedEvent`: sends to a stopped actor, invalid external
197
+ * events and internal events sent from outside their owner. A dead letter is
198
+ * not an actor error. The stream runs until it is interrupted or its scope
199
+ * closes.
200
+ */
201
+ function deadLetters(actor) {
202
+ return Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => actor.system.onRejectedEvent((rejection) => {
203
+ Queue.offerUnsafe(queue, rejection);
204
+ })), (subscription) => Effect.sync(() => {
205
+ subscription.unsubscribe();
206
+ })));
207
+ }
208
+ // src/schema.ts
209
+ function isStandardSchema(value) {
210
+ return value !== null && (typeof value === "object" || typeof value === "function") && "~standard" in value;
211
+ }
212
+ function toStandardSchema(schema) {
213
+ if (Schema.isSchema(schema)) return Schema.toStandardSchemaV1(schema);
214
+ return schema;
215
+ }
216
+ function mapSchemaRecord(value) {
217
+ return Object.fromEntries(Object.keys(value).map((key) => {
218
+ const entry = value[key];
219
+ if (Schema.isSchema(entry) || isStandardSchema(entry)) return [key, toStandardSchema(entry)];
220
+ if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) return [key, mapSchemaRecord(entry)];
221
+ return [key, entry];
222
+ }));
223
+ }
224
+ function toStandardSetupSchemas(schemas) {
225
+ return schemas !== void 0 ? mapSchemaRecord(schemas) : void 0;
226
+ }
227
+ function toStandardSetupStates(states) {
228
+ if (states === void 0) return;
229
+ return Object.fromEntries(Object.entries(states).map(([key, state]) => [key, {
230
+ ...state,
231
+ ...state.schemas !== void 0 ? { schemas: mapSchemaRecord(state.schemas) } : void 0,
232
+ ...state.states !== void 0 ? { states: toStandardSetupStates(state.states) } : void 0
233
+ }]));
234
+ }
235
+ // src/fromEffect.ts
236
+ const EFFECT_INIT = "@xstate.init";
237
+ const EFFECT_RESOLVE = "xstate.effect.resolve";
238
+ const EFFECT_REJECT = "xstate.effect.reject";
239
+ const EFFECT_NEXT = "xstate.effect.next";
240
+ const EFFECT_COMPLETE = "xstate.effect.complete";
241
+ const effectLogicBrand = Symbol.for("@xstate/effect/logic");
242
+ function toError(exit) {
243
+ if (Exit.isSuccess(exit)) return;
244
+ if (Cause.hasInterruptsOnly(exit.cause)) return new EffectInterruptedError({ cause: exit.cause });
245
+ return Cause.squash(exit.cause);
246
+ }
247
+ function createSourceArgs(input, self, system) {
248
+ return {
249
+ input,
250
+ self,
251
+ system,
252
+ emit: (event) => {
253
+ system.emitEvent(self, event);
254
+ }
255
+ };
256
+ }
257
+ function brandLogic(logic, error, requirements) {
258
+ Object.defineProperty(logic, effectLogicBrand, {
259
+ configurable: false,
260
+ enumerable: false,
261
+ value: {
262
+ error,
263
+ requirements
264
+ }
265
+ });
266
+ return logic;
267
+ }
268
+ function fromEffect(sourceOrConfig) {
269
+ const config = typeof sourceOrConfig === "function" || Effect.isEffect(sourceOrConfig) ? { effect: sourceOrConfig } : sourceOrConfig;
270
+ const source = config.effect;
271
+ const schemas = config.schemas !== void 0 ? {
272
+ ...config.schemas.input !== void 0 ? { input: toStandardSchema(config.schemas.input) } : {},
273
+ ...config.schemas.output !== void 0 ? { output: toStandardSchema(config.schemas.output) } : {}
274
+ } : void 0;
275
+ return brandLogic(createLogic({
276
+ id: config.id,
277
+ validator: config.validator,
278
+ schemas,
279
+ context: void 0,
280
+ run: ({ event, input, self, system }, enq) => {
281
+ if (event.type === EFFECT_RESOLVE) return {
282
+ status: "done",
283
+ output: event.output,
284
+ input: void 0
285
+ };
286
+ if (event.type === EFFECT_REJECT) return {
287
+ status: "error",
288
+ error: event.error,
289
+ input: void 0
290
+ };
291
+ if (event.type !== EFFECT_INIT) return;
292
+ const effect = typeof source === "function" ? source(createSourceArgs(input, self, system)) : source;
293
+ enq.effect(() => startHostedEffect(self, Effect.scoped(effect), "fromEffect", (exit) => {
294
+ if (self.getSnapshot().status !== "active") return;
295
+ if (Exit.isSuccess(exit)) self.send({
296
+ type: EFFECT_RESOLVE,
297
+ output: exit.value
298
+ });
299
+ else self.send({
300
+ type: EFFECT_REJECT,
301
+ error: toError(exit)
302
+ });
303
+ }));
304
+ }
305
+ }), void 0, void 0);
306
+ }
307
+ function resolveStreamConfig(sourceOrConfig) {
308
+ return typeof sourceOrConfig === "function" || Stream.isStream(sourceOrConfig) ? { stream: sourceOrConfig } : sourceOrConfig;
309
+ }
310
+ function toLogicSchemas(schemas) {
311
+ return schemas?.input !== void 0 ? { input: toStandardSchema(schemas.input) } : void 0;
312
+ }
313
+ function fromEffectStream(sourceOrConfig) {
314
+ const config = resolveStreamConfig(sourceOrConfig);
315
+ const stream = config.stream;
316
+ return brandLogic(createLogic({
317
+ id: config.id,
318
+ validator: config.validator,
319
+ schemas: toLogicSchemas(config.schemas),
320
+ context: void 0,
321
+ run: ({ event, input, self, system }, enq) => {
322
+ if (event.type === EFFECT_NEXT) return { context: event.value };
323
+ if (event.type === EFFECT_COMPLETE) return {
324
+ status: "done",
325
+ input: void 0
326
+ };
327
+ if (event.type === EFFECT_REJECT) return {
328
+ status: "error",
329
+ error: event.error,
330
+ input: void 0
331
+ };
332
+ if (event.type !== EFFECT_INIT) return;
333
+ const streamValue = typeof stream === "function" ? stream(createSourceArgs(input, self, system)) : stream;
334
+ const consume = Stream.runForEach(streamValue, (value) => Effect.sync(() => {
335
+ if (self.getSnapshot().status === "active") self.send({
336
+ type: EFFECT_NEXT,
337
+ value
338
+ });
339
+ }));
340
+ enq.effect(() => startHostedEffect(self, Effect.scoped(consume), "fromEffectStream", (exit) => {
341
+ if (self.getSnapshot().status !== "active") return;
342
+ if (Exit.isSuccess(exit)) self.send({ type: EFFECT_COMPLETE });
343
+ else self.send({
344
+ type: EFFECT_REJECT,
345
+ error: toError(exit)
346
+ });
347
+ }));
348
+ }
349
+ }), void 0, void 0);
350
+ }
351
+ function fromEffectEventStream(sourceOrConfig) {
352
+ const config = resolveStreamConfig(sourceOrConfig);
353
+ const stream = config.stream;
354
+ return brandLogic(createLogic({
355
+ id: config.id,
356
+ validator: config.validator,
357
+ schemas: toLogicSchemas(config.schemas),
358
+ context: void 0,
359
+ run: ({ event, input, self, system }, enq) => {
360
+ if (event.type === EFFECT_COMPLETE) return {
361
+ status: "done",
362
+ input: void 0
363
+ };
364
+ if (event.type === EFFECT_REJECT) return {
365
+ status: "error",
366
+ error: event.error,
367
+ input: void 0
368
+ };
369
+ if (event.type !== EFFECT_INIT) return;
370
+ const streamValue = typeof stream === "function" ? stream(createSourceArgs(input, self, system)) : stream;
371
+ const consume = Stream.runForEach(streamValue, (value) => Effect.sync(() => relayToParent(self, value)));
372
+ enq.effect(() => startHostedEffect(self, Effect.scoped(consume), "fromEffectEventStream", (exit) => {
373
+ if (self.getSnapshot().status !== "active") return;
374
+ if (Exit.isSuccess(exit)) self.send({ type: EFFECT_COMPLETE });
375
+ else self.send({
376
+ type: EFFECT_REJECT,
377
+ error: toError(exit)
378
+ });
379
+ }));
380
+ }
381
+ }), void 0, void 0);
382
+ }
383
+ // src/setupEffect.ts
384
+ function wrapActions(actions) {
385
+ if (actions === void 0) return;
386
+ const wrapped = {};
387
+ for (const [key, action] of Object.entries(actions)) {
388
+ const invoke = action;
389
+ wrapped[key] = (args) => {
390
+ const result = invoke(args);
391
+ const returned = result;
392
+ return Effect.isEffect(returned) ? runHostedEffect(args.self, result, `action.${key}`) : returned;
393
+ };
394
+ }
395
+ return wrapped;
396
+ }
397
+ /**
398
+ * Makes `machine.provide` host Effect-returning action overrides the same way
399
+ * `setupEffect` hosts declared actions.
400
+ */
401
+ function decorateMachine(machine) {
402
+ const provide = machine.provide.bind(machine);
403
+ machine.provide = ((sources) => decorateMachine(provide({
404
+ ...sources,
405
+ actions: wrapActions(sources["actions"])
406
+ })));
407
+ return machine;
408
+ }
409
+ function decorateEffectSetup(effectSetup) {
410
+ const createMachine = effectSetup.createMachine.bind(effectSetup);
411
+ effectSetup.createMachine = ((config) => decorateMachine(createMachine(config)));
412
+ const extendAny = effectSetup.extend;
413
+ effectSetup.extend = ((extension) => decorateEffectSetup(extendAny({
414
+ ...extension,
415
+ schemas: toStandardSetupSchemas(extension.schemas),
416
+ states: toStandardSetupStates(extension.states),
417
+ actions: wrapActions(extension.actions)
418
+ })));
419
+ return effectSetup;
420
+ }
421
+ function setupEffect(config = {}) {
422
+ return decorateEffectSetup(setup({
423
+ ...config,
424
+ schemas: toStandardSetupSchemas(config.schemas),
425
+ states: toStandardSetupStates(config.states),
426
+ actions: wrapActions(config.actions)
427
+ }));
428
+ }
429
+ export { ActorFailedError, ActorScope, ActorStoppedError, EffectActor, EffectInterruptedError, createEffectActor, deadLetters, emitted, fromEffect, fromEffectEventStream, fromEffectStream, inspect, join, send, setupEffect, snapshots, taggedState, waitFor, withActorScope };