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

@velajs/reliability

v1.28.0

Published

Portable durable idempotency, outbox, inbox and one-off scheduling

Readme

@velajs/reliability

Durable idempotency, outbox delivery, inbox deduplication and one-off scheduling. Each feature works independently. The root package, /http and /testing use Web APIs and have no runtime dependencies. /drizzle is an optional integration for PostgreSQL, asynchronous SQLite/libsql and native D1 statements.

Execution is at least once. A process can fail after an external effect and before recording completion. Lease expiry permits another worker to run that work again. Database fences reject stale completion; they cannot make an external HTTP request, email or payment exactly once. Pass a stable downstream idempotency key when the destination supports one.

Storage and namespaces

import { createOutbox } from '@velajs/reliability';
import {
  createDrizzleReliabilityStore,
  reliabilityPgTable,
} from '@velajs/reliability/drizzle';

// db is an application-owned native Drizzle handle.
export const deliveryTable = reliabilityPgTable('delivery');
const store = createDrizzleReliabilityStore({
  db, table: deliveryTable, dialect: 'pg',
});
const outbox = createOutbox({
  store,
  parsePayload: value => eventSchema.parse(value),
});
const scope = { tenantId: admittedTenantId, namespace: 'entry-events' };

Install @velajs/crud, @velajs/crud-drizzle and drizzle-orm when importing /drizzle. None is loaded by the portable entrypoints. Root types remain generic over the selected store's transaction token.

For SQLite/libsql use reliabilitySqliteTable('delivery') and { db, table, dialect: 'sqlite' }; the native driver must support asynchronous callback transactions. D1 uses that SQLite table and { db, table, driver: 'd1' }. Synchronous SQLite and MySQL are unsupported.

Export the table from the application's Drizzle schema and generate/apply its migration with the application's existing migration tooling. Constructors do not create or migrate tables. The schema has a composite primary key over tenantId, namespace, kind, and id, plus indexes for due work and retention. reliabilityPgTable(name, schema) supports an explicit PostgreSQL schema.

Every call requires a nonempty trusted tenant ID and namespace. Derive them from admitted server context, not an unverified request field. They are included in every lookup, unique identity and mutation predicate. Inbox identities also include the configured consumer. A database initializer can set PostgreSQL RLS: onOpenTransaction(native, { tenantId }). Standalone operations open a real transaction when this initializer is configured. Keep the same initializer function when composing native atomic commands.

Payload parsers receive unknown and may be asynchronous. They validate incoming payloads and persisted JSON before delivery. Return bounded plain JSON that the same parser can validate after persistence. Classes, cycles, undefined values, nonfinite numbers, accessors and sparse arrays are rejected. The default JSON limit is 64 KiB, configurable through maxPayloadBytes up to 1 MiB; nesting and node counts are bounded too. Identifiers, deadlines, lease periods, errors and batch sizes are validated. Store JSON is never cast into a payload type.

Outbox admission and delivery

await outbox.enqueue(scope, {
  id: eventId, // Stable deduplication identity in this tenant namespace.
  payload: { type: 'entry.created', entryId },
  maxAttempts: 10,
});

const claims = await outbox.claimDue(scope, { limit: 20, leaseMs: 30_000 });
for (const claim of claims) {
  try {
    await destination.send(claim.payload, { idempotencyKey: claim.id });
  } catch {
    await outbox.retry(claim, { delayMs: 5_000, error: 'destination-unavailable' });
    continue;
  }
  // If acknowledgement fails, do not automatically invoke the destination again.
  await outbox.acknowledge(claim);
}

An identical admission returns the existing record. Reusing an ID with different content raises CONFLICT. availableAt delays delivery; if supplied, it is part of admission identity. Include topic/version information in your validated payload envelope when needed.

renew(claim, leaseMs) extends an active lease. retry(claim, { delayMs, error }) releases it until its next deadline; exhausting maxAttempts marks it failed. fail(claim, errorCode) records a terminal failure. A poll also marks an expired final attempt failed. Error codes are bounded strings; arbitrary error objects and stack traces are not persisted. get(scope, id) returns validated storage metadata. No background timer, handler registry or automatic handler retry runs inside the package: call claimDue from your scheduler or queue infrastructure.

Claims contain a generation, unique lease token, increasing fence, attempt and expiry. All transitions check the current persisted identity, lease and fence. Database time governs availability, lease validity and retention; worker clocks cannot extend ownership. Processes can reopen stores and recover expired work from durable rows. Generation changes after retention pruning prevent old workers from completing a later use of the same ID.

Share a business transaction

