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