Skip to content

Instantly share code, notes, and snippets.

@f5io
Last active September 7, 2023 18:38
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save f5io/5d029b397f39a37370eeae3eb3e800cb to your computer and use it in GitHub Desktop.
Save f5io/5d029b397f39a37370eeae3eb3e800cb to your computer and use it in GitHub Desktop.
An approach to async middleware for raw `http` in Node.
const http = require('http');
const methods = [ 'get', 'put', 'post', 'delete', 'head' ];
const isStream = obj =>
obj &&
typeof obj === 'object' &&
typeof obj.pipe === 'function';
const isReadable = obj =>
isStream(obj) &&
typeof obj._read === 'function' &&
typeof obj._readableState === 'object';
const isNext = Symbol('isNext');
const composeWithContext = (...mw) => (...args) =>
new Promise(async (resolve, reject) => {
const nxt = args[args.length - 1][isNext] ? args.pop() : () => resolve();
await mw.reduceRight((next, curr) =>
async function() {
next[isNext] = true;
try { await curr(...args.concat(next)) }
catch(e) { reject(e); throw e; }
}, nxt)();
resolve();
});
const resolve = async (req, res, next) => {
await next();
if (isReadable(res.body)) {
res.body.pipe(res);
} else {
res.end(res.body || '');
}
};
const route = (path, fn) => async (req, res, next) => {
if (req.url === path) {
await composeWithContext(fn, next)(req, res);
} else {
await next();
}
};
const method = (me, fn) => async(req, res, next) => {
if (req.method.toLowerCase() === me) {
await composeWithContext(fn, next)(req, res);
} else {
await next();
}
};
const nomad = () => {
const mw = [ resolve ];
const app = {
use: (path, fn) => {
if (!fn && typeof path === 'function') return (mw.push(path), app);
if (typeof path === 'string' && typeof fn === 'function') return (mw.push(route(path, fn)), app);
},
listen: (port, callback = () => {}) => {
http.createServer(composeWithContext(...mw)).listen(port, callback);
}
};
return methods.reduce((a, me) => {
a[me] = (path, fn) => {
if (!fn && typeof path === 'function') return (mw.push(method(me, path)), app);
if (typeof path === 'string' && typeof fn === 'function') return (mw.push(route(path, method(me, fn))), app);
}
return a;
}, app);
};
export default nomad;
import fs from 'fs';
import nomad from './core';
const PORT = 1337;
let count = 0;
const stats = file =>
new Promise((resolve, reject) => {
fs.stat(file, (err, stat) => err ? reject(err) : resolve(stat));
});
const assets = path => async (req, res, next) => {
try {
const filePath = `${path}${req.url}`;
const stat = await stats(filePath);
if (!stat.isFile()) throw new Error('Cannot load directory');
res.statusCode = 200;
res.body = fs.createReadStream(filePath);
} catch(e) {
await next();
}
}
const app = nomad();
app.use(async (req, res, next) => {
const time = Date.now();
console.log(`initiate req ${++count}`);
await next();
res.setHeader('Response-Time', `${Date.now() - time}ms`);
});
app.use(async (req, res, next) => {
try { await next() }
catch(e) {
res.statusCode = 500;
res.body = e.message;
}
});
app.get(assets(process.cwd()));
app.use('/hello', (req, res, next) => {
res.statusCode = 200;
res.body = 'hello';
});
app.use('/world', (req, res, next) => {
res.statusCode = 200;
res.body = 'world';
});
app.get('/file', (req, res, next) => {
res.statusCode = 200;
res.body = fs.createReadStream(`${process.cwd()}/package.json`);
});
app.use(() => {
throw new Error('Route not found.');
});
app.listen(PORT, () => console.log(`Server started on: ${PORT}`));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment