-
Notifications
You must be signed in to change notification settings - Fork 7
/
func.ts
155 lines (138 loc) · 5.6 KB
/
func.ts
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
* Inspired by Golang, creates a function that receives a `defer` keyword which
* can be used to carry deferred jobs.
* @module
*/
// @ts-ignore
import { isAsyncGenerator, isGenerator } from "./external/check-iterable/index.mjs";
/**
* Inspired by Golang, creates a function that receives a `defer` keyword which
* can be used to carry deferred jobs that will be run after the main function
* is complete.
*
* Multiple calls of the `defer` keyword is supported, and the callbacks are
* called in the LIFO order. Callbacks can be async functions if the main
* function is an async function or an async generator function, and all the
* running procedures will be awaited.
*
* @example
* ```ts
* import func from "@ayonli/jsext/func";
* import * as fs from "node:fs/promises";
*
* export const getVersion = func(async (defer) => {
* const file = await fs.open("./package.json", "r");
* defer(() => file.close());
*
* const content = await file.readFile("utf8");
* const pkg = JSON.parse(content);
*
* return pkg.version as string;
* });
* ```
*/
export default function func<T, R = any, A extends any[] = any[]>(
fn: (this: T, defer: (cb: () => any) => void, ...args: A) => R
): (this: T, ...args: A) => R {
return function (this: T, ...args: A) {
const callbacks: (() => void)[] = [];
const defer = (cb: () => void) => void callbacks.push(cb);
type Result = { value?: Awaited<R>; error: unknown; };
let result: Result | undefined;
try {
const returns = fn.call(this, defer, ...args) as any;
if (isAsyncGenerator(returns)) {
const gen = (async function* () {
let input: unknown;
// Use `while` loop instead of `for...of...` in order to
// retrieve the return value of a generator function.
while (true) {
try {
const { done, value } = await returns.next(input);
if (done) {
result = { value, error: null };
break;
} else {
// Receive any potential input value that passed
// to the outer `next()` call, and pass them to
// `res.next()` in the next call.
input = yield Promise.resolve(value);
}
} catch (error) {
// If any error occurs, capture that error and break
// the loop immediately, indicating the process is
// forced broken.
result = { value: void 0, error } as Result;
break;
}
}
for (let i = callbacks.length - 1; i >= 0; i--) {
await (callbacks[i] as () => void | Promise<void>)?.();
}
if (result.error) {
throw result.error;
} else {
return result.value;
}
})() as AsyncGenerator<unknown, any, unknown>;
return gen as R;
} else if (isGenerator(returns)) {
const gen = (function* () {
let input: unknown;
while (true) {
try {
const { done, value } = returns.next(input);
if (done) {
result = { value, error: null };
break;
} else {
input = yield value;
}
} catch (error) {
result = { value: void 0, error } as Result;
break;
}
}
for (let i = callbacks.length - 1; i >= 0; i--) {
callbacks[i]?.();
}
if (result.error) {
throw result.error;
} else {
return result.value;
}
})() as Generator<unknown, R, unknown>;
return gen as R;
} else if (typeof returns?.then === "function") {
return Promise.resolve(returns as PromiseLike<R>).then(value => ({
value,
error: null,
} as Result)).catch((error: unknown) => ({
value: void 0,
error,
} as Result)).then(async result => {
for (let i = callbacks.length - 1; i >= 0; i--) {
await (callbacks[i] as () => void | Promise<void>)?.();
}
if (result.error) {
throw result.error;
} else {
return result.value;
}
}) as R;
} else {
result = { value: returns, error: null } as Result;
}
} catch (error) {
result = { value: void 0, error } as Result;
}
for (let i = callbacks.length - 1; i >= 0; i--) {
callbacks[i]?.();
}
if (result.error) {
throw result.error;
} else {
return result.value as R;
}
};
}