tina4-nodejs 3.13.42 → 3.13.44
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 +2 -1
- package/package.json +1 -1
- package/packages/core/src/graphql.ts +23 -14
- package/packages/core/src/mcp.ts +21 -2
- package/packages/core/src/queue.ts +101 -9
- package/packages/core/src/queueBackends/kafkaBackend.ts +303 -167
- package/packages/core/src/queueBackends/mongoBackend.ts +143 -0
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +97 -31
- package/packages/core/src/server.ts +12 -5
- package/packages/core/src/session.ts +11 -95
- package/packages/core/src/sessionHandlers/mongoClient.ts +238 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +25 -204
- package/packages/core/src/sessionHandlers/redisHandler.ts +69 -114
- package/packages/core/src/sessionHandlers/respClient.ts +171 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +11 -95
- package/packages/orm/src/adapters/firebird.ts +20 -2
- package/packages/orm/src/adapters/mssql.ts +24 -2
- package/packages/orm/src/adapters/mysql.ts +20 -2
- package/packages/orm/src/adapters/postgres.ts +40 -12
- package/packages/orm/src/adapters/sqlite.ts +16 -2
- package/packages/orm/src/autoCrud.ts +13 -0
- package/packages/orm/src/baseModel.ts +3 -1
- package/packages/orm/src/cachedDatabase.ts +1 -1
- package/packages/orm/src/database.ts +42 -11
- package/packages/orm/src/index.ts +1 -1
- package/packages/orm/src/migration.ts +124 -45
- package/packages/orm/src/types.ts +5 -3
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 synchronous MongoDB transport — used by the MongoDB session handler.
|
|
3
|
+
*
|
|
4
|
+
* The session-handler interface is synchronous, but every Mongo client is async,
|
|
5
|
+
* so a command runs in a short-lived `node -e` child (execFileSync) that blocks
|
|
6
|
+
* the caller until it exits. Two transports, mirroring the Python master's
|
|
7
|
+
* pymongo-first design:
|
|
8
|
+
*
|
|
9
|
+
* 1. The official `mongodb` driver when it resolves (robust, the normal path —
|
|
10
|
+
* @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 minimal
|
|
12
|
+
* BSON codec — zero dependencies.
|
|
13
|
+
*
|
|
14
|
+
* The bugs this replaces (raw path): the old encoder put `$db` FIRST in the command
|
|
15
|
+
* document, so the server read `$db` as the command name and answered
|
|
16
|
+
* CommandNotFound; doubles were written big-endian; arrays/booleans were not encoded
|
|
17
|
+
* at all (so `updates: [{... upsert: true}]` was malformed); and the response was
|
|
18
|
+
* "parsed" by regex-matching `"data":{...}` against binary BSON, which can never
|
|
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
|
+
*/
|
|
24
|
+
import { execFileSync } from "node:child_process";
|
|
25
|
+
|
|
26
|
+
export interface MongoTarget {
|
|
27
|
+
host: string;
|
|
28
|
+
port: number;
|
|
29
|
+
database: string;
|
|
30
|
+
collection: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Command args: `filter` (always), plus `data`/`last_accessed` for an update. */
|
|
34
|
+
export interface MongoCommandArgs {
|
|
35
|
+
filter: Record<string, unknown>;
|
|
36
|
+
data?: unknown;
|
|
37
|
+
last_accessed?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run a session Mongo command synchronously and return the raw child stdout.
|
|
42
|
+
*
|
|
43
|
+
* - command "find" -> the matched document as a JSON string, or "__EMPTY__".
|
|
44
|
+
* - command "update" -> "__OK__" (upsert of `{_id, data, last_accessed}`).
|
|
45
|
+
* - command "delete" -> "__OK__".
|
|
46
|
+
*
|
|
47
|
+
* THROWS `<label> command failed: ...` on a transport failure (server unreachable,
|
|
48
|
+
* timeout) OR a Mongo command error (`ok != 1`), so the Session boundary can
|
|
49
|
+
* log-loud + degrade (or re-throw under strict mode).
|
|
50
|
+
*/
|
|
51
|
+
export function mongoCommandSync(
|
|
52
|
+
target: MongoTarget,
|
|
53
|
+
command: "find" | "update" | "delete",
|
|
54
|
+
args: MongoCommandArgs,
|
|
55
|
+
label = "MongoDB",
|
|
56
|
+
): string {
|
|
57
|
+
const script = `
|
|
58
|
+
const net = require("node:net");
|
|
59
|
+
const host = ${JSON.stringify(target.host)};
|
|
60
|
+
const port = ${target.port};
|
|
61
|
+
const database = ${JSON.stringify(target.database)};
|
|
62
|
+
const collection = ${JSON.stringify(target.collection)};
|
|
63
|
+
const command = ${JSON.stringify(command)};
|
|
64
|
+
const args = ${JSON.stringify(args)};
|
|
65
|
+
|
|
66
|
+
// ── driver path (preferred — matches the Python master's pymongo-first) ──
|
|
67
|
+
let hasDriver = false;
|
|
68
|
+
try { require.resolve("mongodb"); hasDriver = true; } catch (e) {}
|
|
69
|
+
|
|
70
|
+
if (hasDriver) {
|
|
71
|
+
(async () => {
|
|
72
|
+
let client;
|
|
73
|
+
try {
|
|
74
|
+
const { MongoClient } = require("mongodb");
|
|
75
|
+
client = new MongoClient("mongodb://" + host + ":" + port, { serverSelectionTimeoutMS: 4000 });
|
|
76
|
+
await client.connect();
|
|
77
|
+
const coll = client.db(database).collection(collection);
|
|
78
|
+
let out;
|
|
79
|
+
if (command === "find") {
|
|
80
|
+
const doc = await coll.findOne(args.filter);
|
|
81
|
+
out = doc ? JSON.stringify(doc) : "__EMPTY__";
|
|
82
|
+
} else if (command === "update") {
|
|
83
|
+
await coll.updateOne(args.filter, { $set: { data: args.data, last_accessed: args.last_accessed } }, { upsert: true });
|
|
84
|
+
out = "__OK__";
|
|
85
|
+
} else {
|
|
86
|
+
await coll.deleteOne(args.filter);
|
|
87
|
+
out = "__OK__";
|
|
88
|
+
}
|
|
89
|
+
await client.close();
|
|
90
|
+
process.stdout.write(out, () => process.exit(0));
|
|
91
|
+
} catch (err) {
|
|
92
|
+
try { if (client) await client.close(); } catch (e) {}
|
|
93
|
+
process.stderr.write(String((err && err.message) || err));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
} else {
|
|
98
|
+
|
|
99
|
+
// ── raw OP_MSG fallback (zero-dep, mirrors the Python master codec) ──
|
|
100
|
+
function encElem(key, value) {
|
|
101
|
+
const ck = Buffer.concat([Buffer.from(key, "utf-8"), Buffer.from([0])]);
|
|
102
|
+
if (value === null || value === undefined) return Buffer.concat([Buffer.from([0x0a]), ck]);
|
|
103
|
+
if (typeof value === "boolean") return Buffer.concat([Buffer.from([0x08]), ck, Buffer.from([value ? 1 : 0])]);
|
|
104
|
+
if (typeof value === "number") {
|
|
105
|
+
if (Number.isInteger(value) && value >= -2147483648 && value <= 2147483647) {
|
|
106
|
+
const b = Buffer.alloc(4); b.writeInt32LE(value, 0);
|
|
107
|
+
return Buffer.concat([Buffer.from([0x10]), ck, b]);
|
|
108
|
+
}
|
|
109
|
+
if (Number.isInteger(value)) {
|
|
110
|
+
const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(value), 0);
|
|
111
|
+
return Buffer.concat([Buffer.from([0x12]), ck, b]);
|
|
112
|
+
}
|
|
113
|
+
const b = Buffer.alloc(8); b.writeDoubleLE(value, 0);
|
|
114
|
+
return Buffer.concat([Buffer.from([0x01]), ck, b]);
|
|
115
|
+
}
|
|
116
|
+
if (typeof value === "string") {
|
|
117
|
+
const s = Buffer.from(value, "utf-8");
|
|
118
|
+
const len = Buffer.alloc(4); len.writeInt32LE(s.length + 1, 0);
|
|
119
|
+
return Buffer.concat([Buffer.from([0x02]), ck, len, s, Buffer.from([0])]);
|
|
120
|
+
}
|
|
121
|
+
if (Array.isArray(value)) {
|
|
122
|
+
const indexed = {};
|
|
123
|
+
value.forEach((v, i) => { indexed[String(i)] = v; });
|
|
124
|
+
return Buffer.concat([Buffer.from([0x04]), ck, encDoc(indexed)]);
|
|
125
|
+
}
|
|
126
|
+
if (typeof value === "object") return Buffer.concat([Buffer.from([0x03]), ck, encDoc(value)]);
|
|
127
|
+
const s = Buffer.from(String(value), "utf-8");
|
|
128
|
+
const len = Buffer.alloc(4); len.writeInt32LE(s.length + 1, 0);
|
|
129
|
+
return Buffer.concat([Buffer.from([0x02]), ck, len, s, Buffer.from([0])]);
|
|
130
|
+
}
|
|
131
|
+
function encDoc(obj) {
|
|
132
|
+
let body = Buffer.alloc(0);
|
|
133
|
+
for (const k of Object.keys(obj)) body = Buffer.concat([body, encElem(k, obj[k])]);
|
|
134
|
+
body = Buffer.concat([body, Buffer.from([0])]);
|
|
135
|
+
const out = Buffer.alloc(4 + body.length);
|
|
136
|
+
out.writeInt32LE(out.length, 0);
|
|
137
|
+
body.copy(out, 4);
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
function decDoc(buf, pos) {
|
|
141
|
+
const docLen = buf.readInt32LE(pos.i); pos.i += 4;
|
|
142
|
+
const end = pos.i + docLen - 5;
|
|
143
|
+
const doc = {};
|
|
144
|
+
while (pos.i < end) {
|
|
145
|
+
const type = buf[pos.i]; pos.i += 1;
|
|
146
|
+
const keyEnd = buf.indexOf(0, pos.i);
|
|
147
|
+
const key = buf.toString("utf-8", pos.i, keyEnd);
|
|
148
|
+
pos.i = keyEnd + 1;
|
|
149
|
+
doc[key] = decVal(buf, pos, type, end);
|
|
150
|
+
}
|
|
151
|
+
pos.i += 1;
|
|
152
|
+
return doc;
|
|
153
|
+
}
|
|
154
|
+
function decVal(buf, pos, type, end) {
|
|
155
|
+
if (type === 0x01) { const v = buf.readDoubleLE(pos.i); pos.i += 8; return v; }
|
|
156
|
+
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; }
|
|
157
|
+
if (type === 0x03) return decDoc(buf, pos);
|
|
158
|
+
if (type === 0x04) { const d = decDoc(buf, pos); return Object.keys(d).map((k) => d[k]); }
|
|
159
|
+
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; }
|
|
160
|
+
if (type === 0x07) { const v = buf.toString("hex", pos.i, pos.i + 12); pos.i += 12; return v; }
|
|
161
|
+
if (type === 0x08) { const v = buf[pos.i] !== 0; pos.i += 1; return v; }
|
|
162
|
+
if (type === 0x09) { const v = Number(buf.readBigInt64LE(pos.i)); pos.i += 8; return v; }
|
|
163
|
+
if (type === 0x0a) return null;
|
|
164
|
+
if (type === 0x10) { const v = buf.readInt32LE(pos.i); pos.i += 4; return v; }
|
|
165
|
+
if (type === 0x11) { const v = Number(buf.readBigUInt64LE(pos.i)); pos.i += 8; return v; }
|
|
166
|
+
if (type === 0x12) { const v = Number(buf.readBigInt64LE(pos.i)); pos.i += 8; return v; }
|
|
167
|
+
// Unknown type: cannot size it — stop at the document boundary to avoid a loop.
|
|
168
|
+
pos.i = end;
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let cmdDoc;
|
|
173
|
+
if (command === "find") {
|
|
174
|
+
cmdDoc = { find: collection, filter: args.filter, limit: 1, "$db": database };
|
|
175
|
+
} else if (command === "update") {
|
|
176
|
+
const u = { _id: args.filter._id, data: args.data, last_accessed: args.last_accessed };
|
|
177
|
+
cmdDoc = { update: collection, updates: [{ q: args.filter, u: u, upsert: true }], "$db": database };
|
|
178
|
+
} else {
|
|
179
|
+
cmdDoc = { delete: collection, deletes: [{ q: args.filter, limit: 1 }], "$db": database };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const bodyDoc = encDoc(cmdDoc);
|
|
183
|
+
const section = Buffer.concat([Buffer.from([0]), bodyDoc]); // section kind 0 = body
|
|
184
|
+
const flags = Buffer.alloc(4); // flagBits = 0
|
|
185
|
+
const payload = Buffer.concat([flags, section]);
|
|
186
|
+
const header = Buffer.alloc(16);
|
|
187
|
+
header.writeInt32LE(16 + payload.length, 0); // messageLength
|
|
188
|
+
header.writeInt32LE(1, 4); // requestID
|
|
189
|
+
header.writeInt32LE(0, 8); // responseTo
|
|
190
|
+
header.writeInt32LE(2013, 12); // opCode = OP_MSG
|
|
191
|
+
const message = Buffer.concat([header, payload]);
|
|
192
|
+
|
|
193
|
+
const sock = net.createConnection({ host, port });
|
|
194
|
+
sock.setNoDelay(true);
|
|
195
|
+
let buffer = Buffer.alloc(0);
|
|
196
|
+
let done = false;
|
|
197
|
+
const timer = setTimeout(() => { if (!done) { done = true; try { sock.destroy(); } catch (e) {} process.stderr.write("timeout"); process.exitCode = 1; } }, 5000);
|
|
198
|
+
function emit(s) {
|
|
199
|
+
if (done) return; done = true; clearTimeout(timer);
|
|
200
|
+
process.stdout.write(s, () => { try { sock.destroy(); } catch (e) {} });
|
|
201
|
+
}
|
|
202
|
+
function fail(msg) {
|
|
203
|
+
if (done) return; done = true; clearTimeout(timer);
|
|
204
|
+
try { sock.destroy(); } catch (e) {}
|
|
205
|
+
process.stderr.write(msg || ""); process.exitCode = 1;
|
|
206
|
+
}
|
|
207
|
+
sock.on("connect", () => sock.write(message));
|
|
208
|
+
sock.on("data", (chunk) => {
|
|
209
|
+
buffer = buffer.length ? Buffer.concat([buffer, chunk]) : chunk;
|
|
210
|
+
if (buffer.length < 4) return;
|
|
211
|
+
const msgLen = buffer.readInt32LE(0);
|
|
212
|
+
if (buffer.length < msgLen) return;
|
|
213
|
+
let doc;
|
|
214
|
+
try { doc = decDoc(buffer, { i: 21 }); } catch (e) { fail("decode error: " + e.message); return; }
|
|
215
|
+
const ok = doc && (doc.ok === 1 || doc.ok === 1.0);
|
|
216
|
+
if (!ok) { fail("command error: " + ((doc && doc.errmsg) || JSON.stringify(doc))); return; }
|
|
217
|
+
if (command === "find") {
|
|
218
|
+
const batch = (doc.cursor && doc.cursor.firstBatch) || [];
|
|
219
|
+
emit(batch.length ? JSON.stringify(batch[0]) : "__EMPTY__");
|
|
220
|
+
} else {
|
|
221
|
+
emit("__OK__");
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
sock.on("error", (err) => fail(err.message));
|
|
225
|
+
sock.on("close", () => { if (!done) fail("connection closed before reply"); });
|
|
226
|
+
}
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
return execFileSync(process.execPath, ["-e", script], {
|
|
231
|
+
encoding: "utf-8",
|
|
232
|
+
timeout: 8000,
|
|
233
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
234
|
+
});
|
|
235
|
+
} catch (err) {
|
|
236
|
+
throw new Error(`${label} command failed: ${(err as Error).message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* TINA4_SESSION_MONGO_DB (default: "tina4_sessions")
|
|
14
14
|
* TINA4_SESSION_MONGO_COLLECTION (default: "sessions")
|
|
15
15
|
*/
|
|
16
|
-
import { execFileSync } from "node:child_process";
|
|
17
16
|
import type { SessionHandler } from "../session.js";
|
|
17
|
+
import { mongoCommandSync, type MongoTarget } from "./mongoClient.js";
|
|
18
18
|
|
|
19
19
|
interface SessionData {
|
|
20
20
|
_created: number;
|
|
@@ -85,221 +85,42 @@ export class MongoSessionHandler implements SessionHandler {
|
|
|
85
85
|
?? "sessions";
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*
|
|
95
|
-
* Returns the command response string (or `__EMPTY__` for an absent doc).
|
|
96
|
-
* A transport/connection FAILURE (server unreachable, socket error, timeout)
|
|
97
|
-
* THROWS so the Session boundary can distinguish "not found" (silent) from
|
|
98
|
-
* "backend failed" (log-loud + degrade). Backend-failure policy parity.
|
|
99
|
-
*/
|
|
100
|
-
private execSync(command: string, args: string): string {
|
|
101
|
-
const script = `
|
|
102
|
-
const net = require("node:net");
|
|
103
|
-
const host = ${JSON.stringify(this.uri ? this.parseUri().host : this.host)};
|
|
104
|
-
const port = ${this.uri ? this.parseUri().port : this.port};
|
|
105
|
-
const database = ${JSON.stringify(this.database)};
|
|
106
|
-
const collection = ${JSON.stringify(this.collection)};
|
|
107
|
-
const command = ${JSON.stringify(command)};
|
|
108
|
-
const args = ${JSON.stringify(args)};
|
|
109
|
-
|
|
110
|
-
// Minimal BSON encoder/decoder for session operations
|
|
111
|
-
function encodeBsonDocument(obj) {
|
|
112
|
-
const parts = [];
|
|
113
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
114
|
-
if (typeof value === "string") {
|
|
115
|
-
const keyBuf = Buffer.from(key + "\\0", "utf-8");
|
|
116
|
-
const valBuf = Buffer.from(value, "utf-8");
|
|
117
|
-
const entry = Buffer.alloc(1 + keyBuf.length + 4 + valBuf.length + 1);
|
|
118
|
-
entry.writeUInt8(2, 0); // string type
|
|
119
|
-
keyBuf.copy(entry, 1);
|
|
120
|
-
entry.writeInt32LE(valBuf.length + 1, 1 + keyBuf.length);
|
|
121
|
-
valBuf.copy(entry, 1 + keyBuf.length + 4);
|
|
122
|
-
entry.writeUInt8(0, entry.length - 1);
|
|
123
|
-
parts.push(entry);
|
|
124
|
-
} else if (typeof value === "number") {
|
|
125
|
-
const keyBuf = Buffer.from(key + "\\0", "utf-8");
|
|
126
|
-
if (Number.isInteger(value)) {
|
|
127
|
-
const entry = Buffer.alloc(1 + keyBuf.length + 4);
|
|
128
|
-
entry.writeUInt8(16, 0); // int32 type
|
|
129
|
-
keyBuf.copy(entry, 1);
|
|
130
|
-
entry.writeInt32LE(value, 1 + keyBuf.length);
|
|
131
|
-
parts.push(entry);
|
|
132
|
-
} else {
|
|
133
|
-
const entry = Buffer.alloc(1 + keyBuf.length + 8);
|
|
134
|
-
entry.writeUInt8(1, 0); // double type
|
|
135
|
-
keyBuf.copy(entry, 1);
|
|
136
|
-
entry.writeDoubleBE(value, 1 + keyBuf.length);
|
|
137
|
-
parts.push(entry);
|
|
138
|
-
}
|
|
139
|
-
} else if (typeof value === "object" && value !== null) {
|
|
140
|
-
const keyBuf = Buffer.from(key + "\\0", "utf-8");
|
|
141
|
-
const subDoc = encodeBsonDocument(value);
|
|
142
|
-
const entry = Buffer.alloc(1 + keyBuf.length + subDoc.length);
|
|
143
|
-
entry.writeUInt8(3, 0); // document type
|
|
144
|
-
keyBuf.copy(entry, 1);
|
|
145
|
-
subDoc.copy(entry, 1 + keyBuf.length);
|
|
146
|
-
parts.push(entry);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
const body = Buffer.concat(parts);
|
|
150
|
-
const doc = Buffer.alloc(4 + body.length + 1);
|
|
151
|
-
doc.writeInt32LE(doc.length, 0);
|
|
152
|
-
body.copy(doc, 4);
|
|
153
|
-
doc.writeUInt8(0, doc.length - 1);
|
|
154
|
-
return doc;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Use MongoDB OP_MSG (opcode 2013) with runCommand
|
|
158
|
-
function buildOpMsg(dbName, cmd) {
|
|
159
|
-
const cmdDoc = Object.assign({ "$db": dbName }, cmd);
|
|
160
|
-
const body = encodeBsonDocument(cmdDoc);
|
|
161
|
-
|
|
162
|
-
// OP_MSG: flagBits(4) + kind(1) + body
|
|
163
|
-
const section = Buffer.alloc(1 + body.length);
|
|
164
|
-
section.writeUInt8(0, 0); // kind 0 = body
|
|
165
|
-
body.copy(section, 1);
|
|
166
|
-
|
|
167
|
-
const msgHeader = Buffer.alloc(16 + 4 + section.length);
|
|
168
|
-
msgHeader.writeInt32LE(msgHeader.length, 0); // messageLength
|
|
169
|
-
msgHeader.writeInt32LE(1, 4); // requestID
|
|
170
|
-
msgHeader.writeInt32LE(0, 8); // responseTo
|
|
171
|
-
msgHeader.writeInt32LE(2013, 12); // opCode = OP_MSG
|
|
172
|
-
msgHeader.writeUInt32LE(0, 16); // flagBits
|
|
173
|
-
section.copy(msgHeader, 20);
|
|
174
|
-
|
|
175
|
-
return msgHeader;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// For this simplified implementation, we use a TCP connection
|
|
179
|
-
// and send/receive raw OP_MSG frames
|
|
180
|
-
const sock = net.createConnection({ host, port }, () => {
|
|
181
|
-
let cmd;
|
|
182
|
-
const parsedArgs = JSON.parse(args);
|
|
183
|
-
|
|
184
|
-
if (command === "find") {
|
|
185
|
-
cmd = {
|
|
186
|
-
find: collection,
|
|
187
|
-
filter: parsedArgs.filter,
|
|
188
|
-
limit: 1
|
|
189
|
-
};
|
|
190
|
-
} else if (command === "update") {
|
|
191
|
-
cmd = {
|
|
192
|
-
update: collection,
|
|
193
|
-
updates: [{ q: parsedArgs.filter, u: { "$set": parsedArgs.data }, upsert: true }]
|
|
194
|
-
};
|
|
195
|
-
} else if (command === "delete") {
|
|
196
|
-
cmd = {
|
|
197
|
-
delete: collection,
|
|
198
|
-
deletes: [{ q: parsedArgs.filter, limit: 1 }]
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const msg = buildOpMsg(database, cmd);
|
|
203
|
-
sock.write(msg);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
let buffer = Buffer.alloc(0);
|
|
207
|
-
sock.on("data", (chunk) => {
|
|
208
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
209
|
-
if (buffer.length >= 4) {
|
|
210
|
-
const msgLen = buffer.readInt32LE(0);
|
|
211
|
-
if (buffer.length >= msgLen) {
|
|
212
|
-
// Parse response — extract body section
|
|
213
|
-
// Header is 16 bytes + 4 bytes flagBits + 1 byte section kind + BSON doc
|
|
214
|
-
if (buffer.length > 21) {
|
|
215
|
-
const bsonStart = 21;
|
|
216
|
-
const bsonLen = buffer.readInt32LE(bsonStart);
|
|
217
|
-
const bsonDoc = buffer.subarray(bsonStart, bsonStart + bsonLen);
|
|
218
|
-
|
|
219
|
-
// Simple BSON parser — just look for the session data string
|
|
220
|
-
const rawStr = bsonDoc.toString("utf-8", 4);
|
|
221
|
-
|
|
222
|
-
if (command === "find") {
|
|
223
|
-
// Look for cursor.firstBatch documents
|
|
224
|
-
// For simplicity, extract JSON-like data from response
|
|
225
|
-
process.stdout.write(rawStr);
|
|
226
|
-
} else {
|
|
227
|
-
process.stdout.write("__OK__");
|
|
228
|
-
}
|
|
229
|
-
} else {
|
|
230
|
-
process.stdout.write("__EMPTY__");
|
|
231
|
-
}
|
|
232
|
-
sock.destroy();
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
sock.on("error", (err) => {
|
|
238
|
-
process.stderr.write(err.message);
|
|
239
|
-
process.exit(1);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
setTimeout(() => { sock.destroy(); process.exit(1); }, 5000);
|
|
243
|
-
`;
|
|
244
|
-
|
|
245
|
-
try {
|
|
246
|
-
return execFileSync(process.execPath, ["-e", script], {
|
|
247
|
-
encoding: "utf-8",
|
|
248
|
-
timeout: 8000,
|
|
249
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
250
|
-
});
|
|
251
|
-
} catch (err) {
|
|
252
|
-
// Non-zero exit = the child hit a socket error / timeout. That is a
|
|
253
|
-
// transport FAILURE, not an absent document — surface it so the Session
|
|
254
|
-
// boundary logs + degrades (or re-throws under strict mode).
|
|
255
|
-
throw new Error(`MongoDB command failed: ${(err as Error).message}`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private parseUri(): { host: string; port: number } {
|
|
260
|
-
if (!this.uri) return { host: this.host, port: this.port };
|
|
261
|
-
try {
|
|
262
|
-
// mongodb://host:port/db
|
|
263
|
-
const match = this.uri.match(/mongodb:\/\/([^/:]+):?(\d+)?/);
|
|
88
|
+
/** Resolve the effective host/port (honours a configured mongodb:// URI). */
|
|
89
|
+
private target(): MongoTarget {
|
|
90
|
+
let host = this.host;
|
|
91
|
+
let port = this.port;
|
|
92
|
+
if (this.uri) {
|
|
93
|
+
const match = this.uri.match(/mongodb:\/\/(?:[^@/]+@)?([^/:]+):?(\d+)?/);
|
|
264
94
|
if (match) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
port: match[2] ? parseInt(match[2], 10) : 27017,
|
|
268
|
-
};
|
|
95
|
+
host = match[1];
|
|
96
|
+
port = match[2] ? parseInt(match[2], 10) : 27017;
|
|
269
97
|
}
|
|
270
|
-
}
|
|
271
|
-
return { host: this.
|
|
98
|
+
}
|
|
99
|
+
return { host, port, database: this.database, collection: this.collection };
|
|
272
100
|
}
|
|
273
101
|
|
|
274
102
|
read(sessionId: string): SessionData | null {
|
|
275
|
-
const result = this.
|
|
276
|
-
|
|
277
|
-
}));
|
|
278
|
-
|
|
279
|
-
if (!result || result === "__EMPTY__") return null;
|
|
280
|
-
|
|
281
|
-
// Try to extract session data from the BSON response
|
|
103
|
+
const result = mongoCommandSync(this.target(), "find", { filter: { _id: sessionId } });
|
|
104
|
+
if (!result || result === "__EMPTY__") return null; // genuine miss
|
|
282
105
|
try {
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return null;
|
|
106
|
+
const doc = JSON.parse(result) as { data?: unknown };
|
|
107
|
+
const data = doc?.data;
|
|
108
|
+
// Stored as a nested document (parity with the Python master); read returns it.
|
|
109
|
+
return data && typeof data === "object" ? (data as SessionData) : null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
291
113
|
}
|
|
292
114
|
|
|
293
115
|
write(sessionId: string, data: SessionData, _ttl: number): void {
|
|
294
|
-
this.
|
|
116
|
+
mongoCommandSync(this.target(), "update", {
|
|
295
117
|
filter: { _id: sessionId },
|
|
296
|
-
data
|
|
297
|
-
|
|
118
|
+
data,
|
|
119
|
+
last_accessed: Date.now() / 1000,
|
|
120
|
+
});
|
|
298
121
|
}
|
|
299
122
|
|
|
300
123
|
destroy(sessionId: string): void {
|
|
301
|
-
this.
|
|
302
|
-
filter: { _id: sessionId },
|
|
303
|
-
}));
|
|
124
|
+
mongoCommandSync(this.target(), "delete", { filter: { _id: sessionId } });
|
|
304
125
|
}
|
|
305
126
|
}
|