What happens
withCircuitBreaker() keeps one state/failures pair in the closure returned by the factory. A middleware registered with email.use() wraps every mounted driver, so one failing provider opens the circuit for all of them.
Reproduction
const healthy = mock()
const email = createEmail({
driver: mock({ fail: true }), // the provider that is down
defaults: { from: "f@x.com" },
use: [withCircuitBreaker({ threshold: 2, now: () => 0 })],
})
email.mount("other", healthy) // a completely different provider
await email.send(msg) // fails
await email.send(msg) // fails -> circuit opens
await email.send({ ...msg, stream: "other" })
Observed:
healthy mount: BLOCKED (NETWORK: [unemail] [mock] circuit is open — provider is failing)
healthy inbox size: 0 (expected 1)
Why it matters
mount() exists so a message can be routed to a different provider — the failover story the README sells. The breaker defeats exactly that: Resend going down stops the SES mount from sending, and the error blames the wrong driver. The outage gets wider instead of narrower.
Where
src/middleware/circuit-breaker.ts:39-42 — state, failures and openedAt are per middleware instance, not per destination. ctx.driver is available on every call and is not consulted.
Suggested fix
Key the state by destination (ctx.driver plus ctx.stream) in a Map, so each provider trips independently. onStateChange already takes the driver name, which suggests per-driver was the intent. Alternatively document that the breaker belongs on wrap(driver, ...) rather than use() — but the default should be the safe one.
What happens
withCircuitBreaker()keeps onestate/failurespair in the closure returned by the factory. A middleware registered withemail.use()wraps every mounted driver, so one failing provider opens the circuit for all of them.Reproduction
Observed:
Why it matters
mount()exists so a message can be routed to a different provider — the failover story the README sells. The breaker defeats exactly that: Resend going down stops the SES mount from sending, and the error blames the wrong driver. The outage gets wider instead of narrower.Where
src/middleware/circuit-breaker.ts:39-42—state,failuresandopenedAtare per middleware instance, not per destination.ctx.driveris available on every call and is not consulted.Suggested fix
Key the state by destination (
ctx.driverplusctx.stream) in aMap, so each provider trips independently.onStateChangealready takes the driver name, which suggests per-driver was the intent. Alternatively document that the breaker belongs onwrap(driver, ...)rather thanuse()— but the default should be the safe one.