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

Snowflyt / tinyeffect / 11716108018

07 Nov 2024 03:31AM UTC coverage: 96.411%. Remained the same
11716108018

push

github

Snowflyt
📃 docs(test): Fix typo in test description

140 of 154 branches covered (90.91%)

Branch coverage included in aggregate %.

451 of 459 relevant lines covered (98.26%)

266.9 hits per line

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

96.19
/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 generator function yielding a single {@link Effect} instance (algebraic effect). Can
8
 * be used with {@link effected} to create an effected program.
9
 *
10
 * For special cases, see {@link error} (for non-resumable error effects) and {@link dependency}
11
 * (for dependency injection).
12
 * @param name Name of the effect. This identifier is used to match effects, so be careful with name
13
 * collisions.
14
 * @param options Options for the effect. Can be used to mark the effect as unresumable.
15
 * @returns
16
 *
17
 * @example
18
 * ```typescript
19
 * const println = effect("println")<unknown[], void>;
20
 * const raise = effect("raise", { resumable: false })<[message?: string], never>;
21
 * ```
22
 *
23
 * @example
24
 * ```typescript
25
 * // Provide more readable type information
26
 * type Println = Effect<"println", unknown[], void>;
27
 * const println: EffectFactory<Println> = effect("println");
28
 * type Raise = Effect<"raise", [message?: string], never>;
29
 * const raise: EffectFactory<Raise> = effect("raise", { resumable: false });
30
 * ```
31
 *
32
 * @see {@link effected}
33
 */
34
export function effect<Name extends string | symbol, Resumable extends boolean = true>(
3✔
35
  name: Name,
447✔
36
  options?: { readonly resumable?: Resumable },
447✔
37
): [Resumable] extends [false] ?
38
  <Payloads extends unknown[], R extends never = never>(
39
    ...payloads: Payloads
40
  ) => Generator<Unresumable<Effect<Name, Payloads, R>>, R, unknown>
41
: <Payloads extends unknown[], R>(
42
    ...payloads: Payloads
43
  ) => Generator<Effect<Name, Payloads, R>, R, unknown> {
447✔
44
  const result = function* (...payloads: unknown[]) {
447✔
45
    return (yield Object.assign(new Effect(name, payloads), options)) as never;
648✔
46
  };
648✔
47
  // Add a `name` property for better debugging experience
48
  Object.defineProperty(result, "name", { value: name });
447✔
49
  return result as never;
447✔
50
}
447✔
51

52
/**
53
 * Create a generator function yielding a single {@link Effect} instance for typical errors (i.e.,
54
 * non-resumable effect with name prefixed with "error:").
55
 *
56
 * It can be seen as an alias of `effect("error:" + name, { resumable: false })`.
57
 *
58
 * You can use {@link Effected#catch} as a shortcut for `terminate("error:" + name, ...)` to catch
59
 * the error effect.
60
 * @param name Name of the error effect. This identifier is used to match effects, so be careful
61
 * with name collisions.
62
 * @returns
63
 *
64
 * @example
65
 * ```typescript
66
 * const authError = error("auth");
67
 * const notFoundError = error("notFound");
68
 * ```
69
 *
70
 * @example
71
 * ```typescript
72
 * // Provide more readable type information
73
 * type AuthError = Effect.Error<"auth">;
74
 * const authError: EffectFactory<AuthError> = error("auth");
75
 * ```
76
 *
77
 * @see {@link effect}
78
 */
79
export function error<Name extends string>(name: Name) {
3✔
80
  const result = function* (
48✔
81
    ...payloads: [message?: string]
72✔
82
  ): Generator<Unresumable<Effect<`error:${Name}`, [message?: string], never>>, never, unknown> {
72✔
83
    return (yield Object.assign(new Effect(`error:${name}`, payloads), {
72✔
84
      resumable: false,
72✔
85
    }) as never) as never;
72✔
86
  };
72✔
87
  // Add a `name` property for better debugging experience
88
  Object.defineProperty(result, "name", { value: `throw${capitalize(name)}Error` });
48✔
89
  return result;
48✔
90
}
48✔
91

92
type ErrorName<E extends Effect> =
93
  E extends Unresumable<Effect<`error:${infer Name}`, any, never>> ? Name : never;
94

95
/**
96
 * Create a generator function yielding a single {@link Effect} instance for dependency injection.
97
 *
98
 * It can be seen as an alias of `effect("dependency:" + name)`.
99
 *
100
 * You can use {@link Effected#provide} and its variants as a shortcut for
101
 * `resume("dependency:" + name, ...)` to provide the value for the dependency.
102
 * @param name Name of the dependency. This identifier is used to match effects, so be careful
103
 * with name collisions.
104
 * @returns
105
 *
106
 * @example
107
 * ```typescript
108
 * const askConfig = dependency("config")<Config | null>;
109
 * const askDatabase = dependency("database")<Database>;
110
 * ```
111
 *
112
 * @example
113
 * ```typescript
114
 * // Provide more readable type information
115
 * type ConfigDependency = Effect.Dependency<"config", Config | null>;
116
 * const askConfig: EffectFactory<ConfigDependency> = dependency("config");
117
 * ```
118
 *
119
 * @see {@link effect}
120
 */
121
export function dependency<Name extends string>(name: Name) {
3✔
122
  const result = function* <R>(): Generator<Effect<`dependency:${Name}`, [], R>, R, unknown> {
21✔
123
    return (yield new Effect(`dependency:${name}`, [])) as never;
81✔
124
  };
81✔
125
  // Add a `name` property for better debugging experience
126
  Object.defineProperty(result, "name", { value: `ask${capitalize(name)}` });
21✔
127
  return result;
21✔
128
}
21✔
129

130
type DependencyName<E extends Effect> =
131
  E extends Effect<`dependency:${infer Name}`, [], unknown> ? Name : never;
132

133
/**
134
 * Define a handler that transforms an effected program into another one.
135
 *
136
 * It is just a simple wrapper to make TypeScript infer the types correctly, and simply returns the
137
 * function you pass to it.
138
 *
139
 * @example
140
 * ```typescript
141
 * type Raise = Unresumable<Effect<"raise", [error: unknown], never>>;
142
 * const raise: EffectFactory<Raise> = effect("raise", { resumable: false });
143
 *
144
 * const safeDivide = (a: number, b: number): Effected<Raise, number> =>
145
 *   effected(function* () {
146
 *     if (b === 0) return yield* raise("Division by zero");
147
 *     return a / b;
148
 *   });
149
 *
150
 * type Option<T> = { kind: "some"; value: T } | { kind: "none" };
151
 * const some = <T>(value: T): Option<T> => ({ kind: "some", value });
152
 * const none: Option<never> = { kind: "none" };
153
 *
154
 * const raiseOption = defineHandlerFor<Raise>().with((effected) =>
155
 *   effected.map((value) => some(value)).terminate("raise", () => none),
156
 * );
157
 *
158
 * const safeDivide2 = (a: number, b: number) => safeDivide(a, b).with(raiseOption);
159
 * //    ^?: (a: number, b: number) => Effected<never, Option<number>>
160
 * ```
161
 */
