s3db.js 13.6.0 → 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 +139 -43
- package/dist/s3db.cjs +72425 -38970
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +72177 -38764
- 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 +94 -49
- 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/opengraph-helper.js +116 -0
- package/src/plugins/api/concerns/route-context.js +601 -0
- package/src/plugins/api/concerns/state-machine.js +288 -0
- package/src/plugins/api/index.js +180 -41
- 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/api/utils/template-engine.js +77 -3
- 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/README.md +126 -126
- 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
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Tier Cache
|
|
3
|
+
*
|
|
4
|
+
* Cascading cache implementation that chains multiple cache drivers (L1 → L2 → L3).
|
|
5
|
+
* Provides automatic promotion of hot data to faster layers and fallback on errors.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Memory → Redis → S3 cascade
|
|
9
|
+
* const cache = new MultiTierCache({
|
|
10
|
+
* drivers: [
|
|
11
|
+
* { driver: memoryInstance, name: 'L1-Memory' },
|
|
12
|
+
* { driver: redisInstance, name: 'L2-Redis' },
|
|
13
|
+
* { driver: s3Instance, name: 'L3-S3' }
|
|
14
|
+
* ],
|
|
15
|
+
* promoteOnHit: true,
|
|
16
|
+
* strategy: 'write-through'
|
|
17
|
+
* });
|
|
18
|
+
*/
|
|
19
|
+
import { Cache } from "./cache.class.js";
|
|
20
|
+
import { CacheError } from "../cache.errors.js";
|
|
21
|
+
|
|
22
|
+
export class MultiTierCache extends Cache {
|
|
23
|
+
constructor({
|
|
24
|
+
drivers = [],
|
|
25
|
+
promoteOnHit = true,
|
|
26
|
+
strategy = 'write-through', // 'write-through' | 'lazy-promotion'
|
|
27
|
+
fallbackOnError = true,
|
|
28
|
+
verbose = false
|
|
29
|
+
}) {
|
|
30
|
+
super();
|
|
31
|
+
|
|
32
|
+
if (!Array.isArray(drivers) || drivers.length === 0) {
|
|
33
|
+
throw new CacheError('MultiTierCache requires at least one driver', {
|
|
34
|
+
operation: 'constructor',
|
|
35
|
+
driver: 'MultiTierCache',
|
|
36
|
+
provided: drivers,
|
|
37
|
+
suggestion: 'Pass drivers array with at least one cache driver instance'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.drivers = drivers.map((d, index) => ({
|
|
42
|
+
instance: d.driver,
|
|
43
|
+
name: d.name || `L${index + 1}`,
|
|
44
|
+
tier: index + 1
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
this.config = {
|
|
48
|
+
promoteOnHit,
|
|
49
|
+
strategy,
|
|
50
|
+
fallbackOnError,
|
|
51
|
+
verbose
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Statistics per tier
|
|
55
|
+
this.stats = {
|
|
56
|
+
enabled: true,
|
|
57
|
+
tiers: this.drivers.map(d => ({
|
|
58
|
+
name: d.name,
|
|
59
|
+
hits: 0,
|
|
60
|
+
misses: 0,
|
|
61
|
+
promotions: 0,
|
|
62
|
+
errors: 0,
|
|
63
|
+
sets: 0
|
|
64
|
+
}))
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Log message if verbose enabled
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
_log(...args) {
|
|
73
|
+
if (this.config.verbose) {
|
|
74
|
+
console.log('[MultiTierCache]', ...args);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get value from cache tiers (cascade L1 → L2 → L3)
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
async _get(key) {
|
|
83
|
+
for (let i = 0; i < this.drivers.length; i++) {
|
|
84
|
+
const tier = this.drivers[i];
|
|
85
|
+
const tierStats = this.stats.tiers[i];
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const value = await tier.instance.get(key);
|
|
89
|
+
|
|
90
|
+
if (value !== null && value !== undefined) {
|
|
91
|
+
// Cache hit!
|
|
92
|
+
tierStats.hits++;
|
|
93
|
+
this._log(`✓ Cache HIT on ${tier.name} for key: ${key}`);
|
|
94
|
+
|
|
95
|
+
// Promote to faster tiers if enabled and not already in L1
|
|
96
|
+
if (this.config.promoteOnHit && i > 0) {
|
|
97
|
+
this._promoteToFasterTiers(key, value, i);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return value;
|
|
101
|
+
} else {
|
|
102
|
+
// Cache miss on this tier, try next
|
|
103
|
+
tierStats.misses++;
|
|
104
|
+
this._log(`✗ Cache MISS on ${tier.name} for key: ${key}`);
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
tierStats.errors++;
|
|
108
|
+
this._log(`⚠ Error on ${tier.name} for key: ${key}`, error.message);
|
|
109
|
+
|
|
110
|
+
if (!this.config.fallbackOnError) {
|
|
111
|
+
throw new CacheError(`Cache get failed on ${tier.name}`, {
|
|
112
|
+
operation: 'get',
|
|
113
|
+
driver: 'MultiTierCache',
|
|
114
|
+
tier: tier.name,
|
|
115
|
+
key,
|
|
116
|
+
cause: error,
|
|
117
|
+
suggestion: 'Enable fallbackOnError to skip failed tiers'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Continue to next tier on error (fallback)
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Miss on all tiers
|
|
127
|
+
this._log(`✗ Cache MISS on ALL tiers for key: ${key}`);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Promote value to faster tiers (L2 hit → write to L1, L3 hit → write to L1+L2)
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
async _promoteToFasterTiers(key, value, hitTierIndex) {
|
|
136
|
+
// Write to all tiers faster than the one where we found the value
|
|
137
|
+
for (let i = 0; i < hitTierIndex; i++) {
|
|
138
|
+
const tier = this.drivers[i];
|
|
139
|
+
const tierStats = this.stats.tiers[i];
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await tier.instance.set(key, value);
|
|
143
|
+
tierStats.promotions++;
|
|
144
|
+
this._log(`↑ Promoted key "${key}" to ${tier.name}`);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
tierStats.errors++;
|
|
147
|
+
this._log(`⚠ Failed to promote key "${key}" to ${tier.name}:`, error.message);
|
|
148
|
+
// Continue promoting to other tiers even if one fails
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Set value in cache tiers
|
|
155
|
+
* @private
|
|
156
|
+
*/
|
|
157
|
+
async _set(key, data) {
|
|
158
|
+
if (this.config.strategy === 'write-through') {
|
|
159
|
+
// Write to ALL tiers immediately
|
|
160
|
+
return this._writeToAllTiers(key, data);
|
|
161
|
+
} else if (this.config.strategy === 'lazy-promotion') {
|
|
162
|
+
// Write only to L1, let promotions handle the rest
|
|
163
|
+
return this._writeToL1Only(key, data);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Write-through strategy: write to all tiers immediately
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
async _writeToAllTiers(key, data) {
|
|
172
|
+
const results = await Promise.allSettled(
|
|
173
|
+
this.drivers.map(async (tier, index) => {
|
|
174
|
+
try {
|
|
175
|
+
await tier.instance.set(key, data);
|
|
176
|
+
this.stats.tiers[index].sets++;
|
|
177
|
+
this._log(`✓ Wrote key "${key}" to ${tier.name}`);
|
|
178
|
+
return { success: true, tier: tier.name };
|
|
179
|
+
} catch (error) {
|
|
180
|
+
this.stats.tiers[index].errors++;
|
|
181
|
+
this._log(`⚠ Failed to write key "${key}" to ${tier.name}:`, error.message);
|
|
182
|
+
return { success: false, tier: tier.name, error };
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Check if at least L1 succeeded
|
|
188
|
+
const l1Success = results[0]?.status === 'fulfilled' && results[0].value.success;
|
|
189
|
+
|
|
190
|
+
if (!l1Success && !this.config.fallbackOnError) {
|
|
191
|
+
throw new CacheError('Failed to write to L1 cache', {
|
|
192
|
+
operation: 'set',
|
|
193
|
+
driver: 'MultiTierCache',
|
|
194
|
+
key,
|
|
195
|
+
results,
|
|
196
|
+
suggestion: 'Enable fallbackOnError or check L1 cache health'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Lazy-promotion strategy: write only to L1
|
|
205
|
+
* @private
|
|
206
|
+
*/
|
|
207
|
+
async _writeToL1Only(key, data) {
|
|
208
|
+
const tier = this.drivers[0];
|
|
209
|
+
const tierStats = this.stats.tiers[0];
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
await tier.instance.set(key, data);
|
|
213
|
+
tierStats.sets++;
|
|
214
|
+
this._log(`✓ Wrote key "${key}" to ${tier.name} (lazy-promotion)`);
|
|
215
|
+
return true;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
tierStats.errors++;
|
|
218
|
+
throw new CacheError(`Failed to write to ${tier.name}`, {
|
|
219
|
+
operation: 'set',
|
|
220
|
+
driver: 'MultiTierCache',
|
|
221
|
+
tier: tier.name,
|
|
222
|
+
key,
|
|
223
|
+
cause: error,
|
|
224
|
+
suggestion: 'Check L1 cache health'
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Delete key from all tiers
|
|
231
|
+
* @private
|
|
232
|
+
*/
|
|
233
|
+
async _del(key) {
|
|
234
|
+
const results = await Promise.allSettled(
|
|
235
|
+
this.drivers.map(async (tier) => {
|
|
236
|
+
try {
|
|
237
|
+
await tier.instance.del(key);
|
|
238
|
+
this._log(`✓ Deleted key "${key}" from ${tier.name}`);
|
|
239
|
+
return { success: true, tier: tier.name };
|
|
240
|
+
} catch (error) {
|
|
241
|
+
this._log(`⚠ Failed to delete key "${key}" from ${tier.name}:`, error.message);
|
|
242
|
+
return { success: false, tier: tier.name, error };
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// Consider successful if at least one tier succeeded
|
|
248
|
+
const anySuccess = results.some(r => r.status === 'fulfilled' && r.value.success);
|
|
249
|
+
|
|
250
|
+
if (!anySuccess && !this.config.fallbackOnError) {
|
|
251
|
+
throw new CacheError('Failed to delete from all cache tiers', {
|
|
252
|
+
operation: 'delete',
|
|
253
|
+
driver: 'MultiTierCache',
|
|
254
|
+
key,
|
|
255
|
+
results,
|
|
256
|
+
suggestion: 'Enable fallbackOnError or check cache health'
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Clear all keys from all tiers
|
|
265
|
+
* @private
|
|
266
|
+
*/
|
|
267
|
+
async _clear(prefix) {
|
|
268
|
+
const results = await Promise.allSettled(
|
|
269
|
+
this.drivers.map(async (tier) => {
|
|
270
|
+
try {
|
|
271
|
+
await tier.instance.clear(prefix);
|
|
272
|
+
this._log(`✓ Cleared ${prefix ? `prefix "${prefix}"` : 'all keys'} from ${tier.name}`);
|
|
273
|
+
return { success: true, tier: tier.name };
|
|
274
|
+
} catch (error) {
|
|
275
|
+
this._log(`⚠ Failed to clear ${tier.name}:`, error.message);
|
|
276
|
+
return { success: false, tier: tier.name, error };
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get total size across all tiers (may have duplicates)
|
|
286
|
+
*/
|
|
287
|
+
async size() {
|
|
288
|
+
let totalSize = 0;
|
|
289
|
+
|
|
290
|
+
for (const tier of this.drivers) {
|
|
291
|
+
try {
|
|
292
|
+
if (typeof tier.instance.size === 'function') {
|
|
293
|
+
const size = await tier.instance.size();
|
|
294
|
+
totalSize += size;
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
this._log(`⚠ Failed to get size from ${tier.name}:`, error.message);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return totalSize;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get all keys from all tiers (deduplicated)
|
|
306
|
+
*/
|
|
307
|
+
async keys() {
|
|
308
|
+
const allKeys = new Set();
|
|
309
|
+
|
|
310
|
+
for (const tier of this.drivers) {
|
|
311
|
+
try {
|
|
312
|
+
if (typeof tier.instance.keys === 'function') {
|
|
313
|
+
const keys = await tier.instance.keys();
|
|
314
|
+
keys.forEach(k => allKeys.add(k));
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
this._log(`⚠ Failed to get keys from ${tier.name}:`, error.message);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return Array.from(allKeys);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get comprehensive statistics
|
|
326
|
+
*/
|
|
327
|
+
getStats() {
|
|
328
|
+
// Calculate totals
|
|
329
|
+
const totals = {
|
|
330
|
+
hits: 0,
|
|
331
|
+
misses: 0,
|
|
332
|
+
promotions: 0,
|
|
333
|
+
errors: 0,
|
|
334
|
+
sets: 0
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
for (const tierStats of this.stats.tiers) {
|
|
338
|
+
totals.hits += tierStats.hits;
|
|
339
|
+
totals.misses += tierStats.misses;
|
|
340
|
+
totals.promotions += tierStats.promotions;
|
|
341
|
+
totals.errors += tierStats.errors;
|
|
342
|
+
totals.sets += tierStats.sets;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const total = totals.hits + totals.misses;
|
|
346
|
+
const hitRate = total > 0 ? totals.hits / total : 0;
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
enabled: true,
|
|
350
|
+
strategy: this.config.strategy,
|
|
351
|
+
promoteOnHit: this.config.promoteOnHit,
|
|
352
|
+
tiers: this.stats.tiers.map(t => {
|
|
353
|
+
const tierTotal = t.hits + t.misses;
|
|
354
|
+
const tierHitRate = tierTotal > 0 ? t.hits / tierTotal : 0;
|
|
355
|
+
return {
|
|
356
|
+
...t,
|
|
357
|
+
hitRate: tierHitRate,
|
|
358
|
+
hitRatePercent: (tierHitRate * 100).toFixed(2) + '%'
|
|
359
|
+
};
|
|
360
|
+
}),
|
|
361
|
+
totals: {
|
|
362
|
+
...totals,
|
|
363
|
+
total,
|
|
364
|
+
hitRate,
|
|
365
|
+
hitRatePercent: (hitRate * 100).toFixed(2) + '%'
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export default MultiTierCache;
|
|
@@ -27,6 +27,7 @@ import fs from 'fs';
|
|
|
27
27
|
import { mkdir, rm as rmdir, readdir, stat, writeFile, readFile } from 'fs/promises';
|
|
28
28
|
import { FilesystemCache } from './filesystem-cache.class.js';
|
|
29
29
|
import tryFn from '../../concerns/try-fn.js';
|
|
30
|
+
import { CacheError } from '../cache.errors.js';
|
|
30
31
|
|
|
31
32
|
export class PartitionAwareFilesystemCache extends FilesystemCache {
|
|
32
33
|
constructor({
|
|
@@ -56,55 +57,68 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
|
|
|
56
57
|
* Generate partition-aware cache key
|
|
57
58
|
*/
|
|
58
59
|
_getPartitionCacheKey(resource, action, partition, partitionValues = {}, params = {}) {
|
|
59
|
-
const
|
|
60
|
+
const segments = [];
|
|
61
|
+
|
|
62
|
+
if (resource) {
|
|
63
|
+
segments.push(`resource=${this._sanitizePathValue(resource)}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const hasPartitionValues = partitionValues && Object.values(partitionValues).some(value => value !== null && value !== undefined && value !== '');
|
|
67
|
+
|
|
68
|
+
if (partition && hasPartitionValues) {
|
|
69
|
+
segments.push(`partition=${this._sanitizePathValue(partition)}`);
|
|
70
|
+
|
|
71
|
+
const sortedFields = Object.entries(partitionValues)
|
|
72
|
+
.filter(([, value]) => value !== null && value !== undefined)
|
|
73
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
60
74
|
|
|
61
|
-
if (partition && Object.keys(partitionValues).length > 0) {
|
|
62
|
-
keyParts.push(`partition=${partition}`);
|
|
63
|
-
|
|
64
|
-
// Sort fields for consistent keys
|
|
65
|
-
const sortedFields = Object.entries(partitionValues).sort(([a], [b]) => a.localeCompare(b));
|
|
66
75
|
for (const [field, value] of sortedFields) {
|
|
67
|
-
|
|
68
|
-
keyParts.push(`${field}=${value}`);
|
|
69
|
-
}
|
|
76
|
+
segments.push(`${field}=${this._sanitizePathValue(value)}`);
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
if (action) {
|
|
81
|
+
segments.push(`action=${this._sanitizePathValue(action)}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (params && Object.keys(params).length > 0) {
|
|
75
85
|
const paramsStr = Object.entries(params)
|
|
76
86
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
77
87
|
.map(([k, v]) => `${k}=${v}`)
|
|
78
88
|
.join('|');
|
|
79
|
-
|
|
89
|
+
segments.push(`params=${this._sanitizePathValue(Buffer.from(paramsStr).toString('base64url'))}`);
|
|
80
90
|
}
|
|
81
91
|
|
|
82
|
-
return
|
|
92
|
+
return segments.join('/');
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
/**
|
|
86
96
|
* Get directory path for partition cache
|
|
87
97
|
*/
|
|
88
98
|
_getPartitionDirectory(resource, partition, partitionValues = {}) {
|
|
89
|
-
const
|
|
99
|
+
const baseSegments = [];
|
|
100
|
+
if (resource) {
|
|
101
|
+
baseSegments.push(`resource=${this._sanitizePathValue(resource)}`);
|
|
102
|
+
}
|
|
90
103
|
|
|
91
104
|
if (!partition) {
|
|
92
|
-
return
|
|
105
|
+
return path.join(this.directory, ...baseSegments);
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
if (this.partitionStrategy === 'flat') {
|
|
96
|
-
|
|
97
|
-
return path.join(basePath, 'partitions');
|
|
109
|
+
return path.join(this.directory, ...baseSegments, 'partitions');
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
if (this.partitionStrategy === 'temporal' && this._isTemporalPartition(partition, partitionValues)) {
|
|
101
|
-
|
|
102
|
-
return this._getTemporalDirectory(basePath, partition, partitionValues);
|
|
113
|
+
return this._getTemporalDirectory(path.join(this.directory, ...baseSegments), partition, partitionValues);
|
|
103
114
|
}
|
|
104
115
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
const pathParts = [
|
|
117
|
+
this.directory,
|
|
118
|
+
...baseSegments,
|
|
119
|
+
`partition=${this._sanitizePathValue(partition)}`
|
|
120
|
+
];
|
|
121
|
+
|
|
108
122
|
const sortedFields = Object.entries(partitionValues).sort(([a], [b]) => a.localeCompare(b));
|
|
109
123
|
for (const [field, value] of sortedFields) {
|
|
110
124
|
if (value !== null && value !== undefined) {
|
|
@@ -122,21 +136,15 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
|
|
|
122
136
|
const { resource, action, partition, partitionValues, params } = options;
|
|
123
137
|
|
|
124
138
|
if (resource && partition) {
|
|
125
|
-
// Use partition-aware storage
|
|
126
139
|
const partitionKey = this._getPartitionCacheKey(resource, action, partition, partitionValues, params);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const filePath = path.join(partitionDir, this._sanitizeFileName(partitionKey));
|
|
132
|
-
|
|
133
|
-
// Track usage if enabled
|
|
140
|
+
|
|
141
|
+
await this._ensurePartitionDirectoryForKey(partitionKey);
|
|
142
|
+
|
|
134
143
|
if (this.trackUsage) {
|
|
135
144
|
await this._trackPartitionUsage(resource, partition, partitionValues);
|
|
136
145
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const partitionData = {
|
|
146
|
+
|
|
147
|
+
const payload = {
|
|
140
148
|
data,
|
|
141
149
|
metadata: {
|
|
142
150
|
resource,
|
|
@@ -146,8 +154,9 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
|
|
|
146
154
|
ttl: this.ttl
|
|
147
155
|
}
|
|
148
156
|
};
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
|
|
158
|
+
await super._set(partitionKey, payload);
|
|
159
|
+
return data;
|
|
151
160
|
}
|
|
152
161
|
|
|
153
162
|
// Fallback to standard set
|
|
@@ -190,24 +199,20 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
|
|
|
190
199
|
|
|
191
200
|
if (resource && partition) {
|
|
192
201
|
const partitionKey = this._getPartitionCacheKey(resource, action, partition, partitionValues, params);
|
|
193
|
-
const
|
|
194
|
-
const filePath = path.join(partitionDir, this._sanitizeFileName(partitionKey));
|
|
202
|
+
const payload = await super._get(partitionKey);
|
|
195
203
|
|
|
196
|
-
if (!
|
|
197
|
-
// Try preloading related partitions
|
|
204
|
+
if (!payload) {
|
|
198
205
|
if (this.preloadRelated) {
|
|
199
206
|
await this._preloadRelatedPartitions(resource, partition, partitionValues);
|
|
200
207
|
}
|
|
201
208
|
return null;
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (result && this.trackUsage) {
|
|
211
|
+
if (this.trackUsage) {
|
|
207
212
|
await this._trackPartitionUsage(resource, partition, partitionValues);
|
|
208
213
|
}
|
|
209
214
|
|
|
210
|
-
return
|
|
215
|
+
return payload?.data ?? null;
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
// Fallback to standard get
|
|
@@ -261,6 +266,48 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
|
|
|
261
266
|
return ok;
|
|
262
267
|
}
|
|
263
268
|
|
|
269
|
+
async _clear(prefix) {
|
|
270
|
+
await super._clear(prefix);
|
|
271
|
+
|
|
272
|
+
if (!prefix) {
|
|
273
|
+
const [entriesOk, , entries] = await tryFn(() => readdir(this.directory));
|
|
274
|
+
if (entriesOk && entries) {
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
const entryPath = path.join(this.directory, entry);
|
|
277
|
+
const [statOk, , entryStat] = await tryFn(() => stat(entryPath));
|
|
278
|
+
if (statOk && entryStat.isDirectory() && entry.startsWith('resource=')) {
|
|
279
|
+
await rmdir(entryPath, { recursive: true }).catch(() => {});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.partitionUsage.clear();
|
|
284
|
+
await this._saveUsageStats();
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const segments = this._splitKeySegments(prefix).map(segment => this._sanitizeFileName(segment));
|
|
289
|
+
if (segments.length > 0) {
|
|
290
|
+
const dirPath = path.join(this.directory, ...segments);
|
|
291
|
+
if (await this._fileExists(dirPath)) {
|
|
292
|
+
await rmdir(dirPath, { recursive: true }).catch(() => {});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const resourceSeg = segments.find(seg => seg.startsWith('resource='));
|
|
296
|
+
const partitionSeg = segments.find(seg => seg.startsWith('partition='));
|
|
297
|
+
const resourceVal = resourceSeg ? resourceSeg.split('=').slice(1).join('=') : '';
|
|
298
|
+
const partitionVal = partitionSeg ? partitionSeg.split('=').slice(1).join('=') : '';
|
|
299
|
+
const usagePrefix = `${resourceVal}/${partitionVal}`;
|
|
300
|
+
for (const key of Array.from(this.partitionUsage.keys())) {
|
|
301
|
+
if (key.startsWith(usagePrefix)) {
|
|
302
|
+
this.partitionUsage.delete(key);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
await this._saveUsageStats();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
264
311
|
/**
|
|
265
312
|
* Get partition cache statistics
|
|
266
313
|
*/
|
|
@@ -378,12 +425,14 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
|
|
|
378
425
|
}
|
|
379
426
|
|
|
380
427
|
_getUsageKey(resource, partition, partitionValues) {
|
|
428
|
+
const sanitizedResource = this._sanitizePathValue(resource || '');
|
|
429
|
+
const sanitizedPartition = this._sanitizePathValue(partition || '');
|
|
381
430
|
const valuePart = Object.entries(partitionValues)
|
|
382
431
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
383
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
432
|
+
.map(([k, v]) => `${k}=${this._sanitizePathValue(v)}`)
|
|
384
433
|
.join('|');
|
|
385
434
|
|
|
386
|
-
return `${
|
|
435
|
+
return `${sanitizedResource}/${sanitizedPartition}/${valuePart}`;
|
|
387
436
|
}
|
|
388
437
|
|
|
389
438
|
async _preloadRelatedPartitions(resource, partition, partitionValues) {
|
|
@@ -426,6 +475,34 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
|
|
|
426
475
|
return filename.replace(/[<>:"/\\|?*]/g, '_');
|
|
427
476
|
}
|
|
428
477
|
|
|
478
|
+
_splitKeySegments(key) {
|
|
479
|
+
return key.split('/').filter(Boolean);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async _ensurePartitionDirectoryForKey(key) {
|
|
483
|
+
const segments = this._splitKeySegments(key);
|
|
484
|
+
if (segments.length <= 1) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const dirPath = path.join(
|
|
489
|
+
this.directory,
|
|
490
|
+
...segments.slice(0, -1).map(segment => this._sanitizeFileName(segment))
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
await this._ensureDirectory(dirPath);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
_getFilePath(key) {
|
|
497
|
+
const segments = this._splitKeySegments(key).map(segment => this._sanitizeFileName(segment));
|
|
498
|
+
const fileName = segments.pop() || this._sanitizeFileName(key);
|
|
499
|
+
const dirPath = segments.length > 0
|
|
500
|
+
? path.join(this.directory, ...segments)
|
|
501
|
+
: this.directory;
|
|
502
|
+
|
|
503
|
+
return path.join(dirPath, `${this.prefix}_${fileName}${this.fileExtension}`);
|
|
504
|
+
}
|
|
505
|
+
|
|
429
506
|
async _calculateDirectoryStats(dir, stats) {
|
|
430
507
|
const [ok, err, files] = await tryFn(() => readdir(dir));
|
|
431
508
|
if (!ok) return;
|
|
@@ -479,7 +556,15 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
|
|
|
479
556
|
});
|
|
480
557
|
|
|
481
558
|
if (!ok) {
|
|
482
|
-
throw new
|
|
559
|
+
throw new CacheError(`Failed to write cache file: ${err.message}`, {
|
|
560
|
+
driver: 'filesystem-partitioned',
|
|
561
|
+
operation: 'writeFileWithMetadata',
|
|
562
|
+
statusCode: 500,
|
|
563
|
+
retriable: false,
|
|
564
|
+
suggestion: 'Check filesystem permissions and disk space for the partition-aware cache directory.',
|
|
565
|
+
filePath,
|
|
566
|
+
original: err
|
|
567
|
+
});
|
|
483
568
|
}
|
|
484
569
|
|
|
485
570
|
return true;
|
|
@@ -498,4 +583,40 @@ export class PartitionAwareFilesystemCache extends FilesystemCache {
|
|
|
498
583
|
return { data: content }; // Fallback for non-JSON data
|
|
499
584
|
}
|
|
500
585
|
}
|
|
501
|
-
|
|
586
|
+
|
|
587
|
+
async size() {
|
|
588
|
+
const keys = await this.keys();
|
|
589
|
+
return keys.length;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async keys() {
|
|
593
|
+
const keys = [];
|
|
594
|
+
await this._collectKeysRecursive(this.directory, [], keys);
|
|
595
|
+
return keys;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async _collectKeysRecursive(currentDir, segments, result) {
|
|
599
|
+
const [ok, err, entries] = await tryFn(() => readdir(currentDir, { withFileTypes: true }));
|
|
600
|
+
if (!ok || !entries) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
for (const entry of entries) {
|
|
605
|
+
const entryPath = path.join(currentDir, entry.name);
|
|
606
|
+
if (entry.isDirectory()) {
|
|
607
|
+
await this._collectKeysRecursive(entryPath, [...segments, entry.name], result);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (!entry.isFile()) continue;
|
|
612
|
+
|
|
613
|
+
if (!entry.name.startsWith(`${this.prefix}_`) || !entry.name.endsWith(this.fileExtension)) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const keyPart = entry.name.slice(this.prefix.length + 1, -this.fileExtension.length);
|
|
618
|
+
const fullSegments = segments.length > 0 ? [...segments, keyPart] : [keyPart];
|
|
619
|
+
result.push(fullSegments.join('/'));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|