tina4-nodejs 3.13.95 → 3.13.96
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/CLAUDE.md +1 -2
- package/package.json +2 -1
- package/packages/cli/dist/bin.js +681 -994
- package/packages/core/dist/index.js +561 -875
- package/packages/core/public/css/tina4.min.css +1 -1
- package/packages/core/src/index.ts +1 -3
- package/packages/core/src/messenger.ts +288 -96
- package/packages/core/src/request.ts +28 -7
- package/packages/core/src/server.ts +135 -7
- package/packages/orm/dist/index.js +612 -926
- package/packages/orm/src/autoCrud.ts +12 -10
- package/packages/orm/src/database.ts +62 -58
- package/packages/orm/src/databaseResult.ts +44 -73
- package/packages/orm/src/index.ts +0 -3
- package/packages/orm/src/migration.ts +26 -8
- package/packages/orm/src/model.ts +4 -0
- package/packages/orm/src/queryBuilder.ts +12 -5
- package/packages/orm/src/types.ts +7 -74
- package/packages/swagger/dist/index.js +78 -20
- package/packages/swagger/src/generator.ts +172 -29
- package/types/core/src/index.d.ts +1 -3
- package/types/core/src/messenger.d.ts +45 -4
- package/types/core/src/server.d.ts +0 -4
- package/types/orm/src/database.d.ts +34 -30
- package/types/orm/src/databaseResult.d.ts +26 -36
- package/types/orm/src/index.d.ts +1 -2
- package/types/orm/src/migration.d.ts +4 -3
- package/types/orm/src/types.d.ts +7 -34
- package/packages/core/src/scss.ts +0 -623
- package/types/core/src/scss.d.ts +0 -19
|
@@ -378,9 +378,29 @@ function openBrowser(url: string) {
|
|
|
378
378
|
* for backwards compatibility.
|
|
379
379
|
*/
|
|
380
380
|
export function resolvePortAndHost(config?: { port?: number; host?: string }): { port: number; host: string } {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
381
|
+
// Port: explicit config > TINA4_PORT > PORT (deprecated) > default.
|
|
382
|
+
//
|
|
383
|
+
// This read PORT and nothing else, so TINA4_PORT - the name the CLI
|
|
384
|
+
// documents and prefers, and the one devAdmin.ts itself reads first - was
|
|
385
|
+
// IGNORED on the path that binds the socket. Setting it did nothing and said
|
|
386
|
+
// nothing.
|
|
387
|
+
//
|
|
388
|
+
// Bare PORT stays honoured so no deployment breaks, and warns so the
|
|
389
|
+
// migration happens. Removal is 3.14.
|
|
390
|
+
const tina4Port = process.env.TINA4_PORT;
|
|
391
|
+
const legacyPort = process.env.PORT;
|
|
392
|
+
let port: number;
|
|
393
|
+
if (config?.port !== undefined) {
|
|
394
|
+
port = config.port;
|
|
395
|
+
} else if (tina4Port && /^\d+$/.test(tina4Port)) {
|
|
396
|
+
port = parseInt(tina4Port, 10);
|
|
397
|
+
} else if (legacyPort && /^\d+$/.test(legacyPort)) {
|
|
398
|
+
port = parseInt(legacyPort, 10);
|
|
399
|
+
warnDeprecatedPort(port);
|
|
400
|
+
} else {
|
|
401
|
+
port = 7148;
|
|
402
|
+
}
|
|
403
|
+
|
|
384
404
|
const host = config?.host
|
|
385
405
|
?? process.env.TINA4_HOST
|
|
386
406
|
?? process.env.HOST
|
|
@@ -388,6 +408,22 @@ export function resolvePortAndHost(config?: { port?: number; host?: string }): {
|
|
|
388
408
|
return { port, host };
|
|
389
409
|
}
|
|
390
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Warn ONCE that bare PORT was used instead of TINA4_PORT.
|
|
413
|
+
*
|
|
414
|
+
* Once, because resolvePortAndHost can be called more than once per process
|
|
415
|
+
* and a warning repeated on every call is a warning people filter out.
|
|
416
|
+
*/
|
|
417
|
+
let portDeprecationWarned = false;
|
|
418
|
+
function warnDeprecatedPort(port: number): void {
|
|
419
|
+
if (portDeprecationWarned) return;
|
|
420
|
+
portDeprecationWarned = true;
|
|
421
|
+
Log.warning(
|
|
422
|
+
`PORT is deprecated and will be removed in 3.14 - use TINA4_PORT instead ` +
|
|
423
|
+
`(binding port ${port} from PORT)`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
391
427
|
/**
|
|
392
428
|
* Whether the boot banner should be suppressed. Set TINA4_SUPPRESS=true to
|
|
393
429
|
* silence the ASCII-art banner and route table on startup — useful in CI,
|
|
@@ -795,6 +831,67 @@ let _serverHandle: { close: () => void; router: Router; port: number } | null =
|
|
|
795
831
|
* Start the Tina4 HTTP server.
|
|
796
832
|
* Thin wrapper around startServer() for cross-framework parity with PHP and Ruby.
|
|
797
833
|
*/
|
|
834
|
+
/**
|
|
835
|
+
* Watch for a handler that occupies the event loop, and say so.
|
|
836
|
+
*
|
|
837
|
+
* Node runs ONE loop. An `await`ing handler yields it and blocks nobody -
|
|
838
|
+
* measured, /fast answers in 0.030s while a route awaits a 2s timer. A
|
|
839
|
+
* CPU-BOUND handler does not yield, and everything else waits: the same /fast
|
|
840
|
+
* took 1.575s during a 2s busy loop.
|
|
841
|
+
*
|
|
842
|
+
* That is inherent to a single-loop runtime, not a bug to engineer away. PHP
|
|
843
|
+
* fixed its equivalent by forking per request because `sleep()` is the obvious
|
|
844
|
+
* thing to write there and it blocks; in JavaScript the obvious thing is
|
|
845
|
+
* `await`, which does not. So the exposure here is narrower - CPU-bound work
|
|
846
|
+
* and synchronous I/O - and the honest fix is to make it VISIBLE rather than
|
|
847
|
+
* to move handlers onto threads a closure cannot cross.
|
|
848
|
+
*
|
|
849
|
+
* The mechanism is loop lag: a timer set for TICK_MS fires late by however
|
|
850
|
+
* long the loop was blocked. If that lateness passes the threshold, something
|
|
851
|
+
* held the loop and the developer wants to know which.
|
|
852
|
+
*
|
|
853
|
+
* A 100ms repeating timer is the classic way to pin a process open forever, so
|
|
854
|
+
* there are two guards against it: close() stops the timer, and the timer is
|
|
855
|
+
* unref'd. Measured: either one alone is enough, and the signal path exits
|
|
856
|
+
* regardless of both. They are kept together because they cost nothing and
|
|
857
|
+
* cover different exits - close() covers the in-process handle, unref() covers
|
|
858
|
+
* a path that never reaches close() at all.
|
|
859
|
+
*/
|
|
860
|
+
const LOOP_WATCHDOG_TICK_MS = 100;
|
|
861
|
+
|
|
862
|
+
function startLoopWatchdog(): { stop: () => void } {
|
|
863
|
+
const raw = (process.env.TINA4_LOOP_LAG_WARN_MS ?? "").trim();
|
|
864
|
+
// 0 or a negative value disables it; a non-numeric value falls to the
|
|
865
|
+
// default rather than silently disabling a diagnostic.
|
|
866
|
+
const threshold = /^\d+$/.test(raw) ? parseInt(raw, 10) : 250;
|
|
867
|
+
if (threshold <= 0) {
|
|
868
|
+
return { stop: () => {} };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
let last = Date.now();
|
|
872
|
+
let warned = 0;
|
|
873
|
+
const timer = setInterval(() => {
|
|
874
|
+
const now = Date.now();
|
|
875
|
+
const lag = now - last - LOOP_WATCHDOG_TICK_MS;
|
|
876
|
+
last = now;
|
|
877
|
+
if (lag < threshold) return;
|
|
878
|
+
|
|
879
|
+
// Rate-limited: a handler that blocks on every request would otherwise
|
|
880
|
+
// produce a wall of identical warnings, which people filter out.
|
|
881
|
+
warned++;
|
|
882
|
+
if (warned > 5 && warned % 20 !== 0) return;
|
|
883
|
+
Log.warning(
|
|
884
|
+
`Event loop blocked for ${lag}ms. Node serves every request on one loop, ` +
|
|
885
|
+
`so a handler doing CPU-bound work or synchronous I/O stalls all the ` +
|
|
886
|
+
`others for that long. Move the work to Tina4's queue, or await it. ` +
|
|
887
|
+
`Set TINA4_LOOP_LAG_WARN_MS to change the ${threshold}ms threshold, or 0 to silence.`,
|
|
888
|
+
);
|
|
889
|
+
}, LOOP_WATCHDOG_TICK_MS);
|
|
890
|
+
timer.unref();
|
|
891
|
+
|
|
892
|
+
return { stop: () => clearInterval(timer) };
|
|
893
|
+
}
|
|
894
|
+
|
|
798
895
|
export async function start(config?: Tina4Config): Promise<{ close: () => void; router: Router; port: number }> {
|
|
799
896
|
const isManaged = process.argv.includes('--managed');
|
|
800
897
|
if (!isManaged && process.env.TINA4_OVERRIDE_CLIENT !== 'true') {
|
|
@@ -1390,8 +1487,18 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
1390
1487
|
const host = resolved.host;
|
|
1391
1488
|
let port = resolved.port;
|
|
1392
1489
|
|
|
1393
|
-
// Claim the requested port — kill whatever is on it if needed
|
|
1394
|
-
|
|
1490
|
+
// Claim the requested port — kill whatever is on it if needed.
|
|
1491
|
+
//
|
|
1492
|
+
// NOT in a cluster worker. A worker does not own the port: the primary binds
|
|
1493
|
+
// it once and hands the handle down through cluster's IPC. A worker running
|
|
1494
|
+
// this finds the port "in use" (the primary is holding it) and KILLS the
|
|
1495
|
+
// process holding it, which is its own parent. Every worker did that, then
|
|
1496
|
+
// died itself with `write EPIPE` from cluster._getServer because the primary
|
|
1497
|
+
// it needed to ask for the socket was gone. Cluster mode never served a
|
|
1498
|
+
// single request.
|
|
1499
|
+
if (!cluster.isWorker) {
|
|
1500
|
+
port = findAvailablePort(port);
|
|
1501
|
+
}
|
|
1395
1502
|
|
|
1396
1503
|
// Cluster mode for production: fork workers based on CPU count
|
|
1397
1504
|
// Only when --production is explicitly set (via TINA4_PRODUCTION env var)
|
|
@@ -1703,8 +1810,26 @@ ${reset}
|
|
|
1703
1810
|
await middleware.run(req, res);
|
|
1704
1811
|
if (res.raw.writableEnded) return;
|
|
1705
1812
|
|
|
1706
|
-
// Parse request body
|
|
1707
|
-
|
|
1813
|
+
// Parse request body.
|
|
1814
|
+
//
|
|
1815
|
+
// A body that breaks a documented limit is the client's error, not the
|
|
1816
|
+
// server's. PayloadTooLargeError already carried `statusCode = 413` and
|
|
1817
|
+
// nothing read it, so an oversized upload answered 500 - which tells the
|
|
1818
|
+
// caller to retry the exact request that will fail again.
|
|
1819
|
+
try {
|
|
1820
|
+
await req.parseBody();
|
|
1821
|
+
} catch (err) {
|
|
1822
|
+
const status = (err as { statusCode?: number })?.statusCode;
|
|
1823
|
+
if (typeof status === "number" && status >= 400 && status < 500) {
|
|
1824
|
+
if (!rawRes.writableEnded) {
|
|
1825
|
+
rawRes.statusCode = status;
|
|
1826
|
+
rawRes.setHeader("content-type", "application/json");
|
|
1827
|
+
rawRes.end(JSON.stringify({ error: (err as Error).message }));
|
|
1828
|
+
}
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
throw err;
|
|
1832
|
+
}
|
|
1708
1833
|
|
|
1709
1834
|
const pathname = req.path;
|
|
1710
1835
|
|
|
@@ -2043,8 +2168,11 @@ ${reset}
|
|
|
2043
2168
|
process.on("SIGTERM", onSigterm);
|
|
2044
2169
|
process.on("SIGINT", onSigint);
|
|
2045
2170
|
|
|
2171
|
+
const loopWatchdog = startLoopWatchdog();
|
|
2172
|
+
|
|
2046
2173
|
resolvePromise({
|
|
2047
2174
|
close: () => {
|
|
2175
|
+
loopWatchdog.stop();
|
|
2048
2176
|
// An explicit close() is not a signal shutdown: drop the handlers so
|
|
2049
2177
|
// a test that starts many servers in one process does not pile up
|
|
2050
2178
|
// listeners (and trip Node's MaxListeners warning).
|