s3db.js 11.1.0 → 11.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/s3db.es.js CHANGED
@@ -5101,8 +5101,10 @@ function createConfig(options, detectedTimezone) {
5101
5101
  consolidationWindow: consolidation.window ?? 24,
5102
5102
  autoConsolidate: consolidation.auto !== false,
5103
5103
  mode: consolidation.mode || "async",
5104
- // ✅ NOVO: Performance tuning - Mark applied concurrency (default 50, antes era 10 hardcoded)
5104
+ // ✅ Performance tuning - Mark applied concurrency (default 50, up from 10)
5105
5105
  markAppliedConcurrency: consolidation.markAppliedConcurrency ?? 50,
5106
+ // ✅ Performance tuning - Recalculate concurrency (default 50, up from 10)
5107
+ recalculateConcurrency: consolidation.recalculateConcurrency ?? 50,
5106
5108
  // Late arrivals
5107
5109
  lateArrivalStrategy: lateArrivals.strategy || "warn",
5108
5110
  // Batch transactions
@@ -5399,6 +5401,38 @@ function groupByCohort(transactions, cohortField) {
5399
5401
  }
5400
5402
  return groups;
5401
5403
  }
5404
+ function ensureCohortHour(transaction, timezone = "UTC", verbose = false) {
5405
+ if (transaction.cohortHour) {
5406
+ return transaction;
5407
+ }
5408
+ if (transaction.timestamp) {
5409
+ const date = new Date(transaction.timestamp);
5410
+ const cohortInfo = getCohortInfo(date, timezone, verbose);
5411
+ if (verbose) {
5412
+ console.log(
5413
+ `[EventualConsistency] Transaction ${transaction.id} missing cohortHour, calculated from timestamp: ${cohortInfo.hour}`
5414
+ );
5415
+ }
5416
+ transaction.cohortHour = cohortInfo.hour;
5417
+ if (!transaction.cohortWeek) {
5418
+ transaction.cohortWeek = cohortInfo.week;
5419
+ }
5420
+ if (!transaction.cohortMonth) {
5421
+ transaction.cohortMonth = cohortInfo.month;
5422
+ }
5423
+ } else if (verbose) {
5424
+ console.warn(
5425
+ `[EventualConsistency] Transaction ${transaction.id} missing both cohortHour and timestamp, cannot calculate cohort`
5426
+ );
5427
+ }
5428
+ return transaction;
5429
+ }
5430
+ function ensureCohortHours(transactions, timezone = "UTC", verbose = false) {
5431
+ if (!transactions || !Array.isArray(transactions)) {
5432
+ return transactions;
5433
+ }
5434
+ return transactions.map((txn) => ensureCohortHour(txn, timezone, verbose));
5435
+ }
5402
5436
 
