This commit is contained in:
186526 2024-07-19 12:38:11 +08:00
parent fccf33d953
commit c216c0a62f
No known key found for this signature in database
GPG Key ID: 2ED6600900186526
34 changed files with 1302 additions and 1196 deletions

View File

@ -1,42 +1,42 @@
{
"name": "Handlers.js Devlopment Container",
"build": {
"dockerfile": "Dockerfile",
"args": {
"VARIANT": "16-bullseye"
}
},
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"dbaeumer.vscode-eslint",
"ms-vscode.vscode-typescript-next",
"oderwat.indent-rainbow",
"GitHub.copilot",
"redhat.vscode-yaml",
"ms-azuretools.vscode-docker"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "yarn install",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node",
"features": {
"git": "latest",
"docker-in-docker": {
"version": "latest",
"moby": true,
"dockerDashComposeVersion": "v1"
},
"rust": {
"version": "latest",
"profile": "minimal"
}
}
}
"name": "Handlers.js Devlopment Container",
"build": {
"dockerfile": "Dockerfile",
"args": {
"VARIANT": "16-bullseye"
}
},
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"dbaeumer.vscode-eslint",
"ms-vscode.vscode-typescript-next",
"oderwat.indent-rainbow",
"GitHub.copilot",
"redhat.vscode-yaml",
"ms-azuretools.vscode-docker"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "yarn install",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node",
"features": {
"git": "latest",
"docker-in-docker": {
"version": "latest",
"moby": true,
"dockerDashComposeVersion": "v1"
},
"rust": {
"version": "latest",
"profile": "minimal"
}
}
}

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 4
}

View File

@ -1,5 +1,5 @@
{
"compile-hero.disable-compile-files-on-did-save-code": true,
"deno.enable": false,
"editor.formatOnSave": true,
}
"editor.formatOnSave": true
}

View File

