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