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

Snowflyt / tinyeffect / 11791946650

12 Nov 2024 06:28AM UTC coverage: 96.753% (+0.07%) from 96.686%
11791946650

push

github

Snowflyt
🐳 chore: Bump version to `0.2.1`

174 of 190 branches covered (91.58%)

Branch coverage included in aggregate %.

571 of 580 relevant lines covered (98.45%)

498.86 hits per line

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

96.59
/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,
519✔
38
  options?: { readonly resumable?: Resumable },
519✔
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> {
519✔
44
  const result = (...payloads: unknown[]) =>
519✔
45
    effected(() => {
810✔
46
      let state = 0;
810✔
47
      return {
810✔
48
        next: (...args) => {
810✔
49
          switch (state) {
1,461✔
50
            case 0:
1,461✔
51
              state++;
810✔
52
              return {
810✔
53
                done: false,
810✔
54
                value:
810✔
55
                  options && options.resumable === false ?
810✔
56
                    Object.assign(new Effect(name, payloads), { resumable: false })
93✔
57
                  : new Effect(name, payloads),
717✔
58
              };
810✔
59
            case 1:
1,461✔
60
              state++;
639✔
61
              return {
639✔
62
                done: true,
639✔
63
                ...(args.length > 0 ? { value: args[0] } : {}),
639✔
64
              } as IteratorReturnResult<unknown>;
639✔
65
            default:
1,461✔
66
              return { done: true } as IteratorReturnResult<unknown>;
12✔
67
          }
1,461✔
68
        },
1,461✔
69
      };
810✔
70
    });
810✔
71
  if (options && (options as any)._overrideFunctionName === false) return result as never;
519✔
72
  return renameFunction(
450✔
73
    result,
450✔
74
    typeof name === "string" ? name : name.toString().slice(7, -1) || "",
519✔
75
  ) as never;
519✔
76
}
519✔
77

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

119
type ErrorName<E extends Effect> =
120
  E extends Unresumable<Effect<`error:${infer Name}`, any, never>> ? Name : never;
121

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

158
type DependencyName<E extends Effect> =
159
  E extends Effect<`dependency:${infer Name}`, [], unknown> ? Name : never;
160

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

212
/**
213
 * An effected program.
214
 */