@ -3,13 +3,13 @@
> Handlers.js is a unified and lightweight web application framework for multiple platforms.
```ts
import handlerJS from "handlers.js";
import handlerJS from 'handlers.js';
const App = new handlerJS();
App.binding(
"/",
App.create("ANY", async () => "Hello World!")
'/',
App.create('ANY', async () => 'Hello World!'),
);
App.useMappingAdapter();

View File

@ -1,141 +1,141 @@
// From https://gist.github.com/186526/82b7372619fb4b029568040ee4a4aa44, Open source with MIT license
import { response, request } from "../index";
import { defaultHeaders } from "../src/interface/response";
import { response, request } from '../index';
import { defaultHeaders } from '../src/interface/response';
function uuid(): string {
const s: any[] = [];
const hexDigits = "0123456789abcdef";
const s: any[] = [];
const hexDigits = '0123456789abcdef';
for (let i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
for (let i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
s[8] = s[13] = s[18] = s[23] = "-";
s[14] = '4'; // bits 12-15 of the time_hi_and_version field to 0010
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
s[8] = s[13] = s[18] = s[23] = '-';
const uuid = s.join("");
const uuid = s.join('');
return uuid;
return uuid;
}
const codeAlternative:{[index: number]:string} = {
100: "Continue",
101: "Switching Protocols",
102: "Processing",
103: "Early Hints",
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",
226: "IM Used",
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 Large",
415: "Unsupported Media Type",
416: "Requested Range not Satisfiable",
417: "Expectation Failed",
418: "I'm a teapot",
421: "Misdirected Request",
422: "Unprocessable Entity",
423: "Locked",
424: "Failed Dependency",
426: "Upgrade Required",
428: "Precondition Required", // RFC 6585
429: "Too Many Requests",
431: "Request Header Fields Too Large", // RFC 6585
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Time-out",
505: "HTTP Version not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
const codeAlternative: { [index: number]: string } = {
100: 'Continue',
101: 'Switching Protocols',
102: 'Processing',
103: 'Early Hints',
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',
226: 'IM Used',
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 Large',
415: 'Unsupported Media Type',
416: 'Requested Range not Satisfiable',
417: 'Expectation Failed',
418: "I'm a teapot",
421: 'Misdirected Request',
422: 'Unprocessable Entity',
423: 'Locked',
424: 'Failed Dependency',
426: 'Upgrade Required',
428: 'Precondition Required', // RFC 6585
429: 'Too Many Requests',
431: 'Request Header Fields Too Large', // RFC 6585
451: 'Unavailable For Legal Reasons',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Time-out',
505: 'HTTP Version not Supported',
506: 'Variant Also Negotiates',
507: 'Insufficient Storage',
508: 'Loop Detected',
510: 'Not Extended',
511: 'Network Authentication Required',
};
const template: string =
'<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /><meta name="renderer" content="webkit" /><meta name="force-rendering" content="webkit" /><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /><link rel="preconnect" href="https://fonts.gstatic.com"><link href="https://fonts.googleapis.com/css2?family=Google+Sans&family=Fira+Mono&family=Ubuntu&display=swap" rel="stylesheet"><title>${statusCode} | ${codeAlternative}</title><link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet"><style type="text/css">:root{ --background-color: #fff; --font-color: #000; --font-color-lighter: rgb(87, 89, 88); --font-size-main: 3.545rem; --font-size-description: 1.245rem; --box-color: #F2F2F2; --working-color: #137333; --working-color-background: #e6f4ea; --error-color-background: #fce8e6; --error-color: #c5221f; --working-with-error-color: #b05a00; --working-with-error-color-background: #fef7e0; --icon-size: 48px;} body{ margin: 2rem 2rem; font-family: Google Sans, Ubuntu, Roboto, Noto Sans SC, sans-serif; color: var(--font-color); background-color: var(--background-color);} nav{ margin-left: 1rem;} nav description{ font-family: Ubuntu, Roboto, Noto Sans SC, sans-serif; font-size: var(--font-size-description); line-height: var(--fonr-size-description); color: var(--font-color-lighter);} nav main{ font-size: var(--font-size-main); line-height: var(--font-size-main); font-family: Fira Mono, Ubuntu, monospace;} code{ font-family: Fira Mono, monospace;} status{ margin-top: 2.5rem; display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; align-items: center;} status block.status-block{ background-color: var(--box-color); padding: 2rem; margin: 1rem 1rem; min-height: 3rem; border-radius: 9px; flex-grow: 1;} status block.status-block.working-block{ background-color: var(--working-color-background);} status block.status-block.error-block{ background-color: var(--error-color-background);} status block.status-block.working-with-error-block{ background-color: var(--working-with-error-color-background);} .status-block main{ font-size: calc(var(--font-size-description) + 0.1rem);} .status-working{ color: var(--working-color);} .status-error{ color: var(--error-color);} .status-working-with-error{ color: var(--working-with-error-color);} icon{ font-size: var(--icon-size) !important;} a{ text-decoration: none; color: #1967d2;} reason{ display: flex; flex-direction: row; flex-wrap: wrap; justify-content: space-between; align-items: center;} reason>*{ display: block; margin: 1rem; flex-grow: 1; max-width: 40%;} reason main{ font-size: calc(var(--font-size-description) + 0.2rem); font-weight: 550;} footer{ margin: 1rem; color: var(--font-color-lighter); font-size: calc(var(--font-size-description) - 0.4rem);} footer>request-status{ font-size: calc(var(--font-size-description) - 0.6rem);} footer>*{ display: block;} @media screen and (max-width:480px){ body{ margin: 6rem 2rem;} :root{ --font-size-main: 3.0rem; --font-size-description: 1.045rem;} reason>*{ max-width: 100%;} footer{ font-size: calc(var(--font-size-description) - 0.2rem);} footer>request-status{ font-size: calc(var(--font-size-description) - 0.4rem);}} @media screen and (min-width: 768px){ body{ margin: 8% 10%;} nav *{ display: inline-block; margin-left: 1%;}} @media (prefers-color-scheme: dark){ :root{ --font-color: rgba(255, 255, 255, 0.86); --font-color-lighter: rgba(255, 255, 255, 0.4); --background-color: rgb(0, 0, 0); --box-color: rgb(40 40 40 / 73%); --working-color-background: #07220f; --error-color-background: #270501; --working-with-error-color-background: #392605;}} </style></head><body><nav><main>${statusCode}</main><description>${codeAlternative}</description></nav><status><block class="status-block ${clientStatusShort}-block" id="client-status-block"><icon class="material-icons-outlined status-${clientStatusShort}">web</icon><main>Your Client</main><status-text class="status-${clientStatusShort}">${clientStatus}</status-text></block><block class="status-block working-block" id="edge-status-block"><icon class="material-icons-outlined status-working">alt_route</icon><main>Handlers.js</main><status-text class="status-working">Working </block><block class="status-block ${ServerStatusLowerCase}-block" id="website-status-block"><icon class="material-icons-outlined status-${ServerStatusLowerCase}">widgets</icon><main>Route</main><status-text class="status-${ServerStatusLowerCase}">${ServerStatus}</status-text></block></status><reason><explain><main>${codeAlternative}</main><p>${explain} </p></explain><howto><main>What can I do?</main><p>${howto} </p></howto></reason><footer><provider>Running with <a href="https://git.186526.xyz/186526/Handlers.js">Handlers.js</a>.</provider><br><request-status>Your IP is <code>${ip}</code><br>Request ID is <code>${id}</code></request-status></footer></body></html>';
'<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /><meta name="renderer" content="webkit" /><meta name="force-rendering" content="webkit" /><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /><link rel="preconnect" href="https://fonts.gstatic.com"><link href="https://fonts.googleapis.com/css2?family=Google+Sans&family=Fira+Mono&family=Ubuntu&display=swap" rel="stylesheet"><title>${statusCode} | ${codeAlternative}</title><link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet"><style type="text/css">:root{ --background-color: #fff; --font-color: #000; --font-color-lighter: rgb(87, 89, 88); --font-size-main: 3.545rem; --font-size-description: 1.245rem; --box-color: #F2F2F2; --working-color: #137333; --working-color-background: #e6f4ea; --error-color-background: #fce8e6; --error-color: #c5221f; --working-with-error-color: #b05a00; --working-with-error-color-background: #fef7e0; --icon-size: 48px;} body{ margin: 2rem 2rem; font-family: Google Sans, Ubuntu, Roboto, Noto Sans SC, sans-serif; color: var(--font-color); background-color: var(--background-color);} nav{ margin-left: 1rem;} nav description{ font-family: Ubuntu, Roboto, Noto Sans SC, sans-serif; font-size: var(--font-size-description); line-height: var(--fonr-size-description); color: var(--font-color-lighter);} nav main{ font-size: var(--font-size-main); line-height: var(--font-size-main); font-family: Fira Mono, Ubuntu, monospace;} code{ font-family: Fira Mono, monospace;} status{ margin-top: 2.5rem; display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; align-items: center;} status block.status-block{ background-color: var(--box-color); padding: 2rem; margin: 1rem 1rem; min-height: 3rem; border-radius: 9px; flex-grow: 1;} status block.status-block.working-block{ background-color: var(--working-color-background);} status block.status-block.error-block{ background-color: var(--error-color-background);} status block.status-block.working-with-error-block{ background-color: var(--working-with-error-color-background);} .status-block main{ font-size: calc(var(--font-size-description) + 0.1rem);} .status-working{ color: var(--working-color);} .status-error{ color: var(--error-color);} .status-working-with-error{ color: var(--working-with-error-color);} icon{ font-size: var(--icon-size) !important;} a{ text-decoration: none; color: #1967d2;} reason{ display: flex; flex-direction: row; flex-wrap: wrap; justify-content: space-between; align-items: center;} reason>*{ display: block; margin: 1rem; flex-grow: 1; max-width: 40%;} reason main{ font-size: calc(var(--font-size-description) + 0.2rem); font-weight: 550;} footer{ margin: 1rem; color: var(--font-color-lighter); font-size: calc(var(--font-size-description) - 0.4rem);} footer>request-status{ font-size: calc(var(--font-size-description) - 0.6rem);} footer>*{ display: block;} @media screen and (max-width:480px){ body{ margin: 6rem 2rem;} :root{ --font-size-main: 3.0rem; --font-size-description: 1.045rem;} reason>*{ max-width: 100%;} footer{ font-size: calc(var(--font-size-description) - 0.2rem);} footer>request-status{ font-size: calc(var(--font-size-description) - 0.4rem);}} @media screen and (min-width: 768px){ body{ margin: 8% 10%;} nav *{ display: inline-block; margin-left: 1%;}} @media (prefers-color-scheme: dark){ :root{ --font-color: rgba(255, 255, 255, 0.86); --font-color-lighter: rgba(255, 255, 255, 0.4); --background-color: rgb(0, 0, 0); --box-color: rgb(40 40 40 / 73%); --working-color-background: #07220f; --error-color-background: #270501; --working-with-error-color-background: #392605;}} </style></head><body><nav><main>${statusCode}</main><description>${codeAlternative}</description></nav><status><block class="status-block ${clientStatusShort}-block" id="client-status-block"><icon class="material-icons-outlined status-${clientStatusShort}">web</icon><main>Your Client</main><status-text class="status-${clientStatusShort}">${clientStatus}</status-text></block><block class="status-block working-block" id="edge-status-block"><icon class="material-icons-outlined status-working">alt_route</icon><main>Handlers.js</main><status-text class="status-working">Working </block><block class="status-block ${ServerStatusLowerCase}-block" id="website-status-block"><icon class="material-icons-outlined status-${ServerStatusLowerCase}">widgets</icon><main>Route</main><status-text class="status-${ServerStatusLowerCase}">${ServerStatus}</status-text></block></status><reason><explain><main>${codeAlternative}</main><p>${explain} </p></explain><howto><main>What can I do?</main><p>${howto} </p></howto></reason><footer><provider>Running with <a href="https://git.186526.xyz/186526/Handlers.js">Handlers.js</a>.</provider><br><request-status>Your IP is <code>${ip}</code><br>Request ID is <code>${id}</code></request-status></footer></body></html>';
export default (
errorCode: number,
errorMessage: string = `The route you are trying access is error.`
) =>
async (request: request<any>) => {
return new response(
template
.replaceAll("${statusCode}", errorCode.toString())
.replaceAll("${codeAlternative}", codeAlternative[errorCode])
.replaceAll("${explain}", errorMessage)
.replaceAll(
"${howto}",
`You can go to the <a href="/">homepage</a> of the website.`
)
.replaceAll("${id}", uuid())
.replaceAll(
"${ServerStatus}",
errorCode >= 500
? "Error"
: errorCode <= 500 && errorCode >= 400
? `Working but ${codeAlternative[errorCode]}`
: "Working"
)
.replaceAll(
"${ServerStatusLowerCase}",
errorCode >= 500
? "error"
: errorCode <= 500 && errorCode >= 400
? `working-with-error`
: "working"
)
.replaceAll("${ip}", request.ip)
.replaceAll(
"${clientStatus}",
errorCode <= 500 && errorCode >= 400
? `Working but receive ${codeAlternative[errorCode]}`
: "Working"
)
.replaceAll(
"${clientStatusShort}",
errorCode <= 500 && errorCode >= 400
? `working-with-error`
: "working"
),
errorCode,
(() => {
const Headers = new defaultHeaders();
Headers.set("Content-Type", "text/html; charset=utf-8");
return Headers;
})()
);
};
errorCode: number,
errorMessage: string = `The route you are trying access is error.`,
) =>
async (request: request<any>) => {
return new response(
template
.replaceAll('${statusCode}', errorCode.toString())
.replaceAll('${codeAlternative}', codeAlternative[errorCode])
.replaceAll('${explain}', errorMessage)
.replaceAll(
'${howto}',
`You can go to the <a href="/">homepage</a> of the website.`,
)
.replaceAll('${id}', uuid())
.replaceAll(
'${ServerStatus}',
errorCode >= 500
? 'Error'
: errorCode <= 500 && errorCode >= 400
? `Working but ${codeAlternative[errorCode]}`
: 'Working',
)
.replaceAll(
'${ServerStatusLowerCase}',
errorCode >= 500
? 'error'
: errorCode <= 500 && errorCode >= 400
? `working-with-error`
: 'working',
)
.replaceAll('${ip}', request.ip)
.replaceAll(
'${clientStatus}',
errorCode <= 500 && errorCode >= 400
? `Working but receive ${codeAlternative[errorCode]}`
: 'Working',
)
.replaceAll(
'${clientStatusShort}',
errorCode <= 500 && errorCode >= 400
? `working-with-error`
: 'working',
),
errorCode,
(() => {
const Headers = new defaultHeaders();
Headers.set('Content-Type', 'text/html; charset=utf-8');
return Headers;
})(),
);
};

View File

@ -1,77 +1,71 @@
import {
rootRouter,
method,
handler,
route,
response,
} from "../index";
import errorHandler from "./errorHandler";
import { rootRouter, method, handler, route, response } from '../index';
import errorHandler from './errorHandler';
const App = new rootRouter<any, any>();
App.binding(
"/(.*)",
new handler("ANY", [
async (request, response) => {
console.log(request);
return undefined;
},
])
'/(.*)',
new handler('ANY', [
async (request, response) => {
console.log(request);
return undefined;
},
]),
);
App.binding(
"/",
App.create(
"ANY",
(): Promise<string> =>
new Promise(() => {
console.log("Hello World!");
throw new response("Hello World!");
})
)
'/',
App.create(
'ANY',
(): Promise<string> =>
new Promise(() => {
console.log('Hello World!');
throw new response('Hello World!');
}),
),
).binding(
"/(.*)",
App.create(
"ANY",
(): Promise<string> =>
new Promise((resolve) => {
resolve("Hello World?")
})
)
'/(.*)',
App.create(
'ANY',
(): Promise<string> =>
new Promise((resolve) => {
resolve('Hello World?');
}),
),
);
App.route("/v1/(.*)")
.add(
new route(
["/echo", "/echo/(.*)"],
[
new handler(method["GET"], [
async (requestMessage, responseMessage) => {
responseMessage = responseMessage ?? new response("");
responseMessage?.headers.set("Hello", "World");
responseMessage.body = requestMessage.url.pathname;
return responseMessage;
},
]),
new handler(method["POST"], [
async (requestMessage, responseMessage) => {
responseMessage = responseMessage ?? new response("");
responseMessage.body = requestMessage.body;
return responseMessage;
},
]),
]
)
)
.binding(
"/error",
App.create(method["ANY"], async () => {
throw new Error("Nothing will happen here.");
})
)
.useErrorResponder(errorHandler);
App.route('/v1/(.*)')
.add(
new route(
['/echo', '/echo/(.*)'],
[
new handler(method['GET'], [
async (requestMessage, responseMessage) => {
responseMessage = responseMessage ?? new response('');
responseMessage?.headers.set('Hello', 'World');
responseMessage.body = requestMessage.url.pathname;
return responseMessage;
},
]),
new handler(method['POST'], [
async (requestMessage, responseMessage) => {
responseMessage = responseMessage ?? new response('');
responseMessage.body = requestMessage.body;
return responseMessage;
},
]),
],
),
)
.binding(
'/error',
App.create(method['ANY'], async () => {
throw new Error('Nothing will happen here.');
}),
)
.useErrorResponder(errorHandler);
App.useMappingAdapter();
App.listen(8080);
export default App;
export default App;

View File

@ -1,12 +1,12 @@
import { rootRouter } from "./src/router";
import { rootRouter } from './src/router';
export { handler } from "./src/handler";
export { route } from "./src/route";
export { router } from "./src/router";
export { methodENUM as method } from "./src/interface/method";
export { response } from "./src/interface/response";
export { request } from "./src/interface/request";
export * as platformAdapater from "./src/platform/export";
export { handler } from './src/handler';
export { route } from './src/route';
export { router } from './src/router';
export { methodENUM as method } from './src/interface/method';
export { response } from './src/interface/response';
export { request } from './src/interface/request';
export * as platformAdapater from './src/platform/export';
export { rootRouter };
export default rootRouter;

View File

@ -1,68 +1,70 @@
{
"name": "handlers.js",
"description": "Handlers.js is a unified and lightweight web application framework for multiple platforms.",
"version": "0.1.1-2",
"main": "./dist/index.js",
"webpack": "./dist/index.js",
"browser": "./dist/main.serviceworker.js",
"module": "./dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"LICENSE"
],
"author": "186526 <i@186526.xyz>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/186526/handlers.js"
},
"keywords": [
"web framework",
"lightweight",
"cross-platform",
"unified"
],
"dependencies": {
"path-to-regexp": "^6.2.1"
},
"devDependencies": {
"@cloudflare/workers-types": "^3.13.0",
"@types/jest": "^28.1.4",
"@types/node": "^18.0.0",
"@webpack-cli/generators": "^2.5.0",
"axios": "^0.27.2",
"bluebird": "^3.7.2",
"bun-types": "^0.1.4",
"jest": "^28.1.2",
"prettier": "^2.7.1",
"ts-jest": "^28.0.5",
"ts-loader": "^9.3.1",
"ts-node": "^10.8.1",
"typescript": "^4.7.4",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0"
},
"sideEffects": false,
"scripts": {
"build": "yarn clean && yarn build:node && yarn build:serviceworker && yarn build:cfworker && yarn build:deno && yarn build:txiki && yarn build:bun",
"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",
"build:bun": "BUILD_TARGET=bun webpack",
"watch": "webpack --watch",
"clean": "rm -rf ./dist",
"demo": "env NODE_ENV=development yarn build:node && node ./dist/main.node.js",
"tsc": "tsc",
"test:node": "jest ./test/node.test.ts",
"test:deno": "BUILD_TARGET=deno:test webpack && jest ./test/deno.test.ts",
"coverage": "jest --collectCoverage --",
"prepublish": "env NODE_ENV=production yarn build && yarn tsc"
},
"engines": {
"node": ">=18.0.0"
},
"type": "module"
"name": "handlers.js",
"description": "Handlers.js is a unified and lightweight web application framework for multiple platforms.",
"version": "0.1.1-2",
"main": "./dist/index.js",
"webpack": "./dist/index.js",
"browser": "./dist/main.serviceworker.js",
"module": "./dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"LICENSE"
],
"author": "186526 <i@186526.xyz>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/186526/handlers.js"
},
"keywords": [
"web framework",
"lightweight",
"cross-platform",
"unified"
],
"dependencies": {
"path-to-regexp": "^6.2.1"
},
"devDependencies": {
"@cloudflare/workers-types": "^3.13.0",
"@types/jest": "^28.1.4",
"@types/node": "^18.0.0",
"@webpack-cli/generators": "^2.5.0",
"axios": "^0.27.2",
"bluebird": "^3.7.2",
"bun-types": "^0.1.4",
"jest": "^28.1.2",
"prettier": "^2.7.1",
"ts-jest": "^28.0.5",
"ts-loader": "^9.3.1",
"ts-node": "^10.8.1",
"typescript": "^4.7.4",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0"
},
"sideEffects": false,
"scripts": {
"build": "yarn clean && yarn build:node && yarn build:serviceworker && yarn build:cfworker && yarn build:deno && yarn build:txiki && yarn build:bun",
"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",
"build:bun": "BUILD_TARGET=bun webpack",
"watch": "webpack --watch",
"clean": "rm -rf ./dist",
"demo": "env NODE_ENV=development yarn build:node && node ./dist/main.node.js",
"tsc": "tsc",
"test:node": "jest ./test/node.test.ts",
"test:deno": "BUILD_TARGET=deno:test webpack && jest ./test/deno.test.ts",
"coverage": "jest --collectCoverage --",
"prepublish": "env NODE_ENV=production yarn build && yarn tsc",
"format": "prettier --write \"**/*.{ts,json,md}\" "
},
"engines": {
"node": ">=18.0.0"
},
"type": "module",
"packageManager": "yarn@1.22.22"
}

View File

@ -1,43 +1,46 @@
import { method, request, responder, response } from "./interface/index";
import { method, request, responder, response } from './interface/index';
export class handler<RequestCustomType, ResponseCustomType> {
public responders: responder<RequestCustomType, ResponseCustomType>[];
public method: method;
public responders: responder<RequestCustomType, ResponseCustomType>[];
public method: method;
constructor(
method: method,
responders: responder<RequestCustomType, ResponseCustomType>[]
) {
this.responders = responders;
this.method = method;
}
constructor(
method: method,
responders: responder<RequestCustomType, ResponseCustomType>[],
) {
this.responders = responders;
this.method = method;
}
add(responder: responder<RequestCustomType, ResponseCustomType>) {
this.responders.push(responder);
}
add(responder: responder<RequestCustomType, ResponseCustomType>) {
this.responders.push(responder);
}
async respond(
request: request<RequestCustomType>,
responseMessage: response<ResponseCustomType> = new response<ResponseCustomType>(
""
)
): Promise<response<ResponseCustomType> | void> {
switch (this.responders.length) {
case 0:
Promise.reject("No responders found in this handler.");
break;
case 1:
return this.responders[0](request, responseMessage);
default:
for (let responder of this.responders) {
let thisResponse = await responder(request, responseMessage);
if (thisResponse instanceof response) {
responseMessage = thisResponse;
}
}
return responseMessage;
}
}
async respond(
request: request<RequestCustomType>,
responseMessage: response<ResponseCustomType> = new response<ResponseCustomType>(
'',
),
): Promise<response<ResponseCustomType> | void> {
switch (this.responders.length) {
case 0:
Promise.reject('No responders found in this handler.');
break;
case 1:
return this.responders[0](request, responseMessage);
default:
for (let responder of this.responders) {
let thisResponse = await responder(
request,
responseMessage,
);
if (thisResponse instanceof response) {
responseMessage = thisResponse;
}
}
return responseMessage;
}
}
}
export default handler;

View File

@ -1,31 +1,31 @@
import * as lib from "../lib";
import * as lib from '../lib';
export class headers {
public headers: { [key: string]: string } = {};
constructor(headers: { [key: string]: string }) {
this.headers = {};
Object.keys(headers).forEach((key) => {
this.headers[lib.firstUpperCase(key)] = headers[key];
});
}
delete(key: string) {
delete this.headers[lib.firstUpperCase(key)];
}
get(key: string): string | undefined {
return this.headers[lib.firstUpperCase(key)];
}
has(key: string): boolean {
return this.headers.hasOwnProperty(lib.firstUpperCase(key));
}
set(key: string, value: string) {
this.headers[lib.firstUpperCase(key)] = value;
}
toObject() {
return this.headers;
}
forEach(func: (key: string, value: string) => any) {
Object.keys(this.headers).forEach((key) => {
func(key, this.headers[key]);
})
}
public headers: { [key: string]: string } = {};
constructor(headers: { [key: string]: string }) {
this.headers = {};
Object.keys(headers).forEach((key) => {
this.headers[lib.firstUpperCase(key)] = headers[key];
});
}
delete(key: string) {
delete this.headers[lib.firstUpperCase(key)];
}
get(key: string): string | undefined {
return this.headers[lib.firstUpperCase(key)];
}
has(key: string): boolean {
return this.headers.hasOwnProperty(lib.firstUpperCase(key));
}
set(key: string, value: string) {
this.headers[lib.firstUpperCase(key)] = value;
}
toObject() {
return this.headers;
}
forEach(func: (key: string, value: string) => any) {
Object.keys(this.headers).forEach((key) => {
func(key, this.headers[key]);
});
}
}
export default headers;

View File

@ -1,7 +1,7 @@
export { request } from "./request";
export { response } from "./response";
export { method } from "./method";
export { headers } from "./headers";
export { responder } from "./responder";
export const AllMismatchInterrupted = new Error("AllMismatchInterrupted");
export { request } from './request';
export { response } from './response';
export { method } from './method';
export { headers } from './headers';
export { responder } from './responder';
export const AllMismatchInterrupted = new Error('AllMismatchInterrupted');
export type path = string | RegExp;

View File

@ -1,61 +1,72 @@
export enum methodENUM {
/**
* The `CONNECT` method establishes a tunnel to the server identified by the
* target resource.
*/
CONNECT = "CONNECT",
/**
* The `CONNECT` method establishes a tunnel to the server identified by the
* target resource.
*/
CONNECT = 'CONNECT',
/**
* The `DELETE` method deletes the specified resource.
*/
DELETE = "DELETE",
/**
* The `DELETE` method deletes the specified resource.
*/
DELETE = 'DELETE',
/**
* The `GET` method requests a representation of the specified resource.
* Requests using GET should only retrieve data.
*/
GET = "GET",
/**
* The `GET` method requests a representation of the specified resource.
* Requests using GET should only retrieve data.
*/
GET = 'GET',
/**
* The `HEAD` method asks for a response identical to that of a GET request,
* but without the response body.
*/
HEAD = "HEAD",
/**
* The `HEAD` method asks for a response identical to that of a GET request,
* but without the response body.
*/
HEAD = 'HEAD',
/**
* The `OPTIONS` method is used to describe the communication options for the
* target resource.
*/
OPTIONS = "OPTIONS",
/**
* The `OPTIONS` method is used to describe the communication options for the
* target resource.
*/
OPTIONS = 'OPTIONS',
/**
* The PATCH method is used to apply partial modifications to a resource.
*/
PATCH = "PATCH",
/**
* The PATCH method is used to apply partial modifications to a resource.
*/
PATCH = 'PATCH',
/**
* The `POST` method is used to submit an entity to the specified resource,
* often causing a change in state or side effects on the server.
*/
POST = "POST",
/**
* The `POST` method is used to submit an entity to the specified resource,
* often causing a change in state or side effects on the server.
*/
POST = 'POST',
/**
* The `PUT` method replaces all current representations of the target
* resource with the request payload.
*/
PUT = "PUT",
/**
* The `PUT` method replaces all current representations of the target
* resource with the request payload.
*/
PUT = 'PUT',
/**
* The `TRACE` method performs a message loop-back test along the path to the
* target resource.
*/
TRACE = "TRACE",
/**
* The `ANY` method will match any method.
*/
ANY = "ANY",
/**
* The `TRACE` method performs a message loop-back test along the path to the
* target resource.
*/
TRACE = 'TRACE',
/**
* The `ANY` method will match any method.
*/
ANY = 'ANY',
}
export type method = "CONNECT" | "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT" | "TRACE" | "ANY" | methodENUM;
export type method =
| 'CONNECT'
| 'DELETE'
| 'GET'
| 'HEAD'
| 'OPTIONS'
| 'PATCH'
| 'POST'
| 'PUT'
| 'TRACE'
| 'ANY'
| methodENUM;
export default method;
export default method;

View File

@ -1,34 +1,37 @@
import method from "./method";
import headers from "./headers";
import method from './method';
import headers from './headers';
export class request<RequestCustomType> {
public readonly method: method;
public readonly url: URL;
public originURL?: URL;
public readonly headers: headers;
public readonly body: any;
public readonly query: URLSearchParams;
public params: { [key: string]: string | undefined };
public custom: RequestCustomType;
public ip: string;
public constructor(
method: method,
url: URL,
headers: headers,
body: any,
params: { [key: string]: string },
ip: string = "0.0.0.0"
) {
this.method = method;
this.url = url;
this.headers = headers;
this.body = body;
this.query = new URLSearchParams(url.search);
this.params = params;
this.ip = headers.get("X-REAL-IP") ?? headers.get("X-Forwarded-For")?.split(" ")[0] ?? ip;
}
public extends(custom: RequestCustomType): request<RequestCustomType> {
this.custom = custom;
return this;
}
public readonly method: method;
public readonly url: URL;
public originURL?: URL;
public readonly headers: headers;
public readonly body: any;
public readonly query: URLSearchParams;
public params: { [key: string]: string | undefined };
public custom: RequestCustomType;
public ip: string;
public constructor(
method: method,
url: URL,
headers: headers,
body: any,
params: { [key: string]: string },
ip: string = '0.0.0.0',
) {
this.method = method;
this.url = url;
this.headers = headers;
this.body = body;
this.query = new URLSearchParams(url.search);
this.params = params;
this.ip =
headers.get('X-REAL-IP') ??
headers.get('X-Forwarded-For')?.split(' ')[0] ??
ip;
}
public extends(custom: RequestCustomType): request<RequestCustomType> {
this.custom = custom;
return this;
}
}
export default request;

View File

@ -1,8 +1,8 @@
import { request, response } from "./index";
import { request, response } from './index';
export interface responder<RequestCustomType, ResponseCustomType> {
(
request: request<RequestCustomType>,
reponse?: response<ResponseCustomType>
): Promise<response<ResponseCustomType>> | Promise<void> | void;
(
request: request<RequestCustomType>,
reponse?: response<ResponseCustomType>,
): Promise<response<ResponseCustomType>> | Promise<void> | void;
}
export default responder;

View File

@ -1,35 +1,35 @@
import headers from "./headers";
import packageJSON from "../../package.json";
import { platform, version } from "../lib";
import headers from './headers';
import packageJSON from '../../package.json';
import { platform, version } from '../lib';
export class defaultHeaders extends headers {
constructor(headers: { [key: string]: string } = {}) {
super(headers);
if (!this.has("Content-Type"))
this.set("Content-Type", "text/plain; charset=utf-8");
this.set(
"Server",
`Handlers.js/${packageJSON.version} ${platform}/${version}`
);
}
constructor(headers: { [key: string]: string } = {}) {
super(headers);
if (!this.has('Content-Type'))
this.set('Content-Type', 'text/plain; charset=utf-8');
this.set(
'Server',
`Handlers.js/${packageJSON.version} ${platform}/${version}`,
);
}
}
export class response<ResponseCustomType> {
public status: number;
public headers: headers;
public body: any;
public custom: ResponseCustomType;
public constructor(
body: any,
status: number = 200,
headers: headers = new defaultHeaders()
) {
this.status = status;
this.headers = headers;
this.body = body;
}
public extends(custom: ResponseCustomType): response<ResponseCustomType> {
this.custom = custom;
return this;
}
public status: number;
public headers: headers;
public body: any;
public custom: ResponseCustomType;
public constructor(
body: any,
status: number = 200,
headers: headers = new defaultHeaders(),
) {
this.status = status;
this.headers = headers;
this.body = body;
}
public extends(custom: ResponseCustomType): response<ResponseCustomType> {
this.custom = custom;
return this;
}
}
export default response;

View File

@ -1,34 +1,34 @@
export const firstUpperCase = ([first, ...rest]: string) =>
first?.toUpperCase() + rest.map((e) => e.toLowerCase()).join("");
first?.toUpperCase() + rest.map((e) => e.toLowerCase()).join('');
export const platform = (() => {
if (typeof process != "undefined") {
return "Node.js";
}
if (typeof Deno != "undefined") {
return "Deno";
}
if (typeof Bun != "undefined") {
return "Bun";
}
if (typeof tjs != "undefined") {
return "txiki.js";
}
if (typeof self != "undefined") {
return "Service Worker";
}
return undefined;
if (typeof process != 'undefined') {
return 'Node.js';
}
if (typeof Deno != 'undefined') {
return 'Deno';
}
if (typeof Bun != 'undefined') {
return 'Bun';
}
if (typeof tjs != 'undefined') {
return 'txiki.js';
}
if (typeof self != 'undefined') {
return 'Service Worker';
}
return undefined;
})();
export const version = (() => {
switch (platform) {
case "Node.js" || "Bun":
return process.version;
case "Deno":
return Deno.version.deno;
case "txiki.js":
return tjs.versions.tjs;
case "Service Worker":
return undefined;
default:
return undefined;
}
switch (platform) {
case 'Node.js' || 'Bun':
return process.version;
case 'Deno':
return Deno.version.deno;
case 'txiki.js':
return tjs.versions.tjs;
case 'Service Worker':
return undefined;
default:
return undefined;
}
})();

View File

@ -1,20 +1,20 @@
import { SWPlatformAdapter } from "./serviceworker";
import { platformAdapater } from "./index";
import { SWPlatformAdapter } from './serviceworker';
import { platformAdapater } from './index';
export class BunPlatformAdapter<T = any, K = any>
extends SWPlatformAdapter<T, K>
implements platformAdapater<T, K>
extends SWPlatformAdapter<T, K>
implements platformAdapater<T, K>
{
async listen(port: number): Promise<void> {
Bun.serve({
fetch: async (request: Request): Promise<Response> => {
return await this.handleResponse(
await this.handleRequest(request).then((request) =>
this.router.respond(request)
)
);
},
port,
});
}
async listen(port: number): Promise<void> {
Bun.serve({
fetch: async (request: Request): Promise<Response> => {
return await this.handleResponse(
await this.handleRequest(request).then((request) =>
this.router.respond(request),
),
);
},
port,
});
}
}

View File

@ -1,26 +1,25 @@
import { SWPlatformAdapter } from "./serviceworker";
import { platformAdapater } from "./index";
import { request } from "../interface/request";
import { headers } from "../interface/headers";
import { methodENUM } from "src/interface/method";
import { SWPlatformAdapter } from './serviceworker';
import { platformAdapater } from './index';
import { request } from '../interface/request';
import { headers } from '../interface/headers';
import { methodENUM } from 'src/interface/method';
const DefaultConn: Deno.Conn = {
localAddr: {
transport: "tcp",
hostname: "0.0.0.0",
transport: 'tcp',
hostname: '0.0.0.0',
port: 80,
},
remoteAddr: {
transport: "tcp",
hostname: "0.0.0.0",
transport: 'tcp',
hostname: '0.0.0.0',
port: 80,
},
rid: 0,
closeWrite: async () => undefined,
readable: "",
writable: "",
readable: '',
writable: '',
read: async (p: Uint8Array) => null,
write: async (p: Uint8Array) => 0,
close: () => undefined,
@ -37,14 +36,19 @@ export class DenoPlatformAdapter<T = any, K = any>
const httpConnection = Deno.serveHttp(connection);
for await (const requestEvent of httpConnection) {
requestEvent.respondWith(this.handler(requestEvent, connection));
requestEvent.respondWith(
this.handler(requestEvent, connection),
);
}
}
}
async handleRequest(nativeRequest: Request, connection: Deno.Conn = DefaultConn): Promise<request<T>> {
async handleRequest(
nativeRequest: Request,
connection: Deno.Conn = DefaultConn,
): Promise<request<T>> {
const requestHeaders = new headers(
Object.fromEntries(nativeRequest.headers.entries())
Object.fromEntries(nativeRequest.headers.entries()),
);
const requestMessage: request<T> = new request(
<methodENUM>nativeRequest.method,
@ -52,16 +56,20 @@ export class DenoPlatformAdapter<T = any, K = any>
requestHeaders,
await nativeRequest.text(),
{},
`${connection.remoteAddr.hostname}:${connection.remoteAddr.port}` || ""
`${connection.remoteAddr.hostname}:${connection.remoteAddr.port}` ||
'',
);
return requestMessage;
}
async handler(event: FetchEvent, connection: Deno.Conn = DefaultConn): Promise<Response> {
async handler(
event: FetchEvent,
connection: Deno.Conn = DefaultConn,
): Promise<Response> {
return await this.handleResponse(
await this.handleRequest(event.request, connection).then((request) =>
this.router.respond(request)
)
)
await this.handleRequest(event.request, connection).then(
(request) => this.router.respond(request),
),
);
}
}

View File

@ -1,13 +1,18 @@
import { NodePlatformAdapter } from "./node";
import { SWPlatformAdapter } from "./serviceworker";
import { DenoPlatformAdapter } from "./deno";
import { TxikiPlatformAdapter } from "./txiki";
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,
'Node.js': NodePlatformAdapter,
'Service Worker': SWPlatformAdapter,
Deno: DenoPlatformAdapter,
'txiki.js': TxikiPlatformAdapter,
};
export { NodePlatformAdapter, SWPlatformAdapter, DenoPlatformAdapter, TxikiPlatformAdapter };
export {
NodePlatformAdapter,
SWPlatformAdapter,
DenoPlatformAdapter,
TxikiPlatformAdapter,
};

View File

@ -1,20 +1,23 @@
import { request, response } from "../interface/index";
import { router } from "../router";
import { request, response } from '../interface/index';
import { router } from '../router';
export interface platformAdapater<T = any, K = any> {
router: router<T, K>;
listen(port: number): void;
handleRequest(nativeRequest: any): Promise<request<T>>;
handleResponse(response: response<K> | Promise<response<K>>, nativeResponse?: any): any;
router: router<T, K>;
listen(port: number): void;
handleRequest(nativeRequest: any): Promise<request<T>>;
handleResponse(
response: response<K> | Promise<response<K>>,
nativeResponse?: any,
): any;
}
export interface platformAdapaterConstructor<T = any, K = any> {
new (router: router<T, K>): platformAdapater<T, K>;
new (router: router<T, K>): platformAdapater<T, K>;
}
export function createPlatformAdapater(
adapater: platformAdapaterConstructor,
router: router
adapater: platformAdapaterConstructor,
router: router,
): platformAdapater {
return new adapater(router);
return new adapater(router);
}

View File

@ -1,77 +1,81 @@
import { platformAdapater } from "./index";
import { request, response } from "../interface/index";
import { router } from "../router";
import { headers } from "../interface/headers";
import { platformAdapater } from './index';
import { request, response } from '../interface/index';
import { router } from '../router';
import { headers } from '../interface/headers';
import http from "http";
import { methodENUM } from "src/interface/method";
import http from 'http';
import { methodENUM } from 'src/interface/method';
export class NodePlatformAdapter<T = any, K = any> implements platformAdapater {
public router: router<T, K>;
public router: router<T, K>;
constructor(router: router<T, K>) {
this.router = router;
}
constructor(router: router<T, K>) {
this.router = router;
}
async listen(port: number): Promise<void> {
const server = http.createServer();
server.on(
"request",
async (req: http.IncomingMessage, res: http.ServerResponse) => {
const request = await this.handleRequest(req);
const response = await this.router.respond(request);
this.handleResponse(response, res);
}
);
server.listen(port);
return;
}
async listen(port: number): Promise<void> {
const server = http.createServer();
server.on(
'request',
async (req: http.IncomingMessage, res: http.ServerResponse) => {
const request = await this.handleRequest(req);
const response = await this.router.respond(request);
this.handleResponse(response, res);
},
);
server.listen(port);
return;
}
async handleRequest(
nativeRequest: http.IncomingMessage
): Promise<request<T>> {
if (
typeof nativeRequest.method != "string" ||
typeof nativeRequest.url != "string" ||
typeof nativeRequest.headers != "object"
) {
throw new Error("Invalid request");
}
async handleRequest(
nativeRequest: http.IncomingMessage,
): Promise<request<T>> {
if (
typeof nativeRequest.method != 'string' ||
typeof nativeRequest.url != 'string' ||
typeof nativeRequest.headers != 'object'
) {
throw new Error('Invalid request');
}
let body: string = "";
const ip: string = nativeRequest.socket.remoteAddress?.replace("::ffff:", "") ?? "0.0.0.0";
const requestHeaders = new headers(<any>nativeRequest.headers);
let body: string = '';
const ip: string =
nativeRequest.socket.remoteAddress?.replace('::ffff:', '') ??
'0.0.0.0';
const requestHeaders = new headers(<any>nativeRequest.headers);
if (!["GET", "HEAD", "DELETE", "OPTIONS"].includes(nativeRequest.method)) {
nativeRequest.on("data", (data: string) => {
body += data;
});
if (
!['GET', 'HEAD', 'DELETE', 'OPTIONS'].includes(nativeRequest.method)
) {
nativeRequest.on('data', (data: string) => {
body += data;
});
await new Promise((resolve) =>
nativeRequest.on("end", () => {
resolve(true);
})
);
}
await new Promise((resolve) =>
nativeRequest.on('end', () => {
resolve(true);
}),
);
}
return new request<T>(
<methodENUM>nativeRequest.method,
new URL(
nativeRequest.url,
`http://${requestHeaders.get("host") ?? "localhost"}`
),
requestHeaders,
body,
{},
ip
);
}
return new request<T>(
<methodENUM>nativeRequest.method,
new URL(
nativeRequest.url,
`http://${requestHeaders.get('host') ?? 'localhost'}`,
),
requestHeaders,
body,
{},
ip,
);
}
handleResponse(response: response<K>, nativeResponse: http.ServerResponse) {
nativeResponse.statusCode = response.status;
response.headers.forEach((key, value) => {
nativeResponse.setHeader(key, value);
});
nativeResponse.end(response.body);
}
handleResponse(response: response<K>, nativeResponse: http.ServerResponse) {
nativeResponse.statusCode = response.status;
response.headers.forEach((key, value) => {
nativeResponse.setHeader(key, value);
});
nativeResponse.end(response.body);
}
}