5403
5437
  function createPartitionConfig() {
5404
5438
  const partitions = {
@@ -5899,11 +5933,33 @@ async function consolidateRecord(originalId, transactionResource, targetResource
5899
5933
  const transactionsToUpdate = transactions.filter((txn) => txn.id !== "__synthetic__");
5900
5934
  const markAppliedConcurrency = config.markAppliedConcurrency || 50;
5901
5935
  const { results, errors } = await PromisePool.for(transactionsToUpdate).withConcurrency(markAppliedConcurrency).process(async (txn) => {
5936
+ const txnWithCohorts = ensureCohortHour(txn, config.cohort.timezone, false);
5937
+ const updateData = { applied: true };
5938
+ if (txnWithCohorts.cohortHour && !txn.cohortHour) {
5939
+ updateData.cohortHour = txnWithCohorts.cohortHour;
5940
+ }
5941
+ if (txnWithCohorts.cohortDate && !txn.cohortDate) {
5942
+ updateData.cohortDate = txnWithCohorts.cohortDate;
5943
+ }
5944
+ if (txnWithCohorts.cohortWeek && !txn.cohortWeek) {
5945
+ updateData.cohortWeek = txnWithCohorts.cohortWeek;
5946
+ }
5947
+ if (txnWithCohorts.cohortMonth && !txn.cohortMonth) {
5948
+ updateData.cohortMonth = txnWithCohorts.cohortMonth;
5949
+ }
5950
+ if (txn.value === null || txn.value === void 0) {
5951
+ updateData.value = 1;
5952
+ }
5902
5953
  const [ok2, err2] = await tryFn(
5903
- () => transactionResource.update(txn.id, { applied: true })
5954
+ () => transactionResource.update(txn.id, updateData)
5904
5955
  );
5905
5956
  if (!ok2 && config.verbose) {
5906
- console.warn(`[EventualConsistency] Failed to mark transaction ${txn.id} as applied:`, err2?.message);
5957
+ console.warn(
5958
+ `[EventualConsistency] Failed to mark transaction ${txn.id} as applied:`,
5959
+ err2?.message,
5960
+ "Update data:",
5961
+ updateData
5962
+ );
5907
5963
  }
5908
5964
  return ok2;
5909
5965
  });
@@ -6095,7 +6151,8 @@ async function recalculateRecord(originalId, transactionResource, targetResource
6095
6151
  }
6096
6152
  }
6097
6153
  const transactionsToReset = allTransactions.filter((txn) => txn.source !== "anchor");
6098
- const { results, errors } = await PromisePool.for(transactionsToReset).withConcurrency(10).process(async (txn) => {
6154
+ const recalculateConcurrency = config.recalculateConcurrency || 50;
6155
+ const { results, errors } = await PromisePool.for(transactionsToReset).withConcurrency(recalculateConcurrency).process(async (txn) => {
6099
6156
  const [ok, err] = await tryFn(
6100
6157
  () => transactionResource.update(txn.id, { applied: false })
6101
6158
  );
@@ -6523,7 +6580,10 @@ async function getAnalytics(resourceName, field, options, fieldHandlers) {
6523
6580
  if (!handler.analyticsResource) {
6524
6581
  throw new Error("Analytics not enabled for this plugin");
6525
6582
  }
6526
- const { period = "day", date, startDate, endDate, month, year, breakdown = false } = options;
6583
+ const { period = "day", date, startDate, endDate, month, year, breakdown = false, recordId } = options;
6584
+ if (recordId) {
6585
+ return await getAnalyticsForRecord(resourceName, field, recordId, options, handler);
6586
+ }
6527
6587
  const [ok, err, allAnalytics] = await tryFn(
6528
6588
  () => handler.analyticsResource.list()
6529
6589
  );
@@ -6562,6 +6622,105 @@ async function getAnalytics(resourceName, field, options, fieldHandlers) {
6562
6622
  recordCount: a.recordCount
6563
6623
  }));
6564
6624
  }
6625
+ async function getAnalyticsForRecord(resourceName, field, recordId, options, handler) {
6626
+ const { period = "day", date, startDate, endDate, month, year } = options;
6627
+ const [okTrue, errTrue, appliedTransactions] = await tryFn(
6628
+ () => handler.transactionResource.query({
6629
+ originalId: recordId,
6630
+ applied: true
6631
+ })
6632
+ );
6633
+ const [okFalse, errFalse, pendingTransactions] = await tryFn(
6634
+ () => handler.transactionResource.query({
6635
+ originalId: recordId,
6636
+ applied: false
6637
+ })
6638
+ );
6639
+ let allTransactions = [
6640
+ ...okTrue && appliedTransactions ? appliedTransactions : [],
6641
+ ...okFalse && pendingTransactions ? pendingTransactions : []
6642
+ ];
6643
+ if (allTransactions.length === 0) {
6644
+ return [];
6645
+ }
6646
+ allTransactions = ensureCohortHours(allTransactions, handler.config?.cohort?.timezone || "UTC", false);
6647
+ let filtered = allTransactions;
6648
+ if (date) {
6649
+ if (period === "hour") {
6650
+ filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(date));
6651
+ } else if (period === "day") {
6652
+ filtered = filtered.filter((t) => t.cohortDate === date);
6653
+ } else if (period === "month") {
6654
+ filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth.startsWith(date));
6655
+ }
6656
+ } else if (startDate && endDate) {
6657
+ if (period === "hour") {
6658
+ filtered = filtered.filter((t) => t.cohortHour && t.cohortHour >= startDate && t.cohortHour <= endDate);
6659
+ } else if (period === "day") {
6660
+ filtered = filtered.filter((t) => t.cohortDate && t.cohortDate >= startDate && t.cohortDate <= endDate);
6661
+ } else if (period === "month") {
6662
+ filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth >= startDate && t.cohortMonth <= endDate);
6663
+ }
6664
+ } else if (month) {
6665
+ if (period === "hour") {
6666
+ filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(month));
6667
+ } else if (period === "day") {
6668
+ filtered = filtered.filter((t) => t.cohortDate && t.cohortDate.startsWith(month));
6669
+ }
6670
+ } else if (year) {
6671
+ if (period === "hour") {
6672
+ filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(String(year)));
6673
+ } else if (period === "day") {
6674
+ filtered = filtered.filter((t) => t.cohortDate && t.cohortDate.startsWith(String(year)));
6675
+ } else if (period === "month") {
6676
+ filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth.startsWith(String(year)));
6677
+ }
6678
+ }
6679
+ const cohortField = period === "hour" ? "cohortHour" : period === "day" ? "cohortDate" : "cohortMonth";
6680
+ const aggregated = aggregateTransactionsByCohort(filtered, cohortField);
6681
+ return aggregated;
6682
+ }
6683
+ function aggregateTransactionsByCohort(transactions, cohortField) {
6684
+ const groups = {};
6685
+ for (const txn of transactions) {
6686
+ const cohort = txn[cohortField];
6687
+ if (!cohort) continue;
6688
+ if (!groups[cohort]) {
6689
+ groups[cohort] = {
6690
+ cohort,
6691
+ count: 0,
6692
+ sum: 0,
6693
+ min: Infinity,
6694
+ max: -Infinity,
6695
+ recordCount: /* @__PURE__ */ new Set(),
6696
+ operations: {}
6697
+ };
6698
+ }
6699
+ const group = groups[cohort];
6700
+ const signedValue = txn.operation === "sub" ? -txn.value : txn.value;
6701
+ group.count++;
6702
+ group.sum += signedValue;
6703
+ group.min = Math.min(group.min, signedValue);
6704
+ group.max = Math.max(group.max, signedValue);
6705
+ group.recordCount.add(txn.originalId);
6706
+ const op = txn.operation;
6707
+ if (!group.operations[op]) {
6708
+ group.operations[op] = { count: 0, sum: 0 };
6709
+ }
6710
+ group.operations[op].count++;
6711
+ group.operations[op].sum += signedValue;
6712
+ }
6713
+ return Object.values(groups).map((g) => ({
6714
+ cohort: g.cohort,
6715
+ count: g.count,
6716
+ sum: g.sum,
6717
+ avg: g.sum / g.count,
6718
+ min: g.min === Infinity ? 0 : g.min,
6719
+ max: g.max === -Infinity ? 0 : g.max,
6720
+ recordCount: g.recordCount.size,
6721
+ operations: g.operations
6722
+ })).sort((a, b) => a.cohort.localeCompare(b.cohort));
6723
+ }
6565
6724
  async function getMonthByDay(resourceName, field, month, options, fieldHandlers) {
6566
6725
  const year = parseInt(month.substring(0, 4));
6567
6726
  const monthNum = parseInt(month.substring(5, 7));
@@ -6596,6 +6755,8 @@ async function getLastNDays(resourceName, field, days, options, fieldHandlers) {
6596
6755
  return date.toISOString().substring(0, 10);
6597
6756
  }).reverse();
