jsql-neo 6.0.0 → 6.0.2
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 +134 -26
- package/lib/pg_server.js +7 -1
- package/lib/redis_server.js +58 -28
- package/lib/sql.js +749 -57
- package/lib/wasm_client.js +58 -8
- package/package.json +4 -3
- package/test/readme-audit.test.js +281 -0
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. The browser. **And it fits in one npm package.**
|
|
5
5
|
|
|
6
|
-
> **v6.0.
|
|
6
|
+
> **v6.0.1** — official release build · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo)
|
|
7
7
|
|
|
8
8
|

|
|
9
9
|

|
|
@@ -124,6 +124,7 @@ For the complete Chinese edition of every topic below, jump to the [中文版](#
|
|
|
124
124
|
- [Migration Tools](#migration-tools)
|
|
125
125
|
- [Storage & Performance](#storage--performance)
|
|
126
126
|
- [Security](#security)
|
|
127
|
+
- [Plugin System](#plugin-system)
|
|
127
128
|
- [Errors](#errors)
|
|
128
129
|
- [TypeScript](#typescript)
|
|
129
130
|
- [Architecture](#architecture)
|
|
@@ -232,7 +233,7 @@ npm install && npm run build # option 3: from source
|
|
|
232
233
|
Verify:
|
|
233
234
|
|
|
234
235
|
```bash
|
|
235
|
-
node -e "console.log(require('jsql-neo/package.json').version)" # 6.0.
|
|
236
|
+
node -e "console.log(require('jsql-neo/package.json').version)" # 6.0.1
|
|
236
237
|
```
|
|
237
238
|
|
|
238
239
|
### 30-second demo
|
|
@@ -410,7 +411,7 @@ Auth failure returns the standard `Access denied for user 'x'@'...' (using passw
|
|
|
410
411
|
|
|
411
412
|
The server answers the system probes ORMs and GUI tools run at connect time:
|
|
412
413
|
|
|
413
|
-
- `SELECT VERSION()` → `8.0.
|
|
414
|
+
- `SELECT VERSION()` → `8.0.1-jsql-neo`
|
|
414
415
|
- System variables: `@@version`, `@@version_comment`, …
|
|
415
416
|
- `SHOW DATABASES / SHOW TABLES / SHOW COLUMNS / SHOW CREATE TABLE`
|
|
416
417
|
- `information_schema.tables / columns / statistics`
|
|
@@ -1064,7 +1065,7 @@ SELECT AGE('2026-08-12', '2020-01-01'); -- '6 years 7 mons 11 days'
|
|
|
1064
1065
|
**Conditional / null** — `IF(cond, a, b)`, `IIF`, `IFNULL(a, b)`, `COALESCE(...)`, `NULLIF`,
|
|
1065
1066
|
`ISNULL`, `CASE WHEN ... THEN ... ELSE ... END` (search/simple form, works with aggregates).
|
|
1066
1067
|
|
|
1067
|
-
**System & misc** — `VERSION()` → `8.0.
|
|
1068
|
+
**System & misc** — `VERSION()` → `8.0.1-jsql-neo`, `DATABASE()`, `USER()`/`CURRENT_USER`,
|
|
1068
1069
|
`LAST_INSERT_ID()`, `ROW_COUNT()`, `CAST`/`CONVERT`, `JSON_EXTRACT(doc, '$.path')`,
|
|
1069
1070
|
`JSON_OBJECT`/`JSON_ARRAY`, `JSON_UNQUOTE`, `JSON_CONTAINS`, `JSON_SET/INSERT/REPLACE/REMOVE`,
|
|
1070
1071
|
`UUID()`, `TYPEOF(x)`, `PRINT(x)`.
|
|
@@ -1091,15 +1092,21 @@ ORDER BY avg_sal DESC;
|
|
|
1091
1092
|
|
|
1092
1093
|
### Operators
|
|
1093
1094
|
|
|
1094
|
-
**Arithmetic** — `+ - * /
|
|
1095
|
+
**Arithmetic** — `+ - * / %`,以及 `MOD` / `DIV` / `POWER()`。
|
|
1095
1096
|
|
|
1096
1097
|
**Comparison** — `= <> != < <= > >=`, `IS NULL`, `IS TRUE/FALSE` (three-valued),
|
|
1097
1098
|
`BETWEEN a AND b`, `IN (...)`, `LIKE`/`NOT LIKE`, `ILIKE` (case-insensitive),
|
|
1098
|
-
`RLIKE`/`REGEXP`,
|
|
1099
|
+
`RLIKE`/`REGEXP`, `EXISTS`, `= ANY` / `> ALL` / `SOME`.
|
|
1099
1100
|
|
|
1100
|
-
**Logic** — `AND OR NOT
|
|
1101
|
+
**Logic** — `AND OR NOT` (short-circuit).
|
|
1101
1102
|
|
|
1102
|
-
**Bitwise** — `& | ^ ~ <<
|
|
1103
|
+
**Bitwise** — `& | ^ ~ << >>`(`^` 是按位异或,`~` 是按位取反)。
|
|
1104
|
+
|
|
1105
|
+
> **运算符方言说明。** `^` 与 `~` 在 MySQL(位运算)和 PostgreSQL(`^` 为幂、
|
|
1106
|
+
> `~` 为正则匹配)中含义冲突,本实现统一采用 **MySQL 语义**:`^` = 按位异或、
|
|
1107
|
+
> `~` = 按位取反。需要幂运算请用 `POWER(x, n)`,需要正则请用 `REGEXP` / `~*`。
|
|
1108
|
+
> 因此 PG 风格的 `^`(幂)、`~ ~* !~ !~*`(正则)与 JSON 操作符
|
|
1109
|
+
> (`-> ->> #> #>> @> <@`)**暂不支持**。
|
|
1103
1110
|
|
|
1104
1111
|
**JSON (PG style)** — `->` (JSON result), `->>` (text), `#>`, `#>>` (paths), `@>` `<@`
|
|
1105
1112
|
(containment), `?` `?|` `?&` (key existence).
|
|
@@ -1457,7 +1464,7 @@ jsql mod --engine wasm # switch engine (restart required)
|
|
|
1457
1464
|
|
|
1458
1465
|
```bash
|
|
1459
1466
|
$ jsql version
|
|
1460
|
-
jsql-neo v6.0.
|
|
1467
|
+
jsql-neo v6.0.1
|
|
1461
1468
|
engine: native (napi) | wasm | js
|
|
1462
1469
|
node: v22.0.0 platform: linux x64
|
|
1463
1470
|
```
|
|
@@ -1478,7 +1485,7 @@ jsql tui --memory -q # memory mode, quiet
|
|
|
1478
1485
|
jsql tui --prompt 'db> ' --no-color
|
|
1479
1486
|
```
|
|
1480
1487
|
|
|
1481
|
-
The status bar shows: `db=<name> dialect=<d> mode=<tui|batch> ver=6.0.
|
|
1488
|
+
The status bar shows: `db=<name> dialect=<d> mode=<tui|batch> ver=6.0.1`.
|
|
1482
1489
|
|
|
1483
1490
|
### Keyboard shortcuts
|
|
1484
1491
|
|
|
@@ -1753,6 +1760,105 @@ normalization on data dirs and import/export paths; Web UI db-name whitelist (no
|
|
|
1753
1760
|
}
|
|
1754
1761
|
```
|
|
1755
1762
|
|
|
1763
|
+
## Plugin System
|
|
1764
|
+
|
|
1765
|
+
JSQL-NEO ships with a unified plugin system that works across all three engine implementations:
|
|
1766
|
+
**Native** (`NativeJSQL`), **WASM** (`JSQL`), and **Pure JS** (`Database`). Full documentation
|
|
1767
|
+
is in [PLUGINS.md](./PLUGINS.md).
|
|
1768
|
+
|
|
1769
|
+
### Quick start
|
|
1770
|
+
|
|
1771
|
+
```js
|
|
1772
|
+
const { Database, Plugin } = require('jsql-neo');
|
|
1773
|
+
const db = new Database();
|
|
1774
|
+
|
|
1775
|
+
db.use({
|
|
1776
|
+
name: 'my-plugin',
|
|
1777
|
+
install(db, ctx) {
|
|
1778
|
+
db.myFlag = true;
|
|
1779
|
+
},
|
|
1780
|
+
hooks: {
|
|
1781
|
+
beforeInsert(table, data) { /* modify data */ return data; },
|
|
1782
|
+
afterInsert(table, data, ids) {},
|
|
1783
|
+
beforeUpdate(table, id, data) { return true; }, // false = abort
|
|
1784
|
+
afterUpdate(table, id, data, result) {},
|
|
1785
|
+
beforeDelete(table, id) { return true; },
|
|
1786
|
+
afterDelete(table, id, result) {},
|
|
1787
|
+
beforeFind(table, query) { return true; },
|
|
1788
|
+
afterFind(table, query, result) {},
|
|
1789
|
+
},
|
|
1790
|
+
});
|
|
1791
|
+
```
|
|
1792
|
+
|
|
1793
|
+
### Hooks reference (18 unified hooks)
|
|
1794
|
+
|
|
1795
|
+
| Hook | Parameters | Return | Description |
|
|
1796
|
+
|---|---|---|---|
|
|
1797
|
+
| `beforeInsert` | `(table, data[])` | modified data / `false` to abort | pre-insert |
|
|
1798
|
+
| `afterInsert` | `(table, data[], ids)` | — | post-insert |
|
|
1799
|
+
| `beforeUpdate` | `(table, id, data)` | `true`/`false` | pre-update |
|
|
1800
|
+
| `afterUpdate` | `(table, id, data, result)` | — | post-update |
|
|
1801
|
+
| `beforeDelete` | `(table, id)` | `true`/`false` | pre-delete |
|
|
1802
|
+
| `afterDelete` | `(table, id, result)` | — | post-delete |
|
|
1803
|
+
| `beforeFind` | `(table, query)` | `true`/`false` | pre-query |
|
|
1804
|
+
| `afterFind` | `(table, query, result)` | — | post-query |
|
|
1805
|
+
| `beforeCreateTable` | `(name, schema)` | `true`/`false` | pre-DDL |
|
|
1806
|
+
| `afterCreateTable` | `(name, schema)` | — | post-DDL |
|
|
1807
|
+
| `beforeDropTable` | `(name)` | `true`/`false` | pre-drop |
|
|
1808
|
+
| `afterDropTable` | `(name)` | — | post-drop |
|
|
1809
|
+
| `beforeFlush` | `()` | `true`/`false` | pre-flush |
|
|
1810
|
+
| `afterFlush` | `()` | — | post-flush |
|
|
1811
|
+
| `beforeCount` | `(table)` | `true`/`false` | pre-count |
|
|
1812
|
+
| `afterCount` | `(table, count)` | — | post-count |
|
|
1813
|
+
| `onStart` | `()` | — | engine start |
|
|
1814
|
+
| `onStop` | `()` | — | engine stop |
|
|
1815
|
+
|
|
1816
|
+
### Events
|
|
1817
|
+
|
|
1818
|
+
| Event | Payload |
|
|
1819
|
+
|---|---|
|
|
1820
|
+
| `insert` | `{ table, count, ids }` |
|
|
1821
|
+
| `update` | `{ table, id, data }` |
|
|
1822
|
+
| `delete` | `{ table, ids }` |
|
|
1823
|
+
| `createTable` | `{ name, schema }` |
|
|
1824
|
+
| `dropTable` | `{ name }` |
|
|
1825
|
+
| `start` / `stop` | `{}` |
|
|
1826
|
+
|
|
1827
|
+
### Plugin helper class
|
|
1828
|
+
|
|
1829
|
+
```js
|
|
1830
|
+
const timestampPlugin = new Plugin('timestamp')
|
|
1831
|
+
.on('beforeInsert', (table, data) => {
|
|
1832
|
+
data.forEach(r => r.created_at = new Date().toISOString());
|
|
1833
|
+
return data;
|
|
1834
|
+
})
|
|
1835
|
+
.onEvent((ev, payload) => console.log(ev))
|
|
1836
|
+
.install((db, ctx) => {})
|
|
1837
|
+
.build();
|
|
1838
|
+
|
|
1839
|
+
db.use(timestampPlugin);
|
|
1840
|
+
```
|
|
1841
|
+
|
|
1842
|
+
### Module system (CLI)
|
|
1843
|
+
|
|
1844
|
+
```bash
|
|
1845
|
+
jsql mod add --address /path/mod.js # register module
|
|
1846
|
+
jsql mod enable <name> # enable (auto-enables dependencies)
|
|
1847
|
+
jsql mod disable <name> # disable (cascades)
|
|
1848
|
+
jsql mod list | ls [--json] # list
|
|
1849
|
+
```
|
|
1850
|
+
|
|
1851
|
+
Modules persist to `~/.config/jsql/mod.config`. Circular dependencies are rejected;
|
|
1852
|
+
disabling cascades to dependents.
|
|
1853
|
+
|
|
1854
|
+
> **插件没有沙箱,请只装可信来源的模块。** 两条装载路径都不做隔离:
|
|
1855
|
+
> - `db.use(plugin)` —— 直接把插件对象挂进核心,与核心同权;
|
|
1856
|
+
> - `jsql mod add <dir>` —— 模块文件走普通 `require()`,同样与核心同权,
|
|
1857
|
+
> 可访问 `fs` / `child_process` 等一切 Node API。
|
|
1858
|
+
>
|
|
1859
|
+
> 早期文档曾写"模块文件在 VM 沙箱中执行",实际代码中不存在 vm 隔离,此处已更正。
|
|
1860
|
+
> 需要隔离时请在进程级解决(容器 / 子进程 / 单独的用户与权限)。
|
|
1861
|
+
|
|
1756
1862
|
---
|
|
1757
1863
|
|
|
1758
1864
|
## TypeScript
|
|
@@ -2378,7 +2484,7 @@ createMysqlServer({
|
|
|
2378
2484
|
|
|
2379
2485
|
支持常见系统查询,让 ORM 与 GUI 工具的初始化探测流程正常工作:
|
|
2380
2486
|
|
|
2381
|
-
- `SELECT VERSION()` → `8.0.
|
|
2487
|
+
- `SELECT VERSION()` → `8.0.1-jsql-neo`
|
|
2382
2488
|
- `SELECT @@version` / `@@version_comment` 等系统变量
|
|
2383
2489
|
- `SHOW DATABASES` / `SHOW TABLES` / `SHOW COLUMNS FROM t` / `SHOW CREATE TABLE t`
|
|
2384
2490
|
- `information_schema.tables` / `information_schema.columns` / `information_schema.statistics`
|
|
@@ -2410,7 +2516,7 @@ SELECT TABLE_NAME, TABLE_ROWS FROM information_schema.tables WHERE TABLE_SCHEMA
|
|
|
2410
2516
|
|
|
2411
2517
|
| 函数 | 行为 |
|
|
2412
2518
|
|---|---|
|
|
2413
|
-
| `VERSION()` | `8.0.
|
|
2519
|
+
| `VERSION()` | `8.0.1-jsql-neo` |
|
|
2414
2520
|
| `NOW()` / `CURRENT_TIMESTAMP` | `YYYY-MM-DD HH:MM:SS`(UTC) |
|
|
2415
2521
|
| `CONCAT(a, b, ...)` | 字符串拼接 |
|
|
2416
2522
|
| `GROUP_CONCAT(x)` | 聚合拼接(逗号分隔) |
|
|
@@ -3104,26 +3210,28 @@ SELECT * FROM orders LIMIT 40 OFFSET 20; -- 标准偏移语法
|
|
|
3104
3210
|
| `BEGIN` / `START TRANSACTION` | 开启事务 |
|
|
3105
3211
|
| `COMMIT` | 提交 |
|
|
3106
3212
|
| `ROLLBACK` | 回滚 |
|
|
3107
|
-
| `SAVEPOINT sp` / `
|
|
3213
|
+
| `SAVEPOINT sp` / `RELEASE SAVEPOINT sp` | 保存点(**仅登记**,无快照能力) |
|
|
3214
|
+
| `ROLLBACK TO SAVEPOINT sp` | **不支持**——会抛错,不会静默"假成功" |
|
|
3108
3215
|
| 自动事务 | 无显式事务时单语句原子 |
|
|
3109
3216
|
|
|
3110
3217
|
事务保证:
|
|
3111
3218
|
|
|
3112
3219
|
- 读已提交(read committed)隔离级别的等价语义
|
|
3113
3220
|
- 事务内修改对事务外不可见(提交前),提交后原子可见
|
|
3114
|
-
-
|
|
3221
|
+
- 回滚撤销本事务全部修改
|
|
3115
3222
|
- 支持跨表事务(引擎级提交日志)
|
|
3116
3223
|
|
|
3117
3224
|
```sql
|
|
3118
3225
|
BEGIN;
|
|
3119
3226
|
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
|
|
3120
3227
|
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
|
|
3121
|
-
SAVEPOINT sp1;
|
|
3122
|
-
UPDATE accounts SET balance = balance - 100 WHERE id = 3;
|
|
3123
|
-
ROLLBACK TO SAVEPOINT sp1; -- 撤销 id=3 的修改
|
|
3124
3228
|
COMMIT;
|
|
3125
3229
|
```
|
|
3126
3230
|
|
|
3231
|
+
> **`ROLLBACK TO SAVEPOINT` 目前不可用。** 引擎没有保存点/快照能力,无法只回退到某个
|
|
3232
|
+
> 保存点。这里刻意选择"抛错"而不是"静默成功"——假装回滚成功会让调用方误以为数据已还原。
|
|
3233
|
+
> 需要部分回滚时请用 `ROLLBACK` 放弃整个事务。
|
|
3234
|
+
|
|
3127
3235
|
#### 其他语句
|
|
3128
3236
|
|
|
3129
3237
|
| 语句 | 说明 |
|
|
@@ -3245,7 +3353,7 @@ INTERVAL 单位:`DAY` `HOUR` `MINUTE` `SECOND` `WEEK` `MONTH` `YEAR` 及其组
|
|
|
3245
3353
|
|
|
3246
3354
|
| 函数 | 说明 | 示例 |
|
|
3247
3355
|
|---|---|---|
|
|
3248
|
-
| `VERSION()` | 服务器版本 | `8.0.
|
|
3356
|
+
| `VERSION()` | 服务器版本 | `8.0.1-jsql-neo` |
|
|
3249
3357
|
| `DATABASE()` | 当前库名 | `DATABASE()` |
|
|
3250
3358
|
| `USER()` / `CURRENT_USER` | 当前用户 | `USER()` → `root` |
|
|
3251
3359
|
| `LAST_INSERT_ID()` | 最近自增 ID | 配合 AUTO_INCREMENT 使用 |
|
|
@@ -4042,7 +4150,7 @@ Data dir: /root/.jsql-neo/data
|
|
|
4042
4150
|
|
|
4043
4151
|
```bash
|
|
4044
4152
|
$ jsql version
|
|
4045
|
-
jsql-neo v6.0.
|
|
4153
|
+
jsql-neo v6.0.1
|
|
4046
4154
|
engine: native (napi) | wasm | js
|
|
4047
4155
|
node: v22.0.0
|
|
4048
4156
|
platform: linux x64
|
|
@@ -4711,7 +4819,7 @@ jsql-neo/
|
|
|
4711
4819
|
├── index.js # ★ 公共 API 出口
|
|
4712
4820
|
├── index.d.ts # TypeScript 声明
|
|
4713
4821
|
├── test/ # 测试套件
|
|
4714
|
-
├── docs/
|
|
4822
|
+
├── PLUGINS.md # 插件与模块文档(与 README 同级,无 docs/ 目录)
|
|
4715
4823
|
├── src/rust/ # Rust 引擎源码(native + wasm)
|
|
4716
4824
|
├── data/ # 默认数据目录(运行时创建)
|
|
4717
4825
|
└── package.json
|
|
@@ -4902,7 +5010,7 @@ npm run typecheck
|
|
|
4902
5010
|
- `src/rust/`:Rust 引擎(native + wasm 同源)
|
|
4903
5011
|
- `lib/`:Node 侧全部逻辑(协议/解析/TUI/兼容层)
|
|
4904
5012
|
- `test/`:单元 + 集成测试
|
|
4905
|
-
- `docs
|
|
5013
|
+
- 扩展文档位于仓库根目录:`PLUGINS.md`(插件与模块)。本仓库**没有** `docs/` 目录。
|
|
4906
5014
|
|
|
4907
5015
|
---
|
|
4908
5016
|
|
|
@@ -5119,7 +5227,7 @@ Apache License
|
|
|
5119
5227
|
|
|
5120
5228
|
*JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
|
|
5121
5229
|
|
|
5122
|
-
*文档版本:v6.0.
|
|
5230
|
+
*文档版本:v6.0.1 · 最后更新:2026-09-27*
|
|
5123
5231
|
|
|
5124
5232
|
---
|
|
5125
5233
|
|
|
@@ -6594,7 +6702,7 @@ FROM orders;
|
|
|
6594
6702
|
#### 会话信息
|
|
6595
6703
|
|
|
6596
6704
|
```sql
|
|
6597
|
-
SELECT VERSION(); -- '8.0.
|
|
6705
|
+
SELECT VERSION(); -- '8.0.1-jsql-neo'
|
|
6598
6706
|
SELECT DATABASE(); -- 'app'
|
|
6599
6707
|
SELECT USER(); -- 'root@localhost'
|
|
6600
6708
|
SELECT CURRENT_USER; -- 'root@localhost'
|
|
@@ -6889,7 +6997,7 @@ Usage: jsql version
|
|
|
6889
6997
|
|
|
6890
6998
|
输出版本与环境信息:
|
|
6891
6999
|
|
|
6892
|
-
jsql-neo v6.0.
|
|
7000
|
+
jsql-neo v6.0.1
|
|
6893
7001
|
engine: native (napi) | wasm | js
|
|
6894
7002
|
node: v22.0.0
|
|
6895
7003
|
platform: linux x64
|
|
@@ -7545,7 +7653,7 @@ CI(GitHub Actions)矩阵:`node 20/22` × `linux/macos/windows` × `native/
|
|
|
7545
7653
|
|
|
7546
7654
|
*JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
|
|
7547
7655
|
|
|
7548
|
-
*文档版本:
|
|
7656
|
+
*文档版本:v6.0.1 · 共 19 个附录 · 最后更新:2026-09-27*
|
|
7549
7657
|
|
|
7550
7658
|
---
|
|
7551
7659
|
|
|
@@ -8152,4 +8260,4 @@ npm test
|
|
|
8152
8260
|
|
|
8153
8261
|
*JSQL-NEO — One engine to rule them all. MySQL. PostgreSQL. MongoDB. Redis. SQL. TypeScript. The browser.*
|
|
8154
8262
|
|
|
8155
|
-
*文档版本:
|
|
8263
|
+
*文档版本:v6.0.1 · 附录 A–Z · 全文 6000+ 行 · 最后更新:2026-09-27*
|
package/lib/pg_server.js
CHANGED
|
@@ -861,4 +861,10 @@ class PgServer {
|
|
|
861
861
|
}
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
-
|
|
864
|
+
// README 的 createXxxServer 工厂系列里,PG 这个之前漏了实现:
|
|
865
|
+
// index.js 一直从本模块解构 createPgServer,实际拿到 undefined。
|
|
866
|
+
function createPgServer(options) {
|
|
867
|
+
return new PgServer(options || {});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
module.exports = { PgServer, createPgServer, PgConnection, buildRowDescription, buildDataRow, buildCommandComplete, buildErrorResponse, buildReadyForQuery, buildParameterStatus, buildBackendKeyData, buildParseComplete, buildBindComplete, buildCloseComplete, buildNoData, buildParameterDesc, TYPE_OIDS, pgTypeOid, pgFormatValue };
|
package/lib/redis_server.js
CHANGED
|
@@ -470,52 +470,81 @@ class RedisServer {
|
|
|
470
470
|
|
|
471
471
|
/** 供多协议嗅探复用:把 socket 交给 Redis 处理,可携带已缓冲的首包 */
|
|
472
472
|
_handleSocket(socket, existing = '') {
|
|
473
|
-
|
|
473
|
+
// 刻意不 setEncoding('utf8'):RESP 的 $N 是**字节**长度,
|
|
474
|
+
// 转成字符串再按字符切片会把多字节 UTF-8(中文等)截断。
|
|
475
|
+
let buf = Buffer.isBuffer(existing) ? existing : Buffer.from(String(existing || ''), 'utf8');
|
|
474
476
|
this._authedSockets.set(socket, false);
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
buf += chunk;
|
|
477
|
+
|
|
478
|
+
const drain = () => {
|
|
478
479
|
for (;;) {
|
|
479
|
-
|
|
480
|
+
let msg;
|
|
481
|
+
try {
|
|
482
|
+
msg = this._parse(buf);
|
|
483
|
+
} catch (e) {
|
|
484
|
+
// 协议错误:回复错误并丢弃缓冲,避免一直卡在同一段坏数据上
|
|
485
|
+
this._send(socket, (e instanceof Error) ? e : new Error('ERR Protocol error'));
|
|
486
|
+
buf = Buffer.alloc(0);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
480
489
|
if (!msg) break;
|
|
481
490
|
buf = buf.slice(msg.consumed);
|
|
491
|
+
if (msg.ignore) continue;
|
|
482
492
|
this._handle(socket, msg.cmd, msg.args);
|
|
483
493
|
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
socket.on('data', (chunk) => {
|
|
497
|
+
buf = Buffer.concat([buf, Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), 'utf8')]);
|
|
498
|
+
drain();
|
|
484
499
|
});
|
|
485
500
|
socket.on('error', () => {});
|
|
486
|
-
|
|
487
|
-
const msg = this._parse(buf);
|
|
488
|
-
if (!msg) break;
|
|
489
|
-
buf = buf.slice(msg.consumed);
|
|
490
|
-
this._handle(socket, msg.cmd, msg.args);
|
|
491
|
-
}
|
|
501
|
+
drain();
|
|
492
502
|
}
|
|
493
503
|
|
|
504
|
+
/**
|
|
505
|
+
* 解析一条 RESP2 请求,返回 { consumed, cmd, args } 或 null(数据不完整)。
|
|
506
|
+
* buf 是 Buffer —— 所有长度计算都按**字节**进行。
|
|
507
|
+
*/
|
|
494
508
|
_parse(buf) {
|
|
495
|
-
if (buf
|
|
496
|
-
|
|
509
|
+
if (!buf || buf.length === 0) return null;
|
|
510
|
+
const crlf = (from) => buf.indexOf('\r\n', from, 'utf8');
|
|
511
|
+
|
|
512
|
+
// 非 '*' 开头:inline 命令(telnet / nc 手工输入)
|
|
513
|
+
if (buf[0] !== 0x2a) {
|
|
514
|
+
const i = crlf(0);
|
|
497
515
|
if (i < 0) return null;
|
|
498
|
-
const
|
|
499
|
-
if (!
|
|
500
|
-
|
|
516
|
+
const line = buf.slice(0, i).toString('utf8').trim();
|
|
517
|
+
if (!line) return { consumed: i + 2, ignore: true };
|
|
518
|
+
const parts = line.split(/\s+/);
|
|
519
|
+
// 之前这里返回的是数组(cmd.map(...)),导致 switch(cmd) 永远匹配不上,
|
|
520
|
+
// inline 输入一律被当成 unknown command。必须是字符串。
|
|
521
|
+
const cmd = parts[0].toUpperCase();
|
|
522
|
+
return { consumed: i + 2, cmd, args: parts.slice(1) };
|
|
501
523
|
}
|
|
502
|
-
|
|
524
|
+
|
|
525
|
+
const i = crlf(0);
|
|
503
526
|
if (i < 0) return null;
|
|
504
|
-
const n = parseInt(buf.slice(1, i), 10);
|
|
505
|
-
if (isNaN(n)) throw new Error('ERR Protocol error');
|
|
527
|
+
const n = parseInt(buf.slice(1, i).toString('utf8'), 10);
|
|
528
|
+
if (isNaN(n)) throw new Error('ERR Protocol error: invalid multibulk length');
|
|
529
|
+
// *0(空命令)与 *-1(null 数组)此前会走到 parts[0].toUpperCase() 崩溃
|
|
530
|
+
if (n <= 0) return { consumed: i + 2, ignore: true };
|
|
531
|
+
|
|
506
532
|
let off = i + 2;
|
|
507
533
|
const parts = [];
|
|
508
534
|
for (let k = 0; k < n; k++) {
|
|
509
|
-
if (buf
|
|
510
|
-
|
|
535
|
+
if (buf.length <= off) return null;
|
|
536
|
+
if (buf[off] !== 0x24) throw new Error('ERR Protocol error: expected bulk string'); // '$'
|
|
537
|
+
const j = crlf(off);
|
|
511
538
|
if (j < 0) return null;
|
|
512
|
-
const len = parseInt(buf.slice(off + 1, j), 10);
|
|
513
|
-
if (isNaN(len)) throw new Error('ERR Protocol error');
|
|
514
|
-
|
|
515
|
-
parts.push(
|
|
516
|
-
|
|
539
|
+
const len = parseInt(buf.slice(off + 1, j).toString('utf8'), 10);
|
|
540
|
+
if (isNaN(len)) throw new Error('ERR Protocol error: invalid bulk length');
|
|
541
|
+
const start = j + 2;
|
|
542
|
+
if (len === -1) { parts.push(null); off = start; continue; } // $-1 null bulk
|
|
543
|
+
if (buf.length < start + len + 2) return null; // 按字节判断
|
|
544
|
+
parts.push(buf.slice(start, start + len).toString('utf8'));
|
|
545
|
+
off = start + len + 2;
|
|
517
546
|
}
|
|
518
|
-
const cmd = parts[0].toUpperCase();
|
|
547
|
+
const cmd = String(parts[0] || '').toUpperCase();
|
|
519
548
|
return { consumed: off, cmd, args: parts.slice(1) };
|
|
520
549
|
}
|
|
521
550
|
|
|
@@ -550,7 +579,8 @@ class RedisServer {
|
|
|
550
579
|
} else if (typeof val === 'number') {
|
|
551
580
|
out = ':' + val + '\r\n';
|
|
552
581
|
} else if (Array.isArray(val)) {
|
|
553
|
-
|
|
582
|
+
// 长度用字节数,不能用 String.length —— 否则中文等多字节值会让客户端解帧错位
|
|
583
|
+
out = '*' + val.length + '\r\n' + val.map(v => '$' + Buffer.byteLength(String(v), 'utf8') + '\r\n' + v + '\r\n').join('');
|
|
554
584
|
} else if (typeof val === 'string') {
|
|
555
585
|
if (val === 'OK' || val === 'PONG') {
|
|
556
586
|
out = '+' + val + '\r\n';
|