jsql-neo 6.5.1-beta.2 → 6.5.1

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
4
4
  > MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
5
5
 
6
- > **v6.5.1-beta.2** — pre-release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.5.1** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
7
7
 
8
8
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
9
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
@@ -38,16 +38,17 @@ npm install jsql-neo
38
38
 
39
39
  **JSQL-NEO** is a Rust-powered embedded database with **zero runtime dependencies**. It runs in Node.js, CLI, Web UI, and the browser (WASM).
40
40
 
41
- - **MySQL wire protocol, ready to use** — `jsql serve` starts it; `mysql2` / most MySQL clients connect directly
41
+ - **Four wire protocols on one port** — `jsql serve` accepts MySQL, PostgreSQL, MongoDB and Redis clients; `mysql2`, `pg`, `mongodb` and `ioredis` connect directly
42
42
  - **Zero dependencies** — no native libs to compile, no system deps to install
43
43
  - **Three engines, same API** — `native` (Rust N-API), `wasm` (same Rust core), `js` (pure JS)
44
44
  - **~2× faster than better-sqlite3** on indexed point lookups
45
45
 
46
- > **About the other wire protocols** — PostgreSQL, MongoDB and Redis protocol stacks
47
- > ship as libraries (`lib/pg_server`, `lib/mongo_server`, `lib/redis_server`) and each
48
- > listens on its own port; there is **no single-port multi-protocol mode**, and the CLI
49
- > exposes no entry point for them. MongoDB additionally does not yet complete a
50
- > handshake with the modern official `mongodb` driver. Treat them as experimental.
46
+ > **Wire protocols** — MySQL, PostgreSQL, MongoDB and Redis share a single listener
47
+ > (`jsql serve`, default port 3306). The server sniffs the first packet of each
48
+ > connection and hands it to the matching protocol handler. MySQL / PostgreSQL /
49
+ > MongoDB read the same `Database` engine (same tables), Redis keeps its own key
50
+ > namespace. Verified with `mysql2`, `pg`, `mongodb` (driver 7.x) and `ioredis`;
51
+ > pass `--mysql-only` to serve the MySQL protocol alone.
51
52
 
52
53
  ---
53
54
 
@@ -100,13 +101,23 @@ db.stop();
100
101
  jsql serve --port 3306 --data-dir ./data
101
102
  ```
102
103
 
104
+ All four protocols share the same port:
105
+
103
106
  ```bash
104
- # MySQL clients (mysql CLI, mysql2, DBeaver, DataGrip …) connect here
105
- mysql -h 127.0.0.1 -P 3306
107
+ # MySQL clients (mysql CLI, mysql2, DBeaver, DataGrip …)
108
+ mysql -h 127.0.0.1 -P 3306 -u root
109
+
110
+ # PostgreSQL clients (psql, pg, pgAdmin …)
111
+ psql "host=127.0.0.1 port=3306 user=postgres dbname=app"
112
+
113
+ # MongoDB clients (mongosh, mongodb driver, Compass …)
114
+ mongosh "mongodb://127.0.0.1:3306/app?directConnection=true"
115
+
116
+ # Redis clients (redis-cli, ioredis …)
117
+ redis-cli -h 127.0.0.1 -p 3306 PING
106
118
  ```
107
119
 
108
- Only MySQL is served by the CLI today. The PostgreSQL / Redis / MongoDB stacks are
109
- available as libraries — see the note near the top of this file.
120
+ Pass `--mysql-only` to serve the MySQL wire protocol alone.
110
121
 
111
122
  ### Browser / WASM
112
123
 
package/bin/jsql CHANGED
@@ -8,7 +8,9 @@ const migrate = require('../lib/migrate');
8
8
  const { nag, setStarred, readConfig } = require('../lib/star-nag');
9
9
 
10
10
  // star 提示:任何异常都不该挡住命令本身
11
- if (!process.env.JSQL_NO_STAR_NAG && process.argv[2] !== 'star') {
11
+ // version / --version 只回版本号、star 本身就是干这件事的 —— 这些不提示,其余全部提示
12
+ const NO_NAG_ARGS = new Set(['version', '--version', '-v', 'star']);
13
+ if (!process.env.JSQL_NO_STAR_NAG && !NO_NAG_ARGS.has(process.argv[2])) {
12
14
  try { nag(); } catch (e) { /* 提示失败与命令无关 */ }
13
15
  }
14
16
 
@@ -154,14 +156,14 @@ const cli = yaggs({ pkg: require('../package.json') })
154
156
  }
155
157
  } catch (e) { fail(e); }
156
158
  })
157
- .command('serve', 'Run the MySQL-compatible server in the foreground', (sub) => {
159
+ .command('serve', 'Run the multi-protocol server (MySQL / PostgreSQL / MongoDB / Redis on one port)', (sub) => {
158
160
  sub.option('port', { alias: ['p'], type: 'number', description: 'Listen port (default 3306)' });
159
161
  sub.option('host', { type: 'string', description: 'Listen host (default 127.0.0.1)' });
160
162
  sub.option('data-dir', { type: 'string', description: 'Directory to store databases' });
161
163
  sub.option('no-auth', { type: 'boolean', description: 'Allow connections without authentication' });
162
164
  sub.option('log', { type: 'boolean', description: 'Log queries to stdout' });
165
+ sub.option('mysql-only', { type: 'boolean', description: 'Serve the MySQL wire protocol only' });
163
166
  }, async (argv) => {
164
- const { createMysqlServer } = require('../lib/mysql_server');
165
167
  const options = {
166
168
  port: argv.port != null ? argv.port : 3306,
167
169
  host: argv.host || '127.0.0.1',
@@ -169,9 +171,27 @@ const cli = yaggs({ pkg: require('../package.json') })
169
171
  noAuth: argv['no-auth'] === true,
170
172
  };
171
173
  if (argv.log) options.onQuery = (q) => console.log('[sql]', q);
172
- const srv = createMysqlServer(options);
173
- srv.listen();
174
- console.log(`jsql-neo MySQL server on ${options.host}:${options.port}${options.dataDir ? ' (data: ' + options.dataDir + ')' : ' (in-memory)'}`);
174
+ options.onError = (err) => {
175
+ console.error(`serve failed: ${err.message}`);
176
+ process.exit(1);
177
+ };
178
+ const mysqlOnly = argv['mysql-only'] === true;
179
+ if (mysqlOnly) {
180
+ const { createMysqlServer } = require('../lib/mysql_server');
181
+ const srv = createMysqlServer(options);
182
+ srv.listen();
183
+ console.log(`jsql-neo MySQL server on ${options.host}:${options.port}${options.dataDir ? ' (data: ' + options.dataDir + ')' : ' (in-memory)'}`);
184
+ } else {
185
+ const { createMultiServer } = require('../lib/multiserver');
186
+ const srv = createMultiServer(options);
187
+ srv.listen(() => {
188
+ console.log(`jsql-neo multi-protocol server on ${options.host}:${options.port}${options.dataDir ? ' (data: ' + options.dataDir + ')' : ' (in-memory)'}`);
189
+ console.log(' MySQL / PostgreSQL / MongoDB / Redis — one port, shared data');
190
+ });
191
+ const shutdown = () => srv.close(() => process.exit(0));
192
+ process.once('SIGINT', shutdown);
193
+ process.once('SIGTERM', shutdown);
194
+ }
175
195
  setInterval(() => {}, 1 << 30);
176
196
  })
177
197
  .command('star', 'Confirm whether you starred this repo (opens browser once)', (sub) => {
@@ -189,7 +209,20 @@ const cli = yaggs({ pkg: require('../package.json') })
189
209
  const nonceUrl = sn.startOauthCheck(); // 打开浏览器 + 后台轮询写回 ~/.jsqlmr
190
210
  if (!process.stdout.isTTY) console.log(' ' + nonceUrl);
191
211
  console.log('After you authorize, this config is updated: ' + sn.configPath());
192
- console.log('Run `jsql star` again afterwards to see the result.');
212
+ console.log('Waiting for authorization (up to 3 minutes)…');
213
+ // startOauthCheck 的定时器是 unref 的,命令必须自己前台等,否则接不到结果
214
+ const res = sn.waitForResult(nonceUrl, 180000);
215
+ if (res && res.starred) {
216
+ sn.setStarred(true);
217
+ console.log(`Confirmed: ${res.login} has starred ${res.repo}. Thanks!`);
218
+ return;
219
+ }
220
+ if (res) {
221
+ console.log(`Authorization finished, but ${res.login} has not starred ${res.repo} yet.`);
222
+ console.log(`Star it at https://github.com/${res.repo} then run \`jsql star\` again.`);
223
+ return;
224
+ }
225
+ console.log('Timed out or the link expired. Run `jsql star` again to retry.');
193
226
  })
