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
package/lib/cache.js CHANGED
@@ -1,328 +1,328 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- class Cache {
5
- constructor(options = {}) {
6
- this._data = new Map();
7
- this._expires = new Map();
8
- this._ttlTimer = null;
9
- this._db = 0;
10
- this._dbs = [this._data];
11
- this._dbsExpires = [this._expires];
12
- this._persistPath = options.path || null;
13
- this._saveInterval = options.saveInterval || 0;
14
- this._dirty = false;
15
- if (this._persistPath) this._load();
16
- if (this._saveInterval > 0) this._startAutoSave();
17
- this._startTTLCleaner();
18
- }
19
-
20
- set(key, value) { this._getData().set(String(key), String(value)); this._dirty = true; return 'OK'; }
21
- get(key) {
22
- const k = String(key);
23
- if (this._isExpired(k)) return null;
24
- const val = this._getData().get(k);
25
- return val !== undefined ? val : null;
26
- }
27
- del(...keys) {
28
- let count = 0;
29
- for (const key of keys) { const k = String(key); if (this._getData().delete(k)) count++; this._getExpires().delete(k); }
30
- if (count > 0) this._dirty = true;
31
- return count;
32
- }
33
- exists(...keys) {
34
- let count = 0;
35
- for (const key of keys) { const k = String(key); if (!this._isExpired(k) && this._getData().has(k)) count++; }
36
- return count;
37
- }
38
- keys(pattern = '*') {
39
- const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
40
- const result = [];
41
- for (const key of this._getData().keys()) { if (!this._isExpired(key) && regex.test(key)) result.push(key); }
42
- return result;
43
- }
44
- type(key) {
45
- const k = String(key);
46
- if (this._isExpired(k)) return 'none';
47
- const val = this._getData().get(k);
48
- if (val === undefined) return 'none';
49
- if (typeof val === 'string') return 'string';
50
- if (Array.isArray(val)) return val._type === 'hash' ? 'hash' : 'list';
51
- if (val instanceof Set) return 'set';
52
- return 'string';
53
- }
54
- expire(key, seconds) {
55
- const k = String(key);
56
- if (!this._getData().has(k)) return 0;
57
- this._getExpires().set(k, Date.now() + seconds * 1000);
58
- return 1;
59
- }
60
- ttl(key) {
61
- const k = String(key);
62
- if (!this._getData().has(k)) return -2;
63
- const expiry = this._getExpires().get(k);
64
- if (expiry === undefined) return -1;
65
- const remaining = Math.ceil((expiry - Date.now()) / 1000);
66
- return remaining > 0 ? remaining : -2;
67
- }
68
- persist(key) { return this._getExpires().delete(String(key)) ? 1 : 0; }
69
- incr(key) { return this._incrby(key, 1); }
70
- decr(key) { return this._incrby(key, -1); }
71
- incrby(key, increment) { return this._incrby(key, increment); }
72
- decrby(key, decrement) { return this._incrby(key, -decrement); }
73
- _incrby(key, delta) {
74
- const k = String(key);
75
- const cur = this._getData().get(k);
76
- const val = (cur !== undefined ? Number(cur) : 0) + delta;
77
- if (!Number.isFinite(val)) throw new Error('value is not an integer or out of range');
78
- this._getData().set(k, String(val));
79
- this._dirty = true;
80
- return val;
81
- }
82
-
83
- hset(key, field, value) {
84
- const k = String(key);
85
- let hash = this._getData().get(k);
86
- if (!hash || hash._type !== 'hash') { hash = { _type: 'hash' }; this._getData().set(k, hash); }
87
- const existed = hash[field] !== undefined;
88
- hash[field] = String(value);
89
- this._dirty = true;
90
- return existed ? 0 : 1;
91
- }
92
- hget(key, field) {
93
- const k = String(key);
94
- if (this._isExpired(k)) return null;
95
- const hash = this._getData().get(k);
96
- if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return null;
97
- return hash[field] !== undefined ? hash[field] : null;
98
- }
99
- hgetall(key) {
100
- const k = String(key);
101
- if (this._isExpired(k)) return null;
102
- const hash = this._getData().get(k);
103
- if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return null;
104
- const result = {};
105
- for (const [f, v] of Object.entries(hash)) { if (f !== '_type') result[f] = v; }
106
- return result;
107
- }
108
- hdel(key, ...fields) {
109
- const k = String(key);
110
- const hash = this._getData().get(k);
111
- if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return 0;
112
- let count = 0;
113
- for (const field of fields) { if (delete hash[field]) count++; }
114
- if (count > 0) this._dirty = true;
115
- return count;
116
- }
117
- hkeys(key) {
118
- const k = String(key);
119
- if (this._isExpired(k)) return [];
120
- const hash = this._getData().get(k);
121
- if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return [];
122
- return Object.keys(hash).filter(f => f !== '_type');
123
- }
124
- hlen(key) {
125
- const k = String(key);
126
- if (this._isExpired(k)) return 0;
127
- const hash = this._getData().get(k);
128
- if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return 0;
129
- return Object.keys(hash).filter(f => f !== '_type').length;
130
- }
131
-
132
- lpush(key, ...values) {
133
- const k = String(key);
134
- let list = this._getData().get(k);
135
- if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
136
- list.unshift(...values.map(String).reverse());
137
- this._dirty = true;
138
- return list.length;
139
- }
140
- rpush(key, ...values) {
141
- const k = String(key);
142
- let list = this._getData().get(k);
143
- if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
144
- list.push(...values.map(String));
145
- this._dirty = true;
146
- return list.length;
147
- }
148
- lpop(key) {
149
- const k = String(key);
150
- const list = this._getData().get(k);
151
- if (!list || !Array.isArray(list) || list._type === 'hash') return null;
152
- if (list.length === 0) return null;
153
- this._dirty = true;
154
- return list.shift();
155
- }
156
- rpop(key) {
157
- const k = String(key);
158
- const list = this._getData().get(k);
159
- if (!list || !Array.isArray(list) || list._type === 'hash') return null;
160
- if (list.length === 0) return null;
161
- this._dirty = true;
162
- return list.pop();
163
- }
164
- llen(key) {
165
- const k = String(key);
166
- if (this._isExpired(k)) return 0;
167
- const list = this._getData().get(k);
168
- if (!list || !Array.isArray(list) || list._type === 'hash') return 0;
169
- return list.length;
170
- }
171
- lrange(key, start, stop) {
172
- const k = String(key);
173
- if (this._isExpired(k)) return [];
174
- const list = this._getData().get(k);
175
- if (!list || !Array.isArray(list) || list._type === 'hash') return [];
176
- if (start < 0) start = Math.max(list.length + start, 0);
177
- if (stop < 0) stop = list.length + stop;
178
- return list.slice(start, stop + 1);
179
- }
180
-
181
- sadd(key, ...members) {
182
- const k = String(key);
183
- let set = this._getData().get(k);
184
- if (!set || !(set instanceof Set)) { set = new Set(); this._getData().set(k, set); }
185
- let count = 0;
186
- for (const m of members) { if (!set.has(String(m))) { set.add(String(m)); count++; } }
187
- if (count > 0) this._dirty = true;
188
- return count;
189
- }
190
- srem(key, ...members) {
191
- const k = String(key);
192
- const set = this._getData().get(k);
193
- if (!set || !(set instanceof Set)) return 0;
194
- let count = 0;
195
- for (const m of members) { if (set.delete(String(m))) count++; }
196
- if (count > 0) this._dirty = true;
197
- return count;
198
- }
199
- smembers(key) {
200
- const k = String(key);
201
- if (this._isExpired(k)) return [];
202
- const set = this._getData().get(k);
203
- if (!set || !(set instanceof Set)) return [];
204
- return [...set];
205
- }
206
- sismember(key, member) {
207
- const k = String(key);
208
- if (this._isExpired(k)) return 0;
209
- const set = this._getData().get(k);
210
- if (!set || !(set instanceof Set)) return 0;
211
- return set.has(String(member)) ? 1 : 0;
212
- }
213
- scard(key) {
214
- const k = String(key);
215
- if (this._isExpired(k)) return 0;
216
- const set = this._getData().get(k);
217
- if (!set || !(set instanceof Set)) return 0;
218
- return set.size;
219
- }
220
-
221
- flushall() {
222
- 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;
226
- this._dirty = true;
227
- return 'OK';
228
- }
229
- flushdb() { this._getData().clear(); this._getExpires().clear(); this._dirty = true; return 'OK'; }
230
- dbsize() {
231
- let count = 0;
232
- for (const key of this._getData().keys()) { if (!this._isExpired(key)) count++; }
233
- return count;
234
- }
235
- select(index) {
236
- const idx = Number(index);
237
- if (idx < 0 || idx > 15) throw new Error('DB index out of range');
238
- this._db = idx;
239
- if (!this._dbs[idx]) { this._dbs[idx] = new Map(); this._dbsExpires[idx] = new Map(); }
240
- this._data = this._dbs[idx];
241
- this._expires = this._dbsExpires[idx];
242
- return 'OK';
243
- }
244
-
245
- save() {
246
- 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 };
261
- }
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';
273
- }
274
-
275
- _load() {
276
- try {
277
- if (!fs.existsSync(this._persistPath)) return;
278
- const dump = JSON.parse(fs.readFileSync(this._persistPath, 'utf8'));
279
- if (dump.version !== 1) return;
280
- for (const db of dump.databases || []) {
281
- const idx = db.index;
282
- if (!this._dbs[idx]) { this._dbs[idx] = new Map(); this._dbsExpires[idx] = new Map(); }
283
- const data = this._dbs[idx], expires = this._dbsExpires[idx];
284
- for (const [key, entry] of Object.entries(db.data || {})) {
285
- if (entry._t === 'set') data.set(key, new Set(entry._v));
286
- else if (entry._t === 'list') data.set(key, entry._v);
287
- else if (entry._t === 'hash') data.set(key, { _type: 'hash', ...entry._v });
288
- else data.set(key, entry._v);
289
- }
290
- for (const [key, ttl] of Object.entries(db.expires || {})) { if (ttl > Date.now()) expires.set(key, ttl); }
291
- }
292
- this._data = this._dbs[this._db] || new Map();
293
- this._expires = this._dbsExpires[this._db] || new Map();
294
- } catch (_) { /* ignore */ }
295
- }
296
-
297
- _startAutoSave() { this._saveTimer = setInterval(() => { if (this._dirty) this.save(); }, this._saveInterval); }
298
- _startTTLCleaner() {
299
- this._ttlTimer = setInterval(() => {
300
- for (let i = 0; i < this._dbs.length; i++) {
301
- const exp = this._dbsExpires[i]; if (!exp) continue;
302
- const now = Date.now();
303
- for (const [key, ttl] of exp) { if (ttl <= now) { if (this._dbs[i]) this._dbs[i].delete(key); exp.delete(key); } }
304
- }
305
- }, 1000);
306
- }
307
-
308
- close() { if (this._persistPath && this._dirty) this.save(); if (this._saveTimer) clearInterval(this._saveTimer); if (this._ttlTimer) clearInterval(this._ttlTimer); }
309
- _isExpired(key) {
310
- const expiry = this._getExpires().get(key);
311
- if (expiry !== undefined && expiry <= Date.now()) { this._getData().delete(key); this._getExpires().delete(key); return true; }
312
- return false;
313
- }
314
- _getData() { return this._data; }
315
- _getExpires() { return this._expires; }
316
-
317
- static redisPlugin(db, options = {}) {
318
- const cache = new Cache(options);
319
- db._cache = cache;
320
- const redisMethods = ['set','get','del','exists','keys','type','expire','ttl','persist','incr','decr','incrby','decrby','hset','hget','hgetall','hdel','hkeys','hlen','lpush','rpush','lpop','rpop','llen','lrange','sadd','srem','smembers','sismember','scard','flushall','flushdb','dbsize','select'];
321
- for (const m of redisMethods) {
322
- db[m] = cache[m].bind(cache);
323
- }
324
- return cache;
325
- }
326
- }
327
-
328
- module.exports = Cache;
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ class Cache {
5
+ constructor(options = {}) {
6
+ this._data = new Map();
7
+ this._expires = new Map();
8
+ this._ttlTimer = null;
9
+ this._db = 0;
10
+ this._dbs = [this._data];
11
+ this._dbsExpires = [this._expires];
12
+ this._persistPath = options.path || null;
13
+ this._saveInterval = options.saveInterval || 0;
14
+ this._dirty = false;
15
+ if (this._persistPath) this._load();
16
+ if (this._saveInterval > 0) this._startAutoSave();
17
+ this._startTTLCleaner();
18
+ }
19
+
20
+ set(key, value) { this._getData().set(String(key), String(value)); this._dirty = true; return 'OK'; }
21
+ get(key) {
22
+ const k = String(key);
23
+ if (this._isExpired(k)) return null;
24
+ const val = this._getData().get(k);
25
+ return val !== undefined ? val : null;
26
+ }
27
+ del(...keys) {
28
+ let count = 0;
29
+ for (const key of keys) { const k = String(key); if (this._getData().delete(k)) count++; this._getExpires().delete(k); }
30
+ if (count > 0) this._dirty = true;
31
+ return count;
32
+ }
33
+ exists(...keys) {
34
+ let count = 0;
35
+ for (const key of keys) { const k = String(key); if (!this._isExpired(k) && this._getData().has(k)) count++; }
36
+ return count;
37
+ }
38
+ keys(pattern = '*') {
39
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
40
+ const result = [];
41
+ for (const key of this._getData().keys()) { if (!this._isExpired(key) && regex.test(key)) result.push(key); }
42
+ return result;
43
+ }
44
+ type(key) {
45
+ const k = String(key);
46
+ if (this._isExpired(k)) return 'none';
47
+ const val = this._getData().get(k);
48
+ if (val === undefined) return 'none';
49
+ if (typeof val === 'string') return 'string';
50
+ if (Array.isArray(val)) return val._type === 'hash' ? 'hash' : 'list';
51
+ if (val instanceof Set) return 'set';
52
+ return 'string';
53
+ }
54
+ expire(key, seconds) {
55
+ const k = String(key);
56
+ if (!this._getData().has(k)) return 0;
57
+ this._getExpires().set(k, Date.now() + seconds * 1000);
58
+ return 1;
59
+ }
60
+ ttl(key) {
61
+ const k = String(key);
62
+ if (!this._getData().has(k)) return -2;
63
+ const expiry = this._getExpires().get(k);
64
+ if (expiry === undefined) return -1;
65
+ const remaining = Math.ceil((expiry - Date.now()) / 1000);
66
+ return remaining > 0 ? remaining : -2;
67
+ }
68
+ persist(key) { return this._getExpires().delete(String(key)) ? 1 : 0; }
69
+ incr(key) { return this._incrby(key, 1); }
70
+ decr(key) { return this._incrby(key, -1); }
71
+ incrby(key, increment) { return this._incrby(key, increment); }
72
+ decrby(key, decrement) { return this._incrby(key, -decrement); }
73
+ _incrby(key, delta) {
74
+ const k = String(key);
75
+ const cur = this._getData().get(k);
76
+ const val = (cur !== undefined ? Number(cur) : 0) + delta;
77
+ if (!Number.isFinite(val)) throw new Error('value is not an integer or out of range');
78
+ this._getData().set(k, String(val));
79
+ this._dirty = true;
80
+ return val;
81
+ }
82
+
83
+ hset(key, field, value) {
84
+ const k = String(key);
85
+ let hash = this._getData().get(k);
86
+ if (!hash || hash._type !== 'hash') { hash = { _type: 'hash' }; this._getData().set(k, hash); }
87
+ const existed = hash[field] !== undefined;
88
+ hash[field] = String(value);
89
+ this._dirty = true;
90
+ return existed ? 0 : 1;
91
+ }
92
+ hget(key, field) {
93
+ const k = String(key);
94
+ if (this._isExpired(k)) return null;
95
+ const hash = this._getData().get(k);
96
+ if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return null;
97
+ return hash[field] !== undefined ? hash[field] : null;
98
+ }
99
+ hgetall(key) {
100
+ const k = String(key);
101
+ if (this._isExpired(k)) return null;
102
+ const hash = this._getData().get(k);
103
+ if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return null;
104
+ const result = {};
105
+ for (const [f, v] of Object.entries(hash)) { if (f !== '_type') result[f] = v; }
106
+ return result;
107
+ }
108
+ hdel(key, ...fields) {
109
+ const k = String(key);
110
+ const hash = this._getData().get(k);
111
+ if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return 0;
112
+ let count = 0;
113
+ for (const field of fields) { if (delete hash[field]) count++; }
114
+ if (count > 0) this._dirty = true;
115
+ return count;
116
+ }
117
+ hkeys(key) {
118
+ const k = String(key);
119
+ if (this._isExpired(k)) return [];
120
+ const hash = this._getData().get(k);
121
+ if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return [];
122
+ return Object.keys(hash).filter(f => f !== '_type');
123
+ }
124
+ hlen(key) {
125
+ const k = String(key);
126
+ if (this._isExpired(k)) return 0;
127
+ const hash = this._getData().get(k);
128
+ if (!hash || typeof hash !== 'object' || hash._type !== 'hash') return 0;
129
+ return Object.keys(hash).filter(f => f !== '_type').length;
130
+ }
131
+
132
+ lpush(key, ...values) {
133
+ const k = String(key);
134
+ let list = this._getData().get(k);
135
+ if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
136
+ list.unshift(...values.map(String).reverse());
137
+ this._dirty = true;
138
+ return list.length;
139
+ }
140
+ rpush(key, ...values) {
141
+ const k = String(key);
142
+ let list = this._getData().get(k);
143
+ if (!list || !Array.isArray(list) || list._type === 'hash') { list = []; this._getData().set(k, list); }
144
+ list.push(...values.map(String));
145
+ this._dirty = true;
146
+ return list.length;
147
+ }
148
+ lpop(key) {
149
+ const k = String(key);
150
+ const list = this._getData().get(k);
151
+ if (!list || !Array.isArray(list) || list._type === 'hash') return null;
152
+ if (list.length === 0) return null;
153
+ this._dirty = true;
154
+ return list.shift();
155
+ }
156
+ rpop(key) {
157
+ const k = String(key);
158
+ const list = this._getData().get(k);
159
+ if (!list || !Array.isArray(list) || list._type === 'hash') return null;
160
+ if (list.length === 0) return null;
161
+ this._dirty = true;
162
+ return list.pop();
163
+ }
164
+ llen(key) {
165
+ const k = String(key);
166
+ if (this._isExpired(k)) return 0;
167
+ const list = this._getData().get(k);
168
+ if (!list || !Array.isArray(list) || list._type === 'hash') return 0;
169
+ return list.length;
170
+ }
171
+ lrange(key, start, stop) {
172
+ const k = String(key);
173
+ if (this._isExpired(k)) return [];
174
+ const list = this._getData().get(k);
175
+ if (!list || !Array.isArray(list) || list._type === 'hash') return [];
176
+ if (start < 0) start = Math.max(list.length + start, 0);
177
+ if (stop < 0) stop = list.length + stop;
178
+ return list.slice(start, stop + 1);
179
+ }
180
+
181
+ sadd(key, ...members) {
182
+ const k = String(key);
183
+ let set = this._getData().get(k);
184
+ if (!set || !(set instanceof Set)) { set = new Set(); this._getData().set(k, set); }
185
+ let count = 0;
186
+ for (const m of members) { if (!set.has(String(m))) { set.add(String(m)); count++; } }
187
+ if (count > 0) this._dirty = true;
188
+ return count;
189
+ }
190
+ srem(key, ...members) {
191
+ const k = String(key);
192
+ const set = this._getData().get(k);
193
+ if (!set || !(set instanceof Set)) return 0;
194
+ let count = 0;
195
+ for (const m of members) { if (set.delete(String(m))) count++; }
196
+ if (count > 0) this._dirty = true;
197
+ return count;
198
+ }
199
+ smembers(key) {
200
+ const k = String(key);
201
+ if (this._isExpired(k)) return [];
202
+ const set = this._getData().get(k);
203
+ if (!set || !(set instanceof Set)) return [];
204
+ return [...set];
205
+ }
206
+ sismember(key, member) {
207
+ const k = String(key);
208
+ if (this._isExpired(k)) return 0;
209
+ const set = this._getData().get(k);
210
+ if (!set || !(set instanceof Set)) return 0;
211
+ return set.has(String(member)) ? 1 : 0;
212
+ }
213
+ scard(key) {
214
+ const k = String(key);
215
+ if (this._isExpired(k)) return 0;
216
+ const set = this._getData().get(k);
217
+ if (!set || !(set instanceof Set)) return 0;
218
+ return set.size;
219
+ }
220
+
221
+ flushall() {
222
+ 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;
226
+ this._dirty = true;
227
+ return 'OK';
228
+ }
229
+ flushdb() { this._getData().clear(); this._getExpires().clear(); this._dirty = true; return 'OK'; }
230
+ dbsize() {
231
+ let count = 0;
232
+ for (const key of this._getData().keys()) { if (!this._isExpired(key)) count++; }
233
+ return count;
234
+ }
235
+ select(index) {
236
+ const idx = Number(index);
237
+ if (idx < 0 || idx > 15) throw new Error('DB index out of range');
238
+ this._db = idx;
239
+ if (!this._dbs[idx]) { this._dbs[idx] = new Map(); this._dbsExpires[idx] = new Map(); }
240
+ this._data = this._dbs[idx];
241
+ this._expires = this._dbsExpires[idx];
242
+ return 'OK';
243
+ }
244
+
245
+ save() {
246
+ 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 };
261
+ }
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';
273
+ }
274
+
275
+ _load() {
276
+ try {
277
+ if (!fs.existsSync(this._persistPath)) return;
278
+ const dump = JSON.parse(fs.readFileSync(this._persistPath, 'utf8'));
279
+ if (dump.version !== 1) return;
280
+ for (const db of dump.databases || []) {
281
+ const idx = db.index;
282
+ if (!this._dbs[idx]) { this._dbs[idx] = new Map(); this._dbsExpires[idx] = new Map(); }
283
+ const data = this._dbs[idx], expires = this._dbsExpires[idx];
284
+ for (const [key, entry] of Object.entries(db.data || {})) {
285
+ if (entry._t === 'set') data.set(key, new Set(entry._v));
286
+ else if (entry._t === 'list') data.set(key, entry._v);
287
+ else if (entry._t === 'hash') data.set(key, { _type: 'hash', ...entry._v });
288
+ else data.set(key, entry._v);
289
+ }
290
+ for (const [key, ttl] of Object.entries(db.expires || {})) { if (ttl > Date.now()) expires.set(key, ttl); }
291
+ }
292
+ this._data = this._dbs[this._db] || new Map();
293
+ this._expires = this._dbsExpires[this._db] || new Map();
294
+ } catch (_) { /* ignore */ }
295
+ }
296
+
297
+ _startAutoSave() { this._saveTimer = setInterval(() => { if (this._dirty) this.save(); }, this._saveInterval); }
298
+ _startTTLCleaner() {
299
+ this._ttlTimer = setInterval(() => {
300
+ for (let i = 0; i < this._dbs.length; i++) {
301
+ const exp = this._dbsExpires[i]; if (!exp) continue;
302
+ const now = Date.now();
303
+ for (const [key, ttl] of exp) { if (ttl <= now) { if (this._dbs[i]) this._dbs[i].delete(key); exp.delete(key); } }
304
+ }
305
+ }, 1000);
306
+ }
307
+
308
+ close() { if (this._persistPath && this._dirty) this.save(); if (this._saveTimer) clearInterval(this._saveTimer); if (this._ttlTimer) clearInterval(this._ttlTimer); }
309
+ _isExpired(key) {
310
+ const expiry = this._getExpires().get(key);
311
+ if (expiry !== undefined && expiry <= Date.now()) { this._getData().delete(key); this._getExpires().delete(key); return true; }
312
+ return false;
313
+ }
314
+ _getData() { return this._data; }
315
+ _getExpires() { return this._expires; }
316
+
317
+ static redisPlugin(db, options = {}) {
318
+ const cache = new Cache(options);
319
+ db._cache = cache;
320
+ const redisMethods = ['set','get','del','exists','keys','type','expire','ttl','persist','incr','decr','incrby','decrby','hset','hget','hgetall','hdel','hkeys','hlen','lpush','rpush','lpop','rpop','llen','lrange','sadd','srem','smembers','sismember','scard','flushall','flushdb','dbsize','select'];
321
+ for (const m of redisMethods) {
322
+ db[m] = cache[m].bind(cache);
323
+ }
324
+ return cache;
325
+ }
326
+ }
327
+
328
+ module.exports = Cache;