s3db.js 11.0.2 → 11.0.4

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.
@@ -17,7 +17,7 @@ import {
17
17
  runConsolidation
18
18
  } from "./consolidation.js";
19
19
  import { runGarbageCollection } from "./garbage-collection.js";
20
- import { updateAnalytics, getAnalytics, getMonthByDay, getDayByHour, getLastNDays, getYearByMonth, getMonthByHour, getTopRecords } from "./analytics.js";
20
+ import { updateAnalytics, getAnalytics, getMonthByDay, getDayByHour, getLastNDays, getYearByMonth, getYearByWeek, getMonthByWeek, getMonthByHour, getTopRecords } from "./analytics.js";
21
21
  import { onInstall, onStart, onStop, watchForResource, completeFieldSetup } from "./install.js";
22
22
 
23
23
  export class EventualConsistencyPlugin extends Plugin {
@@ -125,7 +125,7 @@ export class EventualConsistencyPlugin extends Plugin {
125
125
  originalId,
126
126
  this.transactionResource,
127
127
  this.targetResource,
128
- this.lockResource,
128
+ this.getStorage(),
129
129
  this.analyticsResource,
130
130
  (transactions) => this.updateAnalytics(transactions),
131
131
  this.config
@@ -164,7 +164,7 @@ export class EventualConsistencyPlugin extends Plugin {
164
164
  originalId,
165
165
  this.transactionResource,
166
166
  this.targetResource,
167
- this.lockResource,
167
+ this.getStorage(),
168
168
  (id) => this.consolidateRecord(id),
169
169
  this.config
170
170
  );
@@ -188,14 +188,12 @@ export class EventualConsistencyPlugin extends Plugin {
188
188
  const oldField = this.config.field;
189
189
  const oldTransactionResource = this.transactionResource;
190
190
  const oldTargetResource = this.targetResource;
191
- const oldLockResource = this.lockResource;
192
191
  const oldAnalyticsResource = this.analyticsResource;
193
192
 
194
193
  this.config.resource = handler.resource;
195
194
  this.config.field = handler.field;
196
195
  this.transactionResource = handler.transactionResource;
197
196
  this.targetResource = handler.targetResource;
198
- this.lockResource = handler.lockResource;
199
197
  this.analyticsResource = handler.analyticsResource;
200
198
 
201
199
  const result = await this.consolidateRecord(id);
@@ -205,7 +203,6 @@ export class EventualConsistencyPlugin extends Plugin {
205
203
  this.config.field = oldField;
206
204
  this.transactionResource = oldTransactionResource;
207
205
  this.targetResource = oldTargetResource;
208
- this.lockResource = oldLockResource;
209
206
  this.analyticsResource = oldAnalyticsResource;
210
207
 
211
208
  return result;
@@ -220,14 +217,12 @@ export class EventualConsistencyPlugin extends Plugin {
220
217
  const oldField = this.config.field;
221
218
  const oldTransactionResource = this.transactionResource;
222
219
  const oldTargetResource = this.targetResource;
223
- const oldLockResource = this.lockResource;
224
220
  const oldAnalyticsResource = this.analyticsResource;
225
221
 
226
222
  this.config.resource = handler.resource;
227
223
  this.config.field = handler.field;
228
224
  this.transactionResource = handler.transactionResource;
229
225
  this.targetResource = handler.targetResource;
230
- this.lockResource = handler.lockResource;
231
226
  this.analyticsResource = handler.analyticsResource;
232
227
 
233
228
  const result = await this.consolidateRecord(id);
@@ -236,7 +231,6 @@ export class EventualConsistencyPlugin extends Plugin {
236
231
  this.config.field = oldField;
237
232
  this.transactionResource = oldTransactionResource;
238
233
  this.targetResource = oldTargetResource;
239
- this.lockResource = oldLockResource;
240
234
  this.analyticsResource = oldAnalyticsResource;
241
235
 
242
236
  return result;
@@ -276,14 +270,12 @@ export class EventualConsistencyPlugin extends Plugin {
276
270
  const oldField = this.config.field;
277
271
  const oldTransactionResource = this.transactionResource;
278
272
  const oldTargetResource = this.targetResource;
279
- const oldLockResource = this.lockResource;
280
273
  const oldAnalyticsResource = this.analyticsResource;
281
274
 
282
275
  this.config.resource = handler.resource;
283
276
  this.config.field = handler.field;
284
277
  this.transactionResource = handler.transactionResource;
285
278
  this.targetResource = handler.targetResource;
286
- this.lockResource = handler.lockResource;
287
279
  this.analyticsResource = handler.analyticsResource;
288
280
 
289
281
  const result = await this.recalculateRecord(id);
@@ -292,7 +284,6 @@ export class EventualConsistencyPlugin extends Plugin {
292
284
  this.config.field = oldField;
293
285
  this.transactionResource = oldTransactionResource;
294
286
  this.targetResource = oldTargetResource;
295
- this.lockResource = oldLockResource;
296
287
  this.analyticsResource = oldAnalyticsResource;
297
288
 
298
289
  return result;
@@ -307,14 +298,12 @@ export class EventualConsistencyPlugin extends Plugin {
307
298
  const oldField = this.config.field;
308
299
  const oldTransactionResource = this.transactionResource;
309
300
  const oldTargetResource = this.targetResource;
310
- const oldLockResource = this.lockResource;
311
301
  const oldAnalyticsResource = this.analyticsResource;
312
302
 
313
303
  this.config.resource = resourceName;
314
304
  this.config.field = fieldName;
315
305
  this.transactionResource = handler.transactionResource;
316
306
  this.targetResource = handler.targetResource;
317
- this.lockResource = handler.lockResource;
318
307
  this.analyticsResource = handler.analyticsResource;
319
308
 
320
309
  try {
@@ -329,7 +318,6 @@ export class EventualConsistencyPlugin extends Plugin {
329
318
  this.config.field = oldField;
330
319
  this.transactionResource = oldTransactionResource;
331
320
  this.targetResource = oldTargetResource;
332
- this.lockResource = oldLockResource;
333
321
  this.analyticsResource = oldAnalyticsResource;
334
322
  }
335
323
  }
@@ -343,18 +331,16 @@ export class EventualConsistencyPlugin extends Plugin {
343
331
  const oldField = this.config.field;
344
332
  const oldTransactionResource = this.transactionResource;
345
333
  const oldTargetResource = this.targetResource;
346
- const oldLockResource = this.lockResource;
347
334
 
348
335
  this.config.resource = resourceName;
349
336
  this.config.field = fieldName;
350
337
  this.transactionResource = handler.transactionResource;
351
338
  this.targetResource = handler.targetResource;
352
- this.lockResource = handler.lockResource;
353
339
 
354
340
  try {
355
341
  await runGarbageCollection(
356
342
  this.transactionResource,
357
- this.lockResource,
343
+ this.getStorage(),
358
344
  this.config,
359
345
  (event, data) => this.emit(event, data)
360
346
  );
@@ -363,7 +349,6 @@ export class EventualConsistencyPlugin extends Plugin {
363
349
  this.config.field = oldField;
364
350
  this.transactionResource = oldTransactionResource;
365
351
  this.targetResource = oldTargetResource;
366
- this.lockResource = oldLockResource;
367
352
  }
368
353
  }
369
354
 
@@ -440,6 +425,30 @@ export class EventualConsistencyPlugin extends Plugin {
440
425
  return await getMonthByHour(resourceName, field, month, options, this.fieldHandlers);
441
426
  }
442
427
 
428
+ /**
429
+ * Get analytics for entire year, broken down by weeks
430
+ * @param {string} resourceName - Resource name
431
+ * @param {string} field - Field name
432
+ * @param {number} year - Year (e.g., 2025)
433
+ * @param {Object} options - Options
434
+ * @returns {Promise<Array>} Weekly analytics for the year (up to 53 weeks)
435
+ */
436
+ async getYearByWeek(resourceName, field, year, options = {}) {
437
+ return await getYearByWeek(resourceName, field, year, options, this.fieldHandlers);
438
+ }
439
+
440
+ /**
441
+ * Get analytics for entire month, broken down by weeks
442
+ * @param {string} resourceName - Resource name
443
+ * @param {string} field - Field name
444
+ * @param {string} month - Month in YYYY-MM format
445
+ * @param {Object} options - Options
446
+ * @returns {Promise<Array>} Weekly analytics for the month
447
+ */
448
+ async getMonthByWeek(resourceName, field, month, options = {}) {
449
+ return await getMonthByWeek(resourceName, field, month, options, this.fieldHandlers);
450
+ }
451
+
443
452
  /**
444
453
  * Get top records by volume
445
454
  * @param {string} resourceName - Resource name
@@ -84,8 +84,8 @@ export async function completeFieldSetup(handler, database, config, plugin) {
84
84
  const resourceName = handler.resource;
85
85
  const fieldName = handler.field;
86
86
 
87
- // Create transaction resource with partitions
88
- const transactionResourceName = `${resourceName}_transactions_${fieldName}`;
87
+ // Create transaction resource with partitions (plg_ prefix for plugin resources)
88
+ const transactionResourceName = `plg_${resourceName}_tx_${fieldName}`;
89
89
  const partitionConfig = createPartitionConfig();
90
90
 
91
91
  const [ok, err, transactionResource] = await tryFn(() =>
@@ -100,6 +100,7 @@ export async function completeFieldSetup(handler, database, config, plugin) {
100
100
  timestamp: 'string|required',
101
101
  cohortDate: 'string|required',
102
102
  cohortHour: 'string|required',
103
+ cohortWeek: 'string|optional',
103
104
  cohortMonth: 'string|optional',
104
105
  source: 'string|optional',
105
106
  applied: 'boolean|optional'
@@ -118,27 +119,8 @@ export async function completeFieldSetup(handler, database, config, plugin) {
118
119
 
119
120
  handler.transactionResource = ok ? transactionResource : database.resources[transactionResourceName];
120
121
 
121
- // Create lock resource
122
- const lockResourceName = `${resourceName}_consolidation_locks_${fieldName}`;
123
- const [lockOk, lockErr, lockResource] = await tryFn(() =>
124
- database.createResource({
125
- name: lockResourceName,
126
- attributes: {
127
- id: 'string|required',
128
- lockedAt: 'number|required',
129
- workerId: 'string|optional'
130
- },
131
- behavior: 'body-only',
132
- timestamps: false,
133
- createdBy: 'EventualConsistencyPlugin'
134
- })
135
- );
136
-
137
- if (!lockOk && !database.resources[lockResourceName]) {
138
- throw new Error(`Failed to create lock resource for ${resourceName}.${fieldName}: ${lockErr?.message}`);
139
- }
140
-
141
- handler.lockResource = lockOk ? lockResource : database.resources[lockResourceName];
122
+ // Locks are now managed by PluginStorage with TTL - no Resource needed
123
+ // Lock acquisition is handled via storage.acquireLock() with automatic expiration
142
124
 
143
125
  // Create analytics resource if enabled
144
126
  if (config.enableAnalytics) {
@@ -151,8 +133,9 @@ export async function completeFieldSetup(handler, database, config, plugin) {
151
133
  if (config.verbose) {
152
134
  console.log(
153
135
  `[EventualConsistency] ${resourceName}.${fieldName} - ` +
154
- `Setup complete. Resources: ${transactionResourceName}, ${lockResourceName}` +
155
- `${config.enableAnalytics ? `, ${resourceName}_analytics_${fieldName}` : ''}`
136
+ `Setup complete. Resources: ${transactionResourceName}` +
137
+ `${config.enableAnalytics ? `, plg_${resourceName}_an_${fieldName}` : ''}` +
138
+ ` (locks via PluginStorage TTL)`
156
139
  );
157
140
  }
158
141
  }
@@ -167,7 +150,7 @@ export async function completeFieldSetup(handler, database, config, plugin) {
167
150
  * @returns {Promise<void>}
168
151
  */
169
152
  async function createAnalyticsResource(handler, database, resourceName, fieldName) {
170
- const analyticsResourceName = `${resourceName}_analytics_${fieldName}`;
153
+ const analyticsResourceName = `plg_${resourceName}_an_${fieldName}`;
171
154
 
172
155
  const [ok, err, analyticsResource] = await tryFn(() =>
173
156
  database.createResource({
@@ -34,6 +34,11 @@ export function createPartitionConfig() {
34
34
  cohortDate: 'string'
35
35
  }
36
36
  },
37
+ byWeek: {
38
+ fields: {
39
+ cohortWeek: 'string'
40
+ }
41
+ },
37
42
  byMonth: {
38
43
  fields: {
39
44
  cohortMonth: 'string'
@@ -54,6 +54,7 @@ export async function createTransaction(handler, data, config) {
54
54
  timestamp: now.toISOString(),
55
55
  cohortDate: cohortInfo.date,
56
56
  cohortHour: cohortInfo.hour,
57
+ cohortWeek: cohortInfo.week,
57
58
  cohortMonth: cohortInfo.month,
58
59
  source: data.source || 'unknown',
59
60
  applied: false
@@ -64,12 +64,42 @@ export function getTimezoneOffset(timezone, verbose = false) {
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * Calculate ISO 8601 week number for a date
69
+ * @param {Date} date - Date to get week number for
70
+ * @returns {Object} Year and week number { year, week }
71
+ */
72
+ function getISOWeek(date) {
73
+ // Copy date to avoid mutating original
74
+ const target = new Date(date.valueOf());
75
+
76
+ // ISO week starts on Monday (day 1)
77
+ // Find Thursday of this week (ISO week contains Jan 4th)
78
+ const dayNr = (date.getUTCDay() + 6) % 7; // Make Monday = 0 (use UTC)
79
+ target.setUTCDate(target.getUTCDate() - dayNr + 3); // Thursday of this week
80
+
81
+ // Get first Thursday of the year (use UTC)
82
+ const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1));
83
+ const firstThursday = new Date(yearStart.valueOf());
84
+ if (yearStart.getUTCDay() !== 4) {
85
+ firstThursday.setUTCDate(yearStart.getUTCDate() + ((4 - yearStart.getUTCDay()) + 7) % 7);
86
+ }
87
+
88
+ // Calculate week number
89
+ const weekNumber = 1 + Math.round((target - firstThursday) / 604800000);
90
+
91
+ return {
92
+ year: target.getUTCFullYear(),
93
+ week: weekNumber
94
+ };
95
+ }
96
+
67
97
  /**
68
98
  * Get cohort information for a date
69
99
  * @param {Date} date - Date to get cohort info for
70
100
  * @param {string} timezone - IANA timezone name
71
101
  * @param {boolean} verbose - Whether to log warnings
72
- * @returns {Object} Cohort information (date, hour, month)
102
+ * @returns {Object} Cohort information (date, hour, week, month)
73
103
  */
74
104
  export function getCohortInfo(date, timezone, verbose = false) {
75
105
  // Simple timezone offset calculation
@@ -81,9 +111,14 @@ export function getCohortInfo(date, timezone, verbose = false) {
81
111
  const day = String(localDate.getDate()).padStart(2, '0');
82
112
  const hour = String(localDate.getHours()).padStart(2, '0');
83
113
 
114
+ // Calculate ISO week
115
+ const { year: weekYear, week: weekNumber } = getISOWeek(localDate);
116
+ const week = `${weekYear}-W${String(weekNumber).padStart(2, '0')}`;
117
+
84
118
  return {
85
119
  date: `${year}-${month}-${day}`,
86
120
  hour: `${year}-${month}-${day}T${hour}`, // ISO-like format for hour partition
121
+ week: week, // ISO 8601 week format (e.g., '2025-W42')
87
122
  month: `${year}-${month}`
88
123
  };
89
124
  }
@@ -11,6 +11,8 @@ export class FullTextPlugin extends Plugin {
11
11
  ...options
12
12
  };
13
13
  this.indexes = new Map(); // In-memory index for simplicity
14
+ this.dirtyIndexes = new Set(); // Track changed index keys for incremental saves
15
+ this.deletedIndexes = new Set(); // Track deleted index keys
14
16
  }
15
17
 
16
18
  async onInstall() {
@@ -26,7 +28,11 @@ export class FullTextPlugin extends Plugin {
26
28
  recordIds: 'json|required', // Array of record IDs containing this word
27
29
  count: 'number|required',
28
30
  lastUpdated: 'string|required'
29
- }
31
+ },
32
+ partitions: {
33
+ byResource: { fields: { resourceName: 'string' } }
34
+ },
35
+ behavior: 'body-overflow'
30
36
  }));
31
37
  this.indexResource = ok ? indexResource : this.database.resources.fulltext_indexes;
32
38
 
@@ -69,26 +75,71 @@ export class FullTextPlugin extends Plugin {
69
75
 
70
76
  async saveIndexes() {
71
77
  if (!this.indexResource) return;
72
-
78
+
73
79
  const [ok, err] = await tryFn(async () => {
74
- // Clear existing indexes
75
- const existingIndexes = await this.indexResource.getAll();
76
- for (const index of existingIndexes) {
77
- await this.indexResource.delete(index.id);
80
+ // Delete indexes that were removed
81
+ for (const key of this.deletedIndexes) {
82
+ // Find and delete the index record using partition-aware query
83
+ const [resourceName] = key.split(':');
84
+ const [queryOk, queryErr, results] = await tryFn(() =>
85
+ this.indexResource.query({ resourceName })
86
+ );
87
+
88
+ if (queryOk && results) {
89
+ for (const index of results) {
90
+ const indexKey = `${index.resourceName}:${index.fieldName}:${index.word}`;
91
+ if (indexKey === key) {
92
+ await this.indexResource.delete(index.id);
93
+ }
94
+ }
95
+ }
78
96
  }
79
- // Save current indexes
80
- for (const [key, data] of this.indexes.entries()) {
97
+
98
+ // Save or update dirty indexes
99
+ for (const key of this.dirtyIndexes) {
81
100
  const [resourceName, fieldName, word] = key.split(':');
82
- await this.indexResource.insert({
83
- id: `index-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
84
- resourceName,
85
- fieldName,
86
- word,
87
- recordIds: data.recordIds,
88
- count: data.count,
89
- lastUpdated: new Date().toISOString()
90
- });
101
+ const data = this.indexes.get(key);
102
+
103
+ if (!data) continue; // Skip if index was deleted
104
+
105
+ // Try to find existing index record
106
+ const [queryOk, queryErr, results] = await tryFn(() =>
107
+ this.indexResource.query({ resourceName })
108
+ );
109
+
110
+ let existingRecord = null;
111
+ if (queryOk && results) {
112
+ existingRecord = results.find(
113
+ (index) => index.resourceName === resourceName &&
114
+ index.fieldName === fieldName &&
115
+ index.word === word
116
+ );
117
+ }
118
+
119
+ if (existingRecord) {
120
+ // Update existing record
121
+ await this.indexResource.update(existingRecord.id, {
122
+ recordIds: data.recordIds,
123
+ count: data.count,
124
+ lastUpdated: new Date().toISOString()
125
+ });
126
+ } else {
127
+ // Insert new record
128
+ await this.indexResource.insert({
129
+ id: `index-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
130
+ resourceName,
131
+ fieldName,
132
+ word,
133
+ recordIds: data.recordIds,
134
+ count: data.count,
135
+ lastUpdated: new Date().toISOString()
136
+ });
137
+ }
91
138
  }
139
+
140
+ // Clear tracking sets after successful save
141
+ this.dirtyIndexes.clear();
142
+ this.deletedIndexes.clear();
92
143
  });
93
144
  }
94
145
 
@@ -195,21 +246,22 @@ export class FullTextPlugin extends Plugin {
195
246
  }
196
247
 
197
248
  const words = this.tokenize(fieldValue);
198
-
249
+
199
250
  for (const word of words) {
200
251
  if (word.length < this.config.minWordLength) {
201
252
  continue;
202
253
  }
203
-
254
+
204
255
  const key = `${resourceName}:${fieldName}:${word.toLowerCase()}`;
205
256
  const existing = this.indexes.get(key) || { recordIds: [], count: 0 };
206
-
257
+
207
258
  if (!existing.recordIds.includes(recordId)) {
208
259
  existing.recordIds.push(recordId);
209
260
  existing.count = existing.recordIds.length;
210
261
  }
211
-
262
+
212
263
  this.indexes.set(key, existing);
264
+ this.dirtyIndexes.add(key); // Mark as dirty for incremental save
213
265
  }
214
266
  }
215
267
  }
@@ -221,11 +273,13 @@ export class FullTextPlugin extends Plugin {
221
273
  if (index > -1) {
222
274
  data.recordIds.splice(index, 1);
223
275
  data.count = data.recordIds.length;
224
-
276
+
225
277
  if (data.recordIds.length === 0) {
226
278
  this.indexes.delete(key);
279
+ this.deletedIndexes.add(key); // Track deletion for incremental save
227
280
  } else {
228
281
  this.indexes.set(key, data);
282
+ this.dirtyIndexes.add(key); // Mark as dirty for incremental save
229
283
  }
230
284
  }
231
285
  }
@@ -47,8 +47,13 @@ export class MetricsPlugin extends Plugin {
47
47
  errors: 'number|required',
48
48
  avgTime: 'number|required',
49
49
  timestamp: 'string|required',
50
- metadata: 'json'
51
- }
50
+ metadata: 'json',
51
+ createdAt: 'string|required' // YYYY-MM-DD for partitioning
52
+ },
53
+ partitions: {
54
+ byDate: { fields: { createdAt: 'string|maxlength:10' } }
55
+ },
56
+ behavior: 'body-overflow'
52
57
  }));
53
58
  this.metricsResource = ok1 ? metricsResource : this.database.resources.plg_metrics;
54
59
 
@@ -60,8 +65,13 @@ export class MetricsPlugin extends Plugin {
60
65
  operation: 'string|required',
61
66
  error: 'string|required',
62
67
  timestamp: 'string|required',
63
- metadata: 'json'
64
- }
68
+ metadata: 'json',
69
+ createdAt: 'string|required' // YYYY-MM-DD for partitioning
70
+ },
71
+ partitions: {
72
+ byDate: { fields: { createdAt: 'string|maxlength:10' } }
73
+ },
74
+ behavior: 'body-overflow'
65
75
  }));
66
76
  this.errorsResource = ok2 ? errorsResource : this.database.resources.plg_error_logs;
67
77
 
@@ -73,8 +83,13 @@ export class MetricsPlugin extends Plugin {
73
83
  operation: 'string|required',
74
84
  duration: 'number|required',
75
85
  timestamp: 'string|required',
76
- metadata: 'json'
77
- }
86
+ metadata: 'json',
87
+ createdAt: 'string|required' // YYYY-MM-DD for partitioning
88
+ },
89
+ partitions: {
90
+ byDate: { fields: { createdAt: 'string|maxlength:10' } }
91
+ },
92
+ behavior: 'body-overflow'
78
93
  }));
