Skip to content

Instantly share code, notes, and snippets.

@bmeck
Last active December 21, 2021 15:44
Show Gist options
  • Select an option

  • Save bmeck/b8044a739231c389457f2ab3aa7ffffa to your computer and use it in GitHub Desktop.

Select an option

Save bmeck/b8044a739231c389457f2ab3aa7ffffa to your computer and use it in GitHub Desktop.

Revisions

  1. bmeck revised this gist Dec 21, 2021. No changes.
  2. bmeck created this gist Dec 14, 2021.
    144 changes: 144 additions & 0 deletions local-storage-example.cjs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,144 @@
    // 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 ' + httpContext.getStore());
    }
    return readFile(filepath, 'utf8');
    }

    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();