• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Snowflyt / tinyeffect / 11771695233

11 Nov 2024 03:38AM UTC coverage: 96.686% (+0.2%) from 96.469%
11771695233

push

github

Snowflyt
🐳 chore: Bump version to `0.2.0`

159 of 174 branches covered (91.38%)

Branch coverage included in aggregate %.

512 of 520 relevant lines covered (98.46%)

515.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.5
/src/effected.ts
1
import { UnhandledEffectError } from "./errors";
3✔
2
import { Effect } from "./types";
3

4
import type { UnhandledEffect, Unresumable } from "./types";
5

6
/**
7
 * Create a function that returns an {@link Effected} instance (an effected program) which yields a
8
 * single {@link Effect} instance (algebraic effect) and returns the handled value. The returned
9
 * function can be utilized with {@link effected} to compose and integrate into a more complex
10
 * effected program.
11
 *
12
 * For special cases, see {@link error} (for non-resumable error effects) and {@link dependency}
13
 * (for dependency injection).
14
 * @param name Name of the effect. This identifier is used to match effects, so be careful with name
15
 * collisions.
16
 * @param options Options for the effect. Can be used to mark the effect as unresumable.
17
 * @returns
18
 *
19
 * @example
20
 * ```typescript
21
 * const println = effect("println")<unknown[], void>;
22
 * const raise = effect("raise", { resumable: false })<[message?: string], never>;
23
 * ```
24
 *
25
 * @example
26
 * ```typescript
27
 * // Provide more readable type information
28
 * type Println = Effect<"println", unknown[], void>;
29
 * const println: EffectFactory<Println> = effect("println");
30
 * type Raise = Effect<"raise", [message?: string], never>;
31
 * const raise: EffectFactory<Raise> = effect("raise", { resumable: false });
32
 * ```
33
 *
34
 * @see {@link effected}
35
 */
36
export function effect<Name extends string | symbol, Resumable extends boolean = true>(
3✔
37
  name: Name,
516✔
38
  options?: { readonly resumable?: Resumable },
516✔
39
): [Resumable] extends [false] ?
40
  <Payloads extends unknown[], R extends never = never>(
41
    ...payloads: Payloads
42
  ) => Effected<Unresumable<Effect<Name, Payloads, R>>, R>
43
: <Payloads extends unknown[], R>(...payloads: Payloads) => Effected<Effect<Name, Payloads, R>, R> {
516✔
44
  const result = (...payloads: unknown[]) =>
516✔
45
    effected(() => {
801✔
46
      let state = 0;
801✔
47
      return {
801✔
48
        next: (...args) => {
801✔
49
          switch (state) {
1,443✔
50
            case 0:
1,443✔
51
              state++;
801✔
52
              return {
801✔
53
                done: false,
801✔
54
                value:
801✔
55
                  options && options.resumable === false ?
801✔
56
                    Object.assign(new Effect(name, payloads), { resumable: false })
93✔
57
                  : new Effect(name, payloads),
708✔
58
              };
801✔
59
            case 1:
1,443✔
60
              state++;
630✔
61
              return {
630✔
62
                done: true,
630✔
63
                ...(args.length > 0 ? { value: args[0] } : {}),
630✔
64
              } as IteratorReturnResult<unknown>;
630✔
65
            default:
1,443✔
66
              return { done: true } as IteratorReturnResult<unknown>;
12✔
67
          }
1,443✔
68
        },
1,443✔
69
      };
801✔
70
    });
801✔
71
  if (options && (options as any)._overrideFunctionName === false) return result as never;
516✔
72
  return renameFunction(result, typeof name === "string" ? name : name.description || "") as never;
516✔
73
}
516✔
74

75
/**
76
 * Create a function that returns an {@link Effected} instance (an effected program) which yields a
77
 * single {@link Effect} instance for typical errors (i.e., non-resumable effect with name prefixed
78
 * with "error:").
79
 *
80
 * It can be seen as an alias of `effect("error:" + name, { resumable: false })`.
81
 *
82
 * You can use {@link Effected#catch} as a shortcut for `terminate("error:" + name, ...)` to catch
83
 * the error effect.
84
 * @param name Name of the error effect. This identifier is used to match effects, so be careful
85
 * with name collisions.
86
 * @returns
87
 *
88
 * @example
89
 * ```typescript
90
 * const authError = error("auth");
91
 * const notFoundError = error("notFound");
92
 * ```
93
 *
94
 * @example
95
 * ```typescript
96
 * // Provide more readable type information
97
 * type AuthError = Effect.Error<"auth">;
98
 * const authError: EffectFactory<AuthError> = error("auth");
99
 * ```
100
 *
101
 * @see {@link effect}
102
 */
103
export function error<Name extends string>(
3✔
104
  name: Name,
48✔
105
): (
106
  message?: string,
107
) => Effected<Unresumable<Effect<`error:${Name}`, [message?: string], never>>, never> {
48✔
108
  return renameFunction(
48✔
109
    effect(`error:${name}`, { resumable: false, _overrideFunctionName: false } as {
48✔
110
      resumable: false;
111
    }),
48✔
112
    `throw${capitalize(name)}Error`,
48✔
113
  );
48✔
114
}
48✔
115

116
type ErrorName<E extends Effect> =
117
  E extends Unresumable<Effect<`error:${infer Name}`, any, never>> ? Name : never;
118

119
/**
120
 * Create a function that returns an {@link Effected} instance (an effected program) which yields a
121
 * single {@link Effect} instance for dependency injection.
122
 *
123
 * It can be seen as an alias of `effect("dependency:" + name)`.
124
 *
125
 * You can use {@link Effected#provide} and its variants as a shortcut for
126
 * `resume("dependency:" + name, ...)` to provide the value for the dependency.
127
 * @param name Name of the dependency. This identifier is used to match effects, so be careful
128
 * with name collisions.
129
 * @returns
130
 *
131
 * @example
132
 * ```typescript
133
 * const askConfig = dependency("config")<Config | null>;
134
 * const askDatabase = dependency("database")<Database>;
135
 * ```
136
 *
137
 * @example
138
 * ```typescript
139
 * // Provide more readable type information
140
 * type ConfigDependency = Effect.Dependency<"config", Config | null>;
141
 * const askConfig: EffectFactory<ConfigDependency> = dependency("config");
142
 * ```
143
 *
144
 * @see {@link effect}
145
 */
146
export function dependency<Name extends string>(
3✔
147
  name: Name,
21✔
148
): <R>() => Effected<Effect<`dependency:${Name}`, [], R>, R> {
21✔
149
  return renameFunction(
21✔
150
    effect(`dependency:${name}`, { _overrideFunctionName: false } as {}),
21✔
151
    `ask${capitalize(name)}`,
21✔
152
  );
21✔
153
}
21✔
154

155
type DependencyName<E extends Effect> =
156
  E extends Effect<`dependency:${infer Name}`, [], unknown> ? Name : never;
157

158
/**
159
 * Define a handler that transforms an effected program into another one.
160
 *
161
 * It is just a simple wrapper to make TypeScript infer the types correctly, and simply returns the
162
 * function you pass to it.
163
 *
164
 * @example
165
 * ```typescript
166
 * type Raise = Unresumable<Effect<"raise", [error: unknown], never>>;
167
 * const raise: EffectFactory<Raise> = effect("raise", { resumable: false });
168
 *
169
 * const safeDivide = (a: number, b: number): Effected<Raise, number> =>
170
 *   effected(function* () {
171
 *     if (b === 0) return yield* raise("Division by zero");
172
 *     return a / b;
173
 *   });
174
 *
175
 * type Option<T> = { kind: "some"; value: T } | { kind: "none" };
176
 * const some = <T>(value: T): Option<T> => ({ kind: "some", value });
177
 * const none: Option<never> = { kind: "none" };
178
 *
179
 * const raiseOption = defineHandlerFor<Raise>().with((effected) =>
180
 *   effected.map((value) => some(value)).terminate("raise", () => none),
181
 * );
182
 *
183
 * const safeDivide2 = (a: number, b: number) => safeDivide(a, b).with(raiseOption);
184
 * //    ^?: (a: number, b: number) => Effected<never, Option<number>>
185
 * ```
186
 */
