mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Node runs the callback of an operation that a callback-style API starts
with the AsyncLocalStorage stores active when the call started it. The
runtime invokes these callbacks from the event loop, where the context is
empty, so getStore() answered undefined in fs, crypto, zlib, dns,
child_process, net, http and dgram callbacks.
In programs whose scheduling carries contexts (RuntimeFeatures
asyncContext), the lowering now binds each completion-callback argument to
the context active at the call (completion-callbacks.ts): typed closures
through als.bindCallback, whose bound closure enters through a thunk the
backend emits once per callback signature, and dyn functions through
als.bindCallbackDyn. The fs callback family binds in the runtime, where
scr_fs_cb_schedule queues the callback. Other programs compile as before.
AsyncLocalStorage.bind(fn) binds fn to the active context, also when it
is empty (a typed function keeps its type; in JavaScript files the bound
function is named "bound" and forwards its receiver, as in Node).
AsyncLocalStorage.snapshot() answers a function that runs its callback in
the snapshot's context. new AsyncLocalStorage({ defaultValue, name })
validates its options as Node does; getStore() answers the default value
where the store has no entry, exit() runs its callback with the store
undefined, and the name property answers the name option or "".
Event listeners on sockets, requests, responses and streams, stream
callbacks, server.close(callback) and https and tls callbacks still run
with no store; the limitations page lists them. Corpus 7110-7112.
61 lines
2.4 KiB
TypeScript
61 lines
2.4 KiB
TypeScript
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, expect, test } from "vitest";
|
|
import { compile } from "../src/index.js";
|
|
|
|
const directories: string[] = [];
|
|
afterEach(async () => {
|
|
await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
});
|
|
|
|
async function emit(source: string): Promise<string> {
|
|
const outDir = await mkdtemp(join(tmpdir(), "scriptc-completion-callbacks-"));
|
|
directories.push(outDir);
|
|
const entry = join(outDir, "main.ts");
|
|
await writeFile(entry, source);
|
|
const outPath = join(outDir, "program.ll");
|
|
const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" });
|
|
expect(result.ok, JSON.stringify(result)).toBe(true);
|
|
return readFile(outPath, "utf8");
|
|
}
|
|
|
|
const randomBytes = `
|
|
import { randomBytes } from "node:crypto";
|
|
randomBytes(4, (error, buffer) => console.log(error === null, buffer.length));
|
|
`;
|
|
|
|
test("programs without AsyncLocalStorage pass completion callbacks unchanged", async () => {
|
|
const llvm = await emit(randomBytes);
|
|
expect(llvm).not.toContain("@scr_als_bind_closure");
|
|
expect(llvm).not.toContain("sc_alsbound_");
|
|
});
|
|
|
|
test("programs that enter stores bind each completion callback to the active context", async () => {
|
|
const llvm = await emit(`
|
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
${randomBytes}
|
|
const store = new AsyncLocalStorage<string>();
|
|
store.run("a", () => randomBytes(1, (error, buffer) => console.log(store.getStore(), buffer.length)));
|
|
`);
|
|
const binds = llvm.match(
|
|
/call ptr @scr_als_bind_closure\(ptr [^,]+, ptr @sc_alsbound_\d+, i1 false\)/g,
|
|
);
|
|
expect(binds?.length).toBe(2);
|
|
// One thunk per callback signature.
|
|
expect(llvm.match(/^define internal void @sc_alsbound_\d+\(/gm)?.length).toBe(1);
|
|
});
|
|
|
|
test("AsyncLocalStorage.bind binds a typed function with an empty context too", async () => {
|
|
const llvm = await emit(`
|
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
const store = new AsyncLocalStorage<string>();
|
|
const read = AsyncLocalStorage.bind((n: number) => \`\${store.getStore()} \${n}\`);
|
|
console.log(store.run("x", () => read(1)));
|
|
`);
|
|
expect(llvm).toMatch(
|
|
/call ptr @scr_als_bind_closure\(ptr [^,]+, ptr @sc_alsbound_\d+, i1 true\)/,
|
|
);
|
|
expect(llvm).toMatch(/^define internal ptr @sc_alsbound_\d+\(ptr %self, double %a0\)/m);
|
|
});
|