79
94
  this.performanceResource = ok3 ? performanceResource : this.database.resources.plg_performance_logs;
80
95
  });
@@ -359,6 +374,9 @@ export class MetricsPlugin extends Plugin {
359
374
  }
360
375
 
361
376
  // Flush operation metrics
377
+ const now = new Date();
378
+ const createdAt = now.toISOString().slice(0, 10); // YYYY-MM-DD
379
+
362
380
  for (const [operation, data] of Object.entries(this.metrics.operations)) {
363
381
  if (data.count > 0) {
364
382
  await this.metricsResource.insert({
@@ -370,7 +388,8 @@ export class MetricsPlugin extends Plugin {
370
388
  totalTime: data.totalTime,
371
389
  errors: data.errors,
372
390
  avgTime: data.count > 0 ? data.totalTime / data.count : 0,
373
- timestamp: new Date().toISOString(),
391
+ timestamp: now.toISOString(),
392
+ createdAt,
374
393
  metadata
375
394
  });
376
395
  }
@@ -389,7 +408,8 @@ export class MetricsPlugin extends Plugin {
389
408
  totalTime: data.totalTime,
390
409
  errors: data.errors,
391
410
  avgTime: data.count > 0 ? data.totalTime / data.count : 0,
392
- timestamp: new Date().toISOString(),
411
+ timestamp: now.toISOString(),
412
+ createdAt,
393
413
  metadata: resourceMetadata
394
414
  });
395
415
  }
