-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgenerate-promise.ts
150 lines (138 loc) · 6.16 KB
/
generate-promise.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
// To run, deno run generate-promise.ts > Promise.lua
// When editing, may be useful to install watchexec via scoop (scoop install watchexec)
// and run: watchexec --watch generate-promise.ts 'deno run generate-promise.ts > Promise.lua'
// deno-lint-ignore no-explicit-any
function l(strings: TemplateStringsArray, ...values: any[]): void {
console.log(
strings
.reduce((result, str, i) => {
const value = values[i] ? values[i] : "";
return result + str + value;
}, "")
.replace(/^ +/gm, ""), // This line trims all beginning spaces, excluding tabs
);
}
// If a successHandler or failureHandler return a Promise, the Promise will be chained on.
// Therefore, we type our handlers as a union of functions, the first
// returning PromiseLike<T2...> and the second returning T2...
// This has the consequence of the function passed to andThen not registering it's
// parameters correctly, instead being inferred from the body of the function.
// For example, the following code:
// local function foo(a: number) end
// Promise.resolve("hi"):andThen(function(b)
// foo(b)
// end)
// Will infer b as a number, rather than know that it's a string.
// And it will precede to give a very verbose error about how:
// Type '(number) -> ()' could not be converted into '((string) -> (a...)) | ((string) -> PromiseLike<a...>)';
// Therefore, we put it behind a flag that we have off, until Luau becomes a little smarter.
// Until then, a replacement for :andThenCall(promise) is :andThen(function() return promise:expect() end)
const allowReturningPromises = false;
l`-- fewkz/typed-luau-promise 2023`;
l`-- Generated by generate-promise.ts`;
l`type PromiseStatus = "Started" | "Resolved" | "Rejected" | "Cancelled"`;
l`type PromiseLike<T...> = { expect: (self: PromiseLike<T...>) -> T..., [any]: any }`;
function promiseType(name: string, next: string) {
const generic = name == "PromiseExhausted" || name == "_AnyPromise"
? "...any"
: "T...";
const genericNext = next == "PromiseExhausted" || name == "_AnyPromise"
? next
: next + "<T2...>";
// Changing self to "any" fixes bug involving andThenCall
// We change it to PromiseLike<{$generic}> to prevent people from
// accidentally doing dot function calls instead of colon function calls
const self = `PromiseLike<${generic}>`;
l`type ${name} = {`;
l`\tandThen: <T2...>(`;
l`\t\tself: ${self},`;
if (allowReturningPromises) {
l`\t\tsuccessHandler: (((${generic}) -> PromiseLike<T2...>) | ((${generic}) -> T2...))?,`;
l`\t\tfailureHandler: (((...any) -> PromiseLike<T2...>) | ((...any) -> T2...))?`;
} else {
l`\t\tsuccessHandler: ((${generic}) -> T2...)?,`;
l`\t\tfailureHandler: ((...any) -> T2...)?`;
}
l`\t) -> ${genericNext},`;
l`\tandThenCall: <A..., T2...>(`;
l`\t\tself: ${self},`;
if (allowReturningPromises) {
l`\t\tcallback: ((A...) -> PromiseLike<T2...>) | ((A...) -> T2...),`;
} else {
l`\t\tcallback: (A...) -> T2...,`;
}
l`\t\tA...`;
l`\t) -> ${genericNext},`;
if (allowReturningPromises) {
l`\tandThenReturn: <T2...>(self: ${self}, PromiseLike<T2...>) -> ${genericNext}`;
l`\t\t| <T2...>(self: ${self}, T2...) -> ${genericNext},`;
} else {
l`\tandThenReturn: <T2...>(self: ${self}, T2...) -> ${genericNext},`;
}
l`\tcancel: (self: ${self}) -> (),`;
l`\tcatch: <T2...>(`;
l`\t\tself: ${self},`;
if (allowReturningPromises) {
l`\t\tfailureHandler: ((...any) -> PromiseLike<T2...>) | ((...any) -> T2...)`;
} else {
l`\t\tfailureHandler: (...any) -> T2...`;
}
l`\t) -> ${genericNext},`;
l`\texpect: (self: ${self}) -> ${generic},`;
l`\tfinally: <T2...>(self: ${self}, (status: "Resolved" | "Rejected" | "Cancelled") -> T2...) -> ${genericNext},`;
l`\tgetStatus: (self: ${self}) -> PromiseStatus,`;
l`\tnow: (self: ${self}, rejectionValue: any?) -> ${name}`;
l`}`;
}
l`-- stylua: ignore start`;
promiseType("PromiseExhausted", "PromiseExhausted");
promiseType("Promise8<T...>", "PromiseExhausted");
promiseType("Promise7<T...>", "Promise8");
promiseType("Promise6<T...>", "Promise7");
promiseType("Promise5<T...>", "Promise6");
promiseType("Promise4<T...>", "Promise5");
promiseType("Promise3<T...>", "Promise4");
promiseType("Promise2<T...>", "Promise3");
promiseType("Promise1<T...>", "Promise2");
// For cases where you might want to have a variable that stores *any* promise.
// The following code, usually, would error:
// local promise = Promise.resolve()
// promise = Promise.resolve():andThenReturn(3)
// So, by typing `promise` as `Promise`, it will solve this issue.
// However, it sacrifices the inability for
promiseType("_AnyPromise", "any");
l`export type AnyPromise = _AnyPromise`;
l`type PromiseLib = {`;
l`\tStatus: {
\t\tStarted: "Started",
\t\tResolved: "Resolved",
\t\tRejected: "Rejected",
\t\tCancelled: "Cancelled",
\t},`;
l``;
// For Promise.all, we make it so that all promises should return the same result.
// If you want promises to return different results, you should instead do :andThenReturn()
// for each promise in the list, to make them return nothing, and then, instead of reading
// the table passed to the new promise, you should use :expect() on the promises directly.
// This retains the type info.
// For example:
// local p1 = Promise.resolve("3 plus 1 is: "); local p2 = Promise.resolve(3)
// Promise.all({p1:andThenReturn(nil), p2:andThenReturn(nil)}):andThen(function()
// print(p1:expect() .. (p2:expect() + 1))
// end)
// This does not support passing in promises that return (), aka nothing.
// Therefore, all ()-returning promises must go through :andThenReturn(nil)
l`\tall: <T>(promises: { PromiseLike<T> }) -> Promise1<{ T }>,`;
l`\tdelay: (seconds: number) -> Promise1<number>,`;
l`\tfromEvent: <T...>(event: RBXScriptSignal<T...>, predicate: ((T...) -> boolean)?) -> Promise1<T...>,`;
l`\tnew: <T...>((
\t\tresolve: (T...) -> (),
\t\treject: (...any) -> (),
\t\tcancel: ((callback: (() -> ())?) -> boolean) -> ()
\t) -> ()) -> Promise1<T...>,`;
l`\tresolve: <T...>(T...) -> Promise1<T...>,`;
l`\ttry: <T..., A...>(callback: (A...) -> T..., A...) -> Promise1<T...>,`;
l`}`;
l`-- stylua: ignore end`;
l``;
l`return require(game:GetService("ReplicatedStorage").Packages.UntypedPromise) :: PromiseLib`;