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.
Files changed (123) hide show
  1. package/CLAUDE.md +158 -30
  2. package/README.md +1 -1
  3. package/package.json +3 -1
  4. package/packages/cli/dist/bin.js +30911 -28444
  5. package/packages/cli/src/commands/metrics.ts +17 -11
  6. package/packages/cli/src/commands/serve.ts +10 -9
  7. package/packages/core/dist/index.js +30810 -28261
  8. package/packages/core/public/css/tina4.min.css +1 -1
  9. package/packages/core/src/ai.ts +7 -1
  10. package/packages/core/src/auth.ts +191 -39
  11. package/packages/core/src/background.ts +19 -19
  12. package/packages/core/src/cache.ts +492 -49
  13. package/packages/core/src/devAdmin.ts +79 -32
  14. package/packages/core/src/dispatchPipeline.ts +285 -0
  15. package/packages/core/src/dotenv.ts +185 -40
  16. package/packages/core/src/index.ts +6 -7
  17. package/packages/core/src/logger.ts +257 -36
  18. package/packages/core/src/mcp.ts +1 -1
  19. package/packages/core/src/messenger.ts +294 -106
  20. package/packages/core/src/metrics.ts +199 -961
  21. package/packages/core/src/middleware.ts +390 -123
  22. package/packages/core/src/queue.ts +188 -32
  23. package/packages/core/src/queueBackends/kafkaBackend.ts +1 -1
  24. package/packages/core/src/queueBackends/liteBackend.ts +13 -0
  25. package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
  26. package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
  27. package/packages/core/src/rateLimiter.ts +10 -5
  28. package/packages/core/src/request.ts +34 -16
  29. package/packages/core/src/response.ts +46 -1
  30. package/packages/core/src/router.ts +29 -4
  31. package/packages/core/src/server.ts +886 -421
  32. package/packages/core/src/session.ts +244 -27
  33. package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
  34. package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
  35. package/packages/core/src/sessionHandlers/mongoClient.ts +293 -208
  36. package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
  37. package/packages/core/src/sessionHandlers/respClient.ts +16 -147
  38. package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
  39. package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
  40. package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
  41. package/packages/core/src/testClient.ts +18 -5
  42. package/packages/core/src/trustedProxy.ts +249 -0
  43. package/packages/core/src/types.ts +29 -5
  44. package/packages/core/src/websocket.ts +66 -0
  45. package/packages/orm/dist/index.js +22717 -20168
  46. package/packages/orm/src/adapters/firebird.ts +183 -56
  47. package/packages/orm/src/adapters/mongodb.ts +25 -4
  48. package/packages/orm/src/adapters/mssql.ts +114 -29
  49. package/packages/orm/src/adapters/mysql.ts +103 -40
  50. package/packages/orm/src/adapters/odbc.ts +44 -21
  51. package/packages/orm/src/adapters/postgres.ts +118 -26
  52. package/packages/orm/src/adapters/sqlDialect.ts +120 -0
  53. package/packages/orm/src/adapters/sqlite.ts +60 -24
  54. package/packages/orm/src/autoCrud.ts +12 -10
  55. package/packages/orm/src/baseModel.ts +135 -40
  56. package/packages/orm/src/cachedDatabase.ts +43 -19
  57. package/packages/orm/src/connectTimeout.ts +265 -0
  58. package/packages/orm/src/database.ts +241 -197
  59. package/packages/orm/src/databaseResult.ts +51 -28
  60. package/packages/orm/src/databaseUrl.ts +484 -0
  61. package/packages/orm/src/docstore.ts +386 -145
  62. package/packages/orm/src/index.ts +13 -6
  63. package/packages/orm/src/migration.ts +44 -11
  64. package/packages/orm/src/model.ts +4 -0
  65. package/packages/orm/src/queryBuilder.ts +47 -6
  66. package/packages/orm/src/sqlTranslator.ts +310 -4
  67. package/packages/orm/src/types.ts +21 -77
  68. package/packages/swagger/dist/index.js +78 -20
  69. package/packages/swagger/src/generator.ts +172 -29
  70. package/types/core/src/ai.d.ts +1 -1
  71. package/types/core/src/auth.d.ts +28 -5
  72. package/types/core/src/background.d.ts +3 -3
  73. package/types/core/src/cache.d.ts +15 -12
  74. package/types/core/src/dispatchPipeline.d.ts +117 -0
  75. package/types/core/src/dotenv.d.ts +38 -16
  76. package/types/core/src/index.d.ts +6 -9
  77. package/types/core/src/logger.d.ts +93 -16
  78. package/types/core/src/messenger.d.ts +47 -6
  79. package/types/core/src/metrics.d.ts +25 -61
  80. package/types/core/src/middleware.d.ts +134 -11
  81. package/types/core/src/queue.d.ts +54 -5
  82. package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -1
  83. package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
  84. package/types/core/src/queueBackends/mongoBackend.d.ts +24 -2
  85. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +3 -3
  86. package/types/core/src/router.d.ts +14 -3
  87. package/types/core/src/server.d.ts +15 -4
  88. package/types/core/src/session.d.ts +87 -2
  89. package/types/core/src/sessionHandlers/databaseHandler.d.ts +60 -5
  90. package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
  91. package/types/core/src/sessionHandlers/mongoClient.d.ts +16 -5
  92. package/types/core/src/sessionHandlers/mongoHandler.d.ts +51 -3
  93. package/types/core/src/sessionHandlers/respClient.d.ts +2 -2
  94. package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
  95. package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
  96. package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
  97. package/types/core/src/trustedProxy.d.ts +44 -0
  98. package/types/core/src/types.d.ts +28 -5
  99. package/types/core/src/websocket.d.ts +26 -0
  100. package/types/orm/src/adapters/firebird.d.ts +55 -10
  101. package/types/orm/src/adapters/mongodb.d.ts +2 -2
  102. package/types/orm/src/adapters/mssql.d.ts +18 -11
  103. package/types/orm/src/adapters/mysql.d.ts +11 -10
  104. package/types/orm/src/adapters/odbc.d.ts +9 -12
  105. package/types/orm/src/adapters/postgres.d.ts +11 -10
  106. package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
  107. package/types/orm/src/adapters/sqlite.d.ts +15 -3
  108. package/types/orm/src/baseModel.d.ts +45 -9
  109. package/types/orm/src/cachedDatabase.d.ts +18 -5
  110. package/types/orm/src/connectTimeout.d.ts +100 -0
  111. package/types/orm/src/database.d.ts +78 -28
  112. package/types/orm/src/databaseResult.d.ts +29 -15
  113. package/types/orm/src/databaseUrl.d.ts +125 -0
  114. package/types/orm/src/docstore.d.ts +102 -43
  115. package/types/orm/src/index.d.ts +6 -4
  116. package/types/orm/src/migration.d.ts +4 -3
  117. package/types/orm/src/queryBuilder.d.ts +23 -3
  118. package/types/orm/src/sqlTranslator.d.ts +126 -2
  119. package/types/orm/src/types.d.ts +21 -38
  120. package/packages/core/src/scss.ts +0 -623
  121. package/packages/core/src/sessionHandlers/redisHandler.ts +0 -219
  122. package/types/core/src/scss.d.ts +0 -19
  123. package/types/core/src/sessionHandlers/redisHandler.d.ts +0 -60
@@ -1,28 +1,38 @@
1
1
  /**
2
- * Tina4 synchronous MongoDB transport — used by the MongoDB session handler.
2
+ * Tina4 synchronous MongoDB transport — ONE persistent client per target.
3
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:
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
- * 1. The official `mongodb` driver when it resolves (robust, the normal path
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 minimal
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 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
+ * 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 { execFileSync } from "node:child_process";
25
- import { childFailureError } from "./childError.js";
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
- /** Command args: `filter` (always), plus `data`/`last_accessed` for an update. */
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
- * Run a session Mongo command synchronously and return the raw child stdout.
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 unreachable,
49
- * timeout) OR a Mongo command error (`ok != 1`), so the Session boundary can
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 script = `
59
- const net = require("node:net");
60
- const host = ${JSON.stringify(target.host)};
61
- const port = ${target.port};
62
- const database = ${JSON.stringify(target.database)};
63
- const collection = ${JSON.stringify(target.collection)};
64
- const command = ${JSON.stringify(command)};
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
- // ── raw OP_MSG fallback (zero-dep, mirrors the Python master codec) ──
103
- function encElem(key, value) {
104
- const ck = Buffer.concat([Buffer.from(key, "utf-8"), Buffer.from([0])]);
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
  }