194
227
  .command('bench', 'Insert + query benchmark against a data directory', (sub) => {
195
228
  sub.option('n', { type: 'number', description: 'Number of rows (default 10000)' });
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.5.1-beta.2 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.5.1 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
@@ -155,20 +155,28 @@ class MongoServer {
155
155
  this._server = null;
156
156
  this._sockets = new Set();
157
157
  this._dbCollections = new Map(); // db name → Set of collection names
158
+ this._engineByDb = new Map(); // db name → Database (standalone 模式)
158
159
  }
159
160
 
160
- async _getEngine() {
161
+ async _getEngine(db = null) {
162
+ if (this._engineByDb && db) {
163
+ if (!this._engineByDb.has(db)) this._engineByDb.set(db, this._createEngine(db));
164
+ return this._engineByDb.get(db);
165
+ }
161
166
  if (this._engine) return this._engine;
167
+ this._engine = this._createEngine(null);
168
+ return this._engine;
169
+ }
170
+
171
+ _createEngine(db) {
162
172
  if (this.dataDir && this.dataDir !== ':memory:') {
163
- this._engine = new Database(path.join(this.dataDir, 'default'), { autoSave: true });
164
- } else {
165
- this._engine = new Database(':memory:', { autoSave: false });
173
+ return new Database(path.join(this.dataDir, db || 'default'), { autoSave: true });
166
174
  }
167
- return this._engine;
175
+ return new Database(':memory:', { autoSave: false });
168
176
  }
169
177
 
170
178
  async _ensureCollection(name, db = null) {
171
- const engine = await this._getEngine();
179
+ const engine = await this._getEngine(db);
172
180
  if (!engine.getTableSchema(name)) {
173
181
  engine.createTable(name, {});
174
182
  }
@@ -190,25 +198,35 @@ class MongoServer {
190
198
  socket.on('close', () => this._sockets.delete(socket));
191
199
  let buf = Buffer.isBuffer(existing) ? Buffer.from(existing) : Buffer.alloc(0);
192
200
  socket.on('error', () => {});
201
+ let processing = false;
202
+ let again = false;
193
203
  const processLoop = async () => {
194
- for (;;) {
195
- if (buf.length < 4) break;
196
- const len = buf.readInt32LE(0);
197
- if (len < 16 || buf.length < len) break;
198
- const msg = buf.slice(0, len);
199
- buf = buf.slice(len);
200
- try {
201
- await this._handleMessage(socket, msg);
202
- } catch (e) {
203
- try { socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: e.message || String(e) }), this._reqId(msg))); } catch (_) {}
204
+ if (processing) { again = true; return; }
205
+ processing = true;
206
+ try {
207
+ for (;;) {
208
+ if (buf.length < 4) break;
209
+ const len = buf.readInt32LE(0);
210
+ if (len < 16 || len > 48 * 1024 * 1024) break;
211
+ if (buf.length < len) break;
212
+ const msg = buf.slice(0, len);
213
+ buf = buf.slice(len);
214
+ try {
215
+ await this._handleMessage(socket, msg);
216
+ } catch (e) {
217
+ try { socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: e.message || String(e) }), this._reqId(msg))); } catch (_) {}
218
+ }
204
219
  }
220
+ } finally {
221
+ processing = false;
222
+ if (again) { again = false; processLoop(); }
205
223
  }
206
224
  };
