tina4-nodejs 3.13.94 → 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 +157 -28
- package/README.md +1 -1
- package/package.json +2 -1
- package/packages/cli/dist/bin.js +32418 -29638
- package/packages/cli/src/commands/metrics.ts +17 -11
- package/packages/cli/src/commands/serve.ts +10 -9
- package/packages/core/dist/index.js +32364 -29501
- 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/dispatchPipeline.ts +285 -0
- package/packages/core/src/dotenv.ts +185 -40
- package/packages/core/src/index.ts +5 -4
- package/packages/core/src/logger.ts +257 -36
- package/packages/core/src/mcp.ts +1 -1
- package/packages/core/src/messenger.ts +9 -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 +1 -1
- 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/databaseHandler.ts +338 -48
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +293 -208
- package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
- package/packages/core/src/sessionHandlers/respClient.ts +16 -147
- 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/orm/dist/index.js +22367 -19504
- 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 +60 -24
- 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 +237 -197
- 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/core/src/ai.d.ts +1 -1
- package/types/core/src/auth.d.ts +28 -5
- package/types/core/src/background.d.ts +3 -3
- package/types/core/src/cache.d.ts +15 -12
- package/types/core/src/dispatchPipeline.d.ts +117 -0
- package/types/core/src/dotenv.d.ts +38 -16
- package/types/core/src/index.d.ts +5 -6
- package/types/core/src/logger.d.ts +93 -16
- package/types/core/src/messenger.d.ts +2 -2
- package/types/core/src/metrics.d.ts +25 -61
- package/types/core/src/middleware.d.ts +134 -11
- package/types/core/src/queue.d.ts +54 -5
- package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -1
- package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +24 -2
- package/types/core/src/queueBackends/rabbitmqBackend.d.ts +3 -3
- package/types/core/src/router.d.ts +14 -3
- package/types/core/src/server.d.ts +15 -0
- package/types/core/src/session.d.ts +87 -2
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +60 -5
- package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
- package/types/core/src/sessionHandlers/mongoClient.d.ts +16 -5
- package/types/core/src/sessionHandlers/mongoHandler.d.ts +51 -3
- package/types/core/src/sessionHandlers/respClient.d.ts +2 -2
- 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/trustedProxy.d.ts +44 -0
- package/types/core/src/types.d.ts +28 -5
- package/types/core/src/websocket.d.ts +26 -0
- package/types/orm/src/adapters/firebird.d.ts +55 -10
- package/types/orm/src/adapters/mongodb.d.ts +2 -2
- package/types/orm/src/adapters/mssql.d.ts +18 -11
- package/types/orm/src/adapters/mysql.d.ts +11 -10
- package/types/orm/src/adapters/odbc.d.ts +9 -12
- package/types/orm/src/adapters/postgres.d.ts +11 -10
- package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
- package/types/orm/src/adapters/sqlite.d.ts +15 -3
- package/types/orm/src/baseModel.d.ts +45 -9
- package/types/orm/src/cachedDatabase.d.ts +18 -5
- package/types/orm/src/connectTimeout.d.ts +100 -0
- package/types/orm/src/database.d.ts +72 -26
- package/types/orm/src/databaseResult.d.ts +24 -0
- package/types/orm/src/databaseUrl.d.ts +125 -0
- package/types/orm/src/docstore.d.ts +102 -43
- package/types/orm/src/index.d.ts +5 -2
- package/types/orm/src/queryBuilder.d.ts +23 -3
- package/types/orm/src/sqlTranslator.d.ts +126 -2
- package/types/orm/src/types.d.ts +14 -4
- package/packages/core/src/sessionHandlers/redisHandler.ts +0 -219
- package/types/core/src/sessionHandlers/redisHandler.d.ts +0 -60
|
@@ -78,6 +78,102 @@ export declare class SQLTranslator {
|
|
|
78
78
|
* first dot. Firebird has no schemas, so its adapter ignores this.
|
|
79
79
|
*/
|
|
80
80
|
static splitSchema(name: string): [string | null, string];
|
|
81
|
+
/**
|
|
82
|
+
* Hard per-statement bind-parameter ceiling per engine. 0 = never collapse.
|
|
83
|
+
* Sourced from test/fixtures/batch_write_contract.json, byte-identical in all
|
|
84
|
+
* four frameworks.
|
|
85
|
+
*/
|
|
86
|
+
static readonly MAX_BIND_PARAMS: Record<string, number>;
|
|
87
|
+
/**
|
|
88
|
+
* The four frameworks do not agree on what an engine calls itself — Python
|
|
89
|
+
* and PHP report "postgresql", Ruby and Node report "postgres". Without
|
|
90
|
+
* normalising, the cap lookup misses and the collapse silently does nothing
|
|
91
|
+
* on the engine with the largest win.
|
|
92
|
+
*/
|
|
93
|
+
static readonly ENGINE_ALIASES: Record<string, string>;
|
|
94
|
+
private static readonly INSERT_VALUES;
|
|
95
|
+
/**
|
|
96
|
+
* Engines whose lastInsertId reports the FIRST generated id of a multi-row
|
|
97
|
+
* INSERT rather than the last. Verified live, not assumed: a 3-row insert
|
|
98
|
+
* into a fresh MySQL table reports 1 while MAX(id) is 3. SQLite, PostgreSQL
|
|
99
|
+
* and MSSQL already report the last, so collapsing does not change them.
|
|
100
|
+
*/
|
|
101
|
+
static readonly FIRST_ID_ENGINES: readonly string[];
|
|
102
|
+
/**
|
|
103
|
+
* Normalise a collapsed batch's last id to the LAST row's id.
|
|
104
|
+
*
|
|
105
|
+
* A row-at-a-time batch reports the last row's id simply because the last
|
|
106
|
+
* statement inserted the last row. Collapsing rows into one statement changes
|
|
107
|
+
* that on any engine that reports the FIRST generated id, so this restores
|
|
108
|
+
* the contract instead of quietly redefining it. The ids in one statement are
|
|
109
|
+
* consecutive, so the last is `first + rows - 1`.
|
|
110
|
+
*/
|
|
111
|
+
static batchLastId(reportedId: unknown, rowsInChunk: number, engine: string): unknown;
|
|
112
|
+
/**
|
|
113
|
+
* Collapse a row-at-a-time INSERT batch into chunked multi-row VALUES.
|
|
114
|
+
*
|
|
115
|
+
* A batch that loops one INSERT per row pays a full network round-trip per
|
|
116
|
+
* row, and the round-trip — not SQL building — is the entire cost of a batch
|
|
117
|
+
* write. Measured over 500 rows: PostgreSQL 9848ms row-at-a-time against
|
|
118
|
+
* 15.8ms as a single multi-row statement (625x), MySQL 216x, MSSQL 121x.
|
|
119
|
+
*
|
|
120
|
+
* PURE: no I/O and no engine contact, so the chunking rules are checkable
|
|
121
|
+
* without a database. The live-engine runners prove the rows land.
|
|
122
|
+
*
|
|
123
|
+
* @returns Statements to run INSTEAD of the loop, or an EMPTY array meaning
|
|
124
|
+
* "not collapsible — keep looping", which is always correct.
|
|
125
|
+
*/
|
|
126
|
+
static buildBatchInserts(sql: string, paramSets: unknown[][], engine: string): Array<[string, unknown[]]>;
|
|
127
|
+
/**
|
|
128
|
+
* Blank out string literals, quoted identifiers and comments, so a keyword
|
|
129
|
+
* search sees only real SQL. Blanks are spaces of the SAME LENGTH (newlines
|
|
130
|
+
* preserved), so offsets and line structure still line up with the original.
|
|
131
|
+
*
|
|
132
|
+
* This exists because "does the caller's SQL already have a LIMIT?" used to be
|
|
133
|
+
* `sql.toUpperCase().split("--")[0].includes("LIMIT")`, and MEASURED on a real
|
|
134
|
+
* 150-row table with the 100-row cap in force, every one of these returned
|
|
135
|
+
* ALL 150 ROWS instead of 100:
|
|
136
|
+
*
|
|
137
|
+
* SELECT * FROM t WHERE label != 'LIMIT' ORDER BY id -- literal
|
|
138
|
+
* SELECT * FROM t ORDER BY id -- LIMIT 5 -- line comment
|
|
139
|
+
* SELECT * FROM t ORDER BY id /* LIMIT 5 *\/ -- block comment
|
|
140
|
+
*
|
|
141
|
+
* A column named `rate_limit` does it too. That is a silently UNCAPPED read of
|
|
142
|
+
* a whole table, which is the exact production incident the row cap exists to
|
|
143
|
+
* prevent, reachable through an ordinary column name.
|
|
144
|
+
*
|
|
145
|
+
* @param sql Raw SQL, exactly as the caller wrote it.
|
|
146
|
+
* @returns The same string with literals and comments replaced by spaces.
|
|
147
|
+
*/
|
|
148
|
+
static scrubSqlText(sql: string): string;
|
|
149
|
+
/**
|
|
150
|
+
* True when the statement ENDS with its own LIMIT clause, so appending another
|
|
151
|
+
* would be wrong (and on SQLite, a syntax error).
|
|
152
|
+
*
|
|
153
|
+
* Anchored to the END on purpose. A bare "contains LIMIT" test also matches a
|
|
154
|
+
* LIMIT inside a subquery, where the OUTER statement still needs its cap. This
|
|
155
|
+
* is tina4-php's `SqlNormalizerTrait::hasTrailingLimit` regex, ported verbatim
|
|
156
|
+
* so all four frameworks answer identically: it accepts a numeric value, `?`,
|
|
157
|
+
* `$1` and `:name` placeholders, MySQL's `LIMIT a, b`, and a trailing OFFSET.
|
|
158
|
+
*
|
|
159
|
+
* @param sql Raw SQL; literals and comments are scrubbed before matching.
|
|
160
|
+
*/
|
|
161
|
+
static hasTrailingLimit(sql: string): boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Append `LIMIT`/`OFFSET` to a statement unless it already carries its own.
|
|
164
|
+
*
|
|
165
|
+
* The clause goes on a NEW LINE. Appending it inline is the second half of the
|
|
166
|
+
* same bug: `SELECT * FROM t -- note` + ` LIMIT 100` puts the clause INSIDE the
|
|
167
|
+
* trailing comment, where SQLite silently ignores it and the whole table comes
|
|
168
|
+
* back. A newline cannot be commented out by a `--` that started on the line
|
|
169
|
+
* above. Trailing semicolons are stripped first for the same reason
|
|
170
|
+
* (`SELECT * FROM t;` + `LIMIT 100` is a syntax error).
|
|
171
|
+
*
|
|
172
|
+
* @param sql The caller's statement.
|
|
173
|
+
* @param limit Row cap to apply; a non-positive value means "no cap".
|
|
174
|
+
* @param offset Rows to skip; omitted or 0 emits no OFFSET.
|
|
175
|
+
*/
|
|
176
|
+
static appendLimit(sql: string, limit?: number, offset?: number): string;
|
|
81
177
|
}
|
|
82
178
|
/**
|
|
83
179
|
* Simple in-memory query cache with TTL support.
|
|
@@ -91,9 +187,37 @@ export declare class QueryCache {
|
|
|
91
187
|
maxSize?: number;
|
|
92
188
|
});
|
|
93
189
|
/**
|
|
94
|
-
*
|
|
190
|
+
* Stable identity of the DATABASE a cache entry came from.
|
|
191
|
+
*
|
|
192
|
+
* `engine://host:port/database` - and deliberately NOTHING else.
|
|
193
|
+
*
|
|
194
|
+
* WHY IT EXISTS: the key used to be `query:${sql}:${params}` with nothing
|
|
195
|
+
* naming the connection, so on any SHARED backend two databases cross-served
|
|
196
|
+
* each other's rows. Two apps pointed at one Redis, or one app with a primary
|
|
197
|
+
* and an analytics connection, silently read each other's data. Identical SQL
|
|
198
|
+
* text across tenants is the COMMON case, not an edge case, so the collision
|
|
199
|
+
* was the normal outcome.
|
|
200
|
+
*
|
|
201
|
+
* WHY NO CREDENTIALS: a password in the key means every rotation silently
|
|
202
|
+
* cold-starts the cache, and a shared backend's key namespace is visible to
|
|
203
|
+
* every tenant of that backend - a secret must never be folded into it. The
|
|
204
|
+
* username is out for the same reason plus a second: two connections
|
|
205
|
+
* differing only by role read the SAME rows and should share the entry.
|
|
206
|
+
*
|
|
207
|
+
* WHY NOTHING PER-PROCESS: no pid, no object id, no salt. Those would isolate
|
|
208
|
+
* the databases by ACCIDENT and destroy the point of a shared cache, because
|
|
209
|
+
* no instance would ever hit another instance's entry.
|
|
210
|
+
*/
|
|
211
|
+
static cacheIdentity(url: string): string;
|
|
212
|
+
/**
|
|
213
|
+
* Generate a cache key from DATABASE IDENTITY + SQL + params.
|
|
214
|
+
*
|
|
215
|
+
* The NUL separators keep the three parts from running together, so a table
|
|
216
|
+
* named after the tail of a database name cannot forge another database's
|
|
217
|
+
* key. The key is not hashed here: the only backend with a key-length limit
|
|
218
|
+
* is memcached, and its backend already SHA-256-hashes whatever it is given.
|
|
95
219
|
*/
|
|
96
|
-
static queryKey(sql: string, params?: unknown[]): string;
|
|
220
|
+
static queryKey(sql: string, params?: unknown[], identity?: string): string;
|
|
97
221
|
/**
|
|
98
222
|
* Get a cached value. Returns undefined if expired or missing.
|
|
99
223
|
*/
|
package/types/orm/src/types.d.ts
CHANGED
|
@@ -66,8 +66,8 @@ export interface DatabaseAdapter {
|
|
|
66
66
|
fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
|
|
67
67
|
/** Insert one or more rows into a table, returns result with lastId. */
|
|
68
68
|
insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult;
|
|
69
|
-
/** Update rows in a table matching filter, returns affected row count. */
|
|
70
|
-
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown
|
|
69
|
+
/** Update rows in a table matching filter (object or string WHERE), returns affected row count. */
|
|
70
|
+
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): DatabaseResult;
|
|
71
71
|
/** Delete rows from a table matching filter (object, string WHERE, or array of objects). */
|
|
72
72
|
delete(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): DatabaseResult;
|
|
73
73
|
/** Start a transaction. */
|
|
@@ -77,9 +77,9 @@ export interface DatabaseAdapter {
|
|
|
77
77
|
/** Rollback the current transaction. */
|
|
78
78
|
rollback(): void;
|
|
79
79
|
/** List all tables in the database. */
|
|
80
|
-
|
|
80
|
+
getTables(): string[];
|
|
81
81
|
/** List columns with types for a table. */
|
|
82
|
-
|
|
82
|
+
getColumns(table: string): ColumnInfo[];
|
|
83
83
|
/** Get the last inserted id (auto-increment integer, or a UUID/string PK). */
|
|
84
84
|
lastInsertId(): number | bigint | string | null;
|
|
85
85
|
/** Close the connection. */
|
|
@@ -95,6 +95,16 @@ export interface DatabaseAdapter {
|
|
|
95
95
|
}>;
|
|
96
96
|
/** Add a column to an existing table (legacy, used by migration). */
|
|
97
97
|
addColumn?(table: string, colName: string, def: FieldDefinition): void;
|
|
98
|
+
/**
|
|
99
|
+
* Stable identity of the DATABASE this adapter is connected to, as
|
|
100
|
+
* `engine://host:port/database` with NO credentials - set by whoever built
|
|
101
|
+
* the adapter from a URL or config.
|
|
102
|
+
*
|
|
103
|
+
* The query cache folds this into every key. Without it two databases sharing
|
|
104
|
+
* one cache backend cross-serve each other's rows, because identical SQL text
|
|
105
|
+
* across tenants is the common case.
|
|
106
|
+
*/
|
|
107
|
+
cacheIdentity?: string;
|
|
98
108
|
}
|
|
99
109
|
export interface PaginatedResult<T = Record<string, unknown>> {
|
|
100
110
|
data: T[];
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tina4 Redis Session Handler — Redis via `redis` npm package (optional dependency).
|
|
3
|
-
*
|
|
4
|
-
* Provides a session handler backed by the official `redis` npm package,
|
|
5
|
-
* complementing the built-in raw-TCP RedisSessionHandler in session.ts.
|
|
6
|
-
*
|
|
7
|
-
* This handler uses synchronous child-process execution (same pattern as
|
|
8
|
-
* valkeyHandler.ts) so it fits the synchronous SessionHandler interface
|
|
9
|
-
* without requiring async refactoring.
|
|
10
|
-
*
|
|
11
|
-
* Configure via environment variables:
|
|
12
|
-
* TINA4_SESSION_REDIS_HOST (default: "127.0.0.1")
|
|
13
|
-
* TINA4_SESSION_REDIS_PORT (default: 6379)
|
|
14
|
-
* TINA4_SESSION_REDIS_URL (optional — full redis:// URL, overrides host/port)
|
|
15
|
-
* TINA4_SESSION_REDIS_PASSWORD (optional)
|
|
16
|
-
* TINA4_SESSION_REDIS_PREFIX (default: "tina4:session:")
|
|
17
|
-
* TINA4_SESSION_REDIS_DB (default: 0)
|
|
18
|
-
*/
|
|
19
|
-
import { execFileSync } from "node:child_process";
|
|
20
|
-
import { childFailureError } from "./childError.js";
|
|
21
|
-
import { createRequire } from "node:module";
|
|
22
|
-
import type { SessionHandler } from "../session.js";
|
|
23
|
-
import { respCommandSync } from "./respClient.js";
|
|
24
|
-
|
|
25
|
-
// Resolve packages relative to this module so the optional `redis` driver is
|
|
26
|
-
// detected exactly as a consumer would resolve it (createRequire works in ESM).
|
|
27
|
-
const moduleRequire = createRequire(import.meta.url);
|
|
28
|
-
function redisDriverAvailable(): boolean {
|
|
29
|
-
try {
|
|
30
|
-
moduleRequire.resolve("redis");
|
|
31
|
-
return true;
|
|
32
|
-
} catch {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface SessionData {
|
|
38
|
-
_created: number;
|
|
39
|
-
_accessed: number;
|
|
40
|
-
[key: string]: unknown;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface RedisNpmSessionConfig {
|
|
44
|
-
host?: string;
|
|
45
|
-
port?: number;
|
|
46
|
-
url?: string;
|
|
47
|
-
password?: string;
|
|
48
|
-
prefix?: string;
|
|
49
|
-
db?: number;
|
|
50
|
-
// Unified SessionConfig fields are tolerated (and ignored) so the central
|
|
51
|
-
// Session can forward its config object without a structural mismatch.
|
|
52
|
-
backend?: string;
|
|
53
|
-
path?: string;
|
|
54
|
-
ttl?: number;
|
|
55
|
-
redisHost?: string;
|
|
56
|
-
redisPort?: number;
|
|
57
|
-
redisPassword?: string;
|
|
58
|
-
redisPrefix?: string;
|
|
59
|
-
redisDb?: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Redis session handler using the `redis` npm package.
|
|
64
|
-
*
|
|
65
|
-
* Falls back to raw TCP (RESP protocol) if the `redis` package is not
|
|
66
|
-
* installed, matching the approach used by the Valkey handler.
|
|
67
|
-
*
|
|
68
|
-
* Stores session data as JSON strings with Redis TTL for automatic expiry.
|
|
69
|
-
*/
|
|
70
|
-
export class RedisNpmSessionHandler implements SessionHandler {
|
|
71
|
-
private host: string;
|
|
72
|
-
private port: number;
|
|
73
|
-
private url: string;
|
|
74
|
-
private password: string;
|
|
75
|
-
private prefix: string;
|
|
76
|
-
private db: number;
|
|
77
|
-
|
|
78
|
-
constructor(config?: RedisNpmSessionConfig) {
|
|
79
|
-
this.url = config?.url
|
|
80
|
-
?? process.env.TINA4_SESSION_REDIS_URL
|
|
81
|
-
?? "";
|
|
82
|
-
this.host = config?.host
|
|
83
|
-
?? process.env.TINA4_SESSION_REDIS_HOST
|
|
84
|
-
?? "127.0.0.1";
|
|
85
|
-
this.port = config?.port
|
|
86
|
-
?? (process.env.TINA4_SESSION_REDIS_PORT
|
|
87
|
-
? parseInt(process.env.TINA4_SESSION_REDIS_PORT, 10)
|
|
88
|
-
: 6379);
|
|
89
|
-
this.password = config?.password
|
|
90
|
-
?? process.env.TINA4_SESSION_REDIS_PASSWORD
|
|
91
|
-
?? "";
|
|
92
|
-
this.prefix = config?.prefix
|
|
93
|
-
?? process.env.TINA4_SESSION_REDIS_PREFIX
|
|
94
|
-
?? "tina4:session:";
|
|
95
|
-
this.db = config?.db
|
|
96
|
-
?? (process.env.TINA4_SESSION_REDIS_DB
|
|
97
|
-
? parseInt(process.env.TINA4_SESSION_REDIS_DB, 10)
|
|
98
|
-
: 0);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Resolve a host/port for the raw-RESP path (parses TINA4_SESSION_REDIS_URL if set). */
|
|
102
|
-
private resolveHostPort(): { host: string; port: number } {
|
|
103
|
-
if (this.url) {
|
|
104
|
-
try {
|
|
105
|
-
const u = new URL(this.url);
|
|
106
|
-
return { host: u.hostname || "127.0.0.1", port: parseInt(u.port, 10) || 6379 };
|
|
107
|
-
} catch {
|
|
108
|
-
return { host: this.host, port: this.port };
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return { host: this.host, port: this.port };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Execute a Redis command synchronously.
|
|
116
|
-
*
|
|
117
|
-
* Prefers the official `redis` driver when it is installed (the class's reason
|
|
118
|
-
* to exist); otherwise speaks raw RESP via the shared {@link respCommandSync}
|
|
119
|
-
* transport — same correct path as the Valkey handler, no `redis` dependency.
|
|
120
|
-
*
|
|
121
|
-
* A genuine key miss yields `""`; a transport/connection FAILURE (server
|
|
122
|
-
* unreachable, rejected AUTH, timeout) THROWS so the Session boundary can
|
|
123
|
-
* distinguish "not found" (silent) from "backend failed" (log-loud + degrade).
|
|
124
|
-
*/
|
|
125
|
-
private execSync(args: string[]): string {
|
|
126
|
-
if (redisDriverAvailable()) {
|
|
127
|
-
return this.execViaNpm(args);
|
|
128
|
-
}
|
|
129
|
-
const { host, port } = this.resolveHostPort();
|
|
130
|
-
return respCommandSync({ host, port, password: this.password, db: this.db }, args, "Redis");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Drive the command through the `redis` npm client in a short-lived child. */
|
|
134
|
-
private execViaNpm(args: string[]): string {
|
|
135
|
-
const script = `
|
|
136
|
-
const useUrl = ${JSON.stringify(!!this.url)};
|
|
137
|
-
const url = ${JSON.stringify(this.url)};
|
|
138
|
-
const host = ${JSON.stringify(this.host)};
|
|
139
|
-
const port = ${this.port};
|
|
140
|
-
const password = ${JSON.stringify(this.password)};
|
|
141
|
-
const db = ${this.db};
|
|
142
|
-
const args = ${JSON.stringify(args)};
|
|
143
|
-
(async () => {
|
|
144
|
-
try {
|
|
145
|
-
const redis = require("redis");
|
|
146
|
-
// reconnectStrategy: false — this child runs ONE command and exits, so
|
|
147
|
-
// retrying inside it is pointless: the handler is called again on the
|
|
148
|
-
// next request anyway. With the driver's default strategy a refused
|
|
149
|
-
// connection never rejects, the child hangs until execFileSync's 5s
|
|
150
|
-
// timeout kills it, and the caller is told "timed out" when the truth
|
|
151
|
-
// is "connection refused". Off, connect() rejects in ~5ms with the real
|
|
152
|
-
// reason -- a better message AND no 5s stall per request when Redis is
|
|
153
|
-
// down.
|
|
154
|
-
const clientOpts = useUrl
|
|
155
|
-
? { url, socket: { reconnectStrategy: false } }
|
|
156
|
-
: { socket: { host, port, reconnectStrategy: false }, password: password || undefined, database: db };
|
|
157
|
-
const client = redis.createClient(clientOpts);
|
|
158
|
-
client.on("error", () => {});
|
|
159
|
-
await client.connect();
|
|
160
|
-
const cmd = args[0].toUpperCase();
|
|
161
|
-
let result;
|
|
162
|
-
if (cmd === "GET") result = await client.get(args[1]);
|
|
163
|
-
else if (cmd === "SET") result = await client.set(args[1], args[2]);
|
|
164
|
-
else if (cmd === "SETEX") result = await client.setEx(args[1], parseInt(args[2], 10), args[3]);
|
|
165
|
-
else if (cmd === "DEL") result = await client.del(args[1]);
|
|
166
|
-
await client.quit();
|
|
167
|
-
const out = (result === null || result === undefined) ? "__NULL__" : String(result);
|
|
168
|
-
process.stdout.write(out, () => process.exit(0));
|
|
169
|
-
} catch (err) {
|
|
170
|
-
// Exit from the write CALLBACK: stderr to a pipe is an async write and
|
|
171
|
-
// a bare process.exit() truncates it, which left the parent with an
|
|
172
|
-
// empty stderr and nothing but execFileSync's script-dump message.
|
|
173
|
-
process.stderr.write(String((err && err.message) || err), () => process.exit(1));
|
|
174
|
-
}
|
|
175
|
-
})();
|
|
176
|
-
`;
|
|
177
|
-
let result: string;
|
|
178
|
-
try {
|
|
179
|
-
result = execFileSync(process.execPath, ["-e", script], {
|
|
180
|
-
encoding: "utf-8",
|
|
181
|
-
timeout: 5000,
|
|
182
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
183
|
-
});
|
|
184
|
-
} catch (err) {
|
|
185
|
-
// The child's stderr carries the driver's real reason; execFileSync's
|
|
186
|
-
// message carries the whole generated script. Prefer the former.
|
|
187
|
-
throw childFailureError("Redis", err);
|
|
188
|
-
}
|
|
189
|
-
if (result === "__NULL__") return ""; // genuine key miss
|
|
190
|
-
return result;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private key(sessionId: string): string {
|
|
194
|
-
return `${this.prefix}${sessionId}`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
read(sessionId: string): SessionData | null {
|
|
198
|
-
const raw = this.execSync(["GET", this.key(sessionId)]);
|
|
199
|
-
if (!raw) return null; // key miss — normal "no session yet", NOT an error
|
|
200
|
-
try {
|
|
201
|
-
return JSON.parse(raw) as SessionData;
|
|
202
|
-
} catch {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
write(sessionId: string, data: SessionData, ttl: number): void {
|
|
208
|
-
const json = JSON.stringify(data);
|
|
209
|
-
if (ttl > 0) {
|
|
210
|
-
this.execSync(["SETEX", this.key(sessionId), String(ttl), json]);
|
|
211
|
-
} else {
|
|
212
|
-
this.execSync(["SET", this.key(sessionId), json]);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
destroy(sessionId: string): void {
|
|
217
|
-
this.execSync(["DEL", this.key(sessionId)]);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { SessionHandler } from "../session.js";
|
|
2
|
-
interface SessionData {
|
|
3
|
-
_created: number;
|
|
4
|
-
_accessed: number;
|
|
5
|
-
[key: string]: unknown;
|
|
6
|
-
}
|
|
7
|
-
export interface RedisNpmSessionConfig {
|
|
8
|
-
host?: string;
|
|
9
|
-
port?: number;
|
|
10
|
-
url?: string;
|
|
11
|
-
password?: string;
|
|
12
|
-
prefix?: string;
|
|
13
|
-
db?: number;
|
|
14
|
-
backend?: string;
|
|
15
|
-
path?: string;
|
|
16
|
-
ttl?: number;
|
|
17
|
-
redisHost?: string;
|
|
18
|
-
redisPort?: number;
|
|
19
|
-
redisPassword?: string;
|
|
20
|
-
redisPrefix?: string;
|
|
21
|
-
redisDb?: number;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Redis session handler using the `redis` npm package.
|
|
25
|
-
*
|
|
26
|
-
* Falls back to raw TCP (RESP protocol) if the `redis` package is not
|
|
27
|
-
* installed, matching the approach used by the Valkey handler.
|
|
28
|
-
*
|
|
29
|
-
* Stores session data as JSON strings with Redis TTL for automatic expiry.
|
|
30
|
-
*/
|
|
31
|
-
export declare class RedisNpmSessionHandler implements SessionHandler {
|
|
32
|
-
private host;
|
|
33
|
-
private port;
|
|
34
|
-
private url;
|
|
35
|
-
private password;
|
|
36
|
-
private prefix;
|
|
37
|
-
private db;
|
|
38
|
-
constructor(config?: RedisNpmSessionConfig);
|
|
39
|
-
/** Resolve a host/port for the raw-RESP path (parses TINA4_SESSION_REDIS_URL if set). */
|
|
40
|
-
private resolveHostPort;
|
|
41
|
-
/**
|
|
42
|
-
* Execute a Redis command synchronously.
|
|
43
|
-
*
|
|
44
|
-
* Prefers the official `redis` driver when it is installed (the class's reason
|
|
45
|
-
* to exist); otherwise speaks raw RESP via the shared {@link respCommandSync}
|
|
46
|
-
* transport — same correct path as the Valkey handler, no `redis` dependency.
|
|
47
|
-
*
|
|
48
|
-
* A genuine key miss yields `""`; a transport/connection FAILURE (server
|
|
49
|
-
* unreachable, rejected AUTH, timeout) THROWS so the Session boundary can
|
|
50
|
-
* distinguish "not found" (silent) from "backend failed" (log-loud + degrade).
|
|
51
|
-
*/
|
|
52
|
-
private execSync;
|
|
53
|
-
/** Drive the command through the `redis` npm client in a short-lived child. */
|
|
54
|
-
private execViaNpm;
|
|
55
|
-
private key;
|
|
56
|
-
read(sessionId: string): SessionData | null;
|
|
57
|
-
write(sessionId: string, data: SessionData, ttl: number): void;
|
|
58
|
-
destroy(sessionId: string): void;
|
|
59
|
-
}
|
|
60
|
-
export {};
|