s3db.js 13.6.1 → 14.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -15
- package/dist/s3db.cjs +72446 -39022
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72172 -38790
- package/dist/s3db.es.js.map +1 -1
- package/mcp/lib/base-handler.js +157 -0
- package/mcp/lib/handlers/connection-handler.js +280 -0
- package/mcp/lib/handlers/query-handler.js +533 -0
- package/mcp/lib/handlers/resource-handler.js +428 -0
- package/mcp/lib/tool-registry.js +336 -0
- package/mcp/lib/tools/connection-tools.js +161 -0
- package/mcp/lib/tools/query-tools.js +267 -0
- package/mcp/lib/tools/resource-tools.js +404 -0
- package/package.json +85 -50
- package/src/clients/memory-client.class.js +346 -191
- package/src/clients/memory-storage.class.js +300 -84
- package/src/clients/s3-client.class.js +7 -6
- package/src/concerns/geo-encoding.js +19 -2
- package/src/concerns/ip.js +59 -9
- package/src/concerns/money.js +8 -1
- package/src/concerns/password-hashing.js +49 -8
- package/src/concerns/plugin-storage.js +186 -18
- package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
- package/src/database.class.js +139 -29
- package/src/errors.js +332 -42
- package/src/plugins/api/auth/oidc-auth.js +66 -17
- package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
- package/src/plugins/api/auth/strategies/factory.class.js +63 -0
- package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
- package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
- package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
- package/src/plugins/api/concerns/failban-manager.js +106 -57
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/index.js +168 -40
- package/src/plugins/api/routes/auth-routes.js +198 -30
- package/src/plugins/api/routes/resource-routes.js +19 -4
- package/src/plugins/api/server/health-manager.class.js +163 -0
- package/src/plugins/api/server/middleware-chain.class.js +310 -0
- package/src/plugins/api/server/router.class.js +472 -0
- package/src/plugins/api/server.js +280 -1303
- package/src/plugins/api/utils/custom-routes.js +17 -5
- package/src/plugins/api/utils/guards.js +76 -17
- package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
- package/src/plugins/api/utils/openapi-generator.js +7 -6
- package/src/plugins/audit.plugin.js +30 -8
- package/src/plugins/backup.plugin.js +110 -14
- package/src/plugins/cache/cache.class.js +22 -5
- package/src/plugins/cache/filesystem-cache.class.js +116 -19
- package/src/plugins/cache/memory-cache.class.js +211 -57
- package/src/plugins/cache/multi-tier-cache.class.js +371 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
- package/src/plugins/cache/redis-cache.class.js +552 -0
- package/src/plugins/cache/s3-cache.class.js +17 -8
- package/src/plugins/cache.plugin.js +176 -61
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
- package/src/plugins/cloud-inventory/index.js +29 -8
- package/src/plugins/cloud-inventory/registry.js +64 -42
- package/src/plugins/cloud-inventory.plugin.js +240 -138
- package/src/plugins/concerns/plugin-dependencies.js +54 -0
- package/src/plugins/concerns/resource-names.js +100 -0
- package/src/plugins/consumers/index.js +10 -2
- package/src/plugins/consumers/sqs-consumer.js +12 -2
- package/src/plugins/cookie-farm-suite.plugin.js +278 -0
- package/src/plugins/cookie-farm.errors.js +73 -0
- package/src/plugins/cookie-farm.plugin.js +869 -0
- package/src/plugins/costs.plugin.js +7 -1
- package/src/plugins/eventual-consistency/analytics.js +94 -19
- package/src/plugins/eventual-consistency/config.js +15 -7
- package/src/plugins/eventual-consistency/consolidation.js +29 -11
- package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
- package/src/plugins/eventual-consistency/helpers.js +39 -14
- package/src/plugins/eventual-consistency/install.js +21 -2
- package/src/plugins/eventual-consistency/utils.js +32 -10
- package/src/plugins/fulltext.plugin.js +38 -11
- package/src/plugins/geo.plugin.js +61 -9
- package/src/plugins/identity/concerns/config.js +61 -0
- package/src/plugins/identity/concerns/mfa-manager.js +15 -2
- package/src/plugins/identity/concerns/rate-limit.js +124 -0
- package/src/plugins/identity/concerns/resource-schemas.js +9 -1
- package/src/plugins/identity/concerns/token-generator.js +29 -4
- package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
- package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
- package/src/plugins/identity/drivers/index.js +18 -0
- package/src/plugins/identity/drivers/password-driver.js +122 -0
- package/src/plugins/identity/email-service.js +17 -2
- package/src/plugins/identity/index.js +413 -69
- package/src/plugins/identity/oauth2-server.js +413 -30
- package/src/plugins/identity/oidc-discovery.js +16 -8
- package/src/plugins/identity/rsa-keys.js +115 -35
- package/src/plugins/identity/server.js +166 -45
- package/src/plugins/identity/session-manager.js +53 -7
- package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
- package/src/plugins/identity/ui/routes.js +363 -255
- package/src/plugins/importer/index.js +153 -20
- package/src/plugins/index.js +9 -2
- package/src/plugins/kubernetes-inventory/index.js +6 -0
- package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
- package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
- package/src/plugins/kubernetes-inventory.plugin.js +980 -0
- package/src/plugins/metrics.plugin.js +64 -16
- package/src/plugins/ml/base-model.class.js +25 -15
- package/src/plugins/ml/regression-model.class.js +1 -1
- package/src/plugins/ml.errors.js +57 -25
- package/src/plugins/ml.plugin.js +28 -4
- package/src/plugins/namespace.js +210 -0
- package/src/plugins/plugin.class.js +180 -8
- package/src/plugins/puppeteer/console-monitor.js +729 -0
- package/src/plugins/puppeteer/cookie-manager.js +492 -0
- package/src/plugins/puppeteer/network-monitor.js +816 -0
- package/src/plugins/puppeteer/performance-manager.js +746 -0
- package/src/plugins/puppeteer/proxy-manager.js +478 -0
- package/src/plugins/puppeteer/stealth-manager.js +556 -0
- package/src/plugins/puppeteer.errors.js +81 -0
- package/src/plugins/puppeteer.plugin.js +1327 -0
- package/src/plugins/queue-consumer.plugin.js +69 -14
- package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
- package/src/plugins/recon/concerns/command-runner.js +148 -0
- package/src/plugins/recon/concerns/diff-detector.js +372 -0
- package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
- package/src/plugins/recon/concerns/process-manager.js +338 -0
- package/src/plugins/recon/concerns/report-generator.js +478 -0
- package/src/plugins/recon/concerns/security-analyzer.js +571 -0
- package/src/plugins/recon/concerns/target-normalizer.js +68 -0
- package/src/plugins/recon/config/defaults.js +321 -0
- package/src/plugins/recon/config/resources.js +370 -0
- package/src/plugins/recon/index.js +778 -0
- package/src/plugins/recon/managers/dependency-manager.js +174 -0
- package/src/plugins/recon/managers/scheduler-manager.js +179 -0
- package/src/plugins/recon/managers/storage-manager.js +745 -0
- package/src/plugins/recon/managers/target-manager.js +274 -0
- package/src/plugins/recon/stages/asn-stage.js +314 -0
- package/src/plugins/recon/stages/certificate-stage.js +84 -0
- package/src/plugins/recon/stages/dns-stage.js +107 -0
- package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
- package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
- package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
- package/src/plugins/recon/stages/http-stage.js +89 -0
- package/src/plugins/recon/stages/latency-stage.js +148 -0
- package/src/plugins/recon/stages/massdns-stage.js +302 -0
- package/src/plugins/recon/stages/osint-stage.js +1373 -0
- package/src/plugins/recon/stages/ports-stage.js +169 -0
- package/src/plugins/recon/stages/screenshot-stage.js +94 -0
- package/src/plugins/recon/stages/secrets-stage.js +514 -0
- package/src/plugins/recon/stages/subdomains-stage.js +295 -0
- package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
- package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
- package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
- package/src/plugins/recon/stages/whois-stage.js +349 -0
- package/src/plugins/recon.plugin.js +75 -0
- package/src/plugins/recon.plugin.js.backup +2635 -0
- package/src/plugins/relation.errors.js +87 -14
- package/src/plugins/replicator.plugin.js +514 -137
- package/src/plugins/replicators/base-replicator.class.js +89 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
- package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
- package/src/plugins/replicators/mysql-replicator.class.js +52 -17
- package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
- package/src/plugins/replicators/postgres-replicator.class.js +62 -27
- package/src/plugins/replicators/s3db-replicator.class.js +25 -18
- package/src/plugins/replicators/schema-sync.helper.js +3 -3
- package/src/plugins/replicators/sqs-replicator.class.js +8 -2
- package/src/plugins/replicators/turso-replicator.class.js +23 -3
- package/src/plugins/replicators/webhook-replicator.class.js +42 -4
- package/src/plugins/s3-queue.plugin.js +464 -65
- package/src/plugins/scheduler.plugin.js +20 -6
- package/src/plugins/state-machine.plugin.js +40 -9
- package/src/plugins/tfstate/base-driver.js +28 -4
- package/src/plugins/tfstate/errors.js +65 -10
- package/src/plugins/tfstate/filesystem-driver.js +52 -8
- package/src/plugins/tfstate/index.js +163 -90
- package/src/plugins/tfstate/s3-driver.js +64 -6
- package/src/plugins/ttl.plugin.js +72 -17
- package/src/plugins/vector/distances.js +18 -12
- package/src/plugins/vector/kmeans.js +26 -4
- package/src/resource.class.js +115 -19
- package/src/testing/factory.class.js +20 -3
- package/src/testing/seeder.class.js +7 -1
- package/src/clients/memory-client.md +0 -917
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
|
@@ -5,6 +5,7 @@ export class Cache extends EventEmitter {
|
|
|
5
5
|
constructor(config = {}) {
|
|
6
6
|
super();
|
|
7
7
|
this.config = config;
|
|
8
|
+
this._fallbackStore = new Map();
|
|
8
9
|
}
|
|
9
10
|
// to implement:
|
|
10
11
|
async _set (key, data) {}
|
|
@@ -27,22 +28,25 @@ export class Cache extends EventEmitter {
|
|
|
27
28
|
// generic class methods
|
|
28
29
|
async set(key, data) {
|
|
29
30
|
this.validateKey(key);
|
|
31
|
+
this._fallbackStore.set(key, data);
|
|
30
32
|
await this._set(key, data);
|
|
31
|
-
this.emit("set", data);
|
|
33
|
+
this.emit("set", { key, value: data });
|
|
32
34
|
return data
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
async get(key) {
|
|
36
38
|
this.validateKey(key);
|
|
37
39
|
const data = await this._get(key);
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
+
const value = data !== undefined ? data : this._fallbackStore.get(key);
|
|
41
|
+
this.emit("fetched", { key, value });
|
|
42
|
+
return value;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
async del(key) {
|
|
43
46
|
this.validateKey(key);
|
|
44
47
|
const data = await this._del(key);
|
|
45
|
-
this.
|
|
48
|
+
this._fallbackStore.delete(key);
|
|
49
|
+
this.emit("deleted", { key, value: data });
|
|
46
50
|
return data;
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -52,9 +56,22 @@ export class Cache extends EventEmitter {
|
|
|
52
56
|
|
|
53
57
|
async clear(prefix) {
|
|
54
58
|
const data = await this._clear(prefix);
|
|
55
|
-
|
|
59
|
+
if (!prefix) {
|
|
60
|
+
this._fallbackStore.clear();
|
|
61
|
+
} else {
|
|
62
|
+
for (const key of this._fallbackStore.keys()) {
|
|
63
|
+
if (key.startsWith(prefix)) {
|
|
64
|
+
this._fallbackStore.delete(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
this.emit("clear", { prefix, value: data });
|
|
56
69
|
return data;
|
|
57
70
|
}
|
|
71
|
+
|
|
72
|
+
stats() {
|
|
73
|
+
return typeof this.getStats === 'function' ? this.getStats() : {};
|
|
74
|
+
}
|
|
58
75
|
}
|
|
59
76
|
|
|
60
77
|
export default Cache
|
|
@@ -84,6 +84,7 @@ import path from 'path';
|
|
|
84
84
|
import zlib from 'node:zlib';
|
|
85
85
|
import { Cache } from './cache.class.js';
|
|
86
86
|
import tryFn from '../../concerns/try-fn.js';
|
|
87
|
+
import { CacheError } from '../cache.errors.js';
|
|
87
88
|
|
|
88
89
|
export class FilesystemCache extends Cache {
|
|
89
90
|
constructor({
|
|
@@ -112,7 +113,13 @@ export class FilesystemCache extends Cache {
|
|
|
112
113
|
super(config);
|
|
113
114
|
|
|
114
115
|
if (!directory) {
|
|
115
|
-
throw new
|
|
116
|
+
throw new CacheError('FilesystemCache requires a directory', {
|
|
117
|
+
driver: 'filesystem',
|
|
118
|
+
operation: 'constructor',
|
|
119
|
+
statusCode: 400,
|
|
120
|
+
retriable: false,
|
|
121
|
+
suggestion: 'Pass { directory: "./cache" } or configure a valid cache directory before enabling FilesystemCache.'
|
|
122
|
+
});
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
this.directory = path.resolve(directory);
|
|
@@ -147,14 +154,34 @@ export class FilesystemCache extends Cache {
|
|
|
147
154
|
|
|
148
155
|
this.locks = new Map(); // For file locking
|
|
149
156
|
this.cleanupTimer = null;
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
|
|
158
|
+
// Store _init promise to allow tests to handle initialization errors
|
|
159
|
+
this._initPromise = this._init().catch(err => {
|
|
160
|
+
this._initError = err;
|
|
161
|
+
// Silently capture initialization error - will be thrown on first operation
|
|
162
|
+
});
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
async _init() {
|
|
155
166
|
// Create cache directory if needed
|
|
156
167
|
if (this.createDirectory) {
|
|
157
168
|
await this._ensureDirectory(this.directory);
|
|
169
|
+
} else {
|
|
170
|
+
const [exists] = await tryFn(async () => {
|
|
171
|
+
const stats = await stat(this.directory);
|
|
172
|
+
return stats.isDirectory();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!exists) {
|
|
176
|
+
throw new CacheError(`Cache directory "${this.directory}" does not exist and createDirectory is disabled`, {
|
|
177
|
+
driver: 'filesystem',
|
|
178
|
+
operation: 'init',
|
|
179
|
+
statusCode: 500,
|
|
180
|
+
retriable: false,
|
|
181
|
+
suggestion: 'Create the cache directory manually or enable createDirectory in the FilesystemCache configuration.',
|
|
182
|
+
directory: this.directory
|
|
183
|
+
});
|
|
184
|
+
}
|
|
158
185
|
}
|
|
159
186
|
|
|
160
187
|
// Start cleanup timer if enabled
|
|
@@ -168,12 +195,39 @@ export class FilesystemCache extends Cache {
|
|
|
168
195
|
}
|
|
169
196
|
|
|
170
197
|
async _ensureDirectory(dir) {
|
|
198
|
+
if (!this.createDirectory) {
|
|
199
|
+
const [exists] = await tryFn(async () => {
|
|
200
|
+
const stats = await stat(dir);
|
|
201
|
+
return stats.isDirectory();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (!exists) {
|
|
205
|
+
throw new CacheError(`Cache directory "${dir}" is missing (createDirectory disabled)`, {
|
|
206
|
+
driver: 'filesystem',
|
|
207
|
+
operation: 'ensureDirectory',
|
|
208
|
+
statusCode: 500,
|
|
209
|
+
retriable: false,
|
|
210
|
+
suggestion: 'Create the directory before writing cache entries or enable createDirectory.',
|
|
211
|
+
directory: dir
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
171
217
|
const [ok, err] = await tryFn(async () => {
|
|
172
218
|
await mkdir(dir, { recursive: true });
|
|
173
219
|
});
|
|
174
220
|
|
|
175
221
|
if (!ok && err.code !== 'EEXIST') {
|
|
176
|
-
throw new
|
|
222
|
+
throw new CacheError(`Failed to create cache directory: ${err.message}`, {
|
|
223
|
+
driver: 'filesystem',
|
|
224
|
+
operation: 'ensureDirectory',
|
|
225
|
+
statusCode: 500,
|
|
226
|
+
retriable: false,
|
|
227
|
+
suggestion: 'Check filesystem permissions and ensure the process can create directories.',
|
|
228
|
+
directory: dir,
|
|
229
|
+
original: err
|
|
230
|
+
});
|
|
177
231
|
}
|
|
178
232
|
}
|
|
179
233
|
|
|
@@ -190,43 +244,56 @@ export class FilesystemCache extends Cache {
|
|
|
190
244
|
|
|
191
245
|
async _set(key, data) {
|
|
192
246
|
const filePath = this._getFilePath(key);
|
|
193
|
-
|
|
247
|
+
|
|
194
248
|
try {
|
|
195
249
|
// Prepare data
|
|
196
250
|
let serialized = JSON.stringify(data);
|
|
197
251
|
const originalSize = Buffer.byteLength(serialized, this.encoding);
|
|
198
|
-
|
|
252
|
+
|
|
199
253
|
// Check size limit
|
|
200
254
|
if (originalSize > this.maxFileSize) {
|
|
201
|
-
throw new
|
|
255
|
+
throw new CacheError('Cache data exceeds maximum file size', {
|
|
256
|
+
driver: 'filesystem',
|
|
257
|
+
operation: 'set',
|
|
258
|
+
statusCode: 413,
|
|
259
|
+
retriable: false,
|
|
260
|
+
suggestion: 'Increase maxFileSize or reduce the cached payload size.',
|
|
261
|
+
key,
|
|
262
|
+
size: originalSize,
|
|
263
|
+
maxFileSize: this.maxFileSize
|
|
264
|
+
});
|
|
202
265
|
}
|
|
203
|
-
|
|
266
|
+
|
|
204
267
|
let compressed = false;
|
|
205
268
|
let finalData = serialized;
|
|
206
|
-
|
|
269
|
+
|
|
207
270
|
// Compress if enabled and over threshold
|
|
208
271
|
if (this.enableCompression && originalSize >= this.compressionThreshold) {
|
|
209
272
|
const compressedBuffer = zlib.gzipSync(Buffer.from(serialized, this.encoding));
|
|
210
273
|
finalData = compressedBuffer.toString('base64');
|
|
211
274
|
compressed = true;
|
|
212
275
|
}
|
|
213
|
-
|
|
276
|
+
|
|
277
|
+
// Ensure directory exists before writing
|
|
278
|
+
const dir = path.dirname(filePath);
|
|
279
|
+
await this._ensureDirectory(dir);
|
|
280
|
+
|
|
214
281
|
// Create backup if enabled
|
|
215
282
|
if (this.enableBackup && await this._fileExists(filePath)) {
|
|
216
283
|
const backupPath = filePath + this.backupSuffix;
|
|
217
284
|
await this._copyFile(filePath, backupPath);
|
|
218
285
|
}
|
|
219
|
-
|
|
286
|
+
|
|
220
287
|
// Acquire lock if enabled
|
|
221
288
|
if (this.enableLocking) {
|
|
222
289
|
await this._acquireLock(filePath);
|
|
223
290
|
}
|
|
224
|
-
|
|
291
|
+
|
|
225
292
|
try {
|
|
226
293
|
// Write data
|
|
227
|
-
await writeFile(filePath, finalData, {
|
|
294
|
+
await writeFile(filePath, finalData, {
|
|
228
295
|
encoding: compressed ? 'utf8' : this.encoding,
|
|
229
|
-
mode: this.fileMode
|
|
296
|
+
mode: this.fileMode
|
|
230
297
|
});
|
|
231
298
|
|
|
232
299
|
// Write metadata if enabled
|
|
@@ -270,7 +337,15 @@ export class FilesystemCache extends Cache {
|
|
|
270
337
|
if (this.enableStats) {
|
|
271
338
|
this.stats.errors++;
|
|
272
339
|
}
|
|
273
|
-
throw new
|
|
340
|
+
throw new CacheError(`Failed to set cache key '${key}': ${error.message}`, {
|
|
341
|
+
driver: 'filesystem',
|
|
342
|
+
operation: 'set',
|
|
343
|
+
statusCode: 500,
|
|
344
|
+
retriable: false,
|
|
345
|
+
suggestion: 'Verify filesystem permissions and available disk space.',
|
|
346
|
+
key,
|
|
347
|
+
original: error
|
|
348
|
+
});
|
|
274
349
|
}
|
|
275
350
|
}
|
|
276
351
|
|
|
@@ -422,7 +497,15 @@ export class FilesystemCache extends Cache {
|
|
|
422
497
|
if (this.enableStats) {
|
|
423
498
|
this.stats.errors++;
|
|
424
499
|
}
|
|
425
|
-
throw new
|
|
500
|
+
throw new CacheError(`Failed to delete cache key '${key}': ${error.message}`, {
|
|
501
|
+
driver: 'filesystem',
|
|
502
|
+
operation: 'delete',
|
|
503
|
+
statusCode: 500,
|
|
504
|
+
retriable: false,
|
|
505
|
+
suggestion: 'Ensure cache files are writable and not locked by another process.',
|
|
506
|
+
key,
|
|
507
|
+
original: error
|
|
508
|
+
});
|
|
426
509
|
}
|
|
427
510
|
}
|
|
428
511
|
|
|
@@ -522,7 +605,14 @@ export class FilesystemCache extends Cache {
|
|
|
522
605
|
if (this.enableStats) {
|
|
523
606
|
this.stats.errors++;
|
|
524
607
|
}
|
|
525
|
-
throw new
|
|
608
|
+
throw new CacheError(`Failed to clear cache: ${error.message}`, {
|
|
609
|
+
driver: 'filesystem',
|
|
610
|
+
operation: 'clear',
|
|
611
|
+
statusCode: 500,
|
|
612
|
+
retriable: false,
|
|
613
|
+
suggestion: 'Verify the cache directory is accessible and not in use by another process.',
|
|
614
|
+
original: error
|
|
615
|
+
});
|
|
526
616
|
}
|
|
527
617
|
}
|
|
528
618
|
|
|
@@ -633,7 +723,14 @@ export class FilesystemCache extends Cache {
|
|
|
633
723
|
|
|
634
724
|
while (this.locks.has(lockKey)) {
|
|
635
725
|
if (Date.now() - startTime > this.lockTimeout) {
|
|
636
|
-
throw new
|
|
726
|
+
throw new CacheError(`Lock timeout for file: ${filePath}`, {
|
|
727
|
+
driver: 'filesystem',
|
|
728
|
+
operation: 'acquireLock',
|
|
729
|
+
statusCode: 408,
|
|
730
|
+
retriable: true,
|
|
731
|
+
suggestion: 'Increase lockTimeout or investigate long-running cache writes holding the lock.',
|
|
732
|
+
key: lockKey
|
|
733
|
+
});
|
|
637
734
|
}
|
|
638
735
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
639
736
|
}
|
|
@@ -689,4 +786,4 @@ export class FilesystemCache extends Cache {
|
|
|
689
786
|
}
|
|
690
787
|
}
|
|
691
788
|
|
|
692
|
-
export default FilesystemCache;
|
|
789
|
+
export default FilesystemCache;
|
|
@@ -137,10 +137,31 @@
|
|
|
137
137
|
import zlib from 'node:zlib';
|
|
138
138
|
import os from 'node:os';
|
|
139
139
|
import { Cache } from "./cache.class.js"
|
|
140
|
+
import { CacheError } from '../cache.errors.js';
|
|
140
141
|
|
|
141
142
|
export class MemoryCache extends Cache {
|
|
142
143
|
constructor(config = {}) {
|
|
143
144
|
super(config);
|
|
145
|
+
this.caseSensitive = config.caseSensitive !== undefined ? config.caseSensitive : true;
|
|
146
|
+
this.serializer = typeof config.serializer === 'function' ? config.serializer : JSON.stringify;
|
|
147
|
+
|
|
148
|
+
// Default deserializer with Date reconstruction
|
|
149
|
+
const defaultDeserializer = (str) => {
|
|
150
|
+
return JSON.parse(str, (key, value) => {
|
|
151
|
+
// Reconstruct Date objects from ISO strings
|
|
152
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(value)) {
|
|
153
|
+
return new Date(value);
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
this.deserializer = typeof config.deserializer === 'function' ? config.deserializer : defaultDeserializer;
|
|
160
|
+
this.enableStats = config.enableStats === true;
|
|
161
|
+
this.evictionPolicy = (config.evictionPolicy || 'fifo').toLowerCase();
|
|
162
|
+
if (!['lru', 'fifo'].includes(this.evictionPolicy)) {
|
|
163
|
+
this.evictionPolicy = 'fifo';
|
|
164
|
+
}
|
|
144
165
|
this.cache = {};
|
|
145
166
|
this.meta = {};
|
|
146
167
|
this.maxSize = config.maxSize !== undefined ? config.maxSize : 1000;
|
|
@@ -148,19 +169,26 @@ export class MemoryCache extends Cache {
|
|
|
148
169
|
// Validate that only one memory limit option is used
|
|
149
170
|
if (config.maxMemoryBytes && config.maxMemoryBytes > 0 &&
|
|
150
171
|
config.maxMemoryPercent && config.maxMemoryPercent > 0) {
|
|
151
|
-
throw new
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
throw new CacheError('[MemoryCache] Cannot use both maxMemoryBytes and maxMemoryPercent', {
|
|
173
|
+
driver: 'memory',
|
|
174
|
+
operation: 'constructor',
|
|
175
|
+
statusCode: 400,
|
|
176
|
+
retriable: false,
|
|
177
|
+
suggestion: 'Choose either maxMemoryBytes or maxMemoryPercent to limit memory usage.'
|
|
178
|
+
});
|
|
155
179
|
}
|
|
156
180
|
|
|
157
181
|
// Calculate maxMemoryBytes from percentage if provided
|
|
158
182
|
if (config.maxMemoryPercent && config.maxMemoryPercent > 0) {
|
|
159
183
|
if (config.maxMemoryPercent > 1) {
|
|
160
|
-
throw new
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
184
|
+
throw new CacheError('[MemoryCache] maxMemoryPercent must be between 0 and 1', {
|
|
185
|
+
driver: 'memory',
|
|
186
|
+
operation: 'constructor',
|
|
187
|
+
statusCode: 400,
|
|
188
|
+
retriable: false,
|
|
189
|
+
suggestion: 'Provide a fraction between 0 and 1 (e.g., 0.1 for 10%).',
|
|
190
|
+
maxMemoryPercent: config.maxMemoryPercent
|
|
191
|
+
});
|
|
164
192
|
}
|
|
165
193
|
|
|
166
194
|
const totalMemory = os.totalmem();
|
|
@@ -188,17 +216,82 @@ export class MemoryCache extends Cache {
|
|
|
188
216
|
// Memory tracking
|
|
189
217
|
this.currentMemoryBytes = 0;
|
|
190
218
|
this.evictedDueToMemory = 0;
|
|
219
|
+
|
|
220
|
+
// Monotonic counter for LRU ordering (prevents timestamp collisions)
|
|
221
|
+
this._accessCounter = 0;
|
|
222
|
+
|
|
223
|
+
this.stats = {
|
|
224
|
+
hits: 0,
|
|
225
|
+
misses: 0,
|
|
226
|
+
sets: 0,
|
|
227
|
+
deletes: 0,
|
|
228
|
+
evictions: 0
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_normalizeKey(key) {
|
|
233
|
+
return this.caseSensitive ? key : key.toLowerCase();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_recordStat(type) {
|
|
237
|
+
if (!this.enableStats) return;
|
|
238
|
+
if (Object.prototype.hasOwnProperty.call(this.stats, type)) {
|
|
239
|
+
this.stats[type] += 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_selectEvictionCandidate() {
|
|
244
|
+
const entries = Object.entries(this.meta);
|
|
245
|
+
if (entries.length === 0) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (this.evictionPolicy === 'lru') {
|
|
250
|
+
// Use accessOrder (monotonic counter) for stable LRU ordering
|
|
251
|
+
entries.sort((a, b) => (a[1].accessOrder ?? a[1].insertOrder ?? 0) - (b[1].accessOrder ?? b[1].insertOrder ?? 0));
|
|
252
|
+
} else {
|
|
253
|
+
// Default to FIFO (order of insertion)
|
|
254
|
+
entries.sort((a, b) => (a[1].insertOrder ?? a[1].createdAt ?? a[1].ts) - (b[1].insertOrder ?? b[1].createdAt ?? b[1].ts));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return entries[0]?.[0] || null;
|
|
191
258
|
}
|
|
192
259
|
|
|
193
260
|
async _set(key, data) {
|
|
261
|
+
const normalizedKey = this._normalizeKey(key);
|
|
262
|
+
|
|
263
|
+
// Serialize first (needed for both compression and memory limit checks)
|
|
264
|
+
let serialized;
|
|
265
|
+
try {
|
|
266
|
+
serialized = this.serializer(data);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
throw new CacheError(`Failed to serialize data for key '${key}'`, {
|
|
269
|
+
driver: 'memory',
|
|
270
|
+
operation: 'set',
|
|
271
|
+
statusCode: 500,
|
|
272
|
+
retriable: false,
|
|
273
|
+
suggestion: 'Ensure the custom serializer handles the provided data type.',
|
|
274
|
+
key,
|
|
275
|
+
original: error
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
194
279
|
// Prepare data for storage
|
|
195
|
-
let finalData =
|
|
280
|
+
let finalData = serialized;
|
|
196
281
|
let compressed = false;
|
|
197
282
|
let originalSize = 0;
|
|
198
283
|
let compressedSize = 0;
|
|
199
284
|
|
|
200
|
-
|
|
201
|
-
|
|
285
|
+
if (typeof serialized !== 'string') {
|
|
286
|
+
throw new CacheError('MemoryCache serializer must return a string', {
|
|
287
|
+
driver: 'memory',
|
|
288
|
+
operation: 'set',
|
|
289
|
+
statusCode: 500,
|
|
290
|
+
retriable: false,
|
|
291
|
+
suggestion: 'Update the custom serializer to return a string output.'
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
202
295
|
originalSize = Buffer.byteLength(serialized, 'utf8');
|
|
203
296
|
|
|
204
297
|
// Apply compression if enabled
|
|
@@ -232,105 +325,144 @@ export class MemoryCache extends Cache {
|
|
|
232
325
|
const itemSize = compressed ? compressedSize : originalSize;
|
|
233
326
|
|
|
234
327
|
// If replacing existing key, subtract its old size from current memory
|
|
235
|
-
if (Object.prototype.hasOwnProperty.call(this.cache,
|
|
236
|
-
const oldSize = this.meta[
|
|
328
|
+
if (Object.prototype.hasOwnProperty.call(this.cache, normalizedKey)) {
|
|
329
|
+
const oldSize = this.meta[normalizedKey]?.compressedSize || 0;
|
|
237
330
|
this.currentMemoryBytes -= oldSize;
|
|
238
331
|
}
|
|
239
332
|
|
|
240
333
|
// Memory-aware eviction: Remove items until we have space
|
|
241
334
|
if (this.maxMemoryBytes > 0) {
|
|
335
|
+
// If item is too large to fit even in empty cache, don't cache it
|
|
336
|
+
if (itemSize > this.maxMemoryBytes) {
|
|
337
|
+
this.evictedDueToMemory++;
|
|
338
|
+
return data;
|
|
339
|
+
}
|
|
340
|
+
|
|
242
341
|
while (this.currentMemoryBytes + itemSize > this.maxMemoryBytes && Object.keys(this.cache).length > 0) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.evictedDueToMemory++;
|
|
252
|
-
} else {
|
|
253
|
-
break; // No more items to evict
|
|
254
|
-
}
|
|
342
|
+
const candidate = this._selectEvictionCandidate();
|
|
343
|
+
if (!candidate) break;
|
|
344
|
+
const evictedSize = this.meta[candidate]?.compressedSize || 0;
|
|
345
|
+
delete this.cache[candidate];
|
|
346
|
+
delete this.meta[candidate];
|
|
347
|
+
this.currentMemoryBytes -= evictedSize;
|
|
348
|
+
this.evictedDueToMemory++;
|
|
349
|
+
this._recordStat('evictions');
|
|
255
350
|
}
|
|
256
351
|
}
|
|
257
352
|
|
|
258
|
-
// Item count eviction
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
delete this.
|
|
266
|
-
delete this.meta[oldestKey];
|
|
353
|
+
// Item count eviction: only evict if we're about to exceed maxSize
|
|
354
|
+
// Check length before adding the new item (so maxSize=2 allows 2 items, not 1)
|
|
355
|
+
if (this.maxSize > 0 && !Object.prototype.hasOwnProperty.call(this.cache, normalizedKey) && Object.keys(this.cache).length >= this.maxSize) {
|
|
356
|
+
const candidate = this._selectEvictionCandidate();
|
|
357
|
+
if (candidate) {
|
|
358
|
+
const evictedSize = this.meta[candidate]?.compressedSize || 0;
|
|
359
|
+
delete this.cache[candidate];
|
|
360
|
+
delete this.meta[candidate];
|
|
267
361
|
this.currentMemoryBytes -= evictedSize;
|
|
362
|
+
this._recordStat('evictions');
|
|
268
363
|
}
|
|
269
364
|
}
|
|
270
365
|
|
|
271
366
|
// Store the item
|
|
272
|
-
this.cache[
|
|
273
|
-
|
|
274
|
-
|
|
367
|
+
this.cache[normalizedKey] = finalData;
|
|
368
|
+
const timestamp = Date.now();
|
|
369
|
+
const insertOrder = ++this._accessCounter;
|
|
370
|
+
this.meta[normalizedKey] = {
|
|
371
|
+
ts: timestamp,
|
|
372
|
+
createdAt: timestamp,
|
|
373
|
+
lastAccess: timestamp,
|
|
374
|
+
insertOrder,
|
|
375
|
+
accessOrder: insertOrder,
|
|
275
376
|
compressed,
|
|
276
377
|
originalSize,
|
|
277
|
-
compressedSize: itemSize
|
|
378
|
+
compressedSize: itemSize,
|
|
379
|
+
originalKey: key
|
|
278
380
|
};
|
|
279
381
|
|
|
280
382
|
// Update current memory usage
|
|
281
383
|
this.currentMemoryBytes += itemSize;
|
|
282
384
|
|
|
385
|
+
this._recordStat('sets');
|
|
386
|
+
|
|
283
387
|
return data;
|
|
284
388
|
}
|
|
285
389
|
|
|
286
390
|
async _get(key) {
|
|
287
|
-
|
|
391
|
+
const normalizedKey = this._normalizeKey(key);
|
|
392
|
+
|
|
393
|
+
if (!Object.prototype.hasOwnProperty.call(this.cache, normalizedKey)) {
|
|
394
|
+
this._recordStat('misses');
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
288
397
|
|
|
289
398
|
// Check TTL expiration
|
|
290
399
|
if (this.ttl > 0) {
|
|
291
400
|
const now = Date.now();
|
|
292
|
-
const meta = this.meta[
|
|
293
|
-
if (meta && now - meta.ts > this.ttl) {
|
|
401
|
+
const meta = this.meta[normalizedKey];
|
|
402
|
+
if (meta && now - (meta.createdAt ?? meta.ts) > this.ttl) {
|
|
294
403
|
// Expired - decrement memory before deleting
|
|
295
404
|
const itemSize = meta.compressedSize || 0;
|
|
296
405
|
this.currentMemoryBytes -= itemSize;
|
|
297
|
-
delete this.cache[
|
|
298
|
-
delete this.meta[
|
|
406
|
+
delete this.cache[normalizedKey];
|
|
407
|
+
delete this.meta[normalizedKey];
|
|
408
|
+
this._recordStat('misses');
|
|
299
409
|
return null;
|
|
300
410
|
}
|
|
301
411
|
}
|
|
302
412
|
|
|
303
|
-
const rawData = this.cache[
|
|
304
|
-
|
|
413
|
+
const rawData = this.cache[normalizedKey];
|
|
414
|
+
|
|
305
415
|
// Check if data is compressed
|
|
306
416
|
if (rawData && typeof rawData === 'object' && rawData.__compressed) {
|
|
307
417
|
try {
|
|
308
418
|
// Decompress data
|
|
309
419
|
const compressedBuffer = Buffer.from(rawData.__data, 'base64');
|
|
310
420
|
const decompressed = zlib.gunzipSync(compressedBuffer).toString('utf8');
|
|
311
|
-
|
|
421
|
+
const value = this.deserializer(decompressed);
|
|
422
|
+
this._recordStat('hits');
|
|
423
|
+
if (this.evictionPolicy === 'lru' && this.meta[normalizedKey]) {
|
|
424
|
+
this.meta[normalizedKey].lastAccess = Date.now();
|
|
425
|
+
this.meta[normalizedKey].accessOrder = ++this._accessCounter;
|
|
426
|
+
}
|
|
427
|
+
return value;
|
|
312
428
|
} catch (error) {
|
|
313
429
|
console.warn(`[MemoryCache] Decompression failed for key '${key}':`, error.message);
|
|
314
430
|
// If decompression fails, remove corrupted entry
|
|
315
|
-
delete this.cache[
|
|
316
|
-
delete this.meta[
|
|
431
|
+
delete this.cache[normalizedKey];
|
|
432
|
+
delete this.meta[normalizedKey];
|
|
433
|
+
this._recordStat('misses');
|
|
317
434
|
return null;
|
|
318
435
|
}
|
|
319
436
|
}
|
|
320
437
|
|
|
321
|
-
|
|
322
|
-
|
|
438
|
+
try {
|
|
439
|
+
const value = typeof rawData === 'string' ? this.deserializer(rawData) : rawData;
|
|
440
|
+
this._recordStat('hits');
|
|
441
|
+
if (this.evictionPolicy === 'lru' && this.meta[normalizedKey]) {
|
|
442
|
+
this.meta[normalizedKey].lastAccess = Date.now();
|
|
443
|
+
this.meta[normalizedKey].accessOrder = ++this._accessCounter;
|
|
444
|
+
}
|
|
445
|
+
return value;
|
|
446
|
+
} catch (error) {
|
|
447
|
+
console.warn(`[MemoryCache] Deserialization failed for key '${key}':`, error.message);
|
|
448
|
+
delete this.cache[normalizedKey];
|
|
449
|
+
delete this.meta[normalizedKey];
|
|
450
|
+
this._recordStat('misses');
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
323
453
|
}
|
|
324
454
|
|
|
325
455
|
async _del(key) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
456
|
+
const normalizedKey = this._normalizeKey(key);
|
|
457
|
+
|
|
458
|
+
if (Object.prototype.hasOwnProperty.call(this.cache, normalizedKey)) {
|
|
459
|
+
const itemSize = this.meta[normalizedKey]?.compressedSize || 0;
|
|
329
460
|
this.currentMemoryBytes -= itemSize;
|
|
330
461
|
}
|
|
331
462
|
|
|
332
|
-
delete this.cache[
|
|
333
|
-
delete this.meta[
|
|
463
|
+
delete this.cache[normalizedKey];
|
|
464
|
+
delete this.meta[normalizedKey];
|
|
465
|
+
this._recordStat('deletes');
|
|
334
466
|
return true;
|
|
335
467
|
}
|
|
336
468
|
|
|
@@ -339,12 +471,17 @@ export class MemoryCache extends Cache {
|
|
|
339
471
|
this.cache = {};
|
|
340
472
|
this.meta = {};
|
|
341
473
|
this.currentMemoryBytes = 0; // Reset memory counter
|
|
474
|
+
this.evictedDueToMemory = 0;
|
|
475
|
+
if (this.enableStats) {
|
|
476
|
+
this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0, evictions: 0 };
|
|
477
|
+
}
|
|
342
478
|
return true;
|
|
343
479
|
}
|
|
344
480
|
// Remove only keys that start with the prefix
|
|
345
481
|
const removed = [];
|
|
482
|
+
const normalizedPrefix = this._normalizeKey(prefix);
|
|
346
483
|
for (const key of Object.keys(this.cache)) {
|
|
347
|
-
if (key.startsWith(
|
|
484
|
+
if (key.startsWith(normalizedPrefix)) {
|
|
348
485
|
removed.push(key);
|
|
349
486
|
// Decrement memory usage
|
|
350
487
|
const itemSize = this.meta[key]?.compressedSize || 0;
|
|
@@ -363,7 +500,24 @@ export class MemoryCache extends Cache {
|
|
|
363
500
|
}
|
|
364
501
|
|
|
365
502
|
async keys() {
|
|
366
|
-
return Object.keys(this.cache);
|
|
503
|
+
return Object.keys(this.cache).map(key => this.meta[key]?.originalKey || key);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
getStats() {
|
|
507
|
+
if (!this.enableStats) {
|
|
508
|
+
return { enabled: false };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const total = this.stats.hits + this.stats.misses;
|
|
512
|
+
const hitRate = total > 0 ? this.stats.hits / total : 0;
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
...this.stats,
|
|
516
|
+
memoryUsageBytes: this.currentMemoryBytes,
|
|
517
|
+
maxMemoryBytes: this.maxMemoryBytes,
|
|
518
|
+
evictedDueToMemory: this.evictedDueToMemory,
|
|
519
|
+
hitRate
|
|
520
|
+
};
|
|
367
521
|
}
|
|
368
522
|
|
|
369
523
|
/**
|