@harperfast/harper 5.0.30 → 5.0.31

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@harperfast/harper",
3
3
  "description": "Harper is an open-source Node.js performance platform that unifies database, cache, application, and messaging layers into one in-memory process.",
4
- "version": "5.0.30",
4
+ "version": "5.0.31",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://harper.fast",
7
7
  "bugs": {
@@ -1733,6 +1733,32 @@ export function makeTable(options) {
1733
1733
  // of the updates to the record to ensure consistency across the cluster
1734
1734
  // TODO: can the previous version be older, but even more previous version be newer?
1735
1735
  if (audit) {
1736
+ // A re-delivered out-of-order write (full-copy audit-replay re-delivers writes) must not have
1737
+ // its commutative ops re-folded. additionalAuditRefs is the record's own list of folded
1738
+ // out-of-order versions, read with read-your-writes consistency, so this skips the duplicate up
1739
+ // front — before the audit-log walk below, which can miss it: the walk stops at the depth cap, or
1740
+ // breaks early on a not-yet-visible audit entry, before reaching txnTime, and the keyed
1741
+ // transaction-log lookup it would otherwise use can lag a back-to-back re-delivery (that lag
1742
+ // silently double-applied the increment — #1137). This covers the re-delivery while the ref is
1743
+ // still on the record; a later in-order write rewrites the record and drops the ref (it survives
1744
+ // only as previousAdditionalAuditRefs on the audit log), so that case falls back to the
1745
+ // best-effort keyed lookup in the capped block below — see #1148. precedesExistingVersion(...)
1746
+ // === 0 is the identity tie: same version AND same node (the local node is id 0, so an undefined
1747
+ // options?.nodeId resolves to the same 0 the ref stored).
1748
+ if (
1749
+ existingEntry.additionalAuditRefs?.some(
1750
+ (ref) =>
1751
+ ref.version === txnTime &&
1752
+ precedesExistingVersion(
1753
+ txnTime,
1754
+ { version: txnTime, localTime: txnTime, key: id, nodeId: ref.nodeId },
1755
+ options?.nodeId
1756
+ ) === 0
1757
+ )
1758
+ ) {
1759
+ write.skipped = true;
1760
+ return; // out-of-order write already folded into this record
1761
+ }
1736
1762
  // incremental CRDT updates are only available with audit logging on
1737
1763
  let localTime = existingEntry.localTime;
1738
1764
  let auditedVersion = existingEntry.version;
@@ -1864,8 +1890,12 @@ export function makeTable(options) {
1864
1890
  // retained window are not layered in — but the authoritative full-copy record restores exact
1865
1891
  // convergence. Because we stopped before reaching txnTime, the inline duplicate detection in
1866
1892
  // the walk never ran; full-copy audit-replay re-delivers writes, and re-applying one would
1867
- // double-apply its commutative ops, so rule that out here with a single O(1) lookup at txnTime
1868
- // (RocksDB audit logs are keyed by version, and the cap is RocksDB-only).
1893
+ // double-apply its commutative ops. A re-delivered out-of-order write is already ruled out by
1894
+ // the additionalAuditRefs check at the top of this block; this keyed lookup is the best-effort
1895
+ // guard for the remaining case — a re-delivered write that was originally in-order (so it left
1896
+ // no ref) and is now deeper than the cap. It is best-effort because the transaction-log lookup
1897
+ // can intermittently miss an entry under load (tracked separately); the authoritative full-copy
1898
+ // record still restores exact convergence.
1869
1899
  logger.warn?.(
1870
1900
  'Out-of-order audit reconciliation exceeded depth cap; reconciling against most recent updates only',
1871
1901
  {