View File

@ -1,53 +1,55 @@
import { platformAdapater } from "./index";
import { request } from "../interface/request";
import { response } from "../interface/response";
import { router } from "../router";
import { headers } from "../interface/headers";
import { platformAdapater } from './index';
import { request } from '../interface/request';
import { response } from '../interface/response';
import { router } from '../router';
import { headers } from '../interface/headers';
import { methodENUM } from "src/interface/method";
import { methodENUM } from 'src/interface/method';
export class SWPlatformAdapter<T = any, K = any> implements platformAdapater {
public router: router<T, K>;
public router: router<T, K>;
constructor(router: router<T, K>) {
this.router = router;
}
constructor(router: router<T, K>) {
this.router = router;
}
async listen(_port?: number): Promise<void> {
self.addEventListener("fetch", (event: FetchEvent) => {
event.respondWith(this.handler(event));
});
}
async listen(_port?: number): Promise<void> {
self.addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(this.handler(event));
});
}
async handleRequest(nativeRequest: Request): Promise<request<T>> {
const requestHeaders = new headers(
Object.fromEntries(nativeRequest.headers.entries())
);
const requestMessage: request<T> = new request(
<methodENUM>nativeRequest.method,
new URL(nativeRequest.url),
requestHeaders,
await nativeRequest.text(),
{},
requestHeaders.get("CF-Connecting-IP") || ""
);
return requestMessage;
}
async handleRequest(nativeRequest: Request): Promise<request<T>> {
const requestHeaders = new headers(
Object.fromEntries(nativeRequest.headers.entries()),
);
const requestMessage: request<T> = new request(
<methodENUM>nativeRequest.method,
new URL(nativeRequest.url),
requestHeaders,
await nativeRequest.text(),
{},
requestHeaders.get('CF-Connecting-IP') || '',
);
return requestMessage;
}
async handleResponse(response: response<K>): Promise<Response> {
if (response.status === 204) { response.body = null; }
const nativResponse = new Response(response.body, {
status: response.status,
headers: response.headers.headers,
});
return nativResponse;
}
async handleResponse(response: response<K>): Promise<Response> {
if (response.status === 204) {
response.body = null;
}
const nativResponse = new Response(response.body, {
status: response.status,
headers: response.headers.headers,
});
return nativResponse;
}
async handler(event: FetchEvent): Promise<Response> {
return await this.handleResponse(
await this.handleRequest(event.request).then((request) =>
this.router.respond(request)
)
)
}
async handler(event: FetchEvent): Promise<Response> {
return await this.handleResponse(
await this.handleRequest(event.request).then((request) =>
this.router.respond(request),
),
);
}
}

