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.
- package/dist/s3db.cjs.js +612 -308
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +612 -308
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/concerns/plugin-storage.js +274 -9
- package/src/plugins/audit.plugin.js +94 -18
- package/src/plugins/eventual-consistency/analytics.js +350 -15
- package/src/plugins/eventual-consistency/config.js +3 -0
- package/src/plugins/eventual-consistency/consolidation.js +32 -36
- package/src/plugins/eventual-consistency/garbage-collection.js +11 -13
- package/src/plugins/eventual-consistency/index.js +28 -19
- package/src/plugins/eventual-consistency/install.js +9 -26
- package/src/plugins/eventual-consistency/partitions.js +5 -0
- package/src/plugins/eventual-consistency/transactions.js +1 -0
- package/src/plugins/eventual-consistency/utils.js +36 -1
- package/src/plugins/fulltext.plugin.js +76 -22
- package/src/plugins/metrics.plugin.js +70 -20
- package/src/plugins/s3-queue.plugin.js +21 -120
- package/src/plugins/scheduler.plugin.js +11 -37
|
@@ -44,14 +44,16 @@ export async function updateAnalytics(transactions, analyticsResource, config) {
|
|
|
44
44
|
if (config.verbose) {
|
|
45
45
|
console.log(
|
|
46
46
|
`[EventualConsistency] ${config.resource}.${config.field} - ` +
|
|
47
|
-
`Updating ${cohortCount} hourly analytics cohorts...`
|
|
47
|
+
`Updating ${cohortCount} hourly analytics cohorts IN PARALLEL...`
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// Update hourly analytics
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
// ✅ OTIMIZAÇÃO: Update hourly analytics EM PARALELO
|
|
52
|
+
await Promise.all(
|
|
53
|
+
Object.entries(byHour).map(([cohort, txns]) =>
|
|
54
|
+
upsertAnalytics('hour', cohort, txns, analyticsResource, config)
|
|
55
|
+
)
|
|
56
|
+
);
|
|
55
57
|
|
|
56
58
|
// Roll up to daily and monthly if configured
|
|
57
59
|
if (config.analyticsConfig.rollupStrategy === 'incremental') {
|
|
@@ -60,13 +62,16 @@ export async function updateAnalytics(transactions, analyticsResource, config) {
|
|
|
60
62
|
if (config.verbose) {
|
|
61
63
|
console.log(
|
|
62
64
|
`[EventualConsistency] ${config.resource}.${config.field} - ` +
|
|
63
|
-
`Rolling up ${uniqueHours.length} hours to daily/monthly analytics...`
|
|
65
|
+
`Rolling up ${uniqueHours.length} hours to daily/weekly/monthly analytics IN PARALLEL...`
|
|
64
66
|
);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
// ✅ OTIMIZAÇÃO: Rollup analytics EM PARALELO
|
|
70
|
+
await Promise.all(
|
|
71
|
+
uniqueHours.map(cohortHour =>
|
|
72
|
+
rollupAnalytics(cohortHour, analyticsResource, config)
|
|
73
|
+
)
|
|
74
|
+
);
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
if (config.verbose) {
|
|
@@ -206,7 +211,7 @@ function calculateOperationBreakdown(transactions) {
|
|
|
206
211
|
}
|
|
207
212
|
|
|
208
213
|
/**
|
|
209
|
-
* Roll up hourly analytics to daily and monthly
|
|
214
|
+
* Roll up hourly analytics to daily, weekly, and monthly
|
|
210
215
|
* @private
|
|
211
216
|
*/
|
|
212
217
|
async function rollupAnalytics(cohortHour, analyticsResource, config) {
|
|
@@ -214,20 +219,58 @@ async function rollupAnalytics(cohortHour, analyticsResource, config) {
|
|
|
214
219
|
const cohortDate = cohortHour.substring(0, 10); // '2025-10-09'
|
|
215
220
|
const cohortMonth = cohortHour.substring(0, 7); // '2025-10'
|
|
216
221
|
|
|
222
|
+
// Calculate week cohort (ISO 8601 format)
|
|
223
|
+
const date = new Date(cohortDate);
|
|
224
|
+
const cohortWeek = getCohortWeekFromDate(date);
|
|
225
|
+
|
|
217
226
|
// Roll up to day
|
|
218
227
|
await rollupPeriod('day', cohortDate, cohortDate, analyticsResource, config);
|
|
219
228
|
|
|
229
|
+
// Roll up to week
|
|
230
|
+
await rollupPeriod('week', cohortWeek, cohortWeek, analyticsResource, config);
|
|
231
|
+
|
|
220
232
|
// Roll up to month
|
|
221
233
|
await rollupPeriod('month', cohortMonth, cohortMonth, analyticsResource, config);
|
|
222
234
|
}
|
|
223
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Get cohort week string from a date
|
|
238
|
+
* @private
|
|
239
|
+
*/
|
|
240
|
+
function getCohortWeekFromDate(date) {
|
|
241
|
+
// ISO week calculation (use UTC methods)
|
|
242
|
+
const target = new Date(date.valueOf());
|
|
243
|
+
const dayNr = (date.getUTCDay() + 6) % 7;
|
|
244
|
+
target.setUTCDate(target.getUTCDate() - dayNr + 3);
|
|
245
|
+
|
|
246
|
+
const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1));
|
|
247
|
+
const firstThursday = new Date(yearStart.valueOf());
|
|
248
|
+
if (yearStart.getUTCDay() !== 4) {
|
|
249
|
+
firstThursday.setUTCDate(yearStart.getUTCDate() + ((4 - yearStart.getUTCDay()) + 7) % 7);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const weekNumber = 1 + Math.round((target - firstThursday) / 604800000);
|
|
253
|
+
const weekYear = target.getUTCFullYear();
|
|
254
|
+
|
|
255
|
+
return `${weekYear}-W${String(weekNumber).padStart(2, '0')}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
224
258
|
/**
|
|
225
259
|
* Roll up analytics for a specific period
|
|
226
260
|
* @private
|
|
227
261
|
*/
|
|
228
262
|
async function rollupPeriod(period, cohort, sourcePrefix, analyticsResource, config) {
|
|
229
|
-
// Get all source analytics (e.g., all hours for a day)
|
|
230
|
-
|
|
263
|
+
// Get all source analytics (e.g., all hours for a day, all days for a week)
|
|
264
|
+
let sourcePeriod;
|
|
265
|
+
if (period === 'day') {
|
|
266
|
+
sourcePeriod = 'hour';
|
|
267
|
+
} else if (period === 'week') {
|
|
268
|
+
sourcePeriod = 'day'; // Week aggregates from days
|
|
269
|
+
} else if (period === 'month') {
|
|
270
|
+
sourcePeriod = 'day'; // ✅ Month aggregates from days AND hours (like week)
|
|
271
|
+
} else {
|
|
272
|
+
sourcePeriod = 'day'; // Fallback
|
|
273
|
+
}
|
|
231
274
|
|
|
232
275
|
const [ok, err, allAnalytics] = await tryFn(() =>
|
|
233
276
|
analyticsResource.list()
|
|
@@ -236,9 +279,24 @@ async function rollupPeriod(period, cohort, sourcePrefix, analyticsResource, con
|
|
|
236
279
|
if (!ok || !allAnalytics) return;
|
|
237
280
|
|
|
238
281
|
// Filter to matching cohorts
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
282
|
+
let sourceAnalytics;
|
|
283
|
+
if (period === 'week') {
|
|
284
|
+
// For week, we need to find all days that belong to this week
|
|
285
|
+
sourceAnalytics = allAnalytics.filter(a => {
|
|
286
|
+
if (a.period !== sourcePeriod) return false;
|
|
287
|
+
// Check if this day's cohort belongs to the target week
|
|
288
|
+
const dayDate = new Date(a.cohort);
|
|
289
|
+
const dayWeek = getCohortWeekFromDate(dayDate);
|
|
290
|
+
return dayWeek === cohort;
|
|
291
|
+
});
|
|
292
|
+
} else {
|
|
293
|
+
// For day and month, simple prefix matching works
|
|
294
|
+
// day: aggregates from hours (cohort '2025-10-09' matches '2025-10-09T14', '2025-10-09T15', etc)
|
|
295
|
+
// month: aggregates from days (cohort '2025-10' matches '2025-10-01', '2025-10-02', etc)
|
|
296
|
+
sourceAnalytics = allAnalytics.filter(a =>
|
|
297
|
+
a.period === sourcePeriod && a.cohort.startsWith(sourcePrefix)
|
|
298
|
+
);
|
|
299
|
+
}
|
|
242
300
|
|
|
243
301
|
if (sourceAnalytics.length === 0) return;
|
|
244
302
|
|
|
@@ -567,6 +625,66 @@ export async function getYearByMonth(resourceName, field, year, options, fieldHa
|
|
|
567
625
|
return data;
|
|
568
626
|
}
|
|
569
627
|
|
|
628
|
+
/**
|
|
629
|
+
* Get analytics for entire year, broken down by weeks
|
|
630
|
+
*
|
|
631
|
+
* @param {string} resourceName - Resource name
|
|
632
|
+
* @param {string} field - Field name
|
|
633
|
+
* @param {number} year - Year (e.g., 2025)
|
|
634
|
+
* @param {Object} options - Options
|
|
635
|
+
* @param {Object} fieldHandlers - Field handlers map
|
|
636
|
+
* @returns {Promise<Array>} Weekly analytics for the year (up to 53 weeks)
|
|
637
|
+
*/
|
|
638
|
+
export async function getYearByWeek(resourceName, field, year, options, fieldHandlers) {
|
|
639
|
+
const data = await getAnalytics(resourceName, field, {
|
|
640
|
+
period: 'week',
|
|
641
|
+
year
|
|
642
|
+
}, fieldHandlers);
|
|
643
|
+
|
|
644
|
+
// Week data doesn't need gap filling as much as daily/hourly
|
|
645
|
+
// But we can still provide it if requested
|
|
646
|
+
if (options.fillGaps) {
|
|
647
|
+
// ISO weeks: typically 52-53 weeks per year
|
|
648
|
+
const startWeek = `${year}-W01`;
|
|
649
|
+
const endWeek = `${year}-W53`;
|
|
650
|
+
return fillGaps(data, 'week', startWeek, endWeek);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return data;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Get analytics for entire month, broken down by weeks
|
|
658
|
+
*
|
|
659
|
+
* @param {string} resourceName - Resource name
|
|
660
|
+
* @param {string} field - Field name
|
|
661
|
+
* @param {string} month - Month in YYYY-MM format
|
|
662
|
+
* @param {Object} options - Options
|
|
663
|
+
* @param {Object} fieldHandlers - Field handlers map
|
|
664
|
+
* @returns {Promise<Array>} Weekly analytics for the month
|
|
665
|
+
*/
|
|
666
|
+
export async function getMonthByWeek(resourceName, field, month, options, fieldHandlers) {
|
|
667
|
+
// month format: '2025-10'
|
|
668
|
+
const year = parseInt(month.substring(0, 4));
|
|
669
|
+
const monthNum = parseInt(month.substring(5, 7));
|
|
670
|
+
|
|
671
|
+
// Get first and last day of month
|
|
672
|
+
const firstDay = new Date(year, monthNum - 1, 1);
|
|
673
|
+
const lastDay = new Date(year, monthNum, 0);
|
|
674
|
+
|
|
675
|
+
// Find which weeks this month spans
|
|
676
|
+
const firstWeek = getCohortWeekFromDate(firstDay);
|
|
677
|
+
const lastWeek = getCohortWeekFromDate(lastDay);
|
|
678
|
+
|
|
679
|
+
const data = await getAnalytics(resourceName, field, {
|
|
680
|
+
period: 'week',
|
|
681
|
+
startDate: firstWeek,
|
|
682
|
+
endDate: lastWeek
|
|
683
|
+
}, fieldHandlers);
|
|
684
|
+
|
|
685
|
+
return data;
|
|
686
|
+
}
|
|
687
|
+
|
|
570
688
|
/**
|
|
571
689
|
* Get analytics for entire month, broken down by hours
|
|
572
690
|
*
|
|
@@ -690,3 +808,220 @@ export async function getTopRecords(resourceName, field, options, fieldHandlers)
|
|
|
690
808
|
// Limit results
|
|
691
809
|
return records.slice(0, limit);
|
|
692
810
|
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Get analytics for entire year, broken down by days
|
|
814
|
+
*
|
|
815
|
+
* @param {string} resourceName - Resource name
|
|
816
|
+
* @param {string} field - Field name
|
|
817
|
+
* @param {number} year - Year (e.g., 2025)
|
|
818
|
+
* @param {Object} options - Options
|
|
819
|
+
* @param {Object} fieldHandlers - Field handlers map
|
|
820
|
+
* @returns {Promise<Array>} Daily analytics for the year (up to 365/366 records)
|
|
821
|
+
*/
|
|
822
|
+
export async function getYearByDay(resourceName, field, year, options, fieldHandlers) {
|
|
823
|
+
const startDate = `${year}-01-01`;
|
|
824
|
+
const endDate = `${year}-12-31`;
|
|
825
|
+
|
|
826
|
+
const data = await getAnalytics(resourceName, field, {
|
|
827
|
+
period: 'day',
|
|
828
|
+
startDate,
|
|
829
|
+
endDate
|
|
830
|
+
}, fieldHandlers);
|
|
831
|
+
|
|
832
|
+
if (options.fillGaps) {
|
|
833
|
+
return fillGaps(data, 'day', startDate, endDate);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return data;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Get analytics for entire week, broken down by days
|
|
841
|
+
*
|
|
842
|
+
* @param {string} resourceName - Resource name
|
|
843
|
+
* @param {string} field - Field name
|
|
844
|
+
* @param {string} week - Week in YYYY-Www format (e.g., '2025-W42')
|
|
845
|
+
* @param {Object} options - Options
|
|
846
|
+
* @param {Object} fieldHandlers - Field handlers map
|
|
847
|
+
* @returns {Promise<Array>} Daily analytics for the week (7 records)
|
|
848
|
+
*/
|
|
849
|
+
export async function getWeekByDay(resourceName, field, week, options, fieldHandlers) {
|
|
850
|
+
// week format: '2025-W42'
|
|
851
|
+
const year = parseInt(week.substring(0, 4));
|
|
852
|
+
const weekNum = parseInt(week.substring(6, 8));
|
|
853
|
+
|
|
854
|
+
// Calculate the first day of the week (Monday)
|
|
855
|
+
const jan4 = new Date(year, 0, 4);
|
|
856
|
+
const jan4Day = jan4.getDay() || 7; // Sunday = 7
|
|
857
|
+
const firstMonday = new Date(year, 0, 4 - jan4Day + 1);
|
|
858
|
+
const weekStart = new Date(firstMonday);
|
|
859
|
+
weekStart.setDate(weekStart.getDate() + (weekNum - 1) * 7);
|
|
860
|
+
|
|
861
|
+
// Get all 7 days of the week
|
|
862
|
+
const days = [];
|
|
863
|
+
for (let i = 0; i < 7; i++) {
|
|
864
|
+
const day = new Date(weekStart);
|
|
865
|
+
day.setDate(weekStart.getDate() + i);
|
|
866
|
+
days.push(day.toISOString().substring(0, 10));
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const startDate = days[0];
|
|
870
|
+
const endDate = days[6];
|
|
871
|
+
|
|
872
|
+
const data = await getAnalytics(resourceName, field, {
|
|
873
|
+
period: 'day',
|
|
874
|
+
startDate,
|
|
875
|
+
endDate
|
|
876
|
+
}, fieldHandlers);
|
|
877
|
+
|
|
878
|
+
if (options.fillGaps) {
|
|
879
|
+
return fillGaps(data, 'day', startDate, endDate);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return data;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Get analytics for entire week, broken down by hours
|
|
887
|
+
*
|
|
888
|
+
* @param {string} resourceName - Resource name
|
|
889
|
+
* @param {string} field - Field name
|
|
890
|
+
* @param {string} week - Week in YYYY-Www format (e.g., '2025-W42')
|
|
891
|
+
* @param {Object} options - Options
|
|
892
|
+
* @param {Object} fieldHandlers - Field handlers map
|
|
893
|
+
* @returns {Promise<Array>} Hourly analytics for the week (168 records)
|
|
894
|
+
*/
|
|
895
|
+
export async function getWeekByHour(resourceName, field, week, options, fieldHandlers) {
|
|
896
|
+
// week format: '2025-W42'
|
|
897
|
+
const year = parseInt(week.substring(0, 4));
|
|
898
|
+
const weekNum = parseInt(week.substring(6, 8));
|
|
899
|
+
|
|
900
|
+
// Calculate the first day of the week (Monday)
|
|
901
|
+
const jan4 = new Date(year, 0, 4);
|
|
902
|
+
const jan4Day = jan4.getDay() || 7; // Sunday = 7
|
|
903
|
+
const firstMonday = new Date(year, 0, 4 - jan4Day + 1);
|
|
904
|
+
const weekStart = new Date(firstMonday);
|
|
905
|
+
weekStart.setDate(weekStart.getDate() + (weekNum - 1) * 7);
|
|
906
|
+
|
|
907
|
+
// Get first and last day of week
|
|
908
|
+
const weekEnd = new Date(weekStart);
|
|
909
|
+
weekEnd.setDate(weekEnd.getDate() + 6);
|
|
910
|
+
|
|
911
|
+
const startDate = weekStart.toISOString().substring(0, 10);
|
|
912
|
+
const endDate = weekEnd.toISOString().substring(0, 10);
|
|
913
|
+
|
|
914
|
+
const data = await getAnalytics(resourceName, field, {
|
|
915
|
+
period: 'hour',
|
|
916
|
+
startDate,
|
|
917
|
+
endDate
|
|
918
|
+
}, fieldHandlers);
|
|
919
|
+
|
|
920
|
+
if (options.fillGaps) {
|
|
921
|
+
return fillGaps(data, 'hour', startDate, endDate);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return data;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Get analytics for last N hours
|
|
929
|
+
*
|
|
930
|
+
* @param {string} resourceName - Resource name
|
|
931
|
+
* @param {string} field - Field name
|
|
932
|
+
* @param {number} hours - Number of hours to look back (default: 24)
|
|
933
|
+
* @param {Object} options - Options
|
|
934
|
+
* @param {Object} fieldHandlers - Field handlers map
|
|
935
|
+
* @returns {Promise<Array>} Hourly analytics
|
|
936
|
+
*/
|
|
937
|
+
export async function getLastNHours(resourceName, field, hours = 24, options, fieldHandlers) {
|
|
938
|
+
const now = new Date();
|
|
939
|
+
const hoursAgo = new Date(now);
|
|
940
|
+
hoursAgo.setHours(hoursAgo.getHours() - hours);
|
|
941
|
+
|
|
942
|
+
const startDate = hoursAgo.toISOString().substring(0, 13); // YYYY-MM-DDTHH
|
|
943
|
+
const endDate = now.toISOString().substring(0, 13);
|
|
944
|
+
|
|
945
|
+
const data = await getAnalytics(resourceName, field, {
|
|
946
|
+
period: 'hour',
|
|
947
|
+
startDate,
|
|
948
|
+
endDate
|
|
949
|
+
}, fieldHandlers);
|
|
950
|
+
|
|
951
|
+
if (options.fillGaps) {
|
|
952
|
+
const startDay = hoursAgo.toISOString().substring(0, 10);
|
|
953
|
+
const endDay = now.toISOString().substring(0, 10);
|
|
954
|
+
return fillGaps(data, 'hour', startDay, endDay);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return data;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Get analytics for last N weeks
|
|
962
|
+
*
|
|
963
|
+
* @param {string} resourceName - Resource name
|
|
964
|
+
* @param {string} field - Field name
|
|
965
|
+
* @param {number} weeks - Number of weeks to look back (default: 4)
|
|
966
|
+
* @param {Object} options - Options
|
|
967
|
+
* @param {Object} fieldHandlers - Field handlers map
|
|
968
|
+
* @returns {Promise<Array>} Weekly analytics
|
|
969
|
+
*/
|
|
970
|
+
export async function getLastNWeeks(resourceName, field, weeks = 4, options, fieldHandlers) {
|
|
971
|
+
const now = new Date();
|
|
972
|
+
const weeksAgo = new Date(now);
|
|
973
|
+
weeksAgo.setDate(weeksAgo.getDate() - (weeks * 7));
|
|
974
|
+
|
|
975
|
+
// Get week cohorts for the range
|
|
976
|
+
const weekCohorts = [];
|
|
977
|
+
const currentDate = new Date(weeksAgo);
|
|
978
|
+
while (currentDate <= now) {
|
|
979
|
+
const weekCohort = getCohortWeekFromDate(currentDate);
|
|
980
|
+
if (!weekCohorts.includes(weekCohort)) {
|
|
981
|
+
weekCohorts.push(weekCohort);
|
|
982
|
+
}
|
|
983
|
+
currentDate.setDate(currentDate.getDate() + 7);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const startWeek = weekCohorts[0];
|
|
987
|
+
const endWeek = weekCohorts[weekCohorts.length - 1];
|
|
988
|
+
|
|
989
|
+
const data = await getAnalytics(resourceName, field, {
|
|
990
|
+
period: 'week',
|
|
991
|
+
startDate: startWeek,
|
|
992
|
+
endDate: endWeek
|
|
993
|
+
}, fieldHandlers);
|
|
994
|
+
|
|
995
|
+
return data;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Get analytics for last N months
|
|
1000
|
+
*
|
|
1001
|
+
* @param {string} resourceName - Resource name
|
|
1002
|
+
* @param {string} field - Field name
|
|
1003
|
+
* @param {number} months - Number of months to look back (default: 12)
|
|
1004
|
+
* @param {Object} options - Options
|
|
1005
|
+
* @param {Object} fieldHandlers - Field handlers map
|
|
1006
|
+
* @returns {Promise<Array>} Monthly analytics
|
|
1007
|
+
*/
|
|
1008
|
+
export async function getLastNMonths(resourceName, field, months = 12, options, fieldHandlers) {
|
|
1009
|
+
const now = new Date();
|
|
1010
|
+
const monthsAgo = new Date(now);
|
|
1011
|
+
monthsAgo.setMonth(monthsAgo.getMonth() - months);
|
|
1012
|
+
|
|
1013
|
+
const startDate = monthsAgo.toISOString().substring(0, 7); // YYYY-MM
|
|
1014
|
+
const endDate = now.toISOString().substring(0, 7);
|
|
1015
|
+
|
|
1016
|
+
const data = await getAnalytics(resourceName, field, {
|
|
1017
|
+
period: 'month',
|
|
1018
|
+
startDate,
|
|
1019
|
+
endDate
|
|
1020
|
+
}, fieldHandlers);
|
|
1021
|
+
|
|
1022
|
+
if (options.fillGaps) {
|
|
1023
|
+
return fillGaps(data, 'month', startDate, endDate);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return data;
|
|
1027
|
+
}
|
|
@@ -47,6 +47,9 @@ export function createConfig(options, detectedTimezone) {
|
|
|
47
47
|
autoConsolidate: consolidation.auto !== false,
|
|
48
48
|
mode: consolidation.mode || 'async',
|
|
49
49
|
|
|
50
|
+
// ✅ NOVO: Performance tuning - Mark applied concurrency (default 50, antes era 10 hardcoded)
|
|
51
|
+
markAppliedConcurrency: consolidation.markAppliedConcurrency ?? 50,
|
|
52
|
+
|
|
50
53
|
// Late arrivals
|
|
51
54
|
lateArrivalStrategy: lateArrivals.strategy || 'warn',
|
|
52
55
|
|
|
@@ -7,7 +7,6 @@ import tryFn from "../../concerns/try-fn.js";
|
|
|
7
7
|
import { PromisePool } from "@supercharge/promise-pool";
|
|
8
8
|
import { idGenerator } from "../../concerns/id.js";
|
|
9
9
|
import { getCohortInfo, createSyntheticSetTransaction } from "./utils.js";
|
|
10
|
-
import { cleanupStaleLocks } from "./locks.js";
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* Start consolidation timer for a handler
|
|
@@ -169,7 +168,7 @@ export async function runConsolidation(transactionResource, consolidateRecordFn,
|
|
|
169
168
|
* @param {string} originalId - ID of the record to consolidate
|
|
170
169
|
* @param {Object} transactionResource - Transaction resource
|
|
171
170
|
* @param {Object} targetResource - Target resource
|
|
172
|
-
* @param {Object}
|
|
171
|
+
* @param {Object} storage - PluginStorage instance for locks
|
|
173
172
|
* @param {Object} analyticsResource - Analytics resource (optional)
|
|
174
173
|
* @param {Function} updateAnalyticsFn - Function to update analytics (optional)
|
|
175
174
|
* @param {Object} config - Plugin configuration
|
|
@@ -179,26 +178,21 @@ export async function consolidateRecord(
|
|
|
179
178
|
originalId,
|
|
180
179
|
transactionResource,
|
|
181
180
|
targetResource,
|
|
182
|
-
|
|
181
|
+
storage,
|
|
183
182
|
analyticsResource,
|
|
184
183
|
updateAnalyticsFn,
|
|
185
184
|
config
|
|
186
185
|
) {
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
id: lockId,
|
|
195
|
-
lockedAt: Date.now(),
|
|
196
|
-
workerId: process.pid ? String(process.pid) : 'unknown'
|
|
197
|
-
})
|
|
198
|
-
);
|
|
186
|
+
// Acquire distributed lock with TTL to prevent concurrent consolidation
|
|
187
|
+
const lockKey = `consolidation-${config.resource}-${config.field}-${originalId}`;
|
|
188
|
+
const lock = await storage.acquireLock(lockKey, {
|
|
189
|
+
ttl: config.lockTimeout || 30,
|
|
190
|
+
timeout: 0, // Don't wait if locked
|
|
191
|
+
workerId: process.pid ? String(process.pid) : 'unknown'
|
|
192
|
+
});
|
|
199
193
|
|
|
200
194
|
// If lock couldn't be acquired, another worker is consolidating
|
|
201
|
-
if (!
|
|
195
|
+
if (!lock) {
|
|
202
196
|
if (config.verbose) {
|
|
203
197
|
console.log(`[EventualConsistency] Lock for ${originalId} already held, skipping`);
|
|
204
198
|
}
|
|
@@ -508,9 +502,12 @@ export async function consolidateRecord(
|
|
|
508
502
|
// Mark transactions as applied (skip synthetic ones) - use PromisePool for controlled concurrency
|
|
509
503
|
const transactionsToUpdate = transactions.filter(txn => txn.id !== '__synthetic__');
|
|
510
504
|
|
|
505
|
+
// ✅ OTIMIZAÇÃO: Usar concurrency do config (default aumentado de 10 para 50)
|
|
506
|
+
const markAppliedConcurrency = config.markAppliedConcurrency || 50;
|
|
507
|
+
|
|
511
508
|
const { results, errors } = await PromisePool
|
|
512
509
|
.for(transactionsToUpdate)
|
|
513
|
-
.withConcurrency(
|
|
510
|
+
.withConcurrency(markAppliedConcurrency) // ✅ Configurável e maior!
|
|
514
511
|
.process(async (txn) => {
|
|
515
512
|
const [ok, err] = await tryFn(() =>
|
|
516
513
|
transactionResource.update(txn.id, { applied: true })
|
|
@@ -576,10 +573,12 @@ export async function consolidateRecord(
|
|
|
576
573
|
return consolidatedValue;
|
|
577
574
|
} finally {
|
|
578
575
|
// Always release the lock
|
|
579
|
-
const [lockReleased, lockReleaseErr] = await tryFn(() =>
|
|
576
|
+
const [lockReleased, lockReleaseErr] = await tryFn(() =>
|
|
577
|
+
storage.releaseLock(lockKey)
|
|
578
|
+
);
|
|
580
579
|
|
|
581
580
|
if (!lockReleased && config.verbose) {
|
|
582
|
-
console.warn(`[EventualConsistency] Failed to release lock ${
|
|
581
|
+
console.warn(`[EventualConsistency] Failed to release lock ${lockKey}:`, lockReleaseErr?.message);
|
|
583
582
|
}
|
|
584
583
|
}
|
|
585
584
|
}
|
|
@@ -705,7 +704,7 @@ export async function getCohortStats(cohortDate, transactionResource) {
|
|
|
705
704
|
* @param {string} originalId - ID of the record to recalculate
|
|
706
705
|
* @param {Object} transactionResource - Transaction resource
|
|
707
706
|
* @param {Object} targetResource - Target resource
|
|
708
|
-
* @param {Object}
|
|
707
|
+
* @param {Object} storage - PluginStorage instance for locks
|
|
709
708
|
* @param {Function} consolidateRecordFn - Function to consolidate the record
|
|
710
709
|
* @param {Object} config - Plugin configuration
|
|
711
710
|
* @returns {Promise<number>} Recalculated value
|
|
@@ -714,25 +713,20 @@ export async function recalculateRecord(
|
|
|
714
713
|
originalId,
|
|
715
714
|
transactionResource,
|
|
716
715
|
targetResource,
|
|
717
|
-
|
|
716
|
+
storage,
|
|
718
717
|
consolidateRecordFn,
|
|
719
718
|
config
|
|
720
719
|
) {
|
|
721
|
-
//
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
id: lockId,
|
|
729
|
-
lockedAt: Date.now(),
|
|
730
|
-
workerId: process.pid ? String(process.pid) : 'unknown'
|
|
731
|
-
})
|
|
732
|
-
);
|
|
720
|
+
// Acquire distributed lock with TTL to prevent concurrent operations
|
|
721
|
+
const lockKey = `recalculate-${config.resource}-${config.field}-${originalId}`;
|
|
722
|
+
const lock = await storage.acquireLock(lockKey, {
|
|
723
|
+
ttl: config.lockTimeout || 30,
|
|
724
|
+
timeout: 0, // Don't wait if locked
|
|
725
|
+
workerId: process.pid ? String(process.pid) : 'unknown'
|
|
726
|
+
});
|
|
733
727
|
|
|
734
728
|
// If lock couldn't be acquired, another worker is operating on this record
|
|
735
|
-
if (!
|
|
729
|
+
if (!lock) {
|
|
736
730
|
if (config.verbose) {
|
|
737
731
|
console.log(`[EventualConsistency] Recalculate lock for ${originalId} already held, skipping`);
|
|
738
732
|
}
|
|
@@ -832,10 +826,12 @@ export async function recalculateRecord(
|
|
|
832
826
|
return consolidatedValue;
|
|
833
827
|
} finally {
|
|
834
828
|
// Always release the lock
|
|
835
|
-
const [lockReleased, lockReleaseErr] = await tryFn(() =>
|
|
829
|
+
const [lockReleased, lockReleaseErr] = await tryFn(() =>
|
|
830
|
+
storage.releaseLock(lockKey)
|
|
831
|
+
);
|
|
836
832
|
|
|
837
833
|
if (!lockReleased && config.verbose) {
|
|
838
|
-
console.warn(`[EventualConsistency] Failed to release recalculate lock ${
|
|
834
|
+
console.warn(`[EventualConsistency] Failed to release recalculate lock ${lockKey}:`, lockReleaseErr?.message);
|
|
839
835
|
}
|
|
840
836
|
}
|
|
841
837
|
}
|
|
@@ -31,24 +31,22 @@ export function startGarbageCollectionTimer(handler, resourceName, fieldName, ru
|
|
|
31
31
|
* Uses distributed locking to prevent multiple containers from running GC simultaneously
|
|
32
32
|
*
|
|
33
33
|
* @param {Object} transactionResource - Transaction resource
|
|
34
|
-
* @param {Object}
|
|
34
|
+
* @param {Object} storage - PluginStorage instance for locks
|
|
35
35
|
* @param {Object} config - Plugin configuration
|
|
36
36
|
* @param {Function} emitFn - Function to emit events
|
|
37
37
|
* @returns {Promise<void>}
|
|
38
38
|
*/
|
|
39
|
-
export async function runGarbageCollection(transactionResource,
|
|
40
|
-
// Acquire distributed lock for GC operation
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
})
|
|
48
|
-
);
|
|
39
|
+
export async function runGarbageCollection(transactionResource, storage, config, emitFn) {
|
|
40
|
+
// Acquire distributed lock with TTL for GC operation
|
|
41
|
+
const lockKey = `gc-${config.resource}-${config.field}`;
|
|
42
|
+
const lock = await storage.acquireLock(lockKey, {
|
|
43
|
+
ttl: 300, // 5 minutes for GC
|
|
44
|
+
timeout: 0, // Don't wait if locked
|
|
45
|
+
workerId: process.pid ? String(process.pid) : 'unknown'
|
|
46
|
+
});
|
|
49
47
|
|
|
50
48
|
// If another container is already running GC, skip
|
|
51
|
-
if (!
|
|
49
|
+
if (!lock) {
|
|
52
50
|
if (config.verbose) {
|
|
53
51
|
console.log(`[EventualConsistency] GC already running in another container`);
|
|
54
52
|
}
|
|
@@ -121,6 +119,6 @@ export async function runGarbageCollection(transactionResource, lockResource, co
|
|
|
121
119
|
}
|
|
122
120
|
} finally {
|
|
123
121
|
// Always release GC lock
|
|
124
|
-
await tryFn(() =>
|
|
122
|
+
await tryFn(() => storage.releaseLock(lockKey));
|
|
125
123
|
}
|
|
126
124
|
}
|