@smuzi/http-server 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/build/config.js +1 -1
- package/build/drivers/http1Server.js +5 -2
- package/build/router.d.ts +1 -0
- package/build/router.js +47 -15
- package/package.json +16 -11
- package/src/config.ts +60 -0
- package/src/drivers/http1Server.ts +263 -0
- package/src/drivers/http2Server.ts +125 -0
- package/src/index.ts +5 -0
- package/src/router.ts +257 -0
- package/src/types.ts +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 smuzi-ts
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/build/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HttpProtocol, None, HttpResponse, dump } from "@smuzi/std";
|
|
2
2
|
function http1ErrorHandler(context, error) {
|
|
3
3
|
//TODO: write error to log and remove dump()
|
|
4
|
-
dump(error);
|
|
4
|
+
dump({ msg: "http1ErrorHandler", error });
|
|
5
5
|
return HttpResponse.asJson({ error: "Internal Server Error" }, 500);
|
|
6
6
|
}
|
|
7
7
|
export function buildHttp1ServerConfig({ host = 'localhost', port = 8080, router, cert = None(), errorHandler = http1ErrorHandler }) {
|
|
@@ -3,7 +3,8 @@ import https from 'node:https';
|
|
|
3
3
|
import { TLSSocket } from 'node:tls';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { methodFromString } from "#lib/router.js";
|
|
6
|
-
import { isArray, isObject, isString, matchUnknown, OptionFromNullable, Err, Ok, isNull, transformError, StdError, HttpResponse, HttpRequest,
|
|
6
|
+
import { isArray, isObject, isString, StdRecord, matchUnknown, OptionFromNullable, Err, Ok, isNull, transformError, StdError, HttpResponse, HttpRequest, isOption, isResult, RequestHttpHeaders, asList, asRecord, asMap, querystring, StdJson } from '@smuzi/std';
|
|
7
|
+
import { log } from 'node:console';
|
|
7
8
|
export class StdHttp1Server {
|
|
8
9
|
#server;
|
|
9
10
|
constructor(server) {
|
|
@@ -94,7 +95,7 @@ export async function http1ServerRun(config) {
|
|
|
94
95
|
request: new HttpRequest({
|
|
95
96
|
method: request.method,
|
|
96
97
|
path: request.path,
|
|
97
|
-
query: () => new
|
|
98
|
+
query: () => new StdRecord(Object.fromEntries(urlObj.searchParams.entries())),
|
|
98
99
|
headers: new RequestHttpHeaders(nativeRequest.headers),
|
|
99
100
|
buffer: readRequestBodyAsBuffer(nativeRequest),
|
|
100
101
|
body: readRawBody(nativeRequest),
|
|
@@ -171,6 +172,7 @@ export async function http1ServerRun(config) {
|
|
|
171
172
|
}
|
|
172
173
|
});
|
|
173
174
|
server.once('error', (nativeError) => {
|
|
175
|
+
log("Server started on", nativeError);
|
|
174
176
|
resolve(Err({
|
|
175
177
|
errno: OptionFromNullable(nativeError.errno),
|
|
176
178
|
code: OptionFromNullable(nativeError.code),
|
|
@@ -181,6 +183,7 @@ export async function http1ServerRun(config) {
|
|
|
181
183
|
}));
|
|
182
184
|
});
|
|
183
185
|
server.listen(config.port, () => {
|
|
186
|
+
log("Server started on " + config.protocol + "://" + config.host + ":" + config.port);
|
|
184
187
|
resolve(Ok(new StdHttp1Server(server)));
|
|
185
188
|
});
|
|
186
189
|
});
|
package/build/router.d.ts
CHANGED
|
@@ -48,4 +48,5 @@ export declare function contactPaths(path1: PathParam, path2: PathParam): PathPa
|
|
|
48
48
|
export declare function toStartWithPattern(input: PathParam): RegExp;
|
|
49
49
|
export declare function methodFromString(method: string): Option<HttpMethod>;
|
|
50
50
|
export declare function CreateHttpRouter<Resp extends THttpResponse, GR extends Router<Resp>>(groupRoute: GroupRoute, notFound: Action<Resp>): Router<Resp>;
|
|
51
|
+
export declare function CreateHttp1Router(groupRoute: GroupRoute, notFound?: Action<ServerResponse>): Http1Router;
|
|
51
52
|
export {};
|
package/build/router.js
CHANGED
|
@@ -11,7 +11,13 @@ export function contactPaths(path1, path2) {
|
|
|
11
11
|
const path1AsRegExp = asRegExp(path1);
|
|
12
12
|
const path2AsRegExp = asRegExp(path2);
|
|
13
13
|
if (path1AsRegExp || path2AsRegExp) {
|
|
14
|
-
|
|
14
|
+
const source1 = path1AsRegExp
|
|
15
|
+
? path1.source
|
|
16
|
+
: path1;
|
|
17
|
+
const source2 = path2AsRegExp
|
|
18
|
+
? path2.source
|
|
19
|
+
: path2;
|
|
20
|
+
return new RegExp("^" + (source1 + source2).replaceAll('^', ''));
|
|
15
21
|
}
|
|
16
22
|
return path1 + path2;
|
|
17
23
|
}
|
|
@@ -47,8 +53,18 @@ function http2NotFoundHandler(context) {
|
|
|
47
53
|
}
|
|
48
54
|
export function CreateHttpRouter(groupRoute, notFound) {
|
|
49
55
|
const routes = new Map();
|
|
56
|
+
// Every own route/subgroup is registered ONCE at startup. We additionally
|
|
57
|
+
// remember each one's path *relative to this router* so that, if this
|
|
58
|
+
// router later gets attached to a parent via `.group()`, we can recompute
|
|
59
|
+
// every already-registered absolute path exactly once at that moment —
|
|
60
|
+
// never again on every match()/request.
|
|
61
|
+
const ownRoutes = [];
|
|
62
|
+
const ownGroups = [];
|
|
63
|
+
const computeAbsolute = (localPath) => processPath(contactPaths(groupRoute.path, localPath));
|
|
50
64
|
const add = (route, action) => {
|
|
51
|
-
|
|
65
|
+
const localPath = route.path;
|
|
66
|
+
route.path = computeAbsolute(localPath);
|
|
67
|
+
ownRoutes.push({ route, localPath });
|
|
52
68
|
routes.set(route, (routeData) => {
|
|
53
69
|
return {
|
|
54
70
|
action,
|
|
@@ -56,11 +72,26 @@ export function CreateHttpRouter(groupRoute, notFound) {
|
|
|
56
72
|
};
|
|
57
73
|
});
|
|
58
74
|
};
|
|
59
|
-
const addGroup = (
|
|
60
|
-
route
|
|
75
|
+
const addGroup = (localGroupPath, childRouter, action) => {
|
|
76
|
+
const route = { path: computeAbsolute(toStartWithPattern(localGroupPath)) };
|
|
77
|
+
ownGroups.push({ route, localGroupPath, childRouter });
|
|
61
78
|
routes.set(route, action);
|
|
62
79
|
};
|
|
63
|
-
|
|
80
|
+
// Recomputes this router's own base plus every route/subgroup registered
|
|
81
|
+
// on it so far, then recurses into any already-nested group routers so
|
|
82
|
+
// the whole subtree stays consistent — called once, when this router is
|
|
83
|
+
// attached to a parent via `.group()`.
|
|
84
|
+
const rebase = (newBase) => {
|
|
85
|
+
groupRoute.path = newBase;
|
|
86
|
+
for (const entry of ownRoutes) {
|
|
87
|
+
entry.route.path = computeAbsolute(entry.localPath);
|
|
88
|
+
}
|
|
89
|
+
for (const entry of ownGroups) {
|
|
90
|
+
entry.route.path = computeAbsolute(toStartWithPattern(entry.localGroupPath));
|
|
91
|
+
entry.childRouter.__rebase(contactPaths(newBase, entry.localGroupPath));
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const router = {
|
|
64
95
|
get(path, action) {
|
|
65
96
|
add({ path, method: HttpMethod.GET }, action);
|
|
66
97
|
},
|
|
@@ -74,11 +105,13 @@ export function CreateHttpRouter(groupRoute, notFound) {
|
|
|
74
105
|
add({ path, method: HttpMethod.DELETE }, action);
|
|
75
106
|
},
|
|
76
107
|
group(groupRouter) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
addGroup({ path: startWithPattern }, (routeData) => {
|
|
108
|
+
const localGroupPath = groupRouter.getGroupRoute().path;
|
|
109
|
+
addGroup(localGroupPath, groupRouter, (routeData) => {
|
|
80
110
|
return groupRouter.match(routeData.val);
|
|
81
111
|
});
|
|
112
|
+
// Re-derive every path already registered on the nested router
|
|
113
|
+
// (and anything nested under IT) against our current absolute base.
|
|
114
|
+
groupRouter.__rebase(contactPaths(groupRoute.path, localGroupPath));
|
|
82
115
|
},
|
|
83
116
|
getMapRoutes() {
|
|
84
117
|
return routes;
|
|
@@ -93,15 +126,14 @@ export function CreateHttpRouter(groupRoute, notFound) {
|
|
|
93
126
|
pathParams: routeData.params.flatByKey("path"),
|
|
94
127
|
};
|
|
95
128
|
});
|
|
96
|
-
}
|
|
129
|
+
},
|
|
130
|
+
__rebase: rebase,
|
|
97
131
|
};
|
|
132
|
+
return router;
|
|
133
|
+
}
|
|
134
|
+
export function CreateHttp1Router(groupRoute, notFound = http1NotFoundHandler) {
|
|
135
|
+
return CreateHttpRouter(groupRoute, notFound);
|
|
98
136
|
}
|
|
99
|
-
// export function CreateHttp1Router(
|
|
100
|
-
// groupRoute: GroupRoute,
|
|
101
|
-
// notFound: Action<ServerResponse> = http1NotFoundHandler,
|
|
102
|
-
// ): Http1Router {
|
|
103
|
-
// return CreateHttpRouter<ServerResponse, Http1Router>(groupRoute, notFound);
|
|
104
|
-
// }
|
|
105
137
|
// export function CreateHttp2Router(
|
|
106
138
|
// groupRoute: GroupRoute,
|
|
107
139
|
// notFound: Action<ServerHttp2Stream> = http2NotFoundHandler
|
package/package.json
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smuzi/http-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "HTTP server and routing for JavaScript and TypeScript",
|
|
5
|
-
"scripts": {
|
|
6
|
-
"test": "tsx tests/index.ts",
|
|
7
|
-
"build": "tsc --project tsconfig.build.json",
|
|
8
|
-
"prepublishOnly": "npm run build"
|
|
9
|
-
},
|
|
10
5
|
"type": "module",
|
|
6
|
+
"types": "./build/index.d.ts",
|
|
11
7
|
"keywords": [
|
|
12
8
|
"http",
|
|
13
9
|
"router",
|
|
@@ -22,21 +18,30 @@
|
|
|
22
18
|
"access": "public"
|
|
23
19
|
},
|
|
24
20
|
"files": [
|
|
21
|
+
"./src",
|
|
25
22
|
"./build"
|
|
26
23
|
],
|
|
27
24
|
"exports": {
|
|
28
|
-
".": "./
|
|
25
|
+
"./package.json": "./package.json",
|
|
26
|
+
".": "./src/index.ts",
|
|
27
|
+
"./*": "./src/*.ts"
|
|
29
28
|
},
|
|
30
29
|
"imports": {
|
|
31
30
|
"#lib/*": "./src/*"
|
|
32
31
|
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@smuzi/std": "0.2.4",
|
|
34
|
+
"@smuzi/schema": "0.0.2"
|
|
35
|
+
},
|
|
33
36
|
"devDependencies": {
|
|
34
|
-
"@smuzi/
|
|
35
|
-
"@smuzi/
|
|
36
|
-
"@smuzi/schema": "workspace:*",
|
|
37
|
-
"@smuzi/tests": "workspace:*",
|
|
37
|
+
"@smuzi/faker": "0.0.2",
|
|
38
|
+
"@smuzi/tests": "0.0.1",
|
|
38
39
|
"@types/node": "^22.15.21",
|
|
39
40
|
"tsx": "^4.20.6",
|
|
40
41
|
"typescript": "^7.0.2"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "tsx --conditions=development tests/index.ts",
|
|
45
|
+
"build": "tsc --project tsconfig.build.json"
|
|
41
46
|
}
|
|
42
47
|
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {Option, HttpProtocol, None, HttpResponse, transformError, dump} from "@smuzi/std";
|
|
2
|
+
import {ActionErrorHandler, Context, Http1Router} from "#lib/router.js";
|
|
3
|
+
import {ServerResponse} from "node:http";
|
|
4
|
+
|
|
5
|
+
type Cert = Option<{
|
|
6
|
+
key: string,
|
|
7
|
+
cert: string,
|
|
8
|
+
}>
|
|
9
|
+
|
|
10
|
+
export type Http1ServerConfig = {
|
|
11
|
+
host: string;
|
|
12
|
+
port: number,
|
|
13
|
+
router: Http1Router,
|
|
14
|
+
cert: Cert,
|
|
15
|
+
protocol: HttpProtocol,
|
|
16
|
+
errorHandler: ActionErrorHandler<ServerResponse>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type InputHttp1ServerConfig = Partial<Http1ServerConfig> & {
|
|
20
|
+
router: Http1Router,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function http1ErrorHandler(context: Context<ServerResponse>, error) {
|
|
24
|
+
//TODO: write error to log and remove dump()
|
|
25
|
+
dump({msg: "http1ErrorHandler", error})
|
|
26
|
+
return HttpResponse.asJson({error:"Internal Server Error"}, 500);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildHttp1ServerConfig({host = 'localhost', port = 8080, router, cert = None(), errorHandler = http1ErrorHandler}: InputHttp1ServerConfig): Http1ServerConfig {
|
|
30
|
+
return {
|
|
31
|
+
host,
|
|
32
|
+
port,
|
|
33
|
+
router,
|
|
34
|
+
cert,
|
|
35
|
+
protocol: cert.someOrNone(HttpProtocol.HTTPS, HttpProtocol.HTTP),
|
|
36
|
+
errorHandler,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type Http2BaseServerConfig = {
|
|
41
|
+
host: string;
|
|
42
|
+
port: number,
|
|
43
|
+
router: Http1Router,
|
|
44
|
+
cert?: Cert
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type Http2ServerConfig = Http2BaseServerConfig & {
|
|
48
|
+
cert: Cert,
|
|
49
|
+
protocol: HttpProtocol,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildHttp2ServerConfig({host, port, router, cert = None() }: Http2BaseServerConfig): Http2ServerConfig {
|
|
53
|
+
return {
|
|
54
|
+
host,
|
|
55
|
+
port,
|
|
56
|
+
router,
|
|
57
|
+
cert,
|
|
58
|
+
protocol: cert.someOrNone(HttpProtocol.HTTPS, HttpProtocol.HTTP)
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import http, { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import { TLSSocket } from 'node:tls';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
|
|
6
|
+
import { methodFromString } from "#lib/router.js";
|
|
7
|
+
import {
|
|
8
|
+
isArray,
|
|
9
|
+
isObject,
|
|
10
|
+
isString,
|
|
11
|
+
match,
|
|
12
|
+
StdRecord,
|
|
13
|
+
matchUnknown,
|
|
14
|
+
OptionFromNullable,
|
|
15
|
+
Some,
|
|
16
|
+
Result,
|
|
17
|
+
Option,
|
|
18
|
+
Err,
|
|
19
|
+
Ok,
|
|
20
|
+
isNull,
|
|
21
|
+
transformError,
|
|
22
|
+
StdError,
|
|
23
|
+
dump,
|
|
24
|
+
HttpResponse,
|
|
25
|
+
HttpRequest,
|
|
26
|
+
StdMap,
|
|
27
|
+
isSome,
|
|
28
|
+
isOption,
|
|
29
|
+
isResult,
|
|
30
|
+
isIterable,
|
|
31
|
+
ResponseHttpHeaders, RequestHttpHeaders, asList, asRecord, asMap, querystring, QueryParams,
|
|
32
|
+
StdJson, uuid
|
|
33
|
+
} from '@smuzi/std';
|
|
34
|
+
import { HttpServer, HttpServerRunError, Http1ServerConfig } from "#lib/index.js";
|
|
35
|
+
import { log } from 'node:console';
|
|
36
|
+
|
|
37
|
+
type NativeServer = any
|
|
38
|
+
|
|
39
|
+
export class StdHttp1Server implements HttpServer {
|
|
40
|
+
readonly #server: NativeServer
|
|
41
|
+
|
|
42
|
+
constructor(server: NativeServer) {
|
|
43
|
+
this.#server = server;
|
|
44
|
+
}
|
|
45
|
+
async close(): Promise<Result<boolean, StdError>> {
|
|
46
|
+
return new Promise(resolve => {
|
|
47
|
+
this.#server.close((err) => {
|
|
48
|
+
return resolve(isNull(err) ? Ok(true) : Err(transformError(err)));
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readRequestBodyAsBuffer(req: IncomingMessage): () => Promise<Result<Buffer, Error>> {
|
|
55
|
+
return async () => {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const chunks: Buffer[] = [];
|
|
58
|
+
|
|
59
|
+
req.on("data", (chunk) => {
|
|
60
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
req.on("end", () => {
|
|
64
|
+
resolve(Ok(Buffer.concat(chunks)));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
req.on("error", (err) => reject(Err(err)));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readRequestJson(req: IncomingMessage): <T>() => Promise<Result<Option<T>, StdError>> {
|
|
73
|
+
return async(encoding: BufferEncoding = "utf-8") => {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
let body = "";
|
|
76
|
+
|
|
77
|
+
req.setEncoding(encoding);
|
|
78
|
+
|
|
79
|
+
req.on("data", (chunk: string) => {
|
|
80
|
+
body += chunk;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
req.on("end", () => {
|
|
84
|
+
resolve(StdJson.fromString(body));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
req.on("error", (err) => resolve(Err(transformError(err))));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readRequestInput(req: IncomingMessage): <T extends StdRecord<QueryParams>>() => Promise<Result<T, StdError>> {
|
|
93
|
+
return async(encoding: BufferEncoding = "utf-8") => {
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
let body = "";
|
|
96
|
+
|
|
97
|
+
req.setEncoding(encoding);
|
|
98
|
+
|
|
99
|
+
req.on("data", (chunk: string) => {
|
|
100
|
+
body += chunk;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
req.on("end", () => {
|
|
104
|
+
resolve(querystring.fromString(body));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
req.on("error", (err) => resolve(Err(transformError(err))));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readRawBody(req: IncomingMessage): <T>() => Promise<Result<string, StdError>> {
|
|
113
|
+
return async(encoding: BufferEncoding = "utf-8") => {
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
let body = "";
|
|
116
|
+
|
|
117
|
+
req.setEncoding(encoding);
|
|
118
|
+
|
|
119
|
+
req.on("data", (chunk: string) => {
|
|
120
|
+
body += chunk;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
req.on("end", () => {
|
|
124
|
+
resolve(Ok(body));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
req.on("error", (err) => resolve(Err(transformError(err))));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let COUNTER = 0;
|
|
133
|
+
|
|
134
|
+
export async function http1ServerRun(config: Http1ServerConfig): Promise<Result<StdHttp1Server, HttpServerRunError>> {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
async function handler(nativeRequest: IncomingMessage, nativeResponse: ServerResponse) {
|
|
137
|
+
const methodStr = OptionFromNullable(nativeRequest.method).unwrap();
|
|
138
|
+
const fullUrl = nativeRequest.url || "/";
|
|
139
|
+
const isHttps = nativeRequest.socket instanceof TLSSocket;
|
|
140
|
+
const urlObj = new URL(fullUrl, (isHttps ? "http" : "https") + `://${nativeRequest.headers.host}`);
|
|
141
|
+
const path = OptionFromNullable(urlObj.pathname).unwrap();
|
|
142
|
+
const request = {
|
|
143
|
+
path: path.replace(/^\//, '').replace(/\/$/, ''),
|
|
144
|
+
method: methodFromString(methodStr).unwrap(`Error: undefined http method '${methodStr}'`),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const routeMatched = config.router.match(request);
|
|
148
|
+
|
|
149
|
+
let context = {
|
|
150
|
+
request: new HttpRequest({
|
|
151
|
+
method: request.method,
|
|
152
|
+
path: request.path,
|
|
153
|
+
query: () => new StdRecord<QueryParams>(Object.fromEntries(urlObj.searchParams.entries())),
|
|
154
|
+
headers: new RequestHttpHeaders(nativeRequest.headers as any),
|
|
155
|
+
buffer: readRequestBodyAsBuffer(nativeRequest),
|
|
156
|
+
body: readRawBody(nativeRequest),
|
|
157
|
+
json: readRequestJson(nativeRequest),
|
|
158
|
+
form: readRequestInput(nativeRequest),
|
|
159
|
+
}),
|
|
160
|
+
response: nativeResponse,
|
|
161
|
+
pathParams: routeMatched.pathParams,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
let response;
|
|
165
|
+
try {
|
|
166
|
+
response = await routeMatched.action(context)
|
|
167
|
+
} catch (error) {
|
|
168
|
+
response = await config.errorHandler(context, error);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if (isOption(response)) {
|
|
173
|
+
response = response.someOr("");
|
|
174
|
+
} else if(isResult(response)) {
|
|
175
|
+
response = response.unsafeSource();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (isNull(response)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const handlers = new Map();
|
|
183
|
+
|
|
184
|
+
handlers.set(resp => isString(resp) || resp instanceof Buffer, (response) => {
|
|
185
|
+
nativeResponse.writeHead(200, {
|
|
186
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
187
|
+
});
|
|
188
|
+
nativeResponse.end(response);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
handlers.set(resp => resp instanceof HttpResponse, (response: HttpResponse) => {
|
|
192
|
+
nativeResponse.statusCode = response.status;
|
|
193
|
+
nativeResponse.statusMessage = response.statusText;
|
|
194
|
+
nativeResponse.setHeaders(response.headers.unsafeSource());
|
|
195
|
+
nativeResponse.end(response.body.someOr(""));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
handlers.set(resp => resp instanceof StdError, (error: StdError) => {
|
|
199
|
+
nativeResponse.statusCode = 500;
|
|
200
|
+
nativeResponse.statusMessage = error.message;
|
|
201
|
+
nativeResponse.end(error.message);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
handlers.set(
|
|
205
|
+
(response) => isObject(response) || isArray(response) || asList(response) || asRecord(response) || asMap(response),
|
|
206
|
+
(response) => {
|
|
207
|
+
//TODO: return respons on top instead of changed nativeResponse inner
|
|
208
|
+
nativeResponse.setHeader("Content-Type", "application/json; charset=utf-8" );
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const resp = StdJson.toString(response).match({
|
|
212
|
+
Ok: (jsonStr) => ({status: 200, body: jsonStr }),
|
|
213
|
+
Err: (err) => ({status: 500 , body: '{"error":"Internal Server Error"}' }),
|
|
214
|
+
});
|
|
215
|
+
nativeResponse.statusCode = resp.status;
|
|
216
|
+
nativeResponse.end(resp.body);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
nativeResponse.statusCode = 500;
|
|
219
|
+
nativeResponse.end('{"error":"Internal Server Error"}');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
matchUnknown(response, handlers, () => {
|
|
226
|
+
nativeResponse.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
227
|
+
nativeResponse.end("Internal Server Error");
|
|
228
|
+
}, false);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const server = config.cert.match({
|
|
232
|
+
Some: cert => {
|
|
233
|
+
return https.createServer({
|
|
234
|
+
key: fs.readFileSync(cert.key),
|
|
235
|
+
cert: fs.readFileSync(cert.cert),
|
|
236
|
+
}, handler);
|
|
237
|
+
},
|
|
238
|
+
None: () => {
|
|
239
|
+
return http.createServer(handler);
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
server.once('error', (nativeError: any) => {
|
|
245
|
+
log("Server started on", nativeError);
|
|
246
|
+
resolve(Err({
|
|
247
|
+
errno: OptionFromNullable(nativeError.errno),
|
|
248
|
+
code: OptionFromNullable(nativeError.code),
|
|
249
|
+
syscall: OptionFromNullable(nativeError.syscall),
|
|
250
|
+
path: OptionFromNullable(nativeError.path),
|
|
251
|
+
port: OptionFromNullable(nativeError.port),
|
|
252
|
+
address: OptionFromNullable(nativeError.address),
|
|
253
|
+
}));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
server.listen(config.port, () => {
|
|
257
|
+
log("Server started on " + config.protocol + "://" + config.host + ":" + config.port)
|
|
258
|
+
|
|
259
|
+
resolve(Ok(new StdHttp1Server(server)));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
});
|
|
263
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import http2, { Http2SecureServer, Http2Server, IncomingHttpHeaders, ServerHttp2Stream } from 'node:http2';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import { methodFromString } from "#lib/router.js";
|
|
5
|
+
import { isArray, isObject, isString, match, matchUnknown, OptionFromNullable, Some, Result, Option, Err, Ok, isNull, transformError, StdError, dump, HttpResponse, StdJson } from '@smuzi/std';
|
|
6
|
+
import { HttpServer, HttpServerRunError, Http2ServerConfig } from "#lib/index.js";
|
|
7
|
+
|
|
8
|
+
type NativeServer = Http2SecureServer | Http2Server;
|
|
9
|
+
|
|
10
|
+
export class StdHttp2Server implements HttpServer {
|
|
11
|
+
readonly #server: NativeServer
|
|
12
|
+
|
|
13
|
+
constructor(server: NativeServer) {
|
|
14
|
+
this.#server = server;
|
|
15
|
+
}
|
|
16
|
+
async close(): Promise<Result<boolean, StdError>> {
|
|
17
|
+
return new Promise(resolve => {
|
|
18
|
+
this.#server.close((err) => {
|
|
19
|
+
return resolve(isNull(err) ? Ok(true) : Err(transformError(err)));
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function http2ServerRun(config: Http2ServerConfig): Promise<Result<StdHttp2Server, HttpServerRunError>> {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
|
|
28
|
+
const server = config.cert.match({
|
|
29
|
+
Some: cert => {
|
|
30
|
+
return http2.createSecureServer({
|
|
31
|
+
key: fs.readFileSync(cert.key),
|
|
32
|
+
cert: fs.readFileSync(cert.cert),
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
None: () => {
|
|
36
|
+
return http2.createServer();
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
server.once('error', (nativeError) => {
|
|
42
|
+
resolve(Err({
|
|
43
|
+
errno: OptionFromNullable(nativeError.errno),
|
|
44
|
+
code: OptionFromNullable(nativeError.code),
|
|
45
|
+
syscall: OptionFromNullable(nativeError.syscall),
|
|
46
|
+
path: OptionFromNullable(nativeError.path),
|
|
47
|
+
port: OptionFromNullable(nativeError.port),
|
|
48
|
+
address: OptionFromNullable(nativeError.address),
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
server.on('stream', (stream: ServerHttp2Stream, headers: IncomingHttpHeaders) => {
|
|
53
|
+
const methodStr = OptionFromNullable(headers[':method']).unwrap();
|
|
54
|
+
const path = OptionFromNullable(headers[':path']).unwrap().replace(/^\//, '').replace(/\/$/, '');
|
|
55
|
+
const urlObj = new URL(path, `http://${headers[':authority']}`);
|
|
56
|
+
|
|
57
|
+
const request = {
|
|
58
|
+
path: path,
|
|
59
|
+
method: methodFromString(methodStr).unwrap(`Error: undefined http method '${methodStr}'`),
|
|
60
|
+
query: urlObj.searchParams,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const response = config.router.match(request)
|
|
64
|
+
const handlers = new Map();
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
handlers.set(isString, (response) => {
|
|
68
|
+
stream.respond({
|
|
69
|
+
'content-type': 'text/html; charset=utf-8',
|
|
70
|
+
':status': 200,
|
|
71
|
+
});
|
|
72
|
+
stream.end(response)
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
handlers.set(resp => resp instanceof HttpResponse, (response: HttpResponse) => {
|
|
76
|
+
stream.respond({
|
|
77
|
+
'content-type': 'application/json; charset=utf-8',
|
|
78
|
+
':status': response.status,
|
|
79
|
+
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
handlers.set(response => isObject(response) || isArray(response), (response) => {
|
|
86
|
+
stream.respond({
|
|
87
|
+
'content-type': 'application/json; charset=utf-8',
|
|
88
|
+
':status': 200,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
stream.end(StdJson.toString(response).match({
|
|
92
|
+
Ok: (json) => json,
|
|
93
|
+
Err: (err) => {
|
|
94
|
+
stream.respond({
|
|
95
|
+
'content-type': 'application/json; charset=utf-8',
|
|
96
|
+
':status': 500,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return `{"error":"Internal Server Error"}`;
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
matchUnknown(
|
|
107
|
+
response,
|
|
108
|
+
handlers,
|
|
109
|
+
_ => {
|
|
110
|
+
stream.respond({
|
|
111
|
+
':status': 500,
|
|
112
|
+
})
|
|
113
|
+
stream.end('Internal Server Error');
|
|
114
|
+
},
|
|
115
|
+
false
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
server.listen(config.port, () => {
|
|
121
|
+
resolve(Ok(new StdHttp2Server(server)));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
});
|
|
125
|
+
}
|
package/src/index.ts
ADDED
package/src/router.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asRegExp,
|
|
3
|
+
asString,
|
|
4
|
+
match,
|
|
5
|
+
MatchedData,
|
|
6
|
+
None,
|
|
7
|
+
Option,
|
|
8
|
+
Some,
|
|
9
|
+
HttpMethod,
|
|
10
|
+
HttpRequest,
|
|
11
|
+
Result,
|
|
12
|
+
HttpResponse,
|
|
13
|
+
StdError,
|
|
14
|
+
StdMap, StdRecord, StdList,
|
|
15
|
+
} from "@smuzi/std";
|
|
16
|
+
import { ServerResponse } from "node:http";
|
|
17
|
+
import { ServerHttp2Stream } from "node:http2";
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
type Request = { path: string, method: HttpMethod };
|
|
21
|
+
type P = any;
|
|
22
|
+
type ActionPrimitiveResponse = P | StdMap<string> | StdRecord<any> | StdList | Record<PropertyKey, P> | Record<PropertyKey, P>[];
|
|
23
|
+
export type ActionResponse = void | ActionPrimitiveResponse | HttpResponse | Option<ActionPrimitiveResponse> | Result<ActionPrimitiveResponse, ActionPrimitiveResponse>;
|
|
24
|
+
|
|
25
|
+
export type Action<Resp extends THttpResponse> = (context: Context<Resp>) => ActionResponse | Promise<ActionResponse>
|
|
26
|
+
export type PathParam = string | RegExp;
|
|
27
|
+
export type ActionErrorHandler<Resp extends THttpResponse> = (context: Context<Resp>, err: any) => ActionResponse | Promise<ActionResponse>
|
|
28
|
+
|
|
29
|
+
type THttpResponse = ServerResponse | ServerHttp2Stream
|
|
30
|
+
type Route = { path: PathParam, method: HttpMethod };
|
|
31
|
+
type GroupRoute = { path: PathParam };
|
|
32
|
+
type RouteMatched = MatchedData<Request, Option<{ path: Record<string, string> }>>
|
|
33
|
+
type RouteMatchResult<Resp extends THttpResponse> = {
|
|
34
|
+
action: Action<Resp>,
|
|
35
|
+
pathParams: Option<Record<string, string | number | boolean>>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type Router<Resp extends THttpResponse, A = Action<Resp>> = {
|
|
39
|
+
group: (groupRouter: Router<Resp>) => void
|
|
40
|
+
getMapRoutes: () => Map<Route, (routeData: RouteMatched) => RouteMatchResult<Resp>>
|
|
41
|
+
getGroupRoute(): GroupRoute
|
|
42
|
+
get: (path: PathParam, action: A) => void
|
|
43
|
+
post: (path: PathParam, action: A) => void
|
|
44
|
+
put: (path: PathParam, action: A) => void
|
|
45
|
+
delete: (path: PathParam, action: A) => void
|
|
46
|
+
match: (request: Request) => RouteMatchResult<Resp>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type Http1Router = Router<ServerResponse>;
|
|
50
|
+
export type Http2Router = Router<ServerHttp2Stream>;
|
|
51
|
+
|
|
52
|
+
export type Context<Resp extends THttpResponse, Params = unknown,> = {
|
|
53
|
+
request: HttpRequest,
|
|
54
|
+
response: Resp,
|
|
55
|
+
pathParams: Params,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function processPath(path: PathParam): PathParam {
|
|
59
|
+
if (!asString(path)) return path;
|
|
60
|
+
if (! /\{[a-zA-Z0-9_]+\}/g.test(path)) return path;
|
|
61
|
+
|
|
62
|
+
const pattern = `^${path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_, name) => `(?<${name}>[^/]+)`).replace(/\//g, '\\/')}$`;
|
|
63
|
+
return new RegExp(pattern);
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function contactPaths(path1: PathParam, path2: PathParam): PathParam | never {
|
|
68
|
+
const path1AsRegExp = asRegExp(path1);
|
|
69
|
+
const path2AsRegExp = asRegExp(path2);
|
|
70
|
+
|
|
71
|
+
if (path1AsRegExp || path2AsRegExp) {
|
|
72
|
+
const source1 = path1AsRegExp
|
|
73
|
+
? path1.source
|
|
74
|
+
: path1;
|
|
75
|
+
|
|
76
|
+
const source2 = path2AsRegExp
|
|
77
|
+
? path2.source
|
|
78
|
+
: path2;
|
|
79
|
+
|
|
80
|
+
return new RegExp(
|
|
81
|
+
"^"+(source1 + source2).replaceAll('^', '')
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return path1 + path2;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
export function toStartWithPattern(input: PathParam): RegExp {
|
|
90
|
+
if (asString(input)) {
|
|
91
|
+
const escaped = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
92
|
+
return new RegExp(`^${escaped}.*`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let pattern = input.source;
|
|
96
|
+
|
|
97
|
+
if (pattern.endsWith('$')) {
|
|
98
|
+
pattern = pattern.slice(0, -1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return new RegExp(`${pattern}.*`, input.flags);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
export function methodFromString(method: string): Option<HttpMethod> {
|
|
106
|
+
const handers = new Map<string, Option<HttpMethod>>([
|
|
107
|
+
['GET', Some(HttpMethod.GET)],
|
|
108
|
+
['POST', Some(HttpMethod.POST)],
|
|
109
|
+
['PUT', Some(HttpMethod.PUT)],
|
|
110
|
+
['DELETE', Some(HttpMethod.DELETE)],
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
return match(
|
|
114
|
+
method,
|
|
115
|
+
handers,
|
|
116
|
+
None(),
|
|
117
|
+
false
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function http1NotFoundHandler(context: Context<ServerResponse>) {
|
|
122
|
+
return HttpResponse.asJson({error:"Not Found"}, 404);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
function http2NotFoundHandler(context: Context<ServerHttp2Stream>) {
|
|
127
|
+
context.response.respond({
|
|
128
|
+
'content-type': 'application/json; charset=utf-8',
|
|
129
|
+
':status': 404,
|
|
130
|
+
});
|
|
131
|
+
context.response.end();
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
// Internal-only surface: lets a parent router recompute a child router's
|
|
138
|
+
// (and, recursively, its own nested groups') absolute paths after nesting.
|
|
139
|
+
// Not part of the public `Router` type — accessed via a narrow cast.
|
|
140
|
+
type Rebasable = { __rebase: (newBase: PathParam) => void };
|
|
141
|
+
|
|
142
|
+
export function CreateHttpRouter<Resp extends THttpResponse, GR extends Router<Resp>>(
|
|
143
|
+
groupRoute: GroupRoute,
|
|
144
|
+
notFound: Action<Resp>,
|
|
145
|
+
): Router<Resp> {
|
|
146
|
+
const routes = new Map()
|
|
147
|
+
|
|
148
|
+
// Every own route/subgroup is registered ONCE at startup. We additionally
|
|
149
|
+
// remember each one's path *relative to this router* so that, if this
|
|
150
|
+
// router later gets attached to a parent via `.group()`, we can recompute
|
|
151
|
+
// every already-registered absolute path exactly once at that moment —
|
|
152
|
+
// never again on every match()/request.
|
|
153
|
+
const ownRoutes: { route: Route; localPath: PathParam }[] = [];
|
|
154
|
+
const ownGroups: { route: GroupRoute; localGroupPath: PathParam; childRouter: Router<any> }[] = [];
|
|
155
|
+
|
|
156
|
+
const computeAbsolute = (localPath: PathParam) =>
|
|
157
|
+
processPath(contactPaths(groupRoute.path, localPath));
|
|
158
|
+
|
|
159
|
+
const add = (route: Route, action: any) => {
|
|
160
|
+
const localPath = route.path;
|
|
161
|
+
route.path = computeAbsolute(localPath);
|
|
162
|
+
ownRoutes.push({ route, localPath });
|
|
163
|
+
|
|
164
|
+
routes.set(route, (routeData: RouteMatched) => {
|
|
165
|
+
return {
|
|
166
|
+
action,
|
|
167
|
+
pathParams: routeData.params.flatByKey("path"),
|
|
168
|
+
};
|
|
169
|
+
})
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const addGroup = (localGroupPath: PathParam, childRouter: Router<any>, action: any) => {
|
|
173
|
+
const route: GroupRoute = { path: computeAbsolute(toStartWithPattern(localGroupPath)) };
|
|
174
|
+
ownGroups.push({ route, localGroupPath, childRouter });
|
|
175
|
+
routes.set(route, action)
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Recomputes this router's own base plus every route/subgroup registered
|
|
179
|
+
// on it so far, then recurses into any already-nested group routers so
|
|
180
|
+
// the whole subtree stays consistent — called once, when this router is
|
|
181
|
+
// attached to a parent via `.group()`.
|
|
182
|
+
const rebase = (newBase: PathParam) => {
|
|
183
|
+
groupRoute.path = newBase;
|
|
184
|
+
|
|
185
|
+
for (const entry of ownRoutes) {
|
|
186
|
+
entry.route.path = computeAbsolute(entry.localPath);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const entry of ownGroups) {
|
|
190
|
+
entry.route.path = computeAbsolute(toStartWithPattern(entry.localGroupPath));
|
|
191
|
+
(entry.childRouter as unknown as Rebasable).__rebase(
|
|
192
|
+
contactPaths(newBase, entry.localGroupPath)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const router: Router<Resp> & Rebasable = {
|
|
198
|
+
get(path, action) {
|
|
199
|
+
add({ path, method: HttpMethod.GET }, action)
|
|
200
|
+
},
|
|
201
|
+
post(path, action) {
|
|
202
|
+
add({ path, method: HttpMethod.POST }, action)
|
|
203
|
+
},
|
|
204
|
+
put(path, action) {
|
|
205
|
+
add({ path, method: HttpMethod.PUT }, action)
|
|
206
|
+
},
|
|
207
|
+
delete(path, action) {
|
|
208
|
+
add({ path, method: HttpMethod.DELETE }, action)
|
|
209
|
+
},
|
|
210
|
+
group(groupRouter: GR) {
|
|
211
|
+
const localGroupPath = groupRouter.getGroupRoute().path;
|
|
212
|
+
|
|
213
|
+
addGroup(localGroupPath, groupRouter, (routeData: RouteMatched) => {
|
|
214
|
+
return groupRouter.match(routeData.val);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Re-derive every path already registered on the nested router
|
|
218
|
+
// (and anything nested under IT) against our current absolute base.
|
|
219
|
+
(groupRouter as unknown as Rebasable).__rebase(
|
|
220
|
+
contactPaths(groupRoute.path, localGroupPath)
|
|
221
|
+
);
|
|
222
|
+
},
|
|
223
|
+
getMapRoutes() {
|
|
224
|
+
return routes;
|
|
225
|
+
},
|
|
226
|
+
getGroupRoute(): GroupRoute {
|
|
227
|
+
return groupRoute;
|
|
228
|
+
},
|
|
229
|
+
match(request: Request) {
|
|
230
|
+
return match(request, this.getMapRoutes(), (routeData: RouteMatched) => {
|
|
231
|
+
return {
|
|
232
|
+
action: notFound,
|
|
233
|
+
pathParams: routeData.params.flatByKey("path"),
|
|
234
|
+
} as RouteMatchResult<Resp>;
|
|
235
|
+
})
|
|
236
|
+
},
|
|
237
|
+
__rebase: rebase,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
return router;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
export function CreateHttp1Router(
|
|
246
|
+
groupRoute: GroupRoute,
|
|
247
|
+
notFound: Action<ServerResponse> = http1NotFoundHandler,
|
|
248
|
+
): Http1Router {
|
|
249
|
+
return CreateHttpRouter<ServerResponse, Http1Router>(groupRoute, notFound);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// export function CreateHttp2Router(
|
|
253
|
+
// groupRoute: GroupRoute,
|
|
254
|
+
// notFound: Action<ServerHttp2Stream> = http2NotFoundHandler
|
|
255
|
+
// ): Http2Router {
|
|
256
|
+
// return CreateHttpRouter<ServerHttp2Stream, Http2Router>(groupRoute, notFound);
|
|
257
|
+
// }
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Option, Result, StdError } from "@smuzi/std";
|
|
2
|
+
|
|
3
|
+
export interface HttpServer {
|
|
4
|
+
close(): Promise<Result<boolean, StdError>>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type HttpServerRunError = {
|
|
8
|
+
errno: Option<number>,
|
|
9
|
+
code: Option<string>,
|
|
10
|
+
syscall: Option<string>,
|
|
11
|
+
path: Option<string>,
|
|
12
|
+
port: Option<string>,
|
|
13
|
+
address: Option<string>,
|
|
14
|
+
};
|