207
- socket.on('data', async (chunk) => {
225
+ socket.on('data', (chunk) => {
208
226
  buf = Buffer.concat([buf, chunk]);
209
- await process();
227
+ processLoop();
210
228
  });
211
- if (buf.length > 0) process();
229
+ if (buf.length > 0) processLoop();
212
230
  }
213
231
 
214
232
  _reqId(msg) {
@@ -272,13 +290,45 @@ class MongoServer {
272
290
  socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: `opCode ${op2} not supported` }), this._reqId(msg)));
273
291
  return;
274
292
  }
293
+ const flags = msg.readInt32LE(16);
294
+ const msgEnd = (flags & 1) ? msg.length - 4 : msg.length; // checksumPresent
275
295
  let pos = 16 + 4; // header + flagBits
276
- const kind = msg[pos]; pos++; // section kind: 0 = single BSON doc
277
- if (kind !== 0) {
278
- socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: `section kind ${kind} not supported` }), this._reqId(msg)));
296
+ const body = {};
297
+ let sawBody = false;
298
+ while (pos < msgEnd) {
299
+ const kind = msg[pos++];
300
+ if (kind === 0) {
301
+ const r = bsonReadDocument(msg, pos);
302
+ Object.assign(body, r.value);
303
+ pos = r.pos;
304
+ sawBody = true;
305
+ } else if (kind === 1) {
306
+ // Document sequence: int32 size, cstring identifier, BSON docs
307
+ const size = msg.readInt32LE(pos);
308
+ const end = pos + size;
309
+ if (size < 5 || end > msgEnd) throw new Error('malformed document sequence');
310
+ let p = pos + 4;
311
+ const nameEnd = msg.indexOf(0, p);
312
+ if (nameEnd < 0 || nameEnd >= end) throw new Error('malformed document sequence identifier');
313
+ const ident = msg.toString('utf8', p, nameEnd);
314
+ p = nameEnd + 1;
315
+ const seq = [];
316
+ while (p < end) {
317
+ const r = bsonReadDocument(msg, p);
318
+ seq.push(r.value);
319
+ p = r.pos;
320
+ }
321
+ body[ident] = seq;
322
+ pos = end;
323
+ } else {
324
+ socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: `section kind ${kind} not supported` }), this._reqId(msg)));
325
+ return;
326
+ }
327
+ }
328
+ if (!sawBody) {
329
+ socket.write(this._wrapMsg(bsonDocument({ ok: 0, errmsg: 'missing OP_MSG body section' }), this._reqId(msg)));
279
330
  return;
280
331
  }
281
- const body = bsonReadDocument(msg, pos).value;
282
332
  const command = this._commandName(body);
283
333
  const reply = await this._dispatch(command, body);
284
334
  socket.write(this._wrapMsg(bsonDocument(reply), this._reqId(msg)));
