@morojs/moro 1.8.10 → 1.8.11
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/README.md +4 -0
- package/dist/core/http/moro-engine-server.d.ts +20 -0
- package/dist/core/http/moro-engine-server.js +112 -1
- package/dist/core/http/moro-engine-server.js.map +1 -1
- package/dist/core/routing/index.d.ts +9 -2
- package/dist/core/routing/index.js +12 -7
- package/dist/core/routing/index.js.map +1 -1
- package/dist/core/routing/unified-router.d.ts +25 -1
- package/dist/core/routing/unified-router.js +69 -6
- package/dist/core/routing/unified-router.js.map +1 -1
- package/dist/core/utilities/package-utils.d.ts +4 -3
- package/dist/core/utilities/package-utils.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/moro.js +37 -7
- package/dist/moro.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,10 @@ import { createApp, z } from '@morojs/moro';
|
|
|
68
68
|
return { success: true, data: req.body };
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
// A literal body is the whole handler: on Moro's native engine it is
|
|
72
|
+
// answered without entering JS at all
|
|
73
|
+
app.get('/health').handler('ok');
|
|
74
|
+
|
|
71
75
|
app.listen(3000);
|
|
72
76
|
})();
|
|
73
77
|
```
|
|
@@ -191,6 +191,9 @@ export declare class MoroEngineServer {
|
|
|
191
191
|
/** @internal */ _respondPreparedEmpty?: (reqId: number, tplId: number) => void;
|
|
192
192
|
/** @internal */ _endWith?: (reqId: number, chunk: any) => void;
|
|
193
193
|
private _templatesOn;
|
|
194
|
+
/** @internal */ _setStaticRoute?: (serverId: number, method: number, path: string, status: number, headersFlat: string[] | null, body: any) => void;
|
|
195
|
+
private _staticOn;
|
|
196
|
+
private staticRoutes;
|
|
194
197
|
private _batchDesc;
|
|
195
198
|
private _batchCtl;
|
|
196
199
|
private _batchPaths;
|
|
@@ -225,6 +228,7 @@ export declare class MoroEngineServer {
|
|
|
225
228
|
/** @internal read by EngineResponse for buffered-response compression */
|
|
226
229
|
_compression: CompressionSettings;
|
|
227
230
|
private closed;
|
|
231
|
+
private _drainArmed;
|
|
228
232
|
private routerHandler?;
|
|
229
233
|
private errorHandler?;
|
|
230
234
|
private directRoutes;
|
|
@@ -256,6 +260,21 @@ export declare class MoroEngineServer {
|
|
|
256
260
|
* headers) or the engine's per-server store is full. */
|
|
257
261
|
_template(kind: number, status: number): number;
|
|
258
262
|
private _resetTemplates;
|
|
263
|
+
/** Whether this engine build answers static routes itself. */
|
|
264
|
+
get staticRoutesEnabled(): boolean;
|
|
265
|
+
/**
|
|
266
|
+
* Register a fixed reply for one (method, path) that the engine serves
|
|
267
|
+
* without calling into JS: no routing, no middleware, no hooks and no
|
|
268
|
+
* header building per request. The bytes on the wire are those of
|
|
269
|
+
* respond(status, headers, body). Returns false when this engine cannot
|
|
270
|
+
* take it (no capability, or a method the engine does not index), so the
|
|
271
|
+
* caller keeps its JS handler as the answer. Registering the same
|
|
272
|
+
* (method, path) again replaces the earlier reply.
|
|
273
|
+
*/
|
|
274
|
+
setStaticRoute(method: string, path: string, status?: number, headers?: Record<string, string | number | string[]> | string[] | null, body?: string | Buffer | Uint8Array | ArrayBuffer | null): boolean;
|
|
275
|
+
/** Drop every engine-answered static route; the JS routes are untouched. */
|
|
276
|
+
clearStaticRoutes(): void;
|
|
277
|
+
private _reapplyStaticRoutes;
|
|
259
278
|
private registerWithEngine;
|
|
260
279
|
private onRequestBatch;
|
|
261
280
|
private dispatchOne;
|
|
@@ -288,6 +307,7 @@ export declare class MoroEngineServer {
|
|
|
288
307
|
private dispatchDirectRoute;
|
|
289
308
|
setNativeRouteProvider(_provider: () => Array<any>, _onError?: (...args: any[]) => any): void;
|
|
290
309
|
private onRequest;
|
|
310
|
+
private _armMicrotaskDrain;
|
|
291
311
|
private onAborted;
|
|
292
312
|
private onWritable;
|
|
293
313
|
private finishUnrouted;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// Date, chunked encoding, HEAD body suppression and native 413/400 are all
|
|
15
15
|
// engine concerns - this adapter never sees them. No dependency on the uWS
|
|
16
16
|
// adapter: shared helpers live in ./utils.
|
|
17
|
+
import { setImmediate } from 'node:timers';
|
|
17
18
|
import { randomUUID } from 'crypto';
|
|
18
19
|
import { createFrameworkLogger } from '../logger/index.js';
|
|
19
20
|
import { loadNativeEngine, getNativeEngineLoadErrors, } from '../utilities/package-utils.js';
|
|
@@ -104,6 +105,31 @@ const TPL_HEADERS = [
|
|
|
104
105
|
];
|
|
105
106
|
const TPL_CONTENT_TYPE = TPL_HEADERS.map(h => h?.[1]);
|
|
106
107
|
const TPL_KINDS = TPL_HEADERS.length;
|
|
108
|
+
// Engine-answered static routes take the engine's method index; only the
|
|
109
|
+
// named methods (GET..OPTIONS) can short-circuit - 'OTHER' never does.
|
|
110
|
+
const STATIC_ROUTE_METHODS = 7;
|
|
111
|
+
// [k1, v1, k2, v2, ...] for the engine, from any header shape a caller holds.
|
|
112
|
+
// null when there is nothing to send, so the wire bytes equal respond(null).
|
|
113
|
+
function flattenHeaders(headers) {
|
|
114
|
+
if (headers === undefined || headers === null)
|
|
115
|
+
return null;
|
|
116
|
+
if (Array.isArray(headers))
|
|
117
|
+
return headers.length === 0 ? null : headers.map(String);
|
|
118
|
+
let flat = null;
|
|
119
|
+
for (const key in headers) {
|
|
120
|
+
const value = headers[key];
|
|
121
|
+
if (flat === null)
|
|
122
|
+
flat = [];
|
|
123
|
+
if (Array.isArray(value)) {
|
|
124
|
+
for (let i = 0; i < value.length; i++)
|
|
125
|
+
flat.push(key, String(value[i]));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
flat.push(key, String(value));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return flat;
|
|
132
|
+
}
|
|
107
133
|
// Content-Type sniff for send(): does the body start with '{' or '[' after
|
|
108
134
|
// optional leading whitespace? Regex `\s` matches exactly the set trimStart()
|
|
109
135
|
// strips, and the match stops at the first non-space character, so this costs
|
|
@@ -1434,6 +1460,14 @@ export class MoroEngineServer {
|
|
|
1434
1460
|
/** @internal */ _respondPreparedEmpty;
|
|
1435
1461
|
/** @internal */ _endWith;
|
|
1436
1462
|
_templatesOn = false;
|
|
1463
|
+
// Engine-answered static routes (engine >= 1.1.6, capabilities.staticRoutes):
|
|
1464
|
+
// a fixed reply for one (method, path) that the engine sends without calling
|
|
1465
|
+
// into JS. Assigned only when the flag AND the function are present (the
|
|
1466
|
+
// same guard as templates). The list is kept because listen() after close()
|
|
1467
|
+
// registers a fresh native server that knows nothing of the old one's routes.
|
|
1468
|
+
/** @internal */ _setStaticRoute;
|
|
1469
|
+
_staticOn = false;
|
|
1470
|
+
staticRoutes = [];
|
|
1437
1471
|
// Batched pipelined dispatch (engine >= 1.1.6, capabilities.batchDispatch):
|
|
1438
1472
|
// the engine parses complete pipelined requests ahead and delivers them in
|
|
1439
1473
|
// one onRequestBatch(count) call; the descriptors/control/paths buffers are
|
|
@@ -1491,6 +1525,8 @@ export class MoroEngineServer {
|
|
|
1491
1525
|
// Set by close(); listen() then registers a fresh native server, because the
|
|
1492
1526
|
// old serverId's uv handles are torn down and must never be reused.
|
|
1493
1527
|
closed = false;
|
|
1528
|
+
// See _armMicrotaskDrain.
|
|
1529
|
+
_drainArmed = false;
|
|
1494
1530
|
// Direct router dispatch (set from Moro.listen via setRouterHandler)
|
|
1495
1531
|
routerHandler;
|
|
1496
1532
|
// Global error handler (Moro.setErrorHandler) - shapes errors thrown from
|
|
@@ -1553,6 +1589,10 @@ export class MoroEngineServer {
|
|
|
1553
1589
|
this._endWith = surface.endWith;
|
|
1554
1590
|
this._templatesOn = true;
|
|
1555
1591
|
}
|
|
1592
|
+
if (this._capabilities?.staticRoutes === true && typeof surface.setStaticRoute === 'function') {
|
|
1593
|
+
this._setStaticRoute = surface.setStaticRoute;
|
|
1594
|
+
this._staticOn = true;
|
|
1595
|
+
}
|
|
1556
1596
|
if (this._capabilities?.batchDispatch === true &&
|
|
1557
1597
|
typeof surface.getBatchBuffers === 'function' &&
|
|
1558
1598
|
typeof surface.getPath === 'function') {
|
|
@@ -1626,6 +1666,58 @@ export class MoroEngineServer {
|
|
|
1626
1666
|
for (const m of this._tpl)
|
|
1627
1667
|
m.clear();
|
|
1628
1668
|
}
|
|
1669
|
+
/** Whether this engine build answers static routes itself. */
|
|
1670
|
+
get staticRoutesEnabled() {
|
|
1671
|
+
return this._staticOn;
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Register a fixed reply for one (method, path) that the engine serves
|
|
1675
|
+
* without calling into JS: no routing, no middleware, no hooks and no
|
|
1676
|
+
* header building per request. The bytes on the wire are those of
|
|
1677
|
+
* respond(status, headers, body). Returns false when this engine cannot
|
|
1678
|
+
* take it (no capability, or a method the engine does not index), so the
|
|
1679
|
+
* caller keeps its JS handler as the answer. Registering the same
|
|
1680
|
+
* (method, path) again replaces the earlier reply.
|
|
1681
|
+
*/
|
|
1682
|
+
setStaticRoute(method, path, status = 200, headers, body) {
|
|
1683
|
+
const fn = this._setStaticRoute;
|
|
1684
|
+
if (!this._staticOn || fn === undefined)
|
|
1685
|
+
return false;
|
|
1686
|
+
// The engine sends the stored bytes as they are; with compression on, the
|
|
1687
|
+
// JS handler would negotiate an encoding, so the two replies would differ.
|
|
1688
|
+
if (this._compression.enabled)
|
|
1689
|
+
return false;
|
|
1690
|
+
const methodIdx = METHODS.indexOf(method.toUpperCase());
|
|
1691
|
+
if (methodIdx < 0 || methodIdx >= STATIC_ROUTE_METHODS)
|
|
1692
|
+
return false;
|
|
1693
|
+
const headersFlat = flattenHeaders(headers);
|
|
1694
|
+
const fixedBody = body === undefined ? null : body;
|
|
1695
|
+
const entry = { methodIdx, path, status, headersFlat, body: fixedBody };
|
|
1696
|
+
const at = this.staticRoutes.findIndex(r => r.methodIdx === methodIdx && r.path === path);
|
|
1697
|
+
if (at === -1)
|
|
1698
|
+
this.staticRoutes.push(entry);
|
|
1699
|
+
else
|
|
1700
|
+
this.staticRoutes[at] = entry;
|
|
1701
|
+
fn(this.serverId, methodIdx, path, status, headersFlat, fixedBody);
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1704
|
+
/** Drop every engine-answered static route; the JS routes are untouched. */
|
|
1705
|
+
clearStaticRoutes() {
|
|
1706
|
+
this.staticRoutes.length = 0;
|
|
1707
|
+
if (this._staticOn && typeof this._engine.clearStaticRoutes === 'function') {
|
|
1708
|
+
this._engine.clearStaticRoutes(this.serverId);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
// listen() after close() registers a fresh native server; give it every
|
|
1712
|
+
// static route the old one had.
|
|
1713
|
+
_reapplyStaticRoutes() {
|
|
1714
|
+
const fn = this._setStaticRoute;
|
|
1715
|
+
if (fn === undefined)
|
|
1716
|
+
return;
|
|
1717
|
+
for (const r of this.staticRoutes) {
|
|
1718
|
+
fn(this.serverId, r.methodIdx, r.path, r.status, r.headersFlat, r.body);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1629
1721
|
// Register callbacks + limits with the native engine, returning the new
|
|
1630
1722
|
// serverId. Called from the constructor and again by listen() after a
|
|
1631
1723
|
// close() (the old native server is gone; a restart needs a fresh one).
|
|
@@ -2008,7 +2100,25 @@ export class MoroEngineServer {
|
|
|
2008
2100
|
if (!httpRes.writableEnded) {
|
|
2009
2101
|
httpRes._registered = true;
|
|
2010
2102
|
this.inflight.set(reqId, httpRes);
|
|
2011
|
-
|
|
2103
|
+
this._armMicrotaskDrain();
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
// The engine invokes onRequest with a plain V8 call from its own uv
|
|
2107
|
+
// callback, outside a Node callback scope, so microtasks queued during
|
|
2108
|
+
// dispatch - an await on an already-settled promise, a .then chain, the
|
|
2109
|
+
// router's own async 404 continuation - do not run when the callback
|
|
2110
|
+
// returns. They run at the next Node-managed callback, which on a quiet
|
|
2111
|
+
// server is whatever timer fires next: seconds away. A no-op immediate is
|
|
2112
|
+
// that callback: Node drains nextTicks and microtasks when its scope
|
|
2113
|
+
// closes, in this loop iteration's check phase. One per burst is enough,
|
|
2114
|
+
// so it is armed once and re-armed only after it has fired.
|
|
2115
|
+
_armMicrotaskDrain() {
|
|
2116
|
+
if (this._drainArmed)
|
|
2117
|
+
return;
|
|
2118
|
+
this._drainArmed = true;
|
|
2119
|
+
setImmediate(() => {
|
|
2120
|
+
this._drainArmed = false;
|
|
2121
|
+
});
|
|
2012
2122
|
}
|
|
2013
2123
|
onAborted(reqId) {
|
|
2014
2124
|
const httpRes = this.inflight.get(reqId);
|
|
@@ -2344,6 +2454,7 @@ export class MoroEngineServer {
|
|
|
2344
2454
|
if (this.closed) {
|
|
2345
2455
|
this.serverId = this.registerWithEngine();
|
|
2346
2456
|
this._resetTemplates();
|
|
2457
|
+
this._reapplyStaticRoutes();
|
|
2347
2458
|
this.closed = false;
|
|
2348
2459
|
}
|
|
2349
2460
|
// Synchronous bind; throws on bind errors, returns the actual port
|