jsql-neo 6.3.6 → 6.4.0-beta.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/bin/jsql CHANGED
@@ -247,6 +247,96 @@ const cli = yaggs({ pkg: require('../package.json') })
247
247
  console.log(`jsql-neo Redis server on ${argv.host || '127.0.0.1'}:${argv.port != null ? argv.port : 6379}${argv['data-dir'] ? ' (data: ' + argv['data-dir'] + ')' : ''}`);
248
248
  setInterval(() => {}, 1 << 30);
249
249
  })
250
+ .command('plugin', 'Plugin registry management (list, search, install, uninstall, info)', (sub) => {
251
+ sub.option('json', { type: 'boolean', description: 'Output as JSON' });
252
+ sub.option('category', { type: 'string', description: 'Filter by category' });
253
+ sub.option('limit', { type: 'number', description: 'Max results per page' });
254
+ }, async (argv) => {
255
+ const pluginMgr = require('../src/cli/plugin');
256
+ const action = argv._[0];
257
+ const arg = argv._[1];
258
+ const useJson = !!argv.json;
259
+
260
+ const fail = (e) => { console.error(`Error: ${e.message}`); process.exitCode = 1; };
261
+ const out = (obj) => {
262
+ if (obj === undefined) return;
263
+ if (useJson) console.log(JSON.stringify(obj, null, 2));
264
+ else console.log(obj);
265
+ };
266
+
267
+ try {
268
+ switch (action) {
269
+ case 'list':
270
+ case 'ls': {
271
+ const opts = {};
272
+ if (argv.category) opts.category = argv.category;
273
+ if (argv.limit) opts.limit = argv.limit;
274
+ const plugins = await pluginMgr.listPlugins(opts);
275
+ out(useJson ? plugins : pluginMgr.formatPluginList(plugins));
276
+ break;
277
+ }
278
+ case 'search': {
279
+ if (!arg) throw new Error('Usage: jsql plugin search <query>');
280
+ const opts = {};
281
+ if (argv.category) opts.category = argv.category;
282
+ const data = await pluginMgr.searchPlugins(arg, opts);
283
+ // search 返回 { results: [...], query, total } 或 [...]
284
+ const plugins = Array.isArray(data) ? data : (data.results || []);
285
+ if (useJson) {
286
+ out(data);
287
+ } else {
288
+ if (!plugins.length) {
289
+ console.log(`No plugins found for '${arg}'.`);
290
+ } else {
291
+ console.log(`Found ${plugins.length} plugin(s) for '${arg}':`);
292
+ console.log(pluginMgr.formatPluginList(plugins.map(p => p.plugin || p)));
293
+ }
294
+ }
295
+ break;
296
+ }
297
+ case 'install': {
298
+ if (!arg) throw new Error('Usage: jsql plugin install <name>');
299
+ const manifest = await pluginMgr.installPlugin(arg);
300
+ out(useJson ? manifest : `Installed plugin '${manifest.name}' (${manifest.version})`);
301
+ break;
302
+ }
303
+ case 'uninstall':
304
+ case 'remove': {
305
+ if (!arg) throw new Error('Usage: jsql plugin uninstall <name>');
306
+ const r = pluginMgr.uninstallPlugin(arg);
307
+ out(useJson ? r : `Uninstalled plugin '${r.name}'.`);
308
+ break;
309
+ }
310
+ case 'info': {
311
+ if (!arg) throw new Error('Usage: jsql plugin info <name>');
312
+ const p = await pluginMgr.getPluginInfo(arg);
313
+ out(useJson ? p : pluginMgr.formatPluginInfo(p));
314
+ break;
315
+ }
316
+ case undefined:
317
+ // 无子命令 → 显示已安装插件
318
+ const installed = pluginMgr.listInstalled();
319
+ if (useJson) {
320
+ out(installed);
321
+ } else {
322
+ if (!installed.length) {
323
+ console.log('No plugins installed.');
324
+ console.log(`Plugin directory: ${pluginMgr.PLUGIN_DIR}`);
325
+ } else {
326
+ console.log(`Installed plugins (${installed.length}):`);
327
+ installed.forEach(p => {
328
+ console.log(` ${p.name.padEnd(24)} ${p.version || '?'}`);
329
+ });
330
+ }
331
+ }
332
+ break;
333
+ default:
334
+ throw new Error(`Unknown plugin action: ${action}`);
335
+ }
336
+ } catch (e) {
337
+ fail(e);
338
+ }
339
+ })
250
340
  .command('version', 'Show version', null, () => {
251
341
  console.log(require('../package.json').version);
252
342
  })
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.3.6 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.4.0-beta.1 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/cache.js CHANGED
@@ -36,7 +36,8 @@ class Cache {
36
36
  return count;
37
37
  }