View File

@ -1,9 +1,9 @@
import { request } from "../../interface/request";
import { response } from "../../interface";
import { headers } from "../../interface/headers";
import { methodENUM } from "../../interface/method";
import { request } from '../../interface/request';
import { response } from '../../interface';
import { headers } from '../../interface/headers';
import { methodENUM } from '../../interface/method';
import statusCode from "./statusCode.json";
import statusCode from './statusCode.json';
export class HttpConn {
private closed: boolean = false;
@ -16,44 +16,63 @@ export class HttpConn {
}
private readMessage(httpMessage: string): request<any> {
const lines = httpMessage.split("\n");
const lines = httpMessage.split('\n');
const firstLine = lines[0];
const dividingIndex = lines.indexOf("\r") ?? lines.indexOf("");
const dividingIndex = lines.indexOf('\r') ?? lines.indexOf('');
const rawHeaders = lines.slice(1, dividingIndex);
const [method, path, version] = firstLine.split(" ");
const [method, path, version] = firstLine.split(' ');
if (version in ['HTTP/1.1', 'HTTP/1.0', 'HTTP/0.9']) {
this.conn.close();
}
const requestHeaders = new headers({});
for (const header of rawHeaders) {
const [key, value] = header.split(": ");
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 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 body = lines.slice(dividingIndex + 1).join('\n');
const requestMessage = new request<any>(<methodENUM>method, url, requestHeaders, body, {}, this.conn.remoteAddress.ip);
const requestMessage = new request<any>(
<methodENUM>method,
url,
requestHeaders,
body,
{},
this.conn.remoteAddress.ip,
);
return requestMessage;
}
private handleResponse(response: response<any>) {
let responseMessage: string = "";
responseMessage += "HTTP/1.1 " + response.status + " " + statusCode[<"100">response.status.toString()] ?? "";
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' + key + ': ' + value;
});
responseMessage += "\n\n" + response.body;
responseMessage += '\n\n' + response.body;
this.conn.write(new TextEncoder().encode(responseMessage));
this.conn.shutdown();
this.conn.close();
this.closed = true;
}
private async read(): Promise<request<any> | undefined> {
let message = "";
let message = '';
const { done, value } = await this.reader.read();
if (done || this.closed) {
@ -61,9 +80,15 @@ export class HttpConn {
return undefined;
}
message += String.fromCharCode(...Object.values(<{
[key: string]: number
}>value));
message += String.fromCharCode(
...Object.values(
<
{
[key: string]: number;
}
>value,
),
);
const requestMessage = this.readMessage(message);
return requestMessage;
@ -82,13 +107,13 @@ export class HttpConn {
respondWith: (response: response<any>) => {
httpConn.handleResponse(response);
},
}
}
}
}
},
};
},
};
}
}
export default function serveHttp(Connection: tjs.Connection) {
return new HttpConn(Connection);
}
}

