Skip to content

Commit 01dafd3

Browse files
committed
Add 2025-09 slides
1 parent ac9343f commit 01dafd3

File tree

2 files changed

+302
-0
lines changed

2 files changed

+302
-0
lines changed

slides/2025-09-stage-1.md

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
---
2+
theme: default
3+
title: Native promise adoption for Stage 1
4+
info: |
5+
Proposal for adopting native promise state.
6+
class: text-center
7+
drawings:
8+
persist: false
9+
# slide transition: https://sli.dev/guide/animations.html#slide-transitions
10+
transition: fade
11+
fonts:
12+
local: ["Consolas", "Fira Code", "monospace"]
13+
layout: cover
14+
---
15+
16+
# Native promise adoption <br /> for Stage 1
17+
18+
Mathieu Hofman (@mhofman)
19+
20+
---
21+
22+
# Motivation
23+
24+
Promise prototype pollution has surprising / inconsistent effects on async code.
25+
26+
<v-click>
27+
Pollution
28+
29+
<<< @/snippets/snippet.js#pollution
30+
</v-click>
31+
32+
<v-click>
33+
Library code
34+
35+
````md magic-move {at:'3'}
36+
<<< @/snippets/snippet.js#library-async-no-await {*|1|*|2}
37+
<<< @/snippets/snippet.js#library-async-await {2}
38+
````
39+
</v-click>
40+
41+
<v-click>
42+
User code
43+
44+
````md magic-move {at:'4'}
45+
<<< @/snippets/snippet.js#user-exploited {1|3|3}
46+
<<< @/snippets/snippet.js#user-not-exploited {3}
47+
````
48+
</v-click>
49+
50+
---
51+
layout: two-cols-header
52+
---
53+
54+
# What's happening ?
55+
56+
````md magic-move
57+
<<< @/snippets/snippet.js#library-async-no-await
58+
<<< @/snippets/snippet.js#library-promise-no-await {*|*|*|*|1|2}
59+
````
60+
61+
::left::
62+
63+
<div v-click="4">
64+
<<< @/snippets/snippet.js#resolve-promise {*|4-5|6-7,10-17}{at: 5}
65+
</div>
66+
67+
::right::
68+
69+
<div v-click="2">
70+
<<< @/snippets/snippet.js#resolvers {*|8,14|8,14,17}{at: 3}
71+
</div>
72+
73+
---
74+
layout: two-cols-header
75+
---
76+
77+
# What's happening ?
78+
79+
````md magic-move
80+
<<< @/snippets/snippet.js#library-async-await
81+
<<< @/snippets/snippet.js#library-promise-await {*|*|2}
82+
````
83+
84+
::left::
85+
86+
<div v-click="2">
87+
<<< @/snippets/snippet.js#resolve-promise {*|4-5}{at: 3}
88+
</div>
89+
90+
::right::
91+
92+
<div v-click="2">
93+
<<< @/snippets/snippet.js#resolvers {*|8,14,17}{at: 3}
94+
</div>
95+
96+
---
97+
98+
# What can we do ?
99+
100+
<v-clicks depth="2">
101+
102+
* <span v-mark="{ at: 3, type: 'strike-through' }">Automatically await return in async functions?</span>
103+
* <span v-mark="{ at: 3, type: 'strike-through' }">Changes `try`/`catch`/`finally` semantics</span>
104+
* Adopt result promises in async functions?
105+
* Most narrow solution
106+
* Hard to special case, but possible
107+
* Adopt promises in resolve functions?
108+
* Consistent promise behavior
109+
* This is what Promises/A+ intended!
110+
111+
</v-clicks>
112+
113+
---
114+
layout: iframe
115+
url: https://promisesaplus.com/#the-promise-resolution-procedure
116+
---
117+
118+
---
119+
120+
# What is promise adoption
121+
122+
- If `x` is a promise, adopt its state [^1]:
123+
1. If `x` is pending, `promise` must remain pending until `x` is fulfilled or rejected.
124+
1. If/when `x` is fulfilled, fulfill `promise` with the same value.
125+
1. If/when `x` is rejected, reject `promise` with the same reason.
126+
127+
[^1]: Generally, it will only be known that `x` is a true promise if it comes from the current implementation. This clause allows the use of implementation-specific means to adopt the state of known-conformant promises.
128+
129+
---
130+
layout: two-cols-header
131+
---
132+
133+
# Promise adoption in resolve function
134+
135+
<<< @/snippets/snippet.js#library-promise-no-await {*|2}{at: 2}
136+
137+
::left::
138+
139+
````md magic-move
140+
<<< @/snippets/snippet.js#resolve-promise
141+
<<< @/snippets/snippet.js#resolve-promise-proposed {*|6-9,13-20}{at: 2}
142+
````
143+
144+
::right::
145+
146+
<<< @/snippets/snippet.js#resolvers {*|8,14,17}{at: 2}
147+
148+
---
149+
150+
# Web compatibility
151+
152+
<v-clicks depth="2">
153+
154+
* Does not affect resolution with non-native promises / thenables
155+
* Does not change number of ticks for promise resolution
156+
* Only affects code attempting to hijack native promise behavior
157+
* Malicious code
158+
* Possibly some async tracking libraries
159+
160+
</v-clicks>
161+
162+
---
163+
164+
# Web compatibility
165+
166+
Zone.js
167+
168+
<v-clicks depth="2">
169+
170+
* Relies on transpiling async code to avoid native promise adoption in `await`
171+
* a narrow `return` only solution should be safe
172+
* Replaces global `Promise` with `ZoneAwarePromise` (implemented as a thenable)
173+
* Replaces `Promise.prototype.then` to assimilate native promises into their zone aware promises
174+
* Covers explicit calls to `promise.then`
175+
* Doesn't expect to intercept native promise assimilating another native promise
176+
* 262 spec does not use `PromiseCapability`'s `[[Resolve]]` with another native promise
177+
* --> Promise adoption in resolve functions should be compatible
178+
* None-the-less, should measure in the wild
179+
180+
</v-clicks>
181+
182+
---
183+
layout: section
184+
---
185+
# Stage 1?
186+
187+
<br/>
188+
189+
Make native promise adoption more consistent.
190+
191+
Reduce impact of Promise.prototype pollution on async code.
192+
193+
For web browser implementors: measure web compat.

