From 6559a3f134760cfc0d0b3ac735ba97ec0ddae66d Mon Sep 17 00:00:00 2001 From: 186526 Date: Sun, 3 Jul 2022 11:00:26 +0000 Subject: [PATCH] Add Txiki.js Adapter. --- package.json | 6 +- src/lib.ts | 5 ++ src/platform/export.ts | 4 +- src/platform/txiki.js/serveHttp.ts | 94 +++++++++++++++++++++++++++ src/platform/txiki.js/statusCode.json | 64 ++++++++++++++++++ src/platform/txiki.ts | 36 ++++++++++ tsconfig.json | 2 +- types/txiki.d.ts | 53 +++++++++++++++ webpack.config.js | 14 +++- yarn.lock | 5 ++ 10 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 src/platform/txiki.js/serveHttp.ts create mode 100644 src/platform/txiki.js/statusCode.json create mode 100644 src/platform/txiki.ts create mode 100644 types/txiki.d.ts diff --git a/package.json b/package.json index 7a0dcc4..8e683ad 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@types/node": "^18.0.0", "@webpack-cli/generators": "^2.5.0", "axios": "^0.27.2", + "bluebird": "^3.7.2", "jest": "^28.1.2", "prettier": "^2.7.1", "ts-jest": "^28.0.5", @@ -35,11 +36,12 @@ }, "sideEffects": false, "scripts": { - "build": "yarn clean && yarn build:node && yarn build:serviceworker && yarn build:cfworker && yarn build:deno", + "build": "yarn clean && yarn build:node && yarn build:serviceworker && yarn build:cfworker && yarn build:deno && yarn build:txiki", "build:node": "BUILD_TARGET=node webpack", "build:serviceworker": "BUILD_TARGET=serviceworker webpack", "build:cfworker": "BUILD_TARGET=cfworker webpack", "build:deno": "BUILD_TARGET=deno webpack", + "build:txiki": "BUILD_TARGET=txiki webpack", "watch": "webpack --watch", "clean": "rm -rf ./dist", "demo": "env NODE_ENV=development yarn build:node && node ./dist/main.node.js", @@ -51,4 +53,4 @@ "node": ">=14.0.0" }, "type": "module" -} \ No newline at end of file +} diff --git a/src/lib.ts b/src/lib.ts index 399f599..cb0604c 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -7,6 +7,9 @@ export const platform = (() => { if (typeof Deno != "undefined") { return "Deno"; } + if (typeof tjs != "undefined") { + return "txiki.js"; + } if (typeof self != "undefined") { return "Service Worker"; } @@ -18,6 +21,8 @@ export const version = (() => { return process.version; case "Deno": return Deno.version.deno; + case "txiki.js": + return tjs.versions.tjs; case "Service Worker": return undefined; default: diff --git a/src/platform/export.ts b/src/platform/export.ts index 95d0304..1b10a19 100644 --- a/src/platform/export.ts +++ b/src/platform/export.ts @@ -1,11 +1,13 @@ import { NodePlatformAdapter } from "./node"; import { SWPlatformAdapter } from "./serviceworker"; import { DenoPlatformAdapter } from "./deno"; +import { TxikiPlatformAdapter } from "./txiki"; export const platformAdapaterMapping = { "Node.js": NodePlatformAdapter, "Service Worker": SWPlatformAdapter, "Deno": DenoPlatformAdapter, + "txiki.js": TxikiPlatformAdapter, }; -export { NodePlatformAdapter, SWPlatformAdapter, DenoPlatformAdapter }; +export { NodePlatformAdapter, SWPlatformAdapter, DenoPlatformAdapter, TxikiPlatformAdapter }; diff --git a/src/platform/txiki.js/serveHttp.ts b/src/platform/txiki.js/serveHttp.ts new file mode 100644 index 0000000..a9630a6 --- /dev/null +++ b/src/platform/txiki.js/serveHttp.ts @@ -0,0 +1,94 @@ +import { request } from "../../interface/request"; +import { response } from "../../interface"; +import { headers } from "../../interface/headers"; +import { methodENUM } from "../../interface/method"; + +import statusCode from "./statusCode.json"; + +export class HttpConn { + private closed: boolean = false; + private conn: tjs.Connection; + private reader: ReadableStreamDefaultReader; + + constructor(Connection: tjs.Connection) { + this.conn = Connection; + this.reader = this.reader ?? this.conn.readable.getReader(); + } + + private readMessage(httpMessage: string): request { + const lines = httpMessage.split("\n"); + const firstLine = lines[0]; + const dividingIndex = lines.indexOf("\r") ?? lines.indexOf(""); + const rawHeaders = lines.slice(1, dividingIndex); + + const [method, path, version] = firstLine.split(" "); + + const requestHeaders = new headers({}); + for (const header of rawHeaders) { + const [key, value] = header.split(": "); + requestHeaders.set(key, value); + } + + const url = new URL(path, `http://${requestHeaders.get("Host")}/` ?? `http://${this.conn.localAddress.ip}:${this.conn.localAddress.port}/`); + + const body = lines.slice(dividingIndex + 1).join("\n"); + + const requestMessage = new request(method, url, requestHeaders, body, {}, this.conn.remoteAddress.ip); + return requestMessage; + } + + private handleResponse(response: response) { + let responseMessage: string = ""; + responseMessage += "HTTP/1.1 " + response.status + " " + statusCode[<"100">response.status.toString()] ?? ""; + + response.headers.forEach((key, value) => { + responseMessage += "\n" + key + ": " + value; + }); + + responseMessage += "\n\n" + response.body; + + this.conn.write(new TextEncoder().encode(responseMessage)); + this.conn.shutdown(); + this.closed = true; + } + + private async read(): Promise | undefined> { + let message = ""; + const { done, value } = await this.reader.read(); + + if (done || this.closed) { + this.closed = true; + return undefined; + } + + message += String.fromCharCode(...Object.values(<{ + [key: string]: number + }>value)); + + const requestMessage = this.readMessage(message); + return requestMessage; + } + + [Symbol.asyncIterator]() { + const httpConn = this; + + return { + async next() { + if (httpConn.closed) return { done: true, value: undefined }; + return { + done: false, + value: { + request: await httpConn.read(), + respondWith: (response: response) => { + httpConn.handleResponse(response); + }, + } + } + } + } + } +} + +export default function serveHttp(Connection: tjs.Connection) { + return new HttpConn(Connection); +} \ No newline at end of file diff --git a/src/platform/txiki.js/statusCode.json b/src/platform/txiki.js/statusCode.json new file mode 100644 index 0000000..f0ceb7a --- /dev/null +++ b/src/platform/txiki.js/statusCode.json @@ -0,0 +1,64 @@ +{ + "100": "Continue", + "101": "Switching Protocols", + "102": "Processing", + "103": "Checkpoint", + "200": "OK", + "201": "Created", + "202": "Accepted", + "203": "Non-Authoritative Information", + "204": "No Content", + "205": "Reset Content", + "206": "Partial Content", + "207": "Multi-Status", + "208": "Already Reported", + "300": "Multiple Choices", + "301": "Moved Permanently", + "302": "Found", + "303": "See Other", + "304": "Not Modified", + "305": "Use Proxy", + "306": "Switch Proxy", + "307": "Temporary Redirect", + "308": "Permanent Redirect", + "400": "Bad Request", + "401": "Unauthorized", + "402": "Payment Required", + "403": "Forbidden", + "404": "Not Found", + "405": "Method Not Allowed", + "406": "Not Acceptable", + "407": "Proxy Authentication Required", + "408": "Request Time-out", + "409": "Conflict", + "410": "Gone", + "411": "Length Required", + "412": "Precondition Failed", + "413": "Request Entity Too Large", + "414": "Request-URI Too Long", + "415": "Unsupported Media Type", + "416": "Requested Range Not Satisfiable", + "417": "Expectation Failed", + "418": "I'm a teapot", + "421": "Unprocessable Entity", + "422": "Misdirected Request", + "423": "Locked", + "424": "Failed Dependency", + "426": "Upgrade Required", + "428": "Precondition Required", + "429": "Too Many Requests", + "431": "Request Header Fileds Too Large", + "451": "Unavailable For Legal Reasons", + "500": "Internal Server Error", + "501": "Not Implemented", + "502": "Bad Gateway", + "503": "Service Unavailable", + "504": "Gateway Timeout", + "505": "HTTP Version Not Supported", + "506": "Variant Also Negotiates", + "507": "Insufficient Storage", + "508": "Loop Detected", + "509": "Bandwidth Limit Exceeded", + "510": "Not Extended", + "511": "Network Authentication Required" +} \ No newline at end of file diff --git a/src/platform/txiki.ts b/src/platform/txiki.ts new file mode 100644 index 0000000..d563001 --- /dev/null +++ b/src/platform/txiki.ts @@ -0,0 +1,36 @@ +import { platformAdapater } from "./index"; +import { request } from "../interface/request"; +import { response } from "../interface/response"; +import { router } from "../router"; +import serveHttp from "./txiki.js/serveHttp"; + +export class TxikiPlatformAdapter implements platformAdapater { + public router: router; + + constructor(router: router) { + this.router = router; + } + + async listen(port?: number): Promise { + const Server = await tjs.listen("tcp", "0.0.0.0", port); + + for await (const conn of Server) { + const httpConn = serveHttp(conn); + + for await (const conn of httpConn) { + if (typeof conn == "undefined" || typeof conn.request == "undefined") { + return; + } + conn.respondWith(await this.router.respond(conn.request)); + } + } + } + + async handleRequest(nativeRequest: request): Promise> { + return nativeRequest; + } + + async handleResponse(response: response): Promise> { + return response; + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e0115fd..adc8cc8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,6 @@ "declaration": true }, "exclude": ["node_modules"], - "include": ["index.ts", "src/**/*.ts", "demo/**/*.ts", "types/deno.d.ts"], + "include": ["index.ts", "src/**/*.ts", "demo/**/*.ts", "types/*.d.ts"], // "esm": true } \ No newline at end of file diff --git a/types/txiki.d.ts b/types/txiki.d.ts new file mode 100644 index 0000000..fcbf48f --- /dev/null +++ b/types/txiki.d.ts @@ -0,0 +1,53 @@ +type Transport = "tcp" | "udp" | "pipe"; + +interface ListenOptions { + backlog?: number + // Disables dual stack mode. + ipv6Only?: boolean; + // Used on UDP only. Enable address reusing (when binding). What that means is that multiple threads or processes can bind to the same address without error (provided they all set the flag) but only the last one to bind will receive any traffic, in effect "stealing" the port from the previous listener. + reuseAddr?: boolean; +} + +declare namespace tjs { + const versions: { curl: string; quickjs: string; tjs: string; uv: string; wasm3: string }; + + export interface Address { + readonly family: number; + readonly flowinfo?: number; + readonly ip: string; + readonly port: number; + readonly scopeId?: number; + } + + export interface Connection { + readonly localAddress: Address; + readonly readable: ReadableStream; + + readonly remoteAddress: Address; + readonly writeable: WritableStream + + close(): void + read(buf: Uint8Array): Promise + write(buf: Uint8Array): Promise + + setKeepAlive(enable?: boolean): void + setNoDelay(enable?: boolean): void + shutdown(): void + } + + export interface Listener extends AsyncIterable { + readonly localAddress: Address; + + accept(): Promise; + close(): void; + + [Symbol.asyncIterator](): AsyncIterableIterator; + } + + export function listen( + transport: Transport, + host: string, + port?: string | number, + options?: ListenOptions + ): Promise +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index af675e0..e506420 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -8,6 +8,8 @@ const __dirname = path.dirname(__filename); const isProduction = process.env.NODE_ENV == "production"; +import webpack from "webpack"; + const config = { output: { path: path.resolve(__dirname, "dist"), @@ -35,7 +37,8 @@ const config = { resolve: { extensions: [".tsx", ".ts", ".jsx", ".js", "..."], fallback: { - "http": false + "http": false, + "async_hooks": false, } }, experiments: { @@ -69,6 +72,15 @@ export default () => { config.target = "webworker"; config.output.filename = "main.cfworker.js"; break; + case "txiki": + config.mode = "production"; + config.target = "es2021"; + config.output.filename = "main.txiki.js"; + config.plugins.push( + new webpack.ProvidePlugin({ + 'Promise': 'bluebird' + }) + ) default: config.target = "es6"; } diff --git a/yarn.lock b/yarn.lock index 19d6326..a7057d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1480,6 +1480,11 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"