View File

@ -61,4 +61,4 @@
"509": "Bandwidth Limit Exceeded",
"510": "Not Extended",
"511": "Network Authentication Required"
}
}

View File

@ -1,10 +1,12 @@
import { platformAdapater } from "./index";
import { request } from "../interface/request";
import { response } from "../interface/response";
import { router } from "../router";
import serveHttp from "./txiki-js/serveHttp";
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<T = any, K = any> implements platformAdapater {
export class TxikiPlatformAdapter<T = any, K = any>
implements platformAdapater
{
public router: router<T, K>;
constructor(router: router<T, K>) {
@ -12,13 +14,16 @@ export class TxikiPlatformAdapter<T = any, K = any> implements platformAdapater
}
async listen(port?: number): Promise<void> {
const Server = await tjs.listen("tcp", "0.0.0.0", port);
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") {
if (
typeof conn == 'undefined' ||
typeof conn.request == 'undefined'
) {
return;
}
conn.respondWith(await this.router.respond(conn.request));
@ -33,4 +38,4 @@ export class TxikiPlatformAdapter<T = any, K = any> implements platformAdapater
async handleResponse(response: response<K>): Promise<response<K>> {
return response;
}
}
}

View File

@ -1,71 +1,70 @@
import { path } from "./interface";
import handler from "./handler";
import { pathToRegexp } from "path-to-regexp";
import { path } from './interface';
import handler from './handler';
import { pathToRegexp } from 'path-to-regexp';
interface matchedStatus {
matched: boolean;
attributes: {
name: string;
value: string | undefined;
}[];
matched: boolean;
attributes: {
name: string;
value: string | undefined;
}[];
}
interface regExpKey {
name: string;
prefix: string;
suffix: string;
pattern: string;
modifier: string;
};
name: string;
prefix: string;
suffix: string;
pattern: string;
modifier: string;
}
export class route {
private paths: path[];
public handlers: handler<any, any>[];
private regExps: { regExp: RegExp, keys: regExpKey[] }[] = [];
private paths: path[];
public handlers: handler<any, any>[];
private regExps: { regExp: RegExp; keys: regExpKey[] }[] = [];
constructor(paths: path[], handlers: handler<any, any>[]) {
this.paths = paths;
this.handlers = handlers;
constructor(paths: path[], handlers: handler<any, any>[]) {
this.paths = paths;
this.handlers = handlers;
this.paths.forEach(path => {
const keys: regExpKey[] = [];
this.regExps.push({ regExp: pathToRegexp(path, keys), keys });
})
}
async exec(path: string): Promise<matchedStatus> {
let Answer = await Promise.all<Promise<matchedStatus>>(
this.regExps.map(async (it) => {
this.paths.forEach((path) => {
const keys: regExpKey[] = [];
this.regExps.push({ regExp: pathToRegexp(path, keys), keys });
});
}
async exec(path: string): Promise<matchedStatus> {
let Answer = await Promise.all<Promise<matchedStatus>>(
this.regExps.map(async (it) => {
const answer = it.regExp.exec(path);
if (answer === null)
return {
matched: false,
attributes: [],
};
const answer = it.regExp.exec(path);
if (answer === null)
return {
matched: false,
attributes: [],
};
let attributes: matchedStatus['attributes'] = [];
let attributes: matchedStatus["attributes"] = [];
it.keys.forEach((key, index) => {
attributes.push({
name: key.name,
value: answer[index + 1],
});
});
it.keys.forEach((key, index) => {
attributes.push({
name: key.name,
value: answer[index + 1],
});
});
return {
matched: true,
attributes: attributes,
};
})
);
Answer = Answer.filter((it) => it.matched);
if (Answer.length === 0)
return {
matched: false,
attributes: [],
};
else return Answer[0];
}
return {
matched: true,
attributes: attributes,
};
}),
);
Answer = Answer.filter((it) => it.matched);
if (Answer.length === 0)
return {
matched: false,
attributes: [],
};
else return Answer[0];
}
}
export default route;