slides/snippets/snippet.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//#region pollution
2+
const originalThen = Promise.prototype.then;
3+
Promise.prototype.then = function (onFulfilled, onRejected) {
4+
return originalThen.call(this, function (value) {
5+
console.log('fulfilled', value);
6+
return onFulfilled ? onFulfilled.call(this, value) : value;
7+
}, onRejected);
8+
};
9+
//#endregion
10+
11+
//#region library-async-no-await
12+
const add = async (a, b) => a + b;
13+
const inc = async (a) => add(a, 1);
14+
//#endregion
15+
16+
//#region library-async-await
17+
const add = async (a, b) => a + b;
18+
const inc = async (a) => await add(a, 1);
19+
//#endregion
20+
21+
//#region library-promise-no-await
22+
const add = (a, b) => new Promise(resolve => resolve(a + b));
23+
const inc = (a) => new Promise(resolve => resolve(add(a, 1)));
24+
//#endregion
25+
26+
//#region library-promise-await
27+
const add = (a, b) => new Promise(resolve => resolve(a + b));
28+
const inc = (a) => new Promise(resolve => internalThen.call(add(a, 1), resolve));
29+
//#endregion
30+
31+
//#region user-exploited
32+
const three = await add(1, 2); // No console output
33+
34+
const four = await inc(three); // "fulfilled 4"
35+
//#endregion
36+
37+
//#region user-not-exploited
38+
const three = await add(1, 2); // No console output
39+
40+
const four = await inc(three); // No console output
41+
//#endregion
42+
43+
//#region resolve-promise
44+
function ResolvePromise(promise, value) {
45+
if (value === promise) {
46+
RejectPromise(promise, TypeError());
47+
} else if (!IsObject(value)) {
48+
FulfillPromise(promise, value);
49+
} else {
50+
const thenAction = value.then;
51+
if (typeof thenAction !== 'function') {
52+
FulfillPromise(promise, value);
53+
} else {
54+
queue(() => {
55+
const { resolve, reject } =
56+
new Resolvers(promise);
57+
thenAction.call(value, resolve, reject);
58+
});
59+
}
60+
}
61+
}
62+
//#endregion
63+
64+
//#region resolve-promise-proposed
65+
function ResolvePromise(promise, value) {
66+
if (value === promise) {
67+
RejectPromise(promise, TypeError());
68+
} else if (!IsObject(value)) {
69+
FulfillPromise(promise, value);
70+
} else {
71+
const thenAction = IsPromise(value) &&
72+
value.__proto__ === Promise.prototype
73+
? internalThen
74+
: value.then;
75+
if (typeof thenAction !== 'function') {
76+
FulfillPromise(promise, value);
77+
} else {
78+
queue(() => {
79+
const { resolve, reject } =
80+
new Resolvers(promise);
81+
thenAction.call(value, resolve, reject);
82+
});
83+
}
84+
}
85+
}
86+
//#endregion
87+
88+
89+
//#region resolvers
90+
class Resolvers {
91+
#promise; #alreadyResolved;
92+
constructor(promise) {
93+
this.#promise = promise;
94+
this.#alreadyResolved = false;
95+
}
96+
97+
@bound reject(reason) {
98+
if (!this.#alreadyResolved) return;
99+
this.#alreadyResolved = true;
100+
RejectPromise(this.#promise, reason);
101+
}
102+
103+
@bound resolve(value) {
104+
if (!this.#alreadyResolved) return;
105+
this.#alreadyResolved = true;
106+
ResolvePromise(this.#promise, value);
107+
}
108+
}
109+
//#endregion

0 commit comments

Comments
 (0)