Use the same exact native Drizzle handle as the business adapter. Passing a transaction from another handle or tenant fails. Every operation must be awaited.

import { crudTransaction } from '@velajs/crud';

await crudTransaction(entryAdapter, { tenantId: scope.tenantId }, async transaction => {
  await entryResource.execute('create', {
    transaction,
    vars: { tenantId: scope.tenantId },
    body: { id: entryId, title },
  });
  await outbox.enqueue(scope, {
    id: eventId,
    payload: { type: 'entry.created', entryId },
  }, { transaction });
});

Admission and business writes commit together. In-transaction payload validation, SQL or fencing errors poison the transaction even when caught by the caller. Native operations accepted but left pending are drained and cause rollback. Retained bound stores and sessions expire. Deliver external effects after commit; they cannot be rolled back by a database transaction.

Named database registrations use their own owner identity. Bind the native store to that registration before joining its transaction:

import { withCrudTransactionStore } from '@velajs/crud';

const binding = registry.bindTransactionStore('main', store.transactionBinding);
// transaction belongs to a resource adapter resolved from this registration.
await withCrudTransactionStore(transaction, binding, { tenantId: scope.tenantId },
  async boundStore => {
    const boundOutbox = createOutbox({ store: boundStore, parsePayload });
    await boundOutbox.enqueue(scope, { id: eventId, payload });
  });

The native binding must match the registration's database. Do not use the unwrapped native store with a registration-owned transaction. The bound feature already uses the outer transaction and does not receive a second transaction argument. The same pattern works for inbox and idempotency completion.

Inbox completion

import { createInbox, fingerprint } from '@velajs/reliability';

const inbox = createInbox({ store, consumer: 'entry-projector', parsePayload });
const admitted = await inbox.claim(scope, {
  messageId,
  fingerprint: await fingerprint(payload),
  payload,
});
if (admitted.kind === 'claimed') {
  await crudTransaction(projectionAdapter, { tenantId: scope.tenantId }, async transaction => {
    await projectionResource.execute('create', {
      transaction,
      vars: { tenantId: scope.tenantId },
      body: project(admitted.claim.payload),
    });
    await inbox.complete(admitted.claim, { transaction });
  });
}

completed skips processing, busy reports the current retry deadline and conflict indicates the same message identity has different content. A stale completion rolls consumer database writes back, including when the fence error is caught. Standalone completion is available for deduplication, but it cannot make separately committed consumer writes atomic. Use the shared transaction for that guarantee. Inbox consumer names are static configuration.

Idempotency and bounded HTTP replay

import { createIdempotency } from '@velajs/reliability';
import {
  captureHttpResult, fingerprintRequest, parseHttpResult, replayHttpResult,
} from '@velajs/reliability/http';

const idempotency = createIdempotency({ store, parseResult: parseHttpResult });
const outcome = await idempotency.claim(scope, {
  key: idempotencyKey,
  fingerprint: await fingerprintRequest(request, {
    operation: 'create-entry', principal: admittedPrincipalId,
  }),
  leaseMs: 30_000,
  retentionMs: 86_400_000,
});
if (outcome.kind === 'completed') return replayHttpResult(outcome.value);
// Handle busy, conflict, failed, cancelled and expired without rerunning work.
if (outcome.kind !== 'claimed') return mapAdmissionOutcome(outcome);

const response = await createEntry();
let snapshot;
try {
  snapshot = await captureHttpResult(response);
} catch (error) {
  // The business action may already have committed. Do not retry it here.
  await idempotency.unavailable(outcome.claim);
  throw error;
}
await idempotency.complete(outcome.claim, snapshot);
return replayHttpResult(snapshot);

The snippet illustrates replay with a separately committed handler. A crash between createEntry and completion still permits a later execution. For database-only work, construct/validate the bounded snapshot and complete the idempotency claim inside the same business transaction. This is the stronger atomic path; it does not cover external effects. The package never automatically retries a possibly committed handler.

fingerprintRequest hashes bounded request bytes, method, URL, selected headers, and explicit operation/principal scope with Web Crypto. It reads a clone and preserves the request. Select any additional headers affecting behavior. The general fingerprint(value) helper hashes canonical bounded JSON.

captureHttpResult consumes the response body with a streaming byte limit, without unbounded arrayBuffer(). Defaults are 32 KiB body, 8 KiB allowed headers, and 32 headers. The hard body limit is 256 KiB. Increase maxPayloadBytes on the idempotency feature when increasing HTTP limits; stored base64 also uses space. The default replay headers are content-type, cache-control, etag and location. Cookies, hop-by-hop headers and content-length are excluded. Capture before content encoding; encoded bodies fail explicitly rather than replaying under incorrect headers. Bodyless statuses and persisted headers/base64 are checked.

