fix(mocks): fold the audit window, and append MPE_AUDIT under its lock

Two review findings on this branch.

- editors/restore.js spread every audit row for the load into Math.min(...).
  MPE_AUDIT is append-only, so a long-lived estate would eventually exceed the
  engine's argument limit and the RangeError would surface as a failed restore
  of a perfectly restorable load. Folded into a reduce with an Infinity seed.

- auditors/postdata.js appended to MPE_AUDIT as a read-modify-write outside any
  lock. postdata.sas locks the table it writes - MPE_REVIEW around its proc
  append, MPE_SUBMIT around its update - so the audit append now takes the
  MPE_AUDIT lock around a fresh read, the concat and the save, rather than
  reading the table up front and saving a copy that may be missing another
  approval's rows.

53 mock tests pass (node --test sas/mocks/test/).
This commit is contained in:
dc committed 2026-11-05 10:44:40 +00:00
1 parent 1a60c689f4
commit 5ecc7bebec
2 files changed
+25 -9

No files matched your search

+22 -7
View File
@@ -728,11 +728,10 @@ if (action === 'SHOW_DIFFS') {
.join('|')
)
const auditTable = mpeLoadTableData('MPE_AUDIT') || {
name: 'MPE_AUDIT',
columns: [],
rows: []
}
// The rows this approval adds. They are appended to the table under the
// MPE_REVIEW lock below, not to a copy read here: two approvals landing
// together would otherwise each save a table without the other's rows.
const pending = []
const entry = (row, moveType, column, oldVal, newVal, isPk, isDiff) => {
const type = column.type === 'N' ? 'N' : 'C'
@@ -759,7 +758,7 @@ if (action === 'SHOW_DIFFS') {
}
put('OLDVAL', oldVal)
put('NEWVAL', newVal)
auditTable.rows.push(auditRow)
pending.push(auditRow)
}
const isKey = (name) =>
@@ -792,7 +791,23 @@ if (action === 'SHOW_DIFFS') {
}
}
if (auditTable.rows.length) saveTableToLib('MPE_AUDIT', auditTable)
if (pending.length) {
// postdata.sas locks the table it appends to (MPE_REVIEW around its
// proc append, MPE_SUBMIT around its update), so the audit append takes
// the MPE_AUDIT lock rather than borrowing the review one.
lockControl('MPE_AUDIT', loadRef + ' Approval')
try {
const fresh = mpeLoadTableData('MPE_AUDIT') || {
name: 'MPE_AUDIT',
columns: [],
rows: []
}
fresh.rows = (fresh.rows || []).concat(pending)
saveTableToLib('MPE_AUDIT', fresh)
} finally {
unlockControl('MPE_AUDIT')
}
}
} catch (err) {
// an audit failure must not undo an approval that already landed
console.log('Error writing MPE_AUDIT: ' + err.message)
+3 -2
View File
@@ -115,8 +115,9 @@ try {
// The load's timestamp, then every change to that table from then on: the
// change to revert plus all subsequent ones.
const stamp = Math.min(
...mine.map((row) => Number(colVal(row, 'PROCESSED_DTTM')))
const stamp = mine.reduce(
(min, row) => Math.min(min, Number(colVal(row, 'PROCESSED_DTTM'))),
Infinity
)
const window = all.filter(
(row) =>