View File

@ -1,225 +1,238 @@
import handler from "./handler";
import handler from './handler';
import {
path,
response,
request,
AllMismatchInterrupted,
responder,
method,
} from "./interface/index";
import { defaultHeaders } from "./interface/response";
import route from "./route";
import { methodENUM } from "./interface/method";
path,
response,
request,
AllMismatchInterrupted,
responder,
method,
} from './interface/index';
import { defaultHeaders } from './interface/response';
import route from './route';
import { methodENUM } from './interface/method';
import {
createPlatformAdapater,
platformAdapaterConstructor,
platformAdapater,
} from "./platform/index";
import { platformAdapaterMapping } from "./platform/export";
import { platform } from "./lib";
createPlatformAdapater,
platformAdapaterConstructor,
platformAdapater,
} from './platform/index';
import { platformAdapaterMapping } from './platform/export';
import { platform } from './lib';
export class router<K = any, V = any> {
public routes: route[];
public routes: route[];
public errorResponder: (
errorCode: number,
errorMessage?: string
) => responder<K, V>;
public errorResponder: (
errorCode: number,
errorMessage?: string,
) => responder<K, V>;
constructor(routes: route[] = []) {
this.routes = routes;
}
constructor(routes: route[] = []) {
this.routes = routes;
}
add(route: route) {
this.routes.push(route);
return this;
}
add(route: route) {
this.routes.push(route);
return this;
}
binding(path: path, handler: handler<K, V>) {
this.add(new route([path], [handler]));
return this;
}
binding(path: path, handler: handler<K, V>) {
this.add(new route([path], [handler]));
return this;
}
create(
method: method,
responder: (
request: request<K>
) =>
| Promise<response<V>>
| Promise<string>
| Promise<object>
| Promise<number>
| Promise<void>
) {
return new handler<K, V>(method, [
async (request: request<K>) => {
const answer = await responder(request);
if (answer instanceof response) {
return answer;
} else if (typeof answer == "string") {
return new response(answer);
} else if (typeof answer == "number") {
return new response(answer.toString());
} else if (typeof answer == "object") {
return new response(
JSON.stringify(answer),
200,
new defaultHeaders({
"Content-Type": "application/json; charset=utf-8",
})
);
} else {
return new response("", 204);
}
},
]);
}
create(
method: method,
responder: (
request: request<K>,
) =>
| Promise<response<V>>
| Promise<string>
| Promise<object>
| Promise<number>
| Promise<void>,
) {
return new handler<K, V>(method, [
async (request: request<K>) => {
const answer = await responder(request);
if (answer instanceof response) {
return answer;
} else if (typeof answer == 'string') {
return new response(answer);
} else if (typeof answer == 'number') {
return new response(answer.toString());
} else if (typeof answer == 'object') {
return new response(
JSON.stringify(answer),
200,
new defaultHeaders({
'Content-Type': 'application/json; charset=utf-8',
}),
);
} else {
return new response('', 204);
}
},
]);
}
use(routers: router[], path: path): void {
routers.forEach((router) => {
this.binding(path, router.toHandler());
});
}
use(routers: router[], path: path): void {
routers.forEach((router) => {
this.binding(path, router.toHandler());
});
}
route(path: path): router {
const Router = new router([]);
this.use([Router], path);
return Router;
}
route(path: path): router {
const Router = new router([]);
this.use([Router], path);
return Router;
}
async _respond(
request: request<K>,
responseMessage: response<V> = new response<V>("")
): Promise<response<V>> {
request.originURL = request.url;
request.url.pathname = request.params["0"]
? "/" + request.params["0"]
: request.originURL.pathname;
async _respond(
request: request<K>,
responseMessage: response<V> = new response<V>(''),
): Promise<response<V>> {
request.originURL = request.url;
request.url.pathname = request.params['0']
? '/' + request.params['0']
: request.originURL.pathname;
let mismatchCount = 0;
let mismatchCount = 0;
for (let route of this.routes) {
const isMatched = await route.exec(request.url.pathname);
for (let route of this.routes) {
const isMatched = await route.exec(request.url.pathname);
if (!isMatched.matched) {
mismatchCount++;
continue;
}
if (!isMatched.matched) {
mismatchCount++;
continue;
}
isMatched.attributes.forEach((e) => {
request.params[e.name] = e.value;
});
isMatched.attributes.forEach((e) => {
request.params[e.name] = e.value;
});
try {
let thisResponse: response<V> | void = responseMessage;
for (let handler of route.handlers) {
if (
handler.method != request.method &&
handler.method != methodENUM.ANY
) {
continue;
}
thisResponse = await handler.respond(
request,
thisResponse ?? responseMessage
);
}
if (thisResponse instanceof response) {
responseMessage = thisResponse;
} else {
// means that the handler is a middleware that doesn't change the response
throw AllMismatchInterrupted;
}
} catch (e) {
if (e instanceof response) {
throw e;
}
if (e === AllMismatchInterrupted) mismatchCount++;
else {
if (typeof this.errorResponder == "function") {
responseMessage =
(await this.errorResponder(500, e.toString() + "\n")(request)) ??
new response(e.toString(), 500);
console.log(e);
} else {
throw e;
}
}
}
}
try {
let thisResponse: response<V> | void = responseMessage;
for (let handler of route.handlers) {
if (
handler.method != request.method &&
handler.method != methodENUM.ANY
) {
continue;
}
thisResponse = await handler.respond(
request,
thisResponse ?? responseMessage,
);
}
if (thisResponse instanceof response) {
responseMessage = thisResponse;
} else {
// means that the handler is a middleware that doesn't change the response
throw AllMismatchInterrupted;
}
} catch (e) {
if (e instanceof response) {
throw e;
}
if (e === AllMismatchInterrupted) mismatchCount++;
else {
if (typeof this.errorResponder == 'function') {
responseMessage =
(await this.errorResponder(
500,
e.toString() + '\n',
)(request)) ?? new response(e.toString(), 500);
console.log(e);
} else {
throw e;
}
}
}
}
if (mismatchCount == this.routes.length) {
throw AllMismatchInterrupted;
}
if (mismatchCount == this.routes.length) {
throw AllMismatchInterrupted;
}
return responseMessage;
}
return responseMessage;
}
public respond = this._respond;
public respond = this._respond;
toHandler(): handler<K, V> {
return new handler(methodENUM.ANY, [
(request: request<K>, responseMessage?: response<V>) => {
return this.respond(request, responseMessage ?? new response(""));
},
]);
}
toHandler(): handler<K, V> {
return new handler(methodENUM.ANY, [
(request: request<K>, responseMessage?: response<V>) => {
return this.respond(
request,
responseMessage ?? new response(''),
);
},
]);
}
useErrorResponder(
errorResponder: (
errorCode: number,
errorMessage?: string
) => responder<K, V>
): this {
this.errorResponder = errorResponder;
return this;
}
useErrorResponder(
errorResponder: (
errorCode: number,
errorMessage?: string,
) => responder<K, V>,
): this {
this.errorResponder = errorResponder;
return this;
}
}
export default router;
export class rootRouter<K = any, V = any> extends router<K, V> {
public adapater: platformAdapater<K, V>;
errorResponder =
(errorCode: number, errorMessage?: string) =>
async (_request: request<K>): Promise<response<V>> =>
new response(errorMessage ?? "", errorCode);
public adapater: platformAdapater<K, V>;
errorResponder =
(errorCode: number, errorMessage?: string) =>
async (_request: request<K>): Promise<response<V>> =>
new response(errorMessage ?? '', errorCode);
respond = async (request: request<K>): Promise<response<V>> => {
let responseMessage: response<V> = new response("");
try {
responseMessage = await this._respond(request, responseMessage);
} catch (e) {
if (e instanceof response) {
return e;
} else if (e === AllMismatchInterrupted) {
responseMessage =
(await this.errorResponder(404, "404 Not Found\n")(request)) ??
new response("404 Not Found\n", 404);
} else {
responseMessage =
(await this.errorResponder(500, e.toString() + "\n")(request)) ??
new response(e.toString(), 500);
console.log(e);
}
}
respond = async (request: request<K>): Promise<response<V>> => {
let responseMessage: response<V> = new response('');
try {
responseMessage = await this._respond(request, responseMessage);
} catch (e) {
if (e instanceof response) {
return e;
} else if (e === AllMismatchInterrupted) {
responseMessage =
(await this.errorResponder(
404,
'404 Not Found\n',
)(request)) ?? new response('404 Not Found\n', 404);
} else {
responseMessage =
(await this.errorResponder(
500,
e.toString() + '\n',
)(request)) ?? new response(e.toString(), 500);
console.log(e);
}
}
return responseMessage;
};
useAdapater(adapater: platformAdapaterConstructor): this {
this.adapater = createPlatformAdapater(adapater, this);
return this;
}
useMappingAdapter(
mapping: { [platform: string]: platformAdapaterConstructor } = platformAdapaterMapping
): this {
if (typeof platform == "undefined") throw new Error("Cannot detect platform");
if (mapping[platform] == undefined) throw new Error("Platform not found in mapping");
else this.useAdapater(mapping[platform]);
return this;
}
listen(port: number): void {
if (this.adapater == null) throw new Error("No platform adapter set");
this.adapater.listen(port);
}
return responseMessage;
};
useAdapater(adapater: platformAdapaterConstructor): this {
this.adapater = createPlatformAdapater(adapater, this);
return this;
}
useMappingAdapter(
mapping: {
[platform: string]: platformAdapaterConstructor;
} = platformAdapaterMapping,
): this {
if (typeof platform == 'undefined')
throw new Error('Cannot detect platform');
if (mapping[platform] == undefined)
throw new Error('Platform not found in mapping');
else this.useAdapater(mapping[platform]);
return this;
}
listen(port: number): void {
if (this.adapater == null) throw new Error('No platform adapter set');
this.adapater.listen(port);
}
}