Oversized or unsupported responses raise RESULT_UNAVAILABLE. No truncation or automatic rerun occurs. unavailable(claim) persists a terminal failure where the lease is still valid. An expired lease cannot be overwritten to conceal a new owner's work. A still-active attempt can be explicitly retried before completion; choose this only when the application knows retry is safe. Terminal failures cannot be retried with their old lease.

One-off jobs

import { createScheduler } from '@velajs/reliability';
const jobs = createScheduler({ store, parsePayload: jobSchema.parse });
const job = await jobs.schedule(scope, {
  id: jobId, payload, dueAt: Date.now() + 60_000,
});
const edited = await jobs.reschedule(scope, {
  id: job.id, expectedGeneration: job.generation,
  expectedRevision: job.revision, dueAt: Date.now() + 120_000,
});
await jobs.cancel(scope, {
  id: edited.id, expectedGeneration: edited.generation,
  expectedRevision: edited.revision,
});

Generation plus revision prevent lost updates and stale edits after an ID is pruned and reused. Cancel/reschedule invalidate any current lease. They cannot undo an already running external effect. Only pending or leased jobs can be edited; completed, failed and cancelled jobs are terminal. Rescheduling retains the attempt budget. Poll with claimDue and use the same renew/complete/retry/fail methods as outbox delivery. Recurring schedules are application policy.

D1 boundary

D1 supports native standalone compare-and-set operations. It cannot join a callback transaction. Only unconditional, precomputed outbox INSERT admission is exposed for composition with existing atomic business inserts:

import { prepareOutboxInsert } from '@velajs/reliability/drizzle';
import { executeAtomicBatch, requireAtomicBatch } from '@velajs/crud';

const admission = await prepareOutboxInsert({
  db, table: deliveryTable, scope,
  input: { id: eventId, payload }, parsePayload,
  admission: 'unconditional',
});
await executeAtomicBatch(entryAdapter, [
  requireAtomicBatch(entryAdapter).create({ id: entryId, title }),
  admission,
], { tenantId: scope.tenantId });

All commands must use the exact same native handle and initializer. Values are copied and commands authenticated before the native batch. Duplicate admission fails the whole batch; unlike enqueue, an INSERT command does not turn a duplicate into a successful no-op. Its immediate due time is precomputed as zero; claim deadlines still use database time. Creation metadata uses preparation time.

Do not append this unconditional command to a conditional business mutation whose scoped miss is allowed. Conditional admission is rejected by the preparer. Fenced inbox/idempotency completion alongside business writes is unsupported on D1 and fails before writes when requested through a transaction option. A zero-row completion UPDATE is not a native rollback guard. Use a real PostgreSQL/libsql transaction for those workflows. Existing atomic audit and post-commit result-error semantics remain unchanged.

Failure, retention and observability

Standalone claims and transitions use native conditional statements. A poll validates all selected payloads before claiming any. A later SQL/network failure can still leave a partial set of leases on an autocommit backend. ReliabilityClaimError has recoveryRequired: true and the known acquired claims; committed is true when a commit is known and otherwise undefined, never a claim that the database rolled back. Unknown outcomes recover by lease expiry. Handlers have not run inside this API. Explicit shared transactions roll the entire poll back on error. ReliabilityResultError has committed: true when a standalone mutation's returned row fails validation after commit. Native atomic insert batches retain AtomicBatchResultError from the CRUD adapter.

Each feature exposes prune(scope, limit) for bounded removal of expired terminal records; there is no automatic global deletion. Retention defaults to seven days, starts on completion/failure/cancellation and is limited to one year. Completed idempotency/inbox results beyond retention return expired, without starting new work. Pruning permits later reuse of an ID with a new generation. Configure the retention window to exceed the expected duplicate-delivery window.

An optional static observer accepts the structural interface onStart() -> { onEnd('success' | 'error' | 'cancelled') }. It works with createExecutionTelemetryObserver({ telemetry, operation: 'outbox.delivery' }) without importing Vela. Configure operation names statically and keep tenant, payload and idempotency identities out of metric labels. Synchronous exceptions and accidental rejected promises from observers cannot change execution results. These callbacks measure feature operations; application handlers have their own execution boundary.

createMemoryReliabilityStore({ now? }) from /testing is a deterministic, serialized reference adapter, not persistent production storage. Package tests cover it, libsql and PGlite; root integration fixtures cover real PostgreSQL connections, native D1, and the portable entrypoints in a native Worker.