Skip to content

Commit 13176ad

Browse files
PatrickGRich-Harristeemingc
authored
fix: Webcontainer AsyncLocalStorage workaround (#14521)
* don't reset `sync_store` and handle one request at a time * dry Promise.withResolvers * hardening webcontainer detection based on @webcontainer/env * Revert "hardening webcontainer detection based on @webcontainer/env" This reverts commit 0f5e744. * changeset * Update packages/kit/src/runtime/server/constants.js Co-authored-by: Tee Ming <[email protected]> * try this --------- Co-authored-by: Rich Harris <[email protected]> Co-authored-by: Tee Ming <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 85596d0 commit 13176ad

File tree

6 files changed

+75
-18
lines changed

6 files changed

+75
-18
lines changed

.changeset/deep-insects-do.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: Webcontainer AsyncLocalStorage workaround

packages/kit/src/core/postbuild/queue.js

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/** @import { PromiseWithResolvers } from '../../utils/promise.js' */
2+
import { with_resolvers } from '../../utils/promise.js';
3+
14
/**
25
* @typedef {{
36
* fn: () => Promise<any>,
@@ -10,24 +13,12 @@
1013
export function queue(concurrency) {
1114
/** @type {Task[]} */
1215
const tasks = [];
16+
const { promise, resolve, reject } = /** @type {PromiseWithResolvers<void>} */ (with_resolvers());
1317

1418
let current = 0;
15-
16-
// TODO: Whenever Node >21 is minimum supported version, we can use `Promise.withResolvers` to avoid this ceremony
17-
/** @type {(value?: any) => void} */
18-
let fulfil;
19-
20-
/** @type {(error: Error) => void} */
21-
let reject;
22-
2319
let closed = false;
2420

25-
const done = new Promise((f, r) => {
26-
fulfil = f;
27-
reject = r;
28-
});
29-
30-
done.catch(() => {
21+
promise.catch(() => {
3122
// this is necessary in case a catch handler is never added
3223
// to the done promise by the user
3324
});
@@ -51,7 +42,7 @@ export function queue(concurrency) {
5142
});
5243
} else if (current === 0) {
5344
closed = true;
54-
fulfil();
45+
resolve();
5546
}
5647
}
5748
}
@@ -72,10 +63,10 @@ export function queue(concurrency) {
7263
done: () => {
7364
if (current === 0) {
7465
closed = true;
75-
fulfil();
66+
resolve();
7667
}
7768

78-
return done;
69+
return promise;
7970
}
8071
};
8172
}

packages/kit/src/exports/internal/event.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
/** @import { RequestStore } from 'types' */
33
/** @import { AsyncLocalStorage } from 'node:async_hooks' */
44

5+
import { IN_WEBCONTAINER } from '../../runtime/server/constants.js';
6+
57
/** @type {RequestStore | null} */
68
let sync_store = null;
79

@@ -74,6 +76,10 @@ export function with_request_store(store, fn) {
7476
sync_store = store;
7577
return als ? als.run(store, fn) : fn();
7678
} finally {
77-
sync_store = null;
79+
// Since AsyncLocalStorage is not working in webcontainers, we don't reset `sync_store`
80+
// and handle only one request at a time in `src/runtime/server/index.js`.
81+
if (!IN_WEBCONTAINER) {
82+
sync_store = null;
83+
}
7884
}
7985
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
export const NULL_BODY_STATUS = [101, 103, 204, 205, 304];
2+
3+
// eslint-disable-next-line n/prefer-global/process
4+
export const IN_WEBCONTAINER = !!globalThis.process?.versions?.webcontainer;

packages/kit/src/runtime/server/index.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/** @import { PromiseWithResolvers } from '../../utils/promise.js' */
2+
import { with_resolvers } from '../../utils/promise.js';
3+
import { IN_WEBCONTAINER } from './constants.js';
14
import { respond } from './respond.js';
25
import { set_private_env, set_public_env } from '../shared-server.js';
36
import { options, get_hooks } from '__SERVER__/internal.js';
@@ -23,6 +26,26 @@ export class Server {
2326
this.#options = options;
2427
this.#manifest = manifest;
2528

29+
// Since AsyncLocalStorage is not working in webcontainers, we don't reset `sync_store`
30+
// in `src/exports/internal/event.js` and handle only one request at a time.
31+
if (IN_WEBCONTAINER) {
32+
const respond = this.respond.bind(this);
33+
34+
/** @type {Promise<void> | null} */
35+
let current = null;
36+
37+
/** @type {typeof respond} */
38+
this.respond = async (...args) => {
39+
const { promise, resolve } = /** @type {PromiseWithResolvers<void>} */ (with_resolvers());
40+
41+
const previous = current;
42+
current = promise;
43+
44+
await previous;
45+
return respond(...args).finally(resolve);
46+
};
47+
}
48+
2649
set_manifest(manifest);
2750
}
2851

packages/kit/src/utils/promise.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/** @see https://github.com/microsoft/TypeScript/blob/904e7dd97dc8da1352c8e05d70829dff17c73214/src/lib/es2024.promise.d.ts */
2+
3+
/**
4+
* @template T
5+
* @typedef {{
6+
* promise: Promise<T>;
7+
* resolve: (value: T | PromiseLike<T>) => void;
8+
* reject: (reason?: any) => void;
9+
* }} PromiseWithResolvers<T>
10+
*/
11+
12+
/**
13+
* TODO: Whenever Node >21 is minimum supported version, we can use `Promise.withResolvers` to avoid this ceremony
14+
*
15+
* @template T
16+
* @returns {PromiseWithResolvers<T>}
17+
*/
18+
export function with_resolvers() {
19+
let resolve;
20+
let reject;
21+
22+
const promise = new Promise((res, rej) => {
23+
resolve = res;
24+
reject = rej;
25+
});
26+
27+
// @ts-expect-error `resolve` and `reject` are assigned!
28+
return { promise, resolve, reject };
29+
}

0 commit comments

Comments
 (0)