162
export function defineHandlerFor<E extends Effect, R>(): {
163
  with: <H extends (effected: EffectedDraft<E, E, R>) => EffectedDraft<E, Effect, unknown>>(
164
    handler: H,
165
  ) => H;
166
};
167
export function defineHandlerFor<E extends Effect>(): {
168
  with: <H extends <R>(effected: EffectedDraft<E, E, R>) => EffectedDraft<E, Effect, unknown>>(
169
    handler: H,
170
  ) => H;
171
};
172
export function defineHandlerFor() {
3✔
173
  return {
39✔
174
    with: (handler: any) => handler,
39✔
175
  };
39✔
176
}
39✔
177

178
/**
179
 * An effected program.
180
 */
181
export class Effected<out E extends Effect, out R> implements Iterable<E, R, unknown> {
3✔
182
  // @ts-expect-error - TS mistakenly think `[Symbol.iterator]` is not definitely assigned
183
  readonly [Symbol.iterator]: () => Iterator<E, R, unknown>;
1,197✔
184

185
  readonly runSync: [E] extends [never] ? () => R : UnhandledEffect<E>;
1,197✔
186
  readonly runAsync: [E] extends [never] ? () => Promise<R> : UnhandledEffect<E>;
1,197✔
187
  readonly runSyncUnsafe: () => R;
1,197✔
188
  readonly runAsyncUnsafe: () => Promise<R>;
1,197✔
189

190
  private constructor(fn: () => Iterator<E, R, unknown>, magicWords?: string) {
1,197✔
191
    if (magicWords !== "Yes, I’m sure I want to call the constructor of Effected directly.")
1,197✔
192
      console.warn(
1,197✔
193
        "You should not call the constructor of `Effected` directly. Use `effected` instead.",
3✔
194
      );
3✔
195

196
    this[Symbol.iterator] = fn;
1,197✔
197

198
    this.runSync = (() => runSync(this as never)) as never;
1,197✔
199
    this.runAsync = (() => runAsync(this as never)) as never;
1,197✔
200
    this.runSyncUnsafe = () => runSync(this as never);
1,197✔
201
    this.runAsyncUnsafe = () => runAsync(this as never);
1,197✔
202
  }
1,197✔
203

204
  /**
205
   * Handle an effect with a handler.
206
   *
207
   * For more common use cases, see {@link resume} and {@link terminate}, which provide a more
208
   * concise syntax.
209
   * @param effect The effect name or a function to match the effect name.
210
   * @param handler The handler for the effect. The first argument is an object containing the
211
   * encountered effect instance, a `resume` function to resume the effect, and a `terminate`
212
   * function to terminate the effect. The rest of the arguments are the payloads of the effect.
213
   *
214
   * `resume` or `terminate` should be called exactly once in the handler. If you call them more
215
   * than once, a warning will be logged to the console. If neither of them is called, the effected
216
   * program will hang indefinitely.
217
   *
218
   * Calling `resume` or `terminate` in an asynchronous context is also supported. It is _not_
219
   * required to call them synchronously.
220
   * @returns
221
   */
222
  handle<Name extends E["name"], T = R, F extends Effect = never>(
223
    effect: Name,
224
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
225
      (
226
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
227
        ...payloads: Payloads
228
      ) => Effected<F, void>
229
    : E extends Effect<Name, infer Payloads, infer R> ?
230
      (
231
        {
232
          effect,
233
          resume,
234
          terminate,
235
        }: {
236
          effect: Extract<E, Effect<Name>>;
237
          resume: (value: R) => void;
238
          terminate: (value: T) => void;
239
        },
240
        ...payloads: Payloads
241
      ) => Effected<F, void>
242
    : never,
243
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
244
  handle<Name extends string | symbol, T = R, F extends Effect = never>(
245
    effect: (name: E["name"]) => name is Name,
246
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
247
      (
248
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
249
        ...payloads: Payloads
250
      ) => Effected<F, void>
251
    : E extends Effect<Name, infer Payloads, infer R> ?
252
      (
253
        {
254
          effect,
255
          resume,
256
          terminate,
257
        }: {
258
          effect: Extract<E, Effect<Name>>;
259
          resume: (value: R) => void;
260
          terminate: (value: T) => void;
261
        },
262
        ...payloads: Payloads
263
      ) => Effected<F, void>
264
    : never,
265
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
266
  handle<Name extends E["name"], T = R, F extends Effect = never>(
267
    effect: Name,
268
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
269
      (
270
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
271
        ...payloads: Payloads
272
      ) => Generator<F, void, unknown>
273
    : E extends Effect<Name, infer Payloads, infer R> ?
274
      (
275
        {
276
          effect,
277
          resume,
278
          terminate,
279
        }: {
280
          effect: Extract<E, Effect<Name>>;
281
          resume: (value: R) => void;
282
          terminate: (value: T) => void;
283
        },
284
        ...payloads: Payloads
285
      ) => Generator<F, void, unknown>
286
    : never,
287
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
288
  handle<Name extends string | symbol, T = R, F extends Effect = never>(
289
    effect: (name: E["name"]) => name is Name,
290
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
291
      (
292
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
293
        ...payloads: Payloads
294
      ) => Generator<F, void, unknown>
295
    : E extends Effect<Name, infer Payloads, infer R> ?
296
      (
297
        {
298
          effect,
299
          resume,
300
          terminate,
301
        }: {
302
          effect: Extract<E, Effect<Name>>;
303
          resume: (value: R) => void;
304
          terminate: (value: T) => void;
305
        },
306
        ...payloads: Payloads
307
      ) => Generator<F, void, unknown>
308
    : never,
309
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
310
  handle<Name extends E["name"], T = R>(
311
    effect: Name,
312
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
313
      (
314
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
315
        ...payloads: Payloads
316
      ) => void
317
    : E extends Effect<Name, infer Payloads, infer R> ?
318
      (
319
        {
320
          effect,
321
          resume,
322
          terminate,
323
        }: {
324
          effect: Extract<E, Effect<Name>>;
325
          resume: (value: R) => void;
326
          terminate: (value: T) => void;
327
        },
328
        ...payloads: Payloads
329
      ) => void
330
    : never,
331
  ): Effected<Exclude<E, Effect<Name>>, R | T>;
332
  handle<Name extends string | symbol, T = R>(
333
    effect: (name: E["name"]) => name is Name,
334
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
335
      (
336
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
337
        ...payloads: Payloads
338
      ) => void
339
    : E extends Effect<Name, infer Payloads, infer R> ?
340
      (
341
        {
342
          effect,
343
          resume,
344
          terminate,
345
        }: {
346
          effect: Extract<E, Effect<Name>>;
347
          resume: (value: R) => void;
348
          terminate: (value: T) => void;
349
        },
350
        ...payloads: Payloads
351
      ) => void
352
    : never,
353
  ): Effected<Exclude<E, Effect<Name>>, R | T>;
354
  handle(
1,197✔
355
    name: string | symbol | ((name: string | symbol) => boolean),
543✔
356
    handler: (...args: any[]) => unknown,
543✔
357
  ): Effected<any, unknown> {
543✔
358
    const matchEffect = (value: unknown) =>
543✔
359
      value instanceof Effect &&
1,722✔
360
      (typeof name === "function" ? name(value.name) : value.name === name);
1,119✔
361

362
    return effected(() => {
543✔
363
      const iterator = this[Symbol.iterator]();
633✔
364
      let terminated: false | "with-value" | "without-value" = false;
633✔
365
      let terminatedValue: unknown;
633✔
366

367
      return {
633✔
368
        next: (...args: [] | [unknown]) => {
633✔
369
          if (terminated)
2,244✔
370
            return {
2,244✔
371
              done: true,
87✔
372
              ...(terminated === "with-value" ? { value: terminatedValue } : {}),
87✔
373
            } as IteratorReturnResult<unknown>;
87✔
374

375
          const result = iterator.next(...args);
2,157✔
376

377
          const { done, value } = result;
2,157✔
378
          if (done) return result;
2,244✔
379

380
          if (matchEffect(value)) {
2,244✔
381
            const effect = value;
765✔
382

383
            let resumed: false | "with-value" | "without-value" = false;
765✔
384
            let resumedValue: R;
765✔
385
            let onComplete: ((...args: [] | [R]) => void) | null = null;
765✔
386
            const warnMultipleHandling = (type: "resume" | "terminate", ...args: [] | [R]) => {
765✔
387
              let message = `Effect ${stringifyEffectNameQuoted(name)} has been handled multiple times`;
18✔
388
              message += " (received `";
18✔
389
              message += `${type} ${stringifyEffect(effect)}`;
18✔
390
              if (args.length > 0) message += ` with ${stringify(args[0])}`;
18✔
391
              message += "` after it has been ";
18✔
392
              if (resumed) {
18✔
393
                message += "resumed";
12✔
394
                if (resumed === "with-value") message += ` with ${stringify(resumedValue)}`;
12✔
395
              } else if (terminated) {
18✔
396
                message += "terminated";
6✔
397
                if (terminated === "with-value") message += ` with ${stringify(terminatedValue)}`;
6✔
398
              }
6✔
399
              message += "). Only the first handler will be used.";
18✔
400
              console.warn(message);
18✔
401
            };
18✔
402
            const resume = (...args: [] | [R]) => {
765✔
403
              if (resumed || terminated) {
633✔
404
                warnMultipleHandling("resume", ...args);
12✔
405
                return;
12✔
406
              }
12✔
407
              resumed = args.length > 0 ? "with-value" : "without-value";
633✔
408
              if (args.length > 0) resumedValue = args[0]!;
633✔
409
              if (onComplete) {
633✔
410
                onComplete(...args);
21✔
411
                onComplete = null;
21✔
412
              }
21✔
413
            };
633✔
414
            const terminate = (...args: [] | [R]) => {
765✔
415
              if (resumed || terminated) {
93✔
416
                warnMultipleHandling("terminate", ...args);
6✔
417
                return;
6✔
418
              }
6✔
419
              terminated = args.length > 0 ? "with-value" : "without-value";
93✔
420
              if (args.length > 0) terminatedValue = args[0];
93✔
421
              if (onComplete) {
93✔
422
                onComplete(...args);
6✔
423
                onComplete = null;
6✔
424
              }
6✔
425
            };
93✔
426

427
            const constructHandledEffect = ():
765✔
428
              | { _effectSync: true; value?: unknown }
429
              | {
430
                  _effectAsync: true;
431
                  onComplete: (callback: (...args: [] | [R]) => void) => void;
432
                } => {
708✔
433
              // For synchronous effects
434
              if (resumed || terminated)
708✔
435
                return {
708✔
436
                  _effectSync: true,
678✔
437
                  ...(Object.is(resumed, "with-value") ? { value: resumedValue! }
678✔
438
                  : Object.is(terminated, "with-value") ? { value: terminatedValue! }
93✔
439
                  : {}),
18✔
440
                };
678✔
441
              // For asynchronous effects
442
              return {
30✔
443
                _effectAsync: true,
30✔
444
                onComplete: (callback) => {
30✔
445
                  onComplete = callback;
27✔
446
                },
27✔
447
              };
30✔
448
            };
708✔
449

450
            const handlerResult = handler(
765✔
451
              {
765✔
452
                effect,
765✔
453
                resume:
765✔
454
                  (effect as any).resumable === false ?
765✔
455
                    () => {
87✔
456
                      throw new Error(
9✔
457
                        `Cannot resume non-resumable effect: ${stringifyEffect(effect)}`,
9✔
458
                      );
9✔
459
                    }
9✔
460
                  : resume,
678✔
461
                terminate,
765✔
462
              },
765✔
463
              ...effect.payloads,
765✔
464
            );
765✔
465

466
            if (!(handlerResult instanceof Effected) && !isGenerator(handlerResult))
765✔
467
              return { done: false, value: constructHandledEffect() } as never;
765✔
468

469
            const it = handlerResult[Symbol.iterator]();
57✔
470
            const next = iterator.next.bind(iterator);
57✔
471
            iterator.next = (...args: [] | [unknown]) => {
57✔
472
              const result = it.next(...args);
108✔
473
              if (result.done) {
108✔
474
                iterator.next = next;
51✔
475
                return { done: false, value: constructHandledEffect() } as never;
51✔
476
              }
51✔
477
              return result as never;
57✔
478
            };
108✔
479
            return iterator.next();
57✔
480
          }
57✔
481

482
          return result;
957✔
483
        },
2,244✔
484
      };
633✔
485
    });
543✔
486
  }
543✔
487

488
  /**
489
   * Resume an effect with the return value of the handler.
490
   *
491
   * It is a shortcut for
492
   * `handle(effect, ({ resume }, ...payloads) => resume(handler(...payloads)))`.
493
   * @param effect The effect name or a function to match the effect name.
494
   * @param handler The handler for the effect. The arguments are the payloads of the effect.
495
   * @returns
496
   *
497
   * @see {@link handle}
498
   */
499
  resume<Name extends Exclude<E, Unresumable<Effect>>["name"], F extends Effect = never>(
500
    effect: Name,
501
    handler: E extends Effect<Name, infer Payloads, infer R> ?
502
      (...payloads: Payloads) => Effected<F, R>
503
    : never,
504
  ): Effected<Exclude<E, Effect<Name>> | F, R>;
505
  resume<Name extends string | symbol, F extends Effect = never>(
506
    effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
507
    handler: E extends Effect<Name, infer Payloads, infer R> ?
508
      (...payloads: Payloads) => Effected<F, R>
509
    : never,
510
  ): Effected<Exclude<E, Effect<Name>> | F, R>;
511
  resume<Name extends Exclude<E, Unresumable<Effect>>["name"], F extends Effect = never>(
512
    effect: Name,
513
    handler: E extends Effect<Name, infer Payloads, infer R> ?
514
      (...payloads: Payloads) => Generator<F, R, unknown>
515
    : never,
516
  ): Effected<Exclude<E, Effect<Name>> | F, R>;
517
  resume<Name extends string | symbol, F extends Effect = never>(
518
    effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
519
    handler: E extends Effect<Name, infer Payloads, infer R> ?
520
      (...payloads: Payloads) => Generator<F, R, unknown>
521
    : never,
522
  ): Effected<Exclude<E, Effect<Name>> | F, R>;
523
  resume<Name extends Exclude<E, Unresumable<Effect>>["name"]>(
524
    effect: Name,
525
    handler: E extends Effect<Name, infer Payloads, infer R> ? (...payloads: Payloads) => R : never,
526
  ): Effected<Exclude<E, Effect<Name>>, R>;
527
  resume<Name extends string | symbol>(
528
    effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
529
    handler: E extends Effect<Name, infer Payloads, infer R> ? (...payloads: Payloads) => R : never,
530
  ): Effected<Exclude<E, Effect<Name>>, R>;
531
  resume(effect: any, handler: (...payloads: unknown[]) => unknown) {
1,197✔
532
    return this.handle(effect, (({ resume }: any, ...payloads: unknown[]) => {
267✔
533
      const it = handler(...payloads);
567✔
534
      if (!(it instanceof Effected) && !isGenerator(it)) return resume(it);
567✔
535
      return (function* () {
42✔
536
        resume(yield* it);
42✔
537
      })();
42✔
538
    }) as never);
267✔
539
  }
267✔
540

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

592
  /**
593
   * Map the return value of the effected program.
594
   * @param handler The function to map the return value.
595
   * @returns
596
   */
597
  map<S, F extends Effect = never>(handler: (value: R) => Effected<F, S>): Effected<E | F, S>;
598
  map<S, F extends Effect = never>(
599
    handler: (value: R) => Generator<F, S, unknown>,
600
  ): Effected<E | F, S>;
601
  map<S>(handler: (value: R) => S): Effected<E, S>;
602
  map(handler: (value: R) => unknown): Effected<E, unknown> {
1,197✔
603
    const iterator = this[Symbol.iterator]();
108✔
604

605
    return effected(() => {
108✔
606
      let originalIteratorDone = false;
108✔
607
      let appendedIterator: Iterator<E, unknown, unknown>;
108✔
608
      return {
108✔
609
        next: (...args: [] | [R]) => {
108✔
610
          if (originalIteratorDone) return appendedIterator.next(...args);
234✔
611
          const result = iterator.next(...args);
228✔
612
          if (!result.done) return result;
234✔
613
          originalIteratorDone = true;
54✔
614
          const it = handler(result.value);
54✔
615
          if (!(it instanceof Effected) && !isGenerator(it)) return { done: true, value: it };
234✔
616
          appendedIterator = it[Symbol.iterator]();
6✔
617
          return appendedIterator.next();
6✔
618
        },
234✔
619
      };
108✔
620
    });
108✔
621
  }
108✔
622

623
  /**
624
   * Catch an error effect with a handler.
625
   *
626
   * It is a shortcut for `terminate("error:" + name, handler)`.
627
   * @param name The name of the error effect.
628
   * @param handler The handler for the error effect. The argument is the message of the error.
629
   * @returns
630
   *
631
   * @see {@link terminate}
632
   */
633
  catch<Name extends ErrorName<E>, T, F extends Effect = never>(
634
    effect: Name,
635
    handler: (message?: string) => Effected<F, T>,
636
  ): Effected<Exclude<E, Effect.Error<Name>> | F, R | T>;
637
  catch<Name extends ErrorName<E>, T, F extends Effect = never>(
638
    effect: Name,
639
    handler: (message?: string) => Generator<F, T, unknown>,
640
  ): Effected<Exclude<E, Effect.Error<Name>> | F, R | T>;
641
  catch<Name extends ErrorName<E>, T, F extends Effect = never>(
642
    effect: Name,
643
    handler: (message?: string) => Generator<F, T, unknown>,
644
  ): Effected<Exclude<E, Effect.Error<Name>> | F, R | T>;
645
  catch<Name extends ErrorName<E>, T>(
646
    effect: Name,
647
    handler: (message?: string) => T,
648
  ): Effected<Exclude<E, Effect.Error<Name>>, R | T>;
649
  catch(name: string, handler: (message?: string) => unknown): Effected<Effect, unknown> {
1,197✔
650
    return this.terminate(`error:${name}` as never, handler as never);
81✔
651
  }
81✔
652

653
  /**
654
   * Catch all error effects with a handler.
655
   * @param handler The handler for the error effect. The first argument is the name of the error
656
   * effect (without the `"error:"` prefix), and the second argument is the message of the error.
657
   */
658
  catchAll<T, F extends Effect = never>(
659
    handler: (error: ErrorName<E>, message?: string) => Effected<F, T>,
660
  ): Effected<Exclude<E, Effect.Error> | F, R | T>;
661
  catchAll<T, F extends Effect = never>(
662
    handler: (error: ErrorName<E>, message?: string) => Generator<F, T, unknown>,
663
  ): Effected<Exclude<E, Effect.Error> | F, R | T>;
664
  catchAll<T>(
665
    handler: (error: ErrorName<E>, message?: string) => T,
666
  ): Effected<Exclude<E, Effect.Error>, R | T>;
667
  catchAll(handler: (error: ErrorName<E>, message?: string) => unknown): Effected<Effect, unknown> {
1,197✔
668
    return this.handle(
36✔
669
      (name): name is ErrorName<E> => typeof name === "string" && name.startsWith("error:"),
36✔
670
      (({ effect, terminate }: any, ...payloads: [message?: string]) => {
36✔
671
        const error = effect.name.slice(6) as ErrorName<E>;
33✔
672
        const it = handler(error, ...payloads);
33✔
673
        if (!(it instanceof Effected) && !isGenerator(it)) return terminate(it);
33✔
674
        return (function* () {
3✔
675
          terminate(yield* it);
3✔
676
        })();
3✔
677
      }) as never,
33✔
678
    );
36✔
679
  }
36✔
680

681
  /**
682
   * Catch an error effect and throw it as an error.
683
   * @param name The name of the error effect.
684
   * @param message The message of the error. If it is a function, it will be called with the
685
   * message of the error effect, and the return value will be used as the message of the error.
686
   * @returns
687
   *
688
   * @since 0.1.1
689
   */
690
  catchAndThrow<Name extends ErrorName<E>>(
1,197✔
691
    name: Name,
18✔
692
    message?: string | ((message?: string) => string | undefined),
18✔
693
  ): Effected<Exclude<E, Effect.Error<Name>>, R> {
18✔
694
    return this.catch(name, (...args) => {
18✔
695
      throw new (buildErrorClass(name))(
18✔
696
        ...(typeof message === "string" ? [message]
18✔
697
        : typeof message === "function" ? [message(...args)].filter((v) => v !== undefined)
12✔
698
        : args),
6✔
699
      );
18✔
700
    });
18✔
701
  }
18✔
702

703
  /**
704
   * Catch all error effects and throw them as an error.
705
   * @param message The message of the error. If it is a function, it will be called with the name
706
   * and the message of the error effect, and the return value will be used as the message of the
707
   * error.
708
   * @returns
709
   *
710
   * @since 0.1.1
711
   */
712
  catchAllAndThrow(
1,197✔
713
    message?: string | ((error: string, message?: string) => string | undefined),
18✔
714
  ): Effected<Exclude<E, Effect.Error>, R> {
18✔
715
    return this.catchAll((error, ...args) => {
18✔
716
      throw new (buildErrorClass(error))(
18✔
717
        ...(typeof message === "string" ? [message]
18✔
718
        : typeof message === "function" ? [message(error, ...args)].filter((v) => v !== undefined)
12✔
719
        : args),
6✔
720
      );
18✔
721
    });
18✔
722
  }
18✔
723

724
  /**
725
   * Provide a value for a dependency effect.
726
   * @param name The name of the dependency.
727
   * @param value The value to provide for the dependency.
728
   * @returns
729
   */
730
  provide<Name extends DependencyName<E>>(
1,197✔
731
    name: Name,
18✔
732
    value: E extends Effect.Dependency<Name, infer R> ? R : never,
18✔
733
  ): Effected<Exclude<E, Effect.Dependency<Name>>, R> {
18✔
734
    return this.resume(`dependency:${name}` as never, (() => value) as never) as never;
18✔
735
  }
18✔
736

737
  /**
738
   * Provide a value for a dependency effect with a getter.
739
   * @param name The name of the dependency.
740
   * @param getter The getter to provide for the dependency.
741
   * @returns
742
   */
743
  provideBy<Name extends DependencyName<E>, F extends Effect = never>(
744
    name: Name,
745
    getter: E extends Effect.Dependency<Name, infer R> ? () => Effected<F, R> : never,
746
  ): Effected<Exclude<E, Effect.Dependency<Name>> | F, R>;
747
  provideBy<Name extends DependencyName<E>, F extends Effect = never>(
748
    name: Name,
749
    getter: E extends Effect.Dependency<Name, infer R> ? () => Generator<F, R, unknown> : never,
750
  ): Effected<Exclude<E, Effect.Dependency<Name>> | F, R>;
751
  provideBy<Name extends DependencyName<E>>(
752
    name: Name,
753
    getter: E extends Effect.Dependency<Name, infer R> ? () => R : never,
754
  ): Effected<Exclude<E, Effect.Dependency<Name>>, R>;
755
  provideBy(name: string, getter: () => unknown): Effected<Effect, unknown> {
1,197✔
756
    return this.resume(`dependency:${name}`, getter as never);
21✔
757
  }
21✔
758

759
  /**
760
   * Apply a handler to the effected program.
761
   * @param handler The handler to apply to the effected program.
762
   * @returns
763
   */
764
  with<F extends Effect, G extends Effect, S>(
765
    handler: (effected: EffectedDraft<never, never, R>) => EffectedDraft<F, G, S>,
766
  ): Effected<Exclude<E, F> | G, S>;
767
  with<F extends Effect, S>(handler: (effected: Effected<E, R>) => Effected<F, S>): Effected<F, S>;
768
  with(handler: (effected: any) => unknown) {
1,197✔
769
    return handler(this);
81✔
770
  }
81✔
771
}
1,197✔
772

773
interface EffectedDraft<
774
  out P extends Effect = Effect,
775
  out E extends Effect = Effect,
776
  out R = unknown,
777
> extends Iterable<E, R, unknown> {
778
  readonly handle: {
779
    <Name extends E["name"], T = R, F extends Effect = never>(
780
      effect: Name,
781
      handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
782
        (
783
          {
784
            effect,
785
            terminate,
786
          }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
787
          ...payloads: Payloads
788
        ) => Effected<F, void>
789
      : E extends Effect<Name, infer Payloads, infer R> ?
790
        (
791
          {
792
            effect,
793
            resume,
794
            terminate,
795
          }: {
796
            effect: Extract<E, Effect<Name>>;
797
            resume: (value: R) => void;
798
            terminate: (value: T) => void;
799
          },
800
          ...payloads: Payloads
801
        ) => Effected<F, void>
802
      : never,
803
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
804
    <Name extends string | symbol, T = R, F extends Effect = never>(
805
      effect: (name: E["name"]) => name is Name,
806
      handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
807
        (
808
          {
809
            effect,
810
            terminate,
811
          }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
812
          ...payloads: Payloads
813
        ) => Effected<F, void>
814
      : E extends Effect<Name, infer Payloads, infer R> ?
815
        (
816
          {
817
            effect,
818
            resume,
819
            terminate,
820
          }: {
821
            effect: Extract<E, Effect<Name>>;
822
            resume: (value: R) => void;
823
            terminate: (value: T) => void;
824
          },
825
          ...payloads: Payloads
826
        ) => Effected<F, void>
827
      : never,
828
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
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
        ) => Generator<F, void, unknown>
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
        ) => Generator<F, void, unknown>
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
        ) => Generator<F, void, unknown>
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
        ) => Generator<F, void, unknown>
877
      : never,
878
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
879
    <Name extends E["name"], T = R>(
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
        ) => void
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
        ) => void
902
      : never,
903
    ): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
904
    <Name extends string | symbol, T = R>(
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
        ) => void
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
        ) => void
927
      : never,
928
    ): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
