Skip to content

Commit

Permalink
feat(node/http): HTTP Server/Response improvements (#1448)
Browse files Browse the repository at this point in the history
Co-authored-by: Aaron O'Mullan <aaron.omullan@gmail.com>
  • Loading branch information
v 1 r t l and AaronO authored Oct 29, 2021
1 parent 0b31c16 commit 618f7ad
Showing 1 changed file with 96 additions and 49 deletions.
145 changes: 96 additions & 49 deletions node/http.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Status as STATUS_CODES } from "../http/http_status.ts";
import { Buffer } from "./buffer.ts";
import { EventEmitter } from "./events.ts";
import NodeReadable from "./_stream/readable.ts";
import NodeWritable from "./_stream/writable.ts";

Expand Down Expand Up @@ -41,7 +42,6 @@ const METHODS = [
];

type Chunk = string | Buffer | Uint8Array;
type Headers = Record<string, string>;

function chunkToU8(chunk: Chunk): Uint8Array {
if (typeof chunk === "string") {
Expand All @@ -52,12 +52,13 @@ function chunkToU8(chunk: Chunk): Uint8Array {
}

export class ServerResponse extends NodeWritable {
private status?: number;
private headers: Headers;
statusCode?: number = undefined;
statusMessage?: string = undefined;
#headers = new Headers({});
private readable: ReadableStream;
headersSent: boolean;
private reqEvent: Deno.RequestEvent;
private firstChunk: Chunk | null;
headersSent = false;
#reqEvent: Deno.RequestEvent;
#firstChunk: Chunk | null = null;

constructor(reqEvent: Deno.RequestEvent) {
let controller: ReadableByteStreamController;
Expand All @@ -72,21 +73,23 @@ export class ServerResponse extends NodeWritable {
emitClose: true,
write: (chunk, _encoding, cb) => {
if (!this.headersSent) {
if (this.firstChunk === null) {
this.firstChunk = chunk;
if (this.#firstChunk === null) {
this.#firstChunk = chunk;
return cb();
} else {
controller.enqueue(chunkToU8(this.firstChunk));
this.firstChunk = null;
this.respond();
controller.enqueue(chunkToU8(this.#firstChunk));
this.#firstChunk = null;
this.respond(false);
}
}
controller.enqueue(chunkToU8(chunk));
return cb();
},
final: (cb) => {
if (this.firstChunk) {
this.respond(this.firstChunk);
if (this.#firstChunk) {
this.respond(true, this.#firstChunk);
} else if (!this.headersSent) {
this.respond(true);
}
controller.close();
return cb();
Expand All @@ -99,48 +102,63 @@ export class ServerResponse extends NodeWritable {
},
});
this.readable = readable;
this.status = undefined;
this.headers = {};
this.firstChunk = null;
this.headersSent = false;
this.reqEvent = reqEvent;
this.#reqEvent = reqEvent;
}

setHeader(name: string, value: string) {
this.headers[name] = value;
this.#headers.set(name, value);
return this;
}

getHeader(name: string) {
return this.headers[name];
return this.#headers.get(name);
}
removeHeader(name: string) {
return this.#headers.delete(name);
}
getHeaderNames() {
return Array.from(this.#headers.keys());
}
hasHeader(name: string) {
return this.#headers.has(name);
}

writeHead(status: number, headers: Headers) {
this.status = status;
Object.assign(this.headers, headers);
writeHead(status: number, headers: Record<string, string>) {
this.statusCode = status;
for (const k in headers) {
this.#headers.set(k, headers[k]);
}
return this;
}

ensureHeaders(singleChunk?: Chunk) {
if (this.status === null) {
this.status = 200;
this.headers = typeof singleChunk === "string"
? { "content-type": "text/plain" }
: {};
#ensureHeaders(singleChunk?: Chunk) {
if (this.statusCode === undefined) {
this.statusCode = 200;
this.statusMessage = "OK";
}
if (typeof singleChunk === "string" && !this.hasHeader("content-type")) {
this.setHeader("content-type", "text/plain;charset=UTF-8");
}
}

respond(singleChunk?: Chunk) {
respond(final: boolean, singleChunk?: Chunk) {
this.headersSent = true;
this.ensureHeaders(singleChunk);
const body = singleChunk ?? this.readable;
this.reqEvent.respondWith(
new Response(body, { headers: this.headers, status: this.status }),
this.#ensureHeaders(singleChunk);
const body = singleChunk ?? (final ? null : this.readable);
this.#reqEvent.respondWith(
new Response(body, {
headers: this.#headers,
status: this.statusCode,
statusText: this.statusMessage,
}),
);
}
}

// TODO(@AaronO): optimize
export class IncomingMessage extends NodeReadable {
private req: Request;
url: string;

constructor(req: Request) {
// Check if no body (GET/HEAD/OPTIONS/...)
Expand All @@ -166,6 +184,9 @@ export class IncomingMessage extends NodeReadable {
},
});
this.req = req;
// TODO: consider more robust path extraction, e.g:
// url: (new URL(request.url).pathname),
this.url = req.url.slice(this.req.url.indexOf("/", 8));
}

get aborted() {
Expand All @@ -181,35 +202,61 @@ export class IncomingMessage extends NodeReadable {
get method() {
return this.req.method;
}

get url() {
// TODO: consider more robust path extraction, e.g:
// url: (new URL(request.url).pathname),
return this.req.url.slice(this.req.url.indexOf("/", 8));
}
}

type ServerHandler = (req: IncomingMessage, res: ServerResponse) => void;

export class Server {
handler: ServerHandler;
export class Server extends EventEmitter {
#handler: ServerHandler;
#listener?: Deno.Listener;
#listening = false;

constructor(handler: ServerHandler) {
this.handler = handler;
super();
this.#handler = handler;
}

// TODO(AaronO): support options object
listen(port: number, host?: string, cb?: CallableFunction) {
this.#listener = Deno.listen({ port, hostname: host });
this.#listening = true;
// TODO(@AaronO):
// @ts-ignore change EventEmitter's sig to use CallabeFunction
this.once("listening", cb ?? (() => {}));
this.#listenLoop();
this.emit("listening");
}

async listen(port: number) {
for await (const conn of Deno.listen({ port })) {
async #listenLoop() {
for await (const conn of this.#listener!) {
(async () => {
for await (const reqEvent of Deno.serveHttp(conn)) {
this.handler(
new IncomingMessage(reqEvent.request),
new ServerResponse(reqEvent),
);
const req = new IncomingMessage(reqEvent.request);
const res = new ServerResponse(reqEvent);
this.emit("request", req, res);
this.#handler(req, res);
}
})();
}
}

get listening() {
return this.#listening;
}

close() {
this.#listening = false;
this.#listener!.close();
this.emit("close");
}

address() {
const addr = this.#listener!.addr as Deno.NetAddr;
return {
port: addr.port,
address: addr.hostname,
};
}
}

export function createServer(handler: ServerHandler) {
Expand Down

0 comments on commit 618f7ad

Please sign in to comment.