@@ -405,6 +425,7 @@ export class MetricsPlugin extends Plugin {
405
425
  operation: perf.operation,
406
426
  duration: perf.duration,
407
427
  timestamp: perf.timestamp,
428
+ createdAt: perf.timestamp.slice(0, 10), // YYYY-MM-DD from timestamp
408
429
  metadata: perfMetadata
409
430
  });
410
431
  }
@@ -420,6 +441,7 @@ export class MetricsPlugin extends Plugin {
420
441
  error: error.error,
421
442
  stack: error.stack,
422
443
  timestamp: error.timestamp,
444
+ createdAt: error.timestamp.slice(0, 10), // YYYY-MM-DD from timestamp
423
445
  metadata: errorMetadata
424
446
  });
425
447
  }
@@ -597,28 +619,56 @@ export class MetricsPlugin extends Plugin {
597
619
  async cleanupOldData() {
598
620
  const cutoffDate = new Date();
599
621
  cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
622
+ const cutoffDateStr = cutoffDate.toISOString().slice(0, 10); // YYYY-MM-DD
623
+
624
+ // Generate list of dates to delete (all dates before cutoff)
625
+ const datesToDelete = [];
626
+ const startDate = new Date(cutoffDate);
627
+ startDate.setDate(startDate.getDate() - 365); // Go back up to 1 year to catch old data
628
+
629
+ for (let d = new Date(startDate); d < cutoffDate; d.setDate(d.getDate() + 1)) {
630
+ datesToDelete.push(d.toISOString().slice(0, 10));
631
+ }
600
632
 
601
- // Clean up old metrics
633
+ // Clean up old metrics using partition-aware deletion
602
634
  if (this.metricsResource) {
603
- const oldMetrics = await this.getMetrics({ endDate: cutoffDate.toISOString() });
604
- for (const metric of oldMetrics) {
605
- await this.metricsResource.delete(metric.id);
635
+ for (const dateStr of datesToDelete) {
636
+ const [ok, err, oldMetrics] = await tryFn(() =>
637
+ this.metricsResource.query({ createdAt: dateStr })
638
+ );
639
+ if (ok && oldMetrics) {
640
+ for (const metric of oldMetrics) {
641
+ await tryFn(() => this.metricsResource.delete(metric.id));
642
+ }
643
+ }
606
644
  }
607
645
  }
608
646
 
609
- // Clean up old error logs
647
+ // Clean up old error logs using partition-aware deletion
610
648
  if (this.errorsResource) {
611
- const oldErrors = await this.getErrorLogs({ endDate: cutoffDate.toISOString() });
612
- for (const error of oldErrors) {
613
- await this.errorsResource.delete(error.id);
649
+ for (const dateStr of datesToDelete) {
650
+ const [ok, err, oldErrors] = await tryFn(() =>
651
+ this.errorsResource.query({ createdAt: dateStr })
652
+ );
653
+ if (ok && oldErrors) {
654
+ for (const error of oldErrors) {
655
+ await tryFn(() => this.errorsResource.delete(error.id));
656
+ }
657
+ }
614
658
  }
615
659
  }
616
660
 
617
- // Clean up old performance logs
661
+ // Clean up old performance logs using partition-aware deletion
618
662
  if (this.performanceResource) {
619
- const oldPerformance = await this.getPerformanceLogs({ endDate: cutoffDate.toISOString() });
620
- for (const perf of oldPerformance) {
621
- await this.performanceResource.delete(perf.id);
663
+ for (const dateStr of datesToDelete) {
664
+ const [ok, err, oldPerformance] = await tryFn(() =>
665
+ this.performanceResource.query({ createdAt: dateStr })
666
+ );
667
+ if (ok && oldPerformance) {
668
+ for (const perf of oldPerformance) {
669
+ await tryFn(() => this.performanceResource.delete(perf.id));
670
+ }
671
+ }
622
672
  }
623
673
  }
624
674
  }