s3db.js 10.0.13 → 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 +106 -7
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +106 -7
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/plugins/eventual-consistency.plugin.js +159 -11
package/package.json
CHANGED
|
@@ -1092,14 +1092,7 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
1092
1092
|
}
|
|
1093
1093
|
|
|
1094
1094
|
try {
|
|
1095
|
-
// Get
|
|
1096
|
-
const [recordOk, recordErr, record] = await tryFn(() =>
|
|
1097
|
-
this.targetResource.get(originalId)
|
|
1098
|
-
);
|
|
1099
|
-
|
|
1100
|
-
const currentValue = (recordOk && record) ? (record[this.config.field] || 0) : 0;
|
|
1101
|
-
|
|
1102
|
-
// Get all transactions for this record
|
|
1095
|
+
// Get all unapplied transactions for this record
|
|
1103
1096
|
const [ok, err, transactions] = await tryFn(() =>
|
|
1104
1097
|
this.transactionResource.query({
|
|
1105
1098
|
originalId,
|
|
@@ -1108,6 +1101,12 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
1108
1101
|
);
|
|
1109
1102
|
|
|
1110
1103
|
if (!ok || !transactions || transactions.length === 0) {
|
|
1104
|
+
// No pending transactions - try to get current value from record
|
|
1105
|
+
const [recordOk, recordErr, record] = await tryFn(() =>
|
|
1106
|
+
this.targetResource.get(originalId)
|
|
1107
|
+
);
|
|
1108
|
+
const currentValue = (recordOk && record) ? (record[this.config.field] || 0) : 0;
|
|
1109
|
+
|
|
1111
1110
|
if (this.config.verbose) {
|
|
1112
1111
|
console.log(
|
|
1113
1112
|
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
|
|
@@ -1117,20 +1116,169 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
1117
1116
|
return currentValue;
|
|
1118
1117
|
}
|
|
1119
1118
|
|
|
1119
|
+
// Get the LAST APPLIED VALUE from transactions (not from record - avoids S3 eventual consistency issues)
|
|
1120
|
+
// This is the source of truth for the current value
|
|
1121
|
+
const [appliedOk, appliedErr, appliedTransactions] = await tryFn(() =>
|
|
1122
|
+
this.transactionResource.query({
|
|
1123
|
+
originalId,
|
|
1124
|
+
applied: true
|
|
1125
|
+
})
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
let currentValue = 0;
|
|
1129
|
+
|
|
1130
|
+
if (appliedOk && appliedTransactions && appliedTransactions.length > 0) {
|
|
1131
|
+
// Check if record exists - if deleted, ignore old applied transactions
|
|
1132
|
+
const [recordExistsOk, recordExistsErr, recordExists] = await tryFn(() =>
|
|
1133
|
+
this.targetResource.get(originalId)
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
if (!recordExistsOk || !recordExists) {
|
|
1137
|
+
// Record was deleted - ignore applied transactions and start fresh
|
|
1138
|
+
// This prevents old values from being carried over after deletion
|
|
1139
|
+
if (this.config.verbose) {
|
|
1140
|
+
console.log(
|
|
1141
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
|
|
1142
|
+
`Record ${originalId} doesn't exist, deleting ${appliedTransactions.length} old applied transactions`
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Delete old applied transactions to prevent them from being used when record is recreated
|
|
1147
|
+
const { results, errors } = await PromisePool
|
|
1148
|
+
.for(appliedTransactions)
|
|
1149
|
+
.withConcurrency(10)
|
|
1150
|
+
.process(async (txn) => {
|
|
1151
|
+
const [deleted] = await tryFn(() => this.transactionResource.delete(txn.id));
|
|
1152
|
+
return deleted;
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
if (this.config.verbose && errors && errors.length > 0) {
|
|
1156
|
+
console.warn(
|
|
1157
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
|
|
1158
|
+
`Failed to delete ${errors.length} old applied transactions`
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
currentValue = 0;
|
|
1163
|
+
} else {
|
|
1164
|
+
// Record exists - use applied transactions to calculate current value
|
|
1165
|
+
// Sort by timestamp to get chronological order
|
|
1166
|
+
appliedTransactions.sort((a, b) =>
|
|
1167
|
+
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
// Check if there's a 'set' operation in applied transactions
|
|
1171
|
+
const hasSetInApplied = appliedTransactions.some(t => t.operation === 'set');
|
|
1172
|
+
|
|
1173
|
+
if (!hasSetInApplied) {
|
|
1174
|
+
// No 'set' operation in applied transactions means we're missing the base value
|
|
1175
|
+
// This can only happen if:
|
|
1176
|
+
// 1. Record had an initial value before first transaction
|
|
1177
|
+
// 2. First consolidation didn't create an anchor transaction (legacy behavior)
|
|
1178
|
+
// Solution: Get the current record value and create an anchor transaction now
|
|
1179
|
+
const recordValue = recordExists[this.config.field] || 0;
|
|
1180
|
+
|
|
1181
|
+
// Calculate what the base value was by subtracting all applied deltas
|
|
1182
|
+
let appliedDelta = 0;
|
|
1183
|
+
for (const t of appliedTransactions) {
|
|
1184
|
+
if (t.operation === 'add') appliedDelta += t.value;
|
|
1185
|
+
else if (t.operation === 'sub') appliedDelta -= t.value;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const baseValue = recordValue - appliedDelta;
|
|
1189
|
+
|
|
1190
|
+
// Create and save anchor transaction with the base value
|
|
1191
|
+
// Only create if baseValue is non-zero AND we don't already have an anchor transaction
|
|
1192
|
+
const hasExistingAnchor = appliedTransactions.some(t => t.source === 'anchor');
|
|
1193
|
+
if (baseValue !== 0 && !hasExistingAnchor) {
|
|
1194
|
+
// Use the timestamp of the first applied transaction for cohort info
|
|
1195
|
+
const firstTransactionDate = new Date(appliedTransactions[0].timestamp);
|
|
1196
|
+
const cohortInfo = this.getCohortInfo(firstTransactionDate);
|
|
1197
|
+
const anchorTransaction = {
|
|
1198
|
+
id: idGenerator(),
|
|
1199
|
+
originalId: originalId,
|
|
1200
|
+
field: this.config.field,
|
|
1201
|
+
value: baseValue,
|
|
1202
|
+
operation: 'set',
|
|
1203
|
+
timestamp: new Date(firstTransactionDate.getTime() - 1).toISOString(), // 1ms before first txn to ensure it's first
|
|
1204
|
+
cohortDate: cohortInfo.date,
|
|
1205
|
+
cohortHour: cohortInfo.hour,
|
|
1206
|
+
cohortMonth: cohortInfo.month,
|
|
1207
|
+
source: 'anchor',
|
|
1208
|
+
applied: true
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
await this.transactionResource.insert(anchorTransaction);
|
|
1212
|
+
|
|
1213
|
+
// Prepend to applied transactions for this consolidation
|
|
1214
|
+
appliedTransactions.unshift(anchorTransaction);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Apply reducer to get the last consolidated value
|
|
1219
|
+
currentValue = this.config.reducer(appliedTransactions);
|
|
1220
|
+
}
|
|
1221
|
+
} else {
|
|
1222
|
+
// No applied transactions - this is the FIRST consolidation
|
|
1223
|
+
// Try to get initial value from record
|
|
1224
|
+
const [recordOk, recordErr, record] = await tryFn(() =>
|
|
1225
|
+
this.targetResource.get(originalId)
|
|
1226
|
+
);
|
|
1227
|
+
currentValue = (recordOk && record) ? (record[this.config.field] || 0) : 0;
|
|
1228
|
+
|
|
1229
|
+
// If there's an initial value, create and save an anchor transaction
|
|
1230
|
+
// This ensures all future consolidations have a reliable base value
|
|
1231
|
+
if (currentValue !== 0) {
|
|
1232
|
+
// Use timestamp of the first pending transaction (or current time if none)
|
|
1233
|
+
let anchorTimestamp;
|
|
1234
|
+
if (transactions && transactions.length > 0) {
|
|
1235
|
+
const firstPendingDate = new Date(transactions[0].timestamp);
|
|
1236
|
+
anchorTimestamp = new Date(firstPendingDate.getTime() - 1).toISOString();
|
|
1237
|
+
} else {
|
|
1238
|
+
anchorTimestamp = new Date().toISOString();
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const cohortInfo = this.getCohortInfo(new Date(anchorTimestamp));
|
|
1242
|
+
const anchorTransaction = {
|
|
1243
|
+
id: idGenerator(),
|
|
1244
|
+
originalId: originalId,
|
|
1245
|
+
field: this.config.field,
|
|
1246
|
+
value: currentValue,
|
|
1247
|
+
operation: 'set',
|
|
1248
|
+
timestamp: anchorTimestamp,
|
|
1249
|
+
cohortDate: cohortInfo.date,
|
|
1250
|
+
cohortHour: cohortInfo.hour,
|
|
1251
|
+
cohortMonth: cohortInfo.month,
|
|
1252
|
+
source: 'anchor',
|
|
1253
|
+
applied: true
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
await this.transactionResource.insert(anchorTransaction);
|
|
1257
|
+
|
|
1258
|
+
if (this.config.verbose) {
|
|
1259
|
+
console.log(
|
|
1260
|
+
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
|
|
1261
|
+
`Created anchor transaction for ${originalId} with base value ${currentValue}`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1120
1267
|
if (this.config.verbose) {
|
|
1121
1268
|
console.log(
|
|
1122
1269
|
`[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
|
|
1123
1270
|
`Consolidating ${originalId}: ${transactions.length} pending transactions ` +
|
|
1124
|
-
`(current: ${currentValue})`
|
|
1271
|
+
`(current: ${currentValue} from ${appliedOk && appliedTransactions?.length > 0 ? 'applied transactions' : 'record'})`
|
|
1125
1272
|
);
|
|
1126
1273
|
}
|
|
1127
1274
|
|
|
1128
|
-
// Sort transactions by timestamp
|
|
1275
|
+
// Sort pending transactions by timestamp
|
|
1129
1276
|
transactions.sort((a, b) =>
|
|
1130
1277
|
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
1131
1278
|
);
|
|
1132
1279
|
|
|
1133
|
-
// If there's a current value and no 'set' operations
|
|
1280
|
+
// If there's a current value and no 'set' operations in pending transactions,
|
|
1281
|
+
// prepend a synthetic set transaction to preserve the current value
|
|
1134
1282
|
const hasSetOperation = transactions.some(t => t.operation === 'set');
|
|
1135
1283
|
if (currentValue !== 0 && !hasSetOperation) {
|
|
1136
1284
|
transactions.unshift(this._createSyntheticSetTransaction(currentValue));
|