tina4-nodejs 3.13.92 → 3.13.95
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 +170 -28
- package/README.md +2 -2
- package/package.json +13 -9
- package/packages/cli/dist/bin.js +33126 -30055
- package/packages/cli/src/commands/metrics.ts +17 -11
- package/packages/cli/src/commands/serve.ts +10 -9
- package/packages/core/dist/index.js +33062 -29908
- package/packages/core/src/ai.ts +7 -1
- package/packages/core/src/auth.ts +191 -39
- package/packages/core/src/background.ts +19 -19
- package/packages/core/src/cache.ts +492 -49
- package/packages/core/src/devAdmin.ts +79 -32
- package/packages/core/src/devMailbox.ts +20 -44
- package/packages/core/src/dispatchPipeline.ts +285 -0
- package/packages/core/src/dotenv.ts +185 -40
- package/packages/core/src/index.ts +7 -6
- package/packages/core/src/logger.ts +257 -36
- package/packages/core/src/mcp.ts +1 -1
- package/packages/core/src/messenger.ts +81 -13
- package/packages/core/src/metrics.ts +199 -961
- package/packages/core/src/middleware.ts +390 -123
- package/packages/core/src/queue.ts +188 -32
- package/packages/core/src/queueBackends/kafkaBackend.ts +109 -13
- package/packages/core/src/queueBackends/liteBackend.ts +13 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
- package/packages/core/src/rateLimiter.ts +10 -5
- package/packages/core/src/request.ts +6 -9
- package/packages/core/src/response.ts +46 -1
- package/packages/core/src/router.ts +29 -4
- package/packages/core/src/server.ts +751 -414
- package/packages/core/src/session.ts +244 -27
- package/packages/core/src/sessionHandlers/childError.ts +72 -0
- package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +293 -202
- package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
- package/packages/core/src/sessionHandlers/respClient.ts +16 -143
- package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
- package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
- package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
- package/packages/core/src/testClient.ts +18 -5
- package/packages/core/src/trustedProxy.ts +249 -0
- package/packages/core/src/types.ts +29 -5
- package/packages/core/src/websocket.ts +66 -0
- package/packages/frond/dist/index.js +74 -31
- package/packages/frond/src/engine.ts +99 -33
- package/packages/orm/dist/index.js +26554 -23400
- package/packages/orm/src/adapters/firebird.ts +183 -56
- package/packages/orm/src/adapters/mongodb.ts +25 -4
- package/packages/orm/src/adapters/mssql.ts +114 -29
- package/packages/orm/src/adapters/mysql.ts +103 -40
- package/packages/orm/src/adapters/odbc.ts +44 -21
- package/packages/orm/src/adapters/postgres.ts +118 -26
- package/packages/orm/src/adapters/sqlDialect.ts +120 -0
- package/packages/orm/src/adapters/sqlite.ts +64 -25
- package/packages/orm/src/baseModel.ts +135 -40
- package/packages/orm/src/cachedDatabase.ts +43 -19
- package/packages/orm/src/connectTimeout.ts +265 -0
- package/packages/orm/src/database.ts +338 -198
- package/packages/orm/src/databaseResult.ts +65 -13
- package/packages/orm/src/databaseUrl.ts +484 -0
- package/packages/orm/src/docstore.ts +386 -145
- package/packages/orm/src/index.ts +13 -3
- package/packages/orm/src/migration.ts +18 -3
- package/packages/orm/src/queryBuilder.ts +38 -4
- package/packages/orm/src/sqlTranslator.ts +310 -4
- package/packages/orm/src/types.ts +15 -4
- package/types/cli/src/bin.d.ts +92 -0
- package/types/cli/src/commands/build.d.ts +2 -0
- package/types/cli/src/commands/generate.d.ts +47 -0
- package/types/cli/src/commands/init.d.ts +1 -0
- package/types/cli/src/commands/metrics.d.ts +6 -0
- package/types/cli/src/commands/migrate.d.ts +1 -0
- package/types/cli/src/commands/migrateCreate.d.ts +1 -0
- package/types/cli/src/commands/migrateRollback.d.ts +1 -0
- package/types/cli/src/commands/migrateStatus.d.ts +1 -0
- package/types/cli/src/commands/queue.d.ts +20 -0
- package/types/cli/src/commands/routes.d.ts +1 -0
- package/types/cli/src/commands/seed.d.ts +1 -0
- package/types/cli/src/commands/serve.d.ts +6 -0
- package/types/cli/src/commands/test.d.ts +1 -0
- package/types/core/src/ai.d.ts +64 -0
- package/types/core/src/api.d.ts +262 -0
- package/types/core/src/auth.d.ts +177 -0
- package/types/core/src/authGate.d.ts +20 -0
- package/types/core/src/background.d.ts +34 -0
- package/types/core/src/cache.d.ts +163 -0
- package/types/core/src/constants.d.ts +38 -0
- package/types/core/src/container.d.ts +44 -0
- package/types/core/src/context/chunker.d.ts +31 -0
- package/types/core/src/context/index.d.ts +93 -0
- package/types/core/src/devAdmin.d.ts +179 -0
- package/types/core/src/devMailbox.d.ts +54 -0
- package/types/core/src/dispatchPipeline.d.ts +117 -0
- package/types/core/src/docs.d.ts +141 -0
- package/types/core/src/docsAutoDiscovery.d.ts +6 -0
- package/types/core/src/dotenv.d.ts +87 -0
- package/types/core/src/env.d.ts +28 -0
- package/types/core/src/errorOverlay.d.ts +36 -0
- package/types/core/src/events.d.ts +75 -0
- package/types/core/src/fakeData.d.ts +55 -0
- package/types/core/src/feedback.d.ts +90 -0
- package/types/core/src/graphql.d.ts +207 -0
- package/types/core/src/health.d.ts +22 -0
- package/types/core/src/htmlElement.d.ts +75 -0
- package/types/core/src/i18n.d.ts +37 -0
- package/types/core/src/index.d.ts +92 -0
- package/types/core/src/job.d.ts +39 -0
- package/types/core/src/logger.d.ts +200 -0
- package/types/core/src/mcp.d.ts +248 -0
- package/types/core/src/messenger.d.ts +191 -0
- package/types/core/src/metrics.d.ts +41 -0
- package/types/core/src/middleware.d.ts +330 -0
- package/types/core/src/mqtt.d.ts +257 -0
- package/types/core/src/mqttMessage.d.ts +67 -0
- package/types/core/src/plan.d.ts +96 -0
- package/types/core/src/projectIndex.d.ts +56 -0
- package/types/core/src/queue.d.ts +268 -0
- package/types/core/src/queueBackends/kafkaBackend.d.ts +117 -0
- package/types/core/src/queueBackends/liteBackend.d.ts +128 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +119 -0
- package/types/core/src/queueBackends/rabbitmqBackend.d.ts +55 -0
- package/types/core/src/rateLimiter.d.ts +49 -0
- package/types/core/src/request.d.ts +25 -0
- package/types/core/src/response.d.ts +28 -0
- package/types/core/src/routeDiscovery.d.ts +12 -0
- package/types/core/src/router.d.ts +366 -0
- package/types/core/src/scss.d.ts +19 -0
- package/types/core/src/server.d.ts +146 -0
- package/types/core/src/service.d.ts +115 -0
- package/types/core/src/session.d.ts +341 -0
- package/types/core/src/sessionHandlers/childError.d.ts +34 -0
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +97 -0
- package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
- package/types/core/src/sessionHandlers/mongoClient.d.ts +35 -0
- package/types/core/src/sessionHandlers/mongoHandler.d.ts +109 -0
- package/types/core/src/sessionHandlers/respClient.d.ts +22 -0
- package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
- package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
- package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
- package/types/core/src/sessionHandlers/valkeyHandler.d.ts +65 -0
- package/types/core/src/static.d.ts +2 -0
- package/types/core/src/test.d.ts +94 -0
- package/types/core/src/testClient.d.ts +36 -0
- package/types/core/src/testing.d.ts +58 -0
- package/types/core/src/trustedProxy.d.ts +44 -0
- package/types/core/src/types.d.ts +242 -0
- package/types/core/src/validator.d.ts +52 -0
- package/types/core/src/websocket.d.ts +402 -0
- package/types/core/src/websocketBackplane.d.ts +166 -0
- package/types/core/src/websocketConnection.d.ts +54 -0
- package/types/core/src/wsdl.d.ts +101 -0
- package/types/frond/src/engine.d.ts +263 -0
- package/types/frond/src/index.d.ts +2 -0
- package/types/orm/src/adapters/firebird.d.ts +183 -0
- package/types/orm/src/adapters/mongodb.d.ts +81 -0
- package/types/orm/src/adapters/mssql.d.ts +77 -0
- package/types/orm/src/adapters/mysql.d.ts +67 -0
- package/types/orm/src/adapters/odbc.d.ts +94 -0
- package/types/orm/src/adapters/postgres.d.ts +86 -0
- package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
- package/types/orm/src/adapters/sqlite.d.ts +68 -0
- package/types/orm/src/autoCrud.d.ts +73 -0
- package/types/orm/src/baseModel.d.ts +427 -0
- package/types/orm/src/cachedDatabase.d.ts +190 -0
- package/types/orm/src/connectTimeout.d.ts +100 -0
- package/types/orm/src/database.d.ts +655 -0
- package/types/orm/src/databaseResult.d.ts +109 -0
- package/types/orm/src/databaseUrl.d.ts +125 -0
- package/types/orm/src/docstore.d.ts +241 -0
- package/types/orm/src/fakeData.d.ts +22 -0
- package/types/orm/src/index.d.ts +43 -0
- package/types/orm/src/migration.d.ts +275 -0
- package/types/orm/src/model.d.ts +7 -0
- package/types/orm/src/query.d.ts +14 -0
- package/types/orm/src/queryBuilder.d.ts +193 -0
- package/types/orm/src/realtime/index.d.ts +7 -0
- package/types/orm/src/realtime/models/attachment.d.ts +43 -0
- package/types/orm/src/realtime/models/channel.d.ts +32 -0
- package/types/orm/src/realtime/models/channelMember.d.ts +32 -0
- package/types/orm/src/realtime/models/message.d.ts +36 -0
- package/types/orm/src/realtime/models/workspace.d.ts +26 -0
- package/types/orm/src/realtime/realtime.d.ts +24 -0
- package/types/orm/src/realtime/storage.d.ts +61 -0
- package/types/orm/src/seeder.d.ts +118 -0
- package/types/orm/src/sqlTranslator.d.ts +258 -0
- package/types/orm/src/types.d.ts +148 -0
- package/types/orm/src/validation.d.ts +6 -0
- package/types/swagger/src/generator.d.ts +46 -0
- package/types/swagger/src/index.d.ts +2 -0
- package/types/swagger/src/ui.d.ts +11 -0
- package/packages/core/src/sessionHandlers/redisHandler.ts +0 -206
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { SessionHandler } from "../session.js";
|
|
2
|
+
interface SessionData {
|
|
3
|
+
_created: number;
|
|
4
|
+
_accessed: number;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface DatabaseSessionConfig {
|
|
8
|
+
/** SQLite database file path. Explicit config wins over TINA4_DATABASE_URL. */
|
|
9
|
+
dbPath?: string;
|
|
10
|
+
backend?: string;
|
|
11
|
+
path?: string;
|
|
12
|
+
ttl?: number;
|
|
13
|
+
redisHost?: string;
|
|
14
|
+
redisPort?: number;
|
|
15
|
+
redisPassword?: string;
|
|
16
|
+
redisPrefix?: string;
|
|
17
|
+
redisDb?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Database session handler.
|
|
21
|
+
*
|
|
22
|
+
* Stores session data as JSON in a `tina4_session` table.
|
|
23
|
+
* Expiry is checked on read; expired rows are cleaned up lazily.
|
|
24
|
+
*/
|
|
25
|
+
export declare class DatabaseSessionHandler implements SessionHandler {
|
|
26
|
+
private sqliteHandle;
|
|
27
|
+
/** Set for SQLite. Null when this handler talks to a networked engine. */
|
|
28
|
+
private dbPath;
|
|
29
|
+
/** Set for a networked engine. Null for SQLite. */
|
|
30
|
+
private target;
|
|
31
|
+
private initialized;
|
|
32
|
+
/**
|
|
33
|
+
* NO I/O IN A CONSTRUCTOR (ADR-0021).
|
|
34
|
+
*
|
|
35
|
+
* This used to run `new DatabaseSync(dbPath)` and a `PRAGMA journal_mode =
|
|
36
|
+
* WAL` right here. Both are real work against real storage: opening the
|
|
37
|
+
* database CREATES the file, and switching to WAL creates its `-wal` and
|
|
38
|
+
* `-shm` siblings. Measured from a clean temp cwd, merely constructing this
|
|
39
|
+
* handler left three files on disk before a single session was ever read or
|
|
40
|
+
* written.
|
|
41
|
+
*
|
|
42
|
+
* A constructor sits OUTSIDE the log-loud-and-degrade policy, so nothing it
|
|
43
|
+
* does can be logged, degraded, or re-raised by TINA4_SESSION_STRICT - the one
|
|
44
|
+
* place the policy cannot protect is the first thing that runs.
|
|
45
|
+
*
|
|
46
|
+
* Everything below is pure string work. Resolving the target parses a URL;
|
|
47
|
+
* refusing an unsupported engine is a CONFIGURATION error that must still be
|
|
48
|
+
* loud at construction. The database - file or socket - is opened on first
|
|
49
|
+
* use. Going multi-engine is the change most likely to reintroduce
|
|
50
|
+
* constructor-time I/O, which is why test/sessionHandlerConstruction.test.ts
|
|
51
|
+
* measures a real filesystem and a real listening socket rather than trusting
|
|
52
|
+
* this comment.
|
|
53
|
+
*/
|
|
54
|
+
constructor(config?: DatabaseSessionConfig);
|
|
55
|
+
/** Open the SQLite database on FIRST USE, not at construction. */
|
|
56
|
+
private get sqlite();
|
|
57
|
+
private get engine();
|
|
58
|
+
/**
|
|
59
|
+
* Decide, from TINA4_DATABASE_URL, which engine this handler talks to.
|
|
60
|
+
*
|
|
61
|
+
* A NON-SQLITE URL NOW WORKS. It used to throw, because the handler drove
|
|
62
|
+
* `node:sqlite` directly and had no way to reach anything else; the async
|
|
63
|
+
* drivers now ride the sync bridge, so the reason for the refusal is gone.
|
|
64
|
+
*
|
|
65
|
+
* AN UNSUPPORTED ENGINE STILL REFUSES, and that half is not negotiable. The
|
|
66
|
+
* original defect was worse than a refusal: an unrecognised URL fell through
|
|
67
|
+
* to the literal default `"data/tina4_sessions.db"`, so
|
|
68
|
+
* `TINA4_DATABASE_URL=postgres://...` with `TINA4_SESSION_BACKEND=database`
|
|
69
|
+
* round-tripped happily while writing SQLite files into the process working
|
|
70
|
+
* directory. Measured from a clean temp cwd: round-trip true, and `data/`
|
|
71
|
+
* contained `tina4_sessions.db`, `-shm` and `-wal`. Every horizontally-scaled
|
|
72
|
+
* instance therefore had its own private session store and a user was logged
|
|
73
|
+
* out on every request that landed elsewhere.
|
|
74
|
+
*
|
|
75
|
+
* This is the same rule `resolveBackend()` applies one layer up, where an
|
|
76
|
+
* unknown backend name raises rather than falling through to disk.
|
|
77
|
+
*
|
|
78
|
+
* @throws Error naming the offending scheme and the engines this backend
|
|
79
|
+
* speaks. The URL itself is NEVER in the message - it may carry a
|
|
80
|
+
* password.
|
|
81
|
+
*/
|
|
82
|
+
private resolveTarget;
|
|
83
|
+
private unsupportedEngine;
|
|
84
|
+
/** Run a statement that returns rows. */
|
|
85
|
+
private query;
|
|
86
|
+
/** Run a statement that returns nothing. */
|
|
87
|
+
private exec;
|
|
88
|
+
/**
|
|
89
|
+
* Ensure the session table exists (called once on first use).
|
|
90
|
+
*/
|
|
91
|
+
private ensureTable;
|
|
92
|
+
read(sessionId: string): SessionData | null;
|
|
93
|
+
write(sessionId: string, data: SessionData, ttl: number): void;
|
|
94
|
+
destroy(sessionId: string): void;
|
|
95
|
+
gc(_maxLifetime: number): void;
|
|
96
|
+
}
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { SessionHandler } from "../session.js";
|
|
2
|
+
interface SessionData {
|
|
3
|
+
_created: number;
|
|
4
|
+
_accessed: number;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface MemcachedSessionConfig {
|
|
8
|
+
host?: string;
|
|
9
|
+
port?: number;
|
|
10
|
+
prefix?: string;
|
|
11
|
+
ttl?: number;
|
|
12
|
+
backend?: string;
|
|
13
|
+
path?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class MemcachedSessionHandler implements SessionHandler {
|
|
16
|
+
private host;
|
|
17
|
+
private port;
|
|
18
|
+
private prefix;
|
|
19
|
+
private ttl;
|
|
20
|
+
constructor(config?: MemcachedSessionConfig);
|
|
21
|
+
private key;
|
|
22
|
+
/**
|
|
23
|
+
* Run one memcached command synchronously and return the raw reply.
|
|
24
|
+
*
|
|
25
|
+
* Delegates to the shared persistent-connection transport (syncSocket) rather
|
|
26
|
+
* than spawning a child per command: that cost a process spawn plus a fresh
|
|
27
|
+
* TCP connection every time (p50 41ms, p99 487ms) and its tail tripped the
|
|
28
|
+
* deadline under load — the same defect that made the Valkey session tests
|
|
29
|
+
* flaky, which this handler inherited on the day it was written.
|
|
30
|
+
*
|
|
31
|
+
* @throws Error on any transport failure — never swallowed to an empty
|
|
32
|
+
* result, because for a session an outage must be distinguishable
|
|
33
|
+
* from "no session yet".
|
|
34
|
+
*/
|
|
35
|
+
private command;
|
|
36
|
+
read(sessionId: string): SessionData | null;
|
|
37
|
+
/**
|
|
38
|
+
* Convert a ttl in SECONDS to memcached's dual-meaning exptime field.
|
|
39
|
+
*
|
|
40
|
+
* memcached documents exptime as RELATIVE seconds up to 2592000 (30 days),
|
|
41
|
+
* and as an ABSOLUTE UNIX TIMESTAMP for anything larger. Sending a raw ttl of
|
|
42
|
+
* 2592001 therefore does not mean "30 days and one second" - it means
|
|
43
|
+
* 1970-01-31, which is already past, so the item expires the instant it is
|
|
44
|
+
* stored. memcached still replies STORED, so the write looks successful and
|
|
45
|
+
* the very next read is a miss: a silent logout on every request.
|
|
46
|
+
*
|
|
47
|
+
* Measured against real memcached 1.6.45: ttl=2592000 survives, ttl=2592001
|
|
48
|
+
* vanishes instantly.
|
|
49
|
+
*
|
|
50
|
+
* We CONVERT rather than CLAMP. Clamping a 60-day session down to 30 days
|
|
51
|
+
* would silently shorten a lifetime the operator explicitly asked to be
|
|
52
|
+
* longer, which is the same class of lie in the other direction.
|
|
53
|
+
*/
|
|
54
|
+
private expTime;
|
|
55
|
+
write(sessionId: string, data: SessionData, ttl: number): void;
|
|
56
|
+
destroy(sessionId: string): void;
|
|
57
|
+
/** No-op — memcached expires its own keys via the TTL set on write. */
|
|
58
|
+
gc(_maxLifetime: number): void;
|
|
59
|
+
}
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface MongoTarget {
|
|
2
|
+
host: string;
|
|
3
|
+
port: number;
|
|
4
|
+
database: string;
|
|
5
|
+
collection: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Command args: `filter` (always), plus `data`/`expires_at`/`last_accessed` for
|
|
9
|
+
* an update.
|
|
10
|
+
*
|
|
11
|
+
* `expires_at` is an ABSOLUTE epoch-seconds deadline computed at write time. It
|
|
12
|
+
* is what makes the session actually expire: this handler previously stored no
|
|
13
|
+
* expiry at all, created no TTL index, and never consulted anything on read, so
|
|
14
|
+
* MongoDB sessions lived forever.
|
|
15
|
+
*/
|
|
16
|
+
export interface MongoCommandArgs {
|
|
17
|
+
filter: Record<string, unknown>;
|
|
18
|
+
data?: unknown;
|
|
19
|
+
expires_at?: number;
|
|
20
|
+
last_accessed?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run a session Mongo command synchronously.
|
|
24
|
+
*
|
|
25
|
+
* - command "find" -> the matched document as a JSON string, or "__EMPTY__".
|
|
26
|
+
* - command "update" -> "__OK__" (upsert of `{_id, data, last_accessed}`).
|
|
27
|
+
* - command "delete" -> "__OK__".
|
|
28
|
+
*
|
|
29
|
+
* THROWS `<label> command failed: ...` on a transport failure (server
|
|
30
|
+
* unreachable, timeout) OR a Mongo command error (`ok != 1`), so the Session
|
|
31
|
+
* boundary can log-loud + degrade (or re-throw under strict mode). A genuine
|
|
32
|
+
* miss is NOT a failure and comes back as "__EMPTY__" — collapsing the two is
|
|
33
|
+
* how a dead backend silently logs every user out.
|
|
34
|
+
*/
|
|
35
|
+
export declare function mongoCommandSync(target: MongoTarget, command: "find" | "update" | "delete", args: MongoCommandArgs, label?: string): string;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 MongoDB Session Handler — MongoDB wire protocol via raw TCP, zero dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Stores session data in MongoDB using the MongoDB wire protocol directly.
|
|
5
|
+
* No `mongodb` or `mongoose` npm package required.
|
|
6
|
+
*
|
|
7
|
+
* Configure via environment variables:
|
|
8
|
+
* TINA4_SESSION_MONGO_HOST (default: "127.0.0.1")
|
|
9
|
+
* TINA4_SESSION_MONGO_PORT (default: 27017)
|
|
10
|
+
* TINA4_SESSION_MONGO_URI (overrides host/port if set)
|
|
11
|
+
* TINA4_SESSION_MONGO_USERNAME (optional)
|
|
12
|
+
* TINA4_SESSION_MONGO_PASSWORD (optional)
|
|
13
|
+
* TINA4_SESSION_MONGO_DB (default: "tina4")
|
|
14
|
+
* TINA4_SESSION_MONGO_COLLECTION (default: "sessions")
|
|
15
|
+
*/
|
|
16
|
+
import type { SessionHandler } from "../session.js";
|
|
17
|
+
interface SessionData {
|
|
18
|
+
_created: number;
|
|
19
|
+
_accessed: number;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface MongoSessionConfig {
|
|
23
|
+
host?: string;
|
|
24
|
+
port?: number;
|
|
25
|
+
uri?: string;
|
|
26
|
+
username?: string;
|
|
27
|
+
password?: string;
|
|
28
|
+
database?: string;
|
|
29
|
+
collection?: string;
|
|
30
|
+
backend?: string;
|
|
31
|
+
path?: string;
|
|
32
|
+
ttl?: number;
|
|
33
|
+
redisHost?: string;
|
|
34
|
+
redisPort?: number;
|
|
35
|
+
redisPassword?: string;
|
|
36
|
+
redisPrefix?: string;
|
|
37
|
+
redisDb?: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* MongoDB session handler using raw TCP (MongoDB wire protocol).
|
|
41
|
+
*
|
|
42
|
+
* Uses synchronous socket communication via child process — no external
|
|
43
|
+
* MongoDB client library required. Stores session data as BSON documents
|
|
44
|
+
* with TTL index support.
|
|
45
|
+
*/
|
|
46
|
+
export declare class MongoSessionHandler implements SessionHandler {
|
|
47
|
+
private host;
|
|
48
|
+
private port;
|
|
49
|
+
private uri;
|
|
50
|
+
private username;
|
|
51
|
+
private password;
|
|
52
|
+
private database;
|
|
53
|
+
private collection;
|
|
54
|
+
private hostExplicit;
|
|
55
|
+
private portExplicit;
|
|
56
|
+
private uriExplicit;
|
|
57
|
+
constructor(config?: MongoSessionConfig);
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the effective host/port (honours a configured mongodb:// URI).
|
|
60
|
+
*
|
|
61
|
+
* PRECEDENCE, and it runs the way every other resolver in Tina4 runs -
|
|
62
|
+
* EXPLICIT CONFIGURATION BEATS THE ENVIRONMENT:
|
|
63
|
+
*
|
|
64
|
+
* 1. an explicitly passed `uri` - the caller named a complete address
|
|
65
|
+
* 2. an explicitly passed host/port - per field, and an ENV uri may not touch them
|
|
66
|
+
* 3. TINA4_SESSION_MONGO_URI / _URL - the ambient address
|
|
67
|
+
* 4. TINA4_SESSION_MONGO_HOST/_PORT - ambient parts
|
|
68
|
+
* 5. 127.0.0.1:27017
|
|
69
|
+
*
|
|
70
|
+
* THE DEFECT THIS FIXES, measured 2026-08-05 on the lab host: the URI won
|
|
71
|
+
* UNCONDITIONALLY, including over an argument the caller had just passed by
|
|
72
|
+
* hand. With TINA4_SESSION_MONGO_URI=mongodb://127.0.0.1:27017/tina4_node
|
|
73
|
+
* exported - an entirely ordinary deployment setting -
|
|
74
|
+
*
|
|
75
|
+
* new MongoSessionHandler({ host: "127.0.0.1", port: 59999 })
|
|
76
|
+
*
|
|
77
|
+
* resolved to 127.0.0.1:27017. The handler dialled a DIFFERENT SERVER from the
|
|
78
|
+
* one it was told to use, said nothing, and a read against it came back null:
|
|
79
|
+
* indistinguishable from a genuine miss. So an app that points a handler at one
|
|
80
|
+
* Mongo while the environment names another writes its sessions to the wrong
|
|
81
|
+
* server, and the backend-failure policy cannot fire because nothing failed.
|
|
82
|
+
*
|
|
83
|
+
* It also made two suites report a framework contract as broken - the
|
|
84
|
+
* unreachable-server-must-throw cases in sessionHandlers and
|
|
85
|
+
* sessionMongoRawProtocol never reached the dead port at all, so they measured
|
|
86
|
+
* a live server and got a miss. Those cases were RIGHT; this was the bug they
|
|
87
|
+
* were catching.
|
|
88
|
+
*
|
|
89
|
+
* Same class as the TINA4_QUEUE_URL precedence inversion fixed in PHP's
|
|
90
|
+
* Queue::resolveMongoConfig earlier the same day: environment quietly beating
|
|
91
|
+
* an explicit argument.
|
|
92
|
+
*/
|
|
93
|
+
private target;
|
|
94
|
+
read(sessionId: string): SessionData | null;
|
|
95
|
+
/**
|
|
96
|
+
* Write session data.
|
|
97
|
+
*
|
|
98
|
+
* The ttl is consumed HERE, at write time, and baked into an absolute deadline,
|
|
99
|
+
* so nothing at read time needs to know what the ttl was. The parameter used to
|
|
100
|
+
* be named `_ttl` and thrown away.
|
|
101
|
+
*
|
|
102
|
+
* @param sessionId - the session id
|
|
103
|
+
* @param data - the payload to store
|
|
104
|
+
* @param ttl - lifetime in seconds; 0 or less means never expires
|
|
105
|
+
*/
|
|
106
|
+
write(sessionId: string, data: SessionData, ttl?: number): void;
|
|
107
|
+
destroy(sessionId: string): void;
|
|
108
|
+
}
|
|
109
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface RespTarget {
|
|
2
|
+
host: string;
|
|
3
|
+
port: number;
|
|
4
|
+
/** AUTH password (empty/undefined = no AUTH sent). */
|
|
5
|
+
password?: string;
|
|
6
|
+
/** SELECT db index (0/undefined = no SELECT sent). */
|
|
7
|
+
db?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Run a single RESP command synchronously against host:port and return the reply.
|
|
11
|
+
*
|
|
12
|
+
* - A genuine nil / key-miss returns `""` (callers treat "" as "no session yet").
|
|
13
|
+
* - A transport FAILURE (server unreachable, timeout, connection closed before a
|
|
14
|
+
* reply) THROWS `<label> command failed: ...`.
|
|
15
|
+
* - A RESP error reply — including a rejected AUTH/SELECT handshake — THROWS
|
|
16
|
+
* `<label> error: ...`. A rejected handshake is a transport failure, not a
|
|
17
|
+
* result, so it is surfaced ahead of the command reply.
|
|
18
|
+
*
|
|
19
|
+
* The miss/failure split is the whole contract: collapsing them is how a dead
|
|
20
|
+
* backend silently logs every user out instead of surfacing an outage.
|
|
21
|
+
*/
|
|
22
|
+
export declare function respCommandSync(target: RespTarget, args: string[], label?: string): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SQL engines the database session backend speaks.
|
|
3
|
+
*
|
|
4
|
+
* This IS the invariant: it is the engine set of the ORM Database layer minus
|
|
5
|
+
* the two non-SQL entries (mongodb has its own session backend, odbc has no
|
|
6
|
+
* session story in any of the four frameworks). Naming it once means the
|
|
7
|
+
* refusal message and the dispatch can never disagree about what is supported.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SQL_SESSION_ENGINES: readonly ["sqlite", "postgres", "mysql", "mssql", "firebird"];
|
|
10
|
+
export type SqlSessionEngine = (typeof SQL_SESSION_ENGINES)[number];
|
|
11
|
+
/** The engines that need the bridge - everything except already-sync SQLite. */
|
|
12
|
+
export type BridgedEngine = Exclude<SqlSessionEngine, "sqlite">;
|
|
13
|
+
/**
|
|
14
|
+
* A connection target for the worker.
|
|
15
|
+
*
|
|
16
|
+
* A PLAIN object, deliberately - never a `DatabaseUrl`. That class carries a
|
|
17
|
+
* cleartext password and its own docblock forbids persisting it across a
|
|
18
|
+
* structured-clone boundary (test/databaseUrlRedaction.test.ts enforces it).
|
|
19
|
+
* The worker genuinely needs credentials to authenticate, so it gets the fields
|
|
20
|
+
* it needs and nothing that renders itself.
|
|
21
|
+
*/
|
|
22
|
+
export interface SqlTarget {
|
|
23
|
+
engine: BridgedEngine;
|
|
24
|
+
host: string;
|
|
25
|
+
port: number;
|
|
26
|
+
database: string;
|
|
27
|
+
username: string | null;
|
|
28
|
+
password: string | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Run one SQL statement synchronously against a networked engine.
|
|
32
|
+
*
|
|
33
|
+
* @returns the result rows - always an array, empty for a write or DDL.
|
|
34
|
+
* @throws Error on ANY driver failure (server unreachable, bad credentials,
|
|
35
|
+
* SQL error). It is never swallowed into an empty result: for a session
|
|
36
|
+
* store, "the database is down" and "no session yet" must stay
|
|
37
|
+
* distinguishable, or a dead backend silently logs every user out.
|
|
38
|
+
*/
|
|
39
|
+
export declare function sqlCommandSync(target: SqlTarget, sql: string, params?: unknown[], label?: string): Record<string, unknown>[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 sync-over-async bridge — call an ASYNC worker from SYNCHRONOUS code.
|
|
3
|
+
*
|
|
4
|
+
* The SessionHandler interface is synchronous; every backend client Node offers
|
|
5
|
+
* (node:net, the mongodb driver) is async-only. The old resolution was to run
|
|
6
|
+
* each command in a short-lived `node -e` child and block on execFileSync. That
|
|
7
|
+
* works but pays a process spawn — and a fresh connection, and for Mongo a fresh
|
|
8
|
+
* driver load — PER COMMAND. Measured on this machine: spawn min 38ms / p50 41ms
|
|
9
|
+
* / p99 487ms, with a bare TCP connect adding a ~0.5-0.9s tail of its own. That
|
|
10
|
+
* tail tripped the child's deadline under load, which is what made the Valkey
|
|
11
|
+
* session tests flaky with the signature "a value that was just written reads
|
|
12
|
+
* back null" — the write timed out for the caller while still landing on the
|
|
13
|
+
* server.
|
|
14
|
+
*
|
|
15
|
+
* This is the ONE piece of plumbing that replaces it. A Worker thread keeps its
|
|
16
|
+
* own event loop, so it can do ordinary async I/O and hold a long-lived
|
|
17
|
+
* connection. The caller hands it a message with postMessage (delivered on the
|
|
18
|
+
* WORKER's loop, so a blocked main thread cannot deadlock it), then blocks in
|
|
19
|
+
* Atomics.wait until the worker writes a reply into a SharedArrayBuffer and
|
|
20
|
+
* Atomics.notify wakes it.
|
|
21
|
+
*
|
|
22
|
+
* Every session backend that needs sync-over-async uses this — RESP
|
|
23
|
+
* (Redis/Valkey), memcached's text protocol, and MongoDB — so the blocking
|
|
24
|
+
* handshake exists once rather than once per backend.
|
|
25
|
+
*/
|
|
26
|
+
import { Worker } from "node:worker_threads";
|
|
27
|
+
/** Reply status, written by the worker into control[IDX_STATUS]. */
|
|
28
|
+
export declare const STATUS_OK = 0;
|
|
29
|
+
/** A healthy server answered with an error (RESP `-ERR`, a Mongo command error). */
|
|
30
|
+
export declare const STATUS_ERROR = 1;
|
|
31
|
+
/** A genuine miss — no such key/document. NOT a failure. */
|
|
32
|
+
export declare const STATUS_NIL = 2;
|
|
33
|
+
/** The reply did not fit the shared buffer. */
|
|
34
|
+
export declare const STATUS_TOO_LARGE = 3;
|
|
35
|
+
/** Connection/socket/driver failure, as opposed to an answer from a healthy server. */
|
|
36
|
+
export declare const STATUS_TRANSPORT = 4;
|
|
37
|
+
export declare const IDX_SEQ = 0;
|
|
38
|
+
export declare const IDX_LENGTH = 1;
|
|
39
|
+
export declare const IDX_STATUS = 2;
|
|
40
|
+
export declare const IDX_READY = 3;
|
|
41
|
+
/**
|
|
42
|
+
* Reply payload ceiling. A session document beyond this is pathological, and a
|
|
43
|
+
* fixed buffer keeps the fast path allocation-free. A worker reports
|
|
44
|
+
* STATUS_TOO_LARGE rather than truncating — a silently truncated session would
|
|
45
|
+
* deserialise into garbage.
|
|
46
|
+
*/
|
|
47
|
+
export declare const DATA_BYTES: number;
|
|
48
|
+
/** How long a caller blocks before giving up on a reply. */
|
|
49
|
+
export declare const REPLY_TIMEOUT_MS = 5000;
|
|
50
|
+
/**
|
|
51
|
+
* How long the FIRST caller waits for a brand-new worker to come up.
|
|
52
|
+
*
|
|
53
|
+
* Boot and the command round-trip must not share one budget. Cold start is
|
|
54
|
+
* normally ~27ms, but on a loaded machine (the session suite spawns a batch of
|
|
55
|
+
* blocking Mongo children immediately beforehand) it can stretch — and when it
|
|
56
|
+
* ate into the 5s command budget the very first Valkey write failed with
|
|
57
|
+
* "timed out after 5000ms", which read exactly like the flake this transport
|
|
58
|
+
* was built to remove. Boot gets its own generous budget so the per-command
|
|
59
|
+
* timeout can stay tight and mean what it says.
|
|
60
|
+
*/
|
|
61
|
+
export declare const BOOT_TIMEOUT_MS = 15000;
|
|
62
|
+
/**
|
|
63
|
+
* The worker-side helper, injected into every worker body. Kept here so the
|
|
64
|
+
* reply protocol is written once: a worker only has to call
|
|
65
|
+
* `__reply(status, payload)` and never touches Atomics itself.
|
|
66
|
+
*/
|
|
67
|
+
export declare const WORKER_REPLY_HELPER = "\nconst __control = new Int32Array(workerData.controlBuffer);\nconst __data = new Uint8Array(workerData.dataBuffer);\nconst __encoder = new TextEncoder();\n\n// Announce readiness the moment this thread is executing. The parent blocks on\n// this before its first command, so a slow boot can never be mistaken for a\n// slow command. This is signalled by the worker's OWN bootstrap and needs no\n// message from the parent, so it is safe even while the parent is blocked.\nAtomics.store(__control, 3, 1);\nAtomics.notify(__control, 3);\n\nfunction __reply(status, payload) {\n let length = 0;\n if (payload !== undefined && payload !== null && status !== 2) {\n const bytes = __encoder.encode(String(payload));\n if (bytes.length > __data.length) {\n Atomics.store(__control, 2, 3);\n Atomics.store(__control, 1, 0);\n Atomics.add(__control, 0, 1);\n Atomics.notify(__control, 0);\n return;\n }\n __data.set(bytes, 0);\n length = bytes.length;\n }\n Atomics.store(__control, 2, status);\n Atomics.store(__control, 1, length);\n Atomics.add(__control, 0, 1);\n Atomics.notify(__control, 0);\n}\n";
|
|
68
|
+
export interface BridgeReply {
|
|
69
|
+
status: number;
|
|
70
|
+
payload: string;
|
|
71
|
+
}
|
|
72
|
+
export interface Bridge {
|
|
73
|
+
/** Send a message to the worker and BLOCK until it replies. */
|
|
74
|
+
call(message: unknown, label: string): BridgeReply;
|
|
75
|
+
worker: Worker;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get (or create) the bridge for a key. One worker per key, created once and
|
|
79
|
+
* unref'd so it can never hold the process open.
|
|
80
|
+
*
|
|
81
|
+
* @param key Identity of the connection target — same key, same worker
|
|
82
|
+
* @param workerSource The worker body; WORKER_REPLY_HELPER is prepended for it
|
|
83
|
+
* @param workerData Passed to the worker verbatim (plus the shared buffers)
|
|
84
|
+
*/
|
|
85
|
+
export declare function getBridge(key: string, workerSource: string, workerData: Record<string, unknown>): Bridge;
|
|
86
|
+
/**
|
|
87
|
+
* Terminate every worker. Tests and short-lived scripts call this so a spawned
|
|
88
|
+
* worker never outlives the work that created it — "reap what you spawn". Normal
|
|
89
|
+
* apps do not need it: the workers are unref'd.
|
|
90
|
+
*/
|
|
91
|
+
export declare function closeBridges(): void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 synchronous socket transport — ONE persistent connection per target.
|
|
3
|
+
*
|
|
4
|
+
* Serves the RESP backends (Redis, Valkey) and memcached's text protocol. The
|
|
5
|
+
* sync-over-async plumbing lives in syncBridge; this file is only the socket
|
|
6
|
+
* worker and the two protocol entry points.
|
|
7
|
+
*
|
|
8
|
+
* See syncBridge for WHY: each command used to run in a short-lived `node -e`
|
|
9
|
+
* child, paying a process spawn AND a fresh TCP connection every time (spawn p50
|
|
10
|
+
* 41ms / p99 487ms, connect tail 0.5-0.9s), and that tail tripped the child's
|
|
11
|
+
* deadline under load — the cause of the Valkey session flake.
|
|
12
|
+
*
|
|
13
|
+
* Reconnection is the worker's business: a dropped socket is re-established on
|
|
14
|
+
* the next command rather than surfacing as a caller error.
|
|
15
|
+
*/
|
|
16
|
+
import { closeBridges, DATA_BYTES } from "./syncBridge.js";
|
|
17
|
+
export interface SyncSocketTarget {
|
|
18
|
+
host: string;
|
|
19
|
+
port: number;
|
|
20
|
+
/** AUTH password (empty/undefined = no AUTH sent). */
|
|
21
|
+
password?: string;
|
|
22
|
+
/** SELECT db index (0/undefined = no SELECT sent). */
|
|
23
|
+
db?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Run a single RESP command synchronously and return the reply.
|
|
27
|
+
*
|
|
28
|
+
* - A genuine nil / key-miss returns `""` (callers treat "" as "no session yet").
|
|
29
|
+
* - A transport FAILURE (unreachable, timeout, connection closed) THROWS
|
|
30
|
+
* `<label> command failed: ...`.
|
|
31
|
+
* - A RESP error reply — including a rejected AUTH/SELECT — THROWS
|
|
32
|
+
* `<label> error: ...`.
|
|
33
|
+
*
|
|
34
|
+
* The miss/failure split is the whole contract: collapsing them is how a dead
|
|
35
|
+
* backend silently logs every user out instead of surfacing an outage. A dead
|
|
36
|
+
* socket and a healthy server saying "-ERR" are different failures and read
|
|
37
|
+
* differently in a log, so they keep different wording.
|
|
38
|
+
*/
|
|
39
|
+
export declare function syncCommand(target: SyncSocketTarget, args: string[], label?: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Run a raw text-protocol command (memcached) over the same persistent channel.
|
|
42
|
+
*
|
|
43
|
+
* The reply is whatever arrived once one of `terminators` appears — a text
|
|
44
|
+
* protocol carries no length prefix, so the caller names its own end markers.
|
|
45
|
+
* Failure semantics match {@link syncCommand}.
|
|
46
|
+
*/
|
|
47
|
+
export declare function syncTextCommand(target: SyncSocketTarget, payload: string, terminators: string[], label?: string): string;
|
|
48
|
+
/** Re-exported so callers do not need to know the bridge exists. */
|
|
49
|
+
export { closeBridges as closeSyncSockets, DATA_BYTES };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 Valkey Session Handler — Valkey (Redis-compatible) via raw TCP, zero dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Same as the Redis handler but uses VALKEY-prefixed configuration variables.
|
|
5
|
+
* Valkey is a Redis-compatible key-value store fork.
|
|
6
|
+
*
|
|
7
|
+
* Configure via environment variables:
|
|
8
|
+
* TINA4_SESSION_VALKEY_HOST (default: "127.0.0.1")
|
|
9
|
+
* TINA4_SESSION_VALKEY_PORT (default: 6379)
|
|
10
|
+
* TINA4_SESSION_VALKEY_PASSWORD (optional)
|
|
11
|
+
* TINA4_SESSION_VALKEY_PREFIX (default: "tina4:session:")
|
|
12
|
+
* TINA4_SESSION_VALKEY_DB (default: 0)
|
|
13
|
+
*/
|
|
14
|
+
import type { SessionHandler } from "../session.js";
|
|
15
|
+
interface SessionData {
|
|
16
|
+
_created: number;
|
|
17
|
+
_accessed: number;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface ValkeySessionConfig {
|
|
21
|
+
host?: string;
|
|
22
|
+
port?: number;
|
|
23
|
+
password?: string;
|
|
24
|
+
prefix?: string;
|
|
25
|
+
db?: number;
|
|
26
|
+
backend?: string;
|
|
27
|
+
path?: string;
|
|
28
|
+
ttl?: number;
|
|
29
|
+
redisHost?: string;
|
|
30
|
+
redisPort?: number;
|
|
31
|
+
redisPassword?: string;
|
|
32
|
+
redisPrefix?: string;
|
|
33
|
+
redisDb?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Valkey session handler using raw TCP (RESP protocol).
|
|
37
|
+
*
|
|
38
|
+
* Uses synchronous socket communication — no external Valkey/Redis client required.
|
|
39
|
+
* Stores session data as JSON strings with Valkey TTL for automatic expiry.
|
|
40
|
+
*
|
|
41
|
+
* Valkey uses the same RESP protocol as Redis, so this handler is functionally
|
|
42
|
+
* identical to RedisSessionHandler but with VALKEY config variable names.
|
|
43
|
+
*/
|
|
44
|
+
export declare class ValkeySessionHandler implements SessionHandler {
|
|
45
|
+
private host;
|
|
46
|
+
private port;
|
|
47
|
+
private password;
|
|
48
|
+
private prefix;
|
|
49
|
+
private db;
|
|
50
|
+
constructor(config?: ValkeySessionConfig);
|
|
51
|
+
/**
|
|
52
|
+
* Execute a RESP command synchronously against the live Valkey server.
|
|
53
|
+
*
|
|
54
|
+
* Delegates to the shared {@link respCommandSync} transport: a genuine key miss
|
|
55
|
+
* yields `""`, and a transport/connection FAILURE (server unreachable, rejected
|
|
56
|
+
* AUTH, timeout) THROWS so the Session boundary can distinguish "not found"
|
|
57
|
+
* (silent) from "backend failed" (log-loud + degrade). Backend-failure parity.
|
|
58
|
+
*/
|
|
59
|
+
private execSync;
|
|
60
|
+
private key;
|
|
61
|
+
read(sessionId: string): SessionData | null;
|
|
62
|
+
write(sessionId: string, data: SessionData, ttl: number): void;
|
|
63
|
+
destroy(sessionId: string): void;
|
|
64
|
+
}
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 — The Intelligent Native Application 4ramework
|
|
3
|
+
* Copyright 2007 - current Tina4
|
|
4
|
+
* License: MIT https://opensource.org/licenses/MIT
|
|
5
|
+
*
|
|
6
|
+
* Tina4 xUnit-style Test base class.
|
|
7
|
+
*
|
|
8
|
+
* Chapter 18 of the documentation has long shown:
|
|
9
|
+
*
|
|
10
|
+
* class UserApiTest extends Tina4Test {
|
|
11
|
+
* async testHealth() {
|
|
12
|
+
* const resp = await this.get("/health");
|
|
13
|
+
* this.assertEqual(resp.status, 200);
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* Until 3.13.0 this class did not exist — examples crashed with
|
|
18
|
+
* "ReferenceError: Tina4Test is not defined". This is the Node.js
|
|
19
|
+
* parity of the Python `tina4_python.test.Test`, PHP `Tina4\Test`,
|
|
20
|
+
* and Ruby `Tina4::Test` classes shipped at the same time.
|
|
21
|
+
*
|
|
22
|
+
* The class has a built-in runner (`Tina4Test.runAll`) so the docs'
|
|
23
|
+
* `npx tina4nodejs test` flow can discover every subclass without
|
|
24
|
+
* an external test framework. HTTP helpers (get/post/put/patch/delete)
|
|
25
|
+
* delegate to a lazy TestClient. Positional assertions match the
|
|
26
|
+
* cross-framework (actual, expected, message) shape.
|
|
27
|
+
*/
|
|
28
|
+
import { TestClient, type TestResponse, type RequestOptions } from "./testClient.js";
|
|
29
|
+
/** Raised by Tina4Test assertion helpers when an assertion fails. */
|
|
30
|
+
export declare class AssertionError extends Error {
|
|
31
|
+
constructor(message: string);
|
|
32
|
+
}
|
|
33
|
+
/** Result of running a Tina4Test subclass (or all subclasses). */
|
|
34
|
+
export interface TestRunResults {
|
|
35
|
+
passed: number;
|
|
36
|
+
failed: number;
|
|
37
|
+
errors: number;
|
|
38
|
+
details: Array<{
|
|
39
|
+
suite: string;
|
|
40
|
+
test: string;
|
|
41
|
+
status: "passed" | "failed" | "error";
|
|
42
|
+
message?: string;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Tina4 xUnit-style test base class — class-based suites with HTTP
|
|
47
|
+
* helpers and positional assertions, zero external deps.
|
|
48
|
+
*
|
|
49
|
+
* Subclass and define `test*` methods:
|
|
50
|
+
*
|
|
51
|
+
* class BasicTest extends Tina4Test {
|
|
52
|
+
* async testAddition() {
|
|
53
|
+
* this.assertEqual(2 + 2, 4, "addition works");
|
|
54
|
+
* }
|
|
55
|
+
* async testHttpHealth() {
|
|
56
|
+
* const resp = await this.get("/health");
|
|
57
|
+
* this.assertEqual(resp.status, 200);
|
|
58
|
+
* }
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* const results = await Tina4Test.runAll();
|
|
62
|
+
* // → { passed, failed, errors, details }
|
|
63
|
+
*/
|
|
64
|
+
export declare class Tina4Test {
|
|
65
|
+
private _client;
|
|
66
|
+
/** snake_case lifecycle hook — runs before each test. Override in subclasses. */
|
|
67
|
+
setUp(): Promise<void>;
|
|
68
|
+
/** snake_case lifecycle hook — runs after each test. Override in subclasses. */
|
|
69
|
+
tearDown(): Promise<void>;
|
|
70
|
+
/** The lazily-created TestClient instance shared by this suite's tests. */
|
|
71
|
+
protected get client(): TestClient;
|
|
72
|
+
get(path: string, options?: RequestOptions): Promise<TestResponse>;
|
|
73
|
+
post(path: string, options?: RequestOptions): Promise<TestResponse>;
|
|
74
|
+
put(path: string, options?: RequestOptions): Promise<TestResponse>;
|
|
75
|
+
patch(path: string, options?: RequestOptions): Promise<TestResponse>;
|
|
76
|
+
delete(path: string, options?: RequestOptions): Promise<TestResponse>;
|
|
77
|
+
assertEqual(actual: unknown, expected: unknown, message?: string): void;
|
|
78
|
+
assertNotEqual(actual: unknown, expected: unknown, message?: string): void;
|
|
79
|
+
assertTrue(value: unknown, message?: string): void;
|
|
80
|
+
assertFalse(value: unknown, message?: string): void;
|
|
81
|
+
assertNull(value: unknown, message?: string): void;
|
|
82
|
+
assertNotNull(value: unknown, message?: string): void;
|
|
83
|
+
assertRaises(expectedClass: new (...args: never[]) => Error, fn: () => unknown | Promise<unknown>, message?: string): Promise<void>;
|
|
84
|
+
/** Register a subclass for discovery. Called automatically via `extends Tina4Test`. */
|
|
85
|
+
static register(klass: typeof Tina4Test): void;
|
|
86
|
+
/** Run every `test*` method on this class. Returns counts and per-test details. */
|
|
87
|
+
static run(this: typeof Tina4Test): Promise<TestRunResults>;
|
|
88
|
+
/** Run every Tina4Test subclass discovered via auto-registration. */
|
|
89
|
+
static runAll(options?: {
|
|
90
|
+
quiet?: boolean;
|
|
91
|
+
}): Promise<TestRunResults>;
|
|
92
|
+
/** Subclasses array — read-only view used by tests. */
|
|
93
|
+
static get subclasses(): ReadonlyArray<typeof Tina4Test>;
|
|
94
|
+
}
|