View File

@ -1,49 +1,49 @@
import { spawn } from "child_process";
import { spawn } from 'child_process';
import Axios from "axios";
import Axios from 'axios';
const Instance = Axios.create({
baseURL: "http://localhost:3000"
})
baseURL: 'http://localhost:3000',
});
spawn(`deno`, [
"run", "--allow-net", `${__dirname}/../dist/test.deno.js`
]);
spawn(`deno`, ['run', '--allow-net', `${__dirname}/../dist/test.deno.js`]);
const randomString = () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const randomString = () =>
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
describe("Test server", () => {
test("normal 200 response", async () => {
describe('Test server', () => {
test('normal 200 response', async () => {
expect.assertions(2);
await new Promise((r) => setTimeout(r, 200));
const { data, status } = await Instance.get("/");
const { data, status } = await Instance.get('/');
expect(status).toEqual(200);
expect(data).toEqual("200 OK");
})
expect(data).toEqual('200 OK');
});
test("post response", async () => {
test('post response', async () => {
expect.assertions(2);
const string = randomString();
const { data, status } = await Instance.post("/post", string);
const { data, status } = await Instance.post('/post', string);
expect(status).toEqual(200);
expect(data).toEqual(string);
})
});
test("change header and status code", async () => {
test('change header and status code', async () => {
expect.assertions(3);
const { data, status, headers } = await Instance.get("/header");
const { data, status, headers } = await Instance.get('/header');
expect(status).toEqual(204);
expect(headers["itis"]).toEqual("work");
expect(data).toEqual("");
})
expect(headers['itis']).toEqual('work');
expect(data).toEqual('');
});
test("get param", async () => {
test('get param', async () => {
expect.assertions(2);
const string = randomString();
@ -51,14 +51,14 @@ describe("Test server", () => {
expect(status).toEqual(200);
expect(data).toEqual(string);
})
});
test("chain interrupted", async () => {
test('chain interrupted', async () => {
expect.assertions(2);
const { data, status } = await Instance.get(`/info/foo`);
expect(status).toEqual(200);
expect(data).toEqual("hit");
})
})
expect(data).toEqual('hit');
});
});

View File

@ -1,45 +1,47 @@
import _ from "./test-server";
import _ from './test-server';
import Axios from "axios";
import Axios from 'axios';
_.listen(3000);
const Instance = Axios.create({
baseURL: "http://localhost:3000"
})
baseURL: 'http://localhost:3000',
});
const randomString = () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
const randomString = () =>
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
describe("Test server", () => {
test("normal 200 response", async () => {
describe('Test server', () => {
test('normal 200 response', async () => {
expect.assertions(2);
const { data, status } = await Instance.get("/");
const { data, status } = await Instance.get('/');
expect(status).toEqual(200);
expect(data).toEqual("200 OK");
})
expect(data).toEqual('200 OK');
});
test("post response", async () => {
test('post response', async () => {
expect.assertions(2);
const string = randomString();
const { data, status } = await Instance.post("/post", string);
const { data, status } = await Instance.post('/post', string);
expect(status).toEqual(200);
expect(data).toEqual(string);
})
});
test("change header and status code", async () => {
test('change header and status code', async () => {
expect.assertions(3);
const { data, status, headers } = await Instance.get("/header");
const { data, status, headers } = await Instance.get('/header');
expect(status).toEqual(204);
expect(headers["itis"]).toEqual("work");
expect(data).toEqual("");
})
expect(headers['itis']).toEqual('work');
expect(data).toEqual('');
});
test("get param", async () => {
test('get param', async () => {
expect.assertions(2);
const string = randomString();
@ -47,14 +49,14 @@ describe("Test server", () => {
expect(status).toEqual(200);
expect(data).toEqual(string);
})
});
test("chain interrupted", async () => {
test('chain interrupted', async () => {
expect.assertions(2);
const { data, status } = await Instance.get(`/info/foo`);
expect(status).toEqual(200);
expect(data).toEqual("hit");
})
})
expect(data).toEqual('hit');
});
});

View File

@ -1,3 +1,3 @@
import App from "./test-server";
import App from './test-server';
App.listen(3000);
App.listen(3000);

View File

@ -2,24 +2,49 @@ import * as handlersJS from '../index';
const App = new handlersJS.rootRouter();
App.binding("/", App.create("GET", async () => "200 OK"));
App.binding(
'/',
App.create('GET', async () => '200 OK'),
);
App.binding("/post", App.create("POST", async (request: handlersJS.request<any>) => request.body));
App.binding(
'/post',
App.create(
'POST',
async (request: handlersJS.request<any>) => request.body,
),
);
App.binding("/header", App.create("GET", async () => {
const response = new handlersJS.response<any>("");
response.status = 204;
response.headers.set("itis", "work");
return response;
}));
App.binding(
'/header',
App.create('GET', async () => {
const response = new handlersJS.response<any>('');
response.status = 204;
response.headers.set('itis', 'work');
return response;
}),
);
App
.route("/info/(.*)")
.binding("/foo", App.create("GET", (): Promise<handlersJS.response<any>> => new Promise(resolve => {
throw new handlersJS.response("hit")
})))
.binding("/(.*)", App.create("GET", async (request: handlersJS.request<any>) => request.params[0] ?? "not found"));
App.route('/info/(.*)')
.binding(
'/foo',
App.create(
'GET',
(): Promise<handlersJS.response<any>> =>
new Promise((resolve) => {
throw new handlersJS.response('hit');
}),
),
)
.binding(
'/(.*)',
App.create(
'GET',
async (request: handlersJS.request<any>) =>
request.params[0] ?? 'not found',
),
);
App.useMappingAdapter();
export default App;
export default App;

View File

@ -1,50 +1,41 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": [
"ESNext"
],
"types": [
"@cloudflare/workers-types",
"@types/node",
"@types/jest",
"bun-types"
],
"skipLibCheck": true,
"sourceMap": true,
"outDir": "./dist",
"moduleResolution": "node",
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"baseUrl": ".",
"plugins": [
{
"transform": "@zerollup/ts-transform-paths"
}
],
"declaration": true
},
"exclude": [
"node_modules"
],
"include": [
"index.ts",
"src/**/*.ts",
"demo/**/*.ts",
"types/*.d.ts"
],
// "esm": true
}
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": ["ESNext"],
"types": [
"@cloudflare/workers-types",
"@types/node",
"@types/jest",
"bun-types"
],
"skipLibCheck": true,
"sourceMap": true,
"outDir": "./dist",
"moduleResolution": "node",
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"baseUrl": ".",
"plugins": [
{
"transform": "@zerollup/ts-transform-paths"
}
],
"declaration": true
},
"exclude": ["node_modules"],
"include": ["index.ts", "src/**/*.ts", "demo/**/*.ts", "types/*.d.ts"]
// "esm": true
}

