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

token-lease

v0.1.0

Published

Reserve variable token budgets before work begins, then commit actual usage or release the lease.

Readme

token-lease

Reserve a variable token budget before work begins, then settle the reservation against actual usage — or release it — once the work finishes.

The problem it solves: when several concurrent tasks draw from a shared budget (an API quota, a per-tenant token allowance, a rate ceiling), you usually do not know the exact cost of a task until it is done. If you only subtract usage after the fact, concurrent tasks can collectively overshoot the limit. token-lease lets you atomically reserve an estimated amount up front so capacity is accounted for while the work runs, and then commit the real number, release the reservation, or let it expire when the bucket is next accessed.

The lifecycle for a single unit of work is:

reserve  ──►  commit   (settle with actual usage)
         ├─►  release  (give the reservation back unused)
         └─►  expire   (reservation is reclaimed on the next bucket operation)

Requirements

  • Node.js 22 or newer
  • ES modules (the package is ESM-only; there is no CommonJS entry point)

Install

Until the first registry release, install the package directly from GitHub:

npm install github:seoulpro/token-lease

After an npm release is announced, npm install token-lease will install the corresponding registry package.

The package has no runtime dependencies. The PostgreSQL store works against any connection pool you already use; pg is not required by this package.

Quick start (in-memory)

This example runs as-is on Node.js 22+. The default store keeps all state in the current process — it is ideal for a single-process service or for tests.

import { TokenLeaseLedger } from "token-lease";

const ledger = new TokenLeaseLedger();

// 1. Create a bucket with a hard capacity.
await ledger.openBucket({ bucket: "tenant-42", limit: 100_000 });

// 2. Reserve an estimated budget before starting the work.
const { lease, bucket } = await ledger.reserve({
  bucket: "tenant-42",
  requestKey: "job-8ac1", // idempotency key for this reservation
  fingerprint: "summarize:doc-8ac1", // caller-defined identity of the request
  tokens: 4_000, // estimated usage
  ttlMs: 30_000, // optional; defaults to 60_000 ms
});

console.log(lease.state); // "reserved"
console.log(bucket.availableTokens); // 96000  (100000 - 0 used - 4000 reserved)

// 3. Do the work, measure the real cost, then settle the lease.
const settled = await ledger.commit({
  bucket: "tenant-42",
  leaseId: lease.id,
  actualTokens: 3_200,
});

console.log(settled.lease.state); // "committed"
console.log(settled.lease.actualTokens); // 3200
console.log(settled.bucket.usedTokens); // 3200
console.log(settled.bucket.reservedTokens); // 0  (the 800 unused tokens were reclaimed)

If the work never runs — for example the request was rejected before any tokens were spent — hand the reservation back instead of committing:

await ledger.release({ bucket: "tenant-42", leaseId: lease.id });

PostgreSQL

For state that must survive restarts or be shared across processes, use PostgresTokenLeaseStore. It serializes operations per bucket with a transaction-scoped advisory lock and stores each bucket as a single JSONB row.

You own the schema. This package does not create or migrate tables. Apply the shipped schema (schema/postgres.sql) with your own migration tooling before using the store:

-- schema/postgres.sql
CREATE TABLE token_lease_buckets (
  bucket_key text PRIMARY KEY,
  revision bigint NOT NULL DEFAULT 1,
  state jsonb NOT NULL,
  updated_at timestamptz NOT NULL DEFAULT now(),
  CONSTRAINT token_lease_revision_range
    CHECK (revision BETWEEN 1 AND 9007199254740991),
  CONSTRAINT token_lease_state_object
    CHECK (jsonb_typeof(state) = 'object'),
  CONSTRAINT token_lease_state_bucket
    CHECK (
      state ? 'bucket'
      AND jsonb_typeof(state -> 'bucket') = 'string'
      AND state ->> 'bucket' = bucket_key
    ),
  CONSTRAINT token_lease_state_revision
    CHECK (
      state ? 'revision'
      AND jsonb_typeof(state -> 'revision') = 'number'
      AND state ->> 'revision' ~ '^(0|[1-9][0-9]*)$'
      AND (state ->> 'revision')::numeric = revision
    )
);

Migration tooling can locate the packaged asset without assuming a node_modules layout:

import { postgresSchemaUrl } from "token-lease/postgres-schema";

Then wire the store into the ledger:

import { Pool } from "pg";
import { TokenLeaseLedger, PostgresTokenLeaseStore } from "token-lease";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
pool.on("error", (error) => {
  console.error("Unexpected PostgreSQL pool error", error);
});

const ledger = new TokenLeaseLedger({
  store: new PostgresTokenLeaseStore(pool),
});

// The API is identical to the in-memory example.
await ledger.openBucket({ bucket: "tenant-42", limit: 100_000 });

