s3db.js 10.0.0 → 10.0.3
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 +2 -1
- package/dist/s3db.cjs.js +2204 -612
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +70 -3
- package/dist/s3db.es.js +2203 -613
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/client.class.js +6 -5
- package/src/plugins/audit.plugin.js +8 -6
- package/src/plugins/backup.plugin.js +383 -106
- package/src/plugins/cache.plugin.js +203 -150
- package/src/plugins/eventual-consistency.plugin.js +609 -206
- package/src/plugins/fulltext.plugin.js +6 -6
- package/src/plugins/index.js +1 -0
- package/src/plugins/metrics.plugin.js +13 -13
- package/src/plugins/queue-consumer.plugin.js +4 -2
- package/src/plugins/replicator.plugin.js +108 -70
- package/src/plugins/replicators/s3db-replicator.class.js +7 -3
- package/src/plugins/replicators/sqs-replicator.class.js +11 -3
- package/src/plugins/s3-queue.plugin.js +776 -0
- package/src/plugins/scheduler.plugin.js +226 -164
- package/src/plugins/state-machine.plugin.js +109 -81
- package/src/resource.class.js +205 -0
- package/src/s3db.d.ts +70 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "path";
|
|
2
|
+
import jsonStableStringify from "json-stable-stringify";
|
|
3
|
+
import crypto from 'crypto';
|
|
2
4
|
|
|
3
|
-
import { sha256 } from "../concerns/crypto.js";
|
|
4
5
|
import Plugin from "./plugin.class.js";
|
|
5
6
|
import S3Cache from "./cache/s3-cache.class.js";
|
|
6
7
|
import MemoryCache from "./cache/memory-cache.class.js";
|
|
@@ -11,26 +12,34 @@ import tryFn from "../concerns/try-fn.js";
|
|
|
11
12
|
export class CachePlugin extends Plugin {
|
|
12
13
|
constructor(options = {}) {
|
|
13
14
|
super(options);
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
15
|
+
|
|
16
|
+
// Clean, consolidated configuration
|
|
17
|
+
this.config = {
|
|
18
|
+
// Driver configuration
|
|
19
|
+
driver: options.driver || 's3',
|
|
20
|
+
config: {
|
|
21
|
+
ttl: options.ttl,
|
|
22
|
+
maxSize: options.maxSize,
|
|
23
|
+
...options.config // Driver-specific config (can override ttl/maxSize)
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Resource filtering
|
|
27
|
+
include: options.include || null, // Array of resource names to cache (null = all)
|
|
28
|
+
exclude: options.exclude || [], // Array of resource names to exclude
|
|
29
|
+
|
|
30
|
+
// Partition settings
|
|
31
|
+
includePartitions: options.includePartitions !== false,
|
|
32
|
+
partitionStrategy: options.partitionStrategy || 'hierarchical',
|
|
33
|
+
partitionAware: options.partitionAware !== false,
|
|
34
|
+
trackUsage: options.trackUsage !== false,
|
|
35
|
+
preloadRelated: options.preloadRelated !== false,
|
|
36
|
+
|
|
37
|
+
// Retry configuration
|
|
38
|
+
retryAttempts: options.retryAttempts || 3,
|
|
39
|
+
retryDelay: options.retryDelay || 100, // ms
|
|
40
|
+
|
|
41
|
+
// Logging
|
|
42
|
+
verbose: options.verbose || false
|
|
34
43
|
};
|
|
35
44
|
}
|
|
36
45
|
|
|
@@ -40,68 +49,29 @@ export class CachePlugin extends Plugin {
|
|
|
40
49
|
|
|
41
50
|
async onSetup() {
|
|
42
51
|
// Initialize cache driver
|
|
43
|
-
if (this.
|
|
52
|
+
if (this.config.driver && typeof this.config.driver === 'object') {
|
|
44
53
|
// Use custom driver instance if provided
|
|
45
|
-
this.driver = this.
|
|
46
|
-
} else if (this.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
...this.legacyConfig.memoryOptions, // Legacy support (lowest priority)
|
|
50
|
-
...this.config, // New config format (medium priority)
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Add global settings if defined (highest priority)
|
|
54
|
-
if (this.ttl !== undefined) {
|
|
55
|
-
driverConfig.ttl = this.ttl;
|
|
56
|
-
}
|
|
57
|
-
if (this.maxSize !== undefined) {
|
|
58
|
-
driverConfig.maxSize = this.maxSize;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
this.driver = new MemoryCache(driverConfig);
|
|
62
|
-
} else if (this.driverName === 'filesystem') {
|
|
63
|
-
// Build driver configuration with proper precedence
|
|
64
|
-
const driverConfig = {
|
|
65
|
-
...this.legacyConfig.filesystemOptions, // Legacy support (lowest priority)
|
|
66
|
-
...this.config, // New config format (medium priority)
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// Add global settings if defined (highest priority)
|
|
70
|
-
if (this.ttl !== undefined) {
|
|
71
|
-
driverConfig.ttl = this.ttl;
|
|
72
|
-
}
|
|
73
|
-
if (this.maxSize !== undefined) {
|
|
74
|
-
driverConfig.maxSize = this.maxSize;
|
|
75
|
-
}
|
|
76
|
-
|
|
54
|
+
this.driver = this.config.driver;
|
|
55
|
+
} else if (this.config.driver === 'memory') {
|
|
56
|
+
this.driver = new MemoryCache(this.config.config);
|
|
57
|
+
} else if (this.config.driver === 'filesystem') {
|
|
77
58
|
// Use partition-aware filesystem cache if enabled
|
|
78
|
-
if (this.partitionAware) {
|
|
59
|
+
if (this.config.partitionAware) {
|
|
79
60
|
this.driver = new PartitionAwareFilesystemCache({
|
|
80
|
-
partitionStrategy: this.partitionStrategy,
|
|
81
|
-
trackUsage: this.trackUsage,
|
|
82
|
-
preloadRelated: this.preloadRelated,
|
|
83
|
-
...
|
|
61
|
+
partitionStrategy: this.config.partitionStrategy,
|
|
62
|
+
trackUsage: this.config.trackUsage,
|
|
63
|
+
preloadRelated: this.config.preloadRelated,
|
|
64
|
+
...this.config.config
|
|
84
65
|
});
|
|
85
66
|
} else {
|
|
86
|
-
this.driver = new FilesystemCache(
|
|
67
|
+
this.driver = new FilesystemCache(this.config.config);
|
|
87
68
|
}
|
|
88
69
|
} else {
|
|
89
|
-
// Default to S3Cache
|
|
90
|
-
|
|
91
|
-
client: this.database.client,
|
|
92
|
-
...this.
|
|
93
|
-
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
// Add global settings if defined (highest priority)
|
|
97
|
-
if (this.ttl !== undefined) {
|
|
98
|
-
driverConfig.ttl = this.ttl;
|
|
99
|
-
}
|
|
100
|
-
if (this.maxSize !== undefined) {
|
|
101
|
-
driverConfig.maxSize = this.maxSize;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
this.driver = new S3Cache(driverConfig);
|
|
70
|
+
// Default to S3Cache
|
|
71
|
+
this.driver = new S3Cache({
|
|
72
|
+
client: this.database.client,
|
|
73
|
+
...this.config.config
|
|
74
|
+
});
|
|
105
75
|
}
|
|
106
76
|
|
|
107
77
|
// Use database hooks instead of method overwriting
|
|
@@ -117,7 +87,9 @@ export class CachePlugin extends Plugin {
|
|
|
117
87
|
installDatabaseHooks() {
|
|
118
88
|
// Hook into resource creation to install cache middleware
|
|
119
89
|
this.database.addHook('afterCreateResource', async ({ resource }) => {
|
|
120
|
-
this.
|
|
90
|
+
if (this.shouldCacheResource(resource.name)) {
|
|
91
|
+
this.installResourceHooksForResource(resource);
|
|
92
|
+
}
|
|
121
93
|
});
|
|
122
94
|
}
|
|
123
95
|
|
|
@@ -132,10 +104,33 @@ export class CachePlugin extends Plugin {
|
|
|
132
104
|
// Remove the old installDatabaseProxy method
|
|
133
105
|
installResourceHooks() {
|
|
134
106
|
for (const resource of Object.values(this.database.resources)) {
|
|
107
|
+
// Check if resource should be cached
|
|
108
|
+
if (!this.shouldCacheResource(resource.name)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
135
111
|
this.installResourceHooksForResource(resource);
|
|
136
112
|
}
|
|
137
113
|
}
|
|
138
114
|
|
|
115
|
+
shouldCacheResource(resourceName) {
|
|
116
|
+
// Skip plugin resources by default (unless explicitly included)
|
|
117
|
+
if (resourceName.startsWith('plg_') && !this.config.include) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check exclude list
|
|
122
|
+
if (this.config.exclude.includes(resourceName)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check include list (if specified)
|
|
127
|
+
if (this.config.include && !this.config.include.includes(resourceName)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
139
134
|
installResourceHooksForResource(resource) {
|
|
140
135
|
if (!this.driver) return;
|
|
141
136
|
|
|
@@ -300,19 +295,28 @@ export class CachePlugin extends Plugin {
|
|
|
300
295
|
|
|
301
296
|
async clearCacheForResource(resource, data) {
|
|
302
297
|
if (!resource.cache) return; // Skip if no cache is available
|
|
303
|
-
|
|
298
|
+
|
|
304
299
|
const keyPrefix = `resource=${resource.name}`;
|
|
305
|
-
|
|
300
|
+
|
|
306
301
|
// For specific operations, only clear relevant cache entries
|
|
307
302
|
if (data && data.id) {
|
|
308
303
|
// Clear specific item caches for this ID
|
|
309
304
|
const itemSpecificMethods = ['get', 'exists', 'content', 'hasContent'];
|
|
310
305
|
for (const method of itemSpecificMethods) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
306
|
+
const specificKey = await this.generateCacheKey(resource, method, { id: data.id });
|
|
307
|
+
const [ok, err] = await this.clearCacheWithRetry(resource.cache, specificKey);
|
|
308
|
+
|
|
309
|
+
if (!ok) {
|
|
310
|
+
this.emit('cache_clear_error', {
|
|
311
|
+
resource: resource.name,
|
|
312
|
+
method,
|
|
313
|
+
id: data.id,
|
|
314
|
+
error: err.message
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (this.config.verbose) {
|
|
318
|
+
console.warn(`[CachePlugin] Failed to clear ${method} cache for ${resource.name}:${data.id}:`, err.message);
|
|
319
|
+
}
|
|
316
320
|
}
|
|
317
321
|
}
|
|
318
322
|
|
|
@@ -321,34 +325,74 @@ export class CachePlugin extends Plugin {
|
|
|
321
325
|
const partitionValues = this.getPartitionValues(data, resource);
|
|
322
326
|
for (const [partitionName, values] of Object.entries(partitionValues)) {
|
|
323
327
|
if (values && Object.keys(values).length > 0 && Object.values(values).some(v => v !== null && v !== undefined)) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
328
|
+
const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
|
|
329
|
+
const [ok, err] = await this.clearCacheWithRetry(resource.cache, partitionKeyPrefix);
|
|
330
|
+
|
|
331
|
+
if (!ok) {
|
|
332
|
+
this.emit('cache_clear_error', {
|
|
333
|
+
resource: resource.name,
|
|
334
|
+
partition: partitionName,
|
|
335
|
+
error: err.message
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
if (this.config.verbose) {
|
|
339
|
+
console.warn(`[CachePlugin] Failed to clear partition cache for ${resource.name}/${partitionName}:`, err.message);
|
|
340
|
+
}
|
|
329
341
|
}
|
|
330
342
|
}
|
|
331
343
|
}
|
|
332
344
|
}
|
|
333
345
|
}
|
|
334
|
-
|
|
346
|
+
|
|
335
347
|
// Clear aggregate caches more broadly to ensure all variants are cleared
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
348
|
+
const [ok, err] = await this.clearCacheWithRetry(resource.cache, keyPrefix);
|
|
349
|
+
|
|
350
|
+
if (!ok) {
|
|
351
|
+
this.emit('cache_clear_error', {
|
|
352
|
+
resource: resource.name,
|
|
353
|
+
type: 'broad',
|
|
354
|
+
error: err.message
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
if (this.config.verbose) {
|
|
358
|
+
console.warn(`[CachePlugin] Failed to clear broad cache for ${resource.name}, trying specific methods:`, err.message);
|
|
359
|
+
}
|
|
360
|
+
|
|
340
361
|
// If broad clearing fails, try specific method clearing
|
|
341
362
|
const aggregateMethods = ['count', 'list', 'listIds', 'getAll', 'page', 'query'];
|
|
342
363
|
for (const method of aggregateMethods) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
364
|
+
// Try multiple key patterns to ensure we catch all variations
|
|
365
|
+
await this.clearCacheWithRetry(resource.cache, `${keyPrefix}/action=${method}`);
|
|
366
|
+
await this.clearCacheWithRetry(resource.cache, `resource=${resource.name}/action=${method}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async clearCacheWithRetry(cache, key) {
|
|
372
|
+
let lastError;
|
|
373
|
+
|
|
374
|
+
for (let attempt = 0; attempt < this.config.retryAttempts; attempt++) {
|
|
375
|
+
const [ok, err] = await tryFn(() => cache.clear(key));
|
|
376
|
+
|
|
377
|
+
if (ok) {
|
|
378
|
+
return [true, null];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
lastError = err;
|
|
382
|
+
|
|
383
|
+
// Don't retry if it's a "not found" error
|
|
384
|
+
if (err.name === 'NoSuchKey' || err.code === 'NoSuchKey') {
|
|
385
|
+
return [true, null]; // Key doesn't exist, that's fine
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Wait before retry (exponential backoff)
|
|
389
|
+
if (attempt < this.config.retryAttempts - 1) {
|
|
390
|
+
const delay = this.config.retryDelay * Math.pow(2, attempt);
|
|
391
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
350
392
|
}
|
|
351
393
|
}
|
|
394
|
+
|
|
395
|
+
return [false, lastError];
|
|
352
396
|
}
|
|
353
397
|
|
|
354
398
|
async generateCacheKey(resource, action, params = {}, partition = null, partitionValues = null) {
|
|
@@ -369,20 +413,21 @@ export class CachePlugin extends Plugin {
|
|
|
369
413
|
|
|
370
414
|
// Add params if they exist
|
|
371
415
|
if (Object.keys(params).length > 0) {
|
|
372
|
-
const paramsHash =
|
|
416
|
+
const paramsHash = this.hashParams(params);
|
|
373
417
|
keyParts.push(paramsHash);
|
|
374
418
|
}
|
|
375
419
|
|
|
376
420
|
return join(...keyParts) + '.json.gz';
|
|
377
421
|
}
|
|
378
422
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
423
|
+
hashParams(params) {
|
|
424
|
+
// Use json-stable-stringify for deterministic serialization
|
|
425
|
+
// Handles nested objects, dates, and maintains consistent key order
|
|
426
|
+
const serialized = jsonStableStringify(params) || 'empty';
|
|
427
|
+
|
|
428
|
+
// Use MD5 for fast non-cryptographic hashing (10x faster than SHA-256)
|
|
429
|
+
// Security not needed here - just need consistent, collision-resistant hash
|
|
430
|
+
return crypto.createHash('md5').update(serialized).digest('hex').substring(0, 16);
|
|
386
431
|
}
|
|
387
432
|
|
|
388
433
|
// Utility methods
|
|
@@ -413,7 +458,7 @@ export class CachePlugin extends Plugin {
|
|
|
413
458
|
throw new Error(`Resource '${resourceName}' not found`);
|
|
414
459
|
}
|
|
415
460
|
|
|
416
|
-
const { includePartitions = true } = options;
|
|
461
|
+
const { includePartitions = true, sampleSize = 100 } = options;
|
|
417
462
|
|
|
418
463
|
// Use partition-aware warming if available
|
|
419
464
|
if (this.driver instanceof PartitionAwareFilesystemCache && resource.warmPartitionCache) {
|
|
@@ -421,60 +466,63 @@ export class CachePlugin extends Plugin {
|
|
|
421
466
|
return await resource.warmPartitionCache(partitionNames, options);
|
|
422
467
|
}
|
|
423
468
|
|
|
424
|
-
//
|
|
425
|
-
|
|
469
|
+
// Use pagination instead of getAll() for efficiency
|
|
470
|
+
let offset = 0;
|
|
471
|
+
const pageSize = 100;
|
|
472
|
+
const sampledRecords = [];
|
|
473
|
+
|
|
474
|
+
// Get sample of records using pagination
|
|
475
|
+
while (sampledRecords.length < sampleSize) {
|
|
476
|
+
const [ok, err, pageResult] = await tryFn(() => resource.page({ offset, size: pageSize }));
|
|
477
|
+
|
|
478
|
+
if (!ok || !pageResult) {
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// page() might return { items, total } or just an array
|
|
483
|
+
const pageItems = Array.isArray(pageResult) ? pageResult : (pageResult.items || []);
|
|
484
|
+
|
|
485
|
+
if (pageItems.length === 0) {
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
sampledRecords.push(...pageItems);
|
|
490
|
+
offset += pageSize;
|
|
491
|
+
|
|
492
|
+
// Cache the page while we're at it
|
|
493
|
+
// (page() call already cached it via middleware)
|
|
494
|
+
}
|
|
426
495
|
|
|
427
496
|
// Warm partition caches if enabled
|
|
428
|
-
if (includePartitions && resource.config.partitions) {
|
|
497
|
+
if (includePartitions && resource.config.partitions && sampledRecords.length > 0) {
|
|
429
498
|
for (const [partitionName, partitionDef] of Object.entries(resource.config.partitions)) {
|
|
430
499
|
if (partitionDef.fields) {
|
|
431
|
-
// Get
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const recordsArray = Array.isArray(allRecords) ? allRecords : [];
|
|
436
|
-
const partitionValues = new Set();
|
|
437
|
-
|
|
438
|
-
for (const record of recordsArray.slice(0, 10)) { // Sample first 10 records
|
|
500
|
+
// Get unique partition values from sample
|
|
501
|
+
const partitionValuesSet = new Set();
|
|
502
|
+
|
|
503
|
+
for (const record of sampledRecords) {
|
|
439
504
|
const values = this.getPartitionValues(record, resource);
|
|
440
505
|
if (values[partitionName]) {
|
|
441
|
-
|
|
506
|
+
partitionValuesSet.add(JSON.stringify(values[partitionName]));
|
|
442
507
|
}
|
|
443
508
|
}
|
|
444
|
-
|
|
509
|
+
|
|
445
510
|
// Warm cache for each partition value
|
|
446
|
-
for (const partitionValueStr of
|
|
511
|
+
for (const partitionValueStr of partitionValuesSet) {
|
|
447
512
|
const partitionValues = JSON.parse(partitionValueStr);
|
|
448
|
-
await resource.list({ partition: partitionName, partitionValues });
|
|
513
|
+
await tryFn(() => resource.list({ partition: partitionName, partitionValues }));
|
|
449
514
|
}
|
|
450
515
|
}
|
|
451
516
|
}
|
|
452
517
|
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Partition-specific methods
|
|
456
|
-
async getPartitionCacheStats(resourceName, partition = null) {
|
|
457
|
-
if (!(this.driver instanceof PartitionAwareFilesystemCache)) {
|
|
458
|
-
throw new Error('Partition cache statistics are only available with PartitionAwareFilesystemCache');
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
return await this.driver.getPartitionStats(resourceName, partition);
|
|
462
|
-
}
|
|
463
518
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
async clearPartitionCache(resourceName, partition, partitionValues = {}) {
|
|
473
|
-
if (!(this.driver instanceof PartitionAwareFilesystemCache)) {
|
|
474
|
-
throw new Error('Partition cache clearing is only available with PartitionAwareFilesystemCache');
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return await this.driver.clearPartition(resourceName, partition, partitionValues);
|
|
519
|
+
return {
|
|
520
|
+
resourceName,
|
|
521
|
+
recordsSampled: sampledRecords.length,
|
|
522
|
+
partitionsWarmed: includePartitions && resource.config.partitions
|
|
523
|
+
? Object.keys(resource.config.partitions).length
|
|
524
|
+
: 0
|
|
525
|
+
};
|
|
478
526
|
}
|
|
479
527
|
|
|
480
528
|
async analyzeCacheUsage() {
|
|
@@ -493,8 +541,13 @@ export class CachePlugin extends Plugin {
|
|
|
493
541
|
}
|
|
494
542
|
};
|
|
495
543
|
|
|
496
|
-
// Analyze each resource
|
|
544
|
+
// Analyze each resource (respect include/exclude filters)
|
|
497
545
|
for (const [resourceName, resource] of Object.entries(this.database.resources)) {
|
|
546
|
+
// Skip resources that shouldn't be cached
|
|
547
|
+
if (!this.shouldCacheResource(resourceName)) {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
|
|
498
551
|
try {
|
|
499
552
|
analysis.resourceStats[resourceName] = await this.driver.getPartitionStats(resourceName);
|
|
500
553
|
analysis.recommendations[resourceName] = await this.driver.getCacheRecommendations(resourceName);
|