s3db.js 10.0.5 → 10.0.7
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 +143 -13
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +143 -13
- package/dist/s3db.es.js.map +1 -1
- package/package.json +18 -18
- package/src/database.class.js +4 -1
- package/src/plugins/cache.plugin.js +9 -1
- package/src/plugins/eventual-consistency.plugin.js +186 -11
- package/src/resource.class.js +3 -1
package/dist/s3db.es.js
CHANGED
|
@@ -3865,6 +3865,10 @@ class CachePlugin extends Plugin {
|
|
|
3865
3865
|
}
|
|
3866
3866
|
}
|
|
3867
3867
|
shouldCacheResource(resourceName) {
|
|
3868
|
+
const resourceMetadata = this.database.savedMetadata?.resources?.[resourceName];
|
|
3869
|
+
if (resourceMetadata?.createdBy && resourceMetadata.createdBy !== "user" && !this.config.include) {
|
|
3870
|
+
return false;
|
|
3871
|
+
}
|
|
3868
3872
|
if (resourceName.startsWith("plg_") && !this.config.include) {
|
|
3869
3873
|
return false;
|
|
3870
3874
|
}
|
|
@@ -4414,8 +4418,9 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4414
4418
|
behavior: "body-overflow",
|
|
4415
4419
|
timestamps: true,
|
|
4416
4420
|
partitions: partitionConfig,
|
|
4417
|
-
asyncPartitions: true
|
|
4421
|
+
asyncPartitions: true,
|
|
4418
4422
|
// Use async partitions for better performance
|
|
4423
|
+
createdBy: "EventualConsistencyPlugin"
|
|
4419
4424
|
})
|
|
4420
4425
|
);
|
|
4421
4426
|
if (!ok && !this.database.resources[transactionResourceName]) {
|
|
@@ -4432,7 +4437,8 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4432
4437
|
workerId: "string|optional"
|
|
4433
4438
|
},
|
|
4434
4439
|
behavior: "body-only",
|
|
4435
|
-
timestamps: false
|
|
4440
|
+
timestamps: false,
|
|
4441
|
+
createdBy: "EventualConsistencyPlugin"
|
|
4436
4442
|
})
|
|
4437
4443
|
);
|
|
4438
4444
|
if (!lockOk && !this.database.resources[lockResourceName]) {
|
|
@@ -4445,8 +4451,24 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4445
4451
|
this.addHelperMethods();
|
|
4446
4452
|
if (this.config.autoConsolidate) {
|
|
4447
4453
|
this.startConsolidationTimer();
|
|
4454
|
+
if (this.config.verbose) {
|
|
4455
|
+
console.log(
|
|
4456
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Auto-consolidation ENABLED (interval: ${this.config.consolidationInterval}s, window: ${this.config.consolidationWindow}h, mode: ${this.config.mode})`
|
|
4457
|
+
);
|
|
4458
|
+
}
|
|
4459
|
+
} else {
|
|
4460
|
+
if (this.config.verbose) {
|
|
4461
|
+
console.log(
|
|
4462
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Auto-consolidation DISABLED (manual consolidation only)`
|
|
4463
|
+
);
|
|
4464
|
+
}
|
|
4448
4465
|
}
|
|
4449
4466
|
this.startGarbageCollectionTimer();
|
|
4467
|
+
if (this.config.verbose) {
|
|
4468
|
+
console.log(
|
|
4469
|
+
`[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}` : ""}`
|
|
4470
|
+
);
|
|
4471
|
+
}
|
|
4450
4472
|
}
|
|
4451
4473
|
async onStart() {
|
|
4452
4474
|
if (this.deferredSetup) {
|
|
@@ -4528,7 +4550,8 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4528
4550
|
byCohort: {
|
|
4529
4551
|
fields: { cohort: "string" }
|
|
4530
4552
|
}
|
|
4531
|
-
}
|
|
4553
|
+
},
|
|
4554
|
+
createdBy: "EventualConsistencyPlugin"
|
|
4532
4555
|
})
|
|
4533
4556
|
);
|
|
4534
4557
|
if (!ok && !this.database.resources[analyticsResourceName]) {
|
|
@@ -4707,11 +4730,21 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4707
4730
|
};
|
|
4708
4731
|
if (this.config.batchTransactions) {
|
|
4709
4732
|
this.pendingTransactions.set(transaction.id, transaction);
|
|
4733
|
+
if (this.config.verbose) {
|
|
4734
|
+
console.log(
|
|
4735
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Transaction batched: ${data.operation} ${data.value} for ${data.originalId} (batch: ${this.pendingTransactions.size}/${this.config.batchSize})`
|
|
4736
|
+
);
|
|
4737
|
+
}
|
|
4710
4738
|
if (this.pendingTransactions.size >= this.config.batchSize) {
|
|
4711
4739
|
await this.flushPendingTransactions();
|
|
4712
4740
|
}
|
|
4713
4741
|
} else {
|
|
4714
4742
|
await this.transactionResource.insert(transaction);
|
|
4743
|
+
if (this.config.verbose) {
|
|
4744
|
+
console.log(
|
|
4745
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Transaction created: ${data.operation} ${data.value} for ${data.originalId} (cohort: ${cohortInfo.hour}, applied: false)`
|
|
4746
|
+
);
|
|
4747
|
+
}
|
|
4715
4748
|
}
|
|
4716
4749
|
return transaction;
|
|
4717
4750
|
}
|
|
@@ -4776,11 +4809,23 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4776
4809
|
}
|
|
4777
4810
|
startConsolidationTimer() {
|
|
4778
4811
|
const intervalMs = this.config.consolidationInterval * 1e3;
|
|
4812
|
+
if (this.config.verbose) {
|
|
4813
|
+
const nextRun = new Date(Date.now() + intervalMs);
|
|
4814
|
+
console.log(
|
|
4815
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidation timer started. Next run at ${nextRun.toISOString()} (every ${this.config.consolidationInterval}s)`
|
|
4816
|
+
);
|
|
4817
|
+
}
|
|
4779
4818
|
this.consolidationTimer = setInterval(async () => {
|
|
4780
4819
|
await this.runConsolidation();
|
|
4781
4820
|
}, intervalMs);
|
|
4782
4821
|
}
|
|
4783
4822
|
async runConsolidation() {
|
|
4823
|
+
const startTime = Date.now();
|
|
4824
|
+
if (this.config.verbose) {
|
|
4825
|
+
console.log(
|
|
4826
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Starting consolidation run at ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
4827
|
+
);
|
|
4828
|
+
}
|
|
4784
4829
|
try {
|
|
4785
4830
|
const now = /* @__PURE__ */ new Date();
|
|
4786
4831
|
const hoursToCheck = this.config.consolidationWindow || 24;
|
|
@@ -4790,6 +4835,11 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4790
4835
|
const cohortInfo = this.getCohortInfo(date);
|
|
4791
4836
|
cohortHours.push(cohortInfo.hour);
|
|
4792
4837
|
}
|
|
4838
|
+
if (this.config.verbose) {
|
|
4839
|
+
console.log(
|
|
4840
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Querying ${hoursToCheck} hour partitions for pending transactions...`
|
|
4841
|
+
);
|
|
4842
|
+
}
|
|
4793
4843
|
const transactionsByHour = await Promise.all(
|
|
4794
4844
|
cohortHours.map(async (cohortHour) => {
|
|
4795
4845
|
const [ok, err, txns] = await tryFn(
|
|
@@ -4804,26 +4854,47 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4804
4854
|
const transactions = transactionsByHour.flat();
|
|
4805
4855
|
if (transactions.length === 0) {
|
|
4806
4856
|
if (this.config.verbose) {
|
|
4807
|
-
console.log(
|
|
4857
|
+
console.log(
|
|
4858
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - No pending transactions found. Next run in ${this.config.consolidationInterval}s`
|
|
4859
|
+
);
|
|
4808
4860
|
}
|
|
4809
4861
|
return;
|
|
4810
4862
|
}
|
|
4811
4863
|
const uniqueIds = [...new Set(transactions.map((t) => t.originalId))];
|
|
4864
|
+
if (this.config.verbose) {
|
|
4865
|
+
console.log(
|
|
4866
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Found ${transactions.length} pending transactions for ${uniqueIds.length} records. Consolidating with concurrency=${this.config.consolidationConcurrency}...`
|
|
4867
|
+
);
|
|
4868
|
+
}
|
|
4812
4869
|
const { results, errors } = await PromisePool.for(uniqueIds).withConcurrency(this.config.consolidationConcurrency).process(async (id) => {
|
|
4813
4870
|
return await this.consolidateRecord(id);
|
|
4814
4871
|
});
|
|
4872
|
+
const duration = Date.now() - startTime;
|
|
4815
4873
|
if (errors && errors.length > 0) {
|
|
4816
|
-
console.error(
|
|
4874
|
+
console.error(
|
|
4875
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidation completed with ${errors.length} errors in ${duration}ms:`,
|
|
4876
|
+
errors
|
|
4877
|
+
);
|
|
4878
|
+
}
|
|
4879
|
+
if (this.config.verbose) {
|
|
4880
|
+
console.log(
|
|
4881
|
+
`[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`
|
|
4882
|
+
);
|
|
4817
4883
|
}
|
|
4818
4884
|
this.emit("eventual-consistency.consolidated", {
|
|
4819
4885
|
resource: this.config.resource,
|
|
4820
4886
|
field: this.config.field,
|
|
4821
4887
|
recordCount: uniqueIds.length,
|
|
4822
4888
|
successCount: results.length,
|
|
4823
|
-
errorCount: errors.length
|
|
4889
|
+
errorCount: errors.length,
|
|
4890
|
+
duration
|
|
4824
4891
|
});
|
|
4825
4892
|
} catch (error) {
|
|
4826
|
-
|
|
4893
|
+
const duration = Date.now() - startTime;
|
|
4894
|
+
console.error(
|
|
4895
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidation error after ${duration}ms:`,
|
|
4896
|
+
error
|
|
4897
|
+
);
|
|
4827
4898
|
this.emit("eventual-consistency.consolidation-error", error);
|
|
4828
4899
|
}
|
|
4829
4900
|
}
|
|
@@ -4858,8 +4929,18 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4858
4929
|
})
|
|
4859
4930
|
);
|
|
4860
4931
|
if (!ok || !transactions || transactions.length === 0) {
|
|
4932
|
+
if (this.config.verbose) {
|
|
4933
|
+
console.log(
|
|
4934
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - No pending transactions for ${originalId}, skipping`
|
|
4935
|
+
);
|
|
4936
|
+
}
|
|
4861
4937
|
return currentValue;
|
|
4862
4938
|
}
|
|
4939
|
+
if (this.config.verbose) {
|
|
4940
|
+
console.log(
|
|
4941
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidating ${originalId}: ${transactions.length} pending transactions (current: ${currentValue})`
|
|
4942
|
+
);
|
|
4943
|
+
}
|
|
4863
4944
|
transactions.sort(
|
|
4864
4945
|
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
4865
4946
|
);
|
|
@@ -4868,6 +4949,11 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4868
4949
|
transactions.unshift(this._createSyntheticSetTransaction(currentValue));
|
|
4869
4950
|
}
|
|
4870
4951
|
const consolidatedValue = this.config.reducer(transactions);
|
|
4952
|
+
if (this.config.verbose) {
|
|
4953
|
+
console.log(
|
|
4954
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ${originalId}: ${currentValue} \u2192 ${consolidatedValue} (${consolidatedValue > currentValue ? "+" : ""}${consolidatedValue - currentValue})`
|
|
4955
|
+
);
|
|
4956
|
+
}
|
|
4871
4957
|
const [updateOk, updateErr] = await tryFn(
|
|
4872
4958
|
() => this.targetResource.update(originalId, {
|
|
4873
4959
|
[this.config.field]: consolidatedValue
|
|
@@ -4890,6 +4976,23 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4890
4976
|
if (this.config.enableAnalytics && transactionsToUpdate.length > 0) {
|
|
4891
4977
|
await this.updateAnalytics(transactionsToUpdate);
|
|
4892
4978
|
}
|
|
4979
|
+
if (this.targetResource.cache && typeof this.targetResource.cache.delete === "function") {
|
|
4980
|
+
try {
|
|
4981
|
+
const cacheKey = await this.targetResource.cacheKeyFor({ id: originalId });
|
|
4982
|
+
await this.targetResource.cache.delete(cacheKey);
|
|
4983
|
+
if (this.config.verbose) {
|
|
4984
|
+
console.log(
|
|
4985
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Cache invalidated for ${originalId}`
|
|
4986
|
+
);
|
|
4987
|
+
}
|
|
4988
|
+
} catch (cacheErr) {
|
|
4989
|
+
if (this.config.verbose) {
|
|
4990
|
+
console.warn(
|
|
4991
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Failed to invalidate cache for ${originalId}: ${cacheErr?.message}`
|
|
4992
|
+
);
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4893
4996
|
}
|
|
4894
4997
|
return consolidatedValue;
|
|
4895
4998
|
} finally {
|
|
@@ -5104,21 +5207,43 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
5104
5207
|
*/
|
|
5105
5208
|
async updateAnalytics(transactions) {
|
|
5106
5209
|
if (!this.analyticsResource || transactions.length === 0) return;
|
|
5210
|
+
if (this.config.verbose) {
|
|
5211
|
+
console.log(
|
|
5212
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Updating analytics for ${transactions.length} transactions...`
|
|
5213
|
+
);
|
|
5214
|
+
}
|
|
5107
5215
|
try {
|
|
5108
5216
|
const byHour = this._groupByCohort(transactions, "cohortHour");
|
|
5217
|
+
const cohortCount = Object.keys(byHour).length;
|
|
5218
|
+
if (this.config.verbose) {
|
|
5219
|
+
console.log(
|
|
5220
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Updating ${cohortCount} hourly analytics cohorts...`
|
|
5221
|
+
);
|
|
5222
|
+
}
|
|
5109
5223
|
for (const [cohort, txns] of Object.entries(byHour)) {
|
|
5110
5224
|
await this._upsertAnalytics("hour", cohort, txns);
|
|
5111
5225
|
}
|
|
5112
5226
|
if (this.config.analyticsConfig.rollupStrategy === "incremental") {
|
|
5113
5227
|
const uniqueHours = Object.keys(byHour);
|
|
5228
|
+
if (this.config.verbose) {
|
|
5229
|
+
console.log(
|
|
5230
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Rolling up ${uniqueHours.length} hours to daily/monthly analytics...`
|
|
5231
|
+
);
|
|
5232
|
+
}
|
|
5114
5233
|
for (const cohortHour of uniqueHours) {
|
|
5115
5234
|
await this._rollupAnalytics(cohortHour);
|
|
5116
5235
|
}
|
|
5117
5236
|
}
|
|
5118
|
-
} catch (error) {
|
|
5119
5237
|
if (this.config.verbose) {
|
|
5120
|
-
console.
|
|
5238
|
+
console.log(
|
|
5239
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Analytics update complete for ${cohortCount} cohorts`
|
|
5240
|
+
);
|
|
5121
5241
|
}
|
|
5242
|
+
} catch (error) {
|
|
5243
|
+
console.warn(
|
|
5244
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Analytics update error:`,
|
|
5245
|
+
error.message
|
|
5246
|
+
);
|
|
5122
5247
|
}
|
|
5123
5248
|
}
|
|
5124
5249
|
/**
|
|
@@ -9255,7 +9380,8 @@ ${errorDetails}`,
|
|
|
9255
9380
|
versioningEnabled = false,
|
|
9256
9381
|
events = {},
|
|
9257
9382
|
asyncEvents = true,
|
|
9258
|
-
asyncPartitions = true
|
|
9383
|
+
asyncPartitions = true,
|
|
9384
|
+
createdBy = "user"
|
|
9259
9385
|
} = config;
|
|
9260
9386
|
this.name = name;
|
|
9261
9387
|
this.client = client;
|
|
@@ -9284,7 +9410,8 @@ ${errorDetails}`,
|
|
|
9284
9410
|
autoDecrypt,
|
|
9285
9411
|
allNestedObjectsOptional,
|
|
9286
9412
|
asyncEvents,
|
|
9287
|
-
asyncPartitions
|
|
9413
|
+
asyncPartitions,
|
|
9414
|
+
createdBy
|
|
9288
9415
|
};
|
|
9289
9416
|
this.hooks = {
|
|
9290
9417
|
beforeInsert: [],
|
|
@@ -11648,7 +11775,7 @@ class Database extends EventEmitter {
|
|
|
11648
11775
|
this.id = idGenerator(7);
|
|
11649
11776
|
this.version = "1";
|
|
11650
11777
|
this.s3dbVersion = (() => {
|
|
11651
|
-
const [ok, err, version] = tryFn(() => true ? "10.0.
|
|
11778
|
+
const [ok, err, version] = tryFn(() => true ? "10.0.7" : "latest");
|
|
11652
11779
|
return ok ? version : "latest";
|
|
11653
11780
|
})();
|
|
11654
11781
|
this.resources = {};
|
|
@@ -12009,6 +12136,7 @@ class Database extends EventEmitter {
|
|
|
12009
12136
|
metadata.resources[name] = {
|
|
12010
12137
|
currentVersion: version,
|
|
12011
12138
|
partitions: resource.config.partitions || {},
|
|
12139
|
+
createdBy: existingResource?.createdBy || resource.config.createdBy || "user",
|
|
12012
12140
|
versions: {
|
|
12013
12141
|
...existingResource?.versions,
|
|
12014
12142
|
// Preserve previous versions
|
|
@@ -12367,6 +12495,7 @@ class Database extends EventEmitter {
|
|
|
12367
12495
|
* @param {boolean} [config.autoDecrypt=true] - Auto-decrypt secret fields
|
|
12368
12496
|
* @param {Function|number} [config.idGenerator] - Custom ID generator or size
|
|
12369
12497
|
* @param {number} [config.idSize=22] - Size for auto-generated IDs
|
|
12498
|
+
* @param {string} [config.createdBy='user'] - Who created this resource ('user', 'plugin', or plugin name)
|
|
12370
12499
|
* @returns {Promise<Resource>} The created or updated resource
|
|
12371
12500
|
*/
|
|
12372
12501
|
async createResource({ name, attributes, behavior = "user-managed", hooks, ...config }) {
|
|
@@ -12425,7 +12554,8 @@ class Database extends EventEmitter {
|
|
|
12425
12554
|
idGenerator: config.idGenerator,
|
|
12426
12555
|
idSize: config.idSize,
|
|
12427
12556
|
asyncEvents: config.asyncEvents,
|
|
12428
|
-
events: config.events || {}
|
|
12557
|
+
events: config.events || {},
|
|
12558
|
+
createdBy: config.createdBy || "user"
|
|
12429
12559
|
});
|
|
12430
12560
|
resource.database = this;
|
|
12431
12561
|
this.resources[name] = resource;
|