s3db.js 10.0.4 → 10.0.6

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
@@ -4445,8 +4445,24 @@ class EventualConsistencyPlugin extends Plugin {
4445
4445
  this.addHelperMethods();
4446
4446
  if (this.config.autoConsolidate) {
4447
4447
  this.startConsolidationTimer();
4448
+ if (this.config.verbose) {
4449
+ console.log(
4450
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Auto-consolidation ENABLED (interval: ${this.config.consolidationInterval}s, window: ${this.config.consolidationWindow}h, mode: ${this.config.mode})`
4451
+ );
4452
+ }
4453
+ } else {
4454
+ if (this.config.verbose) {
4455
+ console.log(
4456
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Auto-consolidation DISABLED (manual consolidation only)`
4457
+ );
4458
+ }
4448
4459
  }
4449
4460
  this.startGarbageCollectionTimer();
4461
+ if (this.config.verbose) {
4462
+ console.log(
4463
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Setup complete. Resources: ${this.config.resource}_transactions_${this.config.field}, ${this.config.resource}_consolidation_locks_${this.config.field}${this.config.enableAnalytics ? `, ${this.config.resource}_analytics_${this.config.field}` : ""}`
4464
+ );
4465
+ }
4450
4466
  }
4451
4467
  async onStart() {
4452
4468
  if (this.deferredSetup) {
@@ -4707,11 +4723,21 @@ class EventualConsistencyPlugin extends Plugin {
4707
4723
  };
4708
4724
  if (this.config.batchTransactions) {
4709
4725
  this.pendingTransactions.set(transaction.id, transaction);
4726
+ if (this.config.verbose) {
4727
+ console.log(
4728
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Transaction batched: ${data.operation} ${data.value} for ${data.originalId} (batch: ${this.pendingTransactions.size}/${this.config.batchSize})`
4729
+ );
4730
+ }
4710
4731
  if (this.pendingTransactions.size >= this.config.batchSize) {
4711
4732
  await this.flushPendingTransactions();
4712
4733
  }
4713
4734
  } else {
4714
4735
  await this.transactionResource.insert(transaction);
4736
+ if (this.config.verbose) {
4737
+ console.log(
4738
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Transaction created: ${data.operation} ${data.value} for ${data.originalId} (cohort: ${cohortInfo.hour}, applied: false)`
4739
+ );
4740
+ }
4715
4741
  }
4716
4742
  return transaction;
4717
4743
  }
@@ -4776,11 +4802,23 @@ class EventualConsistencyPlugin extends Plugin {
4776
4802
  }
4777
4803
  startConsolidationTimer() {
4778
4804
  const intervalMs = this.config.consolidationInterval * 1e3;
4805
+ if (this.config.verbose) {
4806
+ const nextRun = new Date(Date.now() + intervalMs);
4807
+ console.log(
4808
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidation timer started. Next run at ${nextRun.toISOString()} (every ${this.config.consolidationInterval}s)`
4809
+ );
4810
+ }
4779
4811
  this.consolidationTimer = setInterval(async () => {
4780
4812
  await this.runConsolidation();
4781
4813
  }, intervalMs);
4782
4814
  }
