@mionjs/platform-uws 0.0.0-stage → 0.13.0
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/.dist/cjs/index.cjs +8 -0
- package/.dist/cjs/index.d.ts +4 -0
- package/.dist/cjs/package.json +1 -0
- package/.dist/cjs/src/constants.cjs +11 -0
- package/.dist/cjs/src/constants.cjs.map +1 -0
- package/.dist/cjs/src/constants.d.ts +3 -0
- package/.dist/cjs/src/headers.cjs +16 -0
- package/.dist/cjs/src/headers.cjs.map +1 -0
- package/.dist/cjs/src/headers.d.ts +4 -0
- package/.dist/cjs/src/types.cjs +0 -0
- package/.dist/cjs/src/types.d.ts +8 -0
- package/.dist/cjs/src/uwsHttp.cjs +224 -0
- package/.dist/cjs/src/uwsHttp.cjs.map +1 -0
- package/.dist/cjs/src/uwsHttp.d.ts +12 -0
- package/.dist/esm/index.d.ts +4 -0
- package/.dist/esm/index.js +3 -0
- package/.dist/esm/src/constants.d.ts +3 -0
- package/.dist/esm/src/constants.js +11 -0
- package/.dist/esm/src/constants.js.map +1 -0
- package/.dist/esm/src/headers.d.ts +4 -0
- package/.dist/esm/src/headers.js +15 -0
- package/.dist/esm/src/headers.js.map +1 -0
- package/.dist/esm/src/types.d.ts +8 -0
- package/.dist/esm/src/types.js +0 -0
- package/.dist/esm/src/uwsHttp.d.ts +12 -0
- package/.dist/esm/src/uwsHttp.js +220 -0
- package/.dist/esm/src/uwsHttp.js.map +1 -0
- package/LICENSE +21 -0
- package/README.md +26 -2
- package/package.json +64 -3
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_src_constants = require("./src/constants.cjs");
|
|
3
|
+
const require_src_uwsHttp = require("./src/uwsHttp.cjs");
|
|
4
|
+
exports.DEFAULT_UWS_HTTP_OPTIONS = require_src_constants.DEFAULT_UWS_HTTP_OPTIONS;
|
|
5
|
+
exports.resetUwsHttpOpts = require_src_uwsHttp.resetUwsHttpOpts;
|
|
6
|
+
exports.setUwsHttpOpts = require_src_uwsHttp.setUwsHttpOpts;
|
|
7
|
+
exports.startUwsServer = require_src_uwsHttp.startUwsServer;
|
|
8
|
+
exports.uwsRequestHandler = require_src_uwsHttp.uwsRequestHandler;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/constants.ts
|
|
3
|
+
var DEFAULT_UWS_HTTP_OPTIONS = {
|
|
4
|
+
port: 80,
|
|
5
|
+
/** What a route takes when its own `maxBodySize` and its types say nothing: the shared 128 KB default */
|
|
6
|
+
maxBodySize: require("@mionjs/core").DEFAULT_MAX_BODY_SIZE
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|
|
9
|
+
exports.DEFAULT_UWS_HTTP_OPTIONS = DEFAULT_UWS_HTTP_OPTIONS;
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=constants.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.cjs","names":[],"sources":["../../../src/constants.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {DEFAULT_MAX_BODY_SIZE} from '@mionjs/core';\nimport {UwsHttpOptions} from './types.ts';\n\nexport const DEFAULT_UWS_HTTP_OPTIONS: UwsHttpOptions = {\n port: 80,\n /** What a route takes when its own `maxBodySize` and its types say nothing: the shared 128 KB default */\n maxBodySize: DEFAULT_MAX_BODY_SIZE,\n};\n"],"mappings":";;AAUA,IAAa,2BAA2C;CACtD,MAAM;;CAEN,mCAAa,EAAA;AACf"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _mionjs_router = require("@mionjs/router");
|
|
3
|
+
//#region src/headers.ts
|
|
4
|
+
/** uWS headers are readable only synchronously inside the handler, so snapshot them; uWS already lower-cases the names. */
|
|
5
|
+
function headersFromUwsRequest(req) {
|
|
6
|
+
const record = {};
|
|
7
|
+
req.forEach((name, value) => {
|
|
8
|
+
const previous = record[name];
|
|
9
|
+
record[name] = typeof previous === "string" ? `${previous}, ${value}` : value;
|
|
10
|
+
});
|
|
11
|
+
return (0, _mionjs_router.headersFromRecord)(record, true);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
exports.headersFromUwsRequest = headersFromUwsRequest;
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=headers.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.cjs","names":[],"sources":["../../../src/headers.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {MionHeaders, headersFromRecord} from '@mionjs/router';\nimport type {HttpRequest} from '@mionjs/bin-uws';\n\n/** uWS headers are readable only synchronously inside the handler, so snapshot them; uWS already lower-cases the names. */\nexport function headersFromUwsRequest(req: HttpRequest): MionHeaders {\n const record: Record<string, string> = {};\n req.forEach((name, value) => {\n // a string check, not undefined: a `constructor` header would otherwise find Object.prototype's function\n const previous = record[name];\n record[name] = typeof previous === 'string' ? `${previous}, ${value}` : value;\n });\n return headersFromRecord(record, true);\n}\n"],"mappings":";;;;AAWA,SAAgB,sBAAsB,KAA+B;CACnE,MAAM,SAAiC,CAAC;CACxC,IAAI,SAAS,MAAM,UAAU;EAE3B,MAAM,WAAW,OAAO;EACxB,OAAO,QAAQ,OAAO,aAAa,WAAW,GAAG,SAAS,IAAI,UAAU;CAC1E,CAAC;CACD,QAAA,GAAA,eAAA,mBAAyB,QAAQ,IAAI;AACvC"}
|
|
File without changes
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_src_constants = require("./constants.cjs");
|
|
3
|
+
const require_src_headers = require("./headers.cjs");
|
|
4
|
+
let _mionjs_router = require("@mionjs/router");
|
|
5
|
+
let http = require("http");
|
|
6
|
+
let _mionjs_bin_uws = require("@mionjs/bin-uws");
|
|
7
|
+
let _mionjs_core = require("@mionjs/core");
|
|
8
|
+
//#region src/uwsHttp.ts
|
|
9
|
+
var httpOptions = { ...require_src_constants.DEFAULT_UWS_HTTP_OPTIONS };
|
|
10
|
+
var UWS_MAX_SINGLE_READ = 524288;
|
|
11
|
+
function resetUwsHttpOpts() {
|
|
12
|
+
httpOptions = { ...require_src_constants.DEFAULT_UWS_HTTP_OPTIONS };
|
|
13
|
+
(0, _mionjs_router.resetRouter)();
|
|
14
|
+
}
|
|
15
|
+
function setUwsHttpOpts(options) {
|
|
16
|
+
httpOptions = {
|
|
17
|
+
...httpOptions,
|
|
18
|
+
...options
|
|
19
|
+
};
|
|
20
|
+
return httpOptions;
|
|
21
|
+
}
|
|
22
|
+
/** The platform config the router publishes: everything but the TLS file paths. */
|
|
23
|
+
function serializablePlatformConfig() {
|
|
24
|
+
const { ssl: _ssl, ...serializableConfig } = httpOptions;
|
|
25
|
+
return serializableConfig;
|
|
26
|
+
}
|
|
27
|
+
async function startUwsServer(options) {
|
|
28
|
+
const isTest = (0, _mionjs_core.getENV)("NODE_ENV") === "test";
|
|
29
|
+
if (options) setUwsHttpOpts(options);
|
|
30
|
+
if ((0, _mionjs_router.hostOwnsSocket)()) throw new Error("@mionjs/platform-uws does not support middleware mode: uWebSockets.js owns its own listen socket and cannot mount on a host node server. Use @mionjs/platform-node for middleware mode.");
|
|
31
|
+
const url = `${httpOptions.ssl ? "https" : "http"}://localhost${httpOptions.port !== 80 ? `:${httpOptions.port}` : ""}`;
|
|
32
|
+
if (!isTest) console.log(`mion uws server running on ${url}`, {
|
|
33
|
+
port: httpOptions.port,
|
|
34
|
+
httpOptions
|
|
35
|
+
});
|
|
36
|
+
const uws = (0, _mionjs_bin_uws.loadUws)();
|
|
37
|
+
const app = httpOptions.ssl ? uws.SSLApp(httpOptions.ssl) : uws.App();
|
|
38
|
+
app.any("/*", uwsRequestHandler);
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
app.listen(httpOptions.port, (listenSocket) => {
|
|
41
|
+
if (!listenSocket) {
|
|
42
|
+
reject(/* @__PURE__ */ new Error(`mion uws server failed to listen on port ${httpOptions.port} (port in use?)`));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
(0, _mionjs_router.setPlatformConfig)(serializablePlatformConfig());
|
|
46
|
+
let closed = false;
|
|
47
|
+
const close = () => {
|
|
48
|
+
if (closed) return;
|
|
49
|
+
closed = true;
|
|
50
|
+
uws.us_listen_socket_close(listenSocket);
|
|
51
|
+
};
|
|
52
|
+
const server = {
|
|
53
|
+
app,
|
|
54
|
+
listenSocket,
|
|
55
|
+
close
|
|
56
|
+
};
|
|
57
|
+
const shutdownHandler = function() {
|
|
58
|
+
if (!isTest) console.log(`Shutting down mion server on ${url}`);
|
|
59
|
+
close();
|
|
60
|
+
process.exit(0);
|
|
61
|
+
};
|
|
62
|
+
process.on("SIGINT", shutdownHandler);
|
|
63
|
+
process.on("SIGTERM", shutdownHandler);
|
|
64
|
+
resolve(server);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function drainRequestBody(res) {
|
|
69
|
+
res.onData(() => {});
|
|
70
|
+
}
|
|
71
|
+
function uwsRequestHandler(res, req) {
|
|
72
|
+
const state = {
|
|
73
|
+
replied: false,
|
|
74
|
+
aborted: false
|
|
75
|
+
};
|
|
76
|
+
const path = req.getUrl();
|
|
77
|
+
const query = req.getQuery();
|
|
78
|
+
const urlQuery = query === "" ? void 0 : query;
|
|
79
|
+
const reqHeaders = require_src_headers.headersFromUwsRequest(req);
|
|
80
|
+
const respHeaders = (0, _mionjs_router.getDefaultHeaders)();
|
|
81
|
+
res.onAborted(() => {
|
|
82
|
+
state.aborted = true;
|
|
83
|
+
});
|
|
84
|
+
const rawRequest = {
|
|
85
|
+
path,
|
|
86
|
+
urlQuery,
|
|
87
|
+
headers: reqHeaders
|
|
88
|
+
};
|
|
89
|
+
let chain;
|
|
90
|
+
try {
|
|
91
|
+
chain = (0, _mionjs_router.resolveExecutionChain)(path, urlQuery, rawRequest);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
drainRequestBody(res);
|
|
94
|
+
state.replied = true;
|
|
95
|
+
fatalFail(res, state, respHeaders, (0, _mionjs_router.toRpcError)(err));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const dispatchBody = (rawBody, readQueryBody) => {
|
|
99
|
+
let reqRawBody = rawBody;
|
|
100
|
+
let reqBodyType = _mionjs_core.SerializerModes.stringifyJson;
|
|
101
|
+
try {
|
|
102
|
+
const queryBody = readQueryBody ? (0, _mionjs_router.decodeQueryBody)(urlQuery, reqRawBody || void 0) : void 0;
|
|
103
|
+
if (queryBody) {
|
|
104
|
+
reqRawBody = queryBody.rawBody;
|
|
105
|
+
reqBodyType = queryBody.bodyType;
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
state.replied = true;
|
|
109
|
+
fatalFail(res, state, respHeaders, err);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const context = (0, _mionjs_router.createContextFromChain)(chain, path, urlQuery, reqHeaders, respHeaders, reqRawBody, reqBodyType);
|
|
113
|
+
let dispatched;
|
|
114
|
+
try {
|
|
115
|
+
dispatched = (0, _mionjs_router.dispatchMaybeSync)(context, rawRequest, res);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
failWith(err);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (dispatched instanceof Promise) answerWith(dispatched);
|
|
121
|
+
else answer(dispatched);
|
|
122
|
+
};
|
|
123
|
+
const answer = (mionResponse) => {
|
|
124
|
+
if (state.replied) return;
|
|
125
|
+
state.replied = true;
|
|
126
|
+
try {
|
|
127
|
+
reply(res, state, mionResponse);
|
|
128
|
+
} catch {}
|
|
129
|
+
};
|
|
130
|
+
const failWith = (err) => {
|
|
131
|
+
if (state.replied) return;
|
|
132
|
+
state.replied = true;
|
|
133
|
+
fatalFail(res, state, respHeaders, (0, _mionjs_router.toRpcError)(err));
|
|
134
|
+
};
|
|
135
|
+
const answerWith = (dispatched) => {
|
|
136
|
+
dispatched.then(answer).catch(failWith);
|
|
137
|
+
};
|
|
138
|
+
if (!chain.readsBody) {
|
|
139
|
+
drainRequestBody(res);
|
|
140
|
+
dispatchBody("", false);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
res.collectBody(chain.maxBodySize, (fullBody) => {
|
|
144
|
+
if (state.replied) return;
|
|
145
|
+
if (fullBody === null) {
|
|
146
|
+
answerWith((0, _mionjs_router.dispatchPlatformError)(chain, path, urlQuery, (0, _mionjs_router.requestPayloadTooLarge)(), reqHeaders, respHeaders, rawRequest, res));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (fullBody.byteLength <= UWS_MAX_SINGLE_READ) {
|
|
150
|
+
dispatchBody(fullBody.byteLength === 0 ? "" : Buffer.from(fullBody).toString(), true);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
queueMicrotask(() => {
|
|
154
|
+
if (state.replied) return;
|
|
155
|
+
if (fullBody.byteLength === 0) {
|
|
156
|
+
state.replied = true;
|
|
157
|
+
fatalFail(res, state, respHeaders, new _mionjs_core.FatalError({
|
|
158
|
+
publicMessage: "Internal Server Error",
|
|
159
|
+
type: "unknown-error",
|
|
160
|
+
errorData: { reason: "uws detached a multi-read body buffer (upstream behavior change) — report to mion" }
|
|
161
|
+
}));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
dispatchBody(releaseAfterDecode(fullBody), true);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** Frees an ownership-transferred body buffer right after decoding it; the single-read window must never come through here.
|
|
169
|
+
* `transfer(0)` and not `transfer()`: measured on node 26, only `transfer(0)` releases the backing store there and then.
|
|
170
|
+
* Guarded because a future uWS could hand back a buffer that cannot be transferred, which must not fail the request. */
|
|
171
|
+
function releaseAfterDecode(fullBody) {
|
|
172
|
+
const text = Buffer.from(fullBody).toString();
|
|
173
|
+
try {
|
|
174
|
+
fullBody.transfer?.(0);
|
|
175
|
+
} catch {}
|
|
176
|
+
return text;
|
|
177
|
+
}
|
|
178
|
+
function fatalFail(res, state, respHeaders, error) {
|
|
179
|
+
reply(res, state, (0, _mionjs_router.getRouterFatalErrorResponse)(error, respHeaders));
|
|
180
|
+
}
|
|
181
|
+
function isHeaderSafe(text) {
|
|
182
|
+
for (let i = 0; i < text.length; i++) {
|
|
183
|
+
const code = text.charCodeAt(i);
|
|
184
|
+
if (code === 13 || code === 10 || code === 0) return false;
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
/** The line is a pure function of the code, so it is built once per code rather than per response. */
|
|
189
|
+
var statusLines = /* @__PURE__ */ new Map();
|
|
190
|
+
function statusLine(statusCode) {
|
|
191
|
+
const cached = statusLines.get(statusCode);
|
|
192
|
+
if (cached !== void 0) return cached;
|
|
193
|
+
const statusText = http.STATUS_CODES[statusCode];
|
|
194
|
+
const line = statusText ? `${statusCode} ${statusText}` : `${statusCode}`;
|
|
195
|
+
statusLines.set(statusCode, line);
|
|
196
|
+
return line;
|
|
197
|
+
}
|
|
198
|
+
function reply(res, state, mionResp) {
|
|
199
|
+
if (state.aborted) return;
|
|
200
|
+
const headers = mionResp.headers;
|
|
201
|
+
const payload = JSON.stringify(mionResp.body);
|
|
202
|
+
const write = (name, value) => {
|
|
203
|
+
if (isHeaderSafe(value)) res.writeHeader(name, value);
|
|
204
|
+
};
|
|
205
|
+
res.cork(() => {
|
|
206
|
+
res.writeStatus(statusLine(mionResp.statusCode));
|
|
207
|
+
headers.forEach((name, value) => {
|
|
208
|
+
if (name === "content-length" || name === "content-type" || !isHeaderSafe(name)) return;
|
|
209
|
+
if (typeof value === "string") write(name, value);
|
|
210
|
+
else if (name === "set-cookie") for (const cookie of value) write(name, cookie);
|
|
211
|
+
else write(name, value.join(", "));
|
|
212
|
+
});
|
|
213
|
+
res.writeHeader("content-type", mionResp.contentType);
|
|
214
|
+
res.end(payload);
|
|
215
|
+
});
|
|
216
|
+
(0, _mionjs_router.releaseHeaders)(headers);
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
exports.resetUwsHttpOpts = resetUwsHttpOpts;
|
|
220
|
+
exports.setUwsHttpOpts = setUwsHttpOpts;
|
|
221
|
+
exports.startUwsServer = startUwsServer;
|
|
222
|
+
exports.uwsRequestHandler = uwsRequestHandler;
|
|
223
|
+
|
|
224
|
+
//# sourceMappingURL=uwsHttp.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uwsHttp.cjs","names":[],"sources":["../../../src/uwsHttp.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {\n dispatchMaybeSync,\n dispatchPlatformError,\n resolveExecutionChain,\n createContextFromChain,\n getRouterFatalErrorResponse,\n toRpcError,\n resetRouter,\n decodeQueryBody,\n setPlatformConfig,\n getDefaultHeaders,\n releaseHeaders,\n requestPayloadTooLarge,\n hostOwnsSocket,\n} from '@mionjs/router';\nimport {STATUS_CODES} from 'http';\nimport {loadUws} from '@mionjs/bin-uws';\nimport type {HttpRequest, HttpResponse, TemplatedApp, us_listen_socket} from '@mionjs/bin-uws';\nimport {DEFAULT_UWS_HTTP_OPTIONS} from './constants.ts';\nimport type {UwsHttpOptions} from './types.ts';\nimport type {MethodsExecutionChain, MionHeaders, MionResponse, RecordResponseHeaders} from '@mionjs/router';\nimport {getENV, SerializerModes} from '@mionjs/core';\nimport type {SerializerCode} from '@mionjs/core';\nimport {RpcError, FatalError} from '@mionjs/core';\nimport {headersFromUwsRequest} from './headers.ts';\n\n// ############# PRIVATE STATE #############\n\nlet httpOptions: Readonly<UwsHttpOptions> = {...DEFAULT_UWS_HTTP_OPTIONS};\n\n// The most bytes one uWS socket read can deliver: uSockets' LIBUS_RECV_BUFFER_LENGTH (512 KiB) at the tag pinned in\n// packages/bin-uws. A LARGER body cannot be single-read, which is what makes the zero-copy branch below safe (a\n// detachment tripwire guards it at runtime). Re-verify against uSockets on a tag bump.\nconst UWS_MAX_SINGLE_READ = 524288;\n\n/** The running server: the uWS app plus the socket handle listen() produced. */\nexport interface UwsServer {\n app: TemplatedApp;\n listenSocket: us_listen_socket;\n close(): void;\n}\n\n// ############# PUBLIC METHODS #############\n\nexport function resetUwsHttpOpts() {\n httpOptions = {...DEFAULT_UWS_HTTP_OPTIONS};\n resetRouter();\n}\n\nexport function setUwsHttpOpts(options?: Partial<UwsHttpOptions>) {\n httpOptions = {\n ...httpOptions,\n ...options,\n };\n\n return httpOptions;\n}\n\n/** The platform config the router publishes: everything but the TLS file paths. */\nfunction serializablePlatformConfig(): Record<string, unknown> {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const {ssl: _ssl, ...serializableConfig} = httpOptions;\n return serializableConfig;\n}\n\nexport async function startUwsServer(options?: Partial<UwsHttpOptions>): Promise<UwsServer> {\n const isTest = getENV('NODE_ENV') === 'test';\n\n if (options) setUwsHttpOpts(options);\n // uWS is its own C++ event loop and owns its listen socket, so it cannot mount on a host node server.\n if (hostOwnsSocket()) {\n throw new Error(\n '@mionjs/platform-uws does not support middleware mode: uWebSockets.js owns its own listen ' +\n 'socket and cannot mount on a host node server. Use @mionjs/platform-node for middleware mode.'\n );\n }\n const protocol = httpOptions.ssl ? 'https' : 'http';\n const port = httpOptions.port !== 80 ? `:${httpOptions.port}` : '';\n const url = `${protocol}://localhost${port}`;\n if (!isTest)\n console.log(`mion uws server running on ${url}`, {\n port: httpOptions.port,\n httpOptions,\n });\n\n const uws = loadUws();\n const app = httpOptions.ssl ? uws.SSLApp(httpOptions.ssl) : uws.App();\n app.any('/*', uwsRequestHandler);\n\n return new Promise<UwsServer>((resolve, reject) => {\n app.listen(httpOptions.port, (listenSocket) => {\n if (!listenSocket) {\n reject(new Error(`mion uws server failed to listen on port ${httpOptions.port} (port in use?)`));\n return;\n }\n\n setPlatformConfig(serializablePlatformConfig());\n\n let closed = false;\n const close = () => {\n if (closed) return;\n closed = true;\n uws.us_listen_socket_close(listenSocket);\n };\n const server: UwsServer = {app, listenSocket, close};\n\n const shutdownHandler = function () {\n if (!isTest) console.log(`Shutting down mion server on ${url}`);\n close();\n process.exit(0);\n };\n process.on('SIGINT', shutdownHandler);\n process.on('SIGTERM', shutdownHandler);\n\n resolve(server);\n });\n });\n}\n\n// ############# PRIVATE METHODS #############\n\n// uWS refreshes a socket's idle timeout only from inside a body-data callback, and runs it only when a reader is registered.\n// Without one, a request answered before its body finished arriving is closed mid-upload, so drop every chunk instead.\nfunction drainRequestBody(res: HttpResponse) {\n res.onData(() => {});\n}\n\n// exported for tests and for mounting on a hand-built uWS app; NOT a middleware handler (see startUwsServer).\n// uWS contract: `req` is valid only synchronously here, so the async dispatch snapshots it before the first await.\nexport function uwsRequestHandler(res: HttpResponse, req: HttpRequest): void {\n const state = {replied: false, aborted: false};\n // Everything read from `req` happens HERE, synchronously.\n const path = req.getUrl();\n const query = req.getQuery();\n const urlQuery = query === '' ? undefined : query;\n const reqHeaders = headersFromUwsRequest(req);\n\n const respHeaders = getDefaultHeaders();\n\n // must be registered before any async work: after a disconnect uWS frees the response, and touching it would crash\n res.onAborted(() => {\n state.aborted = true;\n });\n\n // route resolved BEFORE the body, synchronously: the native read stops at the chain's limit, and a late context stays GC-cheap.\n // The raw request object is built once: the one a pathTransform reads and the one the handlers see.\n const rawRequest = {path, urlQuery, headers: reqHeaders};\n let chain: MethodsExecutionChain;\n try {\n chain = resolveExecutionChain(path, urlQuery, rawRequest);\n } catch (err) {\n drainRequestBody(res);\n state.replied = true;\n fatalFail(res, state, respHeaders, toRpcError(err));\n return;\n }\n\n const dispatchBody = (rawBody: string, readQueryBody: boolean) => {\n let reqRawBody: any = rawBody;\n let reqBodyType: SerializerCode = SerializerModes.stringifyJson;\n // a throw here runs inside uWS' native callback (or a microtask): it must become a response\n try {\n const queryBody = readQueryBody ? decodeQueryBody(urlQuery, reqRawBody || undefined) : undefined;\n if (queryBody) {\n reqRawBody = queryBody.rawBody;\n reqBodyType = queryBody.bodyType;\n }\n } catch (err) {\n state.replied = true;\n fatalFail(res, state, respHeaders, err as RpcError<string>);\n return;\n }\n\n const context = createContextFromChain(chain, path, urlQuery, reqHeaders, respHeaders, reqRawBody, reqBodyType);\n let dispatched: MionResponse | Promise<MionResponse>;\n try {\n dispatched = dispatchMaybeSync(context, rawRequest, res);\n } catch (err) {\n failWith(err);\n return;\n }\n // an all-sync chain answers inside uWS' own callback, with no promise in between\n if (dispatched instanceof Promise) answerWith(dispatched);\n else answer(dispatched);\n };\n\n const answer = (mionResponse: MionResponse) => {\n if (state.replied) return;\n state.replied = true;\n try {\n reply(res, state, mionResponse);\n } catch {\n // already marked replied: a failed write has nothing left to answer with\n }\n };\n\n const failWith = (err: unknown) => {\n if (state.replied) return;\n state.replied = true;\n fatalFail(res, state, respHeaders, toRpcError(err));\n };\n\n const answerWith = (dispatched: Promise<MionResponse>) => {\n dispatched.then(answer).catch(failWith);\n };\n\n // collectBody assembles the body natively (uWS' onDataV2 preallocates) and calls back ONCE, with null past maxSize,\n // which is exactly the maxBodySize contract. A not-found chain has no route to feed: its body is dropped as it arrives.\n if (!chain.readsBody) {\n drainRequestBody(res);\n dispatchBody('', false);\n return;\n }\n\n res.collectBody(chain.maxBodySize, (fullBody) => {\n if (state.replied) return;\n if (fullBody === null) {\n // the route resolved, so the refusal still runs the chain's alwaysRun members\n answerWith(\n dispatchPlatformError(chain, path, urlQuery, requestPayloadTooLarge(), reqHeaders, respHeaders, rawRequest, res)\n );\n return;\n }\n\n // collectBody has two paths (verified in the pinned tag's HttpResponseWrapper.h and by test): a single-read body is a\n // zero-copy window into uWS' receive buffer, DETACHED when this callback returns, so it must be decoded synchronously\n // here; a multi-read body was assembled in C++ and OWNERSHIP-TRANSFERRED to JS, so it can be decoded from a microtask.\n if (fullBody.byteLength <= UWS_MAX_SINGLE_READ) {\n // a body-less request (every GET) skips the view and the decode altogether\n dispatchBody(fullBody.byteLength === 0 ? '' : Buffer.from(fullBody).toString(), true);\n return;\n }\n // bigger than one read can deliver → guaranteed the ownership-transferred path, so the buffer survives the microtask.\n // The zero-length check is a tripwire for an upstream change: fail loudly instead of parsing a neutered buffer.\n queueMicrotask(() => {\n if (state.replied) return;\n if (fullBody.byteLength === 0) {\n state.replied = true;\n const error = new FatalError({\n publicMessage: 'Internal Server Error',\n type: 'unknown-error',\n errorData: {reason: 'uws detached a multi-read body buffer (upstream behavior change) — report to mion'},\n });\n fatalFail(res, state, respHeaders, error);\n return;\n }\n dispatchBody(releaseAfterDecode(fullBody), true);\n });\n });\n}\n\n/** Frees an ownership-transferred body buffer right after decoding it; the single-read window must never come through here.\n * `transfer(0)` and not `transfer()`: measured on node 26, only `transfer(0)` releases the backing store there and then.\n * Guarded because a future uWS could hand back a buffer that cannot be transferred, which must not fail the request. */\nfunction releaseAfterDecode(fullBody: ArrayBuffer): string {\n const text = Buffer.from(fullBody).toString();\n try {\n (fullBody as ReleasableBuffer).transfer?.(0);\n } catch {\n // not transferable: the collector gets it instead, exactly as before\n }\n return text;\n}\n\n/** `transfer` is ES2024 and this package compiles against an ES2023 lib; optional because a runtime without it must work. */\ntype ReleasableBuffer = ArrayBuffer & {transfer?: (newByteLength?: number) => ArrayBuffer};\n\n// only called when there is an http error or weird unhandled route errors\nfunction fatalFail(res: HttpResponse, state: {aborted: boolean}, respHeaders: MionHeaders, error: RpcError<string>) {\n const routeResponse = getRouterFatalErrorResponse(error, respHeaders);\n reply(res, state, routeResponse);\n}\n\nfunction isHeaderSafe(text: string): boolean {\n // one pass, no allocation: a CR, LF or NUL anywhere makes the header unsafe to write\n for (let i = 0; i < text.length; i++) {\n const code = text.charCodeAt(i);\n if (code === 13 || code === 10 || code === 0) return false;\n }\n return true;\n}\n\n/** The line is a pure function of the code, so it is built once per code rather than per response. */\nconst statusLines = new Map<number, string>();\nfunction statusLine(statusCode: number): string {\n const cached = statusLines.get(statusCode);\n if (cached !== undefined) return cached;\n const statusText = STATUS_CODES[statusCode];\n const line = statusText ? `${statusCode} ${statusText}` : `${statusCode}`;\n statusLines.set(statusCode, line);\n return line;\n}\n\nfunction reply(res: HttpResponse, state: {aborted: boolean}, mionResp: MionResponse) {\n // uWS freed the response when the client left; touching it would crash\n if (state.aborted) return;\n const headers = mionResp.headers as RecordResponseHeaders;\n\n // serialized BEFORE the cork: uWS warns a cork buffer must not be held across event loop iterations\n const payload = JSON.stringify(mionResp.body);\n\n // cork makes one syscall; uWS headers are write-only and must all precede end().\n // content-length is skipped: uWS writes its own, and a duplicate corrupts the response.\n // uWS writes headers unchecked, so an echoed CR or LF would be header injection: dropped\n const write = (name: string, value: string) => {\n if (isHeaderSafe(value)) res.writeHeader(name, value);\n };\n res.cork(() => {\n res.writeStatus(statusLine(mionResp.statusCode));\n headers.forEach((name, value) => {\n if (name === 'content-length' || name === 'content-type' || !isHeaderSafe(name)) return;\n if (typeof value === 'string') write(name, value);\n // RFC 6265 forbids folding set-cookie; uWS writes a repeated name as its own line\n else if (name === 'set-cookie') for (const cookie of value) write(name, cookie);\n else write(name, value.join(', '));\n });\n // last, and the store's own skipped above: uWS appends a repeated name instead of replacing it\n res.writeHeader('content-type', mionResp.contentType);\n\n res.end(payload);\n });\n releaseHeaders(headers);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HttpRequest, HttpResponse, TemplatedApp, us_listen_socket } from '@mionjs/bin-uws';
|
|
2
|
+
import { UwsHttpOptions } from './types.ts';
|
|
3
|
+
export interface UwsServer {
|
|
4
|
+
app: TemplatedApp;
|
|
5
|
+
listenSocket: us_listen_socket;
|
|
6
|
+
close(): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function resetUwsHttpOpts(): void;
|
|
9
|
+
export declare function setUwsHttpOpts(options?: Partial<UwsHttpOptions>): Readonly<UwsHttpOptions>;
|
|
10
|
+
export declare function startUwsServer(options?: Partial<UwsHttpOptions>): Promise<UwsServer>;
|
|
11
|
+
export declare function uwsRequestHandler(res: HttpResponse, req: HttpRequest): void;
|
|
12
|
+
//# sourceMappingURL=uwsHttp.d.ts.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { DEFAULT_UWS_HTTP_OPTIONS } from "./src/constants.js";
|
|
2
|
+
import { resetUwsHttpOpts, setUwsHttpOpts, startUwsServer, uwsRequestHandler } from "./src/uwsHttp.js";
|
|
3
|
+
export { DEFAULT_UWS_HTTP_OPTIONS, resetUwsHttpOpts, setUwsHttpOpts, startUwsServer, uwsRequestHandler };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DEFAULT_MAX_BODY_SIZE } from "@mionjs/core";
|
|
2
|
+
//#region src/constants.ts
|
|
3
|
+
var DEFAULT_UWS_HTTP_OPTIONS = {
|
|
4
|
+
port: 80,
|
|
5
|
+
/** What a route takes when its own `maxBodySize` and its types say nothing: the shared 128 KB default */
|
|
6
|
+
maxBodySize: DEFAULT_MAX_BODY_SIZE
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { DEFAULT_UWS_HTTP_OPTIONS };
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../../../src/constants.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {DEFAULT_MAX_BODY_SIZE} from '@mionjs/core';\nimport {UwsHttpOptions} from './types.ts';\n\nexport const DEFAULT_UWS_HTTP_OPTIONS: UwsHttpOptions = {\n port: 80,\n /** What a route takes when its own `maxBodySize` and its types say nothing: the shared 128 KB default */\n maxBodySize: DEFAULT_MAX_BODY_SIZE,\n};\n"],"mappings":";;AAUA,IAAa,2BAA2C;CACtD,MAAM;;CAEN,aAAa;AACf"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { headersFromRecord } from "@mionjs/router";
|
|
2
|
+
//#region src/headers.ts
|
|
3
|
+
/** uWS headers are readable only synchronously inside the handler, so snapshot them; uWS already lower-cases the names. */
|
|
4
|
+
function headersFromUwsRequest(req) {
|
|
5
|
+
const record = {};
|
|
6
|
+
req.forEach((name, value) => {
|
|
7
|
+
const previous = record[name];
|
|
8
|
+
record[name] = typeof previous === "string" ? `${previous}, ${value}` : value;
|
|
9
|
+
});
|
|
10
|
+
return headersFromRecord(record, true);
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { headersFromUwsRequest };
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.js","names":[],"sources":["../../../src/headers.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {MionHeaders, headersFromRecord} from '@mionjs/router';\nimport type {HttpRequest} from '@mionjs/bin-uws';\n\n/** uWS headers are readable only synchronously inside the handler, so snapshot them; uWS already lower-cases the names. */\nexport function headersFromUwsRequest(req: HttpRequest): MionHeaders {\n const record: Record<string, string> = {};\n req.forEach((name, value) => {\n // a string check, not undefined: a `constructor` header would otherwise find Object.prototype's function\n const previous = record[name];\n record[name] = typeof previous === 'string' ? `${previous}, ${value}` : value;\n });\n return headersFromRecord(record, true);\n}\n"],"mappings":";;;AAWA,SAAgB,sBAAsB,KAA+B;CACnE,MAAM,SAAiC,CAAC;CACxC,IAAI,SAAS,MAAM,UAAU;EAE3B,MAAM,WAAW,OAAO;EACxB,OAAO,QAAQ,OAAO,aAAa,WAAW,GAAG,SAAS,IAAI,UAAU;CAC1E,CAAC;CACD,OAAO,kBAAkB,QAAQ,IAAI;AACvC"}
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HttpRequest, HttpResponse, TemplatedApp, us_listen_socket } from '@mionjs/bin-uws';
|
|
2
|
+
import { UwsHttpOptions } from './types.ts';
|
|
3
|
+
export interface UwsServer {
|
|
4
|
+
app: TemplatedApp;
|
|
5
|
+
listenSocket: us_listen_socket;
|
|
6
|
+
close(): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function resetUwsHttpOpts(): void;
|
|
9
|
+
export declare function setUwsHttpOpts(options?: Partial<UwsHttpOptions>): Readonly<UwsHttpOptions>;
|
|
10
|
+
export declare function startUwsServer(options?: Partial<UwsHttpOptions>): Promise<UwsServer>;
|
|
11
|
+
export declare function uwsRequestHandler(res: HttpResponse, req: HttpRequest): void;
|
|
12
|
+
//# sourceMappingURL=uwsHttp.d.ts.map
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { DEFAULT_UWS_HTTP_OPTIONS } from "./constants.js";
|
|
2
|
+
import { headersFromUwsRequest } from "./headers.js";
|
|
3
|
+
import { createContextFromChain, decodeQueryBody, dispatchMaybeSync, dispatchPlatformError, getDefaultHeaders, getRouterFatalErrorResponse, hostOwnsSocket, releaseHeaders, requestPayloadTooLarge, resetRouter, resolveExecutionChain, setPlatformConfig, toRpcError } from "@mionjs/router";
|
|
4
|
+
import { STATUS_CODES } from "http";
|
|
5
|
+
import { loadUws } from "@mionjs/bin-uws";
|
|
6
|
+
import { FatalError, SerializerModes, getENV } from "@mionjs/core";
|
|
7
|
+
//#region src/uwsHttp.ts
|
|
8
|
+
var httpOptions = { ...DEFAULT_UWS_HTTP_OPTIONS };
|
|
9
|
+
var UWS_MAX_SINGLE_READ = 524288;
|
|
10
|
+
function resetUwsHttpOpts() {
|
|
11
|
+
httpOptions = { ...DEFAULT_UWS_HTTP_OPTIONS };
|
|
12
|
+
resetRouter();
|
|
13
|
+
}
|
|
14
|
+
function setUwsHttpOpts(options) {
|
|
15
|
+
httpOptions = {
|
|
16
|
+
...httpOptions,
|
|
17
|
+
...options
|
|
18
|
+
};
|
|
19
|
+
return httpOptions;
|
|
20
|
+
}
|
|
21
|
+
/** The platform config the router publishes: everything but the TLS file paths. */
|
|
22
|
+
function serializablePlatformConfig() {
|
|
23
|
+
const { ssl: _ssl, ...serializableConfig } = httpOptions;
|
|
24
|
+
return serializableConfig;
|
|
25
|
+
}
|
|
26
|
+
async function startUwsServer(options) {
|
|
27
|
+
const isTest = getENV("NODE_ENV") === "test";
|
|
28
|
+
if (options) setUwsHttpOpts(options);
|
|
29
|
+
if (hostOwnsSocket()) throw new Error("@mionjs/platform-uws does not support middleware mode: uWebSockets.js owns its own listen socket and cannot mount on a host node server. Use @mionjs/platform-node for middleware mode.");
|
|
30
|
+
const url = `${httpOptions.ssl ? "https" : "http"}://localhost${httpOptions.port !== 80 ? `:${httpOptions.port}` : ""}`;
|
|
31
|
+
if (!isTest) console.log(`mion uws server running on ${url}`, {
|
|
32
|
+
port: httpOptions.port,
|
|
33
|
+
httpOptions
|
|
34
|
+
});
|
|
35
|
+
const uws = loadUws();
|
|
36
|
+
const app = httpOptions.ssl ? uws.SSLApp(httpOptions.ssl) : uws.App();
|
|
37
|
+
app.any("/*", uwsRequestHandler);
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
app.listen(httpOptions.port, (listenSocket) => {
|
|
40
|
+
if (!listenSocket) {
|
|
41
|
+
reject(/* @__PURE__ */ new Error(`mion uws server failed to listen on port ${httpOptions.port} (port in use?)`));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setPlatformConfig(serializablePlatformConfig());
|
|
45
|
+
let closed = false;
|
|
46
|
+
const close = () => {
|
|
47
|
+
if (closed) return;
|
|
48
|
+
closed = true;
|
|
49
|
+
uws.us_listen_socket_close(listenSocket);
|
|
50
|
+
};
|
|
51
|
+
const server = {
|
|
52
|
+
app,
|
|
53
|
+
listenSocket,
|
|
54
|
+
close
|
|
55
|
+
};
|
|
56
|
+
const shutdownHandler = function() {
|
|
57
|
+
if (!isTest) console.log(`Shutting down mion server on ${url}`);
|
|
58
|
+
close();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
};
|
|
61
|
+
process.on("SIGINT", shutdownHandler);
|
|
62
|
+
process.on("SIGTERM", shutdownHandler);
|
|
63
|
+
resolve(server);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function drainRequestBody(res) {
|
|
68
|
+
res.onData(() => {});
|
|
69
|
+
}
|
|
70
|
+
function uwsRequestHandler(res, req) {
|
|
71
|
+
const state = {
|
|
72
|
+
replied: false,
|
|
73
|
+
aborted: false
|
|
74
|
+
};
|
|
75
|
+
const path = req.getUrl();
|
|
76
|
+
const query = req.getQuery();
|
|
77
|
+
const urlQuery = query === "" ? void 0 : query;
|
|
78
|
+
const reqHeaders = headersFromUwsRequest(req);
|
|
79
|
+
const respHeaders = getDefaultHeaders();
|
|
80
|
+
res.onAborted(() => {
|
|
81
|
+
state.aborted = true;
|
|
82
|
+
});
|
|
83
|
+
const rawRequest = {
|
|
84
|
+
path,
|
|
85
|
+
urlQuery,
|
|
86
|
+
headers: reqHeaders
|
|
87
|
+
};
|
|
88
|
+
let chain;
|
|
89
|
+
try {
|
|
90
|
+
chain = resolveExecutionChain(path, urlQuery, rawRequest);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
drainRequestBody(res);
|
|
93
|
+
state.replied = true;
|
|
94
|
+
fatalFail(res, state, respHeaders, toRpcError(err));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const dispatchBody = (rawBody, readQueryBody) => {
|
|
98
|
+
let reqRawBody = rawBody;
|
|
99
|
+
let reqBodyType = SerializerModes.stringifyJson;
|
|
100
|
+
try {
|
|
101
|
+
const queryBody = readQueryBody ? decodeQueryBody(urlQuery, reqRawBody || void 0) : void 0;
|
|
102
|
+
if (queryBody) {
|
|
103
|
+
reqRawBody = queryBody.rawBody;
|
|
104
|
+
reqBodyType = queryBody.bodyType;
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
state.replied = true;
|
|
108
|
+
fatalFail(res, state, respHeaders, err);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const context = createContextFromChain(chain, path, urlQuery, reqHeaders, respHeaders, reqRawBody, reqBodyType);
|
|
112
|
+
let dispatched;
|
|
113
|
+
try {
|
|
114
|
+
dispatched = dispatchMaybeSync(context, rawRequest, res);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
failWith(err);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (dispatched instanceof Promise) answerWith(dispatched);
|
|
120
|
+
else answer(dispatched);
|
|
121
|
+
};
|
|
122
|
+
const answer = (mionResponse) => {
|
|
123
|
+
if (state.replied) return;
|
|
124
|
+
state.replied = true;
|
|
125
|
+
try {
|
|
126
|
+
reply(res, state, mionResponse);
|
|
127
|
+
} catch {}
|
|
128
|
+
};
|
|
129
|
+
const failWith = (err) => {
|
|
130
|
+
if (state.replied) return;
|
|
131
|
+
state.replied = true;
|
|
132
|
+
fatalFail(res, state, respHeaders, toRpcError(err));
|
|
133
|
+
};
|
|
134
|
+
const answerWith = (dispatched) => {
|
|
135
|
+
dispatched.then(answer).catch(failWith);
|
|
136
|
+
};
|
|
137
|
+
if (!chain.readsBody) {
|
|
138
|
+
drainRequestBody(res);
|
|
139
|
+
dispatchBody("", false);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
res.collectBody(chain.maxBodySize, (fullBody) => {
|
|
143
|
+
if (state.replied) return;
|
|
144
|
+
if (fullBody === null) {
|
|
145
|
+
answerWith(dispatchPlatformError(chain, path, urlQuery, requestPayloadTooLarge(), reqHeaders, respHeaders, rawRequest, res));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (fullBody.byteLength <= UWS_MAX_SINGLE_READ) {
|
|
149
|
+
dispatchBody(fullBody.byteLength === 0 ? "" : Buffer.from(fullBody).toString(), true);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
queueMicrotask(() => {
|
|
153
|
+
if (state.replied) return;
|
|
154
|
+
if (fullBody.byteLength === 0) {
|
|
155
|
+
state.replied = true;
|
|
156
|
+
fatalFail(res, state, respHeaders, new FatalError({
|
|
157
|
+
publicMessage: "Internal Server Error",
|
|
158
|
+
type: "unknown-error",
|
|
159
|
+
errorData: { reason: "uws detached a multi-read body buffer (upstream behavior change) — report to mion" }
|
|
160
|
+
}));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
dispatchBody(releaseAfterDecode(fullBody), true);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** Frees an ownership-transferred body buffer right after decoding it; the single-read window must never come through here.
|
|
168
|
+
* `transfer(0)` and not `transfer()`: measured on node 26, only `transfer(0)` releases the backing store there and then.
|
|
169
|
+
* Guarded because a future uWS could hand back a buffer that cannot be transferred, which must not fail the request. */
|
|
170
|
+
function releaseAfterDecode(fullBody) {
|
|
171
|
+
const text = Buffer.from(fullBody).toString();
|
|
172
|
+
try {
|
|
173
|
+
fullBody.transfer?.(0);
|
|
174
|
+
} catch {}
|
|
175
|
+
return text;
|
|
176
|
+
}
|
|
177
|
+
function fatalFail(res, state, respHeaders, error) {
|
|
178
|
+
reply(res, state, getRouterFatalErrorResponse(error, respHeaders));
|
|
179
|
+
}
|
|
180
|
+
function isHeaderSafe(text) {
|
|
181
|
+
for (let i = 0; i < text.length; i++) {
|
|
182
|
+
const code = text.charCodeAt(i);
|
|
183
|
+
if (code === 13 || code === 10 || code === 0) return false;
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
/** The line is a pure function of the code, so it is built once per code rather than per response. */
|
|
188
|
+
var statusLines = /* @__PURE__ */ new Map();
|
|
189
|
+
function statusLine(statusCode) {
|
|
190
|
+
const cached = statusLines.get(statusCode);
|
|
191
|
+
if (cached !== void 0) return cached;
|
|
192
|
+
const statusText = STATUS_CODES[statusCode];
|
|
193
|
+
const line = statusText ? `${statusCode} ${statusText}` : `${statusCode}`;
|
|
194
|
+
statusLines.set(statusCode, line);
|
|
195
|
+
return line;
|
|
196
|
+
}
|
|
197
|
+
function reply(res, state, mionResp) {
|
|
198
|
+
if (state.aborted) return;
|
|
199
|
+
const headers = mionResp.headers;
|
|
200
|
+
const payload = JSON.stringify(mionResp.body);
|
|
201
|
+
const write = (name, value) => {
|
|
202
|
+
if (isHeaderSafe(value)) res.writeHeader(name, value);
|
|
203
|
+
};
|
|
204
|
+
res.cork(() => {
|
|
205
|
+
res.writeStatus(statusLine(mionResp.statusCode));
|
|
206
|
+
headers.forEach((name, value) => {
|
|
207
|
+
if (name === "content-length" || name === "content-type" || !isHeaderSafe(name)) return;
|
|
208
|
+
if (typeof value === "string") write(name, value);
|
|
209
|
+
else if (name === "set-cookie") for (const cookie of value) write(name, cookie);
|
|
210
|
+
else write(name, value.join(", "));
|
|
211
|
+
});
|
|
212
|
+
res.writeHeader("content-type", mionResp.contentType);
|
|
213
|
+
res.end(payload);
|
|
214
|
+
});
|
|
215
|
+
releaseHeaders(headers);
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
export { resetUwsHttpOpts, setUwsHttpOpts, startUwsServer, uwsRequestHandler };
|
|
219
|
+
|
|
220
|
+
//# sourceMappingURL=uwsHttp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uwsHttp.js","names":[],"sources":["../../../src/uwsHttp.ts"],"sourcesContent":["/* ########\n * 2022 mion\n * Author: Ma-jerez\n * License: MIT\n * The software is provided \"as is\", without warranty of any kind.\n * ######## */\n\nimport {\n dispatchMaybeSync,\n dispatchPlatformError,\n resolveExecutionChain,\n createContextFromChain,\n getRouterFatalErrorResponse,\n toRpcError,\n resetRouter,\n decodeQueryBody,\n setPlatformConfig,\n getDefaultHeaders,\n releaseHeaders,\n requestPayloadTooLarge,\n hostOwnsSocket,\n} from '@mionjs/router';\nimport {STATUS_CODES} from 'http';\nimport {loadUws} from '@mionjs/bin-uws';\nimport type {HttpRequest, HttpResponse, TemplatedApp, us_listen_socket} from '@mionjs/bin-uws';\nimport {DEFAULT_UWS_HTTP_OPTIONS} from './constants.ts';\nimport type {UwsHttpOptions} from './types.ts';\nimport type {MethodsExecutionChain, MionHeaders, MionResponse, RecordResponseHeaders} from '@mionjs/router';\nimport {getENV, SerializerModes} from '@mionjs/core';\nimport type {SerializerCode} from '@mionjs/core';\nimport {RpcError, FatalError} from '@mionjs/core';\nimport {headersFromUwsRequest} from './headers.ts';\n\n// ############# PRIVATE STATE #############\n\nlet httpOptions: Readonly<UwsHttpOptions> = {...DEFAULT_UWS_HTTP_OPTIONS};\n\n// The most bytes one uWS socket read can deliver: uSockets' LIBUS_RECV_BUFFER_LENGTH (512 KiB) at the tag pinned in\n// packages/bin-uws. A LARGER body cannot be single-read, which is what makes the zero-copy branch below safe (a\n// detachment tripwire guards it at runtime). Re-verify against uSockets on a tag bump.\nconst UWS_MAX_SINGLE_READ = 524288;\n\n/** The running server: the uWS app plus the socket handle listen() produced. */\nexport interface UwsServer {\n app: TemplatedApp;\n listenSocket: us_listen_socket;\n close(): void;\n}\n\n// ############# PUBLIC METHODS #############\n\nexport function resetUwsHttpOpts() {\n httpOptions = {...DEFAULT_UWS_HTTP_OPTIONS};\n resetRouter();\n}\n\nexport function setUwsHttpOpts(options?: Partial<UwsHttpOptions>) {\n httpOptions = {\n ...httpOptions,\n ...options,\n };\n\n return httpOptions;\n}\n\n/** The platform config the router publishes: everything but the TLS file paths. */\nfunction serializablePlatformConfig(): Record<string, unknown> {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const {ssl: _ssl, ...serializableConfig} = httpOptions;\n return serializableConfig;\n}\n\nexport async function startUwsServer(options?: Partial<UwsHttpOptions>): Promise<UwsServer> {\n const isTest = getENV('NODE_ENV') === 'test';\n\n if (options) setUwsHttpOpts(options);\n // uWS is its own C++ event loop and owns its listen socket, so it cannot mount on a host node server.\n if (hostOwnsSocket()) {\n throw new Error(\n '@mionjs/platform-uws does not support middleware mode: uWebSockets.js owns its own listen ' +\n 'socket and cannot mount on a host node server. Use @mionjs/platform-node for middleware mode.'\n );\n }\n const protocol = httpOptions.ssl ? 'https' : 'http';\n const port = httpOptions.port !== 80 ? `:${httpOptions.port}` : '';\n const url = `${protocol}://localhost${port}`;\n if (!isTest)\n console.log(`mion uws server running on ${url}`, {\n port: httpOptions.port,\n httpOptions,\n });\n\n const uws = loadUws();\n const app = httpOptions.ssl ? uws.SSLApp(httpOptions.ssl) : uws.App();\n app.any('/*', uwsRequestHandler);\n\n return new Promise<UwsServer>((resolve, reject) => {\n app.listen(httpOptions.port, (listenSocket) => {\n if (!listenSocket) {\n reject(new Error(`mion uws server failed to listen on port ${httpOptions.port} (port in use?)`));\n return;\n }\n\n setPlatformConfig(serializablePlatformConfig());\n\n let closed = false;\n const close = () => {\n if (closed) return;\n closed = true;\n uws.us_listen_socket_close(listenSocket);\n };\n const server: UwsServer = {app, listenSocket, close};\n\n const shutdownHandler = function () {\n if (!isTest) console.log(`Shutting down mion server on ${url}`);\n close();\n process.exit(0);\n };\n process.on('SIGINT', shutdownHandler);\n process.on('SIGTERM', shutdownHandler);\n\n resolve(server);\n });\n });\n}\n\n// ############# PRIVATE METHODS #############\n\n// uWS refreshes a socket's idle timeout only from inside a body-data callback, and runs it only when a reader is registered.\n// Without one, a request answered before its body finished arriving is closed mid-upload, so drop every chunk instead.\nfunction drainRequestBody(res: HttpResponse) {\n res.onData(() => {});\n}\n\n// exported for tests and for mounting on a hand-built uWS app; NOT a middleware handler (see startUwsServer).\n// uWS contract: `req` is valid only synchronously here, so the async dispatch snapshots it before the first await.\nexport function uwsRequestHandler(res: HttpResponse, req: HttpRequest): void {\n const state = {replied: false, aborted: false};\n // Everything read from `req` happens HERE, synchronously.\n const path = req.getUrl();\n const query = req.getQuery();\n const urlQuery = query === '' ? undefined : query;\n const reqHeaders = headersFromUwsRequest(req);\n\n const respHeaders = getDefaultHeaders();\n\n // must be registered before any async work: after a disconnect uWS frees the response, and touching it would crash\n res.onAborted(() => {\n state.aborted = true;\n });\n\n // route resolved BEFORE the body, synchronously: the native read stops at the chain's limit, and a late context stays GC-cheap.\n // The raw request object is built once: the one a pathTransform reads and the one the handlers see.\n const rawRequest = {path, urlQuery, headers: reqHeaders};\n let chain: MethodsExecutionChain;\n try {\n chain = resolveExecutionChain(path, urlQuery, rawRequest);\n } catch (err) {\n drainRequestBody(res);\n state.replied = true;\n fatalFail(res, state, respHeaders, toRpcError(err));\n return;\n }\n\n const dispatchBody = (rawBody: string, readQueryBody: boolean) => {\n let reqRawBody: any = rawBody;\n let reqBodyType: SerializerCode = SerializerModes.stringifyJson;\n // a throw here runs inside uWS' native callback (or a microtask): it must become a response\n try {\n const queryBody = readQueryBody ? decodeQueryBody(urlQuery, reqRawBody || undefined) : undefined;\n if (queryBody) {\n reqRawBody = queryBody.rawBody;\n reqBodyType = queryBody.bodyType;\n }\n } catch (err) {\n state.replied = true;\n fatalFail(res, state, respHeaders, err as RpcError<string>);\n return;\n }\n\n const context = createContextFromChain(chain, path, urlQuery, reqHeaders, respHeaders, reqRawBody, reqBodyType);\n let dispatched: MionResponse | Promise<MionResponse>;\n try {\n dispatched = dispatchMaybeSync(context, rawRequest, res);\n } catch (err) {\n failWith(err);\n return;\n }\n // an all-sync chain answers inside uWS' own callback, with no promise in between\n if (dispatched instanceof Promise) answerWith(dispatched);\n else answer(dispatched);\n };\n\n const answer = (mionResponse: MionResponse) => {\n if (state.replied) return;\n state.replied = true;\n try {\n reply(res, state, mionResponse);\n } catch {\n // already marked replied: a failed write has nothing left to answer with\n }\n };\n\n const failWith = (err: unknown) => {\n if (state.replied) return;\n state.replied = true;\n fatalFail(res, state, respHeaders, toRpcError(err));\n };\n\n const answerWith = (dispatched: Promise<MionResponse>) => {\n dispatched.then(answer).catch(failWith);\n };\n\n // collectBody assembles the body natively (uWS' onDataV2 preallocates) and calls back ONCE, with null past maxSize,\n // which is exactly the maxBodySize contract. A not-found chain has no route to feed: its body is dropped as it arrives.\n if (!chain.readsBody) {\n drainRequestBody(res);\n dispatchBody('', false);\n return;\n }\n\n res.collectBody(chain.maxBodySize, (fullBody) => {\n if (state.replied) return;\n if (fullBody === null) {\n // the route resolved, so the refusal still runs the chain's alwaysRun members\n answerWith(\n dispatchPlatformError(chain, path, urlQuery, requestPayloadTooLarge(), reqHeaders, respHeaders, rawRequest, res)\n );\n return;\n }\n\n // collectBody has two paths (verified in the pinned tag's HttpResponseWrapper.h and by test): a single-read body is a\n // zero-copy window into uWS' receive buffer, DETACHED when this callback returns, so it must be decoded synchronously\n // here; a multi-read body was assembled in C++ and OWNERSHIP-TRANSFERRED to JS, so it can be decoded from a microtask.\n if (fullBody.byteLength <= UWS_MAX_SINGLE_READ) {\n // a body-less request (every GET) skips the view and the decode altogether\n dispatchBody(fullBody.byteLength === 0 ? '' : Buffer.from(fullBody).toString(), true);\n return;\n }\n // bigger than one read can deliver → guaranteed the ownership-transferred path, so the buffer survives the microtask.\n // The zero-length check is a tripwire for an upstream change: fail loudly instead of parsing a neutered buffer.\n queueMicrotask(() => {\n if (state.replied) return;\n if (fullBody.byteLength === 0) {\n state.replied = true;\n const error = new FatalError({\n publicMessage: 'Internal Server Error',\n type: 'unknown-error',\n errorData: {reason: 'uws detached a multi-read body buffer (upstream behavior change) — report to mion'},\n });\n fatalFail(res, state, respHeaders, error);\n return;\n }\n dispatchBody(releaseAfterDecode(fullBody), true);\n });\n });\n}\n\n/** Frees an ownership-transferred body buffer right after decoding it; the single-read window must never come through here.\n * `transfer(0)` and not `transfer()`: measured on node 26, only `transfer(0)` releases the backing store there and then.\n * Guarded because a future uWS could hand back a buffer that cannot be transferred, which must not fail the request. */\nfunction releaseAfterDecode(fullBody: ArrayBuffer): string {\n const text = Buffer.from(fullBody).toString();\n try {\n (fullBody as ReleasableBuffer).transfer?.(0);\n } catch {\n // not transferable: the collector gets it instead, exactly as before\n }\n return text;\n}\n\n/** `transfer` is ES2024 and this package compiles against an ES2023 lib; optional because a runtime without it must work. */\ntype ReleasableBuffer = ArrayBuffer & {transfer?: (newByteLength?: number) => ArrayBuffer};\n\n// only called when there is an http error or weird unhandled route errors\nfunction fatalFail(res: HttpResponse, state: {aborted: boolean}, respHeaders: MionHeaders, error: RpcError<string>) {\n const routeResponse = getRouterFatalErrorResponse(error, respHeaders);\n reply(res, state, routeResponse);\n}\n\nfunction isHeaderSafe(text: string): boolean {\n // one pass, no allocation: a CR, LF or NUL anywhere makes the header unsafe to write\n for (let i = 0; i < text.length; i++) {\n const code = text.charCodeAt(i);\n if (code === 13 || code === 10 || code === 0) return false;\n }\n return true;\n}\n\n/** The line is a pure function of the code, so it is built once per code rather than per response. */\nconst statusLines = new Map<number, string>();\nfunction statusLine(statusCode: number): string {\n const cached = statusLines.get(statusCode);\n if (cached !== undefined) return cached;\n const statusText = STATUS_CODES[statusCode];\n const line = statusText ? `${statusCode} ${statusText}` : `${statusCode}`;\n statusLines.set(statusCode, line);\n return line;\n}\n\nfunction reply(res: HttpResponse, state: {aborted: boolean}, mionResp: MionResponse) {\n // uWS freed the response when the client left; touching it would crash\n if (state.aborted) return;\n const headers = mionResp.headers as RecordResponseHeaders;\n\n // serialized BEFORE the cork: uWS warns a cork buffer must not be held across event loop iterations\n const payload = JSON.stringify(mionResp.body);\n\n // cork makes one syscall; uWS headers are write-only and must all precede end().\n // content-length is skipped: uWS writes its own, and a duplicate corrupts the response.\n // uWS writes headers unchecked, so an echoed CR or LF would be header injection: dropped\n const write = (name: string, value: string) => {\n if (isHeaderSafe(value)) res.writeHeader(name, value);\n };\n res.cork(() => {\n res.writeStatus(statusLine(mionResp.statusCode));\n headers.forEach((name, value) => {\n if (name === 'content-length' || name === 'content-type' || !isHeaderSafe(name)) return;\n if (typeof value === 'string') write(name, value);\n // RFC 6265 forbids folding set-cookie; uWS writes a repeated name as its own line\n else if (name === 'set-cookie') for (const cookie of value) write(name, cookie);\n else write(name, value.join(', '));\n });\n // last, and the store's own skipped above: uWS appends a repeated name instead of replacing it\n res.writeHeader('content-type', mionResp.contentType);\n\n res.end(payload);\n });\n releaseHeaders(headers);\n}\n"],"mappings":"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"}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ma Jerez
|
|
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/README.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/MionKit/mion/main/assets/public/bannerx90-dark.png">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/MionKit/mion/main/assets/public/bannerx90.png">
|
|
5
|
+
<img alt='mion, a mikro kit for Typescript Serverless APIs' src='https://raw.githubusercontent.com/MionKit/mion/main/assets/public/bannerx90.png'>
|
|
6
|
+
</picture>
|
|
7
|
+
</p>
|
|
8
|
+
<p align="center">
|
|
9
|
+
<strong>Full Stack APIs at the speed of light 🚀
|
|
10
|
+
</strong>
|
|
11
|
+
</p>
|
|
12
|
+
<p align=center>
|
|
13
|
+
<img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square&maxAge=99999999" alt="npm" style="max-width:100%;">
|
|
14
|
+
<img src="https://img.shields.io/badge/license-MIT-97ca00.svg?style=flat-square&maxAge=99999999" alt="npm" style="max-width:100%;">
|
|
15
|
+
</p>
|
|
2
16
|
|
|
3
|
-
|
|
17
|
+
# `@mionjs/platform-uws`
|
|
18
|
+
|
|
19
|
+
This package contains a [uWebSockets.js](https://github.com/uNetworking/uWebSockets.js) server to run mion APIs at maximum performance! The native uWS binaries install automatically via `@mionjs/bin-uws` (per-platform optional dependencies), so a plain npm install just works. Runs on the Node.js versions uWebSockets.js ships binaries for.
|
|
20
|
+
|
|
21
|
+
## Check Out The [Website And Documentation](http://mion.io) 📚
|
|
22
|
+
|
|
23
|
+
[](http://mion.io)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
_[MIT](../../LICENSE) LICENSE_
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mionjs/platform-uws",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "mion HTTP Server on uWebSockets.js for maximum performance.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"typescript",
|
|
8
|
+
"API",
|
|
9
|
+
"RPC",
|
|
10
|
+
"json",
|
|
11
|
+
"schema",
|
|
12
|
+
"generate",
|
|
13
|
+
"server",
|
|
14
|
+
"serverless",
|
|
15
|
+
"framework",
|
|
16
|
+
"uWebSockets",
|
|
17
|
+
"uws"
|
|
18
|
+
],
|
|
19
|
+
"author": "ma jerez",
|
|
20
|
+
"homepage": "https://mion.io/",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./.dist/esm/index.d.ts",
|
|
25
|
+
"require": "./.dist/cjs/index.cjs",
|
|
26
|
+
"default": "./.dist/esm/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"directories": {
|
|
30
|
+
"lib": ".dist"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
".dist",
|
|
34
|
+
"!.dist/**/*.tsbuildinfo",
|
|
35
|
+
"!.dist/**/*.d.ts.map"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/MionKit/mion.git"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/MionKit/mion/issues"
|
|
46
|
+
},
|
|
47
|
+
"comment:engines": "Inherited constraint from @mionjs/bin-uws: uWebSockets.js ships prebuilt binaries only for these Node majors.",
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22 <23 || >=24 <25 || >=26 <27"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@mionjs/core": "0.13.0",
|
|
53
|
+
"@mionjs/router": "0.13.0",
|
|
54
|
+
"@mionjs/bin-uws": "0.13.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"typecheck:test": "tsc -p tsconfig.json --noEmit",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"dev": "vite build --watch",
|
|
60
|
+
"dev:test": "vitest watch",
|
|
61
|
+
"lint": "eslint src",
|
|
62
|
+
"format": "prettier --write src/**/*.ts",
|
|
63
|
+
"build": "vite build",
|
|
64
|
+
"clean": "rimraf .dist .coverage .mion",
|
|
65
|
+
"fresh-start": "pnpm run clean && rimraf node_modules"
|
|
66
|
+
}
|
|
6
67
|
}
|