jsql-neo 5.4.2 → 5.5.0

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 (72) hide show
  1. package/LICENSE +202 -202
  2. package/README.md +8097 -7971
  3. package/bin/jsql +320 -320
  4. package/bin/jsql-server +90 -90
  5. package/bin/jsql-server-control.js +177 -177
  6. package/index.d.ts +385 -357
  7. package/index.js +124 -118
  8. package/lib/ast.js +214 -0
  9. package/lib/btree.js +413 -413
  10. package/lib/cache.js +328 -328
  11. package/lib/client.js +125 -125
  12. package/lib/database.js +1744 -1744
  13. package/lib/date-types.js +182 -182
  14. package/lib/errors.js +89 -89
  15. package/lib/jsql_format.js +497 -497
  16. package/lib/migrate.js +254 -254
  17. package/lib/mod.js +292 -292
  18. package/lib/mongo_server.js +607 -607
  19. package/lib/multiserver.js +140 -140
  20. package/lib/mysql_compat.js +413 -413
  21. package/lib/mysql_server.js +1237 -1237
  22. package/lib/native_client.js +732 -732
  23. package/lib/nedb_compat.js +506 -506
  24. package/lib/pg_server.js +864 -864
  25. package/lib/plugin.js +34 -34
  26. package/lib/query.js +815 -815
  27. package/lib/redis_server.js +571 -571
  28. package/lib/sql.js +3460 -2925
  29. package/lib/sqlite_compat.js +323 -323
  30. package/lib/sqlite_worker.js +264 -264
  31. package/lib/table.js +1304 -1304
  32. package/lib/tui.js +502 -502
  33. package/lib/wasm.js +1 -1
  34. package/lib/wasm_client.js +718 -718
  35. package/lib/web_ui.js +257 -257
  36. package/native/jsql-neo-native.node +0 -0
  37. package/nativesrc/jsql-neo-core/Cargo.lock +455 -455
  38. package/nativesrc/jsql-neo-core/Cargo.toml +24 -24
  39. package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +455 -455
  40. package/nativesrc/jsql-neo-core/src/engine/memory.rs +168 -168
  41. package/nativesrc/jsql-neo-core/src/engine/mod.rs +62 -62
  42. package/nativesrc/jsql-neo-core/src/engine/table.rs +700 -700
  43. package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
  44. package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
  45. package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
  46. package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -85
  47. package/nativesrc/jsql-neo-core/src/types.rs +93 -93
  48. package/nativesrc/jsql-neo-native/Cargo.lock +606 -606
  49. package/nativesrc/jsql-neo-native/Cargo.toml +16 -16
  50. package/nativesrc/jsql-neo-native/build.rs +3 -3
  51. package/nativesrc/jsql-neo-native/package.json +7 -7
  52. package/nativesrc/jsql-neo-native/src/lib.rs +269 -269
  53. package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -465
  54. package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -15
  55. package/nativesrc/jsql-neo-wasm/src/lib.rs +447 -447
  56. package/package.json +87 -86
  57. package/postinstall.js +158 -158
  58. package/test/btree.test.js +128 -128
  59. package/test/coverage.js +187 -187
  60. package/test/join.test.js +110 -110
  61. package/test/native.test.js +158 -158
  62. package/test/regress-5.1.0.js +271 -271
  63. package/test/smoke.js +58 -58
  64. package/test/sql-parser.test.js +273 -0
  65. package/test/wasm.test.js +110 -110
  66. package/wasm/browser.d.ts +89 -89
  67. package/wasm/browser.mjs +569 -569
  68. package/wasm/browser_bg.mjs +597 -597
  69. package/wasm/jsql_neo_wasm.d.ts +38 -38
  70. package/wasm/jsql_neo_wasm.js +520 -520
  71. package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
  72. package/wasm/package.json +11 -11
