Skip to content

Commit 8c856f1

Browse files
authored
fix Promise.finally() to return new promise via .then() (#1696)
* fix Promise.finally() to return new promise via .then() * simplify Promise.finally test to use expectToMatchJsResult Remove redundant tests that relied on synchronous polyfill behavior differing from JS. Keep only the identity check which matches JS. * add tests for Promise.finally value preservation and rejection passthrough Fix issue reference (#1660, not #1659).
1 parent 2bb18e1 commit 8c856f1

File tree

2 files changed

+65
-23
lines changed

2 files changed

+65
-23
lines changed

src/lualib/Promise.ts

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ export class __TS__Promise<T> implements Promise<T> {
4545

4646
private fulfilledCallbacks: Array<PromiseResolve<T>> = [];
4747
private rejectedCallbacks: PromiseReject[] = [];
48-
private finallyCallbacks: Array<() => void> = [];
4948

5049
// @ts-ignore
5150
public [Symbol.toStringTag]: string; // Required to implement interface, no output Lua
@@ -124,16 +123,23 @@ export class __TS__Promise<T> implements Promise<T> {
124123
}
125124

126125
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally
126+
// Delegates to .then() so that a new Promise is returned (per ES spec §27.2.5.3)
127+
// and the original fulfillment value / rejection reason is preserved.
127128
public finally(onFinally?: () => void): Promise<T> {
128-
if (onFinally) {
129-
this.finallyCallbacks.push(onFinally);
130-
131-
if (this.state !== PromiseState.Pending) {
132-
// If promise already resolved or rejected, immediately fire finally callback
133-
onFinally();
134-
}
135-
}
136-
return this;
129+
return this.then(
130+
onFinally
131+
? (value: T): T => {
132+
onFinally();
133+
return value;
134+
}
135+
: undefined,
136+
onFinally
137+
? (reason: any): never => {
138+
onFinally();
139+
throw reason;
140+
}
141+
: undefined
142+
);
137143
}
138144

139145
private resolve(value: T | PromiseLike<T>): void {
@@ -168,25 +174,13 @@ export class __TS__Promise<T> implements Promise<T> {
168174

169175
private invokeCallbacks<T>(callbacks: ReadonlyArray<(value: T) => void>, value: T): void {
170176
const callbacksLength = callbacks.length;
171-
const finallyCallbacks = this.finallyCallbacks;
172-
const finallyCallbacksLength = finallyCallbacks.length;
173177

174178
if (callbacksLength !== 0) {
175179
for (const i of $range(1, callbacksLength - 1)) {
176180
callbacks[i - 1](value);
177181
}
178182
// Tail call optimization for a common case.
179-
if (finallyCallbacksLength === 0) {
180-
return callbacks[callbacksLength - 1](value);
181-
}
182-
callbacks[callbacksLength - 1](value);
183-
}
184-
185-
if (finallyCallbacksLength !== 0) {
186-
for (const i of $range(1, finallyCallbacksLength - 1)) {
187-
finallyCallbacks[i - 1]();
188-
}
189-
return finallyCallbacks[finallyCallbacksLength - 1]();
183+
return callbacks[callbacksLength - 1](value);
190184
}
191185
}
192186

test/unit/builtins/promise.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,3 +1323,51 @@ describe("Promise.race", () => {
13231323
});
13241324
});
13251325
});
1326+
1327+
// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1660
1328+
describe("Promise.finally", () => {
1329+
test("returns a different promise instance", () => {
1330+
util.testFunction`
1331+
const p1 = new Promise(() => {});
1332+
const p2 = p1.finally();
1333+
return p1 === p2;
1334+
`.expectToMatchJsResult();
1335+
});
1336+
1337+
test("preserves fulfillment value", () => {
1338+
util.testFunction`
1339+
const result = Promise.resolve(42).finally(() => {}) as any;
1340+
return result.value;
1341+
`.expectToEqual(42);
1342+
});
1343+
1344+
test("preserves rejection reason", () => {
1345+
util.testFunction`
1346+
const result = Promise.reject("err").finally(() => {}) as any;
1347+
return result.rejectionReason;
1348+
`.expectToEqual("err");
1349+
});
1350+
1351+
test("callback executes on fulfillment", () => {
1352+
util.testFunction`
1353+
let called = false;
1354+
Promise.resolve(1).finally(() => { called = true; });
1355+
return called;
1356+
`.expectToEqual(true);
1357+
});
1358+
1359+
test("callback executes on rejection", () => {
1360+
util.testFunction`
1361+
let called = false;
1362+
Promise.reject("err").finally(() => { called = true; });
1363+
return called;
1364+
`.expectToEqual(true);
1365+
});
1366+
1367+
test("finally with undefined callback", () => {
1368+
util.testFunction`
1369+
const result = Promise.resolve(99).finally(undefined) as any;
1370+
return result.value;
1371+
`.expectToEqual(99);
1372+
});
1373+
});

0 commit comments

Comments
 (0)