Retry & Pooling
Ride out transient network failures, and see what the connection pool is doing.
Both are configured on DBConfig and read back from the ORM.
Retry Configuration
import { Stabilize, DBType } from "stabilize-orm";
const orm = new Stabilize({ type: DBType.Postgres, connectionString: process.env.DATABASE_URL!, retryAttempts: 3, // default 3 retryDelay: 1000, // default 1000ms maxJitter: 100, // default 100ms});Which Statements Retry
Reads retry. Writes do not.
Retry applies only to statements that begin with SELECT, PRAGMA, SHOW, EXPLAIN or VALUES (leading comments and whitespace are stripped first). Every write runs exactly once, always. This is deliberate: a write that failed after committing on the server, but before the response arrived, would be applied twice by a retry. Reads have no such hazard.
// Retried on failure, up to retryAttemptsawait repo.find().where("status = ?", "active").execute(db.client);await db.rawQuery("SELECT * FROM users");await db.rawQuery("PRAGMA table_info(users)");
// Never retried - one attempt onlyawait repo.create({ title: "Draft" });await db.rawExec("UPDATE users SET active = ? WHERE id = ?", [true, 1]);
// To effectively disable retry, set it to 1const noRetry = new Stabilize({ type: DBType.Postgres, connectionString: url, retryAttempts: 1,});Backoff
Delays double with each attempt, plus a random jitter so that many clients failing together do not all retry in lockstep:
delay = retryDelay * 2^(attempt - 1) + random(0 .. maxJitter)
// With retryDelay: 1000, maxJitter: 100// attempt 1 -> ~1000ms + 0-100ms// attempt 2 -> ~2000ms + 0-100ms// attempt 3 -> ~4000ms + 0-100msThe multiplier is fixed at 2 and there is no option to change it — there is no initialDelay, maxDelay or per-error-code list. maxJitter is the only knob on the curve. Set maxJitter: 0 in tests to make timing deterministic.
Health Checks
The ORM and a repository each expose their own healthCheck(), with different return shapes. The ORM one probes the connection and cache; the repository one counts rows in its table.
const health = await orm.healthCheck();// {// status: "healthy" | "unhealthy",// database: "sqlite" | "postgres" | "mysql" | "mssql" | "mongodb",// latencyMs: 0.42,// cacheStatus: "disabled" | "in-memory" | "connected" | "connected (miss)" | "unknown"// }
const userHealth = await userRepo.healthCheck();// {// status: "healthy" | "unhealthy",// table: "users",// rows: 128, // -1 if the count failed// latencyMs: 0.31// }app.get("/healthz", async (req, res) => { const health = await orm.healthCheck(); res.status(health.status === "healthy" ? 200 : 503).json(health);});Pool Statistics
orm.poolStats() reports { active, idle, total }. Only SQL Server returns real numbers. Every other dialect returns -1 in all three fields:
const { active, idle, total } = await orm.poolStats();
// SQL Server : { active: borrowed, idle: available, total: size }// PostgreSQL : { active: -1, idle: -1, total: -1 }// MySQL : { active: -1, idle: -1, total: -1 }// SQLite : { active: -1, idle: -1, total: -1 } <- no pool at allThe non-MSSQL branches never fire
This is narrower than it looks, and worth knowing before you build a dashboard on it. poolStats() inspects its own DBClient wrapper rather than the driver object underneath, then checks a driver-pool shape — totalCount, then _allConnections, then size — against it. Only the SQL Server pool presents size, and that last branch is additionally gated on the dialect being DBType.MSSQL. Postgres exposes totalCount on the pool, but not on the wrapper, so the check misses it. Hence the sentinels.
Pool size is not configurable here
Every pool is constructed from connectionString alone. There is no max, min, idleTimeout or connectionLimit field on DBConfig. To size a pool, put it in the connection string — e.g. ?connection_limit=20 for PostgreSQL or ?connectionLimit=20 for MySQL. Treat -1 as "not available", not as a problem: a check that asserts total > 0 fails on everything except SQL Server.