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.cjs.js
CHANGED
|
@@ -3869,6 +3869,10 @@ class CachePlugin extends Plugin {
|
|
|
3869
3869
|
}
|
|
3870
3870
|
}
|
|
3871
3871
|
shouldCacheResource(resourceName) {
|
|
3872
|
+
const resourceMetadata = this.database.savedMetadata?.resources?.[resourceName];
|
|
3873
|
+
if (resourceMetadata?.createdBy && resourceMetadata.createdBy !== "user" && !this.config.include) {
|
|
3874
|
+
return false;
|
|
3875
|
+
}
|
|
3872
3876
|
if (resourceName.startsWith("plg_") && !this.config.include) {
|
|
3873
3877
|
return false;
|
|
3874
3878
|
}
|
|
@@ -4418,8 +4422,9 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4418
4422
|
behavior: "body-overflow",
|
|
4419
4423
|
timestamps: true,
|
|
4420
4424
|
partitions: partitionConfig,
|
|
4421
|
-
asyncPartitions: true
|
|
4425
|
+
asyncPartitions: true,
|
|
4422
4426
|
// Use async partitions for better performance
|
|
4427
|
+
createdBy: "EventualConsistencyPlugin"
|
|
4423
4428
|
})
|
|
4424
4429
|
);
|
|
4425
4430
|
if (!ok && !this.database.resources[transactionResourceName]) {
|
|
@@ -4436,7 +4441,8 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4436
4441
|
workerId: "string|optional"
|
|
4437
4442
|
},
|
|
4438
4443
|
behavior: "body-only",
|
|
4439
|
-
timestamps: false
|
|
4444
|
+
timestamps: false,
|
|
4445
|
+
createdBy: "EventualConsistencyPlugin"
|
|
4440
4446
|
})
|
|
4441
4447
|
);
|
|
4442
4448
|
if (!lockOk && !this.database.resources[lockResourceName]) {
|
|
@@ -4449,8 +4455,24 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4449
4455
|
this.addHelperMethods();
|
|
4450
4456
|
if (this.config.autoConsolidate) {
|
|
4451
4457
|
this.startConsolidationTimer();
|
|
4458
|
+
if (this.config.verbose) {
|
|
4459
|
+
console.log(
|
|
4460
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Auto-consolidation ENABLED (interval: ${this.config.consolidationInterval}s, window: ${this.config.consolidationWindow}h, mode: ${this.config.mode})`
|
|
4461
|
+
);
|
|
4462
|
+
}
|
|
4463
|
+
} else {
|
|
4464
|
+
if (this.config.verbose) {
|
|
4465
|
+
console.log(
|
|
4466
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Auto-consolidation DISABLED (manual consolidation only)`
|
|
4467
|
+
);
|
|
4468
|
+
}
|
|
4452
4469
|
}
|
|
4453
4470
|
this.startGarbageCollectionTimer();
|
|
4471
|
+
if (this.config.verbose) {
|
|
4472
|
+
console.log(
|
|
4473
|
+
`[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}` : ""}`
|
|
4474
|
+
);
|
|
4475
|
+
}
|
|
4454
4476
|
}
|
|
4455
4477
|
async onStart() {
|
|
4456
4478
|
if (this.deferredSetup) {
|
|
@@ -4532,7 +4554,8 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4532
4554
|
byCohort: {
|
|
4533
4555
|
fields: { cohort: "string" }
|
|
4534
4556
|
}
|
|
4535
|
-
}
|
|
4557
|
+
},
|
|
4558
|
+
createdBy: "EventualConsistencyPlugin"
|
|
4536
4559
|
})
|
|
4537
4560
|
);
|
|
4538
4561
|
if (!ok && !this.database.resources[analyticsResourceName]) {
|
|
@@ -4711,11 +4734,21 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4711
4734
|
};
|
|
4712
4735
|
if (this.config.batchTransactions) {
|
|
4713
4736
|
this.pendingTransactions.set(transaction.id, transaction);
|
|
4737
|
+
if (this.config.verbose) {
|
|
4738
|
+
console.log(
|
|
4739
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Transaction batched: ${data.operation} ${data.value} for ${data.originalId} (batch: ${this.pendingTransactions.size}/${this.config.batchSize})`
|
|
4740
|
+
);
|
|
4741
|
+
}
|
|
4714
4742
|
if (this.pendingTransactions.size >= this.config.batchSize) {
|
|
4715
4743
|
await this.flushPendingTransactions();
|
|
4716
4744
|
}
|
|
4717
4745
|
} else {
|
|
4718
4746
|
await this.transactionResource.insert(transaction);
|
|
4747
|
+
if (this.config.verbose) {
|
|
4748
|
+
console.log(
|
|
4749
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Transaction created: ${data.operation} ${data.value} for ${data.originalId} (cohort: ${cohortInfo.hour}, applied: false)`
|
|
4750
|
+
);
|
|
4751
|
+
}
|
|
4719
4752
|
}
|
|
4720
4753
|
return transaction;
|
|
4721
4754
|
}
|
|
@@ -4780,11 +4813,23 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4780
4813
|
}
|
|
4781
4814
|
startConsolidationTimer() {
|
|
4782
4815
|
const intervalMs = this.config.consolidationInterval * 1e3;
|
|
4816
|
+
if (this.config.verbose) {
|
|
4817
|
+
const nextRun = new Date(Date.now() + intervalMs);
|
|
4818
|
+
console.log(
|
|
4819
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidation timer started. Next run at ${nextRun.toISOString()} (every ${this.config.consolidationInterval}s)`
|
|
4820
|
+
);
|
|
4821
|
+
}
|
|
4783
4822
|
this.consolidationTimer = setInterval(async () => {
|
|
4784
4823
|
await this.runConsolidation();
|
|
4785
4824
|
}, intervalMs);
|
|
4786
4825
|
}
|
|
4787
4826
|
async runConsolidation() {
|
|
4827
|
+
const startTime = Date.now();
|
|
4828
|
+
if (this.config.verbose) {
|
|
4829
|
+
console.log(
|
|
4830
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Starting consolidation run at ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
4831
|
+
);
|
|
4832
|
+
}
|
|
4788
4833
|
try {
|
|
4789
4834
|
const now = /* @__PURE__ */ new Date();
|
|
4790
4835
|
const hoursToCheck = this.config.consolidationWindow || 24;
|
|
@@ -4794,6 +4839,11 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4794
4839
|
const cohortInfo = this.getCohortInfo(date);
|
|
4795
4840
|
cohortHours.push(cohortInfo.hour);
|
|
4796
4841
|
}
|
|
4842
|
+
if (this.config.verbose) {
|
|
4843
|
+
console.log(
|
|
4844
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Querying ${hoursToCheck} hour partitions for pending transactions...`
|
|
4845
|
+
);
|
|
4846
|
+
}
|
|
4797
4847
|
const transactionsByHour = await Promise.all(
|
|
4798
4848
|
cohortHours.map(async (cohortHour) => {
|
|
4799
4849
|
const [ok, err, txns] = await tryFn(
|
|
@@ -4808,26 +4858,47 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4808
4858
|
const transactions = transactionsByHour.flat();
|
|
4809
4859
|
if (transactions.length === 0) {
|
|
4810
4860
|
if (this.config.verbose) {
|
|
4811
|
-
console.log(
|
|
4861
|
+
console.log(
|
|
4862
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - No pending transactions found. Next run in ${this.config.consolidationInterval}s`
|
|
4863
|
+
);
|
|
4812
4864
|
}
|
|
4813
4865
|
return;
|
|
4814
4866
|
}
|
|
4815
4867
|
const uniqueIds = [...new Set(transactions.map((t) => t.originalId))];
|
|
4868
|
+
if (this.config.verbose) {
|
|
4869
|
+
console.log(
|
|
4870
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Found ${transactions.length} pending transactions for ${uniqueIds.length} records. Consolidating with concurrency=${this.config.consolidationConcurrency}...`
|
|
4871
|
+
);
|
|
4872
|
+
}
|
|
4816
4873
|
const { results, errors } = await promisePool.PromisePool.for(uniqueIds).withConcurrency(this.config.consolidationConcurrency).process(async (id) => {
|
|
4817
4874
|
return await this.consolidateRecord(id);
|
|
4818
4875
|
});
|
|
4876
|
+
const duration = Date.now() - startTime;
|
|
4819
4877
|
if (errors && errors.length > 0) {
|
|
4820
|
-
console.error(
|
|
4878
|
+
console.error(
|
|
4879
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidation completed with ${errors.length} errors in ${duration}ms:`,
|
|
4880
|
+
errors
|
|
4881
|
+
);
|
|
4882
|
+
}
|
|
4883
|
+
if (this.config.verbose) {
|
|
4884
|
+
console.log(
|
|
4885
|
+
`[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`
|
|
4886
|
+
);
|
|
4821
4887
|
}
|
|
4822
4888
|
this.emit("eventual-consistency.consolidated", {
|
|
4823
4889
|
resource: this.config.resource,
|
|
4824
4890
|
field: this.config.field,
|
|
4825
4891
|
recordCount: uniqueIds.length,
|
|
4826
4892
|
successCount: results.length,
|
|
4827
|
-
errorCount: errors.length
|
|
4893
|
+
errorCount: errors.length,
|
|
4894
|
+
duration
|
|
4828
4895
|
});
|
|
4829
4896
|
} catch (error) {
|
|
4830
|
-
|
|
4897
|
+
const duration = Date.now() - startTime;
|
|
4898
|
+
console.error(
|
|
4899
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidation error after ${duration}ms:`,
|
|
4900
|
+
error
|
|
4901
|
+
);
|
|
4831
4902
|
this.emit("eventual-consistency.consolidation-error", error);
|
|
4832
4903
|
}
|
|
4833
4904
|
}
|
|
@@ -4862,8 +4933,18 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4862
4933
|
})
|
|
4863
4934
|
);
|
|
4864
4935
|
if (!ok || !transactions || transactions.length === 0) {
|
|
4936
|
+
if (this.config.verbose) {
|
|
4937
|
+
console.log(
|
|
4938
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - No pending transactions for ${originalId}, skipping`
|
|
4939
|
+
);
|
|
4940
|
+
}
|
|
4865
4941
|
return currentValue;
|
|
4866
4942
|
}
|
|
4943
|
+
if (this.config.verbose) {
|
|
4944
|
+
console.log(
|
|
4945
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Consolidating ${originalId}: ${transactions.length} pending transactions (current: ${currentValue})`
|
|
4946
|
+
);
|
|
4947
|
+
}
|
|
4867
4948
|
transactions.sort(
|
|
4868
4949
|
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
4869
4950
|
);
|
|
@@ -4872,6 +4953,11 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4872
4953
|
transactions.unshift(this._createSyntheticSetTransaction(currentValue));
|
|
4873
4954
|
}
|
|
4874
4955
|
const consolidatedValue = this.config.reducer(transactions);
|
|
4956
|
+
if (this.config.verbose) {
|
|
4957
|
+
console.log(
|
|
4958
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ${originalId}: ${currentValue} \u2192 ${consolidatedValue} (${consolidatedValue > currentValue ? "+" : ""}${consolidatedValue - currentValue})`
|
|
4959
|
+
);
|
|
4960
|
+
}
|
|
4875
4961
|
const [updateOk, updateErr] = await tryFn(
|
|
4876
4962
|
() => this.targetResource.update(originalId, {
|
|
4877
4963
|
[this.config.field]: consolidatedValue
|
|
@@ -4894,6 +4980,23 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4894
4980
|
if (this.config.enableAnalytics && transactionsToUpdate.length > 0) {
|
|
4895
4981
|
await this.updateAnalytics(transactionsToUpdate);
|
|
4896
4982
|
}
|
|
4983
|
+
if (this.targetResource.cache && typeof this.targetResource.cache.delete === "function") {
|
|
4984
|
+
try {
|
|
4985
|
+
const cacheKey = await this.targetResource.cacheKeyFor({ id: originalId });
|
|
4986
|
+
await this.targetResource.cache.delete(cacheKey);
|
|
4987
|
+
if (this.config.verbose) {
|
|
4988
|
+
console.log(
|
|
4989
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Cache invalidated for ${originalId}`
|
|
4990
|
+
);
|
|
4991
|
+
}
|
|
4992
|
+
} catch (cacheErr) {
|
|
4993
|
+
if (this.config.verbose) {
|
|
4994
|
+
console.warn(
|
|
4995
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Failed to invalidate cache for ${originalId}: ${cacheErr?.message}`
|
|
4996
|
+
);
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
4897
5000
|
}
|
|
4898
5001
|
return consolidatedValue;
|
|
4899
5002
|
} finally {
|
|
@@ -5108,21 +5211,43 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
5108
5211
|
*/
|
|
5109
5212
|
async updateAnalytics(transactions) {
|
|
5110
5213
|
if (!this.analyticsResource || transactions.length === 0) return;
|
|
5214
|
+
if (this.config.verbose) {
|
|
5215
|
+
console.log(
|
|
5216
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Updating analytics for ${transactions.length} transactions...`
|
|
5217
|
+
);
|
|
5218
|
+
}
|
|
5111
5219
|
try {
|
|
5112
5220
|
const byHour = this._groupByCohort(transactions, "cohortHour");
|
|
5221
|
+
const cohortCount = Object.keys(byHour).length;
|
|
5222
|
+
if (this.config.verbose) {
|
|
5223
|
+
console.log(
|
|
5224
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Updating ${cohortCount} hourly analytics cohorts...`
|
|
5225
|
+
);
|
|
5226
|
+
}
|
|
5113
5227
|
for (const [cohort, txns] of Object.entries(byHour)) {
|
|
5114
5228
|
await this._upsertAnalytics("hour", cohort, txns);
|
|
5115
5229
|
}
|
|
5116
5230
|
if (this.config.analyticsConfig.rollupStrategy === "incremental") {
|
|
5117
5231
|
const uniqueHours = Object.keys(byHour);
|
|
5232
|
+
if (this.config.verbose) {
|
|
5233
|
+
console.log(
|
|
5234
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Rolling up ${uniqueHours.length} hours to daily/monthly analytics...`
|
|
5235
|
+
);
|
|
5236
|
+
}
|
|
5118
5237
|
for (const cohortHour of uniqueHours) {
|
|
5119
5238
|
await this._rollupAnalytics(cohortHour);
|
|
5120
5239
|
}
|
|
5121
5240
|
}
|
|
5122
|
-
} catch (error) {
|
|
5123
5241
|
if (this.config.verbose) {
|
|
5124
|
-
console.
|
|
5242
|
+
console.log(
|
|
5243
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Analytics update complete for ${cohortCount} cohorts`
|
|
5244
|
+
);
|
|
5125
5245
|
}
|
|
5246
|
+
} catch (error) {
|
|
5247
|
+
console.warn(
|
|
5248
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - Analytics update error:`,
|
|
5249
|
+
error.message
|
|
5250
|
+
);
|
|
5126
5251
|
}
|
|
5127
5252
|
}
|
|
5128
5253
|
/**
|
|
@@ -9259,7 +9384,8 @@ ${errorDetails}`,
|
|
|
9259
9384
|
versioningEnabled = false,
|
|
9260
9385
|
events = {},
|
|
9261
9386
|
asyncEvents = true,
|
|
9262
|
-
asyncPartitions = true
|
|
9387
|
+
asyncPartitions = true,
|
|
9388
|
+
createdBy = "user"
|
|
9263
9389
|
} = config;
|
|
9264
9390
|
this.name = name;
|
|
9265
9391
|
this.client = client;
|
|
@@ -9288,7 +9414,8 @@ ${errorDetails}`,
|
|
|
9288
9414
|
autoDecrypt,
|
|
9289
9415
|
allNestedObjectsOptional,
|
|
9290
9416
|
asyncEvents,
|
|
9291
|
-
asyncPartitions
|
|
9417
|
+
asyncPartitions,
|
|
9418
|
+
createdBy
|
|
9292
9419
|
};
|
|
9293
9420
|
this.hooks = {
|
|
9294
9421
|
beforeInsert: [],
|
|
@@ -11652,7 +11779,7 @@ class Database extends EventEmitter {
|
|
|
11652
11779
|
this.id = idGenerator(7);
|
|
11653
11780
|
this.version = "1";
|
|
11654
11781
|
this.s3dbVersion = (() => {
|
|
11655
|
-
const [ok, err, version] = tryFn(() => true ? "10.0.
|
|
11782
|
+
const [ok, err, version] = tryFn(() => true ? "10.0.7" : "latest");
|
|
11656
11783
|
return ok ? version : "latest";
|
|
11657
11784
|
})();
|
|
11658
11785
|
this.resources = {};
|
|
@@ -12013,6 +12140,7 @@ class Database extends EventEmitter {
|
|
|
12013
12140
|
metadata.resources[name] = {
|
|
12014
12141
|
currentVersion: version,
|
|
12015
12142
|
partitions: resource.config.partitions || {},
|
|
12143
|
+
createdBy: existingResource?.createdBy || resource.config.createdBy || "user",
|
|
12016
12144
|
versions: {
|
|
12017
12145
|
...existingResource?.versions,
|
|
12018
12146
|
// Preserve previous versions
|
|
@@ -12371,6 +12499,7 @@ class Database extends EventEmitter {
|
|
|
12371
12499
|
* @param {boolean} [config.autoDecrypt=true] - Auto-decrypt secret fields
|
|
12372
12500
|
* @param {Function|number} [config.idGenerator] - Custom ID generator or size
|
|
12373
12501
|
* @param {number} [config.idSize=22] - Size for auto-generated IDs
|
|
12502
|
+
* @param {string} [config.createdBy='user'] - Who created this resource ('user', 'plugin', or plugin name)
|
|
12374
12503
|
* @returns {Promise<Resource>} The created or updated resource
|
|
12375
12504
|
*/
|
|
12376
12505
|
async createResource({ name, attributes, behavior = "user-managed", hooks, ...config }) {
|
|
@@ -12429,7 +12558,8 @@ class Database extends EventEmitter {
|
|
|
12429
12558
|
idGenerator: config.idGenerator,
|
|
12430
12559
|
idSize: config.idSize,
|
|
12431
12560
|
asyncEvents: config.asyncEvents,
|
|
12432
|
-
events: config.events || {}
|
|
12561
|
+
events: config.events || {},
|
|
12562
|
+
createdBy: config.createdBy || "user"
|
|
12433
12563
|
});
|
|
12434
12564
|
resource.database = this;
|
|
12435
12565
|
this.resources[name] = resource;
|