@@ -332,13 +382,13 @@ class MongoServer {
332
382
  case 'endSessions': return { ok: 1 };
333
383
  case 'listDatabases': {
334
384
  // P3-17: listDatabases 只返回当前库——返回所有数据库
335
- const engine = await this._getEngine();
385
+ const engine = await this._getEngine(this._dbOf(doc));
336
386
  const dbs = Object.keys(engine._meta && engine._meta.databases ? engine._meta.databases : { [this._dbOf(doc)]: 1 });
337
387
  const databases = Object.keys(dbs).map(name => ({ name, sizeOnDisk: 1, empty: false }));
338
388
  return { ok: 1, databases, totalSize: databases.length };
339
389
  }
340
390
  case 'listCollections': {
341
- const engine = await this._getEngine();
391
+ const engine = await this._getEngine(this._dbOf(doc));
342
392
  const tables = Object.keys(engine._meta && engine._meta.tables ? engine._meta.tables : (engine._tables || {}));
343
393
  const firstBatch = tables.map((t) => ({ name: t, type: 'collection', options: {}, info: { readOnly: false, uuid: '' } }));
344
394
  return { ok: 1, cursor: { id: { $long: 0 }, ns: `${this._dbOf(doc)}.$cmd.listCollections`, firstBatch } };
@@ -349,7 +399,7 @@ class MongoServer {
349
399
  }
350
400
  case 'drop': {
351
401
  try {
352
- const engine = await this._getEngine();
402
+ const engine = await this._getEngine(this._dbOf(doc));
353
403
  engine.dropTable(String(doc.drop));
354
404
  return { ok: 1, ns: `${this._dbOf(doc)}.${doc.drop}` };
355
405
  } catch (e) {
@@ -365,10 +415,11 @@ class MongoServer {
365
415
  const doc2 = { ...d };
366
416
  // 检查 _id 唯一性
367
417
  if (doc2._id !== undefined) {
368
- const existing = engine.find(coll, { _id: doc2._id });
418
+ const existing = await engine.find(coll, { _id: doc2._id });
369
419
  if (existing.length > 0) continue; // 跳过重复 _id
370
420
  }
371
- ids.push(engine.insert(coll, doc2));
421
+ await engine.insert(coll, doc2);
422
+ ids.push(doc2._id);
372
423
  }
373
424
  return { ok: 1, n: ids.length };
374
425
  }
@@ -376,23 +427,23 @@ class MongoServer {
376
427
  const coll = String(doc.findAndModify);
377
428
  const engine = await this._ensureCollection(coll, this._dbOf(doc));
378
429
  const q = doc.query || {};
379
- const rows = this._matching(engine, coll, q);
430
+ const rows = await this._matching(engine, coll, q);
380
431
  const found = rows[0] || null;
381
432
  const set = doc.update && doc.update.$set ? doc.update.$set : (doc.update || {});
382
433
  let value = null;
383
434
  if (found) {
384
435
  if (doc.remove) {
385
- this._removeRows(engine, coll, [found]);
436
+ await this._removeRows(engine, coll, [found]);
386
437
  value = found;
387
438
  } else {
388
439
  const patched = { ...found, ...set };
389
- this._patchRows(engine, coll, [found], set);
440
+ await this._patchRows(engine, coll, [found], set);
390
441
  value = doc.new ? patched : found;
391
442
  }
392
443
  } else if (doc.upsert) {
393
444
  const merged = { ...q, ...set };
394
- const id = engine.insert(coll, merged);
395
- value = doc.new ? { ...merged, _id: id } : null;
445
+ await engine.insert(coll, merged);
446
+ value = doc.new ? { ...merged } : null;
396
447
  }
397
448
  return { ok: 1, value, lastErrorObject: { n: found ? 1 : (doc.upsert ? 1 : 0), updatedExisting: !!found && !doc.remove } };
398
449
  }
@@ -400,7 +451,7 @@ class MongoServer {
400
451
  const coll = String(doc.distinct);
401
452
  const engine = await this._ensureCollection(coll, this._dbOf(doc));
402
453
  const key = String(doc.key || '');
403
- const rows = this._matching(engine, coll, doc.query);
454
+ const rows = await this._matching(engine, coll, doc.query);
404
455
  const seen = new Set();
405
456
  const values = [];
406
457
  for (const r of rows) {
@@ -411,7 +462,7 @@ class MongoServer {
411
462
  }
412
463
  case 'dropDatabase': {
413
464
  const dbName = String(doc.$db || 'test');
414
- const engine = await this._getEngine();
465
+ const engine = await this._getEngine(dbName);
415
466
  const colls = this._dbCollections.get(dbName);
416
467
  if (colls) {
417
468
  for (const c of colls) {
@@ -424,7 +475,7 @@ class MongoServer {
424
475
  case 'find': {
425
476
  const coll = String(doc.find);
426
477
  const engine = await this._ensureCollection(coll, this._dbOf(doc));
427
- let rows = this._matching(engine, coll, doc.filter);
478
+ let rows = await this._matching(engine, coll, doc.filter);
428
479
  const skip = typeof doc.skip === 'number' ? doc.skip : 0;
429
480
  if (skip > 0) rows = rows.slice(skip);
430
481
  const limit = typeof doc.limit === 'number' ? doc.limit : 0;
@@ -442,24 +493,24 @@ class MongoServer {
442
493
  const q = u.q || {};
443
494
  const set = u.u && u.u.$set ? u.u.$set : (u.u || {});
444
495
  const multi = u.multi === true;
445
- let targets = this._matching(engine, coll, q);
496
+ let targets = await this._matching(engine, coll, q);
446
497
  if (!targets.length && u.upsert) {
447
498
  const merged = { ...q, ...set };
448
- engine.insert(coll, merged);
499
+ await engine.insert(coll, merged);
449
500
  n++;
450
501
  continue;
451
502
  }
452
503
  if (multi) {
453
504
  // 更新所有匹配的行
454
505
  for (const target of targets) {
455
- this._patchRows(engine, coll, [target], u.u || { $set: set });
506
+ await this._patchRows(engine, coll, [target], u.u || { $set: set });
456
507
  n++;
457
508
  }
458
509
  } else {
459
510
  // 只更新第一条
460
511
  let target = targets[0] || null;
461
512
  if (target) {
462
- this._patchRows(engine, coll, [target], u.u || { $set: set });
513
+ await this._patchRows(engine, coll, [target], u.u || { $set: set });
463
514
  n++;
464
515
  }
465
516
  }
@@ -471,10 +522,10 @@ class MongoServer {
471
522
  const engine = await this._ensureCollection(coll, this._dbOf(doc));
472
523
  let n = 0;
473
524
  for (const d of (doc.deletes || [])) {
474
- const rows = this._matching(engine, coll, d.q || {});
525
+ const rows = await this._matching(engine, coll, d.q || {});
475
526
  // MongoDB delete: limit=1 means delete only one, limit=0 means delete all
476
527
  const toDelete = d.limit === 1 ? rows.slice(0, 1) : rows;
477
- if (toDelete.length > 0) this._removeRows(engine, coll, toDelete);
528
+ if (toDelete.length > 0) await this._removeRows(engine, coll, toDelete);
478
529
  n += toDelete.length;
479
530
  }
480
531
  return { ok: 1, n };
@@ -482,14 +533,14 @@ class MongoServer {
482
533
  case 'count': {
483
534
  const coll = String(doc.count);
484
535
  const engine = await this._ensureCollection(coll, this._dbOf(doc));
485
- const rows = this._matching(engine, coll, doc.query);
536
+ const rows = await this._matching(engine, coll, doc.query);
486
537
  return { ok: 1, n: Number(doc.limit) > 0 ? Math.min(rows.length, doc.limit) : rows.length };
487
538
  }
488
539
  case 'aggregate': {
489
540
  const coll = String(doc.aggregate);
490
541
  const engine = await this._ensureCollection(coll, this._dbOf(doc));
491
542
  const pipeline = doc.pipeline || [];
492
- let rows = engine.find(coll, {});
543
+ let rows = await engine.find(coll, {});
493
544
  for (const stage of pipeline) {
494
545
  if (stage.$match) rows = rows.filter((r) => this._match(r, stage.$match));
495
546
  else if (stage.$count) rows = [{ [stage.$count]: rows.length }];
@@ -613,11 +664,19 @@ class MongoServer {
613
664
  return row._id != null ? row._id : row.id;
614
665
  }
615
666
 
616
- _matching(engine, coll, filter) {
617
- return engine.find(coll, {}).filter((r) => this._match(r, filter || {}));
667
+ async _matching(engine, coll, filter) {
668
+ const rows = await engine.find(coll, {});
669
+ return rows.filter((r) => this._match(r, filter || {}));
670
+ }
671
+
672
+ _rowKey(engine, coll, row) {
673
+ const table = engine._tables && engine._tables[coll];
674
+ const pk = table && (table._primaryKey || table._autoIncrementField);
675
+ // 有主键用主键值;Mongo 自建集合没有主键,传行对象走深度匹配
676
+ return pk ? row[pk] : { ...row };
618
677
  }
619
678
 
620
- _patchRows(engine, coll, rows, updateDoc) {
679
+ async _patchRows(engine, coll, rows, updateDoc) {
621
680
  const entries = [];
622
681
  // 检查是否有更新操作符
623
682
  const operators = ['$inc', '$unset', '$push', '$set'];
@@ -626,14 +685,14 @@ class MongoServer {
626
685
  if (!hasOperators) {
627
686
  // 没有操作符,直接 set
628
687
  for (const r of rows) {
629
- const id = this._idOf(r);
630
- if (id !== undefined) entries.push([id, { ...updateDoc }]);
688
+ const key = this._rowKey(engine, coll, r);
689
+ if (key !== undefined) entries.push([key, { ...updateDoc }]);
631
690
  }
632
691
  } else {
633
692
  // 有操作符,需要特殊处理
634
693
  for (const r of rows) {
635
- const id = this._idOf(r);
636
- if (id === undefined) continue;
694
+ const key = this._rowKey(engine, coll, r);
695
+ if (key === undefined) continue;
637
696
  const update = {};
638
697
  if (updateDoc.$set) Object.assign(update, updateDoc.$set);
639
698
  if (updateDoc.$inc) {
@@ -654,21 +713,21 @@ class MongoServer {
654
713
  update[field] = null;
655
714
  }
656
715
  }
657
- if (Object.keys(update).length > 0) entries.push([id, update]);
716
+ if (Object.keys(update).length > 0) entries.push([key, update]);
658
717
  }
659
718
  }
660
- if (entries.length > 0) engine.updateByIds(coll, entries);
719
+ if (entries.length > 0) await engine.updateByIds(coll, entries);
661
720
  }
662
721
 
663
- _removeRows(engine, coll, rows) {
722
+ async _removeRows(engine, coll, rows) {
664
723
  // 收集所有需要删除的行的 id,走 engine.removeByIds
665
724
  // 这样会写 WAL、触发 hooks、检查约束
666
725
  const ids = [];
667
726
  for (const r of rows) {
668
- const id = this._idOf(r);
669
- if (id !== undefined) ids.push(id);
727
+ const key = this._rowKey(engine, coll, r);
728
+ if (key !== undefined) ids.push(key);
670
729
  }
671
- if (ids.length > 0) engine.removeByIds(coll, ids);
730
+ if (ids.length > 0) await engine.removeByIds(coll, ids);
672
731
  }
673
732
 
674
733
  _match(row, filter) {
@@ -729,7 +788,11 @@ class MongoServer {
729
788
  if (this._server) { this._server.close(cb || (() => {})); this._server = null; }
730
789
  else if (cb) cb();
731
790
  };
732
- if (this._engine && typeof this._engine.stop === 'function') this._engine.stop().then(done).catch(done);
791
+ const engines = new Set(this._engineByDb ? this._engineByDb.values() : []);
792
+ if (this._engine) engines.add(this._engine);
793
+ const stops = [];
794
+ for (const e of engines) if (e && typeof e.stop === 'function') stops.push(e.stop());
795
+ if (stops.length > 0) Promise.allSettled(stops).then(done).catch(done);
733
796
  else done();
734
797
  return this;
735
798
  }
@@ -42,6 +42,10 @@ class MultiServer {
42
42
  this.port = options.port ?? 5432;
43
43
  this.host = options.host || '127.0.0.1';
44
44
  this.dataDir = options.dataDir || null;
45
+ // 静默连接的兜底协议:MySQL 客户端等待服务器先发握手包,
46
+ // 而空闲的 Redis 连接也不主动发包,只能按配置的二选一处理。
47
+ this.idleProtocol = options.idleProtocol || 'mysql';
48
+ this.sniffTimeout = options.sniffTimeout != null ? options.sniffTimeout : 200;
45
49
  // 共享 SQL 引擎:三种 SQL 协议读写同一份数据
46
50
  this._engines = new Map();
47
51
  const getEngine = async (dbName) => {
@@ -72,13 +76,17 @@ class MultiServer {
72
76
  listen(cb) {
73
77
  this._server = net.createServer((socket) => {
74
78
  this._sockets.add(socket);
79
+ socket.on('error', () => {});
75
80
  socket.on('close', () => this._sockets.delete(socket));
76
81
  let first = Buffer.alloc(0);
77
82
  let routed = false;
78
- // MySQL 客户端会先等服务器握手包,不主动发字节;超时后按 MySQL 处理
83
+ // MySQL 客户端会先等服务器握手包,不主动发字节;超时后按 idleProtocol 处理
79
84
  const t = setTimeout(() => {
80
- if (!routed) { routed = true; socket.removeListener('data', sniff); this._route(socket, first); }
81
- }, 200);
85
+ if (routed) return;
86
+ routed = true;
87
+ socket.removeListener('data', sniff);
88
+ try { this._route(socket, first); } catch (e) { socket.destroy(); }
89
+ }, this.sniffTimeout);
82
90
  t.unref();
83
91
  const sniff = (chunk) => {
84
92
  first = Buffer.concat([first, chunk]);
@@ -104,7 +112,7 @@ class MultiServer {
104
112
  }
105
113
 
106
114
  _route(socket, first) {
107
- const proto = sniffProtocol(first) || 'mysql'; // 无字节(MySQL 客户端等握手)→ 按 MySQL 处理
115
+ const proto = sniffProtocol(first) || this.idleProtocol;
108
116
  if (proto === 'mysql') {
109
117
  const conn = new MysqlConnection(socket, this.mysql);
110
118
  conn._onData(first);
package/lib/star-nag.js CHANGED
@@ -24,7 +24,9 @@ const REPO = 'vexify-org/JSQL-neo';
24
24
  const URL = `https://github.com/${REPO}`;
25
25
  const NAG_INTERVAL_DAYS = 30;
26
26
 
27
- const AUTH_BASE = process.env.JSQL_AUTH_BASE || 'https://auth.vexify.top/callback';
27
+ const AUTH_BASE = process.env.JSQL_AUTH_BASE || 'https://auth.vexify.top';
28
+ const AUTH_START = AUTH_BASE + '/start.php';
29
+ const AUTH_RESULT = AUTH_BASE + '/result.php';
28
30
 
29
31
  /**
30
32
  * 发起 OAuth 浏览器流并在后台轮询结果。
@@ -36,7 +38,7 @@ const AUTH_BASE = process.env.JSQL_AUTH_BASE || 'https://auth.vexify.top/callbac
36
38
  function startOauthCheck() {
37
39
  const crypto = require('crypto');
38
40
  const nonce = crypto.randomBytes(24).toString('base64url');
39
- const url = AUTH_BASE + '?action=start&nonce=' + nonce;
41
+ const url = AUTH_START + '?nonce=' + nonce;
40
42
 
41
43
  // 开浏览器:三平台各一个命令;失败就只打印链接让人手动点
42
44
  const cmd = process.platform === 'win32' ? 'start'
@@ -54,7 +56,7 @@ function startOauthCheck() {
54
56
  const t = setInterval(() => {
55
57
  if (++tries > 45) { clearInterval(t); return; }
56
58
  const { execFile } = require('child_process');
57
- execFile('curl', ['-sS', '-m', '5', AUTH_BASE + '?action=result&nonce=' + nonce],
59
+ execFile('curl', ['-sS', '-m', '5', AUTH_RESULT + '?nonce=' + nonce],
58
60
  { timeout: 6000, encoding: 'utf8' }, (err, stdout) => {
59
61
  if (err) return;
60
62
  let j = null;
@@ -66,7 +68,7 @@ function startOauthCheck() {
66
68
  cfg.login = j.result.login || undefined;
67
69
  cfg.starred = !!j.result.starred;
68
70
  if (cfg.starred) cfg.starredAt = Date.now();
69
- writeConfig(cfg); // nonce.json 由服务端 TTL 自清理
71
+ writeConfig(cfg);
70
72
  });
71
73
  }, 2000);
72
74
  t.unref();
@@ -159,35 +161,37 @@ function checkStarred() {
159
161
  * @param {boolean} [opts.force] 忽略节流,强制提示一次
160
162
  * @returns {boolean} 是否真的打印了
161
163
  */
164
+ /**
165
+ * 在 stdout 上印一行 star 提示。
166
+ *
167
+ * 规则(用户明确要求):
168
+ * 1. **不强制** —— 只印文字,绝不自动弹浏览器。要确认请用户自己跑 `jsql star`。
169
+ * 2. **除 `version` 外的每个命令都印**,没有 30 天节流这类「只提醒一次」的设计。
170
+ * 3. `~/.jsqlmr` 记录是否 star 过 —— `starred: true` 是唯一的闭嘴开关
171
+ * (另外 `JSQL_NO_STAR_NAG=1` 可临时静音)。
172
+ *
173
+ * @returns {boolean} 是否真的印了
174
+ */
162
175
  function nag(opts = {}) {
163
176
  const out = opts.out || process.stdout;
164
177
  const cfg = readConfig();
165
178
 
179
+ // 已确认 star 过 → 永久闭嘴。jsqlmr 就是干这个的。
166
180
  if (!opts.force && cfg.starred) return false;
167
181
 
168
- const last = Number(cfg.lastNagAt) || 0;
169
- if (!opts.force && last && Date.now() - last < NAG_INTERVAL_DAYS * 86400000) {
170
- // 顺便有机会补记 star 状态(不打印)
171
- const s = checkStarred();
172
- if (s) { cfg.starred = true; writeConfig(cfg); }
173
- return false;
174
- }
175
-
176
- // 有 token 就先查,省得对已经 star 的人重复啰嗦
182
+ // 有凭据就先查一下,省得对已经 star 的人反复啰嗦(查不到就当没查,照印)
177
183
  const starred = checkStarred();
178
184
  if (starred) {
179
185
  cfg.starred = true;
180
186
  cfg.starredAt = Date.now();
181
187
  writeConfig(cfg);
182
- return false; // 已 star,闭嘴
188
+ return false;
183
189
  }
184
190
 
185
191
  out.write(`\n If JSQL-NEO saved you time, a star helps others find it: ${URL}\n`);
186
- out.write(` (set JSQL_NO_STAR_NAG=1 to hide, or run \`jsql star\` to confirm and stop seeing this)\n\n`);
187
-
188
- cfg.lastNagAt = Date.now();
189
- writeConfig(cfg);
192
+ out.write(` Run \`jsql star\` to confirm and stop seeing this (or set JSQL_NO_STAR_NAG=1 to hide)\n\n`);
190
193
 
194
+ // 只记录「有没有 star」,不再写 lastNagAt —— 没有节流,写它没有意义。
191
195
  return true;
192
196
  }
193
197
 
@@ -200,4 +204,43 @@ function setStarred(v) {
200
204
  return cfg.starred;
201
205
  }
202
206
 
203
- module.exports = { nag, checkStarred, startOauthCheck, readConfig, setStarred, configPath, REPO, URL };
207
+ /**
208
+ * 同步阻塞轮询授权结果,直到拿到 done / expired 或超时。
209
+ *
210
+ * `jsql star` 用:startOauthCheck 的定时器是 unref 的(不能拖住别的命令),
211
+ * 所以 star 命令必须自己前台等,否则授权完成后 CLI 早已退出,结果没人接。
212
+ *
213
+ * @param {string} startUrl startOauthCheck 返回的 URL(含 nonce)
214
+ * @param {number} [timeoutMs] 最长等待时间,默认 180 秒
215
+ * @returns {object|null} result 对象(含 login/starred/repo),超时或失败返回 null
216
+ */
217
+ function waitForResult(startUrl, timeoutMs = 180000) {
218
+ const { execFileSync } = require('child_process');
219
+ // 注意:本模块顶部有 const URL = 'https://github.com/...' 会遮蔽全局 URL 类,
220
+ // 必须显式从 url 模块取,否则 new URL(...) 抛 TypeError
221
+ const { URL: NodeURL } = require('url');
222
+ let nonce = '';
223
+ try {
224
+ nonce = new NodeURL(startUrl).searchParams.get('nonce') || '';
225
+ } catch (e) {
226
+ return null;
227
+ }
228
+ if (!nonce) return null;
229
+
230
+ const deadline = Date.now() + timeoutMs;
231
+ while (Date.now() < deadline) {
232
+ try {
233
+ const out = execFileSync('curl', ['-sS', '-m', '5', AUTH_RESULT + '?nonce=' + nonce],
234
+ { stdio: ['ignore', 'pipe', 'ignore'], timeout: 8000, encoding: 'utf8' });
235
+ const j = JSON.parse(out);
236
+ if (j && j.state === 'done' && j.result) return j.result;
237
+ if (j && j.state === 'expired') return null;
238
+ } catch (e) {
239
+ // 网络抖动 / 超时 —— 继续等
240
+ }
241
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
242
+ }
243
+ return null;
244
+ }
245
+
246
+ module.exports = { nag, checkStarred, startOauthCheck, waitForResult, readConfig, setStarred, configPath, REPO, URL };
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.5.1-beta.2",
3
+ "version": "6.5.1",
4
4
  "description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -51,13 +51,14 @@
51
51
  "test:btree": "node test/btree.test.js",
52
52
  "test:native": "node test/native.test.js",
53
53
  "test:regress": "node test/regress-5.1.0.js",
54
- "test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js",
54
+ "test:all": "node test/smoke.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/native.test.js && node test/native-multi-instance.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/wasm.test.js && node test/coverage.js && node test/regress-5.1.0.js && node test/join.test.js && node test/btree.test.js && node test/dml-constraints.test.js && node test/savepoint.test.js && node test/alter-table.test.js && node test/null-semantics.test.js && node test/identifier-keywords.test.js && node test/sql-semantics.test.js && node test/extra-coverage.test.js && node test/filter-compile.test.js && node test/recursive-cte.test.js && node test/index-ddl.test.js && node test/mongo-group.test.js && node test/multiprotocol.test.js",
55
55
  "test:coverage": "c8 --all --include='lib/**' --exclude='lib/web_ui.js' --reporter=text --reporter=lcov node test/coverage.js",
56
56
  "test:orms": "node examples/orms/run-all.js",
57
57
  "test:dml": "node test/dml-constraints.test.js",
58
58
  "test:cte": "node test/recursive-cte.test.js",
59
59
  "test:index": "node test/index-ddl.test.js",
60
60
  "test:mongo": "node test/mongo-group.test.js",
61
+ "test:multi": "node test/multiprotocol.test.js",
61
62
  "test:savepoint": "node test/savepoint.test.js",
62
63
  "test:alter": "node test/alter-table.test.js",
63
64
  "test:ident": "node test/identifier-keywords.test.js",
@@ -1,16 +1,5 @@
1
- /* ⚠️ 已知损坏,未纳入任何 CI 套件(test:all / npm test 都不含本文件)
2
- *
3
- * 现状:本文件会崩在 `rows is not iterable`(lib/mongo_server.js:541)。
4
- * 另外 MongoDB 协议栈与**官方现代 driver 的握手尚未打通** ——
5
- * 用官方 mongodb driver 连不上。
6
- *
7
- * 因此「四协议」目前实际是「一个半」:MySQL 可用;PG / Redis 以库形式
8
- * 提供、需自行装配;MongoDB 仅供实验。
9
- * 文档已按此更正(doc/why.md、doc/cli.md)。
10
- *
11
- * 修好前请勿把本文件加进 CI —— 它一崩就会像之前那样掐断 `&&` 链。
12
- * 修 MongoDB 需要单独排期,不属于性能/打包类改动。
13
- */
1
+ /* Test: MongoDB $group aggregation operators
2
+ * Directly tests the _dispatch method of createMongoServer */
14
3
 
15
4
  // Test: MongoDB $group aggregation operators
16
5
  // Directly tests the _dispatch method of createMongoServer
@@ -26,9 +15,9 @@ const ok = (name, cond, extra) => {
26
15
  const srv = createMongoServer({ port: 0, noAuth: true, dataDir: ':memory:' });
27
16
  srv.listen(() => {
28
17
  (async () => {
29
- // Get the engine and pre-populate
30
- const engine = await srv._getEngine();
31
- engine.createTable('sales', { schema: { _id: { type: 'integer' }, item: { type: 'string' }, qty: { type: 'integer' }, price: { type: 'integer' } } });
18
+ // Get the engine and pre-populate(库名必须与 dispatch 的 $db 一致)
19
+ const engine = await srv._getEngine('test');
20
+ await engine.createTable('sales', { _id: { type: 'integer' }, item: { type: 'string' }, qty: { type: 'integer' }, price: { type: 'integer' } });
32
21
  const table = engine._ensureTable('sales');
33
22
  table._rows = [
34
23
  { _id: 1, item: 'A', qty: 10, price: 5 },
@@ -0,0 +1,155 @@
1
+ /**
2
+ * 单端口多协议回归测试
3
+ *
4
+ * 校验 README 里「One Port」这条承诺:
5
+ * 1. 同一个 TCP 端口按首包嗅探到 MySQL / PostgreSQL / MongoDB / Redis
6
+ * 2. 静默连接(MySQL 客户端等服务器先握手)按 idleProtocol 兜底
7
+ * 3. `jsql serve` 默认启动多协议服务器,`--mysql-only` 保留单协议行为
8
+ *
9
+ * 全部使用原始 socket,不依赖 mysql2 / pg / mongodb / ioredis。
10
+ */
11
+ const net = require('net');
12
+ const path = require('path');
13
+ const { spawn } = require('child_process');
14
+ const { createMultiServer, sniffProtocol } = require('../lib/multiserver');
15
+ const { bsonDocument } = require('../lib/mongo_server');
16
+
17
+ let pass = 0, fail = 0;
18
+ function ok(name, cond, extra) {
19
+ if (cond) { pass++; console.log('[OK] ' + name); }
20
+ else { fail++; console.log('[FAIL] ' + name + (extra !== undefined ? ' → ' + JSON.stringify(extra) : '')); }
21
+ }
22
+
23
+ function connect(port) {
24
+ return new Promise((resolve, reject) => {
25
+ const s = net.connect(port, '127.0.0.1', () => resolve(s));
26
+ s.on('error', reject);
27
+ });
28
+ }
29
+
30
+ function readSome(socket, ms = 1500) {
31
+ return new Promise((resolve) => {
32
+ let buf = Buffer.alloc(0);
33
+ const done = () => { clearTimeout(t); socket.removeListener('data', onData); resolve(buf); };
34
+ const onData = (c) => { buf = Buffer.concat([buf, c]); };
35
+ const t = setTimeout(done, ms);
36
+ socket.on('data', onData);
37
+ });
38
+ }
39
+
40
+ function mongoHello() {
41
+ const payload = bsonDocument({ hello: 1, helloOk: true, $db: 'admin' });
42
+ const head = Buffer.alloc(16);
43
+ head.writeInt32LE(16 + 4 + 1 + payload.length, 0);
44
+ head.writeInt32LE(1, 4);
45
+ head.writeInt32LE(0, 8);
46
+ head.writeInt32LE(2013, 12);
47
+ return Buffer.concat([head, Buffer.alloc(4), Buffer.from([0]), payload]);
48
+ }
49
+
50
+ async function main() {
51
+ console.log('--- 首包嗅探 ---');
52
+ ok('PG StartupMessage 判为 pg', sniffProtocol(Buffer.from([0, 0, 0, 60, 0, 3, 0, 0])) === 'pg');
53
+ ok('MySQL 握手版本判为 mysql', sniffProtocol(Buffer.from([0x0a, 0x38, 0x2e, 0x30])) === 'mysql');
54
+ ok('RESP 数组判为 redis', sniffProtocol(Buffer.from('*1\r\n$4\r\nPING\r\n')) === 'redis');
55
+ {
56
+ const m = mongoHello();
57
+ ok('Mongo OP_MSG 判为 mongo', sniffProtocol(m) === 'mongo');
58
+ }
59
+
60
+ console.log('\n--- 同一端口四协议 ---');
61
+ const srv = createMultiServer({ port: 0, host: '127.0.0.1', noAuth: true });
62
+ await new Promise((r) => srv.listen(r));
63
+ const port = srv.address.port;
64
+ ok('监听成功', port > 0, port);
65
+
66
+ {
67
+ const s = await connect(port);
68
+ const buf = await readSome(s);
69
+ s.destroy();
70
+ ok('MySQL 静默连接收到握手包', buf.length > 5 && buf[4] === 0x0a, buf.slice(0, 8).toString('hex'));
71
+ ok('握手包含服务器版本', buf.includes(Buffer.from('jsql-neo')));
72
+ }
73
+
74
+ {
75
+ const s = await connect(port);
76
+ const ssl = Buffer.alloc(8);
77
+ ssl.writeInt32BE(8, 0);
78
+ ssl.writeInt32BE(80877103, 4); // SSLRequest
79
+ s.write(ssl);
80
+ const buf = await readSome(s, 800);
81
+ s.destroy();
82
+ ok('PostgreSQL SSLRequest 被拒绝并回 N', buf.toString('utf8') === 'N', buf.toString('utf8'));
83
+ }
84
+
85
+ {
86
+ const s = await connect(port);
87
+ s.write(Buffer.from('PING\r\n'));
88
+ const buf = await readSome(s, 800);
89
+ s.destroy();
90
+ ok('Redis PING 返回 +PONG', buf.toString('utf8').startsWith('+PONG'), buf.toString('utf8'));
91
+ }
92
+
93
+ {
94
+ const s = await connect(port);
95
+ s.write(mongoHello());
96
+ const buf = await readSome(s, 1500);
97
+ s.destroy();
98
+ ok('Mongo hello 返回 OP_MSG', buf.length >= 16 && buf.readInt32LE(12) === 2013);
99
+ ok('Mongo hello ok:1', buf.includes(Buffer.from('isWritablePrimary')));
100
+ }
101
+
102
+ console.log('\n--- 共享同一份 SQL 引擎 ---');
103
+ {
104
+ // 直接用 MT 服务器内部的引擎实例验证:Mongo 与 SQL 走同一个 Database
105
+ const eng = await srv.mysql._getEngine('shared');
106
+ const sameAsPg = (await srv.pg._getEngine('shared')) === eng;
107
+ const sameAsMongo = (await srv.mongo._getEngine('shared')) === eng;
108
+ ok('MySQL/PG/Mongo 取到同一个引擎', sameAsPg && sameAsMongo);
109
+ }
110
+
111
+ await new Promise((r) => srv.close(r));
112
+
113
+ console.log('\n--- idleProtocol 兜底 ---');
114
+ {
115
+ const redisIdle = createMultiServer({ port: 0, host: '127.0.0.1', idleProtocol: 'redis' });
116
+ await new Promise((r) => redisIdle.listen(r));
117
+ const s = await connect(redisIdle.address.port);
118
+ // 不主动发包:按 idleProtocol 走 Redis,随后发 PING 必须能回
119
+ await new Promise((r) => setTimeout(r, 350));
120
+ s.write(Buffer.from('PING\r\n'));
121
+ const buf = await readSome(s, 800);
122
+ s.destroy();
123
+ ok('idleProtocol=redis 时空闲连接仍可用', buf.toString('utf8').startsWith('+PONG'), buf.toString('utf8'));
124
+ await new Promise((r) => redisIdle.close(r));
125
+ }
126
+
127
+ console.log('\n--- CLI serve 入口 ---');
128
+ {
129
+ const bin = path.join(__dirname, '..', 'bin', 'jsql');
130
+ const child = spawn(process.execPath, [bin, 'serve', '-p', '0'], {
131
+ env: { ...process.env, JSQL_NO_STAR_NAG: '1' },
132
+ stdio: ['ignore', 'pipe', 'pipe'],
133
+ });
134
+ let out = '';
135
+ child.stdout.on('data', (c) => { out += c; });
136
+ child.stderr.on('data', (c) => { out += c; });
137
+ const started = await new Promise((resolve) => {
138
+ const t = setTimeout(() => resolve(false), 5000);
139
+ const check = () => {
140
+ if (/multi-protocol server on/.test(out) || /serve failed/.test(out)) {
141
+ clearTimeout(t); resolve(/multi-protocol server on/.test(out));
142
+ } else setTimeout(check, 50);
143
+ };
144
+ check();
145
+ });
146
+ ok('jsql serve 默认启动多协议服务器', started, out.trim());
147
+ child.kill('SIGTERM');
148
+ await new Promise((r) => { child.on('exit', r); setTimeout(r, 1000); });
149
+ }
150
+
151
+ console.log(`\nRESULT: ${pass} passed, ${fail} failed`);
152
+ if (fail > 0) process.exit(1);
153
+ }
154
+
155
+ main().catch((e) => { console.error(e); process.exit(1); });