Comparing version 5.0.0 to 5.1.0
@@ -16,2 +16,10 @@ "use strict"; | ||
const balance_1 = require("./models/balance"); | ||
const GROUP = { | ||
$group: { | ||
_id: null, | ||
balance: { $sum: { $subtract: ["$credit", "$debit"] } }, | ||
notes: { $sum: 1 }, | ||
lastTransactionId: { $max: "$_id" }, | ||
}, | ||
}; | ||
class Book { | ||
@@ -23,2 +31,4 @@ constructor(name, options = {}) { | ||
this.balanceSnapshotSec = options.balanceSnapshotSec != null ? options.balanceSnapshotSec : 24 * 60 * 60; | ||
this.expireBalanceSnapshotSec = | ||
options.expireBalanceSnapshotSec != null ? options.expireBalanceSnapshotSec : 2 * this.balanceSnapshotSec; | ||
if (typeof this.name !== "string" || this.name.trim().length === 0) { | ||
@@ -36,2 +46,5 @@ throw new errors_1.BookConstructorError("Invalid value for name provided."); | ||
} | ||
if (typeof this.expireBalanceSnapshotSec !== "number" || this.expireBalanceSnapshotSec < 0) { | ||
throw new errors_1.BookConstructorError("Invalid value for expireBalanceSnapshotSec provided."); | ||
} | ||
} | ||
@@ -47,3 +60,2 @@ entry(memo, date = null, original_journal) { | ||
let accountForBalanceSnapshot; | ||
let needToDoBalanceSnapshot = true; | ||
if (this.balanceSnapshotSec) { | ||
@@ -57,4 +69,4 @@ accountForBalanceSnapshot = query.account ? [].concat(query.account).join() : undefined; | ||
if (balanceSnapshot) { | ||
// Use cached balance | ||
parsedQuery._id = { $gt: balanceSnapshot.transaction }; | ||
needToDoBalanceSnapshot = Date.now() > balanceSnapshot.createdAt.getTime() + this.balanceSnapshotSec * 1000; | ||
} | ||
@@ -65,11 +77,3 @@ } | ||
}; | ||
const group = { | ||
$group: { | ||
_id: null, | ||
balance: { $sum: { $subtract: ["$credit", "$debit"] } }, | ||
notes: { $sum: 1 }, | ||
lastTransactionId: { $max: "$_id" }, | ||
}, | ||
}; | ||
const result = (await transaction_1.transactionModel.collection.aggregate([match, group], options).toArray())[0]; | ||
const result = (await transaction_1.transactionModel.collection.aggregate([match, GROUP], options).toArray())[0]; | ||
let balance = 0; | ||
@@ -84,12 +88,53 @@ let notes = 0; | ||
notes += result.notes; | ||
if (needToDoBalanceSnapshot && result.lastTransactionId) { | ||
await (0, balance_1.snapshotBalance)({ | ||
book: this.name, | ||
account: accountForBalanceSnapshot, | ||
meta, | ||
transaction: result.lastTransactionId, | ||
balance, | ||
notes, | ||
expireInSec: this.balanceSnapshotSec * 2, // Keep the document twice longer than needed in case this particular balance() query is not executed very often. | ||
}, options); | ||
// We can do snapshots only if there is at least one entry for this balance | ||
if (this.balanceSnapshotSec && result.lastTransactionId) { | ||
// It's the first (ever?) snapshot for this balance. We just need to save whatever we've just aggregated | ||
// so that the very next balance query would use cached snapshot. | ||
if (!balanceSnapshot) { | ||
await (0, balance_1.snapshotBalance)({ | ||
book: this.name, | ||
account: accountForBalanceSnapshot, | ||
meta, | ||
transaction: result.lastTransactionId, | ||
balance, | ||
notes, | ||
expireInSec: this.expireBalanceSnapshotSec, | ||
}, options); | ||
} | ||
else { | ||
// There is a snapshot already. But let's check if it's too old. | ||
const tooOld = Date.now() > balanceSnapshot.createdAt.getTime() + this.balanceSnapshotSec * 1000; | ||
// If it's too old we would need to cache another snapshot. | ||
if (tooOld) { | ||
delete parsedQuery._id; | ||
const match = { | ||
$match: { ...parsedQuery, ...(0, flattenObject_1.flattenObject)(meta, "meta") }, | ||
}; | ||
// Important! We are going to recalculate the entire balance from the day one. | ||
// Since this operation can take seconds (if you have millions of documents) | ||
// we better run this query IN THE BACKGROUND. | ||
// If this exact balance query would be executed multiple times at the same second we might end up with | ||
// multiple snapshots in the database. Which is fine. The chance of this happening is low. | ||
// Our main goal here is not to delay this .balance() method call. The tradeoff is that | ||
// database will use 100% CPU for few (milli)seconds, which is fine. It's all fine (C) | ||
transaction_1.transactionModel.collection | ||
.aggregate([match, GROUP], options) | ||
.toArray() | ||
.then((results) => { | ||
const resultFull = results[0]; | ||
return (0, balance_1.snapshotBalance)({ | ||
book: this.name, | ||
account: accountForBalanceSnapshot, | ||
meta, | ||
transaction: resultFull.lastTransactionId, | ||
balance: parseFloat(resultFull.balance.toFixed(this.precision)), | ||
notes: resultFull.notes, | ||
expireInSec: this.expireBalanceSnapshotSec, | ||
}, options); | ||
}) | ||
.catch((error) => { | ||
console.error("medici: Couldn't do background balance snapshot.", error); | ||
}); | ||
} | ||
} | ||
} | ||
@@ -96,0 +141,0 @@ } |
{ | ||
"name": "medici", | ||
"version": "5.0.0", | ||
"version": "5.1.0", | ||
"description": "Simple double-entry accounting for Node + Mongoose", | ||
@@ -5,0 +5,0 @@ "main": "build/index.js", |
@@ -405,2 +405,13 @@ # medici | ||
### v5.1.0 | ||
The balance snapshots were never recalculated from the beginning of the ledger. They were always based on the most recent snapshot. It gave us speed. Although, if one of the snapshots gets corrupt or an early ledger entry gets manually edited/deleted then we would always get wrong number from the `.balance()` method. Thus, we have to calculate snapshots from the beginning of the ledger at least once in a while. | ||
BUT! If you have millions of documents in `medici_transactions` collection a full balance recalculation might take up to 10 seconds. So, we can't afford aggregation of the entire database during the `.blance()` invocation. Solution: let's aggregate it **in the background**. Thus, v5.1 was born. | ||
New feature: | ||
- In addition to the existing `balanceSnapshotSec` option, we added `expireBalanceSnapshotSec`. | ||
- The `balanceSnapshotSec` tells medici how often you want those snapshots to be made **in the background** (right after the `.balance()` call). Default value - 24 hours. | ||
- The `expireBalanceSnapshotSec` tells medici when to evict those snapshots from the database (TTL). It is recommended to set `expireBalanceSnapshotSec` higher than `balanceSnapshotSec`. Default value - twice the `balanceSnapshotSec`. | ||
### v5.0.0 | ||
@@ -407,0 +418,0 @@ |
@@ -81,2 +81,3 @@ // Generated by dts-bundle-generator v6.2.0 | ||
balanceSnapshotSec: number; | ||
expireBalanceSnapshotSec: number; | ||
constructor(name: string, options?: { | ||
@@ -86,2 +87,3 @@ precision?: number; | ||
balanceSnapshotSec?: number; | ||
expireBalanceSnapshotSec?: number; | ||
}); | ||
@@ -88,0 +90,0 @@ entry(memo: string, date?: Date, original_journal?: string | Types.ObjectId): Entry<U, J>; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
77087
1134
498