// node [--trace-gc] local-storage-exmaple.cjs // creates a server // will drop /favicon.ico connections poorly without properly closing them // `cleanupContext` AsyncLocalStorage + `cleanup` FinalizationRegistry will clean up // will track the http request URL for errors in httpContext // will show up in error generated for bad permissions // will grant permissions in permissionsContext based upon query params / search params // need ?fs=true to make / respond with a 200 // // goto /?fs=true in browser to see it work // goto /?fs=false in browser to see it error // browsers will automatically request /favicon.ico 'use strict'; const { AsyncLocalStorage } = require('async_hooks'); const http = require('http'); const events = require('events'); const { readFile } = require('fs/promises'); // tracking what permissions we have const permissionsContext = new AsyncLocalStorage(); // tracking which HTTP context we are in const httpContext = new AsyncLocalStorage(); const getSearchParams = (reqUrl) => { const url = new URL(reqUrl, 'http://invalid.invalid/'); return url.searchParams; } /** * FOR DEMO PURPOSES ONLY, NEVER DO THIS * Use real auth methods and not search params * @param req */ async function auth(req, fn) { // NO NO NO NO NO let permissions = {}; for (const [key, value] of getSearchParams(req.url).entries()) { permissions[key] = value; } permissionsContext.run(permissions, fn); } function hasPermission(key) { return permissionsContext.getStore()?.[key] === 'true'; } /** * needs ?fs=true * @param filepath * @returns */ function gatedReadFile(filepath) { // has no reference to how auth is obtained/propagated if (!hasPermission('fs')) { // has no reference to the request / no need to propagate it throw new Error('403'); } return readFile(filepath, 'utf8'); } process.on('uncaughtExceptionMonitor', function (err) { let reqUrl = httpContext.getStore(); if (reqUrl) { console.error('Error from URL %s', reqUrl); } }); const cleanup = new FinalizationRegistry(([req, res, id, cleanup]) => { console.log('closing dropped connection to ', req.url, 'with cleanup handler id', id); cleanup(); try { res.writeHead(500); res.end(); } catch { // this is normal } }); // Garbage generator to keep GC running periodically setInterval(() => [].concat(1), 0); setInterval(() => [].concat(1), 0); setInterval(() => [].concat(1), 0); setInterval(() => [].concat(1), 0); let gcId = 1; const cleanupContext = new AsyncLocalStorage(); /** * @param {import('http').IncomingMessage} req * @param {import('http').ServerResponse} res * @returns */ function setCleanup(req, res, fn) { let id = gcId++; // {id} is our "token" // don't directly hold onto {id}, it won't be able to GC then let GC = new WeakRef({id}); // data to associate with the token let held = [req, res, id, () => { res.off('finish', unregister); }]; console.log(req.url, 'assigned cleanup handler', id); function unregister() { console.log(req.url, 'unregistered cleanup handler', id); cleanup.unregister(GC.deref()); } // setup when the token deallocs, fire handler cleanup.register(GC.deref(), held, GC.deref()); res.on('finish', unregister); // Force the token to stay alive for all async work spawned from the current task cleanupContext.run(GC.deref(), fn); } /** * @param {import('http').IncomingMessage} req * @param {import('http').ServerResponse} res * @returns */ function handleHTTP(req, res) { setCleanup(req, res, () => { if (req.url === '/favicon.ico') { // uh oh, forgot to close (don't worry the Finalization Registry handles it) setTimeout(() => { // keep the req, res alive WAAAAY too long // should see a GC entry before this console.log('can cleanup /favicon.ico') }, 0); return; } httpContext.run(req.url, () => { auth(req, () => { console.log('%s %s', req.method, req.url); // "simulate" a work queue setTimeout(doWork, 1, res); }); }); }); } async function doWork(res) { res.end(await gatedReadFile(__filename)); } async function main() { const server = http.createServer(handleHTTP).listen(process.env.PORT || 0) await events.once(server, 'listening'); console.log('Listening on', server.address()); console.log(`http://127.0.0.1:${server.address().port}`) } main();