215
export class Effected<out E extends Effect, out R> implements Iterable<E, R, unknown> {
3✔
216
  // @ts-expect-error - TS mistakenly think `[Symbol.iterator]` is not definitely assigned
217
  readonly [Symbol.iterator]: () => Iterator<E, R, unknown>;
4,200✔
218

219
  readonly runSync: [E] extends [never] ? () => R : UnhandledEffect<E>;
4,200✔
220
  readonly runAsync: [E] extends [never] ? () => Promise<R> : UnhandledEffect<E>;
4,200✔
221
  readonly runSyncUnsafe: () => R;
4,200✔
222
  readonly runAsyncUnsafe: () => Promise<R>;
4,200✔
223

224
  private constructor(fn: () => Iterator<E, R, unknown>, magicWords?: string) {
4,200✔
225
    if (magicWords !== "Yes, I’m sure I want to call the constructor of Effected directly.")
4,200✔
226
      logger.warn(
4,200✔
227
        "You should not call the constructor of `Effected` directly. Use `effected` instead.",
3✔
228
      );
3✔
229

230
    this[Symbol.iterator] = fn;
4,200✔
231

232
    this.runSync = (() => runSync(this as never)) as never;
4,200✔
233
    this.runAsync = (() => runAsync(this as never)) as never;
4,200✔
234
    this.runSyncUnsafe = () => runSync(this as never);
4,200✔
235
    this.runAsyncUnsafe = () => runAsync(this as never);
4,200✔
236
  }
4,200✔
237

238
  /**
239
   * Create an {@link Effected} instance that just returns the value.
240
   * @param value The value to return.
241
   * @returns
242
   *
243
   * @since 0.1.2
244
   */
245
  static of<R>(value: R): Effected<never, R> {
4,200✔
246
    return effected(() => ({ next: () => ({ done: true, value }) })) as Effected<never, R>;
546✔
247
  }
546✔
248

249
  /**
250
   * Create an {@link Effected} instance that just returns the value from a getter.
251
   * @param getter The getter to get the value.
252
   * @returns
253
   */
254
  static from<R>(getter: () => R): Effected<never, R> {
4,200✔
255
    return effected(() => ({ next: () => ({ done: true, value: getter() }) })) as Effected<
6✔
256
      never,
257
      R
258
    >;
259
  }
6✔
260

261
  /**
262
   * Handle an effect with a handler.
263
   *
264
   * For more common use cases, see {@link resume} and {@link terminate}, which provide a more
265
   * concise syntax.
266
   * @param effect The effect name or a function to match the effect name.
267
   * @param handler The handler for the effect. The first argument is an object containing the
268
   * encountered effect instance, a `resume` function to resume the effect, and a `terminate`
269
   * function to terminate the effect. The rest of the arguments are the payloads of the effect.
270
   *
271
   * `resume` or `terminate` should be called exactly once in the handler. If you call them more
272
   * than once, a warning will be logged to the console. If neither of them is called, the effected
273
   * program will hang indefinitely.
274
   *
275
   * Calling `resume` or `terminate` in an asynchronous context is also supported. It is _not_
276
   * required to call them synchronously.
277
   * @returns
278
   */
279
  handle<Name extends E["name"], T = R, F extends Effect = never>(
280
    effect: Name,
281
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
282
      (
283
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
284
        ...payloads: Payloads
285
      ) => void | Generator<F, void, unknown> | Effected<F, void>
286
    : E extends Effect<Name, infer Payloads, infer R> ?
287
      (
288
        {
289
          effect,
290
          resume,
291
          terminate,
292
        }: {
293
          effect: Extract<E, Effect<Name>>;
294
          resume: (value: R) => void;
295
          terminate: (value: T) => void;
296
        },
297
        ...payloads: Payloads
298
      ) => void | Generator<F, void, unknown> | Effected<F, void>
299
    : never,
300
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
301
  handle<Name extends string | symbol, T = R, F extends Effect = never>(
302
    effect: (name: E["name"]) => name is Name,
303
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
304
      (
305
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
306
        ...payloads: Payloads
307
      ) => void | Generator<F, void, unknown> | Effected<F, void>
308
    : E extends Effect<Name, infer Payloads, infer R> ?
309
      (
310
        {
311
          effect,
312
          resume,
313
          terminate,
314
        }: {
315
          effect: Extract<E, Effect<Name>>;
316
          resume: (value: R) => void;
317
          terminate: (value: T) => void;
318
        },
319
        ...payloads: Payloads
320
      ) => void | Generator<F, void, unknown> | Effected<F, void>
321
    : never,
322
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
323
  handle(
4,200✔
324
    name: string | symbol | ((name: string | symbol) => boolean),
552✔
325
    handler: (...args: any[]) => unknown,
552✔
326
  ): Effected<any, unknown> {
552✔
327
    const matchEffect = (value: unknown) =>
552✔
328
      value instanceof Effect &&
1,731✔
329
      (typeof name === "function" ? name(value.name) : value.name === name);
1,128✔
330

331
    return effected(() => {
552✔
332
      const iterator = this[Symbol.iterator]();
642✔
333
      let interceptIterator: typeof iterator | null = null;
642✔
334
      let terminated: false | "with-value" | "without-value" = false;
642✔
335
      let terminatedValue: unknown;
642✔
336

337
      return {
642✔
338
        next: (...args: [] | [unknown]) => {
642✔
339
          if (terminated)
2,262✔
340
            return {
2,262✔
341
              done: true,
87✔
342
              ...(terminated === "with-value" ? { value: terminatedValue } : {}),
87✔
343
            } as IteratorReturnResult<unknown>;
87✔
344

345
          const result = (interceptIterator || iterator).next(...args);
2,262✔
346

347
          const { done, value } = result;
2,262✔
348
          if (done) return result;
2,262✔
349

350
          if (matchEffect(value)) {
2,262✔
351
            const effect = value;
774✔
352

353
            let resumed: false | "with-value" | "without-value" = false;
774✔
354
            let resumedValue: R;
774✔
355
            let onComplete: ((...args: [] | [R]) => void) | null = null;
774✔
356
            const warnMultipleHandling = (type: "resume" | "terminate", ...args: [] | [R]) => {
774✔
357
              let message = `Effect ${stringifyEffectNameQuoted(name)} has been handled multiple times`;
18✔
358
              message += " (received `";
18✔
359
              message += `${type} ${stringifyEffect(effect)}`;
18✔
360
              if (args.length > 0) message += ` with ${stringify(args[0])}`;
18✔
361
              message += "` after it has been ";
18✔
362
              if (resumed) {
18✔
363
                message += "resumed";
12✔
364
                if (resumed === "with-value") message += ` with ${stringify(resumedValue)}`;
12✔
365
              } else if (terminated) {
18✔
366
                message += "terminated";
6✔
367
                if (terminated === "with-value") message += ` with ${stringify(terminatedValue)}`;
6✔
368
              }
6✔
369
              message += "). Only the first handler will be used.";
18✔
370
              logger.warn(message);
18✔
371
            };
18✔
372
            const resume = (...args: [] | [R]) => {
774✔
373
              if (resumed || terminated) {
642✔
374
                warnMultipleHandling("resume", ...args);
12✔
375
                return;
12✔
376
              }
12✔
377
              resumed = args.length > 0 ? "with-value" : "without-value";
642✔
378
              if (args.length > 0) resumedValue = args[0]!;
642✔
379
              if (onComplete) {
642✔
380
                onComplete(...args);
21✔
381
                onComplete = null;
21✔
382
              }
21✔
383
            };
642✔
384
            const terminate = (...args: [] | [R]) => {
774✔
385
              if (resumed || terminated) {
93✔
386
                warnMultipleHandling("terminate", ...args);
6✔
387
                return;
6✔
388
              }
6✔
389
              terminated = args.length > 0 ? "with-value" : "without-value";
93✔
390
              if (args.length > 0) terminatedValue = args[0];
93✔
391
              if (onComplete) {
93✔
392
                onComplete(...args);
6✔
393
                onComplete = null;
6✔
394
              }
6✔
395
            };
93✔
396

397
            const constructHandledEffect = ():
774✔
398
              | { _effectSync: true; value?: unknown }
399
              | {
400
                  _effectAsync: true;
401
                  onComplete: (callback: (...args: [] | [R]) => void) => void;
402
                } => {
717✔
403
              // For synchronous effects
404
              if (resumed || terminated)
717✔
405
                return {
717✔
406
                  _effectSync: true,
687✔
407
                  ...(Object.is(resumed, "with-value") ? { value: resumedValue! }
687✔
408
                  : Object.is(terminated, "with-value") ? { value: terminatedValue! }
93✔
409
                  : {}),
18✔
410
                };
687✔
411
              // For asynchronous effects
412
              return {
30✔
413
                _effectAsync: true,
30✔
414
                onComplete: (callback) => {
30✔
415
                  onComplete = callback;
27✔
416
                },
27✔
417
              };
30✔
418
            };
717✔
419

420
            const handlerResult = handler(
774✔
421
              {
774✔
422
                effect,
774✔
423
                resume:
774✔
424
                  (effect as any).resumable === false ?
774✔
425
                    () => {
87✔
426
                      throw new Error(
9✔
427
                        `Cannot resume non-resumable effect: ${stringifyEffect(effect)}`,
9✔
428
                      );
9✔
429
                    }
9✔
430
                  : resume,
687✔
431
                terminate,
774✔
432
              },
774✔
433
              ...effect.payloads,
774✔
434
            );
774✔
435

436
            if (
774✔
437
              !(handlerResult instanceof Effected) &&
774✔
438
              !isGenerator(handlerResult) &&
723✔
439
              !isEffectedIterator(handlerResult)
714✔
440
            )
441
              return { done: false, value: constructHandledEffect() } as never;
774✔
442

443
            const iter =
57✔
444
              Symbol.iterator in handlerResult ? handlerResult[Symbol.iterator]() : handlerResult;
774✔
445
            interceptIterator = {
774✔
446
              next: (...args: [] | [unknown]) => {
774✔
447
                const result = iter.next(...args);
108✔
448
                if (result.done) {
108✔
449
                  interceptIterator = null;
51✔
450
                  return { done: false, value: constructHandledEffect() } as never;
51✔
451
                }
51✔
452
                return result as never;
57✔
453
              },
108✔
454
            };
774✔
455
            return interceptIterator.next();
774✔
456
          }
774✔
457

458
          return result;
957✔
459
        },
2,262✔
460
      };
642✔
461
    });
552✔
462
  }
552✔
463

464
  /**
465
   * Resume an effect with the return value of the handler.
466
   *
467
   * It is a shortcut for
468
   * `handle(effect, ({ resume }, ...payloads) => resume(handler(...payloads)))`.
469
   * @param effect The effect name or a function to match the effect name.
470
   * @param handler The handler for the effect. The arguments are the payloads of the effect.
471
   * @returns
472
   *
473
   * @see {@link handle}
474
   */
475
  resume<Name extends Exclude<E, Unresumable<Effect>>["name"], F extends Effect = never>(
476
    effect: Name,
477
    handler: E extends Effect<Name, infer Payloads, infer R> ?
478
      (...payloads: Payloads) => R | Generator<F, R, unknown> | Effected<F, R>
479
    : never,
480
  ): Effected<Exclude<E, Effect<Name>> | F, R>;
481
  resume<Name extends string | symbol, F extends Effect = never>(
482
    effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
483
    handler: E extends Effect<Name, infer Payloads, infer R> ?
484
      (...payloads: Payloads) => R | Generator<F, R, unknown> | Effected<F, R>
485
    : never,
486
  ): Effected<Exclude<E, Effect<Name>> | F, R>;
487
  resume(effect: any, handler: (...payloads: unknown[]) => unknown) {
4,200✔
488
    return this.handle(effect, (({ resume }: any, ...payloads: unknown[]) => {
276✔
489
      const it = handler(...payloads);
576✔
490
      if (!(it instanceof Effected) && !isGenerator(it)) return resume(it);
576✔
491
      const iterator = it[Symbol.iterator]();
42✔
492
      return {
42✔
493
        _effectedIterator: true,
42✔
494
        next: (...args: [] | [unknown]) => {
42✔
495
          const result = iterator.next(...args);
78✔
496
          if (result.done) return { done: true, value: resume(result.value) };
78✔
497
          return result;
42✔
498
        },
78✔
499
      };
42✔
500
    }) as never);
276✔
501
  }
276✔
502

503
  /**
504
   * Terminate an effect with the return value of the handler.
505
   *
506
   * It is a shortcut for
507
   * `handle(effect, ({ terminate }, ...payloads) => terminate(handler(...payloads)))`.
508
   * @param effect The effect name or a function to match the effect name.
509
   * @param handler The handler for the effect. The arguments are the payloads of the effect.
510
   * @returns
511
   *
512
   * @see {@link handle}
513
   */
514
  terminate<Name extends E["name"], T, F extends Effect = never>(
515
    effect: Name,
516
    handler: E extends Effect<Name, infer Payloads> ?
517
      (...payloads: Payloads) => Generator<F, T, unknown> | Effected<F, T>
518
    : never,
519
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
520
  terminate<Name extends string | symbol, T, F extends Effect = never>(
521
    effect: (name: E["name"]) => name is Name,
522
    handler: E extends Effect<Name, infer Payloads> ?
523
      (...payloads: Payloads) => Generator<F, T, unknown> | Effected<F, T>
524
    : never,
525
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
526
  terminate<Name extends E["name"], T>(
527
    effect: Name,
528
    handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
529
  ): Effected<Exclude<E, Effect<Name>>, R | T>;
530
  terminate<Name extends string | symbol, T>(
531
    effect: (name: E["name"]) => name is Name,
532
    handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
533
  ): Effected<Exclude<E, Effect<Name>>, R | T>;
534
  terminate(effect: any, handler: (...payloads: unknown[]) => unknown) {
4,200✔
535
    return this.handle(effect, (({ terminate }: any, ...payloads: unknown[]) => {
150✔
536
      const it = handler(...payloads);
60✔
537
      if (!(it instanceof Effected) && !isGenerator(it)) return terminate(it);
60✔
538
      const iterator = it[Symbol.iterator]();
3✔
539
      return {
3✔
540
        _effectedIterator: true,
3✔
541
        next: (...args: [] | [unknown]) => {
3✔
542
          const result = iterator.next(...args);
6✔
543
          if (result.done) return { done: true, value: terminate(result.value) };
6✔
544
          return result;
3✔
545
        },
6✔
546
      };
3✔
547
    }) as never);
150✔
548
  }
150✔
549

550
  /**
551
   * Map the return value of the effected program.
552
   * @param handler The function to map the return value.
553
   * @returns
554
   */
555
  map<S, F extends Effect = never>(
556
    handler: (value: R) => Generator<F, S, unknown> | Effected<F, S>,
557
  ): Effected<E | F, S>;
558
  map<S>(handler: (value: R) => S): Effected<E, S>;
559
  map(handler: (value: R) => unknown): Effected<Effect, unknown> {
4,200✔
560
    const iterator = this[Symbol.iterator]();
1,176✔
561

562
    return effected(() => {
1,176✔
563
      let originalIteratorDone = false;
1,176✔
564
      let appendedIterator: Iterator<Effect, unknown, unknown>;
1,176✔
565
      return {
1,176✔
566
        next: (...args: [] | [R]) => {
1,176✔
567
          if (originalIteratorDone) return appendedIterator.next(...args);
1,311✔
568
          const result = iterator.next(...args);
1,299✔
569
          if (!result.done) return result;
1,311✔
570
          originalIteratorDone = true;
1,122✔
571
          const it = handler(result.value);
1,122✔
572
          if (!(it instanceof Effected) && !isGenerator(it) && !isEffectedIterator(it))
1,311✔
573
            return { done: true, value: it };
1,311✔
574
          appendedIterator = Symbol.iterator in it ? it[Symbol.iterator]() : it;
1,311✔
575
          return appendedIterator.next();
1,311✔
576
        },
1,311✔
577
      };
1,176✔
578
    });
1,176✔
579
  }
1,176✔
580

581
  /**
582
   * Tap the return value of the effected program.
583
   * @param handler The function to tap the return value.
584
   * @returns
585
   *
586
   * @since 0.2.1
587
   */
588
  tap<F extends Effect = never>(
4,200✔
589
    handler: (value: R) => void | Generator<F, void, unknown> | Effected<F, void>,
12✔
590
  ): Effected<E | F, R> {
12✔
591
    return this.map((value) => {
12✔
592
      const it = handler(value);
12✔
593
      if (!(it instanceof Effected) && !isGenerator(it)) return value;
12✔
594
      const iterator = it[Symbol.iterator]();
6✔
595
      return {
6✔
596
        _effectedIterator: true,
6✔
597
        next: (...args: [] | [unknown]) => {
6✔
598
          const result = iterator.next(...args);
12✔
599
          if (result.done) return { done: true, value };
12✔
600
          return result;
6✔
601
        },
12✔
602
      };
6✔
603
    }) as never;
12✔
604
  }
12✔
605

606
  /**
607
   * Catch an error effect with a handler.
608
   *
609
   * It is a shortcut for `terminate("error:" + name, handler)`.
610
   * @param name The name of the error effect.
611
   * @param handler The handler for the error effect. The argument is the message of the error.
612
   * @returns
613
   *
614
   * @see {@link terminate}
615
   */
616
  catch<Name extends ErrorName<E>, T, F extends Effect = never>(
617
    effect: Name,
618
    handler: (message?: string) => Generator<F, T, unknown> | Effected<F, T>,
619
  ): Effected<Exclude<E, Effect.Error<Name>> | F, R | T>;
620
  catch<Name extends ErrorName<E>, T>(
621
    effect: Name,
622
    handler: (message?: string) => T,
623
  ): Effected<Exclude<E, Effect.Error<Name>>, R | T>;
624
  catch(name: string, handler: (message?: string) => unknown): Effected<Effect, unknown> {
4,200✔
625
    return this.terminate(`error:${name}` as never, handler as never);
81✔
626
  }
81✔
627

628
  /**
629
   * Catch all error effects with a handler.
630
   * @param handler The handler for the error effect. The first argument is the name of the error
631
   * effect (without the `"error:"` prefix), and the second argument is the message of the error.
632
   */
633
  catchAll<T, F extends Effect = never>(
634
    handler: (error: ErrorName<E>, message?: string) => Generator<F, T, unknown> | Effected<F, T>,
635
  ): Effected<Exclude<E, Effect.Error> | F, R | T>;
636
  catchAll<T>(
637
    handler: (error: ErrorName<E>, message?: string) => T,
638
  ): Effected<Exclude<E, Effect.Error>, R | T>;
639
  catchAll(handler: (error: ErrorName<E>, message?: string) => unknown): Effected<Effect, unknown> {
4,200✔
640
    return this.handle(
36✔
641
      (name): name is ErrorName<E> => typeof name === "string" && name.startsWith("error:"),
36✔
642
      (({ effect, terminate }: any, ...payloads: [message?: string]) => {
36✔
643
        const error = effect.name.slice(6) as ErrorName<E>;
33✔
644
        const it = handler(error, ...payloads);
33✔
645
        if (!(it instanceof Effected) && !isGenerator(it)) return terminate(it);
33✔
646
        const iterator = it[Symbol.iterator]();
3✔
647
        return {
3✔
648
          _effectedIterator: true,
3✔
649
          next: (...args: [] | [unknown]) => {
3✔
650
            const result = iterator.next(...args);
6✔
651
            if (result.done) return { done: true, value: terminate(result.value) };
6✔
652
            return result;
3✔
653
          },
6✔
654
        };
3✔
655
      }) as never,
33✔
656
    );
36✔
657
  }
36✔
658

659
  /**
660
   * Catch an error effect and throw it as an error.
661
   * @param name The name of the error effect.
662
   * @param message The message of the error. If it is a function, it will be called with the
663
   * message of the error effect, and the return value will be used as the message of the error.
664
   * @returns
665
   *
666
   * @since 0.1.1
667
   */
668
  catchAndThrow<Name extends ErrorName<E>>(
4,200✔
669
    name: Name,
18✔
670
    message?: string | ((message?: string) => string | undefined),
18✔
671
  ): Effected<Exclude<E, Effect.Error<Name>>, R> {
18✔
672
    return this.catch(name, (...args) => {
18✔
673
      throw new (buildErrorClass(name))(
18✔
674
        ...(typeof message === "string" ? [message]
18✔
675
        : typeof message === "function" ? [message(...args)].filter((v) => v !== undefined)
12✔
676
        : args),
6✔
677
      );
18✔
678
    });
18✔
679
  }
18✔
680

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

702
  /**
703
   * Provide a value for a dependency effect.
704
   * @param name The name of the dependency.
705
   * @param value The value to provide for the dependency.
706
   * @returns
707
   */
708
  provide<Name extends DependencyName<E>>(
4,200✔
709
    name: Name,
18✔
710
    value: E extends Effect.Dependency<Name, infer R> ? R : never,
18✔
711
  ): Effected<Exclude<E, Effect.Dependency<Name>>, R> {
18✔
712
    return this.resume(`dependency:${name}` as never, (() => value) as never) as never;
18✔
713
  }
18✔
714

715
  /**
716
   * Provide a value for a dependency effect with a getter.
717
   * @param name The name of the dependency.
718
   * @param getter The getter to provide for the dependency.
719
   * @returns
720
   */
721
  provideBy<Name extends DependencyName<E>, F extends Effect = never>(
4,200✔
722
    name: Name,
21✔
723
    getter: E extends Effect.Dependency<Name, infer R> ?
21✔
724
      () => R | Generator<F, R, unknown> | Effected<F, R>
725
    : never,
726
  ): Effected<Exclude<E, Effect.Dependency<Name>> | F, R> {
21✔
727
    return this.resume(`dependency:${name}`, getter as never) as never;
21✔
728
  }
21✔
729

730
  /**
731
   * Apply a handler to the effected program.
732
   * @param handler The handler to apply to the effected program.
733
   * @returns
734
   */
735
  with<F extends Effect, G extends Effect, S>(
736
    handler: (effected: EffectedDraft<never, never, R>) => EffectedDraft<F, G, S>,
737
  ): Effected<Exclude<E, F> | G, S>;
738
  with<F extends Effect, S>(handler: (effected: Effected<E, R>) => Effected<F, S>): Effected<F, S>;
739
  with(handler: (effected: any) => unknown) {
4,200✔
740
    return handler(this);
84✔
741
  }
84✔
742
}
4,200✔
743

744
interface EffectedDraft<
745
  out P extends Effect = Effect,
746
  out E extends Effect = Effect,
747
  out R = unknown,
748
> extends Iterable<E, R, unknown> {
749
  handle<Name extends E["name"], T = R, F extends Effect = never>(
750
    effect: Name,
751
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
752
      (
753
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
754
        ...payloads: Payloads
755
      ) => void | Generator<F, void, unknown> | Effected<F, void>
756
    : E extends Effect<Name, infer Payloads, infer R> ?
757
      (
758
        {
759
          effect,
760
          resume,
761
          terminate,
762
        }: {
763
          effect: Extract<E, Effect<Name>>;
764
          resume: (value: R) => void;
765
          terminate: (value: T) => void;
766
        },
767
        ...payloads: Payloads
768
      ) => void | Generator<F, void, unknown> | Effected<F, void>
769
    : never,
770
  ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
771
  handle<Name extends string | symbol, T = R, F extends Effect = never>(
772
    effect: (name: E["name"]) => name is Name,
773
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
774
      (
775
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
776
        ...payloads: Payloads
777
      ) => void | Generator<F, void, unknown> | Effected<F, void>
778
    : E extends Effect<Name, infer Payloads, infer R> ?
779
      (
780
        {
781
          effect,
782
          resume,
783
          terminate,
784
        }: {
785
          effect: Extract<E, Effect<Name>>;
786
          resume: (value: R) => void;
787
          terminate: (value: T) => void;
788
        },
789
        ...payloads: Payloads
790
      ) => void | Generator<F, void, unknown> | Effected<F, void>
791
    : never,
792
  ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
793

794
  resume<Name extends Exclude<E, Unresumable<Effect>>["name"], F extends Effect = never>(
795
    effect: Name,
796
    handler: E extends Effect<Name, infer Payloads, infer R> ?
797
      (...payloads: Payloads) => R | Generator<F, R, unknown> | Effected<F, R>
798
    : never,
799
  ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R>;
800
  resume<Name extends string | symbol, F extends Effect = never>(
801
    effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
802
    handler: E extends Effect<Name, infer Payloads, infer R> ?
803
      (...payloads: Payloads) => R | Generator<F, R, unknown> | Effected<F, R>
804
    : never,
805
  ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R>;
806

807
  terminate<Name extends E["name"], T, F extends Effect = never>(
808
    effect: Name,
809
    handler: E extends Effect<Name, infer Payloads> ?
810
      (...payloads: Payloads) => Generator<F, T, unknown> | Effected<F, T>
811
    : never,
812
  ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
813
  terminate<Name extends string | symbol, T, F extends Effect = never>(
814
    effect: (name: E["name"]) => name is Name,
815
    handler: E extends Effect<Name, infer Payloads> ?
816
      (...payloads: Payloads) => Generator<F, T, unknown> | Effected<F, T>
817
    : never,
818
  ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
819
  terminate<Name extends E["name"], T>(
820
    effect: Name,
821
    handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
822
  ): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
823
  terminate<Name extends string | symbol, T>(
824
    effect: (name: E["name"]) => name is Name,
825
    handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
826
  ): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
827

828
  map<S, F extends Effect = never>(
829
    handler: (value: R) => Generator<F, S, unknown> | Effected<F, S>,
830
  ): EffectedDraft<P, E | F, S>;
831
  map<S>(handler: (value: R) => S): EffectedDraft<P, E, S>;
832

833
  tap<F extends Effect = never>(
834
    handler: (value: R) => void | Generator<F, void, unknown> | Effected<F, void>,
835
  ): EffectedDraft<P, E | F, R>;
836

837
  catch<Name extends ErrorName<E>, T, F extends Effect = never>(
838
    effect: Name,
839
    handler: (message?: string) => Generator<F, T, unknown> | Effected<F, T>,
840
  ): EffectedDraft<P, Exclude<E, Effect.Error<Name>> | F, R | T>;
841
  catch<Name extends ErrorName<E>, T>(
842
    effect: Name,
843
    handler: (message?: string) => T,
844
  ): EffectedDraft<P, Exclude<E, Effect.Error<Name>>, R | T>;
845

846
  catchAll<T, F extends Effect = never>(
847
    handler: (effect: ErrorName<E>, message?: string) => Generator<F, T, unknown> | Effected<F, T>,
848
  ): Effected<Exclude<E, Effect.Error> | F, R | T>;
849
  catchAll<T>(
850
    handler: (effect: ErrorName<E>, message?: string) => T,
851
  ): Effected<Exclude<E, Effect.Error>, R | T>;
852

853
  readonly catchAndThrow: <Name extends ErrorName<E>>(
854
    name: Name,
855
    message?: string | ((message?: string) => string | undefined),
856
  ) => Effected<Exclude<E, Effect.Error<Name>>, R>;
857

858
  readonly catchAllAndThrow: (
859
    message?: string | ((error: string, message?: string) => string | undefined),
860
  ) => Effected<Exclude<E, Effect.Error>, R>;
861

862
  readonly provide: <Name extends DependencyName<E>>(
863
    name: Name,
864
    value: E extends Effect.Dependency<Name, infer R> ? R : never,
865
  ) => EffectedDraft<P, Exclude<E, Effect.Dependency<Name>>, R>;
866
  provideBy<Name extends DependencyName<E>, F extends Effect = never>(
867
    name: Name,
868
    getter: E extends Effect.Dependency<Name, infer R> ?
869
      () => R | Generator<F, R, unknown> | Effected<F, R>
870
    : never,
871
  ): EffectedDraft<P, Exclude<E, Effect.Dependency<Name>> | F, R>;
872

873
  with<F extends Effect, G extends Effect, S>(
874
    handler: (effected: EffectedDraft<never, never, R>) => EffectedDraft<F, G, S>,
875
  ): EffectedDraft<P, Exclude<E, F> | G, S>;
876
  with<F extends Effect, S>(
877
    handler: (effected: Effected<E, R>) => Effected<F, S>,
878
  ): EffectedDraft<P, F, S>;
879
}
880

881
/**
882
 * Create an effected program.
883
 * @param fn A function that returns an iterator.
884
 * @returns
885
 *
886
 * @example
887
 * ```typescript
888
 * type User = { id: number; name: string; role: "admin" | "user" };
889
 *
890
 * // Use `effect` and its variants to define factory functions for effects
891
 * const println = effect("println")<unknown[], void>;
892
 * const executeSQL = effect("executeSQL")<[sql: string, ...params: unknown[]], any>;
893
 * const askCurrentUser = dependency("currentUser")<User | null>;
894
 * const authenticationError = error("authentication");
895
 * const unauthorizedError = error("unauthorized");
896
 *
897
 * // Use `effected` to define an effected program
898
 * const requiresAdmin = () => effected(function* () {
899
 *   const currentUser = yield* askCurrentUser();
900
 *   if (!currentUser) return yield* authenticationError();
901
 *   if (currentUser.role !== "admin")
902
 *     return yield* unauthorizedError(`User "${currentUser.name}" is not an admin`);
903
 * });
904
 *
905
 * // You can yield other effected programs in an effected program
906
 * const createUser = (user: Omit<User, "id">) => effected(function* () {
907
 *   yield* requiresAdmin();
908
 *   const id = yield* executeSQL("INSERT INTO users (name) VALUES (?)", user.name);
909
 *   const savedUser: User = { id, ...user };
910
 *   yield* println("User created:", savedUser);
911
 *   return savedUser;
912
 * });
913
 *
914
 * const program = effected(function* () {
915
 *   yield* createUser({ name: "Alice", role: "user" });
916
 *   yield* createUser({ name: "Bob", role: "admin" });
917
 * })
918
 *   // Handle effects with the `.handle()` method
919
 *   .handle("executeSQL", function* ({ resume, terminate }, sql, ...params) {
920
 *     // You can yield other effects in a handler using a generator function
921
 *     yield* println("Executing SQL:", sql, ...params);
922
 *     // Asynchronous effects are supported
923
 *     db.execute(sql, params, (err, result) => {
924
 *       if (err) return terminate(err);
925
 *       resume(result);
926
 *     });
927
 *   })
928
 *   // a shortcut for `.handle()` that resumes the effect with the return value of the handler
929
 *   .resume("println", (...args) => console.log(...args))
930
 *   // Other shortcuts for special effects (error effects and dependency effects)
931
 *   .provide("currentUser", { id: 1, name: "Charlie", role: "admin" })
932
 *   .catch("authentication", () => console.error("Authentication error"));
933
 *   .catch("unauthorized", () => console.error("Unauthorized error"));
934
 *
935
 * // Run the effected program with `.runSync()` or `.runAsync()`
936
 * await program.runAsync();
937
 * ```
938
 *
939
 * @see {@link effect}
940
 */
941
export function effected<E extends Effect, R>(fn: () => Iterator<E, R, unknown>): Effected<E, R> {
3✔
942
  return new (Effected as any)(
4,197✔
943
    fn,
4,197✔
944
    "Yes, I’m sure I want to call the constructor of Effected directly.",
4,197✔
945
  );
4,197✔
946
}
4,197✔
947

948
/**
949
 * Convert a {@link Promise} to an effected program containing a single {@link Effect}.
950
 * @param promise The promise to effectify.
951
 * @returns
952
 *
953
 * ```typescript
954
 * // Assume we have `db.user.create(user: User): Promise<number>`
955
 * const createUser = (user: Omit<User, "id">) => effected(function* () {
956
 *   yield* requiresAdmin();
957
 *   // Use `yield* effectify(...)` instead of `await ...` in an effected program
958
 *   const id = yield* effectify(db.user.create(user));
959
 *   const savedUser = { id, ...user };
960
 *   yield* println("User created:", savedUser);
961
 *   return savedUser;
962
 * });
963
 * ```
964
 */
965
export function effectify<T>(promise: Promise<T>): Effected<never, T> {
3✔
966
  return effected(() => {
27✔
967
    let state = 0;
27✔
968
    return {
27✔
969
      next: (...args) => {
27✔
970
        switch (state) {
51✔
971
          case 0:
51✔
972
            state++;
27✔
973
            return {
27✔
974
              done: false,
27✔
975
              value: {
27✔
976
                _effectAsync: true,
27✔
977
                onComplete: (
27✔
978
                  ...args: [onComplete: (value: T) => void, onThrow?: (value: unknown) => void]
21✔
979
                ) => promise.then(...args),
21✔
980
              } as never,
27✔
981
            };
27✔
982
          case 1:
51✔
983
            state++;
21✔
984
            return {
21✔
985
              done: true,
21✔
986
              ...(args.length > 0 ? { value: args[0] } : {}),
21!
987
            } as IteratorReturnResult<T>;
21✔
988
          default:
51✔
989
            return { done: true } as IteratorReturnResult<T>;
3✔
990
        }
51✔
991
      },
51✔
992
    };
27✔
993
  });
27✔
994
}
27✔
995

996
/**
997
 * Run an effected program synchronously and return its result.
998
 * @param effected The effected program.
999
 * @returns
1000
 *
1001
 * @throws {UnhandledEffectError} If an unhandled effect is encountered.
1002
 * @throws {Error} If an asynchronous effect is encountered.
1003
 */
1004
export function runSync<E extends Effected<Effect, unknown>>(
3✔
1005
  effected: E extends Effected<infer F extends Effect, unknown> ?
324✔
1006
    [F] extends [never] ?
1007
      E
1008
    : UnhandledEffect<F>
1009
  : never,
1010
): E extends Effected<Effect, infer R> ? R : never {
324✔
1011
  const iterator = (effected as Iterable<any>)[Symbol.iterator]();
324✔
1012
  let { done, value } = iterator.next();
324✔
1013
  while (!done) {
324✔
1014
    if (!value)
660✔
1015
      throw new Error(
660✔
1016
        `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1017
      );
3✔
1018
    if (value._effectSync) {
660✔
1019
      ({ done, value } = iterator.next(...("value" in value ? [value.value] : [])));
633✔
1020
      continue;
633✔
1021
    }
633✔
1022
    if (value._effectAsync)
24✔
1023
      throw new Error(
399✔
1024
        "Cannot run an asynchronous effected program with `runSync`, use `runAsync` instead",
6✔
1025
      );
6✔
1026
    if (value instanceof Effect)
18✔
1027
      throw new UnhandledEffectError(value, `Unhandled effect: ${stringifyEffect(value)}`);
132✔
1028
    throw new Error(
3✔
1029
      `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1030
    );
3✔
1031
  }
3✔
1032
  return value;
252✔
1033
}
252✔
1034

1035
/**
1036
 * Run a (possibly) asynchronous effected program and return its result as a {@link Promise}.
1037
 * @param effected The effected program.
1038
 * @returns
1039
 *
1040
 * @throws {UnhandledEffectError} If an unhandled effect is encountered.
1041
 */
1042
export function runAsync<E extends Effected<Effect, unknown>>(
3✔
1043
  effected: E extends Effected<infer F extends Effect, unknown> ?
66✔
1044
    [F] extends [never] ?
1045
      E
1046
    : UnhandledEffect<F>
1047
  : never,
1048
): Promise<E extends Effected<Effect, infer R> ? R : never> {
66✔
1049
  const iterator = (effected as Iterable<any>)[Symbol.iterator]();
66✔
1050

1051
  return new Promise((resolve, reject) => {
66✔
1052
    const iterate = (...args: [] | [unknown]) => {
66✔
1053
      let done: boolean | undefined;
111✔
1054
      let value: any;
111✔
1055
      try {
111✔
1056
        ({ done, value } = iterator.next(...args));
111✔
1057
      } catch (e) {
111✔
1058
        // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1059
        reject(e);
6✔
1060
        return;
6✔
1061
      }
6✔
1062

1063
      // We use a while loop to avoid stack overflow when there are many synchronous effects
1064
      while (!done) {
111✔
1065
        if (!value) {
111✔
1066
          reject(
3✔
1067
            new Error(
3✔
1068
              `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1069
            ),
3✔
1070
          );
3✔
1071
          return;
3✔
1072
        }
3✔
1073
        if (value._effectSync) {
108✔
1074
          try {
54✔
1075
            ({ done, value } = iterator.next(...("value" in value ? [value.value] : [])));
54!
1076
          } catch (e) {
54✔
1077
            // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1078
            reject(e);
3✔
1079
            return;
3✔
1080
          }
3✔
1081
          continue;
51✔
1082
        }
51✔
1083
        if (value._effectAsync) {
78✔
1084
          // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1085
          value.onComplete(iterate, (...args: unknown[]) => reject(...args.slice(0, 1)));
48✔
1086
          return;
48✔
1087
        }
48✔
1088
        if (value instanceof Effect) {
60✔
1089
          reject(new UnhandledEffectError(value, `Unhandled effect: ${stringifyEffect(value)}`));
3✔
1090
          return;
3✔
1091
        }
3✔
1092
        reject(
3✔
1093
          new Error(
3✔
1094
            `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1095
          ),
3✔
1096
        );
3✔
1097
        return;
3✔
1098
      }
3✔
1099

1100
      resolve(value);
45✔
1101
    };
111✔
1102

1103
    iterate();
66✔
1104
  });
66✔
1105
}
66✔
1106

1107
/*********************
1108
 * Utility functions *
1109
 *********************/
1110
/**
1111
 * Check if a value is a {@link Generator}.
1112
 * @param value The value to check.
1113
 * @returns
1114
 */
1115
const isGenerator = (value: unknown): value is Generator =>
3✔
1116
  Object.prototype.toString.call(value) === "[object Generator]";
1,929✔
1117

1118
/**
1119
 * Check if a value is an `EffectedIterator` (i.e., an {@link Iterator} with an `_effectedIterator`
1120
 * property set to `true`).
1121
 *
1122
 * This is only used internally as an alternative to generators to reduce the overhead of creating
1123
 * generator functions.
1124
 * @param value
1125
 * @returns
1126
 */
1127
const isEffectedIterator = (
3✔
1128
  value: unknown,
1,302✔
1129
): value is Iterator<Effect, unknown, unknown> & { _effectedIterator: true } =>
1130
  typeof value === "object" && value !== null && (value as any)._effectedIterator === true;
1,302✔
1131

1132
/**
1133
 * Capitalize the first letter of a string.
1134
 * @param str The string to capitalize.
1135
 * @returns
1136
 */
1137
const capitalize = (str: string) => {
3✔
1138
  if (str.length === 0) return str;
105!
1139
  return str[0]!.toUpperCase() + str.slice(1);
105✔
1140
};
105✔
1141

1142
/**
1143
 * Change the name of a function for better debugging experience.
1144
 * @param fn The function to rename.
1145
 * @param name The new name of the function.
1146
 * @returns
1147
 */
1148
const renameFunction = <F extends (...args: never) => unknown>(fn: F, name: string): F =>
3✔
1149
  Object.defineProperty(fn, "name", {
519✔
1150
    value: name,
519✔
1151
    writable: false,
519✔
1152
    enumerable: false,
519✔
1153
    configurable: true,
519✔
1154
  });
519✔
1155

1156
const buildErrorClass = (name: string) => {
3✔
1157
  const ErrorClass = class extends Error {
36✔
1158
    constructor(message?: string) {
36✔
1159
      super(message);
36✔
1160
    }
36✔
1161
  };
36✔
1162
  let errorName = capitalize(name);
36✔
1163
  if (!errorName.endsWith("Error") && !errorName.endsWith("error")) errorName += "Error";
36✔
1164
  Object.defineProperty(ErrorClass, "name", {
36✔
1165
    value: errorName,
36✔
1166
    writable: false,
36✔
1167
    enumerable: false,
36✔
1168
    configurable: true,
36✔
1169
  });
36✔
1170
  Object.defineProperty(ErrorClass.prototype, "name", {
36✔
1171
    value: errorName,
36✔
1172
    writable: true,
36✔
1173
    enumerable: false,
36✔
1174
    configurable: true,
36✔
1175
  });
36✔
1176
  return ErrorClass;
36✔
1177
};
36✔
1178

1179
const stringifyEffectName = (name: string | symbol | ((...args: never) => unknown)) =>
3✔
1180
  typeof name === "string" ? name
51✔
1181
  : typeof name === "symbol" ? name.toString()
9✔
1182
  : "[" + name.name + "]";
3✔
1183

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

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

1190
/**
1191
 * Stringify an object, handling common cases that simple `JSON.stringify` does not handle, e.g.,
1192
 * `undefined`, `bigint`, `function`, `symbol`. Circular references are considered.
1193
 * @param x The object to stringify.
1194
 * @param space The number of spaces to use for indentation.
1195
 * @returns
1196
 */
1197
const stringify = (x: unknown, space: number = 0): string => {
3✔
1198
  const seen = new WeakSet();
72✔
1199
  const identifierRegex = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
72✔
1200

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

1203
  const serialize = (value: unknown, level: number): string => {
72✔
1204
    if (typeof value === "bigint") return `${value as any}n`;
78!
1205
    if (typeof value === "function")
78✔
1206
      return value.name ? `[Function: ${value.name}]` : "[Function (anonymous)]";
78!
1207
    if (typeof value === "symbol") return value.toString();
78!
1208
    if (value === undefined) return "undefined";
78!
1209
    if (value === null) return "null";
78✔
1210

1211
    if (typeof value === "object") {
78✔
1212
      if (seen.has(value)) return "[Circular]";
6!
1213
      seen.add(value);
6✔
1214

1215
      const nextLevel = level + 1;
6✔
1216
      const isClassInstance =
6✔
1217
        value.constructor && value.constructor.name && value.constructor.name !== "Object";
6✔
1218
      const className = isClassInstance ? `${value.constructor.name} ` : "";
6!
1219

1220
      if (Array.isArray(value)) {
6!
1221
        const arrayItems = value
×
1222
          .map((item) => serialize(item, nextLevel))
×
1223
          .join(space > 0 ? `,\n${indent(nextLevel)}` : ", ");
×
1224
        let result = `[${space > 0 ? "\n" + indent(nextLevel) : ""}${arrayItems}${space > 0 ? "\n" + indent(level) : ""}]`;
×
1225
        if (className !== "Array ") result = `${className.trim()}(${value.length}) ${result}`;
×
1226
        return result;
×
1227
      }
×
1228

1229
      const objectEntries = Reflect.ownKeys(value)
6✔
1230
        .map((key) => {
6✔
1231
          const keyDisplay =
6✔
1232
            typeof key === "symbol" ? `[${key.toString()}]`
6!
1233
            : identifierRegex.test(key) ? key
6!
1234
            : JSON.stringify(key);
×
1235
          const val = (value as Record<string, unknown>)[key as any];
6✔
1236
          return `${space > 0 ? indent(nextLevel) : ""}${keyDisplay}: ${serialize(val, nextLevel)}`;
6!
1237
        })
6✔
1238
        .join(space > 0 ? `,\n` : ", ");
6!
1239

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

1243
    return JSON.stringify(value);
66✔
1244
  };
78✔
1245

1246
  return serialize(x, 0);
72✔
1247
};
72✔
1248

1249
// `console` is not standard in JavaScript. Though rare, it is possible that `console` is not
1250
// available in some environments. We use a proxy to handle this case and ignore errors if `console`
1251
// is not available.
1252
const logger: {
3✔
1253
  debug(...data: unknown[]): void;
1254
  error(...data: unknown[]): void;
1255
  log(...data: unknown[]): void;
1256
  warn(...data: unknown[]): void;
1257
} = new Proxy({} as never, {
3✔
1258
  get:
3✔
1259
    (_, prop) =>
3✔
1260
    (...args: unknown[]) => {
21✔
1261
      try {
21✔
1262
        // @ts-expect-error - Cannot find name 'console'
1263
        console[prop](...args);
21✔
1264
      } catch {
21!
1265
        // Ignore
1266
      }
×
1267
    },
21✔
1268
});
3✔
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