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

Snowflyt / tinyeffect / 13988009069

21 Mar 2025 08:50AM UTC coverage: 98.956% (+2.6%) from 96.401%
13988009069

push

github

Snowflyt
✨ feat: Bump version to 0.3.2

358 of 367 branches covered (97.55%)

Branch coverage included in aggregate %.

779 of 782 relevant lines covered (99.62%)

400.61 hits per line

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

98.92
/src/effected.ts
1
import { UnhandledEffectError } from "./errors";
3✔
2
import type { UnhandledEffect, Unresumable } from "./types";
3
import { Effect } from "./types";
3✔
4

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

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

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

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

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

160
/**
161
 * Define a handler that transforms an effected program into another one.
162
 *
163
 * It is just a simple wrapper to make TypeScript infer the types correctly, and simply returns the
164
 * function you pass to it.
165
 * @returns
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.andThen((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: <S extends EffectedDraft<E>, H extends (effected: EffectedDraft<E, E, R>) => S>(
192
    handler: H,
193
  ) => H;
194
};
195
export function defineHandlerFor<E extends Effect>(): {
196
  with: <S extends EffectedDraft<E>, H extends <R>(effected: EffectedDraft<E, E, R>) => S>(
197
    handler: H,
198
  ) => H;
199
};
200
export function defineHandlerFor() {
3✔
201
  return {
42✔
202
    with: (handler: any) => handler,
42✔
203
  };
42✔
204
}
42✔
205

206
/**
207
 * An effected program.
208
 */
209
export class Effected<out E extends Effect, out R> implements Iterable<E, R, unknown> {
3✔
210
  declare public readonly [Symbol.iterator]: () => Iterator<E, R, unknown>;
211

212
  declare public readonly runSync: [E] extends [never] ? () => R : UnhandledEffect<E>;
213
  declare public readonly runAsync: [E] extends [never] ? () => Promise<R> : UnhandledEffect<E>;
214
  declare public readonly runSyncUnsafe: () => R;
215
  declare public readonly runAsyncUnsafe: () => Promise<R>;
216

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

223
    this[Symbol.iterator] = fn;
5,730✔
224

225
    this.runSync = (() => runSync(this as never)) as never;
5,730✔
226
    this.runAsync = (() => runAsync(this as never)) as never;
5,730✔
227
    this.runSyncUnsafe = () => runSync(this as never);
5,730✔
228
    this.runAsyncUnsafe = () => runAsync(this as never);
5,730✔
229
  }
5,730✔
230

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

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

254
  /**
255
   * Combine multiple effected programs into one, running them in parallel and produces a tuple or
256
   * object with the results.
257
   * @param effects An iterable of effected programs or an object with effected programs as values.
258
   * @returns
259
   *
260
   * @see {@linkcode Effected.allSeq} for the sequential version.
261
   *
262
   * @since 0.3.2
263
   */
264
  static all<const ES extends Iterable<Effected<Effect, unknown>>>(
265
    effects: ES,
266
  ): Effected<
267
    ES extends Iterable<infer E> ?
268
      [E] extends [never] ? never
269
      : [E] extends [Effected<infer E, unknown>] ? E
270
      : never
271
    : never,
272
    ES extends readonly unknown[] ?
273
      { -readonly [K in keyof ES]: ES[K] extends Effected<Effect, infer R> ? R : never }
274
    : ES extends Iterable<infer E> ?
275
      [E] extends [Effected<Effect, infer R>] ?
276
        R[]
277
      : never
278
    : never
279
  >;
280
  static all<const O extends Record<string, Effected<Effect, unknown>>>(
281
    effects: O,
282
  ): Effected<
283
    O[keyof O] extends infer E ?
284
      [E] extends [never] ? never
285
      : [E] extends [Effected<infer E, unknown>] ? E
286
      : never
287
    : never,
288
    { -readonly [K in keyof O]: O[K] extends Effected<Effect, infer R> ? R : never }
289
  >;
290
  static all(
3✔
291
    effects: Iterable<Effected<Effect, unknown>> | Record<string, Effected<Effect, unknown>>,
48✔
292
  ): Effected<Effect, unknown> {
48✔
293
    return effected(() => {
48✔
294
      const isIterable = Symbol.iterator in effects;
54✔
295
      const keys: (string | number)[] = [];
54✔
296
      const iterators: Iterator<Effect, unknown, unknown>[] = [];
54✔
297
      if (isIterable) {
54✔
298
        for (const e of effects) iterators.push(e[Symbol.iterator]());
45✔
299
        Array.prototype.push.apply(
45✔
300
          keys,
45✔
301
          Array.from({ length: iterators.length }, (_, i) => i),
45✔
302
        );
45✔
303
      } else {
54✔
304
        for (const key in effects) {
9✔
305
          if (!Object.prototype.hasOwnProperty.call(effects, key)) continue;
18!
306
          keys.push(key);
18✔
307
          iterators.push(effects[key]![Symbol.iterator]());
18✔
308
        }
18✔
309
      }
9✔
310

311
      if (keys.length === 0) return { next: () => ({ done: true, value: isIterable ? [] : {} }) };
54✔
312

313
      const label = Symbol(keys.length === 2 ? "inner" : "outer");
54✔
314

315
      const results: any = isIterable ? new Array(keys.length) : {};
54✔
316
      const states = Array.from(
54✔
317
        { length: keys.length },
54✔
318
        () => "idle" as "idle" | "pending" | "done",
54✔
319
      );
54✔
320
      let recover: ((payload: { _effectRecover: symbol; index: number }) => void) | null = null;
54✔
321

322
      let index = 0;
54✔
323
      const nextIdleIndex = () => {
54✔
324
        let i = index;
144✔
325
        do i = (i + 1) % keys.length;
144✔
326
        while (states[i] !== "idle" && i !== index);
144✔
327
        return i === index ? null : i;
144✔
328
      };
144✔
329

330
      return {
54✔
331
        next: (...args: [] | [unknown]) => {
54✔
332
          if (states.every((s) => s === "done")) return { done: true, value: results };
270!
333

334
          if (args[0] != null && (args[0] as any)._effectInterrupt === label) {
270✔
335
            const currIndex = index;
66✔
336
            states[currIndex] = "pending";
66✔
337
            void ((args[0] as any).with as Promise<unknown>).then((value) => {
66✔
338
              results[keys[currIndex]!] = value;
66✔
339
              states[currIndex] = "idle";
66✔
340
              recover!({ _effectRecover: label, index: currIndex });
66✔
341
            });
66✔
342
            const nextIndex = nextIdleIndex();
66✔
343

344
            if (!nextIndex)
66✔
345
              return {
66✔
346
                done: false,
27✔
347
                value: {
27✔
348
                  _effectAsync: true,
27✔
349
                  onComplete: (callback: NonNullable<typeof recover>) => {
27✔
350
                    recover = callback;
27✔
351
                  },
27✔
352
                } as never,
27✔
353
              };
27✔
354

355
            index = nextIndex;
39✔
356
            args = [results[keys[index]!]];
39✔
357
          }
39✔
358

359
          if (args[0] != null && (args[0] as any)._effectRecover === label) {
270✔
360
            index = (args[0] as any).index;
66✔
361
            args = [results[keys[index]!]];
66✔
362
          }
66✔
363

364
          let iterator = iterators[index]!;
243✔
365
          let result = iterator.next(...args);
243✔
366

367
          while (result.done) {
270✔
368
            states[index] = "done";
111✔
369
            results[keys[index]!] = result.value;
111✔
370
            if (states.every((s) => s === "done")) {
111✔
371
              return { done: true, value: results };
33✔
372
            } else {
111✔
373
              const nextIndex = nextIdleIndex();
78✔
374
              if (!nextIndex)
78✔
375
                return {
78✔
376
                  done: false,
39✔
377
                  value: {
39✔
378
                    _effectAsync: true,
39✔
379
                    onComplete: (callback: NonNullable<typeof recover>) => {
39✔
380
                      recover = callback;
39✔
381
                    },
39✔
382
                  } as never,
39✔
383
                };
39✔
384
              index = nextIndex;
39✔
385
              args = [results[keys[index]!]];
39✔
386
              iterator = iterators[index]!;
39✔
387
              result = iterator.next(...args);
39✔
388
            }
39✔
389
          }
111✔
390

391
          if (
171✔
392
            (result.value instanceof Effect || (result.value as any)._effectAsync) &&
270✔
393
            !("interruptable" in (result.value as any))
171✔
394
          )
395
            (result.value as any).interruptable = label;
270✔
396

397
          return result;
171✔
398
        },
270✔
399
      };
54✔
400
    });
48✔
401
  }