The store only needs an object that exposes connect(), returning a client with query(sql, values) and release(). A pg.Pool satisfies this, but so does any compatible wrapper. The wrapper must keep BEGIN through COMMIT or ROLLBACK on one writable PostgreSQL session and must surface checked-out connection failures through query rejections or EventEmitter-compatible error events. It must also return the selected state and revision text without lossy numeric coercion. release(error?) should discard a client when an error is supplied. Applications using pg.Pool must also handle the pool's error event for idle clients, as in the example above. All numeric values in serialized PostgreSQL state, including application-added fields, must be non-negative safe integers.

Idempotency and the fingerprint

reserve takes two caller-supplied identifiers:

  • requestKey — the idempotency key. Retrying reserve with the same bucket and requestKey returns the existing lease instead of creating a second one, so a retried or duplicated call reserves capacity only once.
  • fingerprint — a caller-defined string describing what the request is. Together with the reservation's tokens and ttlMs, it forms the identity that a replay must match.

If the same requestKey is replayed with matching tokens, ttlMs, and fingerprint, the original lease is returned unchanged. If any of those differ, the call fails with code idempotency_conflict — this guards against a reused key accidentally standing in for a different request. The fingerprint is hashed (SHA-256) before storage; its plaintext is never persisted.

Expiration, over-reservation, and over-usage

Expiration. Every lease carries an expiresAt timestamp set to its creation time plus ttlMs. A reserved lease expires once the clock reaches expiresAt, at which point its reserved tokens are returned to the bucket and its state becomes expired. Expiration is evaluated lazily: it happens whenever the bucket is next touched by any ledger operation, and you can force it explicitly:

const { expiredLeaseIds, bucket } = await ledger.reapExpired({ bucket: "tenant-42" });

There is no background timer or sweeper. A reservation for a crashed or abandoned task is reclaimed the next time the bucket is used (or reaped), not at a precise wall-clock instant. Committing a lease that has already expired fails with lease_expired.

Over-reservation. Reservations are estimates, so committed usage is usually lower than what was reserved. commit records only actualTokens as used and returns the entire reservation, so any unused portion becomes available again immediately (the quick start reserves 4000 and commits 3200, freeing 800).

Actual usage above the reservation. Committing more than you reserved is allowed, as long as the bucket still has room. On commit the reservation is released and actualTokens is charged against the limit; if that would push the bucket's committed plus outstanding-reserved total above its limit, the commit fails with commit_exceeds_budget and the lease stays reserved (you can retry with a smaller number, or release it). Plan estimates with enough headroom that real usage rarely exceeds available capacity.

API

All ledger methods are async. Bucket, lease, and request identifiers are validated; invalid arguments reject with TokenLeaseError.

new TokenLeaseLedger(options?)

| Option | Default | Meaning | | --- | --- | --- | | store | new InMemoryTokenLeaseStore() | Backing store implementing mutate() and read(). | | clock | Date.now | Returns the current time as a non-negative safe integer of milliseconds. | | idFactory | crypto.randomUUID | Produces lease ids. | | defaultTtlMs | 60_000 | TTL used when reserve omits ttlMs. | | maxTtlMs | 3_600_000 | Upper bound on any lease TTL. | | maxLeasesPerBucket | 10_000 | Maximum number of retained lease records per bucket. |

Methods

  • openBucket({ bucket, limit })BucketSnapshot. Creates the bucket, or, if it already exists with the same limit, returns it unchanged. A mismatching limit fails with bucket_limit_conflict.
  • setLimit({ bucket, expectedLimit, limit })BucketSnapshot. Compare-and-set the limit; fails with limit_changed if expectedLimit no longer matches, or limit_below_allocated if the new limit is below current used-plus-reserved tokens.
  • reserve({ bucket, requestKey, fingerprint, tokens, ttlMs? })LeaseResult. Atomically reserves tokens. Fails with budget_exceeded when capacity is insufficient, bucket_not_found if the bucket does not exist, or bucket_record_limit once maxLeasesPerBucket is reached.
  • commit({ bucket, leaseId, actualTokens })LeaseResult. Settles a reserved lease. Committing the same lease again with the same actualTokens is idempotent; a different value fails with commit_conflict.
  • release({ bucket, leaseId })LeaseResult. Returns a reservation unused. Releasing an already released or expired lease is idempotent; a committed lease cannot be released (lease_committed).
  • reapExpired({ bucket })ReapResult. Expires every overdue reservation and reports their ids.
  • inspect({ bucket })BucketSnapshot. Returns a current snapshot after expiring any overdue reservations.

Also exported: InMemoryTokenLeaseStore, PostgresTokenLeaseStore, TokenLeaseError, STATE_SCHEMA_VERSION, and assertStoredBucket (validates a stored bucket object against the current schema). The token-lease/postgres-schema entry point exports postgresSchemaUrl:

import { readFile } from "node:fs/promises";
import { postgresSchemaUrl } from "token-lease/postgres-schema";

const schemaSql = await readFile(new URL(postgresSchemaUrl), "utf8");

Custom store contract