@@ -1,571 +1,571 @@
1
- /*
2
- * JSQL-NEO Redis-compatible server (RESP2, zero dependencies).
3
- *
4
- * const { createRedisServer } = require('jsql-neo');
5
- * const srv = createRedisServer({ port: 6379, dataDir: './data' });
6
- * srv.listen();
7
- *
8
- * Supported commands:
9
- * PING, ECHO, SET, GET, SETNX, DEL, EXISTS, KEYS, TYPE, EXPIRE, TTL, PERSIST,
10
- * INCR, DECR, INCRBY, DECRBY, APPEND, STRLEN,
11
- * HSET, HGET, HGETALL, HDEL, HEXISTS, HLEN, HKEYS, HVALS,
12
- * LPUSH, RPUSH, LPOP, RPOP, LLEN, LRANGE, LINDEX, LREM,
13
- * SADD, SREM, SMEMBERS, SISMEMBER, SCARD,
14
- * DBSIZE, FLUSHALL, FLUSHDB, SELECT, INFO, AUTH, QUIT
15
- *
16
- * Persistence: keys are held in memory and snapshotted to data.rdb.json
17
- * (debounced 500ms) plus a final snapshot on shutdown.
18
- */
19
- const net = require('net');
20
- const fs = require('fs');
21
- const path = require('path');
22
-
23
- const TYPE_SIGNATURES = {
24
- PING: 0, ECHO: 1, SET: 2, SETNX: 2, GET: 1, DEL: -1, EXISTS: -1, KEYS: 1,
25
- TYPE: 1, EXPIRE: 2, TTL: 1, PERSIST: 1,
26
- INCR: 1, DECR: 1, INCRBY: 2, DECRBY: 2, APPEND: 2, STRLEN: 1,
27
- MSET: -2, MGET: -1,
28
- HSET: -3, HGET: 2, HGETALL: 1, HDEL: -2, HEXISTS: 2, HLEN: 1, HKEYS: 1, HVALS: 1,
29
- LPUSH: -2, RPUSH: -2, LPOP: 1, RPOP: 1, LLEN: 1, LRANGE: 3, LINDEX: 2, LREM: 3,
30
- SADD: -2, SREM: -2, SMEMBERS: 1, SISMEMBER: 2, SCARD: 1,
31
- ZADD: -3, ZRANGE: -3, ZREVRANGE: -3, ZSCORE: 2, ZCARD: 1, ZREM: -2, ZINCRBY: 3,
32
- DBSIZE: 0, FLUSHALL: 0, FLUSHDB: 0, SELECT: 1, INFO: 0, AUTH: 1, QUIT: 0,
33
- };
34
-
35
- class RedisServer {
36
- constructor(opts = {}) {
37
- this.port = opts.port ?? 6379;
38
- this.host = opts.host || '127.0.0.1';
39
- this.password = opts.password || null;
40
- this.dataDir = opts.dataDir || null;
41
- this.onQuery = opts.onQuery || null;
42
- this.db = new Map();
43
- this.snapshotTimer = null;
44
- this._authedSockets = new WeakMap();
45
- this._load();
46
- }
47
-
48
- snapshotPath() {
49
- return this.dataDir ? path.join(this.dataDir, 'data.rdb.json') : null;
50
- }
51
-
52
- _load() {
53
- const p = this.snapshotPath();
54
- if (!p || !fs.existsSync(p)) return;
55
- try {
56
- const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
57
- for (const [k, v] of Object.entries(raw)) this.db.set(k, v);
58
- } catch (_) {}
59
- }
60
-
61
- _scheduleSnapshot() {
62
- if (!this.dataDir) return;
63
- if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
64
- this.snapshotTimer = setTimeout(() => this._snapshot(), 500);
65
- }
66
-
67
- _snapshot() {
68
- if (this.snapshotTimer) { clearTimeout(this.snapshotTimer); this.snapshotTimer = null; }
69
- const p = this.snapshotPath();
70
- if (!p) return;
71
- fs.mkdirSync(path.dirname(p), { recursive: true });
72
- const out = {};
73
- const now = Date.now();
74
- for (const [k, v] of this.db) {
75
- if (v.ttl && v.ttl <= now) continue;
76
- out[k] = v;
77
- }
78
- const tmp = p + '.tmp';
79
- fs.writeFileSync(tmp, JSON.stringify(out));
80
- fs.renameSync(tmp, p);
81
- }
82
-
83
- stop() {
84
- if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
85
- this._snapshot();
86
- if (this.server) this.server.close();
87
- }
88
-
89
- /* ---------- value helpers ---------- */
90
- _get(key) {
91
- const v = this.db.get(key);
92
- if (!v) return null;
93
- if (v.ttl && v.ttl <= Date.now()) { this.db.delete(key); return null; }
94
- return v;
95
- }
96
-
97
- /* ---------- command execution ---------- */
98
- execute(cmd, args) {
99
- const sig = TYPE_SIGNATURES[cmd];
100
- if (sig === undefined) throw new Error(`ERR unknown command '${cmd}'`);
101
- if (sig > 0 && args.length < sig) throw new Error(`ERR wrong number of arguments for '${cmd}' command`);
102
- if (sig === -1 && args.length < 1) throw new Error(`ERR wrong number of arguments for '${cmd}' command`);
103
- switch (cmd) {
104
- case 'PING': return args.length ? args[0] : 'PONG';
105
- case 'ECHO': return args[0];
106
- case 'AUTH':
107
- if (!this.password) throw new Error('ERR Client sent AUTH, but no password is set');
108
- if (args[0] !== this.password) throw new Error('ERR invalid password');
109
- return 'OK';
110
- case 'SELECT':
111
- if (!/^\d+$/.test(args[0])) throw new Error(`ERR invalid DB index`);
112
- this.selected = parseInt(args[0], 10);
113
- return 'OK';
114
- case 'SET': {
115
- const prev = this._get(args[0]);
116
- this.db.set(args[0], { type: 'string', val: args[1], ttl: null });
117
- this._scheduleSnapshot();
118
- return 'OK';
119
- }
120
- case 'SETNX': {
121
- const prev = this._get(args[0]);
122
- if (prev) return 0;
123
- this.db.set(args[0], { type: 'string', val: args[1], ttl: null });
124
- this._scheduleSnapshot();
125
- return 1;
126
- }
127
- case 'GET': {
128
- const v = this._get(args[0]);
129
- if (!v) return null;
130
- if (v.type !== 'string') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
131
- return v.val;
132
- }
133
- case 'APPEND': {
134
- const v = this._get(args[0]);
135
- const prev = v && v.type === 'string' ? v.val : '';
136
- this.db.set(args[0], { type: 'string', val: prev + args[1], ttl: v ? v.ttl : null });
137
- this._scheduleSnapshot();
138
- return this.db.get(args[0]).val.length;
139
- }
140
- case 'STRLEN': {
141
- const v = this._get(args[0]);
142
- if (!v) return 0;
143
- if (v.type !== 'string') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
144
- return v.val.length;
145
- }
146
- case 'INCR': case 'DECR': case 'INCRBY': case 'DECRBY': {
147
- const by = cmd === 'INCRBY' || cmd === 'DECRBY' ? parseInt(args[1], 10) : 1;
148
- if (isNaN(by)) throw new Error('ERR value is not an integer or out of range');
149
- const dir = cmd === 'DECR' || cmd === 'DECRBY' ? -1 : 1;
150
- const v = this._get(args[0]);
151
- let n;
152
- if (!v) { n = 0; }
153
- else if (v.type === 'string' && /^-?\d+$/.test(v.val)) { n = parseInt(v.val, 10); }
154
- else throw new Error('ERR value is not an integer or out of range');
155
- n += dir * by;
156
- this.db.set(args[0], { type: 'string', val: String(n), ttl: v ? v.ttl : null });
157
- this._scheduleSnapshot();
158
- return n;
159
- }
160
- case 'DEL': {
161
- let n = 0;
162
- for (const k of args) if (this.db.delete(k)) n++;
163
- this._scheduleSnapshot();
164
- return n;
165
- }
166
- case 'EXISTS': {
167
- let n = 0;
168
- for (const k of args) if (this._get(k)) n++;
169
- return n;
170
- }
171
- case 'KEYS': {
172
- const pat = args[0];
173
- const re = new RegExp('^' + pat.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
174
- const keys = [];
175
- for (const k of this.db.keys()) {
176
- if (re.test(k) && this._get(k)) keys.push(k);
177
- }
178
- return keys;
179
- }
180
- case 'TYPE': {
181
- const v = this._get(args[0]);
182
- return v ? v.type : 'none';
183
- }
184
- case 'EXPIRE': {
185
- const v = this._get(args[0]);
186
- if (!v) return 0;
187
- v.ttl = Date.now() + parseInt(args[1], 10) * 1000;
188
- this._scheduleSnapshot();
189
- return 1;
190
- }
191
- case 'TTL': {
192
- const v = this._get(args[0]);
193
- if (!v) return -2;
194
- if (!v.ttl) return -1;
195
- return Math.max(0, Math.round((v.ttl - Date.now()) / 1000));
196
- }
197
- case 'PERSIST': {
198
- const v = this._get(args[0]);
199
- if (!v || !v.ttl) return 0;
200
- v.ttl = null;
201
- return 1;
202
- }
203
- case 'HSET': {
204
- if (args.length % 2 !== 1) throw new Error(`ERR wrong number of arguments for 'hset' command`);
205
- const v = this._get(args[0]);
206
- const h = (v && v.type === 'hash') ? { ...v.val } : {};
207
- let n = 0;
208
- for (let i = 1; i < args.length; i += 2) {
209
- if (!(args[i] in h)) n++;
210
- h[args[i]] = args[i + 1];
211
- }
212
- this.db.set(args[0], { type: 'hash', val: h, ttl: v ? v.ttl : null });
213
- this._scheduleSnapshot();
214
- return n;
215
- }
216
- case 'HGET': {
217
- const v = this._get(args[0]);
218
- if (!v) return null;
219
- if (v.type !== 'hash') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
220
- return v.val[args[1]] === undefined ? null : v.val[args[1]];
221
- }
222
- case 'HGETALL': {
223
- const v = this._get(args[0]);
224
- if (!v) return [];
225
- if (v.type !== 'hash') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
226
- return Object.entries(v.val).flat();
227
- }
228
- case 'HDEL': {
229
- const v = this._get(args[0]);
230
- if (!v) return 0;
231
- if (v.type !== 'hash') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
232
- let n = 0;
233
- for (const f of args.slice(1)) if (v.val[f] !== undefined) { delete v.val[f]; n++; }
234
- this._scheduleSnapshot();
235
- return n;
236
- }
237
- case 'HEXISTS': {
238
- const v = this._get(args[0]);
239
- return (v && v.type === 'hash' && v.val[args[1]] !== undefined) ? 1 : 0;
240
- }
241
- case 'HLEN': {
242
- const v = this._get(args[0]);
243
- return v && v.type === 'hash' ? Object.keys(v.val).length : 0;
244
- }
245
- case 'HKEYS': {
246
- const v = this._get(args[0]);
247
- return v && v.type === 'hash' ? Object.keys(v.val) : [];
248
- }
249
- case 'HVALS': {
250
- const v = this._get(args[0]);
251
- return v && v.type === 'hash' ? Object.values(v.val) : [];
252
- }
253
- case 'LPUSH': case 'RPUSH': {
254
- const v = this._get(args[0]);
255
- const list = (v && v.type === 'list') ? [...v.val] : [];
256
- if (cmd === 'LPUSH') {
257
- for (const val of args.slice(1)) list.unshift(val);
258
- } else {
259
- list.push(...args.slice(1));
260
- }
261
- this.db.set(args[0], { type: 'list', val: list, ttl: v ? v.ttl : null });
262
- this._scheduleSnapshot();
263
- return list.length;
264
- }
265
- case 'LPOP': case 'RPOP': {
266
- const v = this._get(args[0]);
267
- if (!v) return null;
268
- if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
269
- const el = cmd === 'LPOP' ? v.val.shift() : v.val.pop();
270
- this._scheduleSnapshot();
271
- return el === undefined ? null : el;
272
- }
273
- case 'LLEN': {
274
- const v = this._get(args[0]);
275
- return v && v.type === 'list' ? v.val.length : 0;
276
- }
277
- case 'LRANGE': {
278
- const v = this._get(args[0]);
279
- if (!v) return [];
280
- if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
281
- let start = parseInt(args[1], 10), stop = parseInt(args[2], 10);
282
- const len = v.val.length;
283
- if (start < 0) start = Math.max(0, len + start);
284
- if (stop < 0) stop = len + stop;
285
- if (start > stop || start >= len) return [];
286
- return v.val.slice(start, Math.min(stop + 1, len));
287
- }
288
- case 'LINDEX': {
289
- const v = this._get(args[0]);
290
- if (!v) return null;
291
- if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
292
- let i = parseInt(args[1], 10);
293
- if (i < 0) i = v.val.length + i;
294
- return v.val[i] === undefined ? null : v.val[i];
295
- }
296
- case 'LREM': {
297
- const v = this._get(args[0]);
298
- if (!v) return 0;
299
- if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
300
- const count = parseInt(args[1], 10);
301
- const target = args[2];
302
- const out = [];
303
- let removed = 0;
304
- const remaining = Math.abs(count);
305
- for (const el of v.val) {
306
- if (el === target && (count === 0 || removed < remaining)) { removed++; continue; }
307
- out.push(el);
308
- }
309
- v.val = out;
310
- this._scheduleSnapshot();
311
- return removed;
312
- }
313
- case 'SADD': case 'SREM': {
314
- const v = this._get(args[0]);
315
- const set = (v && v.type === 'set') ? new Set(v.val) : new Set();
316
- let n = 0;
317
- for (const m of args.slice(1)) {
318
- if (cmd === 'SADD') { if (!set.has(m)) { set.add(m); n++; } }
319
- else { if (set.delete(m)) n++; }
320
- }
321
- if (set.size === 0) this.db.delete(args[0]);
322
- else this.db.set(args[0], { type: 'set', val: [...set], ttl: v ? v.ttl : null });
323
- this._scheduleSnapshot();
324
- return n;
325
- }
326
- case 'SMEMBERS': {
327
- const v = this._get(args[0]);
328
- return v && v.type === 'set' ? [...v.val] : [];
329
- }
330
- case 'SISMEMBER': {
331
- const v = this._get(args[0]);
332
- return (v && v.type === 'set' && v.val.includes(args[1])) ? 1 : 0;
333
- }
334
- case 'SCARD': {
335
- const v = this._get(args[0]);
336
- return v && v.type === 'set' ? v.val.length : 0;
337
- }
338
- case 'ZADD': {
339
- const key = args[0];
340
- const v = this._get(key);
341
- let zset;
342
- if (v && v.type === 'zset') zset = v.val;
343
- else if (v) throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
344
- else { this.db.set(key, { type: 'zset', val: {} }); zset = this.db.get(key).val; }
345
- const rest = args.slice(1);
346
- if (rest.length % 2 !== 0) throw new Error('ERR syntax error');
347
- let added = 0;
348
- for (let i = 0; i < rest.length; i += 2) {
349
- const score = parseFloat(rest[i]);
350
- const member = rest[i + 1];
351
- if (isNaN(score)) throw new Error('ERR value is not a valid float');
352
- if (!(member in zset)) added++;
353
- zset[member] = score;
354
- }
355
- this._scheduleSnapshot();
356
- return added;
357
- }
358
- case 'ZRANGE': {
359
- const v = this._get(args[0]);
360
- if (!v) return [];
361
- if (v.type !== 'zset') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
362
- const start = parseInt(args[1], 10);
363
- const stop = parseInt(args[2], 10);
364
- const withScores = args[3] && args[3].toUpperCase() === 'WITHSCORES';
365
- const entries = Object.entries(v.val).sort((a, b) => a[1] - b[1] || (a[0] < b[0] ? -1 : 1));
366
- const from = start < 0 ? Math.max(0, entries.length + start) : start;
367
- const to = stop < 0 ? entries.length + stop : stop;
368
- const out = [];
369
- for (let i = from; i <= to && i < entries.length; i++) {
370
- out.push(entries[i][0]);
371
- if (withScores) out.push(entries[i][1]);
372
- }
373
- return out;
374
- }
375
- case 'ZREVRANGE': {
376
- const v = this._get(args[0]);
377
- if (!v) return [];
378
- if (v.type !== 'zset') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
379
- const start = parseInt(args[1], 10);
380
- const stop = parseInt(args[2], 10);
381
- const withScores = args[3] && args[3].toUpperCase() === 'WITHSCORES';
382
- const entries = Object.entries(v.val).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? 1 : -1));
383
- const from = start < 0 ? Math.max(0, entries.length + start) : start;
384
- const to = stop < 0 ? entries.length + stop : stop;
385
- const out = [];
386
- for (let i = from; i <= to && i < entries.length; i++) {
387
- out.push(entries[i][0]);
388
- if (withScores) out.push(entries[i][1]);
389
- }
390
- return out;
391
- }
392
- case 'ZSCORE': {
393
- const v = this._get(args[0]);
394
- if (!v || v.type !== 'zset' || !(args[1] in v.val)) return null;
395
- return v.val[args[1]];
396
- }
397
- case 'ZCARD': {
398
- const v = this._get(args[0]);
399
- if (!v) return 0;
400
- if (v.type !== 'zset') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
401
- return Object.keys(v.val).length;
402
- }
403
- case 'ZREM': {
404
- const v = this._get(args[0]);
405
- if (!v) return 0;
406
- if (v.type !== 'zset') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
407
- let n = 0;
408
- for (const m of args.slice(1)) {
409
- if (m in v.val) { delete v.val[m]; n++; }
410
- }
411
- this._scheduleSnapshot();
412
- return n;
413
- }
414
- case 'ZINCRBY': {
415
- const key = args[0];
416
- const inc = parseFloat(args[1]);
417
- if (isNaN(inc)) throw new Error('ERR value is not a valid float');
418
- const v = this._get(key);
419
- let zset;
420
- if (v && v.type === 'zset') zset = v.val;
421
- else if (v) throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
422
- else { this.db.set(key, { type: 'zset', val: {} }); zset = this.db.get(key).val; }
423
- const member = args[2];
424
- zset[member] = (zset[member] || 0) + inc;
425
- this._scheduleSnapshot();
426
- return zset[member];
427
- }
428
- case 'MSET': {
429
- const rest = args;
430
- if (rest.length % 2 !== 0) throw new Error('ERR wrong number of arguments');
431
- for (let i = 0; i < rest.length; i += 2) this.db.set(rest[i], { type: 'string', val: rest[i + 1], ttl: null });
432
- this._scheduleSnapshot();
433
- return 'OK';
434
- }
435
- case 'MGET': {
436
- return args.map((k) => {
437
- const v = this._get(k);
438
- return v && v.type === 'string' ? v.val : null;
439
- });
440
- }
441
- case 'DBSIZE': return this.db.size;
442
- case 'FLUSHALL': case 'FLUSHDB':
443
- this.db.clear();
444
- this._scheduleSnapshot();
445
- return 'OK';
446
- case 'INFO': {
447
- const lines = [
448
- '# Server',
449
- 'redis_version:7.0.0',
450
- 'jsql_neo_version:' + require('../package.json').version,
451
- '# Memory',
452
- 'used_memory:' + JSON.stringify([...this.db.values()]).length,
453
- '# Stats',
454
- 'db_size:' + this.db.size,
455
- 'connected_clients:1',
456
- ];
457
- return lines.join('\r\n') + '\r\n';
458
- }
459
- case 'QUIT': return 'OK';
460
- default: throw new Error(`ERR unknown command '${cmd}'`);
461
- }
462
- }
463
-
464
- /* ---------- RESP protocol ---------- */
465
- listen() {
466
- this.server = net.createServer((socket) => this._handleSocket(socket));
467
- this.server.listen(this.port, this.host);
468
- return this;
469
- }
470
-
471
- /** 供多协议嗅探复用:把 socket 交给 Redis 处理,可携带已缓冲的首包 */
472
- _handleSocket(socket, existing = '') {
473
- let buf = String(existing || '');
474
- this._authedSockets.set(socket, false);
475
- socket.setEncoding('utf8');
476
- socket.on('data', (chunk) => {
477
- buf += chunk;
478
- for (;;) {
479
- const msg = this._parse(buf);
480
- if (!msg) break;
481
- buf = buf.slice(msg.consumed);
482
- this._handle(socket, msg.cmd, msg.args);
483
- }
484
- });
485
- socket.on('error', () => {});
486
- for (;;) {
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
- }
492
- }
493
-
494
- _parse(buf) {
495
- if (buf[0] !== '*') {
496
- const i = buf.indexOf('\r\n');
497
- if (i < 0) return null;
498
- const cmd = buf.slice(0, i).trim().split(/\s+/);
499
- if (!cmd.length) return null;
500
- return { consumed: i + 2, cmd: cmd.map(c => c.toUpperCase()), args: cmd.slice(1) };
501
- }
502
- const i = buf.indexOf('\r\n');
503
- if (i < 0) return null;
504
- const n = parseInt(buf.slice(1, i), 10);
505
- if (isNaN(n)) throw new Error('ERR Protocol error');
506
- let off = i + 2;
507
- const parts = [];
508
- for (let k = 0; k < n; k++) {
509
- if (buf[off] !== '$') throw new Error('ERR Protocol error: expected bulk string');
510
- const j = buf.indexOf('\r\n', off);
511
- 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
- if (buf.length < j + 2 + len + 2) return null;
515
- parts.push(buf.slice(j + 2, j + 2 + len));
516
- off = j + 2 + len + 2;
517
- }
518
- const cmd = parts[0].toUpperCase();
519
- return { consumed: off, cmd, args: parts.slice(1) };
520
- }
521
-
522
- _handle(socket, cmd, args) {
523
- try {
524
- if (this.onQuery) this.onQuery(cmd, args);
525
- if (cmd === 'AUTH') {
526
- if (!this.password) return this._send(socket, new Error('ERR Client sent AUTH, but no password is set'));
527
- }
528
- if (this.password && !this._authedSockets.get(socket)) {
529
- if (cmd === 'AUTH') {
530
- const ok = this.execute('AUTH', args);
531
- if (ok === 'OK') this._authedSockets.set(socket, true);
532
- return this._send(socket, ok);
533
- }
534
- return this._send(socket, new Error('NOAUTH Authentication required.'));
535
- }
536
- if (cmd === 'QUIT') { this._send(socket, 'OK'); socket.end(); return; }
537
- const r = this.execute(cmd, args);
538
- this._send(socket, r);
539
- } catch (e) {
540
- this._send(socket, e);
541
- }
542
- }
543
-
544
- _send(socket, val) {
545
- let out;
546
- if (val instanceof Error) {
547
- out = '-' + val.message + '\r\n';
548
- } else if (val === null) {
549
- out = '$-1\r\n';
550
- } else if (typeof val === 'number') {
551
- out = ':' + val + '\r\n';
552
- } else if (Array.isArray(val)) {
553
- out = '*' + val.length + '\r\n' + val.map(v => '$' + String(v).length + '\r\n' + v + '\r\n').join('');
554
- } else if (typeof val === 'string') {
555
- if (val === 'OK' || val === 'PONG') {
556
- out = '+' + val + '\r\n';
557
- } else {
558
- out = '$' + Buffer.byteLength(val, 'utf8') + '\r\n' + val + '\r\n';
559
- }
560
- } else {
561
- out = '$-1\r\n';
562
- }
563
- socket.write(out);
564
- }
565
- }
566
-
567
- function createRedisServer(opts = {}) {
568
- return new RedisServer(opts);
569
- }
570
-
571
- module.exports = { RedisServer, createRedisServer };
1
+ /*
2
+ * JSQL-NEO Redis-compatible server (RESP2, zero dependencies).
3
+ *
4
+ * const { createRedisServer } = require('jsql-neo');
5
+ * const srv = createRedisServer({ port: 6379, dataDir: './data' });
6
+ * srv.listen();
7
+ *
8
+ * Supported commands:
9
+ * PING, ECHO, SET, GET, SETNX, DEL, EXISTS, KEYS, TYPE, EXPIRE, TTL, PERSIST,
10
+ * INCR, DECR, INCRBY, DECRBY, APPEND, STRLEN,
11
+ * HSET, HGET, HGETALL, HDEL, HEXISTS, HLEN, HKEYS, HVALS,
12
+ * LPUSH, RPUSH, LPOP, RPOP, LLEN, LRANGE, LINDEX, LREM,
13
+ * SADD, SREM, SMEMBERS, SISMEMBER, SCARD,
14
+ * DBSIZE, FLUSHALL, FLUSHDB, SELECT, INFO, AUTH, QUIT
15
+ *
16
+ * Persistence: keys are held in memory and snapshotted to data.rdb.json
17
+ * (debounced 500ms) plus a final snapshot on shutdown.
18
+ */
19
+ const net = require('net');
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const TYPE_SIGNATURES = {
24
+ PING: 0, ECHO: 1, SET: 2, SETNX: 2, GET: 1, DEL: -1, EXISTS: -1, KEYS: 1,
25
+ TYPE: 1, EXPIRE: 2, TTL: 1, PERSIST: 1,
26
+ INCR: 1, DECR: 1, INCRBY: 2, DECRBY: 2, APPEND: 2, STRLEN: 1,
27
+ MSET: -2, MGET: -1,
28
+ HSET: -3, HGET: 2, HGETALL: 1, HDEL: -2, HEXISTS: 2, HLEN: 1, HKEYS: 1, HVALS: 1,
29
+ LPUSH: -2, RPUSH: -2, LPOP: 1, RPOP: 1, LLEN: 1, LRANGE: 3, LINDEX: 2, LREM: 3,
30
+ SADD: -2, SREM: -2, SMEMBERS: 1, SISMEMBER: 2, SCARD: 1,
31
+ ZADD: -3, ZRANGE: -3, ZREVRANGE: -3, ZSCORE: 2, ZCARD: 1, ZREM: -2, ZINCRBY: 3,
32
+ DBSIZE: 0, FLUSHALL: 0, FLUSHDB: 0, SELECT: 1, INFO: 0, AUTH: 1, QUIT: 0,
33
+ };
34
+
35
+ class RedisServer {
36
+ constructor(opts = {}) {
37
+ this.port = opts.port ?? 6379;
38
+ this.host = opts.host || '127.0.0.1';
39
+ this.password = opts.password || null;
40
+ this.dataDir = opts.dataDir || null;
41
+ this.onQuery = opts.onQuery || null;
42
+ this.db = new Map();
43
+ this.snapshotTimer = null;
44
+ this._authedSockets = new WeakMap();
45
+ this._load();
46
+ }
47
+
48
+ snapshotPath() {
49
+ return this.dataDir ? path.join(this.dataDir, 'data.rdb.json') : null;
50
+ }
51
+
52
+ _load() {
53
+ const p = this.snapshotPath();
54
+ if (!p || !fs.existsSync(p)) return;
55
+ try {
56
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
57
+ for (const [k, v] of Object.entries(raw)) this.db.set(k, v);
58
+ } catch (_) {}
59
+ }
60
+
61
+ _scheduleSnapshot() {
62
+ if (!this.dataDir) return;
63
+ if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
64
+ this.snapshotTimer = setTimeout(() => this._snapshot(), 500);
65
+ }
66
+
67
+ _snapshot() {
68
+ if (this.snapshotTimer) { clearTimeout(this.snapshotTimer); this.snapshotTimer = null; }
69
+ const p = this.snapshotPath();
70
+ if (!p) return;
71
+ fs.mkdirSync(path.dirname(p), { recursive: true });
72
+ const out = {};
73
+ const now = Date.now();
74
+ for (const [k, v] of this.db) {
75
+ if (v.ttl && v.ttl <= now) continue;
76
+ out[k] = v;
77
+ }
78
+ const tmp = p + '.tmp';
79
+ fs.writeFileSync(tmp, JSON.stringify(out));
80
+ fs.renameSync(tmp, p);
81
+ }
82
+
83
+ stop() {
84
+ if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
85
+ this._snapshot();
86
+ if (this.server) this.server.close();
87
+ }
88
+
89
+ /* ---------- value helpers ---------- */
90
+ _get(key) {
91
+ const v = this.db.get(key);
92
+ if (!v) return null;
93
+ if (v.ttl && v.ttl <= Date.now()) { this.db.delete(key); return null; }
94
+ return v;
95
+ }
96
+
97
+ /* ---------- command execution ---------- */
98
+ execute(cmd, args) {
99
+ const sig = TYPE_SIGNATURES[cmd];
100
+ if (sig === undefined) throw new Error(`ERR unknown command '${cmd}'`);
101
+ if (sig > 0 && args.length < sig) throw new Error(`ERR wrong number of arguments for '${cmd}' command`);
102
+ if (sig === -1 && args.length < 1) throw new Error(`ERR wrong number of arguments for '${cmd}' command`);
103
+ switch (cmd) {
104
+ case 'PING': return args.length ? args[0] : 'PONG';
105
+ case 'ECHO': return args[0];
106
+ case 'AUTH':
107
+ if (!this.password) throw new Error('ERR Client sent AUTH, but no password is set');
108
+ if (args[0] !== this.password) throw new Error('ERR invalid password');
109
+ return 'OK';
110
+ case 'SELECT':
111
+ if (!/^\d+$/.test(args[0])) throw new Error(`ERR invalid DB index`);
112
+ this.selected = parseInt(args[0], 10);
113
+ return 'OK';
114
+ case 'SET': {
115
+ const prev = this._get(args[0]);
116
+ this.db.set(args[0], { type: 'string', val: args[1], ttl: null });
117
+ this._scheduleSnapshot();
118
+ return 'OK';
119
+ }
120
+ case 'SETNX': {
121
+ const prev = this._get(args[0]);
122
+ if (prev) return 0;
123
+ this.db.set(args[0], { type: 'string', val: args[1], ttl: null });
124
+ this._scheduleSnapshot();
125
+ return 1;
126
+ }
127
+ case 'GET': {
128
+ const v = this._get(args[0]);
129
+ if (!v) return null;
130
+ if (v.type !== 'string') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
131
+ return v.val;
132
+ }
133
+ case 'APPEND': {
134
+ const v = this._get(args[0]);
135
+ const prev = v && v.type === 'string' ? v.val : '';
136
+ this.db.set(args[0], { type: 'string', val: prev + args[1], ttl: v ? v.ttl : null });
137
+ this._scheduleSnapshot();
138
+ return this.db.get(args[0]).val.length;
139
+ }
140
+ case 'STRLEN': {
141
+ const v = this._get(args[0]);
142
+ if (!v) return 0;
143
+ if (v.type !== 'string') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
144
+ return v.val.length;
145
+ }
146
+ case 'INCR': case 'DECR': case 'INCRBY': case 'DECRBY': {
147
+ const by = cmd === 'INCRBY' || cmd === 'DECRBY' ? parseInt(args[1], 10) : 1;
148
+ if (isNaN(by)) throw new Error('ERR value is not an integer or out of range');
149
+ const dir = cmd === 'DECR' || cmd === 'DECRBY' ? -1 : 1;
150
+ const v = this._get(args[0]);
151
+ let n;
152
+ if (!v) { n = 0; }
153
+ else if (v.type === 'string' && /^-?\d+$/.test(v.val)) { n = parseInt(v.val, 10); }
154
+ else throw new Error('ERR value is not an integer or out of range');
155
+ n += dir * by;
156
+ this.db.set(args[0], { type: 'string', val: String(n), ttl: v ? v.ttl : null });
157
+ this._scheduleSnapshot();
158
+ return n;
159
+ }
160
+ case 'DEL': {
161
+ let n = 0;
162
+ for (const k of args) if (this.db.delete(k)) n++;
163
+ this._scheduleSnapshot();
164
+ return n;
165
+ }
166
+ case 'EXISTS': {
167
+ let n = 0;
168
+ for (const k of args) if (this._get(k)) n++;
169
+ return n;
170
+ }
171
+ case 'KEYS': {
172
+ const pat = args[0];
173
+ const re = new RegExp('^' + pat.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
174
+ const keys = [];
175
+ for (const k of this.db.keys()) {
176
+ if (re.test(k) && this._get(k)) keys.push(k);
177
+ }
178
+ return keys;
179
+ }
180
+ case 'TYPE': {
181
+ const v = this._get(args[0]);
182
+ return v ? v.type : 'none';
183
+ }
184
+ case 'EXPIRE': {
185
+ const v = this._get(args[0]);
186
+ if (!v) return 0;
187
+ v.ttl = Date.now() + parseInt(args[1], 10) * 1000;
188
+ this._scheduleSnapshot();
189
+ return 1;
190
+ }
191
+ case 'TTL': {
192
+ const v = this._get(args[0]);
193
+ if (!v) return -2;
194
+ if (!v.ttl) return -1;
195
+ return Math.max(0, Math.round((v.ttl - Date.now()) / 1000));
196
+ }
197
+ case 'PERSIST': {
198
+ const v = this._get(args[0]);
199
+ if (!v || !v.ttl) return 0;
200
+ v.ttl = null;
201
+ return 1;
202
+ }
203
+ case 'HSET': {
204
+ if (args.length % 2 !== 1) throw new Error(`ERR wrong number of arguments for 'hset' command`);
205
+ const v = this._get(args[0]);
206
+ const h = (v && v.type === 'hash') ? { ...v.val } : {};
207
+ let n = 0;
208
+ for (let i = 1; i < args.length; i += 2) {
209
+ if (!(args[i] in h)) n++;
210
+ h[args[i]] = args[i + 1];
211
+ }
212
+ this.db.set(args[0], { type: 'hash', val: h, ttl: v ? v.ttl : null });
213
+ this._scheduleSnapshot();
214
+ return n;
215
+ }
216
+ case 'HGET': {
217
+ const v = this._get(args[0]);
218
+ if (!v) return null;
219
+ if (v.type !== 'hash') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
220
+ return v.val[args[1]] === undefined ? null : v.val[args[1]];
221
+ }
222
+ case 'HGETALL': {
223
+ const v = this._get(args[0]);
224
+ if (!v) return [];
225
+ if (v.type !== 'hash') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
226
+ return Object.entries(v.val).flat();
227
+ }
228
+ case 'HDEL': {
229
+ const v = this._get(args[0]);
230
+ if (!v) return 0;
231
+ if (v.type !== 'hash') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
232
+ let n = 0;
233
+ for (const f of args.slice(1)) if (v.val[f] !== undefined) { delete v.val[f]; n++; }
234
+ this._scheduleSnapshot();
235
+ return n;
236
+ }
237
+ case 'HEXISTS': {
238
+ const v = this._get(args[0]);
239
+ return (v && v.type === 'hash' && v.val[args[1]] !== undefined) ? 1 : 0;
240
+ }
241
+ case 'HLEN': {
242
+ const v = this._get(args[0]);
243
+ return v && v.type === 'hash' ? Object.keys(v.val).length : 0;
244
+ }
245
+ case 'HKEYS': {
246
+ const v = this._get(args[0]);
247
+ return v && v.type === 'hash' ? Object.keys(v.val) : [];
248
+ }
249
+ case 'HVALS': {
250
+ const v = this._get(args[0]);
251
+ return v && v.type === 'hash' ? Object.values(v.val) : [];
252
+ }
253
+ case 'LPUSH': case 'RPUSH': {
254
+ const v = this._get(args[0]);
255
+ const list = (v && v.type === 'list') ? [...v.val] : [];
256
+ if (cmd === 'LPUSH') {
257
+ for (const val of args.slice(1)) list.unshift(val);
258
+ } else {
259
+ list.push(...args.slice(1));
260
+ }
261
+ this.db.set(args[0], { type: 'list', val: list, ttl: v ? v.ttl : null });
262
+ this._scheduleSnapshot();
263
+ return list.length;
264
+ }
265
+ case 'LPOP': case 'RPOP': {
266
+ const v = this._get(args[0]);
267
+ if (!v) return null;
268
+ if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
269
+ const el = cmd === 'LPOP' ? v.val.shift() : v.val.pop();
270
+ this._scheduleSnapshot();
271
+ return el === undefined ? null : el;
272
+ }
273
+ case 'LLEN': {
274
+ const v = this._get(args[0]);
275
+ return v && v.type === 'list' ? v.val.length : 0;
276
+ }
277
+ case 'LRANGE': {
278
+ const v = this._get(args[0]);
279
+ if (!v) return [];
280
+ if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
281
+ let start = parseInt(args[1], 10), stop = parseInt(args[2], 10);
282
+ const len = v.val.length;
283
+ if (start < 0) start = Math.max(0, len + start);
284
+ if (stop < 0) stop = len + stop;
285
+ if (start > stop || start >= len) return [];
286
+ return v.val.slice(start, Math.min(stop + 1, len));
287
+ }
288
+ case 'LINDEX': {
289
+ const v = this._get(args[0]);
290
+ if (!v) return null;
291
+ if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
292
+ let i = parseInt(args[1], 10);
293
+ if (i < 0) i = v.val.length + i;
294
+ return v.val[i] === undefined ? null : v.val[i];
295
+ }
296
+ case 'LREM': {
297
+ const v = this._get(args[0]);
298
+ if (!v) return 0;
299
+ if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
300
+ const count = parseInt(args[1], 10);
301
+ const target = args[2];
302
+ const out = [];
303
+ let removed = 0;
304
+ const remaining = Math.abs(count);
305
+ for (const el of v.val) {
306
+ if (el === target && (count === 0 || removed < remaining)) { removed++; continue; }
307
+ out.push(el);
308
+ }
309
+ v.val = out;
310
+ this._scheduleSnapshot();
311
+ return removed;
312
+ }
313
+ case 'SADD': case 'SREM': {
314
+ const v = this._get(args[0]);
315
+ const set = (v && v.type === 'set') ? new Set(v.val) : new Set();
316
+ let n = 0;
317
+ for (const m of args.slice(1)) {
318
+ if (cmd === 'SADD') { if (!set.has(m)) { set.add(m); n++; } }
319
+ else { if (set.delete(m)) n++; }
320
+ }
321
+ if (set.size === 0) this.db.delete(args[0]);
322
+ else this.db.set(args[0], { type: 'set', val: [...set], ttl: v ? v.ttl : null });
323
+ this._scheduleSnapshot();
324
+ return n;
325
+ }
326
+ case 'SMEMBERS': {
327
+ const v = this._get(args[0]);
328
+ return v && v.type === 'set' ? [...v.val] : [];
329
+ }
330
+ case 'SISMEMBER': {
331
+ const v = this._get(args[0]);
332
+ return (v && v.type === 'set' && v.val.includes(args[1])) ? 1 : 0;
333
+ }
334
+ case 'SCARD': {
335
+ const v = this._get(args[0]);
336
+ return v && v.type === 'set' ? v.val.length : 0;
337
+ }
338
+ case 'ZADD': {
339
+ const key = args[0];
340
+ const v = this._get(key);
341
+ let zset;
342
+ if (v && v.type === 'zset') zset = v.val;
343
+ else if (v) throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
344
+ else { this.db.set(key, { type: 'zset', val: {} }); zset = this.db.get(key).val; }
345
+ const rest = args.slice(1);
346
+ if (rest.length % 2 !== 0) throw new Error('ERR syntax error');
347
+ let added = 0;
348
+ for (let i = 0; i < rest.length; i += 2) {
349
+ const score = parseFloat(rest[i]);
350
+ const member = rest[i + 1];
351
+ if (isNaN(score)) throw new Error('ERR value is not a valid float');
352
+ if (!(member in zset)) added++;
353
+ zset[member] = score;
354
+ }
355
+ this._scheduleSnapshot();
356
+ return added;
357
+ }
358
+ case 'ZRANGE': {
359
+ const v = this._get(args[0]);
360
+ if (!v) return [];
361
+ if (v.type !== 'zset') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
362
+ const start = parseInt(args[1], 10);
363
+ const stop = parseInt(args[2], 10);
364
+ const withScores = args[3] && args[3].toUpperCase() === 'WITHSCORES';
365
+ const entries = Object.entries(v.val).sort((a, b) => a[1] - b[1] || (a[0] < b[0] ? -1 : 1));
366
+ const from = start < 0 ? Math.max(0, entries.length + start) : start;
367
+ const to = stop < 0 ? entries.length + stop : stop;
368
+ const out = [];
369
+ for (let i = from; i <= to && i < entries.length; i++) {
370
+ out.push(entries[i][0]);
371
+ if (withScores) out.push(entries[i][1]);
372
+ }
373
+ return out;
374
+ }
375
+ case 'ZREVRANGE': {
376
+ const v = this._get(args[0]);
377
+ if (!v) return [];
378
+ if (v.type !== 'zset') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
379
+ const start = parseInt(args[1], 10);
380
+ const stop = parseInt(args[2], 10);
381
+ const withScores = args[3] && args[3].toUpperCase() === 'WITHSCORES';
382
+ const entries = Object.entries(v.val).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? 1 : -1));
383
+ const from = start < 0 ? Math.max(0, entries.length + start) : start;
384
+ const to = stop < 0 ? entries.length + stop : stop;
385
+ const out = [];
386
+ for (let i = from; i <= to && i < entries.length; i++) {
387
+ out.push(entries[i][0]);
388
+ if (withScores) out.push(entries[i][1]);
389
+ }
390
+ return out;
391
+ }
392
+ case 'ZSCORE': {
393
+ const v = this._get(args[0]);
394
+ if (!v || v.type !== 'zset' || !(args[1] in v.val)) return null;
395
+ return v.val[args[1]];
396
+ }
397
+ case 'ZCARD': {
398
+ const v = this._get(args[0]);
399
+ if (!v) return 0;
400
+ if (v.type !== 'zset') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
401
+ return Object.keys(v.val).length;
402
+ }
403
+ case 'ZREM': {
404
+ const v = this._get(args[0]);
405
+ if (!v) return 0;
406
+ if (v.type !== 'zset') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
407
+ let n = 0;
408
+ for (const m of args.slice(1)) {
409
+ if (m in v.val) { delete v.val[m]; n++; }
410
+ }
411
+ this._scheduleSnapshot();
412
+ return n;
413
+ }
414
+ case 'ZINCRBY': {
415
+ const key = args[0];
416
+ const inc = parseFloat(args[1]);
417
+ if (isNaN(inc)) throw new Error('ERR value is not a valid float');
418
+ const v = this._get(key);
419
+ let zset;
420
+ if (v && v.type === 'zset') zset = v.val;
421
+ else if (v) throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
422
+ else { this.db.set(key, { type: 'zset', val: {} }); zset = this.db.get(key).val; }
423
+ const member = args[2];
424
+ zset[member] = (zset[member] || 0) + inc;
425
+ this._scheduleSnapshot();
426
+ return zset[member];
427
+ }
428
+ case 'MSET': {
429
+ const rest = args;
430
+ if (rest.length % 2 !== 0) throw new Error('ERR wrong number of arguments');
431
+ for (let i = 0; i < rest.length; i += 2) this.db.set(rest[i], { type: 'string', val: rest[i + 1], ttl: null });
432
+ this._scheduleSnapshot();
433
+ return 'OK';
434
+ }
435
+ case 'MGET': {
436
+ return args.map((k) => {
437
+ const v = this._get(k);
438
+ return v && v.type === 'string' ? v.val : null;
439
+ });
440
+ }
441
+ case 'DBSIZE': return this.db.size;
442
+ case 'FLUSHALL': case 'FLUSHDB':
443
+ this.db.clear();
444
+ this._scheduleSnapshot();
445
+ return 'OK';
446
+ case 'INFO': {
447
+ const lines = [
448
+ '# Server',
449
+ 'redis_version:7.0.0',
450
+ 'jsql_neo_version:' + require('../package.json').version,
451
+ '# Memory',
452
+ 'used_memory:' + JSON.stringify([...this.db.values()]).length,
453
+ '# Stats',
454
+ 'db_size:' + this.db.size,
455
+ 'connected_clients:1',
456
+ ];
457
+ return lines.join('\r\n') + '\r\n';
458
+ }
459
+ case 'QUIT': return 'OK';
460
+ default: throw new Error(`ERR unknown command '${cmd}'`);
461
+ }
462
+ }
463
+
464
+ /* ---------- RESP protocol ---------- */
465
+ listen() {
466
+ this.server = net.createServer((socket) => this._handleSocket(socket));
467
+ this.server.listen(this.port, this.host);
468
+ return this;
469
+ }
470
+
471
+ /** 供多协议嗅探复用:把 socket 交给 Redis 处理,可携带已缓冲的首包 */
472
+ _handleSocket(socket, existing = '') {
473
+ let buf = String(existing || '');
474
+ this._authedSockets.set(socket, false);
475
+ socket.setEncoding('utf8');
476
+ socket.on('data', (chunk) => {
477
+ buf += chunk;
478
+ for (;;) {
479
+ const msg = this._parse(buf);
480
+ if (!msg) break;
481
+ buf = buf.slice(msg.consumed);
482
+ this._handle(socket, msg.cmd, msg.args);
483
+ }
484
+ });
485
+ socket.on('error', () => {});
486
+ for (;;) {
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
+ }
492
+ }
493
+
494
+ _parse(buf) {
495
+ if (buf[0] !== '*') {
496
+ const i = buf.indexOf('\r\n');
497
+ if (i < 0) return null;
498
+ const cmd = buf.slice(0, i).trim().split(/\s+/);
499
+ if (!cmd.length) return null;
500
+ return { consumed: i + 2, cmd: cmd.map(c => c.toUpperCase()), args: cmd.slice(1) };
501
+ }
502
+ const i = buf.indexOf('\r\n');
503
+ if (i < 0) return null;
504
+ const n = parseInt(buf.slice(1, i), 10);
505
+ if (isNaN(n)) throw new Error('ERR Protocol error');
506
+ let off = i + 2;
507
+ const parts = [];
508
+ for (let k = 0; k < n; k++) {
509
+ if (buf[off] !== '$') throw new Error('ERR Protocol error: expected bulk string');
510
+ const j = buf.indexOf('\r\n', off);
511
+ 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
+ if (buf.length < j + 2 + len + 2) return null;
515
+ parts.push(buf.slice(j + 2, j + 2 + len));
516
+ off = j + 2 + len + 2;
517
+ }
518
+ const cmd = parts[0].toUpperCase();
519
+ return { consumed: off, cmd, args: parts.slice(1) };
520
+ }
521
+
522
+ _handle(socket, cmd, args) {
523
+ try {
524
+ if (this.onQuery) this.onQuery(cmd, args);
525
+ if (cmd === 'AUTH') {
526
+ if (!this.password) return this._send(socket, new Error('ERR Client sent AUTH, but no password is set'));
527
+ }
528
+ if (this.password && !this._authedSockets.get(socket)) {
529
+ if (cmd === 'AUTH') {
530
+ const ok = this.execute('AUTH', args);
531
+ if (ok === 'OK') this._authedSockets.set(socket, true);
532
+ return this._send(socket, ok);
533
+ }
534
+ return this._send(socket, new Error('NOAUTH Authentication required.'));
535
+ }
536
+ if (cmd === 'QUIT') { this._send(socket, 'OK'); socket.end(); return; }
537
+ const r = this.execute(cmd, args);
538
+ this._send(socket, r);
539
+ } catch (e) {
540
+ this._send(socket, e);
541
+ }
542
+ }
543
+
544
+ _send(socket, val) {
545
+ let out;
546
+ if (val instanceof Error) {
547
+ out = '-' + val.message + '\r\n';
548
+ } else if (val === null) {
549
+ out = '$-1\r\n';
550
+ } else if (typeof val === 'number') {
551
+ out = ':' + val + '\r\n';
552
+ } else if (Array.isArray(val)) {
553
+ out = '*' + val.length + '\r\n' + val.map(v => '$' + String(v).length + '\r\n' + v + '\r\n').join('');
554
+ } else if (typeof val === 'string') {
555
+ if (val === 'OK' || val === 'PONG') {
556
+ out = '+' + val + '\r\n';
557
+ } else {
558
+ out = '$' + Buffer.byteLength(val, 'utf8') + '\r\n' + val + '\r\n';
559
+ }
560
+ } else {
561
+ out = '$-1\r\n';
562
+ }
563
+ socket.write(out);
564
+ }
565
+ }
566
+
567
+ function createRedisServer(opts = {}) {
568
+ return new RedisServer(opts);
569
+ }
570
+
571
+ module.exports = { RedisServer, createRedisServer };