48✔
402

403
  /**
404
   * Combine multiple effected programs into one, running them sequentially and produces a tuple or
405
   * object with the results.
406
   * @param effects An iterable of effected programs or an object with effected programs as values.
407
   * @returns
408
   *
409
   * @see {@linkcode Effected.all} for the parallel version.
410
   *
411
   * @since 0.3.2
412
   */
413
  static allSeq<const ES extends Iterable<Effected<Effect, unknown>>>(
414
    effects: ES,
415
  ): Effected<
416
    ES extends Iterable<infer E> ?
417
      [E] extends [never] ? never
418
      : [E] extends [Effected<infer E, unknown>] ? E
419
      : never
420
    : never,
421
    ES extends readonly unknown[] ?
422
      { -readonly [K in keyof ES]: ES[K] extends Effected<Effect, infer R> ? R : never }
423
    : ES extends Iterable<infer E> ?
424
      [E] extends [Effected<Effect, infer R>] ?
425
        R[]
426
      : never
427
    : never
428
  >;
429
  static allSeq<const O extends Record<string, Effected<Effect, unknown>>>(
430
    effects: O,
431
  ): Effected<
432
    O[keyof O] extends infer E ?
433
      [E] extends [never] ? never
434
      : E extends Effected<infer E, unknown> ? E
435
      : never
436
    : never,
437
    { -readonly [K in keyof O]: O[K] extends Effected<Effect, infer R> ? R : never }
438
  >;
439
  static allSeq(
3✔
440
    effects: Iterable<Effected<Effect, unknown>> | Record<string, Effected<Effect, unknown>>,
57✔
441
  ): Effected<Effect, unknown> {
57✔
442
    return effected(() => {
57✔
443
      const isIterable = Symbol.iterator in effects;
57✔
444
      const keys: (string | number)[] = [];
57✔
445
      const iterators: Iterator<Effect, unknown, unknown>[] = [];
57✔
446
      if (isIterable) {
57✔
447
        for (const e of effects) iterators.push(e[Symbol.iterator]());
39✔
448
        Array.prototype.push.apply(
39✔
449
          keys,
39✔
450
          Array.from({ length: iterators.length }, (_, i) => i),
39✔
451
        );
39✔
452
      } else {
57✔
453
        for (const key in effects) {
18✔
454
          if (!Object.prototype.hasOwnProperty.call(effects, key)) continue;
45!
455
          keys.push(key);
45✔
456
          iterators.push(effects[key]![Symbol.iterator]());
45✔
457
        }
45✔
458
      }
18✔
459
      const results: any = isIterable ? new Array(keys.length) : {};
57✔
460
      let index = 0;
57✔
461

462
      return {
57✔
463
        next: (...args: [] | [unknown]) => {
57✔
464
          while (index < keys.length) {
165✔
465
            const key = keys[index]!;
252✔
466
            const iterator = iterators[index]!;
252✔
467
            const result = iterator.next(...args);
252✔
468
            if (!result.done) return result;
252✔
469
            results[key] = result.value;
138✔
470
            index++;
138✔
471
          }
138✔
472
          return { done: true, value: results };
51✔
473
        },
165✔
474
      };
57✔
475
    });
57✔
476
  }
57✔
477

478
  /**
479
   * Handle an effect with a handler.
480
   *
481
   * For more common use cases, see {@link resume} and {@link terminate}, which provide a more
482
   * concise syntax.
483
   * @param effect The effect name or a function to match the effect name.
484
   * @param handler The handler for the effect. The first argument is an object containing the
485
   * encountered effect instance, a `resume` function to resume the effect, and a `terminate`
486
   * function to terminate the effect. The rest of the arguments are the payloads of the effect.
487
   *
488
   * `resume` or `terminate` should be called exactly once in the handler. If you call them more
489
   * than once, a warning will be logged to the console. If neither of them is called, the effected
490
   * program will hang indefinitely.
491
   *
492
   * Calling `resume` or `terminate` in an asynchronous context is also supported. It is _not_
493
   * required to call them synchronously.
494
   * @returns
495
   */
