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

@paykernel/reconciliation

v0.1.1

Published

Portable reconciliation primitives for @paykernel/core: safe provider lookup, machine-readable drift, decision-only policy, store-backed scheduling, batch reconcile (no queue required).

Readme

@paykernel/reconciliation

Portable reconciliation primitives for @paykernel/core: safe ordered provider lookup, machine-readable drift, decision-only policy helpers, store-backed durable scheduling (no mandatory queue), and bounded-concurrency batch reconcile.

Portable. No Node-only imports. No Redis/queue product required. Runtime: Bun / Node ≥ 18 / Deno / Workers (Web APIs). Depends only on @paykernel/core.

Install

bun add @paykernel/reconciliation
# peer / workspace: @paykernel/core

Quickstart

1. Safe check of an indeterminate payment

Inject a ProviderLookupPort (and optionally a durable or test store for scheduling).

import {
  createPaymentReconciler,
  decideReconciliationPolicy,
  type ProviderLookupPort,
  type ReconciliationTarget,
} from "@paykernel/reconciliation";

declare const lookup: ProviderLookupPort;

const reconciler = createPaymentReconciler({ lookup });

const target: ReconciliationTarget = {
  gateway: "stripe",
  gatewayPaymentId: "pi_123",
  expected: { status: "pending" },
};

const result = await reconciler.reconcile(target);
const decision = decideReconciliationPolicy(result, target);

// Apply local updates in YOUR app — this package never mutates payments.
if (decision.action === "update_local_to_paid" && decision.safe) {
  // await orderService.markPaid(decision.provider);
}
// NEVER create a replacement charge while original is indeterminate.
// Multi-match is never silent pick-first (outcome: ambiguous_match).

2. Durable schedule (store-backed, no queue)

import {
  createPaymentReconciler,
  createReconciliationScheduler,
  decideReconciliationPolicy,
  type ProviderLookupPort,
  type ReconciliationStore,
  type ReconciliationTarget,
} from "@paykernel/reconciliation";

declare const store: ReconciliationStore; // testkit memory in tests; adapter in production
declare const lookup: ProviderLookupPort;
declare function loadTarget(job: { record: { subjectId: string } }): Promise<ReconciliationTarget>;

const reconciler = createPaymentReconciler({ lookup });
const scheduler = createReconciliationScheduler({ store, maxAttempts: 8 });

await scheduler.schedule({
  target: {
    gateway: "stripe",
    gatewayPaymentId: "pi_123",
    expected: { status: "pending" },
  },
  runAt: new Date().toISOString(),
  reason: "indeterminate_create",
});

// Production poll loop (the only one): processDue claims immediately before
// each handler and auto-renews on leaseMs/3.
await scheduler.processDue({
  limit: 10,
  handler: async (job) => {
    const target = await loadTarget(job); // store row is subjectId + reason, not a full target
    const result = await reconciler.reconcile(target);
    const decision = decideReconciliationPolicy(result, target);
    // Never complete on raw result.outcome === "consistent" — pending/processing
    // still settling maps to retry_later, not recovery-complete.

    if (decision.action === "mark_consistent" && decision.safe) {
      return { disposition: "complete" };
    }
    if (
      (decision.action === "update_local_to_paid" ||
        decision.action === "update_local_to_failed") &&
      decision.safe
    ) {
      // apply the safe local paid/failed update in YOUR app first, then complete:
      return { disposition: "complete" };
    }
    if (decision.action === "retry_later") {
      return { disposition: "retry_later", error: new Error("retry_later") };
    }
    if (decision.action === "do_not_create_replacement") {
      // Never createPayment for the same intent; reschedule lookup if needed.
      return { disposition: "retry", error: new Error(decision.reason) };
    }
    if (
      decision.action === "manual_review" ||
      decision.action === "apply_drift_review"
    ) {
      return { disposition: "manual_review", note: decision.action };
    }
    return { disposition: "retry" };
  },
});

Production worker: processDue is the only production poll loop. claimDue is discovery / test inspection: it claims sequentially but still returns N live leases. Do not claimDue({ limit: N }) then serial-work that array — later default-30s leases expire while the first handler runs, and a peer can steal them (lease_lost after a successful lookup). Use processDue, which claims immediately before each handler and auto-renews on leaseMs/3.

Inject any ReconciliationStore (testkit createMemoryReconciliationStore in tests; postgres/redis/sqlite/turso/d1/do adapters in production). Durable adapters must pass runReconciliationStoreConformanceSuite from @paykernel/testkit.

(@paykernel/reconciliation does not depend on testkit — import memory stores only from test code.)

Dual memory-store honesty: this package keeps a non-exported in-package memory store for domain unit tests. Testkit ships a separate createMemoryReconciliationStore. Both are test-only / NON-PRODUCTION and can drift vs durable SQL fencing; production apps inject @paykernel/store-* adapters that pass runReconciliationStoreConformanceSuite.

3. Batch with concurrency limit

for await (const { index, target, result } of reconciler.reconcileMany(targets, {
  concurrency: 5,
})) {
  // Completion order; use index/target to correlate (RECON-1).
  // Persist or alert in application code — package does not auto-mutate.
  void index;
  void target;
  void result;
}

Design rules (authoritative)

  1. Decision-only policy — helpers return enums; never auto-mutate local payments.
  2. No replacement charges while original is indeterminate or matches are ambiguous.
  3. Never invent failure from timeouts / unavailable provider responses.
  4. Multi-match → ambiguous_match — never silent pick-first.
  5. Atomic claim only via store.claim — no get-then-set races in the scheduler.
  6. No secrets in errors, logs, or stored lastError (use sanitizeReconciliationError).
  7. No mandatory queueReconciliationStore is the scheduling abstraction.

Lookup order

When keys and methods are available:

  1. provider payment ID (findByPaymentId)
  2. idempotency key (findByIdempotencyKey)
  3. merchant local reference (findByLocalReference)
  4. provider request ID (findByProviderRequestId)

Unsupported methods are skipped (capability-aware). Missing all methods for available keys → manual_review_required.

Documentation

| Doc | Contents | | --- | -------- | | docs/overview.md | Purpose, package boundary, mental model | | docs/reconciliation.md | Target, snapshots, results, compare, policy | | docs/safe-lookup.md | Ordered lookup, ProviderLookupPort, multi-match | | docs/scheduling.md | Scheduler, backoff/jitter, manual review, no queue | | docs/batch.md | reconcileMany, concurrency, app persist/alert | | docs/crash-boundaries.md | Schedule / claim / lookup / complete under crash | | testkit store-contracts | Lease-aware store semantics + conformance | | adapter selection | Choosing a durable store adapter |

Package boundary

  • Depends only on @paykernel/core (core).
  • Does not import testkit, webhooks, adapters, Redis, or DB drivers.
  • Dual-owns ReconciliationStore structurally compatible with Phase 9 testkit.
  • Core must never depend on this package.

License

MIT