s3db.js 10.0.12 → 10.0.14
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 +346 -41
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +346 -41
- package/dist/s3db.es.js.map +1 -1
- package/package.json +5 -5
- package/src/plugins/eventual-consistency.plugin.js +467 -56
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:
|
|
4666
|
-
|
|
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
|
-
|
|
4677
|
+
field: handler.field,
|
|
4669
4678
|
value,
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
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:
|
|
4679
|
-
|
|
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
|
-
|
|
4719
|
+
field: handler.field,
|
|
4682
4720
|
value: amount,
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
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
|
|
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:
|
|
4693
|
-
|
|
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
|
-
|
|
4763
|
+
field: handler.field,
|
|
4696
4764
|
value: amount,
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
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
|
|
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
|
|
4710
|
-
if (!
|
|
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
|
-
|
|
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
|
|
4720
|
-
if (!
|
|
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
|
-
|
|
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) {
|
|
@@ -4950,10 +5113,6 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4950
5113
|
return recordOk && record ? record[this.config.field] || 0 : 0;
|
|
4951
5114
|
}
|
|
4952
5115
|
try {
|
|
4953
|
-
const [recordOk, recordErr, record] = await tryFn(
|
|
4954
|
-
() => this.targetResource.get(originalId)
|
|
4955
|
-
);
|
|
4956
|
-
const currentValue = recordOk && record ? record[this.config.field] || 0 : 0;
|
|
4957
5116
|
const [ok, err, transactions] = await tryFn(
|
|
4958
5117
|
() => this.transactionResource.query({
|
|
4959
5118
|
originalId,
|
|
@@ -4961,16 +5120,119 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4961
5120
|
})
|
|
4962
5121
|
);
|
|
4963
5122
|
if (!ok || !transactions || transactions.length === 0) {
|
|
5123
|
+
const [recordOk, recordErr, record] = await tryFn(
|
|
5124
|
+
() => this.targetResource.get(originalId)
|
|
5125
|
+
);
|
|
5126
|
+
const currentValue2 = recordOk && record ? record[this.config.field] || 0 : 0;
|
|
4964
5127
|
if (this.config.verbose) {
|
|
4965
5128
|
console.log(
|
|
4966
5129
|
`[EventualConsistency] ${this.config.resource}.${this.config.field} - No pending transactions for ${originalId}, skipping`
|
|
4967
5130
|
);
|
|
4968
5131
|
}
|
|
4969
|
-
return
|
|
5132
|
+
return currentValue2;
|
|
5133
|
+
}
|
|
5134
|
+
const [appliedOk, appliedErr, appliedTransactions] = await tryFn(
|
|
5135
|
+
() => this.transactionResource.query({
|
|
5136
|
+
originalId,
|
|
5137
|
+
applied: true
|
|
5138
|
+
})
|
|
5139
|
+
);
|
|
5140
|
+
let currentValue = 0;
|
|
5141
|
+
if (appliedOk && appliedTransactions && appliedTransactions.length > 0) {
|
|
5142
|
+
const [recordExistsOk, recordExistsErr, recordExists] = await tryFn(
|
|
5143
|
+
() => this.targetResource.get(originalId)
|
|
5144
|
+
);
|
|
5145
|
+
if (!recordExistsOk || !recordExists) {
|
|
5146
|
+
if (this.config.verbose) {
|
|
5147
|
+
console.log(
|
|
5148
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Record ${originalId} doesn't exist, deleting ${appliedTransactions.length} old applied transactions`
|
|
5149
|
+
);
|
|
5150
|
+
}
|
|
5151
|
+
const { results, errors } = await promisePool.PromisePool.for(appliedTransactions).withConcurrency(10).process(async (txn) => {
|
|
5152
|
+
const [deleted] = await tryFn(() => this.transactionResource.delete(txn.id));
|
|
5153
|
+
return deleted;
|
|
5154
|
+
});
|
|
5155
|
+
if (this.config.verbose && errors && errors.length > 0) {
|
|
5156
|
+
console.warn(
|
|
5157
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Failed to delete ${errors.length} old applied transactions`
|
|
5158
|
+
);
|
|
5159
|
+
}
|
|
5160
|
+
currentValue = 0;
|
|
5161
|
+
} else {
|
|
5162
|
+
appliedTransactions.sort(
|
|
5163
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
5164
|
+
);
|
|
5165
|
+
const hasSetInApplied = appliedTransactions.some((t) => t.operation === "set");
|
|
5166
|
+
if (!hasSetInApplied) {
|
|
5167
|
+
const recordValue = recordExists[this.config.field] || 0;
|
|
5168
|
+
let appliedDelta = 0;
|
|
5169
|
+
for (const t of appliedTransactions) {
|
|
5170
|
+
if (t.operation === "add") appliedDelta += t.value;
|
|
5171
|
+
else if (t.operation === "sub") appliedDelta -= t.value;
|
|
5172
|
+
}
|
|
5173
|
+
const baseValue = recordValue - appliedDelta;
|
|
5174
|
+
const hasExistingAnchor = appliedTransactions.some((t) => t.source === "anchor");
|
|
5175
|
+
if (baseValue !== 0 && !hasExistingAnchor) {
|
|
5176
|
+
const firstTransactionDate = new Date(appliedTransactions[0].timestamp);
|
|
5177
|
+
const cohortInfo = this.getCohortInfo(firstTransactionDate);
|
|
5178
|
+
const anchorTransaction = {
|
|
5179
|
+
id: idGenerator(),
|
|
5180
|
+
originalId,
|
|
5181
|
+
field: this.config.field,
|
|
5182
|
+
value: baseValue,
|
|
5183
|
+
operation: "set",
|
|
5184
|
+
timestamp: new Date(firstTransactionDate.getTime() - 1).toISOString(),
|
|
5185
|
+
// 1ms before first txn to ensure it's first
|
|
5186
|
+
cohortDate: cohortInfo.date,
|
|
5187
|
+
cohortHour: cohortInfo.hour,
|
|
5188
|
+
cohortMonth: cohortInfo.month,
|
|
5189
|
+
source: "anchor",
|
|
5190
|
+
applied: true
|
|
5191
|
+
};
|
|
5192
|
+
await this.transactionResource.insert(anchorTransaction);
|
|
5193
|
+
appliedTransactions.unshift(anchorTransaction);
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
5196
|
+
currentValue = this.config.reducer(appliedTransactions);
|
|
5197
|
+
}
|
|
5198
|
+
} else {
|
|
5199
|
+
const [recordOk, recordErr, record] = await tryFn(
|
|
5200
|
+
() => this.targetResource.get(originalId)
|
|
5201
|
+
);
|
|
5202
|
+
currentValue = recordOk && record ? record[this.config.field] || 0 : 0;
|
|
5203
|
+
if (currentValue !== 0) {
|
|
5204
|
+
let anchorTimestamp;
|
|
5205
|
+
if (transactions && transactions.length > 0) {
|
|
5206
|
+
const firstPendingDate = new Date(transactions[0].timestamp);
|
|
5207
|
+
anchorTimestamp = new Date(firstPendingDate.getTime() - 1).toISOString();
|
|
5208
|
+
} else {
|
|
5209
|
+
anchorTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
5210
|
+
}
|
|
5211
|
+
const cohortInfo = this.getCohortInfo(new Date(anchorTimestamp));
|
|
5212
|
+
const anchorTransaction = {
|
|
5213
|
+
id: idGenerator(),
|
|
5214
|
+
originalId,
|
|
5215
|
+
field: this.config.field,
|
|
5216
|
+
value: currentValue,
|
|
5217
|
+
operation: "set",
|
|
5218
|
+
timestamp: anchorTimestamp,
|
|
5219
|
+
cohortDate: cohortInfo.date,
|
|
5220
|
+
cohortHour: cohortInfo.hour,
|
|
5221
|
+
cohortMonth: cohortInfo.month,
|
|
5222
|
+
source: "anchor",
|
|
5223
|
+
applied: true
|
|
5224
|
+
};
|
|
5225
|
+
await this.transactionResource.insert(anchorTransaction);
|
|
5226
|
+
if (this.config.verbose) {
|
|
5227
|
+
console.log(
|
|
5228
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Created anchor transaction for ${originalId} with base value ${currentValue}`
|
|
5229
|
+
);
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
4970
5232
|
}
|
|
4971
5233
|
if (this.config.verbose) {
|
|
4972
5234
|
console.log(
|
|
4973
|
-
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidating ${originalId}: ${transactions.length} pending transactions (current: ${currentValue})`
|
|
5235
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidating ${originalId}: ${transactions.length} pending transactions (current: ${currentValue} from ${appliedOk && appliedTransactions?.length > 0 ? "applied transactions" : "record"})`
|
|
4974
5236
|
);
|
|
4975
5237
|
}
|
|
4976
5238
|
transactions.sort(
|
|
@@ -5048,7 +5310,7 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
5048
5310
|
if (this.config.enableAnalytics && transactionsToUpdate.length > 0) {
|
|
5049
5311
|
await this.updateAnalytics(transactionsToUpdate);
|
|
5050
5312
|
}
|
|
5051
|
-
if (this.targetResource.cache && typeof this.targetResource.cache.delete === "function") {
|
|
5313
|
+
if (this.targetResource && this.targetResource.cache && typeof this.targetResource.cache.delete === "function") {
|
|
5052
5314
|
try {
|
|
5053
5315
|
const cacheKey = await this.targetResource.cacheKeyFor({ id: originalId });
|
|
5054
5316
|
await this.targetResource.cache.delete(cacheKey);
|
|
@@ -5201,6 +5463,33 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
5201
5463
|
await this.runGarbageCollection();
|
|
5202
5464
|
}, gcIntervalMs);
|
|
5203
5465
|
}
|
|
5466
|
+
startGarbageCollectionTimerForHandler(handler, resourceName, fieldName) {
|
|
5467
|
+
const gcIntervalMs = this.config.gcInterval * 1e3;
|
|
5468
|
+
handler.gcTimer = setInterval(async () => {
|
|
5469
|
+
await this.runGarbageCollectionForHandler(handler, resourceName, fieldName);
|
|
5470
|
+
}, gcIntervalMs);
|
|
5471
|
+
}
|
|
5472
|
+
async runGarbageCollectionForHandler(handler, resourceName, fieldName) {
|
|
5473
|
+
const oldResource = this.config.resource;
|
|
5474
|
+
const oldField = this.config.field;
|
|
5475
|
+
const oldTransactionResource = this.transactionResource;
|
|
5476
|
+
const oldTargetResource = this.targetResource;
|
|
5477
|
+
const oldLockResource = this.lockResource;
|
|
5478
|
+
this.config.resource = resourceName;
|
|
5479
|
+
this.config.field = fieldName;
|
|
5480
|
+
this.transactionResource = handler.transactionResource;
|
|
5481
|
+
this.targetResource = handler.targetResource;
|
|
5482
|
+
this.lockResource = handler.lockResource;
|
|
5483
|
+
try {
|
|
5484
|
+
await this.runGarbageCollection();
|
|
5485
|
+
} finally {
|
|
5486
|
+
this.config.resource = oldResource;
|
|
5487
|
+
this.config.field = oldField;
|
|
5488
|
+
this.transactionResource = oldTransactionResource;
|
|
5489
|
+
this.targetResource = oldTargetResource;
|
|
5490
|
+
this.lockResource = oldLockResource;
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5204
5493
|
/**
|
|
5205
5494
|
* Delete old applied transactions based on retention policy
|
|
5206
5495
|
* Uses distributed locking to prevent multiple containers from running GC simultaneously
|
|
@@ -5503,12 +5792,20 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
5503
5792
|
* @returns {Promise<Array>} Analytics data
|
|
5504
5793
|
*/
|
|
5505
5794
|
async getAnalytics(resourceName, field, options = {}) {
|
|
5506
|
-
|
|
5795
|
+
const fieldHandlers = this.fieldHandlers.get(resourceName);
|
|
5796
|
+
if (!fieldHandlers) {
|
|
5797
|
+
throw new Error(`No eventual consistency configured for resource: ${resourceName}`);
|
|
5798
|
+
}
|
|
5799
|
+
const handler = fieldHandlers.get(field);
|
|
5800
|
+
if (!handler) {
|
|
5801
|
+
throw new Error(`No eventual consistency configured for field: ${resourceName}.${field}`);
|
|
5802
|
+
}
|
|
5803
|
+
if (!handler.analyticsResource) {
|
|
5507
5804
|
throw new Error("Analytics not enabled for this plugin");
|
|
5508
5805
|
}
|
|
5509
5806
|
const { period = "day", date, startDate, endDate, month, year, breakdown = false } = options;
|
|
5510
5807
|
const [ok, err, allAnalytics] = await tryFn(
|
|
5511
|
-
() =>
|
|
5808
|
+
() => handler.analyticsResource.list()
|
|
5512
5809
|
);
|
|
5513
5810
|
if (!ok || !allAnalytics) {
|
|
5514
5811
|
return [];
|
|
@@ -5734,12 +6031,20 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
5734
6031
|
* @returns {Promise<Array>} Top records
|
|
5735
6032
|
*/
|
|
5736
6033
|
async getTopRecords(resourceName, field, options = {}) {
|
|
5737
|
-
|
|
6034
|
+
const fieldHandlers = this.fieldHandlers.get(resourceName);
|
|
6035
|
+
if (!fieldHandlers) {
|
|
6036
|
+
throw new Error(`No eventual consistency configured for resource: ${resourceName}`);
|
|
6037
|
+
}
|
|
6038
|
+
const handler = fieldHandlers.get(field);
|
|
6039
|
+
if (!handler) {
|
|
6040
|
+
throw new Error(`No eventual consistency configured for field: ${resourceName}.${field}`);
|
|
6041
|
+
}
|
|
6042
|
+
if (!handler.transactionResource) {
|
|
5738
6043
|
throw new Error("Transaction resource not initialized");
|
|
5739
6044
|
}
|
|
5740
6045
|
const { period = "day", date, metric = "transactionCount", limit = 10 } = options;
|
|
5741
6046
|
const [ok, err, transactions] = await tryFn(
|
|
5742
|
-
() =>
|
|
6047
|
+
() => handler.transactionResource.list()
|
|
5743
6048
|
);
|
|
5744
6049
|
if (!ok || !transactions) {
|
|
5745
6050
|
return [];
|
|
@@ -11847,7 +12152,7 @@ class Database extends EventEmitter {
|
|
|
11847
12152
|
this.id = idGenerator(7);
|
|
11848
12153
|
this.version = "1";
|
|
11849
12154
|
this.s3dbVersion = (() => {
|
|
11850
|
-
const [ok, err, version] = tryFn(() => true ? "10.0.
|
|
12155
|
+
const [ok, err, version] = tryFn(() => true ? "10.0.14" : "latest");
|
|
11851
12156
|
return ok ? version : "latest";
|
|
11852
12157
|
})();
|
|
11853
12158
|
this.resources = {};
|