npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@benjosivo/mysql

v1.4.2

Published

Shared MySQL client (pooling, automatic retry on deadlock/lock-timeout, transaction helpers) for use across multiple projects. Nothing connects until you call `init()` — the package has no side effects at import time and no dependency on any particular en

Readme

@benjosivo/mysql

Shared MySQL client (pooling, automatic retry on deadlock/lock-timeout, transaction helpers) for use across multiple projects. Nothing connects until you call init() — the package has no side effects at import time and no dependency on any particular env var naming.

Install

npm install @benjosivo/mysql

Usage

import { init, executeMySQLQuery, closeMySQLConnection } from '@benjosivo/mysql';

await init({
    host: process.env.DB_HOST!,
    user: process.env.DB_USER!,
    password: process.env.DB_PASSWORD!,
    database: process.env.DB_NAME!,
    onError: (error, context) => myLogger.error(context, error), // optional, defaults to console.error
});

const rows = await executeMySQLQuery('SELECT * FROM users WHERE id = ?', [userId]);

// on shutdown
await closeMySQLConnection();

Call init() once per process, at startup. Each consuming project supplies its own credentials and (optionally) its own error reporting — this package has no opinion on either.

Config

| Field | Required | Default | Description | |---|---|---|---| | host, user, password, database | yes | — | MySQL connection details | | waitForConnections | no | true | passed through to mysql2 pool | | connectionLimit | no | 50 | passed through to mysql2 pool | | queueLimit | no | 0 | passed through to mysql2 pool | | multipleStatements | no | true | passed through to mysql2 pool | | staleTransactionMs | no | 300000 (5 min) | a transaction with no statement activity for this long is auto rolled back | | defaultQueryTimeoutMs | no | — (off) | default per-statement timeout; overridable per call with timeoutMs | | onError | no | console.error | called with (error, context) whenever the client can't throw the error directly (e.g. background retry loops) |

Calling init() again (e.g. to reconfigure) closes the previous pool first.

Queries

executeMySQLQuery3<T>({ query, values?, connKey?, returnFieldTypes?, returnListTables?, timeoutMs? })

Preferred for new code. Always resolves to a wrapper — it never throws and never returns bare rows, so every result is safe to inspect before use:

const res = await executeMySQLQuery3<User[]>({ query: 'SELECT * FROM users WHERE id = ?', values: [userId] });

if (!res.ok) {
    // res.error is always a non-empty string
    logger.error(res.error, res.code, res.errno, res.sqlState);
    return;
}

res.rows[0].email; // narrowed by `ok` — TypeScript knows rows is present

T types the rows: User[] for a SELECT, ResultSetHeader for an INSERT/UPDATE. It defaults to any, so untyped calls keep working.

| On success | On failure | |---|---| | ok: true | ok: false | | rows: T | error: string (never empty), code?, errno?, sqlState?, cause? | | fieldsType: [] unless returnFieldTypes | stage: which step failed (execute, transaction-lookup, procedure, …) | | tables: [] unless returnListTables | values: the parameters that were sent | | connKey: string \| null | connKey: string \| null |

Because the union is discriminated on ok, TypeScript will not let you read rows off a result you haven't checked.

Retries

Only transient failures are retried (up to 5 attempts, with a backoff): deadlocks, lock timeouts, lost connections, and a server that is out of connections. Deterministic failures — duplicate key, bad syntax, constraint violations, unknown column, access denied — are returned on the first attempt, since re-sending them can only produce the same error.

Timeouts

timeoutMs (or defaultQueryTimeoutMs in the config) bounds a statement, covering both the wait for a pooled connection and the query itself. On expiry the statement is aborted server-side with KILL QUERY so it stops consuming resources, and the failure comes back as { ok: false, code: 'ETIMEDOUT' }. Timed-out statements are never retried — a killed write must not be silently re-applied.

const res = await executeMySQLQuery3({ query: 'SELECT ...', values, timeoutMs: 10_000 });

Legacy shape

executeMySQLQuery(query, values?, returnFieldTypes?, connKey?, timeoutMs?)
executeMySQLQuery2({ query, values?, connKey?, returnFieldTypes?, returnListTables?, timeoutMs? })

Unchanged: these return the rows directly on success (or { rows, fieldsType, connKey, tables } when a transaction or extra metadata was requested), and { error, values, connKey } on failure instead of throwing. Failures now also carry code, errno and sqlState alongside the message. They share the retry, timeout and transaction handling described above; executeMySQLQuery2 is a thin adapter over executeMySQLQuery3.

Statements prepared statements can't run

executeMySQLQuery uses execute() (MySQL's prepared-statement protocol), and MySQL rejects a number of statements there with "This command is not supported in the prepared statement protocol yet" — SET GLOBAL ..., USE ..., LOCK TABLES, several SHOW variants, and so on. For those, use runMySQLQuery, which goes through query() (text protocol) instead:

import { runMySQLQuery } from '@benjosivo/mysql';

await runMySQLQuery("SET GLOBAL general_log = 'OFF'");

// placeholders still work — mysql2 escapes and interpolates them client-side
await runMySQLQuery('SET GLOBAL general_log = ?', ['OFF']);

// optionally run inside an existing transaction
await runMySQLQuery('SELECT * FROM users WHERE id = ?', [userId], connKey);
runMySQLQuery(query, values?, connKey?, timeoutMs?)

Same result shape as executeMySQLQuery: rows on success, { error, values, connKey } on failure (with code/errno/sqlState when MySQL supplied them). It's deliberately simple — no deadlock/lock-timeout retry — so prefer executeMySQLQuery for regular application queries and keep this one for statements the prepared-statement protocol refuses.

Transactions

const opened = await executeMySQLQuery3({ query: 'INSERT ...', values, connKey: true });
if (!opened.ok) return opened.error;

await executeMySQLQuery3({ query: 'UPDATE ...', values, connKey: opened.connKey }); // same transaction

await connectionCommit(opened.connKey!);
// or: await connectionRollback(opened.connKey!);

A transaction is retired as soon as it can no longer be used — MySQL rolled it back after a deadlock, its connection was lost, or the background sweep found it idle. Its connection is released immediately, and any later use reports why:

// Transaction <key> is no longer usable: MySQL rolled it back after a deadlock

connectionRollback on a transaction that was already retired succeeds (there is nothing left to undo); connectionCommit reports an error, because the data did not make it. Both always free the connection, even if the COMMIT or ROLLBACK itself fails.

staleTransactionMs measures idle time: a transaction is swept only when no statement has run on it for that long, so a long-running but active transaction is left alone.

Shutdown

await closeMySQLConnection();

Rolls back any open transactions, ends the pool, and stops the background sweep. Safe to call even if init() was never called.