tina4-nodejs 3.13.94 → 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 +158 -30
- package/README.md +1 -1
- package/package.json +3 -1
- package/packages/cli/dist/bin.js +30911 -28444
- package/packages/cli/src/commands/metrics.ts +17 -11
- package/packages/cli/src/commands/serve.ts +10 -9
- package/packages/core/dist/index.js +30810 -28261
- package/packages/core/public/css/tina4.min.css +1 -1
- 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 +6 -7
- package/packages/core/src/logger.ts +257 -36
- package/packages/core/src/mcp.ts +1 -1
- package/packages/core/src/messenger.ts +294 -106
- 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 +34 -16
- package/packages/core/src/response.ts +46 -1
- package/packages/core/src/router.ts +29 -4
- package/packages/core/src/server.ts +886 -421
- 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 +22717 -20168
- 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/autoCrud.ts +12 -10
- 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 +241 -197
- package/packages/orm/src/databaseResult.ts +51 -28
- package/packages/orm/src/databaseUrl.ts +484 -0
- package/packages/orm/src/docstore.ts +386 -145
- package/packages/orm/src/index.ts +13 -6
- package/packages/orm/src/migration.ts +44 -11
- package/packages/orm/src/model.ts +4 -0
- package/packages/orm/src/queryBuilder.ts +47 -6
- package/packages/orm/src/sqlTranslator.ts +310 -4
- package/packages/orm/src/types.ts +21 -77
- package/packages/swagger/dist/index.js +78 -20
- package/packages/swagger/src/generator.ts +172 -29
- 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 +6 -9
- package/types/core/src/logger.d.ts +93 -16
- package/types/core/src/messenger.d.ts +47 -6
- 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 -4
- 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 +78 -28
- package/types/orm/src/databaseResult.d.ts +29 -15
- 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 +6 -4
- package/types/orm/src/migration.d.ts +4 -3
- 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 +21 -38
- package/packages/core/src/scss.ts +0 -623
- package/packages/core/src/sessionHandlers/redisHandler.ts +0 -219
- package/types/core/src/scss.d.ts +0 -19
- package/types/core/src/sessionHandlers/redisHandler.d.ts +0 -60
|
@@ -1,28 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tina4 synchronous MongoDB transport —
|
|
2
|
+
* Tina4 synchronous MongoDB transport — ONE persistent client per target.
|
|
3
3
|
*
|
|
4
|
-
* The session-handler interface is synchronous
|
|
5
|
-
* so
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The session-handler interface is synchronous but every Mongo client is async,
|
|
5
|
+
* so this rides the shared sync-over-async bridge (syncBridge): a Worker thread
|
|
6
|
+
* keeps its own event loop and holds the connection, the caller blocks in
|
|
7
|
+
* Atomics.wait until the worker answers through a SharedArrayBuffer.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* WHY IT CHANGED. Each command used to run in a short-lived `node -e` child, so
|
|
10
|
+
* every session read or write paid a process spawn AND a `require("mongodb")`
|
|
11
|
+
* AND a fresh connection handshake. Measured on this machine that was p50 230ms
|
|
12
|
+
* per session write+read — by far the worst of the four backends (Valkey on the
|
|
13
|
+
* same bridge is p50 10.5ms), and the same class of defect that made the Valkey
|
|
14
|
+
* session tests flaky. The client is now created once and reused.
|
|
15
|
+
*
|
|
16
|
+
* Two transports are kept, mirroring the Python master's pymongo-first design:
|
|
17
|
+
*
|
|
18
|
+
* 1. The official `mongodb` driver when it resolves (the normal path —
|
|
10
19
|
* @tina4/orm depends on it, so it is present in any app that uses the ORM).
|
|
11
|
-
* 2. A raw MongoDB wire-protocol (OP_MSG) fallback over node:net with a
|
|
12
|
-
* BSON codec — zero dependencies.
|
|
20
|
+
* 2. A raw MongoDB wire-protocol (OP_MSG) fallback over node:net with a
|
|
21
|
+
* minimal BSON codec — zero dependencies.
|
|
13
22
|
*
|
|
14
|
-
* The
|
|
15
|
-
*
|
|
16
|
-
* CommandNotFound
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* match. Here the command name comes first and `$db` last (matching the Python
|
|
20
|
-
* master), the BSON codec is little-endian and handles every type the commands use,
|
|
21
|
-
* and the OP_MSG response is decoded properly (cursor.firstBatch for find, `ok` for
|
|
22
|
-
* writes).
|
|
23
|
+
* The raw path's hard-won details are preserved exactly: the command name comes
|
|
24
|
+
* FIRST and `$db` LAST (putting `$db` first made the server read it as the
|
|
25
|
+
* command name and answer CommandNotFound), the BSON codec is little-endian and
|
|
26
|
+
* encodes arrays/booleans/doubles properly, and the OP_MSG response is decoded
|
|
27
|
+
* rather than regex-matched against binary.
|
|
23
28
|
*/
|
|
24
|
-
import {
|
|
25
|
-
|
|
29
|
+
import {
|
|
30
|
+
getBridge,
|
|
31
|
+
STATUS_ERROR,
|
|
32
|
+
STATUS_NIL,
|
|
33
|
+
STATUS_OK,
|
|
34
|
+
STATUS_TRANSPORT,
|
|
35
|
+
} from "./syncBridge.js";
|
|
26
36
|
|
|
27
37
|
export interface MongoTarget {
|
|
28
38
|
host: string;
|
|
@@ -31,23 +41,271 @@ export interface MongoTarget {
|
|
|
31
41
|
collection: string;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Command args: `filter` (always), plus `data`/`expires_at`/`last_accessed` for
|
|
46
|
+
* an update.
|
|
47
|
+
*
|
|
48
|
+
* `expires_at` is an ABSOLUTE epoch-seconds deadline computed at write time. It
|
|
49
|
+
* is what makes the session actually expire: this handler previously stored no
|
|
50
|
+
* expiry at all, created no TTL index, and never consulted anything on read, so
|
|
51
|
+
* MongoDB sessions lived forever.
|
|
52
|
+
*/
|
|
35
53
|
export interface MongoCommandArgs {
|
|
36
54
|
filter: Record<string, unknown>;
|
|
37
55
|
data?: unknown;
|
|
56
|
+
expires_at?: number;
|
|
38
57
|
last_accessed?: number;
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
/**
|
|
42
|
-
*
|
|
61
|
+
* Server-selection budget for the driver. Kept BELOW the bridge's reply timeout
|
|
62
|
+
* so an unreachable server is reported as a Mongo connection failure with a real
|
|
63
|
+
* message, rather than surfacing as the caller's generic reply timeout.
|
|
64
|
+
*/
|
|
65
|
+
const SERVER_SELECTION_MS = 3000;
|
|
66
|
+
|
|
67
|
+
const MONGO_WORKER = `
|
|
68
|
+
const net = require("node:net");
|
|
69
|
+
const target = workerData.target;
|
|
70
|
+
const host = target.host;
|
|
71
|
+
const port = target.port;
|
|
72
|
+
const database = target.database;
|
|
73
|
+
const collection = target.collection;
|
|
74
|
+
|
|
75
|
+
// TINA4_MONGO_FORCE_RAW selects the zero-dep wire-protocol path even where the
|
|
76
|
+
// driver IS installed. Without it the raw fallback can never be exercised on a
|
|
77
|
+
// machine that has mongodb (which is every machine using @tina4/orm), so the
|
|
78
|
+
// zero-dependency path would ship untested. This selects a REAL code path
|
|
79
|
+
// against a REAL server - it is not a stub.
|
|
80
|
+
let hasDriver = false;
|
|
81
|
+
if (!workerData.forceRaw) {
|
|
82
|
+
try { require.resolve("mongodb"); hasDriver = true; } catch (e) {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── driver path (preferred — matches the Python master's pymongo-first) ──
|
|
86
|
+
// The client is created ONCE and reused. Creating and closing one per command
|
|
87
|
+
// was the whole cost: a driver load plus a handshake on every session read.
|
|
88
|
+
let client = null;
|
|
89
|
+
let coll = null;
|
|
90
|
+
|
|
91
|
+
async function driverCall(command, args) {
|
|
92
|
+
try {
|
|
93
|
+
if (!coll) {
|
|
94
|
+
const { MongoClient } = require("mongodb");
|
|
95
|
+
client = new MongoClient("mongodb://" + host + ":" + port, { serverSelectionTimeoutMS: ${SERVER_SELECTION_MS} });
|
|
96
|
+
await client.connect();
|
|
97
|
+
coll = client.db(database).collection(collection);
|
|
98
|
+
}
|
|
99
|
+
if (command === "find") {
|
|
100
|
+
const doc = await coll.findOne(args.filter);
|
|
101
|
+
// A genuine miss is NOT a failure - it is "no session yet".
|
|
102
|
+
if (!doc) { __reply(${STATUS_NIL}, null); return; }
|
|
103
|
+
__reply(${STATUS_OK}, JSON.stringify(doc));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (command === "update") {
|
|
107
|
+
await coll.updateOne(
|
|
108
|
+
args.filter,
|
|
109
|
+
{ $set: { data: args.data, expires_at: args.expires_at, last_accessed: args.last_accessed } },
|
|
110
|
+
{ upsert: true },
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
await coll.deleteOne(args.filter);
|
|
114
|
+
}
|
|
115
|
+
__reply(${STATUS_OK}, "__OK__");
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// Drop the client so the NEXT command reconnects - a transient blip must
|
|
118
|
+
// not poison the channel forever.
|
|
119
|
+
try { if (client) await client.close(); } catch (e) {}
|
|
120
|
+
client = null;
|
|
121
|
+
coll = null;
|
|
122
|
+
__reply(${STATUS_TRANSPORT}, String((err && err.message) || err));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── raw OP_MSG fallback (zero-dep, mirrors the Python master codec) ──
|
|
127
|
+
function encElem(key, value) {
|
|
128
|
+
const ck = Buffer.concat([Buffer.from(key, "utf-8"), Buffer.from([0])]);
|
|
129
|
+
if (value === null || value === undefined) return Buffer.concat([Buffer.from([0x0a]), ck]);
|
|
130
|
+
if (typeof value === "boolean") return Buffer.concat([Buffer.from([0x08]), ck, Buffer.from([value ? 1 : 0])]);
|
|
131
|
+
if (typeof value === "number") {
|
|
132
|
+
if (Number.isInteger(value) && value >= -2147483648 && value <= 2147483647) {
|
|
133
|
+
const b = Buffer.alloc(4); b.writeInt32LE(value, 0);
|
|
134
|
+
return Buffer.concat([Buffer.from([0x10]), ck, b]);
|
|
135
|
+
}
|
|
136
|
+
if (Number.isInteger(value)) {
|
|
137
|
+
const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(value), 0);
|
|
138
|
+
return Buffer.concat([Buffer.from([0x12]), ck, b]);
|
|
139
|
+
}
|
|
140
|
+
const b = Buffer.alloc(8); b.writeDoubleLE(value, 0);
|
|
141
|
+
return Buffer.concat([Buffer.from([0x01]), ck, b]);
|
|
142
|
+
}
|
|
143
|
+
if (typeof value === "string") {
|
|
144
|
+
const s = Buffer.from(value, "utf-8");
|
|
145
|
+
const len = Buffer.alloc(4); len.writeInt32LE(s.length + 1, 0);
|
|
146
|
+
return Buffer.concat([Buffer.from([0x02]), ck, len, s, Buffer.from([0])]);
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
const indexed = {};
|
|
150
|
+
value.forEach((v, i) => { indexed[String(i)] = v; });
|
|
151
|
+
return Buffer.concat([Buffer.from([0x04]), ck, encDoc(indexed)]);
|
|
152
|
+
}
|
|
153
|
+
if (typeof value === "object") return Buffer.concat([Buffer.from([0x03]), ck, encDoc(value)]);
|
|
154
|
+
const s = Buffer.from(String(value), "utf-8");
|
|
155
|
+
const len = Buffer.alloc(4); len.writeInt32LE(s.length + 1, 0);
|
|
156
|
+
return Buffer.concat([Buffer.from([0x02]), ck, len, s, Buffer.from([0])]);
|
|
157
|
+
}
|
|
158
|
+
function encDoc(obj) {
|
|
159
|
+
let body = Buffer.alloc(0);
|
|
160
|
+
for (const k of Object.keys(obj)) body = Buffer.concat([body, encElem(k, obj[k])]);
|
|
161
|
+
body = Buffer.concat([body, Buffer.from([0])]);
|
|
162
|
+
const out = Buffer.alloc(4 + body.length);
|
|
163
|
+
out.writeInt32LE(out.length, 0);
|
|
164
|
+
body.copy(out, 4);
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
function decDoc(buf, pos) {
|
|
168
|
+
const docLen = buf.readInt32LE(pos.i); pos.i += 4;
|
|
169
|
+
const end = pos.i + docLen - 5;
|
|
170
|
+
const doc = {};
|
|
171
|
+
while (pos.i < end) {
|
|
172
|
+
const type = buf[pos.i]; pos.i += 1;
|
|
173
|
+
const keyEnd = buf.indexOf(0, pos.i);
|
|
174
|
+
const key = buf.toString("utf-8", pos.i, keyEnd);
|
|
175
|
+
pos.i = keyEnd + 1;
|
|
176
|
+
doc[key] = decVal(buf, pos, type, end);
|
|
177
|
+
}
|
|
178
|
+
pos.i += 1;
|
|
179
|
+
return doc;
|
|
180
|
+
}
|
|
181
|
+
function decVal(buf, pos, type, end) {
|
|
182
|
+
if (type === 0x01) { const v = buf.readDoubleLE(pos.i); pos.i += 8; return v; }
|
|
183
|
+
if (type === 0x02) { const len = buf.readInt32LE(pos.i); pos.i += 4; const v = buf.toString("utf-8", pos.i, pos.i + len - 1); pos.i += len; return v; }
|
|
184
|
+
if (type === 0x03) return decDoc(buf, pos);
|
|
185
|
+
if (type === 0x04) { const d = decDoc(buf, pos); return Object.keys(d).map((k) => d[k]); }
|
|
186
|
+
if (type === 0x05) { const len = buf.readInt32LE(pos.i); pos.i += 4; pos.i += 1; const v = buf.subarray(pos.i, pos.i + len); pos.i += len; return v; }
|
|
187
|
+
if (type === 0x07) { const v = buf.toString("hex", pos.i, pos.i + 12); pos.i += 12; return v; }
|
|
188
|
+
if (type === 0x08) { const v = buf[pos.i] !== 0; pos.i += 1; return v; }
|
|
189
|
+
if (type === 0x09) { const v = Number(buf.readBigInt64LE(pos.i)); pos.i += 8; return v; }
|
|
190
|
+
if (type === 0x0a) return null;
|
|
191
|
+
if (type === 0x10) { const v = buf.readInt32LE(pos.i); pos.i += 4; return v; }
|
|
192
|
+
if (type === 0x11) { const v = Number(buf.readBigUInt64LE(pos.i)); pos.i += 8; return v; }
|
|
193
|
+
if (type === 0x12) { const v = Number(buf.readBigInt64LE(pos.i)); pos.i += 8; return v; }
|
|
194
|
+
// Unknown type: cannot size it — stop at the document boundary to avoid a loop.
|
|
195
|
+
pos.i = end;
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let sock = null;
|
|
200
|
+
let buffer = Buffer.alloc(0);
|
|
201
|
+
let pending = null; // { command }
|
|
202
|
+
let requestId = 0;
|
|
203
|
+
|
|
204
|
+
function settle(status, payload) {
|
|
205
|
+
if (!pending) return;
|
|
206
|
+
pending = null;
|
|
207
|
+
__reply(status, payload);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function drop(err) {
|
|
211
|
+
try { if (sock) sock.destroy(); } catch (e) {}
|
|
212
|
+
sock = null;
|
|
213
|
+
buffer = Buffer.alloc(0);
|
|
214
|
+
settle(${STATUS_TRANSPORT}, err || "connection lost");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function onData(chunk) {
|
|
218
|
+
buffer = buffer.length ? Buffer.concat([buffer, chunk]) : chunk;
|
|
219
|
+
if (!pending) return;
|
|
220
|
+
if (buffer.length < 4) return;
|
|
221
|
+
const msgLen = buffer.readInt32LE(0);
|
|
222
|
+
if (buffer.length < msgLen) return; // OP_MSG split across TCP chunks
|
|
223
|
+
|
|
224
|
+
const frame = buffer.subarray(0, msgLen);
|
|
225
|
+
buffer = buffer.subarray(msgLen);
|
|
226
|
+
|
|
227
|
+
let doc;
|
|
228
|
+
// 16 header + 4 flagBits + 1 section kind = 21
|
|
229
|
+
try { doc = decDoc(frame, { i: 21 }); } catch (e) { settle(${STATUS_TRANSPORT}, "decode error: " + e.message); return; }
|
|
230
|
+
|
|
231
|
+
const ok = doc && (doc.ok === 1 || doc.ok === 1.0);
|
|
232
|
+
if (!ok) { settle(${STATUS_ERROR}, "command error: " + ((doc && doc.errmsg) || JSON.stringify(doc))); return; }
|
|
233
|
+
|
|
234
|
+
if (pending.command === "find") {
|
|
235
|
+
const batch = (doc.cursor && doc.cursor.firstBatch) || [];
|
|
236
|
+
if (!batch.length) { settle(${STATUS_NIL}, null); return; }
|
|
237
|
+
settle(${STATUS_OK}, JSON.stringify(batch[0]));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
settle(${STATUS_OK}, "__OK__");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildMessage(command, args) {
|
|
244
|
+
let cmdDoc;
|
|
245
|
+
// The command name FIRST and $db LAST. Reversed, the server reads $db as the
|
|
246
|
+
// command name and answers CommandNotFound.
|
|
247
|
+
if (command === "find") {
|
|
248
|
+
cmdDoc = { find: collection, filter: args.filter, limit: 1, "$db": database };
|
|
249
|
+
} else if (command === "update") {
|
|
250
|
+
const u = { _id: args.filter._id, data: args.data, expires_at: args.expires_at, last_accessed: args.last_accessed };
|
|
251
|
+
cmdDoc = { update: collection, updates: [{ q: args.filter, u: u, upsert: true }], "$db": database };
|
|
252
|
+
} else {
|
|
253
|
+
cmdDoc = { delete: collection, deletes: [{ q: args.filter, limit: 1 }], "$db": database };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const bodyDoc = encDoc(cmdDoc);
|
|
257
|
+
const section = Buffer.concat([Buffer.from([0]), bodyDoc]); // section kind 0 = body
|
|
258
|
+
const flags = Buffer.alloc(4); // flagBits = 0
|
|
259
|
+
const payload = Buffer.concat([flags, section]);
|
|
260
|
+
const header = Buffer.alloc(16);
|
|
261
|
+
requestId += 1;
|
|
262
|
+
header.writeInt32LE(16 + payload.length, 0); // messageLength
|
|
263
|
+
header.writeInt32LE(requestId, 4); // requestID
|
|
264
|
+
header.writeInt32LE(0, 8); // responseTo
|
|
265
|
+
header.writeInt32LE(2013, 12); // opCode = OP_MSG
|
|
266
|
+
return Buffer.concat([header, payload]);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function connect(then) {
|
|
270
|
+
const s = net.createConnection({ host: host, port: port });
|
|
271
|
+
s.setNoDelay(true);
|
|
272
|
+
const connectTimer = setTimeout(() => {
|
|
273
|
+
if (sock !== s) { try { s.destroy(); } catch (e) {} drop("connect timed out"); }
|
|
274
|
+
}, ${SERVER_SELECTION_MS});
|
|
275
|
+
s.on("data", onData);
|
|
276
|
+
s.on("error", (e) => { clearTimeout(connectTimer); drop(e.message); });
|
|
277
|
+
s.on("close", () => { clearTimeout(connectTimer); if (sock === s) drop("connection closed"); });
|
|
278
|
+
s.on("connect", () => { clearTimeout(connectTimer); sock = s; then(); });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function rawCall(command, args) {
|
|
282
|
+
// Set pending BEFORE connecting: a connect error otherwise arrives with
|
|
283
|
+
// nothing in flight and the caller blocks for its whole reply timeout.
|
|
284
|
+
pending = { command: command };
|
|
285
|
+
const write = () => {
|
|
286
|
+
try { sock.write(buildMessage(command, args)); } catch (e) { drop(e.message); }
|
|
287
|
+
};
|
|
288
|
+
if (sock) write(); else { try { connect(write); } catch (e) { drop(e.message); } }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
parentPort.on("message", (msg) => {
|
|
292
|
+
if (hasDriver) void driverCall(msg.command, msg.args);
|
|
293
|
+
else rawCall(msg.command, msg.args);
|
|
294
|
+
});
|
|
295
|
+
`;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Run a session Mongo command synchronously.
|
|
43
299
|
*
|
|
44
300
|
* - command "find" -> the matched document as a JSON string, or "__EMPTY__".
|
|
45
301
|
* - command "update" -> "__OK__" (upsert of `{_id, data, last_accessed}`).
|
|
46
302
|
* - command "delete" -> "__OK__".
|
|
47
303
|
*
|
|
48
|
-
* THROWS `<label> command failed: ...` on a transport failure (server
|
|
49
|
-
* timeout) OR a Mongo command error (`ok != 1`), so the Session
|
|
50
|
-
* log-loud + degrade (or re-throw under strict mode).
|
|
304
|
+
* THROWS `<label> command failed: ...` on a transport failure (server
|
|
305
|
+
* unreachable, timeout) OR a Mongo command error (`ok != 1`), so the Session
|
|
306
|
+
* boundary can log-loud + degrade (or re-throw under strict mode). A genuine
|
|
307
|
+
* miss is NOT a failure and comes back as "__EMPTY__" — collapsing the two is
|
|
308
|
+
* how a dead backend silently logs every user out.
|
|
51
309
|
*/
|
|
52
310
|
export function mongoCommandSync(
|
|
53
311
|
target: MongoTarget,
|
|
@@ -55,190 +313,17 @@ export function mongoCommandSync(
|
|
|
55
313
|
args: MongoCommandArgs,
|
|
56
314
|
label = "MongoDB",
|
|
57
315
|
): string {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const args = ${JSON.stringify(args)};
|
|
66
|
-
|
|
67
|
-
// ── driver path (preferred — matches the Python master's pymongo-first) ──
|
|
68
|
-
let hasDriver = false;
|
|
69
|
-
try { require.resolve("mongodb"); hasDriver = true; } catch (e) {}
|
|
70
|
-
|
|
71
|
-
if (hasDriver) {
|
|
72
|
-
(async () => {
|
|
73
|
-
let client;
|
|
74
|
-
try {
|
|
75
|
-
const { MongoClient } = require("mongodb");
|
|
76
|
-
client = new MongoClient("mongodb://" + host + ":" + port, { serverSelectionTimeoutMS: 4000 });
|
|
77
|
-
await client.connect();
|
|
78
|
-
const coll = client.db(database).collection(collection);
|
|
79
|
-
let out;
|
|
80
|
-
if (command === "find") {
|
|
81
|
-
const doc = await coll.findOne(args.filter);
|
|
82
|
-
out = doc ? JSON.stringify(doc) : "__EMPTY__";
|
|
83
|
-
} else if (command === "update") {
|
|
84
|
-
await coll.updateOne(args.filter, { $set: { data: args.data, last_accessed: args.last_accessed } }, { upsert: true });
|
|
85
|
-
out = "__OK__";
|
|
86
|
-
} else {
|
|
87
|
-
await coll.deleteOne(args.filter);
|
|
88
|
-
out = "__OK__";
|
|
89
|
-
}
|
|
90
|
-
await client.close();
|
|
91
|
-
process.stdout.write(out, () => process.exit(0));
|
|
92
|
-
} catch (err) {
|
|
93
|
-
try { if (client) await client.close(); } catch (e) {}
|
|
94
|
-
// Exit from the write CALLBACK: stderr to a pipe is an async write and
|
|
95
|
-
// a bare process.exit() truncates it, which left the parent with an
|
|
96
|
-
// empty stderr and nothing but execFileSync's script-dump message.
|
|
97
|
-
process.stderr.write(String((err && err.message) || err), () => process.exit(1));
|
|
98
|
-
}
|
|
99
|
-
})();
|
|
100
|
-
} else {
|
|
316
|
+
const key = `mongo:${target.host}:${target.port}:${target.database}:${target.collection}`;
|
|
317
|
+
const forceRaw = process.env.TINA4_MONGO_FORCE_RAW ? 1 : 0;
|
|
318
|
+
const { status, payload } = getBridge(
|
|
319
|
+
`${key}:${forceRaw ? "raw" : "drv"}`,
|
|
320
|
+
MONGO_WORKER,
|
|
321
|
+
{ target, forceRaw },
|
|
322
|
+
).call({ command, args }, label);
|
|
101
323
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (value === null || value === undefined) return Buffer.concat([Buffer.from([0x0a]), ck]);
|
|
106
|
-
if (typeof value === "boolean") return Buffer.concat([Buffer.from([0x08]), ck, Buffer.from([value ? 1 : 0])]);
|
|
107
|
-
if (typeof value === "number") {
|
|
108
|
-
if (Number.isInteger(value) && value >= -2147483648 && value <= 2147483647) {
|
|
109
|
-
const b = Buffer.alloc(4); b.writeInt32LE(value, 0);
|
|
110
|
-
return Buffer.concat([Buffer.from([0x10]), ck, b]);
|
|
111
|
-
}
|
|
112
|
-
if (Number.isInteger(value)) {
|
|
113
|
-
const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(value), 0);
|
|
114
|
-
return Buffer.concat([Buffer.from([0x12]), ck, b]);
|
|
115
|
-
}
|
|
116
|
-
const b = Buffer.alloc(8); b.writeDoubleLE(value, 0);
|
|
117
|
-
return Buffer.concat([Buffer.from([0x01]), ck, b]);
|
|
118
|
-
}
|
|
119
|
-
if (typeof value === "string") {
|
|
120
|
-
const s = Buffer.from(value, "utf-8");
|
|
121
|
-
const len = Buffer.alloc(4); len.writeInt32LE(s.length + 1, 0);
|
|
122
|
-
return Buffer.concat([Buffer.from([0x02]), ck, len, s, Buffer.from([0])]);
|
|
123
|
-
}
|
|
124
|
-
if (Array.isArray(value)) {
|
|
125
|
-
const indexed = {};
|
|
126
|
-
value.forEach((v, i) => { indexed[String(i)] = v; });
|
|
127
|
-
return Buffer.concat([Buffer.from([0x04]), ck, encDoc(indexed)]);
|
|
128
|
-
}
|
|
129
|
-
if (typeof value === "object") return Buffer.concat([Buffer.from([0x03]), ck, encDoc(value)]);
|
|
130
|
-
const s = Buffer.from(String(value), "utf-8");
|
|
131
|
-
const len = Buffer.alloc(4); len.writeInt32LE(s.length + 1, 0);
|
|
132
|
-
return Buffer.concat([Buffer.from([0x02]), ck, len, s, Buffer.from([0])]);
|
|
133
|
-
}
|
|
134
|
-
function encDoc(obj) {
|
|
135
|
-
let body = Buffer.alloc(0);
|
|
136
|
-
for (const k of Object.keys(obj)) body = Buffer.concat([body, encElem(k, obj[k])]);
|
|
137
|
-
body = Buffer.concat([body, Buffer.from([0])]);
|
|
138
|
-
const out = Buffer.alloc(4 + body.length);
|
|
139
|
-
out.writeInt32LE(out.length, 0);
|
|
140
|
-
body.copy(out, 4);
|
|
141
|
-
return out;
|
|
142
|
-
}
|
|
143
|
-
function decDoc(buf, pos) {
|
|
144
|
-
const docLen = buf.readInt32LE(pos.i); pos.i += 4;
|
|
145
|
-
const end = pos.i + docLen - 5;
|
|
146
|
-
const doc = {};
|
|
147
|
-
while (pos.i < end) {
|
|
148
|
-
const type = buf[pos.i]; pos.i += 1;
|
|
149
|
-
const keyEnd = buf.indexOf(0, pos.i);
|
|
150
|
-
const key = buf.toString("utf-8", pos.i, keyEnd);
|
|
151
|
-
pos.i = keyEnd + 1;
|
|
152
|
-
doc[key] = decVal(buf, pos, type, end);
|
|
153
|
-
}
|
|
154
|
-
pos.i += 1;
|
|
155
|
-
return doc;
|
|
156
|
-
}
|
|
157
|
-
function decVal(buf, pos, type, end) {
|
|
158
|
-
if (type === 0x01) { const v = buf.readDoubleLE(pos.i); pos.i += 8; return v; }
|
|
159
|
-
if (type === 0x02) { const len = buf.readInt32LE(pos.i); pos.i += 4; const v = buf.toString("utf-8", pos.i, pos.i + len - 1); pos.i += len; return v; }
|
|
160
|
-
if (type === 0x03) return decDoc(buf, pos);
|
|
161
|
-
if (type === 0x04) { const d = decDoc(buf, pos); return Object.keys(d).map((k) => d[k]); }
|
|
162
|
-
if (type === 0x05) { const len = buf.readInt32LE(pos.i); pos.i += 4; pos.i += 1; const v = buf.subarray(pos.i, pos.i + len); pos.i += len; return v; }
|
|
163
|
-
if (type === 0x07) { const v = buf.toString("hex", pos.i, pos.i + 12); pos.i += 12; return v; }
|
|
164
|
-
if (type === 0x08) { const v = buf[pos.i] !== 0; pos.i += 1; return v; }
|
|
165
|
-
if (type === 0x09) { const v = Number(buf.readBigInt64LE(pos.i)); pos.i += 8; return v; }
|
|
166
|
-
if (type === 0x0a) return null;
|
|
167
|
-
if (type === 0x10) { const v = buf.readInt32LE(pos.i); pos.i += 4; return v; }
|
|
168
|
-
if (type === 0x11) { const v = Number(buf.readBigUInt64LE(pos.i)); pos.i += 8; return v; }
|
|
169
|
-
if (type === 0x12) { const v = Number(buf.readBigInt64LE(pos.i)); pos.i += 8; return v; }
|
|
170
|
-
// Unknown type: cannot size it — stop at the document boundary to avoid a loop.
|
|
171
|
-
pos.i = end;
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
let cmdDoc;
|
|
176
|
-
if (command === "find") {
|
|
177
|
-
cmdDoc = { find: collection, filter: args.filter, limit: 1, "$db": database };
|
|
178
|
-
} else if (command === "update") {
|
|
179
|
-
const u = { _id: args.filter._id, data: args.data, last_accessed: args.last_accessed };
|
|
180
|
-
cmdDoc = { update: collection, updates: [{ q: args.filter, u: u, upsert: true }], "$db": database };
|
|
181
|
-
} else {
|
|
182
|
-
cmdDoc = { delete: collection, deletes: [{ q: args.filter, limit: 1 }], "$db": database };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const bodyDoc = encDoc(cmdDoc);
|
|
186
|
-
const section = Buffer.concat([Buffer.from([0]), bodyDoc]); // section kind 0 = body
|
|
187
|
-
const flags = Buffer.alloc(4); // flagBits = 0
|
|
188
|
-
const payload = Buffer.concat([flags, section]);
|
|
189
|
-
const header = Buffer.alloc(16);
|
|
190
|
-
header.writeInt32LE(16 + payload.length, 0); // messageLength
|
|
191
|
-
header.writeInt32LE(1, 4); // requestID
|
|
192
|
-
header.writeInt32LE(0, 8); // responseTo
|
|
193
|
-
header.writeInt32LE(2013, 12); // opCode = OP_MSG
|
|
194
|
-
const message = Buffer.concat([header, payload]);
|
|
195
|
-
|
|
196
|
-
const sock = net.createConnection({ host, port });
|
|
197
|
-
sock.setNoDelay(true);
|
|
198
|
-
let buffer = Buffer.alloc(0);
|
|
199
|
-
let done = false;
|
|
200
|
-
const timer = setTimeout(() => { if (!done) { done = true; try { sock.destroy(); } catch (e) {} process.stderr.write("timeout"); process.exitCode = 1; } }, 5000);
|
|
201
|
-
function emit(s) {
|
|
202
|
-
if (done) return; done = true; clearTimeout(timer);
|
|
203
|
-
process.stdout.write(s, () => { try { sock.destroy(); } catch (e) {} });
|
|
204
|
-
}
|
|
205
|
-
function fail(msg) {
|
|
206
|
-
if (done) return; done = true; clearTimeout(timer);
|
|
207
|
-
try { sock.destroy(); } catch (e) {}
|
|
208
|
-
process.stderr.write(msg || ""); process.exitCode = 1;
|
|
209
|
-
}
|
|
210
|
-
sock.on("connect", () => sock.write(message));
|
|
211
|
-
sock.on("data", (chunk) => {
|
|
212
|
-
buffer = buffer.length ? Buffer.concat([buffer, chunk]) : chunk;
|
|
213
|
-
if (buffer.length < 4) return;
|
|
214
|
-
const msgLen = buffer.readInt32LE(0);
|
|
215
|
-
if (buffer.length < msgLen) return;
|
|
216
|
-
let doc;
|
|
217
|
-
try { doc = decDoc(buffer, { i: 21 }); } catch (e) { fail("decode error: " + e.message); return; }
|
|
218
|
-
const ok = doc && (doc.ok === 1 || doc.ok === 1.0);
|
|
219
|
-
if (!ok) { fail("command error: " + ((doc && doc.errmsg) || JSON.stringify(doc))); return; }
|
|
220
|
-
if (command === "find") {
|
|
221
|
-
const batch = (doc.cursor && doc.cursor.firstBatch) || [];
|
|
222
|
-
emit(batch.length ? JSON.stringify(batch[0]) : "__EMPTY__");
|
|
223
|
-
} else {
|
|
224
|
-
emit("__OK__");
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
sock.on("error", (err) => fail(err.message));
|
|
228
|
-
sock.on("close", () => { if (!done) fail("connection closed before reply"); });
|
|
229
|
-
}
|
|
230
|
-
`;
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
return execFileSync(process.execPath, ["-e", script], {
|
|
234
|
-
encoding: "utf-8",
|
|
235
|
-
timeout: 8000,
|
|
236
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
237
|
-
});
|
|
238
|
-
} catch (err) {
|
|
239
|
-
// The child's stderr carries the real reason (connection refused, auth
|
|
240
|
-
// failure, timeout); execFileSync's message carries the whole generated
|
|
241
|
-
// script. Prefer the former.
|
|
242
|
-
throw childFailureError(label, err);
|
|
324
|
+
if (status === STATUS_NIL) return "__EMPTY__"; // genuine miss, not an error
|
|
325
|
+
if (status === STATUS_TRANSPORT || status === STATUS_ERROR) {
|
|
326
|
+
throw new Error(`${label} command failed: ${payload}`);
|
|
243
327
|
}
|
|
328
|
+
return payload;
|
|
244
329
|
}
|