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.cjs.js CHANGED
@@ -5105,8 +5105,10 @@ function createConfig(options, detectedTimezone) {
5105
5105
  consolidationWindow: consolidation.window ?? 24,
5106
5106
  autoConsolidate: consolidation.auto !== false,
5107
5107
  mode: consolidation.mode || "async",
5108
- // ✅ NOVO: Performance tuning - Mark applied concurrency (default 50, antes era 10 hardcoded)
5108
+ // ✅ Performance tuning - Mark applied concurrency (default 50, up from 10)
5109
5109
  markAppliedConcurrency: consolidation.markAppliedConcurrency ?? 50,
5110
+ // ✅ Performance tuning - Recalculate concurrency (default 50, up from 10)
5111
+ recalculateConcurrency: consolidation.recalculateConcurrency ?? 50,
5110
5112
  // Late arrivals
5111
5113
  lateArrivalStrategy: lateArrivals.strategy || "warn",
5112
5114
  // Batch transactions
@@ -5403,6 +5405,38 @@ function groupByCohort(transactions, cohortField) {
5403
5405
  }
5404
5406
  return groups;
5405
5407
  }
5408
+ function ensureCohortHour(transaction, timezone = "UTC", verbose = false) {
5409
+ if (transaction.cohortHour) {
5410
+ return transaction;
5411
+ }
5412
+ if (transaction.timestamp) {
5413
+ const date = new Date(transaction.timestamp);
5414
+ const cohortInfo = getCohortInfo(date, timezone, verbose);
5415
+ if (verbose) {
5416
+ console.log(
5417
+ `[EventualConsistency] Transaction ${transaction.id} missing cohortHour, calculated from timestamp: ${cohortInfo.hour}`
5418
+ );
5419
+ }
5420
+ transaction.cohortHour = cohortInfo.hour;
5421
+ if (!transaction.cohortWeek) {
5422
+ transaction.cohortWeek = cohortInfo.week;
5423
+ }
5424
+ if (!transaction.cohortMonth) {
5425
+ transaction.cohortMonth = cohortInfo.month;
5426
+ }
5427
+ } else if (verbose) {
5428
+ console.warn(
5429
+ `[EventualConsistency] Transaction ${transaction.id} missing both cohortHour and timestamp, cannot calculate cohort`
5430
+ );
5431
+ }
5432
+ return transaction;
5433
+ }
5434
+ function ensureCohortHours(transactions, timezone = "UTC", verbose = false) {
5435
+ if (!transactions || !Array.isArray(transactions)) {
5436
+ return transactions;
5437
+ }
5438
+ return transactions.map((txn) => ensureCohortHour(txn, timezone, verbose));
5439
+ }
5406
5440
 