6598
6757
  const data = await getAnalytics(resourceName, field, {
6758
+ ...options,
6759
+ // ✅ Include all options (recordId, etc.)
6599
6760
  period: "day",
6600
6761
  startDate: dates[0],
6601
6762
  endDate: dates[dates.length - 1]
@@ -6789,6 +6950,8 @@ async function getLastNHours(resourceName, field, hours = 24, options, fieldHand
6789
6950
  const startHour = hoursAgo.toISOString().substring(0, 13);
6790
6951
  const endHour = now.toISOString().substring(0, 13);
6791
6952
  const data = await getAnalytics(resourceName, field, {
6953
+ ...options,
6954
+ // ✅ Include all options (recordId, etc.)
6792
6955
  period: "hour",
6793
6956
  startDate: startHour,
6794
6957
  endDate: endHour
@@ -6836,6 +6999,8 @@ async function getLastNMonths(resourceName, field, months = 12, options, fieldHa
6836
6999
  const startDate = monthsAgo.toISOString().substring(0, 7);
6837
7000
  const endDate = now.toISOString().substring(0, 7);
6838
7001
  const data = await getAnalytics(resourceName, field, {
7002
+ ...options,
7003
+ // ✅ Include all options (recordId, etc.)
6839
7004
  period: "month",
6840
7005
  startDate,
6841
7006
  endDate
@@ -7028,7 +7193,8 @@ async function completeFieldSetup(handler, database, config, plugin) {
7028
7193
  operation: "string|required",
7029
7194
  timestamp: "string|required",
7030
7195
  cohortDate: "string|required",
7031
- cohortHour: "string|required",
7196
+ cohortHour: "string|optional",
7197
+ // ✅ FIX BUG #2: Changed from required to optional for migration compatibility
7032
7198
  cohortWeek: "string|optional",
7033
7199
  cohortMonth: "string|optional",
7034
7200
  source: "string|optional",
@@ -7077,6 +7243,28 @@ async function createAnalyticsResource(handler, database, resourceName, fieldNam
7077
7243
  },
7078
7244
  behavior: "body-overflow",
7079
7245
  timestamps: false,
7246
+ asyncPartitions: true,
7247
+ // ✅ Multi-attribute partitions for optimal analytics query performance
7248
+ partitions: {
7249
+ // Query by period (hour/day/week/month)
7250
+ byPeriod: {
7251
+ fields: { period: "string" }
7252
+ },
7253
+ // Query by period + cohort (e.g., all hour records for specific hours)
7254
+ byPeriodCohort: {
7255
+ fields: {
7256
+ period: "string",
7257
+ cohort: "string"
7258
+ }
7259
+ },
7260
+ // Query by field + period (e.g., all daily analytics for clicks field)
7261
+ byFieldPeriod: {
7262
+ fields: {
7263
+ field: "string",
7264
+ period: "string"
7265
+ }
7266
+ }
7267
+ },
7080
7268
  createdBy: "EventualConsistencyPlugin"
7081
7269
  })
7082
7270
  );
@@ -13606,7 +13794,7 @@ class Database extends EventEmitter {
13606
13794
  this.id = idGenerator(7);
13607
13795
  this.version = "1";
13608
13796
  this.s3dbVersion = (() => {
13609
- const [ok, err, version] = tryFn(() => true ? "11.1.0" : "latest");
13797
+ const [ok, err, version] = tryFn(() => true ? "11.2.2" : "latest");
13610
13798
  return ok ? version : "latest";
13611
13799
  })();
13612
13800
  this.resources = {};