929
  };
930
  readonly resume: {
931
    <Name extends Exclude<E, Unresumable<Effect>>["name"], F extends Effect = never>(
932
      effect: Name,
933
      handler: E extends Effect<Name, infer Payloads, infer R> ?
934
        (...payloads: Payloads) => Effected<F, R>
935
      : never,
936
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R>;
937
    <Name extends string | symbol, F extends Effect = never>(
938
      effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
939
      handler: E extends Effect<Name, infer Payloads, infer R> ?
940
        (...payloads: Payloads) => Effected<F, R>
941
      : never,
942
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R>;
943
    <Name extends Exclude<E, Unresumable<Effect>>["name"], F extends Effect = never>(
944
      effect: Name,
945
      handler: E extends Effect<Name, infer Payloads, infer R> ?
946
        (...payloads: Payloads) => Generator<F, R, unknown>
947
      : never,
948
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R>;
949
    <Name extends string | symbol, F extends Effect = never>(
950
      effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
951
      handler: E extends Effect<Name, infer Payloads, infer R> ?
952
        (...payloads: Payloads) => Generator<F, R, unknown>
953
      : never,
954
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R>;
955
    <Name extends Exclude<E, Unresumable<Effect>>["name"]>(
956
      effect: Name,
957
      handler: E extends Effect<Name, infer Payloads, infer R> ? (...payloads: Payloads) => R
958
      : never,
959
    ): EffectedDraft<P, Exclude<E, Effect<Name>>, R>;
960
    <Name extends string | symbol>(
961
      effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
962
      handler: E extends Effect<Name, infer Payloads, infer R> ? (...payloads: Payloads) => R
963
      : never,
964
    ): EffectedDraft<P, Exclude<E, Effect<Name>>, R>;
965
  };
966
  readonly terminate: {
967
    <Name extends E["name"], T, F extends Effect = never>(
968
      effect: Name,
969
      handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => Effected<F, T>
970
      : never,
971
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
972
    <Name extends string | symbol, T, F extends Effect = never>(
973
      effect: (name: E["name"]) => name is Name,
974
      handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => Effected<F, T>
975
      : never,
976
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
977
    <Name extends E["name"], T, F extends Effect = never>(
978
      effect: Name,
979
      handler: E extends Effect<Name, infer Payloads> ?
980
        (...payloads: Payloads) => Generator<F, T, unknown>
981
      : never,
982
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
983
    <Name extends E["name"], T, F extends Effect = never>(
984
      effect: Name,
985
      handler: E extends Effect<Name, infer Payloads> ?
986
        (...payloads: Payloads) => Generator<F, T, unknown>
987
      : never,
988
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
989
    <Name extends string | symbol, T, F extends Effect = never>(
990
      effect: (name: E["name"]) => name is Name,
991
      handler: E extends Effect<Name, infer Payloads> ?
992
        (...payloads: Payloads) => Generator<F, T, unknown>
993
      : never,
994
    ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
995
    <Name extends E["name"], T>(
996
      effect: Name,
997
      handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
998
    ): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
999
    <Name extends string | symbol, T>(
1000
      effect: (name: E["name"]) => name is Name,
1001
      handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
1002
    ): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
1003
  };
1004

1005
  readonly map: {
1006
    <S, F extends Effect = never>(
1007
      handler: (value: R) => Effected<F, S>,
1008
    ): EffectedDraft<P, E | F, S>;
1009
    <S, F extends Effect = never>(
1010
      handler: (value: R) => Generator<F, S, unknown>,
1011
    ): EffectedDraft<P, E | F, S>;
1012
    <S>(handler: (value: R) => S): EffectedDraft<P, E, S>;
1013
  };
1014

1015
  readonly catch: {
1016
    <Name extends ErrorName<E>, T, F extends Effect = never>(
1017
      effect: Name,
1018
      handler: (message?: string) => Effected<F, T>,
1019
    ): EffectedDraft<P, Exclude<E, Effect.Error<Name>> | F, R | T>;
1020
    <Name extends ErrorName<E>, T, F extends Effect = never>(
1021
      effect: Name,
1022
      handler: (message?: string) => Generator<F, T, unknown>,
1023
    ): EffectedDraft<P, Exclude<E, Effect.Error<Name>> | F, R | T>;
1024
    <Name extends ErrorName<E>, T>(
1025
      effect: Name,
1026
      handler: (message?: string) => T,
1027
    ): EffectedDraft<P, Exclude<E, Effect.Error<Name>>, R | T>;
1028
  };
1029

1030
  readonly catchAll: {
1031
    <T, F extends Effect = never>(
1032
      handler: (effect: ErrorName<E>, message?: string) => Effected<F, T>,
1033
    ): Effected<Exclude<E, Effect.Error> | F, R | T>;
1034
    <T, F extends Effect = never>(
1035
      handler: (effect: ErrorName<E>, message?: string) => Generator<F, T, unknown>,
1036
    ): Effected<Exclude<E, Effect.Error> | F, R | T>;
1037
    <T>(
1038
      handler: (effect: ErrorName<E>, message?: string) => T,
1039
    ): Effected<Exclude<E, Effect.Error>, R | T>;
1040
  };
1041

1042
  readonly catchAndThrow: <Name extends ErrorName<E>>(
1043
    name: Name,
1044
    message?: string | ((message?: string) => string | undefined),
1045
  ) => Effected<Exclude<E, Effect.Error<Name>>, R>;
1046

1047
  readonly catchAllAndThrow: (
1048
    message?: string | ((error: string, message?: string) => string | undefined),
1049
  ) => Effected<Exclude<E, Effect.Error>, R>;
1050

1051
  readonly provide: <Name extends DependencyName<E>>(
1052
    name: Name,
1053
    value: E extends Effect.Dependency<Name, infer R> ? R : never,
1054
  ) => EffectedDraft<P, Exclude<E, Effect.Dependency<Name>>, R>;
1055
  readonly provideBy: {
1056
    <Name extends DependencyName<E>, F extends Effect = never>(
1057
      name: Name,
1058
      getter: E extends Effect.Dependency<Name, infer R> ? () => Effected<F, R> : never,
1059
    ): EffectedDraft<P, Exclude<E, Effect.Dependency<Name>> | F, R>;
1060
    <Name extends DependencyName<E>, F extends Effect = never>(
1061
      name: Name,
1062
      getter: E extends Effect.Dependency<Name, infer R> ? () => Generator<F, R, unknown> : never,
1063
    ): EffectedDraft<P, Exclude<E, Effect.Dependency<Name>> | F, R>;
1064
    <Name extends DependencyName<E>>(
1065
      name: Name,
1066
      getter: E extends Effect.Dependency<Name, infer R> ? () => R : never,
1067
    ): EffectedDraft<P, Exclude<E, Effect.Dependency<Name>>, R>;
1068
  };
1069

1070
  readonly with: {
1071
    <F extends Effect, G extends Effect, S>(
1072
      handler: (effected: EffectedDraft<never, never, R>) => EffectedDraft<F, G, S>,
1073
    ): EffectedDraft<P, Exclude<E, F> | G, S>;
1074
    <F extends Effect, S>(
1075
      handler: (effected: Effected<E, R>) => Effected<F, S>,
1076
    ): EffectedDraft<P, F, S>;
1077
  };
1078
}
1079

1080
/**
1081
 * Create an effected program.
1082
 * @param fn A function that returns an iterator.
1083
 * @returns
1084
 *
1085
 * @example
1086
 * ```typescript
1087
 * type User = { id: number; name: string; role: "admin" | "user" };
1088
 *
1089
 * // Use `effect` and its variants to define factory functions for effects
1090
 * const println = effect("println")<unknown[], void>;
1091
 * const executeSQL = effect("executeSQL")<[sql: string, ...params: unknown[]], any>;
1092
 * const askCurrentUser = dependency("currentUser")<User | null>;
1093
 * const authenticationError = error("authentication");
1094
 * const unauthorizedError = error("unauthorized");
1095
 *
1096
 * // Use `effected` to define an effected program
1097
 * const requiresAdmin = () => effected(function* () {
1098
 *   const currentUser = yield* askCurrentUser();
1099
 *   if (!currentUser) return yield* authenticationError();
1100
 *   if (currentUser.role !== "admin")
1101
 *     return yield* unauthorizedError(`User "${currentUser.name}" is not an admin`);
1102
 * });
1103
 *
1104
 * // You can yield other effected programs in an effected program
1105
 * const createUser = (user: Omit<User, "id">) => effected(function* () {
1106
 *   yield* requiresAdmin();
1107
 *   const id = yield* executeSQL("INSERT INTO users (name) VALUES (?)", user.name);
1108
 *   const savedUser: User = { id, ...user };
1109
 *   yield* println("User created:", savedUser);
1110
 *   return savedUser;
1111
 * });
1112
 *
1113
 * const program = effected(function* () {
1114
 *   yield* createUser({ name: "Alice", role: "user" });
1115
 *   yield* createUser({ name: "Bob", role: "admin" });
1116
 * })
1117
 *   // Handle effects with the `.handle()` method
1118
 *   .handle("executeSQL", function* ({ resume, terminate }, sql, ...params) {
1119
 *     // You can yield other effects in a handler using a generator function
1120
 *     yield* println("Executing SQL:", sql, ...params);
1121
 *     // Asynchronous effects are supported
1122
 *     db.execute(sql, params, (err, result) => {
1123
 *       if (err) return terminate(err);
1124
 *       resume(result);
1125
 *     });
1126
 *   })
1127
 *   // a shortcut for `.handle()` that resumes the effect with the return value of the handler
1128
 *   .resume("println", (...args) => console.log(...args))
1129
 *   // Other shortcuts for special effects (error effects and dependency effects)
1130
 *   .provide("currentUser", { id: 1, name: "Charlie", role: "admin" })
1131
 *   .catch("authentication", () => console.error("Authentication error"));
1132
 *   .catch("unauthorized", () => console.error("Unauthorized error"));
1133
 *
1134
 * // Run the effected program with `.runSync()` or `.runAsync()`
1135
 * await program.runAsync();
1136
 * ```
1137
 *
1138
 * @see {@link effect}
1139
 */
1140
export function effected<E extends Effect, R>(fn: () => Iterator<E, R, unknown>): Effected<E, R> {
3✔
1141
  return new (Effected as any)(
1,194✔
1142
    fn,
1,194✔
1143
    "Yes, I’m sure I want to call the constructor of Effected directly.",
1,194✔
1144
  );
1,194✔
1145
}
1,194✔
1146

1147
/**
1148
 * Convert a {@link Promise} to a generator containing a single {@link Effect} that can be used in
1149
 * an effected program.
1150
 * @param promise The promise to effectify.
1151
 * @returns
1152
 *
1153
 * ```typescript
1154
 * // Assume we have `db.user.create(user: User): Promise<number>`
1155
 * const createUser = (user: Omit<User, "id">) => effected(function* () {
1156
 *   yield* requiresAdmin();
1157
 *   // Use `yield* effectify(...)` instead of `await ...` in an effected program
1158
 *   const id = yield* effectify(db.user.create(user));
1159
 *   const savedUser = { id, ...user };
1160
 *   yield* println("User created:", savedUser);
1161
 *   return savedUser;
1162
 * });
1163
 * ```
1164
 */
1165
export function* effectify<T>(promise: Promise<T>): Generator<never, T, unknown> {
3✔
1166
  return (yield {
24✔
1167
    _effectAsync: true,
24✔
1168
    onComplete: (...args: [onComplete: (value: T) => void, onThrow?: (value: unknown) => void]) =>
24✔
1169
      promise.then(...args),
21✔
1170
  } as never) as never;
24✔
1171
}
24✔
1172

1173
/**
1174
 * Run an effected program synchronously and return its result.
1175
 * @param effected The effected program.
1176
 * @returns
1177
 *
1178
 * @throws {UnhandledEffectError} If an unhandled effect is encountered.
1179
 * @throws {Error} If an asynchronous effect is encountered.
1180
 */
1181
export function runSync<E extends Effected<Effect, unknown>>(
3✔
1182
  effected: E extends Effected<infer F extends Effect, unknown> ?
291✔
1183
    [F] extends [never] ?
1184
      E
1185
    : UnhandledEffect<F>
1186
  : never,
1187
): E extends Effected<Effect, infer R> ? R : never {
291✔
1188
  const iterator = (effected as Iterable<any>)[Symbol.iterator]();
291✔
1189
  let { done, value } = iterator.next();
291✔
1190
  while (!done) {
291✔
1191
    if (!value)
651✔
1192
      throw new Error(
651✔
1193
        `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1194
      );
3✔
1195
    if (value._effectSync) {
651✔
1196
      ({ done, value } = iterator.next(...("value" in value ? [value.value] : [])));
624✔
1197
      continue;
624✔
1198
    }
624✔
1199
    if (value._effectAsync)
24✔
1200
      throw new Error(
390✔
1201
        "Cannot run an asynchronous effected program with `runSync`, use `runAsync` instead",
6✔
1202
      );
6✔
1203
    if (value instanceof Effect)
18✔
1204
      throw new UnhandledEffectError(value, `Unhandled effect: ${stringifyEffect(value)}`);
126✔
1205
    throw new Error(
3✔
1206
      `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1207
    );
3✔
1208
  }
3✔
1209
  return value;
219✔
1210
}
219✔
1211

1212
/**
1213
 * Run a (possibly) asynchronous effected program and return its result as a {@link Promise}.
1214
 * @param effected The effected program.
1215
 * @returns
1216
 *
1217
 * @throws {UnhandledEffectError} If an unhandled effect is encountered.
1218
 */
1219
export function runAsync<E extends Effected<Effect, unknown>>(
3✔
1220
  effected: E extends Effected<infer F extends Effect, unknown> ?
66✔
1221
    [F] extends [never] ?
1222
      E
1223
    : UnhandledEffect<F>
1224
  : never,
1225
): Promise<E extends Effected<Effect, infer R> ? R : never> {
66✔
1226
  const iterator = (effected as Iterable<any>)[Symbol.iterator]();
66✔
1227

1228
  return new Promise((resolve, reject) => {
66✔
1229
    const iterate = (...args: [] | [unknown]) => {
66✔
1230
      let done: boolean | undefined;
111✔
1231
      let value: any;
111✔
1232
      try {
111✔
1233
        ({ done, value } = iterator.next(...args));
111✔
1234
      } catch (e) {
111✔
1235
        // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1236
        reject(e);
6✔
1237
        return;
6✔
1238
      }
6✔
1239

1240
      // We use a while loop to avoid stack overflow when there are many synchronous effects
1241
      while (!done) {
111✔
1242
        if (!value) {
111✔
1243
          reject(
3✔
1244
            new Error(
3✔
1245
              `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1246
            ),
3✔
1247
          );
3✔
1248
          return;
3✔
1249
        }
3✔
1250
        if (value._effectSync) {
108✔
1251
          try {
54✔
1252
            ({ done, value } = iterator.next(...("value" in value ? [value.value] : [])));
54!
1253
          } catch (e) {
54✔
1254
            // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1255
            reject(e);
3✔
1256
            return;
3✔
1257
          }
3✔
1258
          continue;
51✔
1259
        }
51✔
1260
        if (value._effectAsync) {
78✔
1261
          // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1262
          value.onComplete(iterate, (...args: unknown[]) => reject(...args.slice(0, 1)));
48✔
1263
          return;
48✔
1264
        }
48✔
1265
        if (value instanceof Effect) {
60✔
1266
          reject(new UnhandledEffectError(value, `Unhandled effect: ${stringifyEffect(value)}`));
3✔
1267
          return;
3✔
1268
        }
3✔
1269
        reject(
3✔
1270
          new Error(
3✔
1271
            `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1272
          ),
3✔
1273
        );
3✔
1274
        return;
3✔
1275
      }
3✔
1276

1277
      resolve(value);
45✔
1278
    };
111✔
1279

1280
    iterate();
66✔
1281
  });
66✔
1282
}
66✔
1283

1284
/*********************
1285
 * Utility functions *
1286
 *********************/
1287
/**
1288
 * Check if a value is a {@link Generator}.
1289
 * @param value The value to check.
1290
 * @returns
1291
 */
1292
const isGenerator = (value: unknown): value is Generator =>
3✔
1293
  Object.prototype.toString.call(value) === "[object Generator]";
1,386✔
1294

1295
/**
1296
 * Capitalize the first letter of a string.
1297
 * @param str The string to capitalize.
1298
 * @returns
1299
 */
1300
const capitalize = (str: string) => {
3✔
1301
  if (str.length === 0) return str;
105!
1302
  return str[0]!.toUpperCase() + str.slice(1);
105✔
1303
};
105✔
1304

1305
const buildErrorClass = (name: string) => {
3✔
1306
  const ErrorClass = class extends Error {
36✔
1307
    constructor(message?: string) {
36✔
1308
      super(message);
36✔
1309
    }
36✔
1310
  };
36✔
1311
  let errorName = capitalize(name);
36✔
1312
  if (!errorName.endsWith("Error") && !errorName.endsWith("error")) errorName += "Error";
36✔
1313
  Object.defineProperty(ErrorClass, "name", {
36✔
1314
    value: errorName,
36✔
1315
    writable: false,
36✔
1316
    enumerable: false,
36✔
1317
    configurable: true,
36✔
1318
  });
36✔
1319
  Object.defineProperty(ErrorClass.prototype, "name", {
36✔
1320
    value: errorName,
36✔
1321
    writable: true,
36✔
1322
    enumerable: false,
36✔
1323
    configurable: true,
36✔
1324
  });
36✔
1325
  return ErrorClass;
36✔
1326
};
36✔
1327

1328
const stringifyEffectName = (name: string | symbol | ((...args: never) => unknown)) =>
3✔
1329
  typeof name === "string" ? name
51✔
1330
  : typeof name === "symbol" ? name.toString()
9✔
1331
  : "[" + name.name + "]";
3✔
1332

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

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

1339
/**
1340
 * Stringify an object, handling common cases that simple `JSON.stringify` does not handle, e.g.,
1341
 * `undefined`, `bigint`, `function`, `symbol`. Circular references are considered.
1342
 * @param x The object to stringify.
1343
 * @param space The number of spaces to use for indentation.
1344
 * @returns
1345
 */
1346
const stringify = (x: unknown, space: number = 0): string => {
3✔
1347
  const seen = new WeakSet();
72✔
1348
  const identifierRegex = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
72✔
1349

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

1352
  const serialize = (value: unknown, level: number): string => {
72✔
1353
    if (typeof value === "bigint") return `${value}n`;
78!
1354
    if (typeof value === "function")
78✔
1355
      return value.name ? `[Function: ${value.name}]` : "[Function (anonymous)]";
78!
1356
    if (typeof value === "symbol") return value.toString();
78!
1357
    if (value === undefined) return "undefined";
78!
1358
    if (value === null) return "null";
78✔
1359

1360
    if (typeof value === "object") {
78✔
1361
      if (seen.has(value)) return "[Circular]";
6!
1362
      seen.add(value);
6✔
1363

1364
      const nextLevel = level + 1;
6✔
1365
      const isClassInstance =
6✔
1366
        value.constructor && value.constructor.name && value.constructor.name !== "Object";
6✔
1367
      const className = isClassInstance ? `${value.constructor.name} ` : "";
6!
1368

1369
      if (Array.isArray(value)) {
6!
1370
        const arrayItems = value
×
1371
          .map((item) => serialize(item, nextLevel))
×
1372
          .join(space > 0 ? `,\n${indent(nextLevel)}` : ", ");
×
1373
        let result = `[${space > 0 ? "\n" + indent(nextLevel) : ""}${arrayItems}${space > 0 ? "\n" + indent(level) : ""}]`;
×
1374
        if (className !== "Array ") result = `${className.trimEnd()}(${value.length}) ${result}`;
×
1375
        return result;
×
1376
      }
×
1377

1378
      const objectEntries = Reflect.ownKeys(value)
6✔
1379
        .map((key) => {
6✔
1380
          const keyDisplay =
6✔
1381
            typeof key === "symbol" ? `[${key.toString()}]`
6!
1382
            : identifierRegex.test(key) ? key
6!
1383
            : JSON.stringify(key);
×
1384
          const val = (value as Record<string, unknown>)[key as any];
6✔
1385
          return `${space > 0 ? indent(nextLevel) : ""}${keyDisplay}: ${serialize(val, nextLevel)}`;
6!
1386
        })
6✔
1387
        .join(space > 0 ? `,\n` : ", ");
6!
1388

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

1392
    return JSON.stringify(value);
66✔
1393
  };
78✔
1394

1395
  return serialize(x, 0);
72✔
1396
};
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