Skip to content

Changelog - 2026-07-18

Repository Read Retry

New Feature

In one line. find, findOne, and findById accept an opt-in retry option: re-read with backoff until the result looks right. Built for read-after-write lag behind a replicated pool such as PgDog.

The problem it solves

You create a row, then read it back. The read lands on a replica that has not caught up, so the row "is not there". retry re-reads until it is:

typescript
const user = await userRepository.findById({
  id,
  options: { retry: { maxAttempts: 4 } },
});

After an update, tell retry what "fresh" means:

typescript
const order = await orderRepository.findById({
  id,
  options: { retry: { until: result => result?.status === 'PAID' } },
});

What changed

  • find, findOne, findById accept options.retry - on the PostgreSQL chain and the search chain (Typesense, Meilisearch).
  • Off by default. Without retry, behavior is exactly what it was before.
  • Write verbs (create, updateById, ...) do not have this option. retry on a write is a compile error.
  • New helper in @venizia/ignis-helpers: executeWithRetryUntil - the loop behind the option, usable anywhere you need to poll for a condition.

The rules, in plain words

  • Retry happens only when the read SUCCEEDED but the result is not what you want yet.
  • A real database error is never retried. It throws immediately, same as before.
  • Out of attempts? You get the last result as-is. No new error.
  • Inside a transaction, retry is skipped - transactions already go to the primary.

Options

OptionDefaultMeaning
maxAttempts3Total reads, including the first
untilper verb (below)Return true to stop: "the result is fresh enough"
maxTotalMsunlimitedStop starting new attempts after this much time
backoff50ms up to 500ms, jitteredWait between attempts
signal-Cancel the loop (rejects the call)

Default until per verb - the predicate is typed, so it sees exactly what the verb returns:

VerbStops when
findOne / findByIdresult is not null/undefined
findarray is non-empty
find + shouldQueryRange: truedata is non-empty

Who is affected

  • Single-primary setups: nothing to do.
  • Apps behind a replicated pool: add retry to reads that follow writes. Delete your hand-rolled polling loops.

See also