187
export function defineHandlerFor<E extends Effect, R>(): {
188
  with: <H extends (effected: EffectedDraft<E, E, R>) => EffectedDraft<E, Effect, unknown>>(
189
    handler: H,
190
  ) => H;
191
};
192
export function defineHandlerFor<E extends Effect>(): {
193
  with: <H extends <R>(effected: EffectedDraft<E, E, R>) => EffectedDraft<E, Effect, unknown>>(
194
    handler: H,
195
  ) => H;
196
};
197
export function defineHandlerFor() {
3✔
198
  return {
39✔
199
    with: (handler: any) => handler,
39✔
200
  };
39✔
201
}
39✔
202

203
/**
204
 * An effected program.
205
 */
206
export class Effected<out E extends Effect, out R> implements Iterable<E, R, unknown> {
3✔
207
  // @ts-expect-error - TS mistakenly think `[Symbol.iterator]` is not definitely assigned
208
  readonly [Symbol.iterator]: () => Iterator<E, R, unknown>;
4,155✔
209

210
  readonly runSync: [E] extends [never] ? () => R : UnhandledEffect<E>;
4,155✔
211
  readonly runAsync: [E] extends [never] ? () => Promise<R> : UnhandledEffect<E>;
4,155✔
212
  readonly runSyncUnsafe: () => R;
4,155✔
213
  readonly runAsyncUnsafe: () => Promise<R>;
4,155✔
214

215
  private constructor(fn: () => Iterator<E, R, unknown>, magicWords?: string) {
4,155✔
216
    if (magicWords !== "Yes, I’m sure I want to call the constructor of Effected directly.")
4,155✔
217
      console.warn(
4,155✔
218
        "You should not call the constructor of `Effected` directly. Use `effected` instead.",
3✔
219
      );
3✔
220

221
    this[Symbol.iterator] = fn;
4,155✔
222

223
    this.runSync = (() => runSync(this as never)) as never;
4,155✔
224
    this.runAsync = (() => runAsync(this as never)) as never;
4,155✔
225
    this.runSyncUnsafe = () => runSync(this as never);
4,155✔
226
    this.runAsyncUnsafe = () => runAsync(this as never);
4,155✔
227
  }
4,155✔
228

229
  /**
230
   * Create an {@link Effected} instance that just returns the value.
231
   * @param value The value to return.
232
   * @returns
233
   *
234
   * @since 0.1.2
235
   */
236
  static of<R>(value: R): Effected<never, R> {
4,155✔
237
    return effected(() => ({ next: () => ({ done: true, value }) })) as Effected<never, R>;
537✔
238
  }
537✔
239

240
  /**
241
   * Create an {@link Effected} instance that just returns the value from a getter.
242
   * @param getter The getter to get the value.
243
   * @returns
244
   */
245
  static from<R>(getter: () => R): Effected<never, R> {
4,155✔
246
    return effected(() => ({ next: () => ({ done: true, value: getter() }) })) as Effected<
6✔
247
      never,
248
      R
249
    >;
250
  }
6✔
251

252
  /**
253
   * Handle an effect with a handler.
254
   *
255
   * For more common use cases, see {@link resume} and {@link terminate}, which provide a more
256
   * concise syntax.
257
   * @param effect The effect name or a function to match the effect name.
258
   * @param handler The handler for the effect. The first argument is an object containing the
259
   * encountered effect instance, a `resume` function to resume the effect, and a `terminate`
260
   * function to terminate the effect. The rest of the arguments are the payloads of the effect.
261
   *
262
   * `resume` or `terminate` should be called exactly once in the handler. If you call them more
263
   * than once, a warning will be logged to the console. If neither of them is called, the effected
264
   * program will hang indefinitely.
265
   *
266
   * Calling `resume` or `terminate` in an asynchronous context is also supported. It is _not_
267
   * required to call them synchronously.
268
   * @returns
269
   */
270
  handle<Name extends E["name"], T = R, F extends Effect = never>(
271
    effect: Name,
272
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
273
      (
274
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
275
        ...payloads: Payloads
276
      ) => Effected<F, void>
277
    : E extends Effect<Name, infer Payloads, infer R> ?
278
      (
279
        {
280
          effect,
281
          resume,
282
          terminate,
283
        }: {
284
          effect: Extract<E, Effect<Name>>;
285
          resume: (value: R) => void;
286
          terminate: (value: T) => void;
287
        },
288
        ...payloads: Payloads
289
      ) => Effected<F, void>
290
    : never,
291
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
292
  handle<Name extends string | symbol, T = R, F extends Effect = never>(
293
    effect: (name: E["name"]) => name is Name,
294
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
295
      (
296
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
297
        ...payloads: Payloads
298
      ) => Effected<F, void>
299
    : E extends Effect<Name, infer Payloads, infer R> ?
300
      (
301
        {
302
          effect,
303
          resume,
304
          terminate,
305
        }: {
306
          effect: Extract<E, Effect<Name>>;
307
          resume: (value: R) => void;
308
          terminate: (value: T) => void;
309
        },
310
        ...payloads: Payloads
311
      ) => Effected<F, void>
312
    : never,
313
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
314
  handle<Name extends E["name"], T = R, F extends Effect = never>(
315
    effect: Name,
316
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
317
      (
318
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
319
        ...payloads: Payloads
320
      ) => Generator<F, void, unknown>
321
    : E extends Effect<Name, infer Payloads, infer R> ?
322
      (
323
        {
324
          effect,
325
          resume,
326
          terminate,
327
        }: {
328
          effect: Extract<E, Effect<Name>>;
329
          resume: (value: R) => void;
330
          terminate: (value: T) => void;
331
        },
332
        ...payloads: Payloads
333
      ) => Generator<F, void, unknown>
334
    : never,
335
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
336
  handle<Name extends string | symbol, T = R, F extends Effect = never>(
337
    effect: (name: E["name"]) => name is Name,
338
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
339
      (
340
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
341
        ...payloads: Payloads
342
      ) => Generator<F, void, unknown>
343
    : E extends Effect<Name, infer Payloads, infer R> ?
344
      (
345
        {
346
          effect,
347
          resume,
348
          terminate,
349
        }: {
350
          effect: Extract<E, Effect<Name>>;
351
          resume: (value: R) => void;
352
          terminate: (value: T) => void;
353
        },
354
        ...payloads: Payloads
355
      ) => Generator<F, void, unknown>
356
    : never,
357
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
358
  handle<Name extends E["name"], T = R>(
359
    effect: Name,
360
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
361
      (
362
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
363
        ...payloads: Payloads
364
      ) => void
365
    : E extends Effect<Name, infer Payloads, infer R> ?
366
      (
367
        {
368
          effect,
369
          resume,
370
          terminate,
371
        }: {
372
          effect: Extract<E, Effect<Name>>;
373
          resume: (value: R) => void;
374
          terminate: (value: T) => void;
375
        },
376
        ...payloads: Payloads
377
      ) => void
378
    : never,
379
  ): Effected<Exclude<E, Effect<Name>>, R | T>;
380
  handle<Name extends string | symbol, T = R>(
381
    effect: (name: E["name"]) => name is Name,
382
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
383
      (
384
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
385
        ...payloads: Payloads
386
      ) => void
387
    : E extends Effect<Name, infer Payloads, infer R> ?
388
      (
389
        {
390
          effect,
391
          resume,
392
          terminate,
393
        }: {
394
          effect: Extract<E, Effect<Name>>;
395
          resume: (value: R) => void;
396
          terminate: (value: T) => void;
397
        },
398
        ...payloads: Payloads
399
      ) => void
400
    : never,
401
  ): Effected<Exclude<E, Effect<Name>>, R | T>;
402
  handle(
4,155✔
403
    name: string | symbol | ((name: string | symbol) => boolean),
543✔
404
    handler: (...args: any[]) => unknown,
543✔
405
  ): Effected<any, unknown> {
543✔
406
    const matchEffect = (value: unknown) =>
543✔
407
      value instanceof Effect &&
1,722✔
408
      (typeof name === "function" ? name(value.name) : value.name === name);
1,119✔
409

410
    return effected(() => {
543✔
411
      const iterator = this[Symbol.iterator]();
633✔
412
      let interceptIterator: typeof iterator | null = null;
633✔
413
      let terminated: false | "with-value" | "without-value" = false;
633✔
414
      let terminatedValue: unknown;
633✔
415

416
      return {
633✔
417
        next: (...args: [] | [unknown]) => {
633✔
418
          if (terminated)
2,244✔
419
            return {
2,244✔
420
              done: true,
87✔
421
              ...(terminated === "with-value" ? { value: terminatedValue } : {}),
87✔
422
            } as IteratorReturnResult<unknown>;
87✔
423

424
          const result = (interceptIterator || iterator).next(...args);
2,244✔
425

426
          const { done, value } = result;
2,244✔
427
          if (done) return result;
2,244✔
428

429
          if (matchEffect(value)) {
2,244✔
430
            const effect = value;
765✔
431

432
            let resumed: false | "with-value" | "without-value" = false;
765✔
433
            let resumedValue: R;
765✔
434
            let onComplete: ((...args: [] | [R]) => void) | null = null;
765✔
435
            const warnMultipleHandling = (type: "resume" | "terminate", ...args: [] | [R]) => {
765✔
436
              let message = `Effect ${stringifyEffectNameQuoted(name)} has been handled multiple times`;
18✔
437
              message += " (received `";
18✔
438
              message += `${type} ${stringifyEffect(effect)}`;
18✔
439
              if (args.length > 0) message += ` with ${stringify(args[0])}`;
18✔
440
              message += "` after it has been ";
18✔
441
              if (resumed) {
18✔
442
                message += "resumed";
12✔
443
                if (resumed === "with-value") message += ` with ${stringify(resumedValue)}`;
12✔
444
              } else if (terminated) {
18✔
445
                message += "terminated";
6✔
446
                if (terminated === "with-value") message += ` with ${stringify(terminatedValue)}`;
6✔
447
              }
6✔
448
              message += "). Only the first handler will be used.";
18✔
449
              console.warn(message);
18✔
450
            };
18✔
451
            const resume = (...args: [] | [R]) => {
765✔
452
              if (resumed || terminated) {
633✔
453
                warnMultipleHandling("resume", ...args);
12✔
454
                return;
12✔
455
              }
12✔
456
              resumed = args.length > 0 ? "with-value" : "without-value";
633✔
457
              if (args.length > 0) resumedValue = args[0]!;
633✔
458
              if (onComplete) {
633✔
459
                onComplete(...args);
21✔
460
                onComplete = null;
21✔
461
              }
21✔
462
            };
633✔
463
            const terminate = (...args: [] | [R]) => {
765✔
464
              if (resumed || terminated) {
93✔
465
                warnMultipleHandling("terminate", ...args);
6✔
466
                return;
6✔
467
              }
6✔
468
              terminated = args.length > 0 ? "with-value" : "without-value";
93✔
469
              if (args.length > 0) terminatedValue = args[0];
93✔
470
              if (onComplete) {
93✔
471
                onComplete(...args);
6✔
472
                onComplete = null;
6✔
473
              }
6✔
474
            };
93✔
475

476
            const constructHandledEffect = ():
765✔
477
              | { _effectSync: true; value?: unknown }
478
              | {
479
                  _effectAsync: true;
480
                  onComplete: (callback: (...args: [] | [R]) => void) => void;
481
                } => {
708✔
482
              // For synchronous effects
483
              if (resumed || terminated)
708✔
484
                return {
708✔
485
                  _effectSync: true,
678✔
486
                  ...(Object.is(resumed, "with-value") ? { value: resumedValue! }
678✔
487
                  : Object.is(terminated, "with-value") ? { value: terminatedValue! }
93✔
488
                  : {}),
18✔
489
                };
678✔
490
              // For asynchronous effects
491
              return {
30✔
492
                _effectAsync: true,
30✔
493
                onComplete: (callback) => {
30✔
494
                  onComplete = callback;
27✔
495
                },
27✔
496
              };
30✔
497
            };
708✔
498

499
            const handlerResult = handler(
765✔
500
              {
765✔
501
                effect,
765✔
502
                resume:
765✔
503
                  (effect as any).resumable === false ?
765✔
504
                    () => {
87✔
505
                      throw new Error(
9✔
506
                        `Cannot resume non-resumable effect: ${stringifyEffect(effect)}`,
9✔
507
                      );
9✔
508
                    }
9✔
509
                  : resume,
678✔
510
                terminate,
765✔
511
              },
765✔
512
              ...effect.payloads,
765✔
513
            );
765✔
514

515
            if (!(handlerResult instanceof Effected) && !isGenerator(handlerResult))
765✔
516
              return { done: false, value: constructHandledEffect() } as never;
765✔
517

518
            const it = handlerResult[Symbol.iterator]();
57✔
519
            interceptIterator = {
57✔
520
              next: (...args: [] | [unknown]) => {
57✔
521
                const result = it.next(...args);
108✔
522
                if (result.done) {
108✔
523
                  interceptIterator = null;
51✔
524
                  return { done: false, value: constructHandledEffect() } as never;
51✔
525
                }
51✔
526
                return result as never;
57✔
527
              },
108✔
528
            };
57✔
529
            return interceptIterator.next();
57✔
530
          }
57✔
531

532
          return result;
957✔
533
        },
2,244✔
534
      };
633✔
535
    });
543✔
536
  }
543✔
537

538
  /**
539
   * Resume an effect with the return value of the handler.
540
   *
541
   * It is a shortcut for
542
   * `handle(effect, ({ resume }, ...payloads) => resume(handler(...payloads)))`.
543
   * @param effect The effect name or a function to match the effect name.
544
   * @param handler The handler for the effect. The arguments are the payloads of the effect.
545
   * @returns
546
   *
547
   * @see {@link handle}
548
   */
549
  resume<Name extends Exclude<E, Unresumable<Effect>>["name"], F extends Effect = never>(
550
    effect: Name,
551
    handler: E extends Effect<Name, infer Payloads, infer R> ?
552
      (...payloads: Payloads) => Effected<F, R>
553
    : never,
554
  ): Effected<Exclude<E, Effect<Name>> | F, R>;
555
  resume<Name extends string | symbol, F extends Effect = never>(
556
    effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
557
    handler: E extends Effect<Name, infer Payloads, infer R> ?
558
      (...payloads: Payloads) => Effected<F, R>
559
    : never,
560
  ): Effected<Exclude<E, Effect<Name>> | F, R>;
561
  resume<Name extends Exclude<E, Unresumable<Effect>>["name"], F extends Effect = never>(
562
    effect: Name,
563
    handler: E extends Effect<Name, infer Payloads, infer R> ?
564
      (...payloads: Payloads) => Generator<F, R, unknown>
565
    : never,
566
  ): Effected<Exclude<E, Effect<Name>> | F, R>;
567
  resume<Name extends string | symbol, F extends Effect = never>(
568
    effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
569
    handler: E extends Effect<Name, infer Payloads, infer R> ?
570
      (...payloads: Payloads) => Generator<F, R, unknown>
571
    : never,
572
  ): Effected<Exclude<E, Effect<Name>> | F, R>;
573
  resume<Name extends Exclude<E, Unresumable<Effect>>["name"]>(
574
    effect: Name,
575
    handler: E extends Effect<Name, infer Payloads, infer R> ? (...payloads: Payloads) => R : never,
576
  ): Effected<Exclude<E, Effect<Name>>, R>;
577
  resume<Name extends string | symbol>(
578
    effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
579
    handler: E extends Effect<Name, infer Payloads, infer R> ? (...payloads: Payloads) => R : never,
580
  ): Effected<Exclude<E, Effect<Name>>, R>;
581
  resume(effect: any, handler: (...payloads: unknown[]) => unknown) {
4,155✔
582
    return this.handle(effect, (({ resume }: any, ...payloads: unknown[]) => {
267✔
583
      const it = handler(...payloads);
567✔
584
      if (!(it instanceof Effected) && !isGenerator(it)) return resume(it);
567✔
585
      return (function* () {
42✔
586
        resume(yield* it);
42✔
587
      })();
42✔
588
    }) as never);
267✔
589
  }
267✔
590

591
  /**
592
   * Terminate an effect with the return value of the handler.
593
   *
594
   * It is a shortcut for
595
   * `handle(effect, ({ terminate }, ...payloads) => terminate(handler(...payloads)))`.
596
   * @param effect The effect name or a function to match the effect name.
597
   * @param handler The handler for the effect. The arguments are the payloads of the effect.
598
   * @returns
599
   *
600
   * @see {@link handle}
601
   */
602
  terminate<Name extends E["name"], T, F extends Effect = never>(
603
    effect: Name,
604
    handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => Effected<F, T>
605
    : never,
606
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
607
  terminate<Name extends string | symbol, T, F extends Effect = never>(
608
    effect: (name: E["name"]) => name is Name,
609
    handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => Effected<F, T>
610
    : never,
611
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
612
  terminate<Name extends E["name"], T, F extends Effect = never>(
613
    effect: Name,
614
    handler: E extends Effect<Name, infer Payloads> ?
615
      (...payloads: Payloads) => Generator<F, T, unknown>
616
    : never,
617
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
618
  terminate<Name extends string | symbol, T, F extends Effect = never>(
619
    effect: (name: E["name"]) => name is Name,
620
    handler: E extends Effect<Name, infer Payloads> ?
621
      (...payloads: Payloads) => Generator<F, T, unknown>
622
    : never,
623
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
624
  terminate<Name extends E["name"], T>(
625
    effect: Name,
626
    handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
627
  ): Effected<Exclude<E, Effect<Name>>, R | T>;
628
  terminate<Name extends string | symbol, T>(
629
    effect: (name: E["name"]) => name is Name,
630
    handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
631
  ): Effected<Exclude<E, Effect<Name>>, R | T>;
632
  terminate(effect: any, handler: (...payloads: unknown[]) => unknown) {
4,155✔
633
    return this.handle(effect, (({ terminate }: any, ...payloads: unknown[]) => {
150✔
634
      const it = handler(...payloads);
60✔
635
      if (!(it instanceof Effected) && !isGenerator(it)) return terminate(it);
60✔
636
      return (function* () {
3✔
637
        terminate(yield* it);
3✔
638
      })();
3✔
639
    }) as never);
150✔
640
  }
150✔
641

642
  /**
643
   * Map the return value of the effected program.
644
   * @param handler The function to map the return value.
645
   * @returns
646
   */
647
  map<S, F extends Effect = never>(handler: (value: R) => Effected<F, S>): Effected<E | F, S>;
648
  map<S, F extends Effect = never>(
649
    handler: (value: R) => Generator<F, S, unknown>,
650
  ): Effected<E | F, S>;
651
  map<S>(handler: (value: R) => S): Effected<E, S>;
652
  map(handler: (value: R) => unknown): Effected<E, unknown> {
4,155✔
653
    const iterator = this[Symbol.iterator]();
1,164✔
654

655
    return effected(() => {
1,164✔
656
      let originalIteratorDone = false;
1,164✔
657
      let appendedIterator: Iterator<E, unknown, unknown>;
1,164✔
658
      return {
1,164✔
659
        next: (...args: [] | [R]) => {
1,164✔
660
          if (originalIteratorDone) return appendedIterator.next(...args);
1,290✔
661
          const result = iterator.next(...args);
1,284✔
662
          if (!result.done) return result;
1,290✔
663
          originalIteratorDone = true;
1,110✔
664
          const it = handler(result.value);
1,110✔
665
          if (!(it instanceof Effected) && !isGenerator(it)) return { done: true, value: it };
1,290✔
666
          appendedIterator = it[Symbol.iterator]();
534✔
667
          return appendedIterator.next();
534✔
668
        },
1,290✔
669
      };
1,164✔
670
    });
1,164✔
671
  }
1,164✔
672

673
  /**
674
   * Catch an error effect with a handler.
675
   *
676
   * It is a shortcut for `terminate("error:" + name, handler)`.
677
   * @param name The name of the error effect.
678
   * @param handler The handler for the error effect. The argument is the message of the error.
679
   * @returns
680
   *
681
   * @see {@link terminate}
682
   */
683
  catch<Name extends ErrorName<E>, T, F extends Effect = never>(
684
    effect: Name,
685
    handler: (message?: string) => Effected<F, T>,
686
  ): Effected<Exclude<E, Effect.Error<Name>> | F, R | T>;
687
  catch<Name extends ErrorName<E>, T, F extends Effect = never>(
688
    effect: Name,
689
    handler: (message?: string) => Generator<F, T, unknown>,
690
  ): Effected<Exclude<E, Effect.Error<Name>> | F, R | T>;
691
  catch<Name extends ErrorName<E>, T, F extends Effect = never>(
692
    effect: Name,
693
    handler: (message?: string) => Generator<F, T, unknown>,
694
  ): Effected<Exclude<E, Effect.Error<Name>> | F, R | T>;
695
  catch<Name extends ErrorName<E>, T>(
696
    effect: Name,
697
    handler: (message?: string) => T,
698
  ): Effected<Exclude<E, Effect.Error<Name>>, R | T>;
699
  catch(name: string, handler: (message?: string) => unknown): Effected<Effect, unknown> {
4,155✔
700
    return this.terminate(`error:${name}` as never, handler as never);
81✔
701
  }
81✔
702

703
  /**
704
   * Catch all error effects with a handler.
705
   * @param handler The handler for the error effect. The first argument is the name of the error
706
   * effect (without the `"error:"` prefix), and the second argument is the message of the error.
707
   */
708
  catchAll<T, F extends Effect = never>(
709
    handler: (error: ErrorName<E>, message?: string) => Effected<F, T>,
710
  ): Effected<Exclude<E, Effect.Error> | F, R | T>;
711
  catchAll<T, F extends Effect = never>(
712
    handler: (error: ErrorName<E>, message?: string) => Generator<F, T, unknown>,
713
  ): Effected<Exclude<E, Effect.Error> | F, R | T>;
714
  catchAll<T>(
715
    handler: (error: ErrorName<E>, message?: string) => T,
716
  ): Effected<Exclude<E, Effect.Error>, R | T>;
717
  catchAll(handler: (error: ErrorName<E>, message?: string) => unknown): Effected<Effect, unknown> {
4,155✔
718
    return this.handle(
36✔
719
      (name): name is ErrorName<E> => typeof name === "string" && name.startsWith("error:"),
36✔
720
      (({ effect, terminate }: any, ...payloads: [message?: string]) => {
36✔
721
        const error = effect.name.slice(6) as ErrorName<E>;
33✔
722
        const it = handler(error, ...payloads);
33✔
723
        if (!(it instanceof Effected) && !isGenerator(it)) return terminate(it);
33✔
724
        return (function* () {
3✔
725
          terminate(yield* it);
3✔
726
        })();
3✔
727
      }) as never,
33✔
728
    );
36✔
729
  }
36✔
730

731
  /**
732
   * Catch an error effect and throw it as an error.
733
   * @param name The name of the error effect.
734
   * @param message The message of the error. If it is a function, it will be called with the
735
   * message of the error effect, and the return value will be used as the message of the error.
736
   * @returns
737
   *
738
   * @since 0.1.1
739
   */
740
  catchAndThrow<Name extends ErrorName<E>>(
4,155✔
741
    name: Name,
18✔
742
    message?: string | ((message?: string) => string | undefined),
18✔
743
  ): Effected<Exclude<E, Effect.Error<Name>>, R> {
18✔
744
    return this.catch(name, (...args) => {
18✔
745
      throw new (buildErrorClass(name))(
18✔
746
        ...(typeof message === "string" ? [message]
18✔
747
        : typeof message === "function" ? [message(...args)].filter((v) => v !== undefined)
12✔
748
        : args),
6✔
749
      );
18✔
750
    });
18✔
751
  }
18✔
752

753
  /**
754
   * Catch all error effects and throw them as an error.
755
   * @param message The message of the error. If it is a function, it will be called with the name
756
   * and the message of the error effect, and the return value will be used as the message of the
757
   * error.
758
   * @returns
759
   *
760
   * @since 0.1.1
761
   */
762
  catchAllAndThrow(
4,155✔
763
    message?: string | ((error: string, message?: string) => string | undefined),
18✔
764
  ): Effected<Exclude<E, Effect.Error>, R> {
18✔
765
    return this.catchAll((error, ...args) => {
18✔
766
      throw new (buildErrorClass(error))(
18✔
767
        ...(typeof message === "string" ? [message]
18✔
768
        : typeof message === "function" ? [message(error, ...args)].filter((v) => v !== undefined)
12✔
769
        : args),
6✔
770
      );
18✔
771
    });
18✔
772
  }
18✔
773

774
  /**
775
   * Provide a value for a dependency effect.
776
   * @param name The name of the dependency.
777
   * @param value The value to provide for the dependency.
778
   * @returns
779
   */
780
  provide<Name extends DependencyName<E>>(
4,155✔
781
    name: Name,
18✔
782
    value: E extends Effect.Dependency<Name, infer R> ? R : never,
18✔
783
  ): Effected<Exclude<E, Effect.Dependency<Name>>, R> {
18✔
784
    return this.resume(`dependency:${name}` as never, (() => value) as never) as never;
18✔
785
  }
18✔
786

787
  /**
788
   * Provide a value for a dependency effect with a getter.
789
   * @param name The name of the dependency.
790
   * @param getter The getter to provide for the dependency.
791
   * @returns
792
   */
793
  provideBy<Name extends DependencyName<E>, F extends Effect = never>(
794
    name: Name,
795
    getter: E extends Effect.Dependency<Name, infer R> ? () => Effected<F, R> : never,
796
  ): Effected<Exclude<E, Effect.Dependency<Name>> | F, R>;
797
  provideBy<Name extends DependencyName<E>, F extends Effect = never>(
798
    name: Name,
799
    getter: E extends Effect.Dependency<Name, infer R> ? () => Generator<F, R, unknown> : never,
800
  ): Effected<Exclude<E, Effect.Dependency<Name>> | F, R>;
801
  provideBy<Name extends DependencyName<E>>(
802
    name: Name,
803
    getter: E extends Effect.Dependency<Name, infer R> ? () => R : never,
804
  ): Effected<Exclude<E, Effect.Dependency<Name>>, R>;
805
  provideBy(name: string, getter: () => unknown): Effected<Effect, unknown> {
4,155✔
806
    return this.resume(`dependency:${name}`, getter as never);
21✔
807
  }
21✔
808

809
  /**
810
   * Apply a handler to the effected program.
811
   * @param handler The handler to apply to the effected program.
812
   * @returns
813
   */
814
  with<F extends Effect, G extends Effect, S>(
815
    handler: (effected: EffectedDraft<never, never, R>) => EffectedDraft<F, G, S>,
816
  ): Effected<Exclude<E, F> | G, S>;
817
  with<F extends Effect, S>(handler: (effected: Effected<E, R>) => Effected<F, S>): Effected<F, S>;
818
  with(handler: (effected: any) => unknown) {
4,155✔
819
    return handler(this);
81✔
820
  }
81✔
821
}
4,155✔
822

823
interface EffectedDraft<
824
  out P extends Effect = Effect,
825
  out E extends Effect = Effect,
826
  out R = unknown,
827
> extends Iterable<E, R, unknown> {
828
  readonly handle: {
829
    <Name extends E["name"], T = R, F extends Effect = never>(
830
      effect: Name,
831
      handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
832
        (
833
          {
834
            effect,
835
            terminate,
836
          }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
837
          ...payloads: Payloads
838
        ) => Effected<F, void>
839
      : E extends Effect<Name, infer Payloads, infer R> ?
840
        (
841
          {
842
            effect,
843
            resume,
844
            terminate,
845
          }: {
846
            effect: Extract<E, Effect<Name>>;
847
            resume: (value: R) => void;
848
            terminate: (value: T) => void;
849
          },
850
          ...payloads: Payloads
851
        ) => Effected<F, void>
852
      : never,
853
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
854
    <Name extends string | symbol, T = R, F extends Effect = never>(
855
      effect: (name: E["name"]) => name is Name,
856
      handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
857
        (
858
          {
859
            effect,
860
            terminate,
861
          }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
862
          ...payloads: Payloads
863
        ) => Effected<F, void>
864
      : E extends Effect<Name, infer Payloads, infer R> ?
865
        (
866
          {
867
            effect,
868
            resume,
869
            terminate,
870
          }: {
871
            effect: Extract<E, Effect<Name>>;
872
            resume: (value: R) => void;
873
            terminate: (value: T) => void;
874
          },
875
          ...payloads: Payloads
876
        ) => Effected<F, void>
877
      : never,
878
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
879
    <Name extends E["name"], T = R, F extends Effect = never>(
880
      effect: Name,
881
      handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
882
        (
883
          {
884
            effect,
885
            terminate,
886
          }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
887
          ...payloads: Payloads
888
        ) => Generator<F, void, unknown>
889
      : E extends Effect<Name, infer Payloads, infer R> ?
890
        (
891
          {
892
            effect,
893
            resume,
894
            terminate,
895
          }: {
896
            effect: Extract<E, Effect<Name>>;
897
            resume: (value: R) => void;
898
            terminate: (value: T) => void;
899
          },
900
          ...payloads: Payloads
901
        ) => Generator<F, void, unknown>
902
      : never,
903
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
904
    <Name extends string | symbol, T = R, F extends Effect = never>(
905
      effect: (name: E["name"]) => name is Name,
906
      handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
907
        (
908
          {
909
            effect,
910
            terminate,
911
          }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
912
          ...payloads: Payloads
913
        ) => Generator<F, void, unknown>
914
      : E extends Effect<Name, infer Payloads, infer R> ?
915
        (
916
          {
917
            effect,
918
            resume,
919
            terminate,
920
          }: {
921
            effect: Extract<E, Effect<Name>>;
922
            resume: (value: R) => void;
923
            terminate: (value: T) => void;
924
          },
925
          ...payloads: Payloads
926
        ) => Generator<F, void, unknown>
927
      : never,
928
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
929
    <Name extends E["name"], T = R>(
930
      effect: Name,
931
      handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
932
        (
933
          {
934
            effect,
935
            terminate,
936
          }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
937
          ...payloads: Payloads
938
        ) => void
939
      : E extends Effect<Name, infer Payloads, infer R> ?
940
        (
941
          {
942
            effect,
943
            resume,
944
            terminate,
945
          }: {
946
            effect: Extract<E, Effect<Name>>;
947
            resume: (value: R) => void;
948
            terminate: (value: T) => void;
949
          },
950
          ...payloads: Payloads
951
        ) => void
952
      : never,
953
    ): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
954
    <Name extends string | symbol, T = R>(
955
      effect: (name: E["name"]) => name is Name,
956
      handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
957
        (
958
          {
959
            effect,
960
            terminate,
961
          }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
962
          ...payloads: Payloads
963
        ) => void
964
      : E extends Effect<Name, infer Payloads, infer R> ?
965
        (
966
          {
967
            effect,
968
            resume,
969
            terminate,
970
          }: {
971
            effect: Extract<E, Effect<Name>>;
972
            resume: (value: R) => void;
973
            terminate: (value: T) => void;
974
          },
975
          ...payloads: Payloads
976
        ) => void
977
      : never,
978
    ): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
979
  };
980
  readonly resume: {
981
    <Name extends Exclude<E, Unresumable<Effect>>["name"], F extends Effect = never>(
982
      effect: Name,
983
      handler: E extends Effect<Name, infer Payloads, infer R> ?
984
        (...payloads: Payloads) => Effected<F, R>
985
      : never,
986
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R>;
987
    <Name extends string | symbol, F extends Effect = never>(
988
      effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
989
      handler: E extends Effect<Name, infer Payloads, infer R> ?
990
        (...payloads: Payloads) => Effected<F, R>
991
      : never,
992
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R>;
993
    <Name extends Exclude<E, Unresumable<Effect>>["name"], F extends Effect = never>(
994
      effect: Name,
995
      handler: E extends Effect<Name, infer Payloads, infer R> ?
996
        (...payloads: Payloads) => Generator<F, R, unknown>
997
      : never,
998
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R>;
999
    <Name extends string | symbol, F extends Effect = never>(
1000
      effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
1001
      handler: E extends Effect<Name, infer Payloads, infer R> ?
1002
        (...payloads: Payloads) => Generator<F, R, unknown>
1003
      : never,
1004
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R>;
1005
    <Name extends Exclude<E, Unresumable<Effect>>["name"]>(
1006
      effect: Name,
1007
      handler: E extends Effect<Name, infer Payloads, infer R> ? (...payloads: Payloads) => R
1008
      : never,
1009
    ): EffectedDraft<P, Exclude<E, Effect<Name>>, R>;
1010
    <Name extends string | symbol>(
1011
      effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
1012
      handler: E extends Effect<Name, infer Payloads, infer R> ? (...payloads: Payloads) => R
1013
      : never,
1014
    ): EffectedDraft<P, Exclude<E, Effect<Name>>, R>;
1015
  };
1016
  readonly terminate: {
1017
    <Name extends E["name"], T, F extends Effect = never>(
1018
      effect: Name,
1019
      handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => Effected<F, T>
1020
      : never,
1021
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
1022
    <Name extends string | symbol, T, F extends Effect = never>(
1023
      effect: (name: E["name"]) => name is Name,
1024
      handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => Effected<F, T>
1025
      : never,
1026
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
1027
    <Name extends E["name"], T, F extends Effect = never>(
1028
      effect: Name,
1029
      handler: E extends Effect<Name, infer Payloads> ?
1030
        (...payloads: Payloads) => Generator<F, T, unknown>
1031
      : never,
1032
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
1033
    <Name extends E["name"], T, F extends Effect = never>(
1034
      effect: Name,
1035
      handler: E extends Effect<Name, infer Payloads> ?
1036
        (...payloads: Payloads) => Generator<F, T, unknown>
1037
      : never,
1038
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
1039
    <Name extends string | symbol, T, F extends Effect = never>(
1040
      effect: (name: E["name"]) => name is Name,
1041
      handler: E extends Effect<Name, infer Payloads> ?
1042
        (...payloads: Payloads) => Generator<F, T, unknown>
1043
      : never,
1044
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
1045
    <Name extends E["name"], T>(
1046
      effect: Name,
1047
      handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
1048
    ): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
1049
    <Name extends string | symbol, T>(
1050
      effect: (name: E["name"]) => name is Name,
1051
      handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
1052
    ): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
1053
  };
1054

1055
  readonly map: {
1056
    <S, F extends Effect = never>(
1057
      handler: (value: R) => Effected<F, S>,
1058
    ): EffectedDraft<P, E | F, S>;
1059
    <S, F extends Effect = never>(
1060
      handler: (value: R) => Generator<F, S, unknown>,
1061
    ): EffectedDraft<P, E | F, S>;
1062
    <S>(handler: (value: R) => S): EffectedDraft<P, E, S>;
1063
  };
1064

1065
  readonly catch: {
1066
    <Name extends ErrorName<E>, T, F extends Effect = never>(
1067
      effect: Name,
1068
      handler: (message?: string) => Effected<F, T>,
1069
    ): EffectedDraft<P, Exclude<E, Effect.Error<Name>> | F, R | T>;
1070
    <Name extends ErrorName<E>, T, F extends Effect = never>(
1071
      effect: Name,
1072
      handler: (message?: string) => Generator<F, T, unknown>,
1073
    ): EffectedDraft<P, Exclude<E, Effect.Error<Name>> | F, R | T>;
1074
    <Name extends ErrorName<E>, T>(
1075
      effect: Name,
1076
      handler: (message?: string) => T,
1077
    ): EffectedDraft<P, Exclude<E, Effect.Error<Name>>, R | T>;
1078
  };
1079

1080
  readonly catchAll: {
1081
    <T, F extends Effect = never>(
1082
      handler: (effect: ErrorName<E>, message?: string) => Effected<F, T>,
1083
    ): Effected<Exclude<E, Effect.Error> | F, R | T>;
1084
    <T, F extends Effect = never>(
1085
      handler: (effect: ErrorName<E>, message?: string) => Generator<F, T, unknown>,
1086
    ): Effected<Exclude<E, Effect.Error> | F, R | T>;
1087
    <T>(
1088
      handler: (effect: ErrorName<E>, message?: string) => T,
1089
    ): Effected<Exclude<E, Effect.Error>, R | T>;
1090
  };
1091

1092
  readonly catchAndThrow: <Name extends ErrorName<E>>(
1093
    name: Name,
1094
    message?: string | ((message?: string) => string | undefined),
1095
  ) => Effected<Exclude<E, Effect.Error<Name>>, R>;
1096

1097
  readonly catchAllAndThrow: (
1098
    message?: string | ((error: string, message?: string) => string | undefined),
1099
  ) => Effected<Exclude<E, Effect.Error>, R>;
1100

1101
  readonly provide: <Name extends DependencyName<E>>(
1102
    name: Name,
1103
    value: E extends Effect.Dependency<Name, infer R> ? R : never,
1104
  ) => EffectedDraft<P, Exclude<E, Effect.Dependency<Name>>, R>;
1105
  readonly provideBy: {
1106
    <Name extends DependencyName<E>, F extends Effect = never>(
1107
      name: Name,
1108
      getter: E extends Effect.Dependency<Name, infer R> ? () => Effected<F, R> : never,
1109
    ): EffectedDraft<P, Exclude<E, Effect.Dependency<Name>> | F, R>;
1110
    <Name extends DependencyName<E>, F extends Effect = never>(
1111
      name: Name,
1112
      getter: E extends Effect.Dependency<Name, infer R> ? () => Generator<F, R, unknown> : never,
1113
    ): EffectedDraft<P, Exclude<E, Effect.Dependency<Name>> | F, R>;
1114
    <Name extends DependencyName<E>>(
1115
      name: Name,
1116
      getter: E extends Effect.Dependency<Name, infer R> ? () => R : never,
1117
    ): EffectedDraft<P, Exclude<E, Effect.Dependency<Name>>, R>;
1118
  };
1119

1120
  readonly with: {
1121
    <F extends Effect, G extends Effect, S>(
1122
      handler: (effected: EffectedDraft<never, never, R>) => EffectedDraft<F, G, S>,
1123
    ): EffectedDraft<P, Exclude<E, F> | G, S>;
1124
    <F extends Effect, S>(
1125
      handler: (effected: Effected<E, R>) => Effected<F, S>,
1126
    ): EffectedDraft<P, F, S>;
1127
  };
1128
}
1129

1130
/**
1131
 * Create an effected program.
1132
 * @param fn A function that returns an iterator.
1133
 * @returns
1134
 *
1135
 * @example
1136
 * ```typescript
1137
 * type User = { id: number; name: string; role: "admin" | "user" };
1138
 *
1139
 * // Use `effect` and its variants to define factory functions for effects
1140
 * const println = effect("println")<unknown[], void>;
1141
 * const executeSQL = effect("executeSQL")<[sql: string, ...params: unknown[]], any>;
1142
 * const askCurrentUser = dependency("currentUser")<User | null>;
1143
 * const authenticationError = error("authentication");
1144
 * const unauthorizedError = error("unauthorized");
1145
 *
1146
 * // Use `effected` to define an effected program
1147
 * const requiresAdmin = () => effected(function* () {
1148
 *   const currentUser = yield* askCurrentUser();
1149
 *   if (!currentUser) return yield* authenticationError();
1150
 *   if (currentUser.role !== "admin")
1151
 *     return yield* unauthorizedError(`User "${currentUser.name}" is not an admin`);
1152
 * });
1153
 *
1154
 * // You can yield other effected programs in an effected program
1155
 * const createUser = (user: Omit<User, "id">) => effected(function* () {
1156
 *   yield* requiresAdmin();
1157
 *   const id = yield* executeSQL("INSERT INTO users (name) VALUES (?)", user.name);
1158
 *   const savedUser: User = { id, ...user };
1159
 *   yield* println("User created:", savedUser);
1160
 *   return savedUser;
1161
 * });
1162
 *
1163
 * const program = effected(function* () {
1164
 *   yield* createUser({ name: "Alice", role: "user" });
1165
 *   yield* createUser({ name: "Bob", role: "admin" });
1166
 * })
1167
 *   // Handle effects with the `.handle()` method
1168
 *   .handle("executeSQL", function* ({ resume, terminate }, sql, ...params) {
1169
 *     // You can yield other effects in a handler using a generator function
1170
 *     yield* println("Executing SQL:", sql, ...params);
1171
 *     // Asynchronous effects are supported
1172
 *     db.execute(sql, params, (err, result) => {
1173
 *       if (err) return terminate(err);
1174
 *       resume(result);
1175
 *     });
1176
 *   })
1177
 *   // a shortcut for `.handle()` that resumes the effect with the return value of the handler
1178
 *   .resume("println", (...args) => console.log(...args))
1179
 *   // Other shortcuts for special effects (error effects and dependency effects)
1180
 *   .provide("currentUser", { id: 1, name: "Charlie", role: "admin" })
1181
 *   .catch("authentication", () => console.error("Authentication error"));
1182
 *   .catch("unauthorized", () => console.error("Unauthorized error"));
1183
 *
1184
 * // Run the effected program with `.runSync()` or `.runAsync()`
1185
 * await program.runAsync();
1186
 * ```
1187
 *
1188
 * @see {@link effect}
1189
 */
1190
export function effected<E extends Effect, R>(fn: () => Iterator<E, R, unknown>): Effected<E, R> {
3✔
1191
  return new (Effected as any)(
4,152✔
1192
    fn,
4,152✔
1193
    "Yes, I’m sure I want to call the constructor of Effected directly.",
4,152✔
1194
  );
4,152✔
1195
}
4,152✔
1196

1197
/**
1198
 * Convert a {@link Promise} to an effected program containing a single {@link Effect}.
1199
 * @param promise The promise to effectify.
1200
 * @returns
1201
 *
1202
 * ```typescript
1203
 * // Assume we have `db.user.create(user: User): Promise<number>`
1204
 * const createUser = (user: Omit<User, "id">) => effected(function* () {
1205
 *   yield* requiresAdmin();
1206
 *   // Use `yield* effectify(...)` instead of `await ...` in an effected program
1207
 *   const id = yield* effectify(db.user.create(user));
1208
 *   const savedUser = { id, ...user };
1209
 *   yield* println("User created:", savedUser);
1210
 *   return savedUser;
1211
 * });
1212
 * ```
1213
 */
1214
export function effectify<T>(promise: Promise<T>): Effected<never, T> {
3✔
1215
  return effected(() => {
27✔
1216
    let state = 0;
27✔
1217
    return {
27✔
1218
      next: (...args) => {
27✔
1219
        switch (state) {
51✔
1220
          case 0:
51✔
1221
            state++;
27✔
1222
            return {
27✔
1223
              done: false,
27✔
1224
              value: {
27✔
1225
                _effectAsync: true,
27✔
1226
                onComplete: (
27✔
1227
                  ...args: [onComplete: (value: T) => void, onThrow?: (value: unknown) => void]
21✔
1228
                ) => promise.then(...args),
21✔
1229
              } as never,
27✔
1230
            };
27✔
1231
          case 1:
51✔
1232
            state++;
21✔
1233
            return {
21✔
1234
              done: true,
21✔
1235
              ...(args.length > 0 ? { value: args[0] } : {}),
21!
1236
            } as IteratorReturnResult<T>;
21✔
1237
          default:
51✔
1238
            return { done: true } as IteratorReturnResult<T>;
3✔
1239
        }
51✔
1240
      },
51✔
1241
    };
27✔
1242
  });
27✔
1243
}
27✔
1244

1245
/**
1246
 * Run an effected program synchronously and return its result.
1247
 * @param effected The effected program.
1248
 * @returns
1249
 *
1250
 * @throws {UnhandledEffectError} If an unhandled effect is encountered.
1251
 * @throws {Error} If an asynchronous effect is encountered.
1252
 */
1253
export function runSync<E extends Effected<Effect, unknown>>(
3✔
1254
  effected: E extends Effected<infer F extends Effect, unknown> ?
312✔
1255
    [F] extends [never] ?
1256
      E
1257
    : UnhandledEffect<F>
1258
  : never,
1259
): E extends Effected<Effect, infer R> ? R : never {
312✔
1260
  const iterator = (effected as Iterable<any>)[Symbol.iterator]();
312✔
1261
  let { done, value } = iterator.next();
312✔
1262
  while (!done) {
312✔
1263
    if (!value)
651✔
1264
      throw new Error(
651✔
1265
        `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1266
      );
3✔
1267
    if (value._effectSync) {
651✔
1268
      ({ done, value } = iterator.next(...("value" in value ? [value.value] : [])));
624✔
1269
      continue;
624✔
1270
    }
624✔
1271
    if (value._effectAsync)
24✔
1272
      throw new Error(
390✔
1273
        "Cannot run an asynchronous effected program with `runSync`, use `runAsync` instead",
6✔
1274
      );
6✔
1275
    if (value instanceof Effect)
18✔
1276
      throw new UnhandledEffectError(value, `Unhandled effect: ${stringifyEffect(value)}`);
126✔
1277
    throw new Error(
3✔
1278
      `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1279
    );
3✔
1280
  }
3✔
1281
  return value;
240✔
1282
}
240✔
1283

1284
/**
1285
 * Run a (possibly) asynchronous effected program and return its result as a {@link Promise}.
1286
 * @param effected The effected program.
1287
 * @returns
1288
 *
1289
 * @throws {UnhandledEffectError} If an unhandled effect is encountered.
1290
 */
1291
export function runAsync<E extends Effected<Effect, unknown>>(
3✔
1292
  effected: E extends Effected<infer F extends Effect, unknown> ?
66✔
1293
    [F] extends [never] ?
1294
      E
1295
    : UnhandledEffect<F>
1296
  : never,
1297
): Promise<E extends Effected<Effect, infer R> ? R : never> {
66✔
1298
  const iterator = (effected as Iterable<any>)[Symbol.iterator]();
66✔
1299

1300
  return new Promise((resolve, reject) => {
66✔
1301
    const iterate = (...args: [] | [unknown]) => {
66✔
1302
      let done: boolean | undefined;
111✔
1303
      let value: any;
111✔
1304
      try {
111✔
1305
        ({ done, value } = iterator.next(...args));
111✔
1306
      } catch (e) {
111✔
1307
        // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1308
        reject(e);
6✔
1309
        return;
6✔
1310
      }
6✔
1311

1312
      // We use a while loop to avoid stack overflow when there are many synchronous effects
1313
      while (!done) {
111✔
1314
        if (!value) {
111✔
1315
          reject(
3✔
1316
            new Error(
3✔
1317
              `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1318
            ),
3✔
1319
          );
3✔
1320
          return;
3✔
1321
        }
3✔
1322
        if (value._effectSync) {
108✔
1323
          try {
54✔
1324
            ({ done, value } = iterator.next(...("value" in value ? [value.value] : [])));
54!
1325
          } catch (e) {
54✔
1326
            // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1327
            reject(e);
3✔
1328
            return;
3✔
1329
          }
3✔
1330
          continue;
51✔
1331
        }
51✔
1332
        if (value._effectAsync) {
78✔
1333
          // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1334
          value.onComplete(iterate, (...args: unknown[]) => reject(...args.slice(0, 1)));
48✔
1335
          return;
48✔
1336
        }
48✔
1337
        if (value instanceof Effect) {
60✔
1338
          reject(new UnhandledEffectError(value, `Unhandled effect: ${stringifyEffect(value)}`));
3✔
1339
          return;
3✔
1340
        }
3✔
1341
        reject(
3✔
1342
          new Error(
3✔
1343
            `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1344
          ),
3✔
1345
        );
3✔
1346
        return;
3✔
1347
      }
3✔
1348

1349
      resolve(value);
45✔
1350
    };
111✔
1351

1352
    iterate();
66✔
1353
  });
66✔
1354
}
66✔
1355

1356
/*********************
1357
 * Utility functions *
1358
 *********************/
1359
/**
1360
 * Check if a value is a {@link Generator}.
1361
 * @param value The value to check.
1362
 * @returns
1363
 */
1364
const isGenerator = (value: unknown): value is Generator =>
3✔
1365
  Object.prototype.toString.call(value) === "[object Generator]";
1,890✔
1366

1367
/**
1368
 * Capitalize the first letter of a string.
1369
 * @param str The string to capitalize.
1370
 * @returns
1371
 */
1372
const capitalize = (str: string) => {
3✔
1373
  if (str.length === 0) return str;
105!
1374
  return str[0]!.toUpperCase() + str.slice(1);
105✔
1375
};
105✔
1376

1377
/**
1378
 * Change the name of a function for better debugging experience.
1379
 * @param fn The function to rename.
1380
 * @param name The new name of the function.
1381
 * @returns
1382
 */
1383
const renameFunction = <F extends (...args: never) => unknown>(fn: F, name: string): F =>
3✔
1384
  Object.defineProperty(fn, "name", {
516✔
1385
    value: name,
516✔
1386
    writable: false,
516✔
1387
    enumerable: false,
516✔
1388
    configurable: true,
516✔
1389
  });
516✔
1390

1391
const buildErrorClass = (name: string) => {
3✔
1392
  const ErrorClass = class extends Error {
36✔
1393
    constructor(message?: string) {
36✔
1394
      super(message);
36✔
1395
    }
36✔
1396
  };
36✔
1397
  let errorName = capitalize(name);
36✔
1398
  if (!errorName.endsWith("Error") && !errorName.endsWith("error")) errorName += "Error";
36✔
1399
  Object.defineProperty(ErrorClass, "name", {
36✔
1400
    value: errorName,
36✔
1401
    writable: false,
36✔
1402
    enumerable: false,
36✔
1403
    configurable: true,
36✔
1404
  });
36✔
1405
  Object.defineProperty(ErrorClass.prototype, "name", {
36✔
1406
    value: errorName,
36✔
1407
    writable: true,
36✔
1408
    enumerable: false,
36✔
1409
    configurable: true,
36✔
1410
  });
36✔
1411
  return ErrorClass;
36✔
1412
};
36✔
1413

1414
const stringifyEffectName = (name: string | symbol | ((...args: never) => unknown)) =>
3✔
1415
  typeof name === "string" ? name
51✔
1416
  : typeof name === "symbol" ? name.toString()
9✔
1417
  : "[" + name.name + "]";
3✔
1418

1419
const stringifyEffectNameQuoted = (name: string | symbol | ((...args: never) => unknown)) =>
3✔
1420
  typeof name === "string" ? `"${name}"` : stringifyEffectName(name);
18✔
1421

1422
const stringifyEffect = (effect: Effect) =>
3✔
1423
  `${stringifyEffectName(effect.name)}(${effect.payloads.map(stringify).join(", ")})`;
45✔
1424

1425
/**
1426
 * Stringify an object, handling common cases that simple `JSON.stringify` does not handle, e.g.,
1427
 * `undefined`, `bigint`, `function`, `symbol`. Circular references are considered.
1428
 * @param x The object to stringify.
1429
 * @param space The number of spaces to use for indentation.
1430
 * @returns
1431
 */
1432
const stringify = (x: unknown, space: number = 0): string => {
3✔
1433
  const seen = new WeakSet();
72✔
1434
  const identifierRegex = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
72✔
1435

1436
  const indent = (level: number): string => (space > 0 ? " ".repeat(level * space) : "");
72✔
1437

1438
  const serialize = (value: unknown, level: number): string => {
72✔
1439
    if (typeof value === "bigint") return `${value}n`;
78!
1440
    if (typeof value === "function")
78✔
1441
      return value.name ? `[Function: ${value.name}]` : "[Function (anonymous)]";
78!
1442
    if (typeof value === "symbol") return value.toString();
78!
1443
    if (value === undefined) return "undefined";
78!
1444
    if (value === null) return "null";
78✔
1445

1446
    if (typeof value === "object") {
78✔
1447
      if (seen.has(value)) return "[Circular]";
6!
1448
      seen.add(value);
6✔
1449

1450
      const nextLevel = level + 1;
6✔
1451
      const isClassInstance =
6✔
1452
        value.constructor && value.constructor.name && value.constructor.name !== "Object";
6✔
1453
      const className = isClassInstance ? `${value.constructor.name} ` : "";
6!
1454

1455
      if (Array.isArray(value)) {
6!
1456
        const arrayItems = value
×
1457
          .map((item) => serialize(item, nextLevel))
×
1458
          .join(space > 0 ? `,\n${indent(nextLevel)}` : ", ");
×
1459
        let result = `[${space > 0 ? "\n" + indent(nextLevel) : ""}${arrayItems}${space > 0 ? "\n" + indent(level) : ""}]`;
×
1460
        if (className !== "Array ") result = `${className.trimEnd()}(${value.length}) ${result}`;
×
1461
        return result;
×
1462
      }
×
1463

1464
      const objectEntries = Reflect.ownKeys(value)
6✔
1465
        .map((key) => {
6✔
1466
          const keyDisplay =
6✔
1467
            typeof key === "symbol" ? `[${key.toString()}]`
6!
1468
            : identifierRegex.test(key) ? key
6!
1469
            : JSON.stringify(key);
×
1470
          const val = (value as Record<string, unknown>)[key as any];
6✔
1471
          return `${space > 0 ? indent(nextLevel) : ""}${keyDisplay}: ${serialize(val, nextLevel)}`;
6!
1472
        })
6✔
1473
        .join(space > 0 ? `,\n` : ", ");
6!
1474

1475
      return `${className}{${space > 0 ? "\n" : " "}${objectEntries}${space > 0 ? "\n" + indent(level) : " "}}`;
6!
1476
    }
6✔
1477

1478
    return JSON.stringify(value);
66✔
1479
  };
78✔
1480

1481
  return serialize(x, 0);
72✔
1482
};
72✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc