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.
- package/LICENSE +202 -202
- package/README.md +8097 -7971
- package/bin/jsql +320 -320
- package/bin/jsql-server +90 -90
- package/bin/jsql-server-control.js +177 -177
- package/index.d.ts +385 -357
- package/index.js +124 -118
- package/lib/ast.js +214 -0
- package/lib/btree.js +413 -413
- package/lib/cache.js +328 -328
- package/lib/client.js +125 -125
- package/lib/database.js +1744 -1744
- package/lib/date-types.js +182 -182
- package/lib/errors.js +89 -89
- package/lib/jsql_format.js +497 -497
- package/lib/migrate.js +254 -254
- package/lib/mod.js +292 -292
- package/lib/mongo_server.js +607 -607
- package/lib/multiserver.js +140 -140
- package/lib/mysql_compat.js +413 -413
- package/lib/mysql_server.js +1237 -1237
- package/lib/native_client.js +732 -732
- package/lib/nedb_compat.js +506 -506
- package/lib/pg_server.js +864 -864
- package/lib/plugin.js +34 -34
- package/lib/query.js +815 -815
- package/lib/redis_server.js +571 -571
- package/lib/sql.js +3460 -2925
- package/lib/sqlite_compat.js +323 -323
- package/lib/sqlite_worker.js +264 -264
- package/lib/table.js +1304 -1304
- package/lib/tui.js +502 -502
- package/lib/wasm.js +1 -1
- package/lib/wasm_client.js +718 -718
- package/lib/web_ui.js +257 -257
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +455 -455
- package/nativesrc/jsql-neo-core/Cargo.toml +24 -24
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +455 -455
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +168 -168
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +62 -62
- package/nativesrc/jsql-neo-core/src/engine/table.rs +700 -700
- package/nativesrc/jsql-neo-core/src/lib.rs +2 -2
- package/nativesrc/jsql-neo-core/src/storage/mod.rs +2 -2
- package/nativesrc/jsql-neo-core/src/storage/persistent.rs +1 -1
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +85 -85
- package/nativesrc/jsql-neo-core/src/types.rs +93 -93
- package/nativesrc/jsql-neo-native/Cargo.lock +606 -606
- package/nativesrc/jsql-neo-native/Cargo.toml +16 -16
- package/nativesrc/jsql-neo-native/build.rs +3 -3
- package/nativesrc/jsql-neo-native/package.json +7 -7
- package/nativesrc/jsql-neo-native/src/lib.rs +269 -269
- package/nativesrc/jsql-neo-wasm/Cargo.lock +465 -465
- package/nativesrc/jsql-neo-wasm/Cargo.toml +15 -15
- package/nativesrc/jsql-neo-wasm/src/lib.rs +447 -447
- package/package.json +87 -86
- package/postinstall.js +158 -158
- package/test/btree.test.js +128 -128
- package/test/coverage.js +187 -187
- package/test/join.test.js +110 -110
- package/test/native.test.js +158 -158
- package/test/regress-5.1.0.js +271 -271
- package/test/smoke.js +58 -58
- package/test/sql-parser.test.js +273 -0
- package/test/wasm.test.js +110 -110
- package/wasm/browser.d.ts +89 -89
- package/wasm/browser.mjs +569 -569
- package/wasm/browser_bg.mjs +597 -597
- package/wasm/jsql_neo_wasm.d.ts +38 -38
- package/wasm/jsql_neo_wasm.js +520 -520
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +26 -26
- 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;
|