s3db.js 10.0.12 → 10.0.13

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
@@ -4545,6 +4545,12 @@ class EventualConsistencyPlugin extends Plugin {
4545
4545
  for (const [resourceName, fieldHandlers] of this.fieldHandlers) {
4546
4546
  for (const [fieldName, handler] of fieldHandlers) {
4547
4547
  if (!handler.deferredSetup) {
4548
+ if (this.config.autoConsolidate && this.config.mode === "async") {
4549
+ this.startConsolidationTimerForHandler(handler, resourceName, fieldName);
4550
+ }
4551
+ if (this.config.transactionRetention && this.config.transactionRetention > 0) {
4552
+ this.startGarbageCollectionTimerForHandler(handler, resourceName, fieldName);
4553
+ }
4548
4554
  this.emit("eventual-consistency.started", {
4549
4555
  resource: resourceName,
4550
4556
  field: fieldName,
@@ -4658,68 +4664,189 @@ class EventualConsistencyPlugin extends Plugin {
4658
4664
  const resource = firstHandler.targetResource;
4659
4665
  const plugin = this;
4660
4666
  resource.set = async (id, field, value) => {
4661
- const { plugin: fieldPlugin } = plugin._resolveFieldAndPlugin(resource, field, value);
4662
- await fieldPlugin.createTransaction(fieldPlugin, {
4667
+ const { plugin: handler } = plugin._resolveFieldAndPlugin(resource, field, value);
4668
+ const now = /* @__PURE__ */ new Date();
4669
+ const cohortInfo = plugin.getCohortInfo(now);
4670
+ const transaction = {
4671
+ id: idGenerator(),
4663
4672
  originalId: id,
4664
- operation: "set",
4673
+ field: handler.field,
4665
4674
  value,
4666
- source: "set"
4667
- });
4668
- if (fieldPlugin.config.mode === "sync") {
4669
- return await fieldPlugin._syncModeConsolidate(fieldPlugin, id, field);
4675
+ operation: "set",
4676
+ timestamp: now.toISOString(),
4677
+ cohortDate: cohortInfo.date,
4678
+ cohortHour: cohortInfo.hour,
4679
+ cohortMonth: cohortInfo.month,
4680
+ source: "set",
4681
+ applied: false
4682
+ };
4683
+ await handler.transactionResource.insert(transaction);
4684
+ if (plugin.config.mode === "sync") {
4685
+ const oldResource = plugin.config.resource;
4686
+ const oldField = plugin.config.field;
4687
+ const oldTransactionResource = plugin.transactionResource;
4688
+ const oldTargetResource = plugin.targetResource;
4689
+ const oldLockResource = plugin.lockResource;
4690
+ const oldAnalyticsResource = plugin.analyticsResource;
4691
+ plugin.config.resource = handler.resource;
4692
+ plugin.config.field = handler.field;
4693
+ plugin.transactionResource = handler.transactionResource;
4694
+ plugin.targetResource = handler.targetResource;
4695
+ plugin.lockResource = handler.lockResource;
4696
+ plugin.analyticsResource = handler.analyticsResource;
4697
+ const result = await plugin._syncModeConsolidate(id, field);
4698
+ plugin.config.resource = oldResource;
4699
+ plugin.config.field = oldField;
4700
+ plugin.transactionResource = oldTransactionResource;
4701
+ plugin.targetResource = oldTargetResource;
4702
+ plugin.lockResource = oldLockResource;
4703
+ plugin.analyticsResource = oldAnalyticsResource;
4704
+ return result;
4670
4705
  }
4671
4706
  return value;
4672
4707
  };
4673
4708
  resource.add = async (id, field, amount) => {
4674
- const { plugin: fieldPlugin } = plugin._resolveFieldAndPlugin(resource, field, amount);
4675
- await fieldPlugin.createTransaction(fieldPlugin, {
4709
+ const { plugin: handler } = plugin._resolveFieldAndPlugin(resource, field, amount);
4710
+ const now = /* @__PURE__ */ new Date();
4711
+ const cohortInfo = plugin.getCohortInfo(now);
4712
+ const transaction = {
4713
+ id: idGenerator(),
4676
4714
  originalId: id,
4677
- operation: "add",
4715
+ field: handler.field,
4678
4716
  value: amount,
4679
- source: "add"
4680
- });
4681
- if (fieldPlugin.config.mode === "sync") {
4682
- return await fieldPlugin._syncModeConsolidate(fieldPlugin, id, field);
4717
+ operation: "add",
4718
+ timestamp: now.toISOString(),
4719
+ cohortDate: cohortInfo.date,
4720
+ cohortHour: cohortInfo.hour,
4721
+ cohortMonth: cohortInfo.month,
4722
+ source: "add",
4723
+ applied: false
4724
+ };
4725
+ await handler.transactionResource.insert(transaction);
4726
+ if (plugin.config.mode === "sync") {
4727
+ const oldResource = plugin.config.resource;
4728
+ const oldField = plugin.config.field;
4729
+ const oldTransactionResource = plugin.transactionResource;
4730
+ const oldTargetResource = plugin.targetResource;
4731
+ const oldLockResource = plugin.lockResource;
4732
+ const oldAnalyticsResource = plugin.analyticsResource;
4733
+ plugin.config.resource = handler.resource;
4734
+ plugin.config.field = handler.field;
4735
+ plugin.transactionResource = handler.transactionResource;
4736
+ plugin.targetResource = handler.targetResource;
4737
+ plugin.lockResource = handler.lockResource;
4738
+ plugin.analyticsResource = handler.analyticsResource;
4739
+ const result = await plugin._syncModeConsolidate(id, field);
4740
+ plugin.config.resource = oldResource;
4741
+ plugin.config.field = oldField;
4742
+ plugin.transactionResource = oldTransactionResource;
4743
+ plugin.targetResource = oldTargetResource;
4744
+ plugin.lockResource = oldLockResource;
4745
+ plugin.analyticsResource = oldAnalyticsResource;
4746
+ return result;
4683
4747
  }
4684
- const currentValue = await fieldPlugin.getConsolidatedValue(fieldPlugin, id);
4748
+ const [ok, err, record] = await tryFn(() => handler.targetResource.get(id));
4749
+ const currentValue = ok && record ? record[field] || 0 : 0;
4685
4750
  return currentValue + amount;
4686
4751
  };
4687
4752
  resource.sub = async (id, field, amount) => {
4688
- const { plugin: fieldPlugin } = plugin._resolveFieldAndPlugin(resource, field, amount);
4689
- await fieldPlugin.createTransaction(fieldPlugin, {
4753
+ const { plugin: handler } = plugin._resolveFieldAndPlugin(resource, field, amount);
4754
+ const now = /* @__PURE__ */ new Date();
4755
+ const cohortInfo = plugin.getCohortInfo(now);
4756
+ const transaction = {
4757
+ id: idGenerator(),
4690
4758
  originalId: id,
4691
- operation: "sub",
4759
+ field: handler.field,
4692
4760
  value: amount,
4693
- source: "sub"
4694
- });
4695
- if (fieldPlugin.config.mode === "sync") {
4696
- return await fieldPlugin._syncModeConsolidate(fieldPlugin, id, field);
4761
+ operation: "sub",
4762
+ timestamp: now.toISOString(),
4763
+ cohortDate: cohortInfo.date,
4764
+ cohortHour: cohortInfo.hour,
4765
+ cohortMonth: cohortInfo.month,
4766
+ source: "sub",
4767
+ applied: false
4768
+ };
4769
+ await handler.transactionResource.insert(transaction);
4770
+ if (plugin.config.mode === "sync") {
4771
+ const oldResource = plugin.config.resource;
4772
+ const oldField = plugin.config.field;
4773
+ const oldTransactionResource = plugin.transactionResource;
4774
+ const oldTargetResource = plugin.targetResource;
4775
+ const oldLockResource = plugin.lockResource;
4776
+ const oldAnalyticsResource = plugin.analyticsResource;
4777
+ plugin.config.resource = handler.resource;
4778
+ plugin.config.field = handler.field;
4779
+ plugin.transactionResource = handler.transactionResource;
4780
+ plugin.targetResource = handler.targetResource;
4781
+ plugin.lockResource = handler.lockResource;
4782
+ plugin.analyticsResource = handler.analyticsResource;
4783
+ const result = await plugin._syncModeConsolidate(id, field);
4784
+ plugin.config.resource = oldResource;
4785
+ plugin.config.field = oldField;
4786
+ plugin.transactionResource = oldTransactionResource;
4787
+ plugin.targetResource = oldTargetResource;
4788
+ plugin.lockResource = oldLockResource;
4789
+ plugin.analyticsResource = oldAnalyticsResource;
4790
+ return result;
4697
4791
  }
4698
- const currentValue = await fieldPlugin.getConsolidatedValue(fieldPlugin, id);
4792
+ const [ok, err, record] = await tryFn(() => handler.targetResource.get(id));
4793
+ const currentValue = ok && record ? record[field] || 0 : 0;
4699
4794
  return currentValue - amount;
4700
4795
  };
4701
4796
  resource.consolidate = async (id, field) => {
4702
4797
  if (!field) {
4703
4798
  throw new Error(`Field parameter is required: consolidate(id, field)`);
4704
4799
  }
4705
- const fieldPlugin = resource._eventualConsistencyPlugins[field];
4706
- if (!fieldPlugin) {
4800
+ const handler = resource._eventualConsistencyPlugins[field];
4801
+ if (!handler) {
4707
4802
  const availableFields = Object.keys(resource._eventualConsistencyPlugins).join(", ");
4708
4803
  throw new Error(
4709
4804
  `No eventual consistency plugin found for field "${field}". Available fields: ${availableFields}`
4710
4805
  );
4711
4806
  }
4712
- return await fieldPlugin.consolidateRecord(fieldPlugin, id);
4807
+ const oldResource = plugin.config.resource;
4808
+ const oldField = plugin.config.field;
4809
+ const oldTransactionResource = plugin.transactionResource;
4810
+ const oldTargetResource = plugin.targetResource;
4811
+ const oldLockResource = plugin.lockResource;
4812
+ const oldAnalyticsResource = plugin.analyticsResource;
4813
+ plugin.config.resource = handler.resource;
4814
+ plugin.config.field = handler.field;
4815
+ plugin.transactionResource = handler.transactionResource;
4816
+ plugin.targetResource = handler.targetResource;
4817
+ plugin.lockResource = handler.lockResource;
4818
+ plugin.analyticsResource = handler.analyticsResource;
4819
+ const result = await plugin.consolidateRecord(id);
4820
+ plugin.config.resource = oldResource;
4821
+ plugin.config.field = oldField;
4822
+ plugin.transactionResource = oldTransactionResource;
4823
+ plugin.targetResource = oldTargetResource;
4824
+ plugin.lockResource = oldLockResource;
4825
+ plugin.analyticsResource = oldAnalyticsResource;
4826
+ return result;
4713
4827
  };
4714
4828
  resource.getConsolidatedValue = async (id, field, options = {}) => {
4715
- const fieldPlugin = resource._eventualConsistencyPlugins[field];
4716
- if (!fieldPlugin) {
4829
+ const handler = resource._eventualConsistencyPlugins[field];
4830
+ if (!handler) {
4717
4831
  const availableFields = Object.keys(resource._eventualConsistencyPlugins).join(", ");
4718
4832
  throw new Error(
4719
4833
  `No eventual consistency plugin found for field "${field}". Available fields: ${availableFields}`
4720
4834
  );
4721
4835
  }
4722
- return await fieldPlugin.getConsolidatedValue(fieldPlugin, id, options);
4836
+ const oldResource = plugin.config.resource;
4837
+ const oldField = plugin.config.field;
4838
+ const oldTransactionResource = plugin.transactionResource;
4839
+ const oldTargetResource = plugin.targetResource;
4840
+ plugin.config.resource = handler.resource;
4841
+ plugin.config.field = handler.field;
4842
+ plugin.transactionResource = handler.transactionResource;
4843
+ plugin.targetResource = handler.targetResource;
4844
+ const result = await plugin.getConsolidatedValue(id, options);
4845
+ plugin.config.resource = oldResource;
4846
+ plugin.config.field = oldField;
4847
+ plugin.transactionResource = oldTransactionResource;
4848
+ plugin.targetResource = oldTargetResource;
4849
+ return result;
4723
4850
  };
4724
4851
  }
4725
4852
  async createTransaction(handler, data) {
@@ -4847,6 +4974,42 @@ class EventualConsistencyPlugin extends Plugin {
4847
4974
  await this.runConsolidation();
4848
4975
  }, intervalMs);
4849
4976
  }
4977
+ startConsolidationTimerForHandler(handler, resourceName, fieldName) {
4978
+ const intervalMs = this.config.consolidationInterval * 1e3;
4979
+ if (this.config.verbose) {
4980
+ const nextRun = new Date(Date.now() + intervalMs);
4981
+ console.log(
4982
+ `[EventualConsistency] ${resourceName}.${fieldName} - Consolidation timer started. Next run at ${nextRun.toISOString()} (every ${this.config.consolidationInterval}s)`
4983
+ );
4984
+ }
4985
+ handler.consolidationTimer = setInterval(async () => {
4986
+ await this.runConsolidationForHandler(handler, resourceName, fieldName);
4987
+ }, intervalMs);
4988
+ }
4989
+ async runConsolidationForHandler(handler, resourceName, fieldName) {
4990
+ const oldResource = this.config.resource;
4991
+ const oldField = this.config.field;
4992
+ const oldTransactionResource = this.transactionResource;
4993
+ const oldTargetResource = this.targetResource;
4994
+ const oldLockResource = this.lockResource;
4995
+ const oldAnalyticsResource = this.analyticsResource;
4996
+ this.config.resource = resourceName;
4997
+ this.config.field = fieldName;
4998
+ this.transactionResource = handler.transactionResource;
4999
+ this.targetResource = handler.targetResource;
5000
+ this.lockResource = handler.lockResource;
5001
+ this.analyticsResource = handler.analyticsResource;
5002
+ try {
5003
+ await this.runConsolidation();
5004
+ } finally {
5005
+ this.config.resource = oldResource;
5006
+ this.config.field = oldField;
5007
+ this.transactionResource = oldTransactionResource;
5008
+ this.targetResource = oldTargetResource;
5009
+ this.lockResource = oldLockResource;
5010
+ this.analyticsResource = oldAnalyticsResource;
5011
+ }
5012
+ }
4850
5013
  async runConsolidation() {
4851
5014
  const startTime = Date.now();
4852
5015
  if (this.config.verbose) {
@@ -5044,7 +5207,7 @@ class EventualConsistencyPlugin extends Plugin {
5044
5207
  if (this.config.enableAnalytics && transactionsToUpdate.length > 0) {
5045
5208
  await this.updateAnalytics(transactionsToUpdate);
5046
5209
  }
5047
- if (this.targetResource.cache && typeof this.targetResource.cache.delete === "function") {
5210
+ if (this.targetResource && this.targetResource.cache && typeof this.targetResource.cache.delete === "function") {
5048
5211
  try {
5049
5212
  const cacheKey = await this.targetResource.cacheKeyFor({ id: originalId });
5050
5213
  await this.targetResource.cache.delete(cacheKey);
@@ -5197,6 +5360,33 @@ class EventualConsistencyPlugin extends Plugin {
5197
5360
  await this.runGarbageCollection();
5198
5361
  }, gcIntervalMs);
5199
5362
  }
5363
+ startGarbageCollectionTimerForHandler(handler, resourceName, fieldName) {
5364
+ const gcIntervalMs = this.config.gcInterval * 1e3;
5365
+ handler.gcTimer = setInterval(async () => {
5366
+ await this.runGarbageCollectionForHandler(handler, resourceName, fieldName);
5367
+ }, gcIntervalMs);
5368
+ }
5369
+ async runGarbageCollectionForHandler(handler, resourceName, fieldName) {
5370
+ const oldResource = this.config.resource;
5371
+ const oldField = this.config.field;
5372
+ const oldTransactionResource = this.transactionResource;
5373
+ const oldTargetResource = this.targetResource;
5374
+ const oldLockResource = this.lockResource;
5375
+ this.config.resource = resourceName;
5376
+ this.config.field = fieldName;
5377
+ this.transactionResource = handler.transactionResource;
5378
+ this.targetResource = handler.targetResource;
5379
+ this.lockResource = handler.lockResource;
5380
+ try {
5381
+ await this.runGarbageCollection();
5382
+ } finally {
5383
+ this.config.resource = oldResource;
5384
+ this.config.field = oldField;
5385
+ this.transactionResource = oldTransactionResource;
5386
+ this.targetResource = oldTargetResource;
5387
+ this.lockResource = oldLockResource;
5388
+ }
5389
+ }
5200
5390
  /**
5201
5391
  * Delete old applied transactions based on retention policy
5202
5392
  * Uses distributed locking to prevent multiple containers from running GC simultaneously
@@ -5499,12 +5689,20 @@ class EventualConsistencyPlugin extends Plugin {
5499
5689
  * @returns {Promise<Array>} Analytics data
5500
5690
  */
5501
5691
  async getAnalytics(resourceName, field, options = {}) {
5502
- if (!this.analyticsResource) {
5692
+ const fieldHandlers = this.fieldHandlers.get(resourceName);
5693
+ if (!fieldHandlers) {
5694
+ throw new Error(`No eventual consistency configured for resource: ${resourceName}`);
5695
+ }
5696
+ const handler = fieldHandlers.get(field);
5697
+ if (!handler) {
5698
+ throw new Error(`No eventual consistency configured for field: ${resourceName}.${field}`);
5699
+ }
5700
+ if (!handler.analyticsResource) {
5503
5701
  throw new Error("Analytics not enabled for this plugin");
5504
5702
  }
5505
5703
  const { period = "day", date, startDate, endDate, month, year, breakdown = false } = options;
5506
5704
  const [ok, err, allAnalytics] = await tryFn(
5507
- () => this.analyticsResource.list()
5705
+ () => handler.analyticsResource.list()
5508
5706
  );
5509
5707
  if (!ok || !allAnalytics) {
5510
5708
  return [];
@@ -5730,12 +5928,20 @@ class EventualConsistencyPlugin extends Plugin {
5730
5928
  * @returns {Promise<Array>} Top records
5731
5929
  */
5732
5930
  async getTopRecords(resourceName, field, options = {}) {
5733
- if (!this.transactionResource) {
5931
+ const fieldHandlers = this.fieldHandlers.get(resourceName);
5932
+ if (!fieldHandlers) {
5933
+ throw new Error(`No eventual consistency configured for resource: ${resourceName}`);
5934
+ }
5935
+ const handler = fieldHandlers.get(field);
5936
+ if (!handler) {
5937
+ throw new Error(`No eventual consistency configured for field: ${resourceName}.${field}`);
5938
+ }
5939
+ if (!handler.transactionResource) {
5734
5940
  throw new Error("Transaction resource not initialized");
5735
5941
  }
5736
5942
  const { period = "day", date, metric = "transactionCount", limit = 10 } = options;
5737
5943
  const [ok, err, transactions] = await tryFn(
5738
- () => this.transactionResource.list()
5944
+ () => handler.transactionResource.list()
5739
5945
  );
5740
5946
  if (!ok || !transactions) {
5741
5947
  return [];
@@ -11843,7 +12049,7 @@ class Database extends EventEmitter {
11843
12049
  this.id = idGenerator(7);
11844
12050
  this.version = "1";
11845
12051
  this.s3dbVersion = (() => {
11846
- const [ok, err, version] = tryFn(() => true ? "10.0.12" : "latest");
12052
+ const [ok, err, version] = tryFn(() => true ? "10.0.13" : "latest");
11847
12053
  return ok ? version : "latest";
11848
12054
  })();
11849
12055
  this.resources = {};