38
38
  keys(pattern = '*') {
39
- const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
39
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.');
40
+ const regex = new RegExp('^' + escaped + '$');
40
41
  const result = [];
41
42
  for (const key of this._getData().keys()) { if (!this._isExpired(key) && regex.test(key)) result.push(key); }
42
43
  return result;
@@ -47,8 +48,9 @@ class Cache {
47
48
  const val = this._getData().get(k);
48
49
  if (val === undefined) return 'none';
49
50
  if (typeof val === 'string') return 'string';
50
- if (Array.isArray(val)) return val._type === 'hash' ? 'hash' : 'list';
51
51
  if (val instanceof Set) return 'set';
52
+ if (Array.isArray(val)) return val._type === 'hash' ? 'hash' : 'list';
53
+ if (val !== null && typeof val === 'object' && val._type === 'hash') return 'hash';
52
54
  return 'string';
53
55
  }
54
56
  expire(key, seconds) {
@@ -83,7 +85,10 @@ class Cache {
83
85
  hset(key, field, value) {
84
86
  const k = String(key);
85
87
  let hash = this._getData().get(k);
86
- if (!hash || hash._type !== 'hash') { hash = { _type: 'hash' }; this._getData().set(k, hash); }
88
+ if (hash !== undefined && (typeof hash !== 'object' || hash._type !== 'hash')) {
89
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
90
+ }
91
+ if (!hash) { hash = { _type: 'hash' }; this._getData().set(k, hash); }
87
92
  const existed = hash[field] !== undefined;
88
93
  hash[field] = String(value);
89
94
  this._dirty = true;
@@ -132,7 +137,10 @@ class Cache {
132
137
  lpush(key, ...values) {
133
138
  const k = String(key);
134
139
  let list = this._getData().get(k);
135
- if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
140
+ if (list !== undefined && (!Array.isArray(list) || list._type === 'hash')) {
141
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
142
+ }
143
+ if (!list) { list = []; this._getData().set(k, list); }
136
144
  list.unshift(...values.map(String).reverse());
137
145
  this._dirty = true;
138
146
  return list.length;
@@ -140,7 +148,10 @@ class Cache {
140
148
  rpush(key, ...values) {
141
149
  const k = String(key);
142
150
  let list = this._getData().get(k);
143
- if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
151
+ if (list !== undefined && (!Array.isArray(list) || list._type === 'hash')) {
152
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
153
+ }
154
+ if (!list) { list = []; this._getData().set(k, list); }
144
155
  list.push(...values.map(String));
145
156
  this._dirty = true;
146
157
  return list.length;
@@ -175,13 +186,18 @@ class Cache {
175
186
  if (!list || !Array.isArray(list) || list._type === 'hash') return [];
176
187
  if (start < 0) start = Math.max(list.length + start, 0);
177
188
  if (stop < 0) stop = list.length + stop;
189
+ stop = Math.min(stop, list.length - 1);
190
+ if (start > stop) return [];
178
191
  return list.slice(start, stop + 1);
179
192
  }
180
193
 
181
194
  sadd(key, ...members) {
182
195
  const k = String(key);
183
196
  let set = this._getData().get(k);
184
- if (!set || !(set instanceof Set)) { set = new Set(); this._getData().set(k, set); }
197
+ if (set !== undefined && !(set instanceof Set)) {
198
+ throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
199
+ }
200
+ if (!set) { set = new Set(); this._getData().set(k, set); }
185
201
  let count = 0;
186
202
  for (const m of members) { if (!set.has(String(m))) { set.add(String(m)); count++; } }
187
203
  if (count > 0) this._dirty = true;
@@ -219,10 +235,11 @@ class Cache {
219
235
  }
220
236
 
221
237
  flushall() {
238
+ const currentDb = this._db;
222
239
  for (let i = 0; i < this._dbs.length; i++) { if (this._dbs[i]) this._dbs[i].clear(); if (this._dbsExpires[i]) this._dbsExpires[i].clear(); }
223
- this._data = this._dbs[0] = new Map();
224
- this._expires = this._dbsExpires[0] = new Map();
225
- this._db = 0;
240
+ this._data = this._dbs[currentDb] = new Map();
241
+ this._expires = this._dbsExpires[currentDb] = new Map();
242
+ this._db = currentDb;
226
243
  this._dirty = true;
227
244
  return 'OK';
228
245
  }
@@ -244,32 +261,34 @@ class Cache {
244
261
 
245
262
  save() {
246
263
  if (!this._persistPath) return;
247
- const dump = { version: 1, databases: [] };
248
- for (let i = 0; i < this._dbs.length; i++) {
249
- const db = this._dbs[i];
250
- if (!db || db.size === 0) continue;
251
- const data = {};
252
- for (const [key, val] of db) {
253
- const exp = this._dbsExpires[i];
254
- if (exp && exp.has(key) && exp.get(key) <= Date.now()) continue;
255
- if (val instanceof Set) data[key] = { _t: 'set', _v: [...val] };
256
- else if (Array.isArray(val)) data[key] = { _t: 'list', _v: val };
257
- else if (typeof val === 'object' && val._type === 'hash') {
258
- const clean = {}; for (const [f, v] of Object.entries(val)) { if (f !== '_type') clean[f] = v; }
259
- data[key] = { _t: 'hash', _v: clean };
260
- } else data[key] = { _t: 'string', _v: val };
264
+ try {
265
+ const dump = { version: 1, databases: [] };
266
+ for (let i = 0; i < this._dbs.length; i++) {
267
+ const db = this._dbs[i];
268
+ if (!db || db.size === 0) continue;
269
+ const data = {};
270
+ for (const [key, val] of db) {
271
+ const exp = this._dbsExpires[i];
272
+ if (exp && exp.has(key) && exp.get(key) <= Date.now()) continue;
273
+ if (val instanceof Set) data[key] = { _t: 'set', _v: [...val] };
274
+ else if (Array.isArray(val)) data[key] = { _t: 'list', _v: val };
275
+ else if (typeof val === 'object' && val._type === 'hash') {
276
+ const clean = {}; for (const [f, v] of Object.entries(val)) { if (f !== '_type') clean[f] = v; }
277
+ data[key] = { _t: 'hash', _v: clean };
278
+ } else data[key] = { _t: 'string', _v: val };
279
+ }
280
+ const expires = {};
281
+ const expMap = this._dbsExpires[i];
282
+ if (expMap) { for (const [key, ttl] of expMap) { if (ttl > Date.now()) expires[key] = ttl; } }
283
+ if (Object.keys(data).length > 0 || Object.keys(expires).length > 0) dump.databases.push({ index: i, data, expires });
261
284
  }
262
- const expires = {};
263
- const expMap = this._dbsExpires[i];
264
- if (expMap) { for (const [key, ttl] of expMap) { if (ttl > Date.now()) expires[key] = ttl; } }
265
- if (Object.keys(data).length > 0 || Object.keys(expires).length > 0) dump.databases.push({ index: i, data, expires });
266
- }
267
- const dir = path.dirname(this._persistPath);
268
- if (dir) fs.mkdirSync(dir, { recursive: true });
269
- fs.writeFileSync(this._persistPath + '.tmp', JSON.stringify(dump), 'utf8');
270
- fs.renameSync(this._persistPath + '.tmp', this._persistPath);
271
- this._dirty = false;
272
- return 'OK';
285
+ const dir = path.dirname(this._persistPath);
286
+ if (dir) fs.mkdirSync(dir, { recursive: true });
287
+ fs.writeFileSync(this._persistPath + '.tmp', JSON.stringify(dump), 'utf8');
288
+ fs.renameSync(this._persistPath + '.tmp', this._persistPath);
289
+ this._dirty = false;
290
+ return 'OK';
291
+ } catch (e) { return null; }
273
292
  }
274
293
 
275
294
  _load() {
package/lib/client.js CHANGED
@@ -12,7 +12,8 @@ function binPath() {
12
12
  class JSQL {
13
13
  constructor(opts = {}) {
14
14
  this.host = opts.host || '127.0.0.1';
15
- this.port = opts.port || 6379;
15
+ // P3-5: 默认端口 6379 会与 Redis 冲突,改为 3000
16
+ this.port = opts.port || 3000;
16
17
  this.dataDir = opts.dataDir || null;
17
18
  this._process = null;
18
19
  }
@@ -45,7 +46,8 @@ class JSQL {
45
46
  async stop() {
46
47
  if (!this._process) return;
47
48
  this._process.kill();
48
- await new Promise(r => setTimeout(r, 500));
49
+ // P3-6: 不再硬等 500ms,给进程一点时间退出即可
50
+ await new Promise(r => setTimeout(r, 100));
49
51
  this._process = null;
50
52
  }
51
53
 
@@ -74,6 +76,13 @@ class JSQL {
74
76
  headers: body ? { 'Content-Type': 'application/json' } : {},
75
77
  };
76
78
  const req = http.request(opts, (res) => {
79
+ // P3-7: 未处理 res 错误——非 2xx 状态码应 reject
80
+ if (res.statusCode < 200 || res.statusCode >= 300) {
81
+ let errData = '';
82
+ res.on('data', c => errData += c);
83
+ res.on('end', () => reject(new Error(`HTTP ${res.statusCode}: ${errData}`)));
84
+ return;
85
+ }
77
86
  let data = '';
78
87
  res.on('data', c => data += c);
79
88
  res.on('end', () => {