-
-
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
http1andhttpsto thequicprotocol 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,userandframeworkapis? My nit pick on this is that the wordcoreis ambiguous, as in the end I think we would expose all but theframeworkapi 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?