4783
4815
  async runConsolidation() {
4816
+ const startTime = Date.now();
4817
+ if (this.config.verbose) {
4818
+ console.log(
4819
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Starting consolidation run at ${(/* @__PURE__ */ new Date()).toISOString()}`
4820
+ );
4821
+ }
4784
4822
  try {
4785
4823
  const now = /* @__PURE__ */ new Date();
4786
4824
  const hoursToCheck = this.config.consolidationWindow || 24;
@@ -4790,6 +4828,11 @@ class EventualConsistencyPlugin extends Plugin {
4790
4828
  const cohortInfo = this.getCohortInfo(date);
4791
4829
  cohortHours.push(cohortInfo.hour);
4792
4830
  }
4831
+ if (this.config.verbose) {
4832
+ console.log(
4833
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Querying ${hoursToCheck} hour partitions for pending transactions...`
4834
+ );
4835
+ }
4793
4836
  const transactionsByHour = await Promise.all(
4794
4837
  cohortHours.map(async (cohortHour) => {
4795
4838
  const [ok, err, txns] = await tryFn(
@@ -4804,26 +4847,47 @@ class EventualConsistencyPlugin extends Plugin {
4804
4847
  const transactions = transactionsByHour.flat();
4805
4848
  if (transactions.length === 0) {
4806
4849
  if (this.config.verbose) {
4807
- console.log(`[EventualConsistency] No pending transactions to consolidate`);
4850
+ console.log(
4851
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - No pending transactions found. Next run in ${this.config.consolidationInterval}s`
4852
+ );
4808
4853
  }
4809
4854
  return;
4810
4855
  }
4811
4856
  const uniqueIds = [...new Set(transactions.map((t) => t.originalId))];
4857
+ if (this.config.verbose) {
4858
+ console.log(
4859
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Found ${transactions.length} pending transactions for ${uniqueIds.length} records. Consolidating with concurrency=${this.config.consolidationConcurrency}...`
4860
+ );
4861
+ }
4812
4862
  const { results, errors } = await PromisePool.for(uniqueIds).withConcurrency(this.config.consolidationConcurrency).process(async (id) => {
4813
4863
  return await this.consolidateRecord(id);
4814
4864
  });
4865
+ const duration = Date.now() - startTime;
4815
4866
  if (errors && errors.length > 0) {
4816
- console.error(`Consolidation completed with ${errors.length} errors:`, errors);
4867
+ console.error(
4868
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidation completed with ${errors.length} errors in ${duration}ms:`,
4869
+ errors
4870
+ );
4871
+ }
4872
+ if (this.config.verbose) {
4873
+ console.log(
4874
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidation complete: ${results.length} records consolidated in ${duration}ms (${errors.length} errors). Next run in ${this.config.consolidationInterval}s`
4875
+ );
4817
4876
  }
4818
4877
  this.emit("eventual-consistency.consolidated", {
4819
4878
  resource: this.config.resource,
4820
4879
  field: this.config.field,
4821
4880
  recordCount: uniqueIds.length,
4822
4881
  successCount: results.length,
4823
- errorCount: errors.length
4882
+ errorCount: errors.length,
4883
+ duration
4824
4884
  });
4825
4885
  } catch (error) {
4826
- console.error("Consolidation error:", error);
4886
+ const duration = Date.now() - startTime;
4887
+ console.error(
4888
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidation error after ${duration}ms:`,
4889
+ error
4890
+ );
4827
4891
  this.emit("eventual-consistency.consolidation-error", error);
4828
4892
  }
4829
4893
  }
@@ -4858,8 +4922,18 @@ class EventualConsistencyPlugin extends Plugin {
4858
4922
  })
4859
4923
  );
4860
4924
  if (!ok || !transactions || transactions.length === 0) {
4925
+ if (this.config.verbose) {
4926
+ console.log(
4927
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - No pending transactions for ${originalId}, skipping`
4928
+ );
4929
+ }
4861
4930
  return currentValue;
4862
4931
  }
4932
+ if (this.config.verbose) {
4933
+ console.log(
4934
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidating ${originalId}: ${transactions.length} pending transactions (current: ${currentValue})`
4935
+ );
4936
+ }
4863
4937
  transactions.sort(
4864
4938
  (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
4865
4939
  );
@@ -4868,6 +4942,11 @@ class EventualConsistencyPlugin extends Plugin {
4868
4942
  transactions.unshift(this._createSyntheticSetTransaction(currentValue));
4869
4943
  }
4870
4944
  const consolidatedValue = this.config.reducer(transactions);
4945
+ if (this.config.verbose) {
4946
+ console.log(
4947
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ${originalId}: ${currentValue} \u2192 ${consolidatedValue} (${consolidatedValue > currentValue ? "+" : ""}${consolidatedValue - currentValue})`
4948
+ );
4949
+ }
4871
4950
  const [updateOk, updateErr] = await tryFn(
4872
4951
  () => this.targetResource.update(originalId, {
4873
4952
  [this.config.field]: consolidatedValue
@@ -5104,21 +5183,43 @@ class EventualConsistencyPlugin extends Plugin {
5104
5183
  */
5105
5184
  async updateAnalytics(transactions) {
5106
5185
  if (!this.analyticsResource || transactions.length === 0) return;
5186
+ if (this.config.verbose) {
5187
+ console.log(
5188
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Updating analytics for ${transactions.length} transactions...`
5189
+ );
5190
+ }
5107
5191
  try {
5108
5192
  const byHour = this._groupByCohort(transactions, "cohortHour");
5193
+ const cohortCount = Object.keys(byHour).length;
5194
+ if (this.config.verbose) {
5195
+ console.log(
5196
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Updating ${cohortCount} hourly analytics cohorts...`
5197
+ );
5198
+ }
5109
5199
  for (const [cohort, txns] of Object.entries(byHour)) {
5110
5200
  await this._upsertAnalytics("hour", cohort, txns);
5111
5201
  }
5112
5202
  if (this.config.analyticsConfig.rollupStrategy === "incremental") {
5113
5203
  const uniqueHours = Object.keys(byHour);
5204
+ if (this.config.verbose) {
5205
+ console.log(
5206
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Rolling up ${uniqueHours.length} hours to daily/monthly analytics...`
5207
+ );
5208
+ }
5114
5209
  for (const cohortHour of uniqueHours) {
5115
5210
  await this._rollupAnalytics(cohortHour);
5116
5211
  }
5117
5212
  }
5118
- } catch (error) {
5119
5213
  if (this.config.verbose) {
5120
- console.warn(`[EventualConsistency] Analytics update error:`, error.message);
5214
+ console.log(
5215
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Analytics update complete for ${cohortCount} cohorts`
5216
+ );
5121
5217
  }
5218
+ } catch (error) {
5219
+ console.warn(
5220
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - Analytics update error:`,
5221
+ error.message
5222
+ );
5122
5223
  }
5123
5224
  }
5124
5225
  /**
@@ -5348,79 +5449,163 @@ class EventualConsistencyPlugin extends Plugin {
5348
5449
  recordCount: a.recordCount
5349
5450
  }));
5350
5451
  }
5452
+ /**
5453
+ * Fill gaps in analytics data with zeros for continuous time series
5454
+ * @private
5455
+ * @param {Array} data - Sparse analytics data
5456
+ * @param {string} period - Period type ('hour', 'day', 'month')
5457
+ * @param {string} startDate - Start date (ISO format)
5458
+ * @param {string} endDate - End date (ISO format)
5459
+ * @returns {Array} Complete time series with gaps filled
5460
+ */
5461
+ _fillGaps(data, period, startDate, endDate) {
5462
+ if (!data || data.length === 0) {
5463
+ data = [];
5464
+ }
5465
+ const dataMap = /* @__PURE__ */ new Map();
5466
+ data.forEach((item) => {
5467
+ dataMap.set(item.cohort, item);
5468
+ });
5469
+ const result = [];
5470
+ const emptyRecord = {
5471
+ count: 0,
5472
+ sum: 0,
5473
+ avg: 0,
5474
+ min: 0,
5475
+ max: 0,
5476
+ recordCount: 0
5477
+ };
5478
+ if (period === "hour") {
5479
+ const start = /* @__PURE__ */ new Date(startDate + "T00:00:00Z");
5480
+ const end = /* @__PURE__ */ new Date(endDate + "T23:59:59Z");
5481
+ for (let dt = new Date(start); dt <= end; dt.setHours(dt.getHours() + 1)) {
5482
+ const cohort = dt.toISOString().substring(0, 13);
5483
+ result.push(dataMap.get(cohort) || { cohort, ...emptyRecord });
5484
+ }
5485
+ } else if (period === "day") {
5486
+ const start = new Date(startDate);
5487
+ const end = new Date(endDate);
5488
+ for (let dt = new Date(start); dt <= end; dt.setDate(dt.getDate() + 1)) {
5489
+ const cohort = dt.toISOString().substring(0, 10);
5490
+ result.push(dataMap.get(cohort) || { cohort, ...emptyRecord });
5491
+ }
5492
+ } else if (period === "month") {
5493
+ const startYear = parseInt(startDate.substring(0, 4));
5494
+ const startMonth = parseInt(startDate.substring(5, 7));
5495
+ const endYear = parseInt(endDate.substring(0, 4));
5496
+ const endMonth = parseInt(endDate.substring(5, 7));
5497
+ for (let year = startYear; year <= endYear; year++) {
5498
+ const firstMonth = year === startYear ? startMonth : 1;
5499
+ const lastMonth = year === endYear ? endMonth : 12;
5500
+ for (let month = firstMonth; month <= lastMonth; month++) {
5501
+ const cohort = `${year}-${month.toString().padStart(2, "0")}`;
5502
+ result.push(dataMap.get(cohort) || { cohort, ...emptyRecord });
5503
+ }
5504
+ }
5505
+ }
5506
+ return result;
5507
+ }
5351
5508
  /**
5352
5509
  * Get analytics for entire month, broken down by days
5353
5510
  * @param {string} resourceName - Resource name
5354
5511
  * @param {string} field - Field name
5355
5512
  * @param {string} month - Month in YYYY-MM format
5513
+ * @param {Object} options - Options
5514
+ * @param {boolean} options.fillGaps - Fill missing days with zeros (default: false)
5356
5515
  * @returns {Promise<Array>} Daily analytics for the month
5357
5516
  */
5358
- async getMonthByDay(resourceName, field, month) {
5517
+ async getMonthByDay(resourceName, field, month, options = {}) {
5359
5518
  const year = parseInt(month.substring(0, 4));
5360
5519
  const monthNum = parseInt(month.substring(5, 7));
5361
5520
  const firstDay = new Date(year, monthNum - 1, 1);
5362
5521
  const lastDay = new Date(year, monthNum, 0);
5363
5522
  const startDate = firstDay.toISOString().substring(0, 10);
5364
5523
  const endDate = lastDay.toISOString().substring(0, 10);
5365
- return await this.getAnalytics(resourceName, field, {
5524
+ const data = await this.getAnalytics(resourceName, field, {
5366
5525
  period: "day",
5367
5526
  startDate,
5368
5527
  endDate
5369
5528
  });
5529
+ if (options.fillGaps) {
5530
+ return this._fillGaps(data, "day", startDate, endDate);
5531
+ }
5532
+ return data;
5370
5533
  }
5371
5534
  /**
5372
5535
  * Get analytics for entire day, broken down by hours
5373
5536
  * @param {string} resourceName - Resource name
5374
5537
  * @param {string} field - Field name
5375
5538
  * @param {string} date - Date in YYYY-MM-DD format
5539
+ * @param {Object} options - Options
5540
+ * @param {boolean} options.fillGaps - Fill missing hours with zeros (default: false)
5376
5541
  * @returns {Promise<Array>} Hourly analytics for the day
5377
5542
  */
5378
- async getDayByHour(resourceName, field, date) {
5379
- return await this.getAnalytics(resourceName, field, {
5543
+ async getDayByHour(resourceName, field, date, options = {}) {
5544
+ const data = await this.getAnalytics(resourceName, field, {
5380
5545
  period: "hour",
5381
5546
  date
5382
5547
  });
5548
+ if (options.fillGaps) {
5549
+ return this._fillGaps(data, "hour", date, date);
5550
+ }
5551
+ return data;
5383
5552
  }
5384
5553
  /**
5385
5554
  * Get analytics for last N days, broken down by days
5386
5555
  * @param {string} resourceName - Resource name
5387
5556
  * @param {string} field - Field name
5388
5557
  * @param {number} days - Number of days to look back (default: 7)
5558
+ * @param {Object} options - Options
5559
+ * @param {boolean} options.fillGaps - Fill missing days with zeros (default: false)
5389
5560
  * @returns {Promise<Array>} Daily analytics
5390
5561
  */
5391
- async getLastNDays(resourceName, field, days = 7) {
5562
+ async getLastNDays(resourceName, field, days = 7, options = {}) {
5392
5563
  const dates = Array.from({ length: days }, (_, i) => {
5393
5564
  const date = /* @__PURE__ */ new Date();
5394
5565
  date.setDate(date.getDate() - i);
5395
5566
  return date.toISOString().substring(0, 10);
5396
5567
  }).reverse();
5397
- return await this.getAnalytics(resourceName, field, {
5568
+ const data = await this.getAnalytics(resourceName, field, {
5398
5569
  period: "day",
5399
5570
  startDate: dates[0],
5400
5571
  endDate: dates[dates.length - 1]
5401
5572
  });
5573
+ if (options.fillGaps) {
5574
+ return this._fillGaps(data, "day", dates[0], dates[dates.length - 1]);
5575
+ }
5576
+ return data;
5402
5577
  }
5403
5578
  /**
5404
5579
  * Get analytics for entire year, broken down by months
5405
5580
  * @param {string} resourceName - Resource name
5406
5581
  * @param {string} field - Field name
5407
5582
  * @param {number} year - Year (e.g., 2025)
5583
+ * @param {Object} options - Options
5584
+ * @param {boolean} options.fillGaps - Fill missing months with zeros (default: false)
5408
5585
  * @returns {Promise<Array>} Monthly analytics for the year
5409
5586
  */
5410
- async getYearByMonth(resourceName, field, year) {
5411
- return await this.getAnalytics(resourceName, field, {
5587
+ async getYearByMonth(resourceName, field, year, options = {}) {
5588
+ const data = await this.getAnalytics(resourceName, field, {
5412
5589
  period: "month",
5413
5590
  year
5414
5591
  });
5592
+ if (options.fillGaps) {
5593
+ const startDate = `${year}-01`;
5594
+ const endDate = `${year}-12`;
5595
+ return this._fillGaps(data, "month", startDate, endDate);
5596
+ }
5597
+ return data;
5415
5598
  }
5416
5599
  /**
5417
5600
  * Get analytics for entire month, broken down by hours
5418
5601
  * @param {string} resourceName - Resource name
5419
5602
  * @param {string} field - Field name
5420
5603
  * @param {string} month - Month in YYYY-MM format (or 'last' for previous month)
5604
+ * @param {Object} options - Options
5605
+ * @param {boolean} options.fillGaps - Fill missing hours with zeros (default: false)
5421
5606
  * @returns {Promise<Array>} Hourly analytics for the month (up to 24*31=744 records)
5422
5607
  */
5423
- async getMonthByHour(resourceName, field, month) {
5608
+ async getMonthByHour(resourceName, field, month, options = {}) {
5424
5609
  let year, monthNum;
5425
5610
  if (month === "last") {
5426
5611
  const now = /* @__PURE__ */ new Date();
@@ -5435,11 +5620,15 @@ class EventualConsistencyPlugin extends Plugin {
5435
5620
  const lastDay = new Date(year, monthNum, 0);
5436
5621
  const startDate = firstDay.toISOString().substring(0, 10);
5437
5622
  const endDate = lastDay.toISOString().substring(0, 10);
5438
- return await this.getAnalytics(resourceName, field, {
5623
+ const data = await this.getAnalytics(resourceName, field, {
5439
5624
  period: "hour",
5440
5625
  startDate,
5441
5626
  endDate
5442
5627
  });
5628
+ if (options.fillGaps) {
5629
+ return this._fillGaps(data, "hour", startDate, endDate);
5630
+ }
5631
+ return data;
5443
5632
  }
5444
5633
  /**
5445
5634
  * Get top records by volume
@@ -11560,7 +11749,7 @@ class Database extends EventEmitter {
11560
11749
  this.id = idGenerator(7);
11561
11750
  this.version = "1";
11562
11751
  this.s3dbVersion = (() => {
11563
- const [ok, err, version] = tryFn(() => true ? "10.0.4" : "latest");
11752
+ const [ok, err, version] = tryFn(() => true ? "10.0.6" : "latest");
11564
11753
  return ok ? version : "latest";
11565
11754
  })();
11566
11755
  this.resources = {};