s3db.js 11.1.0 → 11.2.2
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/SECURITY.md +76 -0
- package/dist/s3db-cli.js +55029 -0
- package/dist/s3db.cjs.js +195 -7
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +195 -7
- package/dist/s3db.es.js.map +1 -1
- package/package.json +2 -4
- package/src/plugins/eventual-consistency/analytics.js +164 -2
- package/src/plugins/eventual-consistency/config.js +4 -1
- package/src/plugins/eventual-consistency/consolidation.js +38 -4
- package/src/plugins/eventual-consistency/install.js +23 -1
- package/src/plugins/eventual-consistency/utils.js +64 -0
package/dist/s3db.cjs.js
CHANGED
|
@@ -5105,8 +5105,10 @@ function createConfig(options, detectedTimezone) {
|
|
|
5105
5105
|
consolidationWindow: consolidation.window ?? 24,
|
|
5106
5106
|
autoConsolidate: consolidation.auto !== false,
|
|
5107
5107
|
mode: consolidation.mode || "async",
|
|
5108
|
-
// ✅
|
|
5108
|
+
// ✅ Performance tuning - Mark applied concurrency (default 50, up from 10)
|
|
5109
5109
|
markAppliedConcurrency: consolidation.markAppliedConcurrency ?? 50,
|
|
5110
|
+
// ✅ Performance tuning - Recalculate concurrency (default 50, up from 10)
|
|
5111
|
+
recalculateConcurrency: consolidation.recalculateConcurrency ?? 50,
|
|
5110
5112
|
// Late arrivals
|
|
5111
5113
|
lateArrivalStrategy: lateArrivals.strategy || "warn",
|
|
5112
5114
|
// Batch transactions
|
|
@@ -5403,6 +5405,38 @@ function groupByCohort(transactions, cohortField) {
|
|
|
5403
5405
|
}
|
|
5404
5406
|
return groups;
|
|
5405
5407
|
}
|
|
5408
|
+
function ensureCohortHour(transaction, timezone = "UTC", verbose = false) {
|
|
5409
|
+
if (transaction.cohortHour) {
|
|
5410
|
+
return transaction;
|
|
5411
|
+
}
|
|
5412
|
+
if (transaction.timestamp) {
|
|
5413
|
+
const date = new Date(transaction.timestamp);
|
|
5414
|
+
const cohortInfo = getCohortInfo(date, timezone, verbose);
|
|
5415
|
+
if (verbose) {
|
|
5416
|
+
console.log(
|
|
5417
|
+
`[EventualConsistency] Transaction ${transaction.id} missing cohortHour, calculated from timestamp: ${cohortInfo.hour}`
|
|
5418
|
+
);
|
|
5419
|
+
}
|
|
5420
|
+
transaction.cohortHour = cohortInfo.hour;
|
|
5421
|
+
if (!transaction.cohortWeek) {
|
|
5422
|
+
transaction.cohortWeek = cohortInfo.week;
|
|
5423
|
+
}
|
|
5424
|
+
if (!transaction.cohortMonth) {
|
|
5425
|
+
transaction.cohortMonth = cohortInfo.month;
|
|
5426
|
+
}
|
|
5427
|
+
} else if (verbose) {
|
|
5428
|
+
console.warn(
|
|
5429
|
+
`[EventualConsistency] Transaction ${transaction.id} missing both cohortHour and timestamp, cannot calculate cohort`
|
|
5430
|
+
);
|
|
5431
|
+
}
|
|
5432
|
+
return transaction;
|
|
5433
|
+
}
|
|
5434
|
+
function ensureCohortHours(transactions, timezone = "UTC", verbose = false) {
|
|
5435
|
+
if (!transactions || !Array.isArray(transactions)) {
|
|
5436
|
+
return transactions;
|
|
5437
|
+
}
|
|
5438
|
+
return transactions.map((txn) => ensureCohortHour(txn, timezone, verbose));
|
|
5439
|
+
}
|
|
5406
5440
|
|
|
5407
5441
|
function createPartitionConfig() {
|
|
5408
5442
|
const partitions = {
|
|
@@ -5903,11 +5937,33 @@ async function consolidateRecord(originalId, transactionResource, targetResource
|
|
|
5903
5937
|
const transactionsToUpdate = transactions.filter((txn) => txn.id !== "__synthetic__");
|
|
5904
5938
|
const markAppliedConcurrency = config.markAppliedConcurrency || 50;
|
|
5905
5939
|
const { results, errors } = await promisePool.PromisePool.for(transactionsToUpdate).withConcurrency(markAppliedConcurrency).process(async (txn) => {
|
|
5940
|
+
const txnWithCohorts = ensureCohortHour(txn, config.cohort.timezone, false);
|
|
5941
|
+
const updateData = { applied: true };
|
|
5942
|
+
if (txnWithCohorts.cohortHour && !txn.cohortHour) {
|
|
5943
|
+
updateData.cohortHour = txnWithCohorts.cohortHour;
|
|
5944
|
+
}
|
|
5945
|
+
if (txnWithCohorts.cohortDate && !txn.cohortDate) {
|
|
5946
|
+
updateData.cohortDate = txnWithCohorts.cohortDate;
|
|
5947
|
+
}
|
|
5948
|
+
if (txnWithCohorts.cohortWeek && !txn.cohortWeek) {
|
|
5949
|
+
updateData.cohortWeek = txnWithCohorts.cohortWeek;
|
|
5950
|
+
}
|
|
5951
|
+
if (txnWithCohorts.cohortMonth && !txn.cohortMonth) {
|
|
5952
|
+
updateData.cohortMonth = txnWithCohorts.cohortMonth;
|
|
5953
|
+
}
|
|
5954
|
+
if (txn.value === null || txn.value === void 0) {
|
|
5955
|
+
updateData.value = 1;
|
|
5956
|
+
}
|
|
5906
5957
|
const [ok2, err2] = await tryFn(
|
|
5907
|
-
() => transactionResource.update(txn.id,
|
|
5958
|
+
() => transactionResource.update(txn.id, updateData)
|
|
5908
5959
|
);
|
|
5909
5960
|
if (!ok2 && config.verbose) {
|
|
5910
|
-
console.warn(
|
|
5961
|
+
console.warn(
|
|
5962
|
+
`[EventualConsistency] Failed to mark transaction ${txn.id} as applied:`,
|
|
5963
|
+
err2?.message,
|
|
5964
|
+
"Update data:",
|
|
5965
|
+
updateData
|
|
5966
|
+
);
|
|
5911
5967
|
}
|
|
5912
5968
|
return ok2;
|
|
5913
5969
|
});
|
|
@@ -6099,7 +6155,8 @@ async function recalculateRecord(originalId, transactionResource, targetResource
|
|
|
6099
6155
|
}
|
|
6100
6156
|
}
|
|
6101
6157
|
const transactionsToReset = allTransactions.filter((txn) => txn.source !== "anchor");
|
|
6102
|
-
const
|
|
6158
|
+
const recalculateConcurrency = config.recalculateConcurrency || 50;
|
|
6159
|
+
const { results, errors } = await promisePool.PromisePool.for(transactionsToReset).withConcurrency(recalculateConcurrency).process(async (txn) => {
|
|
6103
6160
|
const [ok, err] = await tryFn(
|
|
6104
6161
|
() => transactionResource.update(txn.id, { applied: false })
|
|
6105
6162
|
);
|
|
@@ -6527,7 +6584,10 @@ async function getAnalytics(resourceName, field, options, fieldHandlers) {
|
|
|
6527
6584
|
if (!handler.analyticsResource) {
|
|
6528
6585
|
throw new Error("Analytics not enabled for this plugin");
|
|
6529
6586
|
}
|
|
6530
|
-
const { period = "day", date, startDate, endDate, month, year, breakdown = false } = options;
|
|
6587
|
+
const { period = "day", date, startDate, endDate, month, year, breakdown = false, recordId } = options;
|
|
6588
|
+
if (recordId) {
|
|
6589
|
+
return await getAnalyticsForRecord(resourceName, field, recordId, options, handler);
|
|
6590
|
+
}
|
|
6531
6591
|
const [ok, err, allAnalytics] = await tryFn(
|
|
6532
6592
|
() => handler.analyticsResource.list()
|
|
6533
6593
|
);
|
|
@@ -6566,6 +6626,105 @@ async function getAnalytics(resourceName, field, options, fieldHandlers) {
|
|
|
6566
6626
|
recordCount: a.recordCount
|
|
6567
6627
|
}));
|
|
6568
6628
|
}
|
|
6629
|
+
async function getAnalyticsForRecord(resourceName, field, recordId, options, handler) {
|
|
6630
|
+
const { period = "day", date, startDate, endDate, month, year } = options;
|
|
6631
|
+
const [okTrue, errTrue, appliedTransactions] = await tryFn(
|
|
6632
|
+
() => handler.transactionResource.query({
|
|
6633
|
+
originalId: recordId,
|
|
6634
|
+
applied: true
|
|
6635
|
+
})
|
|
6636
|
+
);
|
|
6637
|
+
const [okFalse, errFalse, pendingTransactions] = await tryFn(
|
|
6638
|
+
() => handler.transactionResource.query({
|
|
6639
|
+
originalId: recordId,
|
|
6640
|
+
applied: false
|
|
6641
|
+
})
|
|
6642
|
+
);
|
|
6643
|
+
let allTransactions = [
|
|
6644
|
+
...okTrue && appliedTransactions ? appliedTransactions : [],
|
|
6645
|
+
...okFalse && pendingTransactions ? pendingTransactions : []
|
|
6646
|
+
];
|
|
6647
|
+
if (allTransactions.length === 0) {
|
|
6648
|
+
return [];
|
|
6649
|
+
}
|
|
6650
|
+
allTransactions = ensureCohortHours(allTransactions, handler.config?.cohort?.timezone || "UTC", false);
|
|
6651
|
+
let filtered = allTransactions;
|
|
6652
|
+
if (date) {
|
|
6653
|
+
if (period === "hour") {
|
|
6654
|
+
filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(date));
|
|
6655
|
+
} else if (period === "day") {
|
|
6656
|
+
filtered = filtered.filter((t) => t.cohortDate === date);
|
|
6657
|
+
} else if (period === "month") {
|
|
6658
|
+
filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth.startsWith(date));
|
|
6659
|
+
}
|
|
6660
|
+
} else if (startDate && endDate) {
|
|
6661
|
+
if (period === "hour") {
|
|
6662
|
+
filtered = filtered.filter((t) => t.cohortHour && t.cohortHour >= startDate && t.cohortHour <= endDate);
|
|
6663
|
+
} else if (period === "day") {
|
|
6664
|
+
filtered = filtered.filter((t) => t.cohortDate && t.cohortDate >= startDate && t.cohortDate <= endDate);
|
|
6665
|
+
} else if (period === "month") {
|
|
6666
|
+
filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth >= startDate && t.cohortMonth <= endDate);
|
|
6667
|
+
}
|
|
6668
|
+
} else if (month) {
|
|
6669
|
+
if (period === "hour") {
|
|
6670
|
+
filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(month));
|
|
6671
|
+
} else if (period === "day") {
|
|
6672
|
+
filtered = filtered.filter((t) => t.cohortDate && t.cohortDate.startsWith(month));
|
|
6673
|
+
}
|
|
6674
|
+
} else if (year) {
|
|
6675
|
+
if (period === "hour") {
|
|
6676
|
+
filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(String(year)));
|
|
6677
|
+
} else if (period === "day") {
|
|
6678
|
+
filtered = filtered.filter((t) => t.cohortDate && t.cohortDate.startsWith(String(year)));
|
|
6679
|
+
} else if (period === "month") {
|
|
6680
|
+
filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth.startsWith(String(year)));
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6683
|
+
const cohortField = period === "hour" ? "cohortHour" : period === "day" ? "cohortDate" : "cohortMonth";
|
|
6684
|
+
const aggregated = aggregateTransactionsByCohort(filtered, cohortField);
|
|
6685
|
+
return aggregated;
|
|
6686
|
+
}
|
|
6687
|
+
function aggregateTransactionsByCohort(transactions, cohortField) {
|
|
6688
|
+
const groups = {};
|
|
6689
|
+
for (const txn of transactions) {
|
|
6690
|
+
const cohort = txn[cohortField];
|
|
6691
|
+
if (!cohort) continue;
|
|
6692
|
+
if (!groups[cohort]) {
|
|
6693
|
+
groups[cohort] = {
|
|
6694
|
+
cohort,
|
|
6695
|
+
count: 0,
|
|
6696
|
+
sum: 0,
|
|
6697
|
+
min: Infinity,
|
|
6698
|
+
max: -Infinity,
|
|
6699
|
+
recordCount: /* @__PURE__ */ new Set(),
|
|
6700
|
+
operations: {}
|
|
6701
|
+
};
|
|
6702
|
+
}
|
|
6703
|
+
const group = groups[cohort];
|
|
6704
|
+
const signedValue = txn.operation === "sub" ? -txn.value : txn.value;
|
|
6705
|
+
group.count++;
|
|
6706
|
+
group.sum += signedValue;
|
|
6707
|
+
group.min = Math.min(group.min, signedValue);
|
|
6708
|
+
group.max = Math.max(group.max, signedValue);
|
|
6709
|
+
group.recordCount.add(txn.originalId);
|
|
6710
|
+
const op = txn.operation;
|
|
6711
|
+
if (!group.operations[op]) {
|
|
6712
|
+
group.operations[op] = { count: 0, sum: 0 };
|
|
6713
|
+
}
|
|
6714
|
+
group.operations[op].count++;
|
|
6715
|
+
group.operations[op].sum += signedValue;
|
|
6716
|
+
}
|
|
6717
|
+
return Object.values(groups).map((g) => ({
|
|
6718
|
+
cohort: g.cohort,
|
|
6719
|
+
count: g.count,
|
|
6720
|
+
sum: g.sum,
|
|
6721
|
+
avg: g.sum / g.count,
|
|
6722
|
+
min: g.min === Infinity ? 0 : g.min,
|
|
6723
|
+
max: g.max === -Infinity ? 0 : g.max,
|
|
6724
|
+
recordCount: g.recordCount.size,
|
|
6725
|
+
operations: g.operations
|
|
6726
|
+
})).sort((a, b) => a.cohort.localeCompare(b.cohort));
|
|
6727
|
+
}
|
|
6569
6728
|
async function getMonthByDay(resourceName, field, month, options, fieldHandlers) {
|
|
6570
6729
|
const year = parseInt(month.substring(0, 4));
|
|
6571
6730
|
const monthNum = parseInt(month.substring(5, 7));
|
|
@@ -6600,6 +6759,8 @@ async function getLastNDays(resourceName, field, days, options, fieldHandlers) {
|
|
|
6600
6759
|
return date.toISOString().substring(0, 10);
|
|
6601
6760
|
}).reverse();
|
|
6602
6761
|
const data = await getAnalytics(resourceName, field, {
|
|
6762
|
+
...options,
|
|
6763
|
+
// ✅ Include all options (recordId, etc.)
|
|
6603
6764
|
period: "day",
|
|
6604
6765
|
startDate: dates[0],
|
|
6605
6766
|
endDate: dates[dates.length - 1]
|
|
@@ -6793,6 +6954,8 @@ async function getLastNHours(resourceName, field, hours = 24, options, fieldHand
|
|
|
6793
6954
|
const startHour = hoursAgo.toISOString().substring(0, 13);
|
|
6794
6955
|
const endHour = now.toISOString().substring(0, 13);
|
|
6795
6956
|
const data = await getAnalytics(resourceName, field, {
|
|
6957
|
+
...options,
|
|
6958
|
+
// ✅ Include all options (recordId, etc.)
|
|
6796
6959
|
period: "hour",
|
|
6797
6960
|
startDate: startHour,
|
|
6798
6961
|
endDate: endHour
|
|
@@ -6840,6 +7003,8 @@ async function getLastNMonths(resourceName, field, months = 12, options, fieldHa
|
|
|
6840
7003
|
const startDate = monthsAgo.toISOString().substring(0, 7);
|
|
6841
7004
|
const endDate = now.toISOString().substring(0, 7);
|
|
6842
7005
|
const data = await getAnalytics(resourceName, field, {
|
|
7006
|
+
...options,
|
|
7007
|
+
// ✅ Include all options (recordId, etc.)
|
|
6843
7008
|
period: "month",
|
|
6844
7009
|
startDate,
|
|
6845
7010
|
endDate
|
|
@@ -7032,7 +7197,8 @@ async function completeFieldSetup(handler, database, config, plugin) {
|
|
|
7032
7197
|
operation: "string|required",
|
|
7033
7198
|
timestamp: "string|required",
|
|
7034
7199
|
cohortDate: "string|required",
|
|
7035
|
-
cohortHour: "string|
|
|
7200
|
+
cohortHour: "string|optional",
|
|
7201
|
+
// ✅ FIX BUG #2: Changed from required to optional for migration compatibility
|
|
7036
7202
|
cohortWeek: "string|optional",
|
|
7037
7203
|
cohortMonth: "string|optional",
|
|
7038
7204
|
source: "string|optional",
|
|
@@ -7081,6 +7247,28 @@ async function createAnalyticsResource(handler, database, resourceName, fieldNam
|
|
|
7081
7247
|
},
|
|
7082
7248
|
behavior: "body-overflow",
|
|
7083
7249
|
timestamps: false,
|
|
7250
|
+
asyncPartitions: true,
|
|
7251
|
+
// ✅ Multi-attribute partitions for optimal analytics query performance
|
|
7252
|
+
partitions: {
|
|
7253
|
+
// Query by period (hour/day/week/month)
|
|
7254
|
+
byPeriod: {
|
|
7255
|
+
fields: { period: "string" }
|
|
7256
|
+
},
|
|
7257
|
+
// Query by period + cohort (e.g., all hour records for specific hours)
|
|
7258
|
+
byPeriodCohort: {
|
|
7259
|
+
fields: {
|
|
7260
|
+
period: "string",
|
|
7261
|
+
cohort: "string"
|
|
7262
|
+
}
|
|
7263
|
+
},
|
|
7264
|
+
// Query by field + period (e.g., all daily analytics for clicks field)
|
|
7265
|
+
byFieldPeriod: {
|
|
7266
|
+
fields: {
|
|
7267
|
+
field: "string",
|
|
7268
|
+
period: "string"
|
|
7269
|
+
}
|
|
7270
|
+
}
|
|
7271
|
+
},
|
|
7084
7272
|
createdBy: "EventualConsistencyPlugin"
|
|
7085
7273
|
})
|
|
7086
7274
|
);
|
|
@@ -13610,7 +13798,7 @@ class Database extends EventEmitter {
|
|
|
13610
13798
|
this.id = idGenerator(7);
|
|
13611
13799
|
this.version = "1";
|
|
13612
13800
|
this.s3dbVersion = (() => {
|
|
13613
|
-
const [ok, err, version] = tryFn(() => true ? "11.
|
|
13801
|
+
const [ok, err, version] = tryFn(() => true ? "11.2.2" : "latest");
|
|
13614
13802
|
return ok ? version : "latest";
|
|
13615
13803
|
})();
|
|
13616
13804
|
this.resources = {};
|