-
-
Save wesleytodd/e5642c0d39fa71bdebf8ef31ddbd5e40 to your computer and use it in GitHub Desktop.
'use strict' | |
// Notice no certs, one thing I have thought | |
// for a long time is that frameworks should | |
// directly have support for spinning up with | |
// a cert if none was provided. | |
require('h3xt')() | |
.get('/', (req) => { | |
// Access the associated session | |
// req.session | |
// No need to check if can push, frameworks does this | |
// The framework has something like a ROOT so you can | |
// resolve static files, but the content type negotiation | |
// is something I think belongs in the underlying core api | |
req.pushFile('favicon.ico') | |
req.pushJSON('/foo.json', { | |
message: await req.body() | |
}) | |
// Frameworks could do whatever templating they saw fit, | |
// delivering the resulting string/buffer to `req.respond()` | |
// In this example I am assuming a "return response" approach, | |
// and the `.sendFile` call would return a `Response` | |
// object, and the middleware/routing layer would use that | |
// object to actually send | |
return req.sendFile('index.html', { | |
// template data | |
}) | |
}) | |
.listen(8443) |
'use strict' | |
const http = require('http-next') | |
const fs = require('fs') | |
http.createServer({ | |
allowHTTP1: true, | |
allowHTTP2: true, | |
allowHTTP3: true, | |
key: fs.readFileSync('localhost-privkey.pem'), | |
cert: fs.readFileSync('localhost-cert.pem') | |
}) | |
.on('session', (session) => { | |
// I am not even sure what an end user would do with session. | |
// Is it ok to store application state on? Like if a user cookie | |
// was present in a request, could you load the user metadata onto | |
// the session object? | |
// Seems like a very useful feature with session pinning on your LB | |
// and something like session.locals = Object.create(null) where users | |
// could load on to. | |
// And none of this would apply in http1, so there is also that to consider. | |
session.on('stream', (stream, headers, flags) => { | |
// Do a push stream if allowed | |
// This covers the cases for no support in http1, http2 settings | |
// and could even go from true to false when http3's max push is hit | |
if (stream.pushAllowed) { | |
stream.pushStream({ | |
path: '/favicon.ico' | |
}, (err, pushStream, headers) => { | |
if (err) { | |
throw err | |
} | |
// Even in HTTP1 noone liked the way statusCode works! | |
// I honeslty think we can do it all in one method signature | |
// again with this, it is like 90% uf use cases | |
pushStream.respond(200, { | |
'content-type': 'image/x-icon' | |
}, ourPreloadedFaviconBuffer) | |
}) | |
} | |
// A conviencence method for consuming the entire body | |
// This covers like 90% of use cases | |
// Also, we could add .consumeAsJSON to map to the new .respondWithJSON? | |
stream.consume((body) => { | |
// Always check again, because if we hit the max streams this would change | |
// Actually, I wonder if it would be better for `stream.pushStream` to do | |
// this check internally then just return false if it was not allowed? | |
if (stream.pushAllowed) { | |
stream.pushStream({ | |
path: '/foo.json' | |
}, (err, pushStream, headers) => { | |
if (err) { | |
throw err | |
} | |
// Again with this, it is like 90% uf use cases | |
pushStream.respondWithJSON({ | |
message: body | |
}) | |
}) | |
} | |
// The basic response with html | |
stream.respondWithFile('index.html', { | |
// This should be infered from the file name if not specified | |
// 'content-type': 'text/html; charset=utf-8' | |
}) | |
}) | |
}) | |
}) | |
.listen(8443) |
const http = require('http-next') | |
module.exports = function (opts) { | |
return new Application() | |
} | |
class Application { | |
constructor () { | |
this.stack = [] | |
for (const method of http.METHODS) { | |
this[method] = this.use.bind(this, method) | |
} | |
this.server = http.createServer() | |
this.server.onRequest(this.onRequest) | |
} | |
use (method, path, fnc) { | |
this.stack.push({ path, method, fnc }) | |
} | |
async onRequest (ctx) { | |
// Accept and parse bodies | |
if (ctx.method !== 'HEAD' && ctx.method !== 'GET') { | |
// Read body | |
const _body = [] | |
for await (const chunk of ctx.body) { | |
_body.push(chunk) | |
} | |
let body = Buffer.from(..._body) | |
if (ctx.headers['content-type'] === 'application/json') { | |
body = JSON.parse(body.toString()) | |
} | |
ctx.body = body | |
} | |
for (const layer of this.stack) { | |
if (layer.method && ctx.method !== layer.method) { | |
continue | |
} | |
if (layer.path && ctx.path !== layer.path) { | |
continue | |
} | |
let statusCode = 200 | |
let headers = {} | |
let body = null | |
const ret = await layer.fnc(ctx) | |
if (ret === undefined) { | |
continue | |
} | |
if (Number.isFinite(ret)) { | |
statusCode = ret | |
let msg = '' | |
switch (statusCode) { | |
case 200: | |
msg = 'Success' | |
break | |
case 404: | |
msg = 'Not Found' | |
break | |
case 500: | |
msg = 'Server Error' | |
break | |
} | |
body = Buffer.from(msg) | |
headers = { | |
'content-type': 'text/plain', | |
'content-length': body.length | |
} | |
} else if (typeof ret !== 'undefined' && typeof ret === 'object') { | |
body = Buffer.from(JSON.stringify(ret)) | |
headers = { | |
'content-type': 'application/json', | |
'content-length': body.length | |
} | |
} else if (typeof ret === 'string') { | |
body = Buffer.from(ret, 'utf8') | |
headers = { | |
'content-type': 'text/plain', | |
'content-length': body.length | |
} | |
} | |
return ctx.respondWith(statusCode, headers, body) | |
} | |
} | |
listen (...args) { | |
return this.server.listen(...args) | |
} | |
} |
TBQH, maybe we do need one more layer in there?
Yea. That's what I'm thinking.
This is my thoughts:
transport | protocol | core api | helper api | libraries/frameworks
-----------------------------------------------------------------------------------------------------------
net | http1 | | low level framework |
tls | https/http2 | low level http generic API | high level http generic | high level framework
quic | http3 | | |
Essentially 3-5 layers depending on what makes sense. At least conceptually. In practice it might be difficult, e.g. I'm not sure whether the transport and protocol layers should/can be separated in all cases.
I think what you are aiming for is the "helper api" layer while I'm looking for "core api". Whether it actually makes sense to separate these is worth discussing.
reasons I wanted to know how to best contact you
I hope you got the answer?
This is a great diagram! And totally aligns with how I have grown to think about it lately. One addition I would make is to add http1
and https
to the quic
protocol layer so we understand what the future will look like in a complete sense.
I might also think about naming a bit more, maybe: transport
, protocol
, foundation
, user
and framework
apis? My nit pick on this is that the word core
is ambiguous, as in the end I think we would expose all but the framework
api as a part of "node core" which would lead the double usage of "core" to confuse people.
This is a great diagram! And totally aligns with how I have grown to think about it lately. One addition I would make is to add
http1
andhttps
to thequic
protocol layer so we understand what the future will look like in a complete sense.I might also think about naming a bit more, maybe:
transport
,protocol
,foundation
,user
andframework
apis? My nit pick on this is that the wordcore
is ambiguous, as in the end I think we would expose all but theframework
api as a part of "node core" which would lead the double usage of "core" to confuse people.
I agree with all of the above.
transport | protocol | core | working group | user |
---|---|---|---|---|
net | http | low level generic api | high level generic api | framework api |
tls | https | |||
quic | http2 | |||
http3 |
Applying this to undici it would look something like:
transport | protocol | core | working group | user |
---|---|---|---|---|
net | http | dispatch | request, stream, connect, upgrade | got |
tls | https |
A few proposed updates from our call:
transport | protocol | core | working group | user |
---|---|---|---|---|
tcp | http | quic compat api | high level generic api | framework api |
tls | https | |||
quic |
httpNext.createServer({})
http.createServer({
protocol: 'quic+http1',
port: 1234
}).on('session', (sess) => fmw.emit('session', sess))
http.createServer({
protocol: 'https'
}).on('session', (sess) => fmw.emit('session', sess))
TBQH, maybe we do need one more layer in there?