5407
5441
  function createPartitionConfig() {
5408
5442
  const partitions = {
@@ -5903,11 +5937,33 @@ async function consolidateRecord(originalId, transactionResource, targetResource
5903
5937
  const transactionsToUpdate = transactions.filter((txn) => txn.id !== "__synthetic__");
5904
5938
  const markAppliedConcurrency = config.markAppliedConcurrency || 50;
5905
5939
  const { results, errors } = await promisePool.PromisePool.for(transactionsToUpdate).withConcurrency(markAppliedConcurrency).process(async (txn) => {
5940
+ const txnWithCohorts = ensureCohortHour(txn, config.cohort.timezone, false);
5941
+ const updateData = { applied: true };
5942
+ if (txnWithCohorts.cohortHour && !txn.cohortHour) {
5943
+ updateData.cohortHour = txnWithCohorts.cohortHour;
5944
+ }
5945
+ if (txnWithCohorts.cohortDate && !txn.cohortDate) {
5946
+ updateData.cohortDate = txnWithCohorts.cohortDate;
5947
+ }
5948
+ if (txnWithCohorts.cohortWeek && !txn.cohortWeek) {
5949
+ updateData.cohortWeek = txnWithCohorts.cohortWeek;
5950
+ }
5951
+ if (txnWithCohorts.cohortMonth && !txn.cohortMonth) {
5952
+ updateData.cohortMonth = txnWithCohorts.cohortMonth;
5953
+ }
5954
+ if (txn.value === null || txn.value === void 0) {
5955
+ updateData.value = 1;
5956
+ }
5906
5957
  const [ok2, err2] = await tryFn(
5907
- () => transactionResource.update(txn.id, { applied: true })
5958
+ () => transactionResource.update(txn.id, updateData)
5908
5959
  );
5909
5960
  if (!ok2 && config.verbose) {
5910
- console.warn(`[EventualConsistency] Failed to mark transaction ${txn.id} as applied:`, err2?.message);
5961
+ console.warn(
5962
+ `[EventualConsistency] Failed to mark transaction ${txn.id} as applied:`,
5963
+ err2?.message,
5964
+ "Update data:",
5965
+ updateData
5966
+ );
5911
5967
  }
5912
5968
  return ok2;
5913
5969
  });
@@ -6099,7 +6155,8 @@ async function recalculateRecord(originalId, transactionResource, targetResource
6099
6155
  }
6100
6156
  }
6101
6157
  const transactionsToReset = allTransactions.filter((txn) => txn.source !== "anchor");
6102
- const { results, errors } = await promisePool.PromisePool.for(transactionsToReset).withConcurrency(10).process(async (txn) => {
6158
+ const recalculateConcurrency = config.recalculateConcurrency || 50;
6159
+ const { results, errors } = await promisePool.PromisePool.for(transactionsToReset).withConcurrency(recalculateConcurrency).process(async (txn) => {
6103
6160
  const [ok, err] = await tryFn(
6104
6161
  () => transactionResource.update(txn.id, { applied: false })
6105
6162
  );
@@ -6527,7 +6584,10 @@ async function getAnalytics(resourceName, field, options, fieldHandlers) {
6527
6584
  if (!handler.analyticsResource) {
6528
6585
  throw new Error("Analytics not enabled for this plugin");
6529
6586
  }
6530
- const { period = "day", date, startDate, endDate, month, year, breakdown = false } = options;
6587
+ const { period = "day", date, startDate, endDate, month, year, breakdown = false, recordId } = options;
6588
+ if (recordId) {
6589
+ return await getAnalyticsForRecord(resourceName, field, recordId, options, handler);
6590
+ }
6531
6591
  const [ok, err, allAnalytics] = await tryFn(
6532
6592
  () => handler.analyticsResource.list()
6533
6593
  );
@@ -6566,6 +6626,105 @@ async function getAnalytics(resourceName, field, options, fieldHandlers) {
6566
6626
  recordCount: a.recordCount
6567
6627
  }));
6568
6628
  }
6629
+ async function getAnalyticsForRecord(resourceName, field, recordId, options, handler) {
6630
+ const { period = "day", date, startDate, endDate, month, year } = options;
6631
+ const [okTrue, errTrue, appliedTransactions] = await tryFn(
6632
+ () => handler.transactionResource.query({
6633
+ originalId: recordId,
6634
+ applied: true
6635
+ })
6636
+ );
6637
+ const [okFalse, errFalse, pendingTransactions] = await tryFn(
6638
+ () => handler.transactionResource.query({
6639
+ originalId: recordId,
6640
+ applied: false
6641
+ })
6642
+ );
6643
+ let allTransactions = [
6644
+ ...okTrue && appliedTransactions ? appliedTransactions : [],
6645
+ ...okFalse && pendingTransactions ? pendingTransactions : []
6646
+ ];
6647
+ if (allTransactions.length === 0) {
6648
+ return [];
6649
+ }
6650
+ allTransactions = ensureCohortHours(allTransactions, handler.config?.cohort?.timezone || "UTC", false);
6651
+ let filtered = allTransactions;
6652
+ if (date) {
6653
+ if (period === "hour") {
6654
+ filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(date));
6655
+ } else if (period === "day") {
6656
+ filtered = filtered.filter((t) => t.cohortDate === date);
6657
+ } else if (period === "month") {
6658
+ filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth.startsWith(date));
6659
+ }
6660
+ } else if (startDate && endDate) {
6661
+ if (period === "hour") {
6662
+ filtered = filtered.filter((t) => t.cohortHour && t.cohortHour >= startDate && t.cohortHour <= endDate);
6663
+ } else if (period === "day") {
6664
+ filtered = filtered.filter((t) => t.cohortDate && t.cohortDate >= startDate && t.cohortDate <= endDate);
6665
+ } else if (period === "month") {
6666
+ filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth >= startDate && t.cohortMonth <= endDate);
6667
+ }
6668
+ } else if (month) {
6669
+ if (period === "hour") {
6670
+ filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(month));
6671
+ } else if (period === "day") {
6672
+ filtered = filtered.filter((t) => t.cohortDate && t.cohortDate.startsWith(month));
6673
+ }
6674
+ } else if (year) {
6675
+ if (period === "hour") {
6676
+ filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(String(year)));
6677
+ } else if (period === "day") {
6678
+ filtered = filtered.filter((t) => t.cohortDate && t.cohortDate.startsWith(String(year)));
6679
+ } else if (period === "month") {
6680
+ filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth.startsWith(String(year)));
6681
+ }
6682
+ }
6683
+ const cohortField = period === "hour" ? "cohortHour" : period === "day" ? "cohortDate" : "cohortMonth";
6684
+ const aggregated = aggregateTransactionsByCohort(filtered, cohortField);
6685
+ return aggregated;
6686
+ }
6687
+ function aggregateTransactionsByCohort(transactions, cohortField) {
6688
+ const groups = {};
6689
+ for (const txn of transactions) {
6690
+ const cohort = txn[cohortField];
6691
+ if (!cohort) continue;
6692
+ if (!groups[cohort]) {
6693
+ groups[cohort] = {
6694
+ cohort,
6695
+ count: 0,
6696
+ sum: 0,
6697
+ min: Infinity,
6698
+ max: -Infinity,
6699
+ recordCount: /* @__PURE__ */ new Set(),
6700
+ operations: {}
6701
+ };
6702
+ }
6703
+ const group = groups[cohort];
6704
+ const signedValue = txn.operation === "sub" ? -txn.value : txn.value;
6705
+ group.count++;
6706
+ group.sum += signedValue;
6707
+ group.min = Math.min(group.min, signedValue);
6708
+ group.max = Math.max(group.max, signedValue);
6709
+ group.recordCount.add(txn.originalId);
6710
+ const op = txn.operation;
6711
+ if (!group.operations[op]) {
6712
+ group.operations[op] = { count: 0, sum: 0 };
6713
+ }
6714
+ group.operations[op].count++;
6715
+ group.operations[op].sum += signedValue;
6716
+ }
6717
+ return Object.values(groups).map((g) => ({
6718
+ cohort: g.cohort,
6719
+ count: g.count,
6720
+ sum: g.sum,
6721
+ avg: g.sum / g.count,
6722
+ min: g.min === Infinity ? 0 : g.min,
6723
+ max: g.max === -Infinity ? 0 : g.max,
6724
+ recordCount: g.recordCount.size,
6725
+ operations: g.operations
6726
+ })).sort((a, b) => a.cohort.localeCompare(b.cohort));
6727
+ }
6569
6728
  async function getMonthByDay(resourceName, field, month, options, fieldHandlers) {
6570
6729
  const year = parseInt(month.substring(0, 4));
6571
6730
  const monthNum = parseInt(month.substring(5, 7));
@@ -6600,6 +6759,8 @@ async function getLastNDays(resourceName, field, days, options, fieldHandlers) {
6600
6759
  return date.toISOString().substring(0, 10);
6601
6760
  }).reverse();