496
  handle<Name extends E["name"], T = R, F extends Effect = never>(
497
    effect: Name,
498
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
499
      (
500
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
501
        ...payloads: Payloads
502
        // TODO: Define a type alias to reduce repetition
503
        // eslint-disable-next-line sonarjs/use-type-alias
504
      ) => void | Generator<F, void, unknown> | Effected<F, void>
505
    : E extends Effect<Name, infer Payloads, infer R> ?
506
      (
507
        {
508
          effect,
509
          resume,
510
          terminate,
511
        }: {
512
          effect: Extract<E, Effect<Name>>;
513
          resume: (value: R) => void;
514
          terminate: (value: T) => void;
515
        },
516
        ...payloads: Payloads
517
      ) => void | Generator<F, void, unknown> | Effected<F, void>
518
    : never,
519
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
520
  handle<Name extends string | symbol, T = R, F extends Effect = never>(
521
    effect: (name: E["name"]) => name is Name,
522
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
523
      (
524
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
525
        ...payloads: Payloads
526
      ) => void | Generator<F, void, unknown> | Effected<F, void>
527
    : E extends Effect<Name, infer Payloads, infer R> ?
528
      (
529
        {
530
          effect,
531
          resume,
532
          terminate,
533
        }: {
534
          effect: Extract<E, Effect<Name>>;
535
          resume: (value: R) => void;
536
          terminate: (value: T) => void;
537
        },
538
        ...payloads: Payloads
539
      ) => void | Generator<F, void, unknown> | Effected<F, void>
540
    : never,
541
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
542
  handle(
3✔
543
    name: string | symbol | ((name: string | symbol) => boolean),
813✔
544
    handler: (...args: any[]) => unknown,
813✔
545
  ): Effected<any, unknown> {
813✔
546
    const matchEffect = (value: unknown) =>
813✔
547
      value instanceof Effect &&
2,715✔
548
      (typeof name === "function" ? name(value.name) : value.name === name);
1,782✔
549

550
    return effected(() => {
813✔
551
      const iterator = this[Symbol.iterator]();
924✔
552
      let interceptIterator: typeof iterator | null = null;
924✔
553
      let terminated: false | "with-value" | "without-value" = false;
924✔
554
      let terminatedValue: unknown;
924✔
555

556
      return {
924✔
557
        next: (...args: [] | [unknown]) => {
924✔
558
          if (terminated)
3,501✔
559
            return {
3,501✔
560
              done: true,
102✔
561
              ...(terminated === "with-value" ? { value: terminatedValue } : {}),
102✔
562
            } as IteratorReturnResult<unknown>;
102✔
563

564
          const result = (interceptIterator || iterator).next(...args);
3,501✔
565

566
          const { done, value } = result;
3,501✔
567
          if (done) return result;
3,501✔
568

569
          if (matchEffect(value)) {
3,501✔
570
            const effect = value;
1,200✔
571

572
            let resumed: false | "with-value" | "without-value" = false;
1,200✔
573
            let resumedValue: R;
1,200✔
574
            let onComplete: ((...args: [] | [R]) => void) | null = null;
1,200✔
575
            const warnMultipleHandling = (type: "resume" | "terminate", ...args: [] | [R]) => {
1,200✔
576
              let message = `Effect ${stringifyEffectNameQuoted(name)} has been handled multiple times`;
18✔
577
              message += " (received `";
18✔
578
              message += `${type} ${stringifyEffect(effect)}`;
18✔
579
              if (args.length > 0) message += ` with ${stringify(args[0])}`;
18✔
580
              message += "` after it has been ";
18✔
581
              if (resumed) {
18✔
582
                message += "resumed";
12✔
583
                if (resumed === "with-value") message += ` with ${stringify(resumedValue)}`;
12✔
584
              } else if (terminated) {
18✔
585
                message += "terminated";
6✔
586
                if (terminated === "with-value") message += ` with ${stringify(terminatedValue)}`;
6✔
587
              }
6✔
588
              message += "). Only the first handler will be used.";
18✔
589
              logger.warn(message);
18✔
590
            };
18✔
591
            const resume = (...args: [] | [R]) => {
1,200✔
592
              if (resumed || terminated) {
1,050✔
593
                warnMultipleHandling("resume", ...args);
12✔
594
                return;
12✔
595
              }
12✔
596
              resumed = args.length > 0 ? "with-value" : "without-value";
1,050✔
597
              if (args.length > 0) resumedValue = args[0]!;
1,050✔
598
              if (onComplete) {
1,050✔
599
                onComplete(...args);
111✔
600
                onComplete = null;
111✔
601
              }
111✔
602
            };
1,050✔
603
            const terminate = (...args: [] | [R]) => {
1,200✔
604
              if (resumed || terminated) {
108✔
605
                warnMultipleHandling("terminate", ...args);
6✔
606
                return;
6✔
607
              }
6✔
608
              terminated = args.length > 0 ? "with-value" : "without-value";
108✔
609
              if (args.length > 0) terminatedValue = args[0];
108✔
610
              if (onComplete) {
108✔
611
                onComplete(...args);
6✔
612
                onComplete = null;
6✔
613
              }
6✔
614
            };
108✔
615

616
            const constructHandledEffect = ():
1,200✔
617
              | { _effectSync: true; value?: unknown }
618
              | {
619
                  _effectAsync: true;
620
                  onComplete: (callback: (...args: [] | [R]) => void) => void;
621
                } => {
1,140✔
622
              // For synchronous effects
623
              if (resumed || terminated)
1,140✔
624
                return {
1,140✔
625
                  _effectSync: true,
1,020✔
626
                  ...(Object.is(resumed, "with-value") ? { value: resumedValue! }
1,020✔
627
                  : Object.is(terminated, "with-value") ? { value: terminatedValue! }
108✔
628
                  : {}),
18✔
629
                };
1,020✔
630
              // For asynchronous effects
631
              const handledEffect: ReturnType<typeof constructHandledEffect> = {
120✔
632
                _effectAsync: true,
120✔
633
                onComplete: (callback) => {
120✔
634
                  onComplete = callback;
117✔
635
                },
117✔
636
              };
120✔
637
              if ((effect as any).interruptable)
120✔
638
                (handledEffect as any).interruptable = (effect as any).interruptable;
876✔
639
              return handledEffect;
120✔
640
            };
1,140✔
641

642
            const handlerResult = handler(
1,200✔
643
              {
1,200✔
644
                effect,
1,200✔
645
                resume:
1,200✔
646
                  (effect as any).resumable === false ?
1,200✔
647
                    () => {
102✔
648
                      throw new Error(
9✔
649
                        `Cannot resume non-resumable effect: ${stringifyEffect(effect)}`,
9✔
650
                      );
9✔
651
                    }
9✔
652
                  : resume,
1,098✔
653
                terminate,
1,200✔
654
              },
1,200✔
655
              ...effect.payloads,
1,200✔
656
            );
1,200✔
657

658
            if (
1,200✔
659
              !(handlerResult instanceof Effected) &&
1,200✔
660
              !isGenerator(handlerResult) &&
1,149✔
661
              !isEffectedIterator(handlerResult)
1,140✔
662
            )
663
              return { done: false, value: constructHandledEffect() } as never;
1,200✔
664

665
            const iter =
60✔
666
              Symbol.iterator in handlerResult ? handlerResult[Symbol.iterator]() : handlerResult;
1,200✔
667
            interceptIterator = {
1,200✔
668
              next: (...args: [] | [unknown]) => {
1,200✔
669
                const result = iter.next(...args);
111✔
670
                if (result.done) {
111✔
671
                  interceptIterator = null;
51✔
672
                  return { done: false, value: constructHandledEffect() } as never;
51✔
673
                }
51✔
674
                return result as never;
60✔
675
              },
111✔
676
            };
1,200✔
677
            return interceptIterator.next();
1,200✔
678
          }
1,200✔
679

680
          return result;
1,515✔
681
        },
3,501✔
682
      };
924✔
683
    });
813✔
684
  }
813✔
685

686
  /**
687
   * Resume an effect with the return value of the handler.
688
   *
689
   * It is a shortcut for
690
   * `handle(effect, ({ resume }, ...payloads) => resume(handler(...payloads)))`.
691
   * @param effect The effect name or a function to match the effect name.
692
   * @param handler The handler for the effect. The arguments are the payloads of the effect.
693
   * @returns
694
   *
695
   * @see {@link handle}
696
   */
697
  resume<Name extends Exclude<E, Unresumable<Effect>>["name"], F extends Effect = never>(
698
    effect: Name,
699
    handler: E extends Effect<Name, infer Payloads, infer R> ?
700
      // TODO: Define a type alias to reduce repetition
701
      // eslint-disable-next-line sonarjs/use-type-alias
702
      (...payloads: Payloads) => R | Generator<F, R, unknown> | Effected<F, R>
703
    : never,
704
  ): Effected<Exclude<E, Effect<Name>> | F, R>;
705
  resume<Name extends string | symbol, F extends Effect = never>(
706
    effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
707
    handler: E extends Effect<Name, infer Payloads, infer R> ?
708
      (...payloads: Payloads) => R | Generator<F, R, unknown> | Effected<F, R>
709
    : never,
710
  ): Effected<Exclude<E, Effect<Name>> | F, R>;
711
  resume(effect: any, handler: (...payloads: unknown[]) => unknown) {
3✔
712
    return this.handle(effect, (({ resume }: any, ...payloads: unknown[]) => {
486✔
713
      const it = handler(...payloads);
897✔
714
      if (!(it instanceof Effected) && !isGenerator(it)) return resume(it);
897✔
715
      const iterator = it[Symbol.iterator]();
45✔
716
      return {
45✔
717
        _effectedIterator: true,
45✔
718
        next: (...args: [] | [unknown]) => {
45✔
719
          const result = iterator.next(...args);
81✔
720
          if (result.done) return { done: true, value: resume(result.value) };
81✔
721
          return result;
45✔
722
        },
81✔
723
      };
45✔
724
    }) as never);
486✔
725
  }
486✔
726

727
  /**
728
   * Terminate an effect with the return value of the handler.
729
   *
730
   * It is a shortcut for
731
   * `handle(effect, ({ terminate }, ...payloads) => terminate(handler(...payloads)))`.
732
   * @param effect The effect name or a function to match the effect name.
733
   * @param handler The handler for the effect. The arguments are the payloads of the effect.
734
   * @returns
735
   *
736
   * @see {@link handle}
737
   */
738
  terminate<Name extends E["name"], T, F extends Effect = never>(
739
    effect: Name,
740
    handler: E extends Effect<Name, infer Payloads> ?
741
      (...payloads: Payloads) => Generator<F, T, unknown> | Effected<F, T>
742
    : never,
743
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
744
  terminate<Name extends string | symbol, T, F extends Effect = never>(
745
    effect: (name: E["name"]) => name is Name,
746
    handler: E extends Effect<Name, infer Payloads> ?
747
      (...payloads: Payloads) => Generator<F, T, unknown> | Effected<F, T>
748
    : never,
749
  ): Effected<Exclude<E, Effect<Name>> | F, R | T>;
750
  terminate<Name extends E["name"], T>(
751
    effect: Name,
752
    handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
753
  ): Effected<Exclude<E, Effect<Name>>, R | T>;
754
  terminate<Name extends string | symbol, T>(
755
    effect: (name: E["name"]) => name is Name,
756
    handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
757
  ): Effected<Exclude<E, Effect<Name>>, R | T>;
758
  terminate(effect: any, handler: (...payloads: unknown[]) => unknown) {
3✔
759
    return this.handle(effect, (({ terminate }: any, ...payloads: unknown[]) => {
168✔
760
      const it = handler(...payloads);
75✔
761
      if (!(it instanceof Effected) && !isGenerator(it)) return terminate(it);
75✔
762
      const iterator = it[Symbol.iterator]();
3✔
763
      return {
3✔
764
        _effectedIterator: true,
3✔
765
        next: (...args: [] | [unknown]) => {
3✔
766
          const result = iterator.next(...args);
6✔
767
          if (result.done) return { done: true, value: terminate(result.value) };
6✔
768
          return result;
3✔
769
        },
6✔
770
      };
3✔
771
    }) as never);
168✔
772
  }
168✔
773

774
  /**
775
   * Overwrite the return value of the effected program with a new value.
776
   * @param value The new value to return.
777
   * @returns
778
   *
779
   * @since 0.3.2
780
   */
781
  as<S>(value: S): Effected<E, S> {
3✔
782
    return this.map(() => value);
48✔
783
  }
48✔
784
  /**
785
   * Overwrite the return value of the effected program with `void`.
786
   * @returns
787
   *
788
   * @since 0.3.2
789
   */
790
  asVoid(): Effected<E, void> {
3✔
791
    return this.as(undefined);
24✔
792
  }
24✔
793

794
  /**
795
   * Maps the return value using a pure function without handling effects.
796
   * Optimized for the simple value transformation case.
797
   * @param mapper The function to transform the result value.
798
   * @returns
799
   *
800
   * @since 0.3.2
801
   */
802
  map<S>(mapper: (value: R) => S): Effected<E, S> {
3✔
803
    return effected(() => {
87✔
804
      const iterator = this[Symbol.iterator]();
102✔
805
      return {
102✔
806
        next: (...args: [] | [unknown]) => {
102✔
807
          const result = iterator.next(...args);
132✔
808
          if (!result.done) return result;
132✔
809
          return { done: true, value: mapper(result.value) };
102✔
810
        },
132✔
811
      };
102✔
812
    });
87✔
813
  }
87✔
814

815
  /**
816
   * Chains an effected program after the current one, where the chained effected program will
817
   * receive the return value of the current one.
818
   * @param mapper A function that returns an effected program or generator.
819
   * @returns
820
   *
821
   * @since 0.3.2
822
   */
823
  flatMap<S, F extends Effect = never>(
3✔
824
    mapper: (value: R) => Generator<F, S, unknown> | Effected<F, S>,
24✔
825
  ): Effected<E | F, S> {
24✔
826
    return effected(() => {
24✔
827
      const iterator = this[Symbol.iterator]();
33✔
828
      let originalDone = false;
33✔
829
      let appendedIterator: Iterator<Effect, unknown, unknown>;
33✔
830
      return {
33✔
831
        next: (...args: [] | [R]) => {
33✔
832
          if (originalDone) return appendedIterator.next(...args);
84✔
833
          const result = iterator.next(...args);
48✔
834
          if (!result.done) return result;
84✔
835
          originalDone = true;
33✔
836
          const it = mapper(result.value);
33✔
837
          appendedIterator = it[Symbol.iterator]();
33✔
838
          return appendedIterator.next();
33✔
839
        },
84✔
840
      };
33✔
841
    }) as never;
24✔
842
  }
24✔
843

844
  /**
845
   * Chains another function or effected program after the current one, where the chained function
846
   * or effected program will receive the return value of the current one.
847
   * @param handler The function or effected program to chain after the current one.
848
   * @returns
849
   *
850
   * @since 0.3.0
851
   */
852
  andThen<S, F extends Effect = never>(
853
    handler: (value: R) => Generator<F, S, unknown> | Effected<F, S> | S,
854
  ): Effected<E | F, S>;
855
  andThen(handler: (value: R) => unknown): Effected<Effect, unknown> {
3✔
856
    return effected(() => {
1,257✔
857
      const iterator = this[Symbol.iterator]();
1,284✔
858
      let originalIteratorDone = false;
1,284✔
859
      let appendedIterator: Iterator<Effect, unknown, unknown>;
1,284✔
860
      return {
1,284✔
861
        next: (...args: [] | [R]) => {
1,284✔
862
          if (originalIteratorDone) return appendedIterator.next(...args);
1,563✔
863
          const result = iterator.next(...args);
1,494✔
864
          if (!result.done) return result;
1,563✔
865
          originalIteratorDone = true;
1,230✔
866
          const it = handler(result.value);
1,230✔
867
          if (!(it instanceof Effected) && !isGenerator(it) && !isEffectedIterator(it))
1,563✔
868
            return { done: true, value: it };
1,563✔
869
          appendedIterator = Symbol.iterator in it ? it[Symbol.iterator]() : it;
1,563✔
870
          return appendedIterator.next();
1,563✔
871
        },
1,563✔
872
      };
1,284✔
873
    });
1,257✔
874
  }
1,257✔
875

876
  /**
877
   * Tap the return value of the effected program.
878
   * @param handler The function to tap the return value.
879
   * @returns
880
   *
881
   * @since 0.2.1
882
   */
883
  tap<F extends Effect = never>(
3✔
884
    handler: (value: R) => void | Generator<F, void, unknown> | Effected<F, void>,
36✔
885
  ): Effected<E | F, R> {
36✔
886
    return this.andThen((value) => {
36✔
887
      const it = handler(value);
48✔
888
      if (!(it instanceof Effected) && !isGenerator(it)) return value;
48✔
889
      const iterator = it[Symbol.iterator]();
24✔
890
      return {
24✔
891
        _effectedIterator: true,
24✔
892
        next: (...args: [] | [unknown]) => {
24✔
893
          const result = iterator.next(...args);
48✔
894
          if (result.done) return { done: true, value };
48✔
895
          return result;
24✔
896
        },
48✔
897
      };
24✔
898
    }) as never;
36✔
899
  }
36✔
900

901
  /**
902
   * Catch an error effect with a handler.
903
   *
904
   * It is a shortcut for `terminate("error:" + name, handler)`.
905
   * @param name The name of the error effect.
906
   * @param handler The handler for the error effect. The argument is the message of the error.
907
   * @returns
908
   *
909
   * @see {@link terminate}
910
   */
911
  catch<Name extends ErrorName<E>, T, F extends Effect = never>(
912
    effect: Name,
913
    handler: (message?: string) => Generator<F, T, unknown> | Effected<F, T>,
914
  ): Effected<Exclude<E, Effect.Error<Name>> | F, R | T>;
915
  catch<Name extends ErrorName<E>, T>(
916
    effect: Name,
917
    handler: (message?: string) => T,
918
  ): Effected<Exclude<E, Effect.Error<Name>>, R | T>;
919
  catch(name: string, handler: (message?: string) => unknown): Effected<Effect, unknown> {
3✔
920
    return this.terminate(`error:${name}` as never, handler as never);
99✔
921
  }
99✔
922

923
  /**
924
   * Catch all error effects with a handler.
925
   * @param handler The handler for the error effect. The first argument is the name of the error
926
   * effect (without the `"error:"` prefix), and the second argument is the message of the error.
927
   */
928
  catchAll<T, F extends Effect = never>(
929
    handler: (error: ErrorName<E>, message?: string) => Generator<F, T, unknown> | Effected<F, T>,
930
  ): Effected<Exclude<E, Effect.Error> | F, R | T>;
931
  catchAll<T>(
932
    handler: (error: ErrorName<E>, message?: string) => T,
933
  ): Effected<Exclude<E, Effect.Error>, R | T>;
934
  catchAll(handler: (error: ErrorName<E>, message?: string) => unknown): Effected<Effect, unknown> {
3✔
935
    return this.handle(
42✔
936
      (name): name is ErrorName<E> => typeof name === "string" && name.startsWith("error:"),
42✔
937
      (({ effect, terminate }: any, ...payloads: [message?: string]) => {
42✔
938
        const error = effect.name.slice(6) as ErrorName<E>;
33✔
939
        const it = handler(error, ...payloads);
33✔
940
        if (!(it instanceof Effected) && !isGenerator(it)) return terminate(it);
33✔
941
        const iterator = it[Symbol.iterator]();
3✔
942
        return {
3✔
943
          _effectedIterator: true,
3✔
944
          next: (...args: [] | [unknown]) => {
3✔
945
            const result = iterator.next(...args);
6✔
946
            if (result.done) return { done: true, value: terminate(result.value) };
6✔
947
            return result;
3✔
948
          },
6✔
949
        };
3✔
950
      }) as never,
33✔
951
    );
42✔
952
  }
42✔
953

954
  /**
955
   * Catch an error effect and throw it as an error.
956
   * @param name The name of the error effect.
957
   * @param message The message of the error. If it is a function, it will be called with the
958
   * message of the error effect, and the return value will be used as the message of the error.
959
   * @returns
960
   *
961
   * @since 0.1.1
962
   */
963
  catchAndThrow<Name extends ErrorName<E>>(
3✔
964
    name: Name,
18✔
965
    message?: string | ((message?: string) => string | undefined),
18✔
966
  ): Effected<Exclude<E, Effect.Error<Name>>, R> {
18✔
967
    return this.catch(name, (...args) => {
18✔
968
      throw new (buildErrorClass(name))(
18✔
969
        ...(typeof message === "string" ? [message]
18✔
970
        : typeof message === "function" ? [message(...args)].filter((v) => v !== undefined)
12✔
971
        : args),
6✔
972
      );
18✔
973
    });
18✔
974
  }
18✔
975

976
  /**
977
   * Catch all error effects and throw them as an error.
978
   * @param message The message of the error. If it is a function, it will be called with the name
979
   * and the message of the error effect, and the return value will be used as the message of the
980
   * error.
981
   * @returns
982
   *
983
   * @since 0.1.1
984
   */
985
  catchAllAndThrow(
3✔
986
    message?: string | ((error: string, message?: string) => string | undefined),
18✔
987
  ): Effected<Exclude<E, Effect.Error>, R> {
18✔
988
    return this.catchAll((error, ...args) => {
18✔
989
      throw new (buildErrorClass(error))(
18✔
990
        ...(typeof message === "string" ? [message]
18✔
991
        : typeof message === "function" ? [message(error, ...args)].filter((v) => v !== undefined)
12✔
992
        : args),
6✔
993
      );
18✔
994
    });
18✔
995
  }
18✔
996

997
  /**
998
   * Provide a value for a dependency effect.
999
   * @param name The name of the dependency.
1000
   * @param value The value to provide for the dependency.
1001
   * @returns
1002
   */
1003
  provide<Name extends DependencyName<E>>(
3✔
1004
    name: Name,
33✔
1005
    value: E extends Effect.Dependency<Name, infer R> ? R : never,
33✔
1006
  ): Effected<Exclude<E, Effect.Dependency<Name>>, R> {
33✔
1007
    return this.resume(`dependency:${name}` as never, (() => value) as never) as never;
33✔
1008
  }
33✔
1009

1010
  /**
1011
   * Provide a value for a dependency effect with a getter.
1012
   * @param name The name of the dependency.
1013
   * @param getter The getter to provide for the dependency.
1014
   * @returns
1015
   */
1016
  provideBy<Name extends DependencyName<E>, F extends Effect = never>(
3✔
1017
    name: Name,
21✔
1018
    getter: E extends Effect.Dependency<Name, infer R> ?
21✔
1019
      () => R | Generator<F, R, unknown> | Effected<F, R>
1020
    : never,
1021
  ): Effected<Exclude<E, Effect.Dependency<Name>> | F, R> {
21✔
1022
    return this.resume(`dependency:${name}`, getter as never) as never;
21✔
1023
  }
21✔
1024

1025
  /**
1026
   * Apply a handler to the effected program.
1027
   * @param handler The handler to apply to the effected program.
1028
   * @returns
1029
   */
1030
  with<F extends Effect, G extends Effect, S>(
1031
    handler: (effected: EffectedDraft<never, never, R>) => EffectedDraft<F, G, S>,
1032
  ): Effected<Exclude<E, F> | G, S>;
1033
  with<F extends Effect, S>(handler: (effected: Effected<E, R>) => Effected<F, S>): Effected<F, S>;
1034
  with(handler: (effected: any) => unknown) {
3✔
1035
    return handler(this);
84✔
1036
  }
84✔
1037
}
3✔
1038

1039
interface EffectedDraft<
1040
  out P extends Effect = Effect,
1041
  out E extends Effect = Effect,
1042
  out R = unknown,
1043
> extends Iterable<E, R, unknown> {
1044
  handle<Name extends E["name"], T = R, F extends Effect = never>(
1045
    effect: Name,
1046
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
1047
      (
1048
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
1049
        ...payloads: Payloads
1050
      ) => void | Generator<F, void, unknown> | Effected<F, void>
1051
    : E extends Effect<Name, infer Payloads, infer R> ?
1052
      (
1053
        {
1054
          effect,
1055
          resume,
1056
          terminate,
1057
        }: {
1058
          effect: Extract<E, Effect<Name>>;
1059
          resume: (value: R) => void;
1060
          terminate: (value: T) => void;
1061
        },
1062
        ...payloads: Payloads
1063
      ) => void | Generator<F, void, unknown> | Effected<F, void>
1064
    : never,
1065
  ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
1066
  handle<Name extends string | symbol, T = R, F extends Effect = never>(
1067
    effect: (name: E["name"]) => name is Name,
1068
    handler: E extends Unresumable<Effect<Name, infer Payloads>> ?
1069
      (
1070
        { effect, terminate }: { effect: Extract<E, Effect<Name>>; terminate: (value: T) => void },
1071
        ...payloads: Payloads
1072
      ) => void | Generator<F, void, unknown> | Effected<F, void>
1073
    : E extends Effect<Name, infer Payloads, infer R> ?
1074
      (
1075
        {
1076
          effect,
1077
          resume,
1078
          terminate,
1079
        }: {
1080
          effect: Extract<E, Effect<Name>>;
1081
          resume: (value: R) => void;
1082
          terminate: (value: T) => void;
1083
        },
1084
        ...payloads: Payloads
1085
      ) => void | Generator<F, void, unknown> | Effected<F, void>
1086
    : never,
1087
  ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
1088

1089
  resume<Name extends Exclude<E, Unresumable<Effect>>["name"], F extends Effect = never>(
1090
    effect: Name,
1091
    handler: E extends Effect<Name, infer Payloads, infer R> ?
1092
      (...payloads: Payloads) => R | Generator<F, R, unknown> | Effected<F, R>
1093
    : never,
1094
  ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R>;
1095
  resume<Name extends string | symbol, F extends Effect = never>(
1096
    effect: (name: Exclude<E, Unresumable<Effect>>["name"]) => name is Name,
1097
    handler: E extends Effect<Name, infer Payloads, infer R> ?
1098
      (...payloads: Payloads) => R | Generator<F, R, unknown> | Effected<F, R>
1099
    : never,
1100
  ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R>;
1101

1102
  terminate<Name extends E["name"], T, F extends Effect = never>(
1103
    effect: Name,
1104
    handler: E extends Effect<Name, infer Payloads> ?
1105
      (...payloads: Payloads) => Generator<F, T, unknown> | Effected<F, T>
1106
    : never,
1107
  ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
1108
  terminate<Name extends string | symbol, T, F extends Effect = never>(
1109
    effect: (name: E["name"]) => name is Name,
1110
    handler: E extends Effect<Name, infer Payloads> ?
1111
      (...payloads: Payloads) => Generator<F, T, unknown> | Effected<F, T>
1112
    : never,
1113
  ): EffectedDraft<P, Exclude<E, Effect<Name>> | F, R | T>;
1114
  terminate<Name extends E["name"], T>(
1115
    effect: Name,
1116
    handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
1117
  ): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
1118
  terminate<Name extends string | symbol, T>(
1119
    effect: (name: E["name"]) => name is Name,
1120
    handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
1121
  ): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
1122

1123
  as<S>(value: S): EffectedDraft<P, E, S>;
1124
  asVoid(): EffectedDraft<P, E, void>;
1125

1126
  map<S>(mapper: (value: R) => S): EffectedDraft<P, E, S>;
1127

1128
  flatMap<S, F extends Effect = never>(
1129
    mapper: (value: R) => Generator<F, S, unknown> | Effected<F, S>,
1130
  ): EffectedDraft<P, E | F, S>;
1131

1132
  andThen<S, F extends Effect = never>(
1133
    handler: (value: R) => Generator<F, S, unknown> | Effected<F, S> | S,
1134
  ): EffectedDraft<P, E | F, S>;
1135

1136
  tap<F extends Effect = never>(
1137
    handler: (value: R) => void | Generator<F, void, unknown> | Effected<F, void>,
1138
  ): EffectedDraft<P, E | F, R>;
1139

1140
  catch<Name extends ErrorName<E>, T, F extends Effect = never>(
1141
    effect: Name,
1142
    handler: (message?: string) => Generator<F, T, unknown> | Effected<F, T>,
1143
  ): EffectedDraft<P, Exclude<E, Effect.Error<Name>> | F, R | T>;
1144
  catch<Name extends ErrorName<E>, T>(
1145
    effect: Name,
1146
    handler: (message?: string) => T,
1147
  ): EffectedDraft<P, Exclude<E, Effect.Error<Name>>, R | T>;
1148

1149
  catchAll<T, F extends Effect = never>(
1150
    handler: (effect: ErrorName<E>, message?: string) => Generator<F, T, unknown> | Effected<F, T>,
1151
  ): Effected<Exclude<E, Effect.Error> | F, R | T>;
1152
  catchAll<T>(
1153
    handler: (effect: ErrorName<E>, message?: string) => T,
1154
  ): Effected<Exclude<E, Effect.Error>, R | T>;
1155

1156
  readonly catchAndThrow: <Name extends ErrorName<E>>(
1157
    name: Name,
1158
    message?: string | ((message?: string) => string | undefined),
1159
  ) => Effected<Exclude<E, Effect.Error<Name>>, R>;
1160

1161
  readonly catchAllAndThrow: (
1162
    message?: string | ((error: string, message?: string) => string | undefined),
1163
  ) => Effected<Exclude<E, Effect.Error>, R>;
1164

1165
  readonly provide: <Name extends DependencyName<E>>(
1166
    name: Name,
1167
    value: E extends Effect.Dependency<Name, infer R> ? R : never,
1168
  ) => EffectedDraft<P, Exclude<E, Effect.Dependency<Name>>, R>;
1169
  provideBy<Name extends DependencyName<E>, F extends Effect = never>(
1170
    name: Name,
1171
    getter: E extends Effect.Dependency<Name, infer R> ?
1172
      () => R | Generator<F, R, unknown> | Effected<F, R>
1173
    : never,
1174
  ): EffectedDraft<P, Exclude<E, Effect.Dependency<Name>> | F, R>;
1175

1176
  with<F extends Effect, G extends Effect, S>(
1177
    handler: (effected: EffectedDraft<never, never, R>) => EffectedDraft<F, G, S>,
1178
  ): EffectedDraft<P, Exclude<E, F> | G, S>;
1179
  with<F extends Effect, S>(
1180
    handler: (effected: Effected<E, R>) => Effected<F, S>,
1181
  ): EffectedDraft<P, F, S>;
1182
}
1183

1184
/**
1185
 * Create an effected program.
1186
 * @param fn A function that returns an iterator.
1187
 * @returns
1188
 *
1189
 * @example
1190
 * ```typescript
1191
 * type User = { id: number; name: string; role: "admin" | "user" };
1192
 *
1193
 * // Use `effect` and its variants to define factory functions for effects
1194
 * const println = effect("println")<unknown[], void>;
1195
 * const executeSQL = effect("executeSQL")<[sql: string, ...params: unknown[]], any>;
1196
 * const askCurrentUser = dependency("currentUser")<User | null>;
1197
 * const authenticationError = error("authentication");
1198
 * const unauthorizedError = error("unauthorized");
1199
 *
1200
 * // Use `effected` to define an effected program
1201
 * const requiresAdmin = () => effected(function* () {
1202
 *   const currentUser = yield* askCurrentUser();
1203
 *   if (!currentUser) return yield* authenticationError();
1204
 *   if (currentUser.role !== "admin")
1205
 *     return yield* unauthorizedError(`User "${currentUser.name}" is not an admin`);
1206
 * });
1207
 *
1208
 * // You can yield other effected programs in an effected program
1209
 * const createUser = (user: Omit<User, "id">) => effected(function* () {
1210
 *   yield* requiresAdmin();
1211
 *   const id = yield* executeSQL("INSERT INTO users (name) VALUES (?)", user.name);
1212
 *   const savedUser: User = { id, ...user };
1213
 *   yield* println("User created:", savedUser);
1214
 *   return savedUser;
1215
 * });
1216
 *
1217
 * const program = effected(function* () {
1218
 *   yield* createUser({ name: "Alice", role: "user" });
1219
 *   yield* createUser({ name: "Bob", role: "admin" });
1220
 * })
1221
 *   // Handle effects with the `.handle()` method
1222
 *   .handle("executeSQL", function* ({ resume, terminate }, sql, ...params) {
1223
 *     // You can yield other effects in a handler using a generator function
1224
 *     yield* println("Executing SQL:", sql, ...params);
1225
 *     // Asynchronous effects are supported
1226
 *     db.execute(sql, params, (err, result) => {
1227
 *       if (err) return terminate(err);
1228
 *       resume(result);
1229
 *     });
1230
 *   })
1231
 *   // a shortcut for `.handle()` that resumes the effect with the return value of the handler
1232
 *   .resume("println", (...args) => console.log(...args))
1233
 *   // Other shortcuts for special effects (error effects and dependency effects)
1234
 *   .provide("currentUser", { id: 1, name: "Charlie", role: "admin" })
1235
 *   .catch("authentication", () => console.error("Authentication error"));
1236
 *   .catch("unauthorized", () => console.error("Unauthorized error"));
1237
 *
1238
 * // Run the effected program with `.runSync()` or `.runAsync()`
1239
 * await program.runAsync();
1240
 * ```
1241
 *
1242
 * @see {@link effect}
1243
 */
1244
export function effected<E extends Effect, R>(fn: () => Iterator<E, R, unknown>): Effected<E, R> {
3✔
1245
  return new (Effected as any)(
5,727✔
1246
    fn,
5,727✔
1247
    "Yes, I’m sure I want to call the constructor of Effected directly.",
5,727✔
1248
  );
5,727✔
1249
}
5,727✔
1250

1251
/**
1252
 * Convert a {@link Promise} to an effected program containing a single {@link Effect}.
1253
 * @param promise The promise to effectify.
1254
 * @returns
1255
 *
1256
 * ```typescript
1257
 * // Assume we have `db.user.create(user: User): Promise<number>`
1258
 * const createUser = (user: Omit<User, "id">) => effected(function* () {
1259
 *   yield* requiresAdmin();
1260
 *   // Use `yield* effectify(...)` instead of `await ...` in an effected program
1261
 *   const id = yield* effectify(db.user.create(user));
1262
 *   const savedUser = { id, ...user };
1263
 *   yield* println("User created:", savedUser);
1264
 *   return savedUser;
1265
 * });
1266
 * ```
1267
 */
1268
export function effectify<T>(promise: Promise<T>): Effected<never, T> {
3✔
1269
  return effected(() => {
27✔
1270
    let state = 0;
27✔
1271
    return {
27✔
1272
      next: (...args) => {
27✔
1273
        switch (state) {
51✔
1274
          case 0:
51✔
1275
            state++;
27✔
1276
            return {
27✔
1277
              done: false,
27✔
1278
              value: {
27✔
1279
                _effectAsync: true,
27✔
1280
                onComplete: (
27✔
1281
                  ...args: [onComplete: (value: T) => void, onThrow?: (value: unknown) => void]
21✔
1282
                ) => promise.then(...args),
21✔
1283
              } as never,
27✔
1284
            };
27✔
1285
          case 1:
51✔
1286
            state++;
21✔
1287
            return {
21✔
1288
              done: true,
21✔
1289
              ...(args.length > 0 ? { value: args[0] } : {}),
21!
1290
            } as IteratorReturnResult<T>;
21✔
1291
          default:
51✔
1292
            return { done: true } as IteratorReturnResult<T>;
3✔
1293
        }
51✔
1294
      },
51✔
1295
    };
27✔
1296
  });
27✔
1297
}
27✔
1298

1299
/**
1300
 * Run an effected program synchronously and return its result.
1301
 * @param effected The effected program.
1302
 * @returns
1303
 *
1304
 * @throws {UnhandledEffectError} If an unhandled effect is encountered.
1305
 * @throws {Error} If an asynchronous effect is encountered.
1306
 */
1307
export function runSync<E extends Effected<Effect, unknown>>(
3✔
1308
  effected: E extends Effected<infer F extends Effect, unknown> ?
624✔
1309
    [F] extends [never] ?
1310
      E
1311
    : UnhandledEffect<F>
1312
  : never,
1313
): E extends Effected<Effect, infer R> ? R : never {
624✔
1314
  const iterator = (effected as Iterable<any>)[Symbol.iterator]();
624✔
1315
  let { done, value } = iterator.next();
624✔
1316
  while (!done) {
624✔
1317
    if (!value)
954✔
1318
      throw new Error(
954✔
1319
        `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1320
      );
3✔
1321
    if (value._effectSync) {
954✔
1322
      ({ done, value } = iterator.next(...("value" in value ? [value.value] : [])));
840✔
1323
      continue;
840✔
1324
    }
840✔
1325
    if (value._effectAsync)
111✔
1326
      throw new Error(
693✔
1327
        "Cannot run an asynchronous effected program with `runSync`, use `runAsync` instead",
6✔
1328
      );
6✔
1329
    if (value instanceof Effect)
105✔
1330
      throw new UnhandledEffectError(value, `Unhandled effect: ${stringifyEffect(value)}`);
396✔
1331
    throw new Error(
3✔
1332
      `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1333
    );
3✔
1334
  }
3✔
1335
  return value;
465✔
1336
}
465✔
1337

1338
/**
1339
 * Run a (possibly) asynchronous effected program and return its result as a {@link Promise}.
1340
 * @param effected The effected program.
1341
 * @returns
1342
 *
1343
 * @throws {UnhandledEffectError} If an unhandled effect is encountered.
1344
 */
1345
export function runAsync<E extends Effected<Effect, unknown>>(
3✔
1346
  effected: E extends Effected<infer F extends Effect, unknown> ?
126✔
1347
    [F] extends [never] ?
1348
      E
1349
    : UnhandledEffect<F>
1350
  : never,
1351
): Promise<E extends Effected<Effect, infer R> ? R : never> {
126✔
1352
  const iterator = (effected as Iterable<any>)[Symbol.iterator]();
126✔
1353

1354
  return new Promise((resolve, reject) => {
126✔
1355
    const iterate = (...args: [] | [unknown]) => {
126✔
1356
      let done: boolean | undefined;
261✔
1357
      let value: any;
261✔
1358
      try {
261✔
1359
        ({ done, value } = iterator.next(...args));
261✔
1360
      } catch (e) {
261✔
1361
        // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1362
        reject(e);
6✔
1363
        return;
6✔
1364
      }
6✔
1365

1366
      // We use a while loop to avoid stack overflow when there are many synchronous effects
1367
      while (!done) {
261✔
1368
        if (!value) {
408✔
1369
          reject(
3✔
1370
            new Error(
3✔
1371
              `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1372
            ),
3✔
1373
          );
3✔
1374
          return;
3✔
1375
        }
3✔
1376
        if (value._effectSync) {
405✔
1377
          try {
180✔
1378
            ({ done, value } = iterator.next(...("value" in value ? [value.value] : [])));
180!
1379
          } catch (e) {
180✔
1380
            // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1381
            reject(e);
3✔
1382
            return;
3✔
1383
          }
3✔
1384
          continue;
177✔
1385
        }
177✔
1386
        if (value._effectAsync) {
312✔
1387
          if (value.interruptable) {
204✔
1388
            let resolve!: (value: unknown) => void;
66✔
1389
            const promise = new Promise((_resolve) => (resolve = _resolve));
66✔
1390
            // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1391
            value.onComplete(resolve, (...args: unknown[]) => reject(...args.slice(0, 1)));
66✔
1392
            ({ done, value } = iterator.next({
66✔
1393
              _effectInterrupt: value.interruptable,
66✔
1394
              with: promise,
66✔
1395
            }));
66✔
1396
            continue;
66✔
1397
          } else {
204✔
1398
            // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1399
            value.onComplete(iterate, (...args: unknown[]) => reject(...args.slice(0, 1)));
138✔
1400
            return;
138✔
1401
          }
138✔
1402
        }
204✔
1403
        if (value instanceof Effect) {
255✔
1404
          reject(new UnhandledEffectError(value, `Unhandled effect: ${stringifyEffect(value)}`));
18✔
1405
          return;
18✔
1406
        }
18✔
1407
        reject(
3✔
1408
          new Error(
3✔
1409
            `Invalid effected program: an effected program should yield only effects (received ${stringify(value)})`,
3✔
1410
          ),
3✔
1411
        );
3✔
1412
        return;
3✔
1413
      }
3✔
1414

1415
      resolve(value);
90✔
1416
    };
261✔
1417

1418
    iterate();
126✔
1419
  });
126✔
1420
}
126✔
1421

1422
/*********************
1423
 * Utility functions *
1424
 *********************/
1425
/**
1426
 * Check if a value is a {@link Generator}.
1427
 * @param value The value to check.
1428
 * @returns
1429
 */
1430
const isGenerator = (value: unknown): value is Generator =>
3✔
1431
  Object.prototype.toString.call(value) === "[object Generator]";
2,799✔
1432

1433
/**
1434
 * Check if a value is an `EffectedIterator` (i.e., an {@link Iterator} with an `_effectedIterator`
1435
 * property set to `true`).
1436
 *
1437
 * This is only used internally as an alternative to generators to reduce the overhead of creating
1438
 * generator functions.
1439
 * @param value The value to check.
1440
 * @returns
1441
 */
1442
const isEffectedIterator = (
3✔
1443
  value: unknown,
1,788✔
1444
): value is Iterator<Effect, unknown, unknown> & { _effectedIterator: true } =>
1445
  typeof value === "object" && value !== null && (value as any)._effectedIterator === true;
1,788✔
1446

1447
/**
1448
 * Capitalize the first letter of a string.
1449
 * @param str The string to capitalize.
1450
 * @returns
1451
 */
1452
const capitalize = (str: string) => {
3✔
1453
  if (str.length === 0) return str;
129!
1454
  return str[0]!.toUpperCase() + str.slice(1);
129✔
1455
};
129✔
1456

1457
/**
1458
 * Change the name of a function for better debugging experience.
1459
 * @param fn The function to rename.
1460
 * @param name The new name of the function.
1461
 * @returns
1462
 */
1463
const renameFunction = <F extends (...args: never) => unknown>(fn: F, name: string): F =>
3✔
1464
  Object.defineProperty(fn, "name", {
780✔
1465
    value: name,
780✔
1466
    writable: false,
780✔
1467
    enumerable: false,
780✔
1468
    configurable: true,
780✔
1469
  });
780✔
1470

1471
const buildErrorClass = (name: string) => {
3✔
1472
  const ErrorClass = class extends Error {};
36✔
1473
  let errorName = capitalize(name);
36✔
1474
  if (!errorName.endsWith("Error") && !errorName.endsWith("error")) errorName += "Error";
36✔
1475
  Object.defineProperty(ErrorClass, "name", {
36✔
1476
    value: errorName,
36✔
1477
    writable: false,
36✔
1478
    enumerable: false,
36✔
1479
    configurable: true,
36✔
1480
  });
36✔
1481
  Object.defineProperty(ErrorClass.prototype, "name", {
36✔
1482
    value: errorName,
36✔
1483
    writable: true,
36✔
1484
    enumerable: false,
36✔
1485
    configurable: true,
36✔
1486
  });
36✔
1487
  return ErrorClass;
36✔
1488
};
36✔
1489

1490
const stringifyEffectName = (name: string | symbol | ((...args: never) => unknown)) =>
3✔
1491
  typeof name === "string" ? name
153✔
1492
  : typeof name === "symbol" ? name.toString()
9✔
1493
  : "[" + name.name + "]";
3✔
1494

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

1498
const stringifyEffect = (effect: Effect) =>
3✔
1499
  `${stringifyEffectName(effect.name)}(${effect.payloads.map(stringify).join(", ")})`;
147✔
1500

1501
/**
1502
 * Stringify an object to provide better debugging experience, handling common cases that simple
1503
 * `JSON.stringify` does not handle, e.g., `undefined`, `bigint`, `function`, `symbol`, `Date`.
1504
 * Circular references are considered.
1505
 *
1506
 * This is a simple port of the [showify](https://github.com/Snowflyt/showify/blob/7759b8778d54f686c85eba4d88b2dac2afdbcdd6/packages/lite/src/index.ts)
1507
 * package, which is a library for stringifying objects in a human-readable way.
1508
 * @param x The object to stringify.
1509
 * @returns
1510
 */
1511
const stringify = (x: unknown): string => {
3✔
1512
  const seen = new WeakSet();
174✔
1513
  const identifierRegex = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
174✔
1514

1515
  const serialize = (value: unknown): string => {
174✔
1516
    if (typeof value === "bigint") return `${value as any}n`;
312✔
1517
    if (typeof value === "function")
309✔
1518
      return value.name ? `[Function: ${value.name}]` : "[Function (anonymous)]";
312✔
1519
    if (typeof value === "symbol") return value.toString();
312✔
1520
    if (value === undefined) return "undefined";
312✔
1521
    if (value === null) return "null";
312✔
1522

1523
    if (typeof value === "object") {
312✔
1524
      if (seen.has(value)) return "[Circular]";
90✔
1525
      seen.add(value);
84✔
1526

1527
      // Handle special object types
1528
      if (value instanceof Date) return value.toISOString();
90✔
1529

1530
      if (value instanceof RegExp) return value.toString();
90✔
1531

1532
      if (value instanceof Map) {
90✔
1533
        const entries = Array.from(value.entries())
9✔
1534
          .map(([k, v]) => `${serialize(k)} => ${serialize(v)}`)
9✔
1535
          .join(", ");
9✔
1536
        return `Map(${value.size}) ` + (entries ? `{ ${entries} }` : "{}");
9✔
1537
      }
9✔
1538

1539
      if (value instanceof Set) {
90✔
1540
        const values = Array.from(value)
9✔
1541
          .map((v) => serialize(v))
9✔
1542
          .join(", ");
9✔
1543
        return `Set(${value.size}) ` + (values ? `{ ${values} }` : "{}");
9✔
1544
      }
9✔
1545

1546
      // Handle arrays and objects
1547
      const isClassInstance =
57✔
1548
        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1549
        value.constructor && value.constructor.name && value.constructor.name !== "Object";
90✔
1550
      const className = isClassInstance ? value.constructor.name : "";
90✔
1551

1552
      if (Array.isArray(value)) {
90✔
1553
        const arrayItems = value.map((item) => serialize(item)).join(", ");
15✔
1554
        let result = `[${arrayItems}]`;
15✔
1555
        if (className !== "Array") result = `${className}(${value.length}) ${result}`;
15✔
1556
        return result;
15✔
1557
      }
15✔
1558

1559
      const objectEntries = Reflect.ownKeys(value)
42✔
1560
        .map((key) => {
42✔
1561
          const keyDisplay =
66✔
1562
            typeof key === "symbol" ? `[${key.toString()}]`
66✔
1563
            : identifierRegex.test(key) ? key
60✔
1564
            : JSON.stringify(key);
12✔
1565
          const val = (value as Record<string, unknown>)[key as any];
66✔
1566
          return `${keyDisplay}: ${serialize(val)}`;
66✔
1567
        })
42✔
1568
        .join(", ");
42✔
1569

1570
      return (className ? `${className} ` : "") + (objectEntries ? `{ ${objectEntries} }` : "{}");
90✔
1571
    }
90✔
1572

1573
    return JSON.stringify(value);
198✔
1574
  };
312✔
1575

1576
  return serialize(x);
174✔
1577
};
174✔
1578

1579
// `console` is not standard in JavaScript. Though rare, it is possible that `console` is not
1580
// available in some environments. We use a proxy to handle this case and ignore errors if `console`
1581
// is not available.
1582
const getConsole = (() => {
3✔
1583
  let cachedConsole: any = undefined;
12✔
1584
  return () => {
12✔
1585
    if (cachedConsole !== undefined) return cachedConsole;
21✔
1586
    try {
3✔
1587
      // eslint-disable-next-line @typescript-eslint/no-implied-eval
1588
      cachedConsole = new Function("return console")();
3✔
1589
    } catch {
21!
1590
      cachedConsole = null;
×
1591
    }
✔
1592
    return cachedConsole;
3✔
1593
  };
21✔
1594
})();
3✔
1595
const logger: {
3✔
1596
  debug(...data: unknown[]): void;
1597
  error(...data: unknown[]): void;
1598
  log(...data: unknown[]): void;
1599
  warn(...data: unknown[]): void;
1600
} = new Proxy({} as never, {
3✔
1601
  get:
3✔
1602
    (_, prop) =>
3✔
1603
    (...args: unknown[]) => {
21✔
1604
      try {
21✔
1605
        getConsole()[prop](...args);
21✔
1606
      } catch {
21!
1607
        // Ignore
1608
      }
×
1609
    },
21✔
1610
});
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