All files / src/mixin awaited.ts

10.34% Statements 3/29
14.28% Branches 1/7
9.09% Functions 1/11
7.69% Lines 2/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137                                                                                              1x 2x                                                                                                                                                                                
import { makeFluent } from "../base";
import type { HKT } from "../base/hkt";
import { Mixin, shim, type Input, type Return } from "../base/mixin";
import { assert, assertType, never } from "../internal";
import { Base } from "./base";
 
interface AwaitedIdentitity extends HKT {
	new: (t: HKT.T<this>) => Awaited<Promise<typeof t>>;
}
 
class Awaited<T extends Promise<unknown>> {
	public value: T;
	public constructor(value: T) {
		this.value = value;
	}
 
	public get [shim]() {
		return never as {
			input: T extends Promise<infer U> ? U : never;
			output: AwaitedIdentitity;
			value: T;
		};
	}
}
 
export interface AsyncMixin extends Mixin.HKT {
	new: (t: HKT.T<this>) => Input<typeof t> extends infer T
		? T extends Promise<unknown>
			? {
					readonly awaited: Return<Awaited<T>, typeof t>;
					then: <U>(
						callback: (
							value: T extends Promise<infer T>
								? T
								: never,
						) => U,
					) => Return<Promise<U>, typeof t>;
				}
			: unknown
		: never;
}
 
interface BaseFluent<T> {
	value: T;
	[K: PropertyKey]: unknown;
}
 
export const AsyncMixin = Mixin<AsyncMixin>((value, $, fluent) => {
	Eif (!(value instanceof Promise)) return;
 
	$.then = (callback: (value: unknown) => unknown) => {
		return fluent(value.then(callback));
	};
 
	Object.defineProperty($, "awaited", {
		enumerable: true,
		get() {
			let v: Promise<BaseFluent<unknown>> = value.then((v) =>
				fluent(v),
			);
 
			const path: PropertyKey[] = [];
			// eslint-disable-next-line no-empty-pattern
			const proxy = new Proxy((...[]: unknown[]) => proxy, {
				get: (_, key) => {
					if (key === "value")
						return v.then((v) => v.value);
					path.push(key);
					return proxy;
				},
				apply: (target, thisArg, args: unknown[]) => {
					v = v.then((v) => {
						let obj: unknown = v;
						for (const node of path) {
							assert(
								typeof obj === "object" &&
									obj !== null &&
									node in obj,
							);
							assertType<Record<PropertyKey, unknown>>(
								obj,
							);
 
							obj = obj[node];
						}
 
						assert(typeof obj === "function");
						assertType<
							(
								...args: unknown[]
							) => BaseFluent<unknown>
						>(obj);
 
						return obj(...args);
					});
					return target.apply(thisArg, args);
				},
			});
 
			return proxy;
		},
	});
});
 
if (import.meta.vitest) {
	const { test, expect } = import.meta.vitest;
 
	const registry = [Base, AsyncMixin] as const;
	const $ = makeFluent(registry);
 
	test(".awaited", async () => {
		const value = 10;
		const promise = new Promise<number>((r) => {
			r(value);
		});
 
		const increment = (n: number) => n + 1;
 
		const result = $(promise).awaited.transform(increment).value;
 
		expect(await result).toBe(increment(value));
	});
 
	test("then()", async () => {
		const value = 10;
		const promise = new Promise<number>((r) => {
			r(value);
		});
 
		const increment = (n: number) => n + 1;
 
		const result = $(promise).then(increment).value;
 
		expect(await result).toBe(increment(value));
	});
}