A custom TokenLeaseStore is part of the ledger's correctness boundary. mutate(bucket, callback) must serialize all mutations for that bucket across every process that shares the store, invoke the callback once with the latest isolated state, and make a changed: true state durable before resolving. A changed state starts at revision 1 and advances the current revision by exactly one; the built-in stores reject any other transition. Callback, validation, serialization, or write failures must leave the previous state observable when the store knows that commit has not occurred, and read() must never expose a partial mutation. An indeterminate transport failure may require an idempotent retry, as described below. The built-in stores isolate persisted state from caller mutation; mutation results are awaited before persistence and otherwise returned without imposing a serialization format.

Result shapes

reserve, commit, and release resolve to a LeaseResult:

{
  lease: {
    id,                 // string
    state,              // "reserved" | "committed" | "released" | "expired"
    reservedTokens,     // number
    actualTokens,       // number | null (set once committed)
    createdAt,          // number (ms)
    expiresAt,          // number (ms)
    settledAt,          // number | null (set once committed/released/expired)
  },
  bucket: {
    bucket,             // string
    limit,              // number
    usedTokens,         // number
    reservedTokens,     // number
    availableTokens,    // number = limit - usedTokens - reservedTokens
    revision,           // number (increments on every change)
    createdAt,          // number (ms)
    updatedAt,          // number (ms)
    leases,             // array of lease snapshots, oldest first
  },
}

openBucket, setLimit, and inspect resolve to the bucket shape above. reapExpired resolves to { expiredLeaseIds, bucket }.

Error handling

Expected domain failures are surfaced as a thrown TokenLeaseError with a stable code, a human-readable message, and a frozen details object. Branch on code, not on message text:

import { TokenLeaseError } from "token-lease";

try {
  await ledger.reserve({
    bucket: "tenant-42",
    requestKey: "job-9001",
    fingerprint: "summarize:doc-9001",
    tokens: 50_000,
  });
} catch (error) {
  if (error instanceof TokenLeaseError && error.code === "budget_exceeded") {
    // error.details includes { requestedTokens, availableTokens }
    // back off, queue, or retry with a smaller estimate
  } else {
    throw error;
  }
}

Codes you may want to handle include bucket_not_found, budget_exceeded, idempotency_conflict, commit_conflict, commit_exceeds_budget, lease_not_found, lease_expired, lease_released, lease_committed, and invalid_input. Configuration, storage, or integrity failures can also use invalid_clock, state_corrupt, and store_contract_violation.

Operational notes and non-goals

  • Lease records are retained. Committed, released, and expired leases stay in the bucket state; the library never deletes them. This preserves idempotent replay but means a bucket's record set grows over time, bounded by maxLeasesPerBucket (default 10,000), after which reserve fails with bucket_record_limit. Provision buckets, TTLs, and this bound for your request volume, or rotate bucket keys.
  • Clock behavior. Time comes from the injected clock (default Date.now), measured in integer milliseconds. Per bucket, time never moves backwards: each operation uses the later of the clock reading and the bucket's own last-updated time. TTLs are compared against this clock; there is no monotonic timer. Keep clocks synchronized across servers: a clock that jumps ahead ratchets the bucket forward and can expire reservations early.
  • Shared configuration must match. Ledgers sharing PostgreSQL buckets should use the same defaultTtlMs, maxTtlMs, and maxLeasesPerBucket. In particular, an omitted ttlMs is resolved before it enters the idempotency fingerprint, so different defaults can turn a retry into idempotency_conflict.
  • A lost commit acknowledgement is ambiguous. A connection can fail after PostgreSQL commits but before the caller receives the result. Retrying reserve, commit, or release with the same inputs is safe. After an uncertain setLimit, call inspect() and retry with the observed limit.
  • You own the PostgreSQL schema. The library reads and writes the token_lease_buckets table but does not create, migrate, or clean it up. Apply schema/postgres.sql and manage its lifecycle yourself.
  • The in-memory store is process-local. State is not shared across processes or restarts. Use PostgresTokenLeaseStore when you need durability or cross-process coordination.
  • No metering or billing. This library accounts for token budgets you supply. It does not measure token usage, meter consumption, price it, or produce invoices — you provide the estimates and the actual numbers.
  • Estimates are yours. The ledger enforces the bucket limit against the numbers you pass; the quality of your reservations determines how tightly the limit is respected under concurrency.

Security and privacy

  • The fingerprint and requestKey values you pass are hashed with SHA-256 before storage; their plaintext is not persisted. Hashing is data minimization, not encryption, so low-entropy values may still be guessable. Avoid putting raw secrets in them, and treat the stored bucket state (including in PostgreSQL) according to your own data-handling requirements.
  • Input identifiers are validated for type, length, and control characters, and numeric fields must be non-negative safe integers, but the library performs no authentication or authorization — enforce access control in your application.
  • This project has not undergone any external security review and makes no production-readiness or fitness guarantees. Review the code and test it against your own requirements before relying on it.

License

MIT