6602
6761
  const data = await getAnalytics(resourceName, field, {
6762
+ ...options,
6763
+ // ✅ Include all options (recordId, etc.)
6603
6764
  period: "day",
6604
6765
  startDate: dates[0],
6605
6766
  endDate: dates[dates.length - 1]
@@ -6793,6 +6954,8 @@ async function getLastNHours(resourceName, field, hours = 24, options, fieldHand
6793
6954
  const startHour = hoursAgo.toISOString().substring(0, 13);
6794
6955
  const endHour = now.toISOString().substring(0, 13);
6795
6956
  const data = await getAnalytics(resourceName, field, {
6957
+ ...options,
6958
+ // ✅ Include all options (recordId, etc.)
6796
6959
  period: "hour",
6797
6960
  startDate: startHour,
6798
6961
  endDate: endHour
@@ -6840,6 +7003,8 @@ async function getLastNMonths(resourceName, field, months = 12, options, fieldHa
6840
7003
  const startDate = monthsAgo.toISOString().substring(0, 7);
6841
7004
  const endDate = now.toISOString().substring(0, 7);
6842
7005
  const data = await getAnalytics(resourceName, field, {
7006
+ ...options,
7007
+ // ✅ Include all options (recordId, etc.)
6843
7008
  period: "month",
6844
7009
  startDate,
6845
7010
  endDate
@@ -7032,7 +7197,8 @@ async function completeFieldSetup(handler, database, config, plugin) {
7032
7197
  operation: "string|required",
7033
7198
  timestamp: "string|required",
7034
7199
  cohortDate: "string|required",
7035
- cohortHour: "string|required",
7200
+ cohortHour: "string|optional",
7201
+ // ✅ FIX BUG #2: Changed from required to optional for migration compatibility
7036
7202
  cohortWeek: "string|optional",
7037
7203
  cohortMonth: "string|optional",
7038
7204
  source: "string|optional",
@@ -7081,6 +7247,28 @@ async function createAnalyticsResource(handler, database, resourceName, fieldNam
7081
7247
  },
7082
7248
  behavior: "body-overflow",
7083
7249
  timestamps: false,
7250
+ asyncPartitions: true,
7251
+ // ✅ Multi-attribute partitions for optimal analytics query performance
7252
+ partitions: {
7253
+ // Query by period (hour/day/week/month)
7254
+ byPeriod: {
7255
+ fields: { period: "string" }
7256
+ },
7257
+ // Query by period + cohort (e.g., all hour records for specific hours)
7258
+ byPeriodCohort: {
7259
+ fields: {
7260
+ period: "string",
7261
+ cohort: "string"
7262
+ }
7263
+ },
7264
+ // Query by field + period (e.g., all daily analytics for clicks field)
7265
+ byFieldPeriod: {
7266
+ fields: {
7267
+ field: "string",
7268
+ period: "string"
7269
+ }
7270
+ }
7271
+ },
7084
7272
  createdBy: "EventualConsistencyPlugin"
7085
7273
  })
7086
7274
  );
@@ -13610,7 +13798,7 @@ class Database extends EventEmitter {
13610
13798
  this.id = idGenerator(7);
13611
13799
  this.version = "1";
13612
13800
  this.s3dbVersion = (() => {
13613
- const [ok, err, version] = tryFn(() => true ? "11.1.0" : "latest");
13801
+ const [ok, err, version] = tryFn(() => true ? "11.2.2" : "latest");
13614
13802
  return ok ? version : "latest";
13615
13803
  })();
13616
13804
  this.resources = {};