s3db.js 7.2.1 → 7.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/s3db.cjs.js +149 -1489
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.es.js +150 -1490
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +149 -1489
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +15 -8
- package/src/behaviors/body-only.js +2 -2
- package/src/behaviors/truncate-data.js +2 -2
- package/src/client.class.js +1 -1
- package/src/database.class.js +1 -1
- package/src/errors.js +1 -1
- package/src/plugins/audit.plugin.js +5 -5
- package/src/plugins/cache/filesystem-cache.class.js +661 -0
- package/src/plugins/cache/index.js +4 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +480 -0
- package/src/plugins/cache.plugin.js +159 -9
- package/src/plugins/consumers/index.js +3 -3
- package/src/plugins/consumers/sqs-consumer.js +2 -2
- package/src/plugins/fulltext.plugin.js +5 -5
- package/src/plugins/metrics.plugin.js +2 -2
- package/src/plugins/queue-consumer.plugin.js +3 -3
- package/src/plugins/replicator.plugin.js +259 -362
- package/src/plugins/replicators/s3db-replicator.class.js +35 -19
- package/src/plugins/replicators/sqs-replicator.class.js +17 -5
- package/src/resource.class.js +14 -14
- package/src/schema.class.js +3 -3
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partition-Aware Filesystem Cache Implementation
|
|
3
|
+
*
|
|
4
|
+
* Extends FilesystemCache to provide intelligent caching for s3db.js partitions.
|
|
5
|
+
* Creates hierarchical directory structures that mirror partition organization.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Basic partition-aware caching
|
|
9
|
+
* const cache = new PartitionAwareFilesystemCache({
|
|
10
|
+
* directory: './cache',
|
|
11
|
+
* partitionStrategy: 'hierarchical',
|
|
12
|
+
* preloadRelated: true
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Advanced configuration with analytics
|
|
17
|
+
* const cache = new PartitionAwareFilesystemCache({
|
|
18
|
+
* directory: './data/cache',
|
|
19
|
+
* partitionStrategy: 'incremental',
|
|
20
|
+
* trackUsage: true,
|
|
21
|
+
* preloadThreshold: 10,
|
|
22
|
+
* maxCacheSize: '1GB'
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import fs from 'fs';
|
|
27
|
+
import { promisify } from 'util';
|
|
28
|
+
import { FilesystemCache } from './filesystem-cache.class.js';
|
|
29
|
+
import tryFn from '../../concerns/try-fn.js';
|
|
30
|
+
|
|
31
|
+
const mkdir = promisify(fs.mkdir);
|
|
32
|
+
const rmdir = promisify(fs.rmdir);
|
|
33
|
+
const readdir = promisify(fs.readdir);
|
|
34
|
+
const stat = promisify(fs.stat);
|
|
35
|
+
const writeFile = promisify(fs.writeFile);
|
|
36
|
+
const readFile = promisify(fs.readFile);
|
|
37
|
+
|
|
38
|
+
export class PartitionAwareFilesystemCache extends FilesystemCache {
|
|
39
|
+
constructor({
|
|
40
|
+
partitionStrategy = 'hierarchical', // 'hierarchical', 'flat', 'temporal'
|
|
41
|
+
trackUsage = true,
|
|
42
|
+
preloadRelated = false,
|
|
43
|
+
preloadThreshold = 10,
|
|
44
|
+
maxCacheSize = null,
|
|
45
|
+
usageStatsFile = 'partition-usage.json',
|
|
46
|
+
...config
|
|
47
|
+
}) {
|
|
48
|
+
super(config);
|
|
49
|
+
|
|
50
|
+
this.partitionStrategy = partitionStrategy;
|
|
51
|
+
this.trackUsage = trackUsage;
|
|
52
|
+
this.preloadRelated = preloadRelated;
|
|
53
|
+
this.preloadThreshold = preloadThreshold;
|
|
54
|
+
this.maxCacheSize = maxCacheSize;
|
|
55
|
+
this.usageStatsFile = path.join(this.directory, usageStatsFile);
|
|
56
|
+
|
|
57
|
+
// Partition usage statistics
|
|
58
|
+
this.partitionUsage = new Map();
|
|
59
|
+
this.loadUsageStats();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate partition-aware cache key
|
|
64
|
+
*/
|
|
65
|
+
_getPartitionCacheKey(resource, action, partition, partitionValues = {}, params = {}) {
|
|
66
|
+
const keyParts = [`resource=${resource}`, `action=${action}`];
|
|
67
|
+
|
|
68
|
+
if (partition && Object.keys(partitionValues).length > 0) {
|
|
69
|
+
keyParts.push(`partition=${partition}`);
|
|
70
|
+
|
|
71
|
+
// Sort fields for consistent keys
|
|
72
|
+
const sortedFields = Object.entries(partitionValues).sort(([a], [b]) => a.localeCompare(b));
|
|
73
|
+
for (const [field, value] of sortedFields) {
|
|
74
|
+
if (value !== null && value !== undefined) {
|
|
75
|
+
keyParts.push(`${field}=${value}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Add params hash if exists
|
|
81
|
+
if (Object.keys(params).length > 0) {
|
|
82
|
+
const paramsStr = Object.entries(params)
|
|
83
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
84
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
85
|
+
.join('|');
|
|
86
|
+
keyParts.push(`params=${Buffer.from(paramsStr).toString('base64')}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return keyParts.join('/') + this.fileExtension;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get directory path for partition cache
|
|
94
|
+
*/
|
|
95
|
+
_getPartitionDirectory(resource, partition, partitionValues = {}) {
|
|
96
|
+
const basePath = path.join(this.directory, `resource=${resource}`);
|
|
97
|
+
|
|
98
|
+
if (!partition) {
|
|
99
|
+
return basePath;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (this.partitionStrategy === 'flat') {
|
|
103
|
+
// Flat structure: all partitions in same level
|
|
104
|
+
return path.join(basePath, 'partitions');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.partitionStrategy === 'temporal' && this._isTemporalPartition(partition, partitionValues)) {
|
|
108
|
+
// Temporal structure: organize by time hierarchy
|
|
109
|
+
return this._getTemporalDirectory(basePath, partition, partitionValues);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Hierarchical structure (default)
|
|
113
|
+
const pathParts = [basePath, `partition=${partition}`];
|
|
114
|
+
|
|
115
|
+
const sortedFields = Object.entries(partitionValues).sort(([a], [b]) => a.localeCompare(b));
|
|
116
|
+
for (const [field, value] of sortedFields) {
|
|
117
|
+
if (value !== null && value !== undefined) {
|
|
118
|
+
pathParts.push(`${field}=${this._sanitizePathValue(value)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return path.join(...pathParts);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Enhanced set method with partition awareness
|
|
127
|
+
*/
|
|
128
|
+
async _set(key, data, options = {}) {
|
|
129
|
+
const { resource, action, partition, partitionValues, params } = options;
|
|
130
|
+
|
|
131
|
+
if (resource && partition) {
|
|
132
|
+
// Use partition-aware storage
|
|
133
|
+
const partitionKey = this._getPartitionCacheKey(resource, action, partition, partitionValues, params);
|
|
134
|
+
const partitionDir = this._getPartitionDirectory(resource, partition, partitionValues);
|
|
135
|
+
|
|
136
|
+
await this._ensureDirectory(partitionDir);
|
|
137
|
+
|
|
138
|
+
const filePath = path.join(partitionDir, this._sanitizeFileName(partitionKey));
|
|
139
|
+
|
|
140
|
+
// Track usage if enabled
|
|
141
|
+
if (this.trackUsage) {
|
|
142
|
+
await this._trackPartitionUsage(resource, partition, partitionValues);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Store with partition metadata
|
|
146
|
+
const partitionData = {
|
|
147
|
+
data,
|
|
148
|
+
metadata: {
|
|
149
|
+
resource,
|
|
150
|
+
partition,
|
|
151
|
+
partitionValues,
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
ttl: this.ttl
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return this._writeFileWithMetadata(filePath, partitionData);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fallback to standard set
|
|
161
|
+
return super._set(key, data);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Enhanced get method with partition awareness
|
|
166
|
+
*/
|
|
167
|
+
async _get(key, options = {}) {
|
|
168
|
+
const { resource, action, partition, partitionValues, params } = options;
|
|
169
|
+
|
|
170
|
+
if (resource && partition) {
|
|
171
|
+
const partitionKey = this._getPartitionCacheKey(resource, action, partition, partitionValues, params);
|
|
172
|
+
const partitionDir = this._getPartitionDirectory(resource, partition, partitionValues);
|
|
173
|
+
const filePath = path.join(partitionDir, this._sanitizeFileName(partitionKey));
|
|
174
|
+
|
|
175
|
+
if (!await this._fileExists(filePath)) {
|
|
176
|
+
// Try preloading related partitions
|
|
177
|
+
if (this.preloadRelated) {
|
|
178
|
+
await this._preloadRelatedPartitions(resource, partition, partitionValues);
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const result = await this._readFileWithMetadata(filePath);
|
|
184
|
+
|
|
185
|
+
if (result && this.trackUsage) {
|
|
186
|
+
await this._trackPartitionUsage(resource, partition, partitionValues);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return result?.data || null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Fallback to standard get
|
|
193
|
+
return super._get(key);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Clear cache for specific partition
|
|
198
|
+
*/
|
|
199
|
+
async clearPartition(resource, partition, partitionValues = {}) {
|
|
200
|
+
const partitionDir = this._getPartitionDirectory(resource, partition, partitionValues);
|
|
201
|
+
|
|
202
|
+
const [ok, err] = await tryFn(async () => {
|
|
203
|
+
if (await this._fileExists(partitionDir)) {
|
|
204
|
+
await rmdir(partitionDir, { recursive: true });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!ok) {
|
|
209
|
+
console.warn(`Failed to clear partition cache: ${err.message}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Clear from usage stats
|
|
213
|
+
const usageKey = this._getUsageKey(resource, partition, partitionValues);
|
|
214
|
+
this.partitionUsage.delete(usageKey);
|
|
215
|
+
await this._saveUsageStats();
|
|
216
|
+
|
|
217
|
+
return ok;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Clear all partitions for a resource
|
|
222
|
+
*/
|
|
223
|
+
async clearResourcePartitions(resource) {
|
|
224
|
+
const resourceDir = path.join(this.directory, `resource=${resource}`);
|
|
225
|
+
|
|
226
|
+
const [ok, err] = await tryFn(async () => {
|
|
227
|
+
if (await this._fileExists(resourceDir)) {
|
|
228
|
+
await rmdir(resourceDir, { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Clear usage stats for resource
|
|
233
|
+
for (const [key] of this.partitionUsage.entries()) {
|
|
234
|
+
if (key.startsWith(`${resource}/`)) {
|
|
235
|
+
this.partitionUsage.delete(key);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
await this._saveUsageStats();
|
|
239
|
+
|
|
240
|
+
return ok;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get partition cache statistics
|
|
245
|
+
*/
|
|
246
|
+
async getPartitionStats(resource, partition = null) {
|
|
247
|
+
const stats = {
|
|
248
|
+
totalFiles: 0,
|
|
249
|
+
totalSize: 0,
|
|
250
|
+
partitions: {},
|
|
251
|
+
usage: {}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const resourceDir = path.join(this.directory, `resource=${resource}`);
|
|
255
|
+
|
|
256
|
+
if (!await this._fileExists(resourceDir)) {
|
|
257
|
+
return stats;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await this._calculateDirectoryStats(resourceDir, stats);
|
|
261
|
+
|
|
262
|
+
// Add usage statistics
|
|
263
|
+
for (const [key, usage] of this.partitionUsage.entries()) {
|
|
264
|
+
if (key.startsWith(`${resource}/`)) {
|
|
265
|
+
const partitionName = key.split('/')[1];
|
|
266
|
+
if (!partition || partitionName === partition) {
|
|
267
|
+
stats.usage[partitionName] = usage;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return stats;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get cache recommendations based on usage patterns
|
|
277
|
+
*/
|
|
278
|
+
async getCacheRecommendations(resource) {
|
|
279
|
+
const recommendations = [];
|
|
280
|
+
const now = Date.now();
|
|
281
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
282
|
+
|
|
283
|
+
for (const [key, usage] of this.partitionUsage.entries()) {
|
|
284
|
+
if (key.startsWith(`${resource}/`)) {
|
|
285
|
+
const [, partition] = key.split('/');
|
|
286
|
+
const daysSinceLastAccess = (now - usage.lastAccess) / dayMs;
|
|
287
|
+
const accessesPerDay = usage.count / Math.max(1, daysSinceLastAccess);
|
|
288
|
+
|
|
289
|
+
let recommendation = 'keep';
|
|
290
|
+
let priority = usage.count;
|
|
291
|
+
|
|
292
|
+
if (daysSinceLastAccess > 30) {
|
|
293
|
+
recommendation = 'archive';
|
|
294
|
+
priority = 0;
|
|
295
|
+
} else if (accessesPerDay < 0.1) {
|
|
296
|
+
recommendation = 'reduce_ttl';
|
|
297
|
+
priority = 1;
|
|
298
|
+
} else if (accessesPerDay > 10) {
|
|
299
|
+
recommendation = 'preload';
|
|
300
|
+
priority = 100;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
recommendations.push({
|
|
304
|
+
partition,
|
|
305
|
+
recommendation,
|
|
306
|
+
priority,
|
|
307
|
+
usage: accessesPerDay,
|
|
308
|
+
lastAccess: new Date(usage.lastAccess).toISOString()
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return recommendations.sort((a, b) => b.priority - a.priority);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Preload frequently accessed partitions
|
|
318
|
+
*/
|
|
319
|
+
async warmPartitionCache(resource, options = {}) {
|
|
320
|
+
const { partitions = [], maxFiles = 1000 } = options;
|
|
321
|
+
let warmedCount = 0;
|
|
322
|
+
|
|
323
|
+
for (const partition of partitions) {
|
|
324
|
+
const usageKey = `${resource}/${partition}`;
|
|
325
|
+
const usage = this.partitionUsage.get(usageKey);
|
|
326
|
+
|
|
327
|
+
if (usage && usage.count >= this.preloadThreshold) {
|
|
328
|
+
// This would integrate with the actual resource to preload data
|
|
329
|
+
console.log(`🔥 Warming cache for ${resource}/${partition} (${usage.count} accesses)`);
|
|
330
|
+
warmedCount++;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (warmedCount >= maxFiles) break;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return warmedCount;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Private helper methods
|
|
340
|
+
|
|
341
|
+
async _trackPartitionUsage(resource, partition, partitionValues) {
|
|
342
|
+
const usageKey = this._getUsageKey(resource, partition, partitionValues);
|
|
343
|
+
const current = this.partitionUsage.get(usageKey) || {
|
|
344
|
+
count: 0,
|
|
345
|
+
firstAccess: Date.now(),
|
|
346
|
+
lastAccess: Date.now()
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
current.count++;
|
|
350
|
+
current.lastAccess = Date.now();
|
|
351
|
+
this.partitionUsage.set(usageKey, current);
|
|
352
|
+
|
|
353
|
+
// Periodically save stats
|
|
354
|
+
if (current.count % 10 === 0) {
|
|
355
|
+
await this._saveUsageStats();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
_getUsageKey(resource, partition, partitionValues) {
|
|
360
|
+
const valuePart = Object.entries(partitionValues)
|
|
361
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
362
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
363
|
+
.join('|');
|
|
364
|
+
|
|
365
|
+
return `${resource}/${partition}/${valuePart}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async _preloadRelatedPartitions(resource, partition, partitionValues) {
|
|
369
|
+
// This would implement intelligent preloading based on:
|
|
370
|
+
// - Temporal patterns (load next/previous time periods)
|
|
371
|
+
// - Geographic patterns (load adjacent regions)
|
|
372
|
+
// - Categorical patterns (load related categories)
|
|
373
|
+
|
|
374
|
+
console.log(`🎯 Preloading related partitions for ${resource}/${partition}`);
|
|
375
|
+
|
|
376
|
+
// Example: for date partitions, preload next day
|
|
377
|
+
if (partitionValues.timestamp || partitionValues.date) {
|
|
378
|
+
// Implementation would go here
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_isTemporalPartition(partition, partitionValues) {
|
|
383
|
+
const temporalFields = ['date', 'timestamp', 'createdAt', 'updatedAt'];
|
|
384
|
+
return Object.keys(partitionValues).some(field =>
|
|
385
|
+
temporalFields.some(tf => field.toLowerCase().includes(tf))
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
_getTemporalDirectory(basePath, partition, partitionValues) {
|
|
390
|
+
// Create year/month/day hierarchy for temporal data
|
|
391
|
+
const dateValue = Object.values(partitionValues)[0];
|
|
392
|
+
if (typeof dateValue === 'string' && dateValue.match(/^\d{4}-\d{2}-\d{2}/)) {
|
|
393
|
+
const [year, month, day] = dateValue.split('-');
|
|
394
|
+
return path.join(basePath, 'temporal', year, month, day);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return path.join(basePath, `partition=${partition}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_sanitizePathValue(value) {
|
|
401
|
+
return String(value).replace(/[<>:"/\\|?*]/g, '_');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
_sanitizeFileName(filename) {
|
|
405
|
+
return filename.replace(/[<>:"/\\|?*]/g, '_');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async _calculateDirectoryStats(dir, stats) {
|
|
409
|
+
const [ok, err, files] = await tryFn(() => readdir(dir));
|
|
410
|
+
if (!ok) return;
|
|
411
|
+
|
|
412
|
+
for (const file of files) {
|
|
413
|
+
const filePath = path.join(dir, file);
|
|
414
|
+
const [statOk, statErr, fileStat] = await tryFn(() => stat(filePath));
|
|
415
|
+
|
|
416
|
+
if (statOk) {
|
|
417
|
+
if (fileStat.isDirectory()) {
|
|
418
|
+
await this._calculateDirectoryStats(filePath, stats);
|
|
419
|
+
} else {
|
|
420
|
+
stats.totalFiles++;
|
|
421
|
+
stats.totalSize += fileStat.size;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async loadUsageStats() {
|
|
428
|
+
const [ok, err, content] = await tryFn(async () => {
|
|
429
|
+
const data = await readFile(this.usageStatsFile, 'utf8');
|
|
430
|
+
return JSON.parse(data);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (ok && content) {
|
|
434
|
+
this.partitionUsage = new Map(Object.entries(content));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async _saveUsageStats() {
|
|
439
|
+
const statsObject = Object.fromEntries(this.partitionUsage);
|
|
440
|
+
|
|
441
|
+
await tryFn(async () => {
|
|
442
|
+
await writeFile(
|
|
443
|
+
this.usageStatsFile,
|
|
444
|
+
JSON.stringify(statsObject, null, 2),
|
|
445
|
+
'utf8'
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async _writeFileWithMetadata(filePath, data) {
|
|
451
|
+
const content = JSON.stringify(data);
|
|
452
|
+
|
|
453
|
+
const [ok, err] = await tryFn(async () => {
|
|
454
|
+
await writeFile(filePath, content, {
|
|
455
|
+
encoding: this.encoding,
|
|
456
|
+
mode: this.fileMode
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (!ok) {
|
|
461
|
+
throw new Error(`Failed to write cache file: ${err.message}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async _readFileWithMetadata(filePath) {
|
|
468
|
+
const [ok, err, content] = await tryFn(async () => {
|
|
469
|
+
return await readFile(filePath, this.encoding);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
if (!ok || !content) return null;
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
return JSON.parse(content);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
return { data: content }; // Fallback for non-JSON data
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
@@ -4,6 +4,8 @@ import { sha256 } from "../concerns/crypto.js";
|
|
|
4
4
|
import Plugin from "./plugin.class.js";
|
|
5
5
|
import S3Cache from "./cache/s3-cache.class.js";
|
|
6
6
|
import MemoryCache from "./cache/memory-cache.class.js";
|
|
7
|
+
import { FilesystemCache } from "./cache/filesystem-cache.class.js";
|
|
8
|
+
import { PartitionAwareFilesystemCache } from "./cache/partition-aware-filesystem-cache.class.js";
|
|
7
9
|
import tryFn from "../concerns/try-fn.js";
|
|
8
10
|
|
|
9
11
|
export class CachePlugin extends Plugin {
|
|
@@ -12,6 +14,10 @@ export class CachePlugin extends Plugin {
|
|
|
12
14
|
this.driver = options.driver;
|
|
13
15
|
this.config = {
|
|
14
16
|
includePartitions: options.includePartitions !== false,
|
|
17
|
+
partitionStrategy: options.partitionStrategy || 'hierarchical',
|
|
18
|
+
partitionAware: options.partitionAware !== false,
|
|
19
|
+
trackUsage: options.trackUsage !== false,
|
|
20
|
+
preloadRelated: options.preloadRelated !== false,
|
|
15
21
|
...options
|
|
16
22
|
};
|
|
17
23
|
}
|
|
@@ -27,6 +33,18 @@ export class CachePlugin extends Plugin {
|
|
|
27
33
|
this.driver = this.config.driver;
|
|
28
34
|
} else if (this.config.driverType === 'memory') {
|
|
29
35
|
this.driver = new MemoryCache(this.config.memoryOptions || {});
|
|
36
|
+
} else if (this.config.driverType === 'filesystem') {
|
|
37
|
+
// Use partition-aware filesystem cache if enabled
|
|
38
|
+
if (this.config.partitionAware) {
|
|
39
|
+
this.driver = new PartitionAwareFilesystemCache({
|
|
40
|
+
partitionStrategy: this.config.partitionStrategy,
|
|
41
|
+
trackUsage: this.config.trackUsage,
|
|
42
|
+
preloadRelated: this.config.preloadRelated,
|
|
43
|
+
...this.config.filesystemOptions
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
this.driver = new FilesystemCache(this.config.filesystemOptions || {});
|
|
47
|
+
}
|
|
30
48
|
} else {
|
|
31
49
|
// Default to S3Cache, sempre passa o client do database
|
|
32
50
|
this.driver = new S3Cache({ client: this.database.client, ...(this.config.s3Options || {}) });
|
|
@@ -89,6 +107,25 @@ export class CachePlugin extends Plugin {
|
|
|
89
107
|
return this.generateCacheKey(resource, action, params, partition, partitionValues);
|
|
90
108
|
};
|
|
91
109
|
|
|
110
|
+
// Add partition-aware methods if using PartitionAwareFilesystemCache
|
|
111
|
+
if (this.driver instanceof PartitionAwareFilesystemCache) {
|
|
112
|
+
resource.clearPartitionCache = async (partition, partitionValues = {}) => {
|
|
113
|
+
return await this.driver.clearPartition(resource.name, partition, partitionValues);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
resource.getPartitionCacheStats = async (partition = null) => {
|
|
117
|
+
return await this.driver.getPartitionStats(resource.name, partition);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
resource.getCacheRecommendations = async () => {
|
|
121
|
+
return await this.driver.getCacheRecommendations(resource.name);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
resource.warmPartitionCache = async (partitions = [], options = {}) => {
|
|
125
|
+
return await this.driver.warmPartitionCache(resource.name, { partitions, ...options });
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
92
129
|
// List of methods to cache
|
|
93
130
|
const cacheMethods = [
|
|
94
131
|
'count', 'listIds', 'getMany', 'getAll', 'page', 'list', 'get'
|
|
@@ -110,14 +147,50 @@ export class CachePlugin extends Plugin {
|
|
|
110
147
|
} else if (method === 'get') {
|
|
111
148
|
key = await resource.cacheKeyFor({ action: method, params: { id: ctx.args[0] } });
|
|
112
149
|
}
|
|
113
|
-
// Try cache
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
150
|
+
// Try cache with partition awareness
|
|
151
|
+
let cached;
|
|
152
|
+
if (this.driver instanceof PartitionAwareFilesystemCache) {
|
|
153
|
+
// Extract partition info for partition-aware cache
|
|
154
|
+
let partition, partitionValues;
|
|
155
|
+
if (method === 'list' || method === 'listIds' || method === 'count' || method === 'page') {
|
|
156
|
+
const args = ctx.args[0] || {};
|
|
157
|
+
partition = args.partition;
|
|
158
|
+
partitionValues = args.partitionValues;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const [ok, err, result] = await tryFn(() => resource.cache._get(key, {
|
|
162
|
+
resource: resource.name,
|
|
163
|
+
action: method,
|
|
164
|
+
partition,
|
|
165
|
+
partitionValues
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
if (ok && result !== null && result !== undefined) return result;
|
|
169
|
+
if (!ok && err.name !== 'NoSuchKey') throw err;
|
|
170
|
+
|
|
171
|
+
// Not cached, call next
|
|
172
|
+
const freshResult = await next();
|
|
173
|
+
|
|
174
|
+
// Store with partition context
|
|
175
|
+
await resource.cache._set(key, freshResult, {
|
|
176
|
+
resource: resource.name,
|
|
177
|
+
action: method,
|
|
178
|
+
partition,
|
|
179
|
+
partitionValues
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return freshResult;
|
|
183
|
+
} else {
|
|
184
|
+
// Standard cache behavior
|
|
185
|
+
const [ok, err, result] = await tryFn(() => resource.cache.get(key));
|
|
186
|
+
if (ok && result !== null && result !== undefined) return result;
|
|
187
|
+
if (!ok && err.name !== 'NoSuchKey') throw err;
|
|
188
|
+
|
|
189
|
+
// Not cached, call next
|
|
190
|
+
const freshResult = await next();
|
|
191
|
+
await resource.cache.set(key, freshResult);
|
|
192
|
+
return freshResult;
|
|
193
|
+
}
|
|
121
194
|
});
|
|
122
195
|
}
|
|
123
196
|
|
|
@@ -240,7 +313,13 @@ export class CachePlugin extends Plugin {
|
|
|
240
313
|
|
|
241
314
|
const { includePartitions = true } = options;
|
|
242
315
|
|
|
243
|
-
//
|
|
316
|
+
// Use partition-aware warming if available
|
|
317
|
+
if (this.driver instanceof PartitionAwareFilesystemCache && resource.warmPartitionCache) {
|
|
318
|
+
const partitionNames = resource.config.partitions ? Object.keys(resource.config.partitions) : [];
|
|
319
|
+
return await resource.warmPartitionCache(partitionNames, options);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Fallback to standard warming
|
|
244
323
|
await resource.getAll();
|
|
245
324
|
|
|
246
325
|
// Warm partition caches if enabled
|
|
@@ -270,6 +349,77 @@ export class CachePlugin extends Plugin {
|
|
|
270
349
|
}
|
|
271
350
|
}
|
|
272
351
|
}
|
|
352
|
+
|
|
353
|
+
// Partition-specific methods
|
|
354
|
+
async getPartitionCacheStats(resourceName, partition = null) {
|
|
355
|
+
if (!(this.driver instanceof PartitionAwareFilesystemCache)) {
|
|
356
|
+
throw new Error('Partition cache statistics are only available with PartitionAwareFilesystemCache');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return await this.driver.getPartitionStats(resourceName, partition);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async getCacheRecommendations(resourceName) {
|
|
363
|
+
if (!(this.driver instanceof PartitionAwareFilesystemCache)) {
|
|
364
|
+
throw new Error('Cache recommendations are only available with PartitionAwareFilesystemCache');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return await this.driver.getCacheRecommendations(resourceName);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async clearPartitionCache(resourceName, partition, partitionValues = {}) {
|
|
371
|
+
if (!(this.driver instanceof PartitionAwareFilesystemCache)) {
|
|
372
|
+
throw new Error('Partition cache clearing is only available with PartitionAwareFilesystemCache');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return await this.driver.clearPartition(resourceName, partition, partitionValues);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async analyzeCacheUsage() {
|
|
379
|
+
if (!(this.driver instanceof PartitionAwareFilesystemCache)) {
|
|
380
|
+
return { message: 'Cache usage analysis is only available with PartitionAwareFilesystemCache' };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const analysis = {
|
|
384
|
+
totalResources: Object.keys(this.database.resources).length,
|
|
385
|
+
resourceStats: {},
|
|
386
|
+
recommendations: {},
|
|
387
|
+
summary: {
|
|
388
|
+
mostUsedPartitions: [],
|
|
389
|
+
leastUsedPartitions: [],
|
|
390
|
+
suggestedOptimizations: []
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Analyze each resource
|
|
395
|
+
for (const [resourceName, resource] of Object.entries(this.database.resources)) {
|
|
396
|
+
try {
|
|
397
|
+
analysis.resourceStats[resourceName] = await this.driver.getPartitionStats(resourceName);
|
|
398
|
+
analysis.recommendations[resourceName] = await this.driver.getCacheRecommendations(resourceName);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
analysis.resourceStats[resourceName] = { error: error.message };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Generate summary
|
|
405
|
+
const allRecommendations = Object.values(analysis.recommendations).flat();
|
|
406
|
+
analysis.summary.mostUsedPartitions = allRecommendations
|
|
407
|
+
.filter(r => r.recommendation === 'preload')
|
|
408
|
+
.sort((a, b) => b.priority - a.priority)
|
|
409
|
+
.slice(0, 5);
|
|
410
|
+
|
|
411
|
+
analysis.summary.leastUsedPartitions = allRecommendations
|
|
412
|
+
.filter(r => r.recommendation === 'archive')
|
|
413
|
+
.slice(0, 5);
|
|
414
|
+
|
|
415
|
+
analysis.summary.suggestedOptimizations = [
|
|
416
|
+
`Consider preloading ${analysis.summary.mostUsedPartitions.length} high-usage partitions`,
|
|
417
|
+
`Archive ${analysis.summary.leastUsedPartitions.length} unused partitions`,
|
|
418
|
+
`Monitor cache hit rates for partition efficiency`
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
return analysis;
|
|
422
|
+
}
|
|
273
423
|
}
|
|
274
424
|
|
|
275
425
|
export default CachePlugin;
|
|
@@ -10,9 +10,9 @@ export const CONSUMER_DRIVERS = {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
* @param {string} driver -
|
|
15
|
-
* @param {Object} config -
|
|
13
|
+
* Creates a consumer instance based on the driver
|
|
14
|
+
* @param {string} driver - Driver type (sqs, rabbitmq, kafka...)
|
|
15
|
+
* @param {Object} config - Consumer configuration
|
|
16
16
|
* @returns {SqsConsumer|RabbitMqConsumer|KafkaConsumer}
|
|
17
17
|
*/
|
|
18
18
|
export function createConsumer(driver, config) {
|