184
types/deno.d.ts vendored
View File

@ -1,109 +1,109 @@
declare namespace Deno {
export const version: {
/** Deno's version. For example: `"1.0.0"` */
deno: string;
/** The V8 version used by Deno. For example: `"8.0.0.0"` */
v8: string;
/** The TypeScript version used by Deno. For example: `"4.0.0"` */
typescript: string;
};
export const version: {
/** Deno's version. For example: `"1.0.0"` */
deno: string;
/** The V8 version used by Deno. For example: `"8.0.0.0"` */
v8: string;
/** The TypeScript version used by Deno. For example: `"4.0.0"` */
typescript: string;
};
export interface NetAddr {
transport: "tcp" | "udp";
hostname: string;
port: number;
}
export interface NetAddr {
transport: 'tcp' | 'udp';
hostname: string;
port: number;
}
export type Addr = NetAddr;
export type Addr = NetAddr;
export interface Closer {
close(): void;
}
export interface Closer {
close(): void;
}
export interface Reader {
/** Reads up to `p.byteLength` bytes into `p`. It resolves to the number of
* bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error
* encountered. Even if `read()` resolves to `n` < `p.byteLength`, it may
* use all of `p` as scratch space during the call. If some data is
* available but not `p.byteLength` bytes, `read()` conventionally resolves
* to what is available instead of waiting for more.
*
* When `read()` encounters end-of-file condition, it resolves to EOF
* (`null`).
*
* When `read()` encounters an error, it rejects with an error.
*
* Callers should always process the `n` > `0` bytes returned before
* considering the EOF (`null`). Doing so correctly handles I/O errors that
* happen after reading some bytes and also both of the allowed EOF
* behaviors.
*
* Implementations should not retain a reference to `p`.
*
* Use `itereateReader` from from https://deno.land/std/streams/conversion.ts to
* turn a Reader into an AsyncIterator.
*/
read(p: Uint8Array): Promise<number | null>;
}
export interface Reader {
/** Reads up to `p.byteLength` bytes into `p`. It resolves to the number of
* bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error
* encountered. Even if `read()` resolves to `n` < `p.byteLength`, it may
* use all of `p` as scratch space during the call. If some data is
* available but not `p.byteLength` bytes, `read()` conventionally resolves
* to what is available instead of waiting for more.
*
* When `read()` encounters end-of-file condition, it resolves to EOF
* (`null`).
*
* When `read()` encounters an error, it rejects with an error.
*
* Callers should always process the `n` > `0` bytes returned before
* considering the EOF (`null`). Doing so correctly handles I/O errors that
* happen after reading some bytes and also both of the allowed EOF
* behaviors.
*
* Implementations should not retain a reference to `p`.
*
* Use `itereateReader` from from https://deno.land/std/streams/conversion.ts to
* turn a Reader into an AsyncIterator.
*/
read(p: Uint8Array): Promise<number | null>;
}
export interface Writer {
/** Writes `p.byteLength` bytes from `p` to the underlying data stream. It
* resolves to the number of bytes written from `p` (`0` <= `n` <=
* `p.byteLength`) or reject with the error encountered that caused the
* write to stop early. `write()` must reject with a non-null error if
* would resolve to `n` < `p.byteLength`. `write()` must not modify the
* slice data, even temporarily.
*
* Implementations should not retain a reference to `p`.
*/
write(p: Uint8Array): Promise<number>;
}
export interface Writer {
/** Writes `p.byteLength` bytes from `p` to the underlying data stream. It
* resolves to the number of bytes written from `p` (`0` <= `n` <=
* `p.byteLength`) or reject with the error encountered that caused the
* write to stop early. `write()` must reject with a non-null error if
* would resolve to `n` < `p.byteLength`. `write()` must not modify the
* slice data, even temporarily.
*
* Implementations should not retain a reference to `p`.
*/
write(p: Uint8Array): Promise<number>;
}
export interface Conn extends Reader, Writer, Closer {
/** The local address of the connection. */
readonly localAddr: Addr;
/** The remote address of the connection. */
readonly remoteAddr: Addr;
/** The resource ID of the connection. */
readonly rid: number;
/** Shuts down (`shutdown(2)`) the write side of the connection. Most
* callers should just use `close()`. */
closeWrite(): Promise<void>;
export interface Conn extends Reader, Writer, Closer {
/** The local address of the connection. */
readonly localAddr: Addr;
/** The remote address of the connection. */
readonly remoteAddr: Addr;
/** The resource ID of the connection. */
readonly rid: number;
/** Shuts down (`shutdown(2)`) the write side of the connection. Most
* callers should just use `close()`. */
closeWrite(): Promise<void>;
readonly readable: ReadableStream<Uint8Array>;
readonly writable: WritableStream<Uint8Array>;
}
readonly readable: ReadableStream<Uint8Array>;
readonly writable: WritableStream<Uint8Array>;
}
/** A generic network listener for stream-oriented protocols. */
export interface Listener extends AsyncIterable<Conn> {
/** Waits for and resolves to the next connection to the `Listener`. */
accept(): Promise<Conn>;
/** Close closes the listener. Any pending accept promises will be rejected
* with errors. */
close(): void;
/** Return the address of the `Listener`. */
readonly addr: Addr;
/** A generic network listener for stream-oriented protocols. */
export interface Listener extends AsyncIterable<Conn> {
/** Waits for and resolves to the next connection to the `Listener`. */
accept(): Promise<Conn>;
/** Close closes the listener. Any pending accept promises will be rejected
* with errors. */
close(): void;
/** Return the address of the `Listener`. */
readonly addr: Addr;
/** Return the rid of the `Listener`. */
readonly rid: number;
/** Return the rid of the `Listener`. */
readonly rid: number;
[Symbol.asyncIterator](): AsyncIterableIterator<Conn>;
}
[Symbol.asyncIterator](): AsyncIterableIterator<Conn>;
}
export interface ListenOptions {
port: number;
}
export interface ListenOptions {
port: number;
}
export function listen(
options: ListenOptions & { transport?: "tcp" }
): Listener;
export function listen(
options: ListenOptions & { transport?: 'tcp' },
): Listener;
export function serveHttp(conn: Conn): HttpConn;
export function serveHttp(conn: Conn): HttpConn;
export interface HttpConn extends AsyncIterable<FetchEvent> {
readonly rid: number;
export interface HttpConn extends AsyncIterable<FetchEvent> {
readonly rid: number;
nextRequest(): Promise<FetchEvent | null>;
close(): void;
}
nextRequest(): Promise<FetchEvent | null>;
close(): void;
}
}

32
types/txiki.d.ts vendored
View File

@ -1,7 +1,7 @@
type Transport = "tcp" | "udp" | "pipe";
type Transport = 'tcp' | 'udp' | 'pipe';
interface ListenOptions {
backlog?: number
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.
@ -9,7 +9,13 @@ interface ListenOptions {
}
declare namespace tjs {
const versions: { curl: string; quickjs: string; tjs: string; uv: string; wasm3: string };
const versions: {
curl: string;
quickjs: string;
tjs: string;
uv: string;
wasm3: string;
};
export interface Address {
readonly family: number;
@ -24,15 +30,15 @@ declare namespace tjs {
readonly readable: ReadableStream<Uint8Array>;
readonly remoteAddress: Address;
readonly writeable: WritableStream<Uint8Array>
readonly writeable: WritableStream<Uint8Array>;
close(): void
read(buf: Uint8Array): Promise<number>
write(buf: Uint8Array): Promise<number>
close(): void;
read(buf: Uint8Array): Promise<number>;
write(buf: Uint8Array): Promise<number>;
setKeepAlive(enable?: boolean): void
setNoDelay(enable?: boolean): void
shutdown(): void
setKeepAlive(enable?: boolean): void;
setNoDelay(enable?: boolean): void;
shutdown(): void;
}
export interface Listener extends AsyncIterable<Connection> {
@ -48,6 +54,6 @@ declare namespace tjs {
transport: Transport,
host: string,
port?: string | number,
options?: ListenOptions
): Promise<Listener>
}
options?: ListenOptions,
): Promise<Listener>;
}