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

@setu-ts/cloudflare-plugin

v0.11.0

Published

Cloudflare bindings: KV, R2, D1, Queues, Cron, Cache API, and Durable Objects

Readme

@setu-ts/cloudflare-plugin

Cloudflare Workers platform bindings for Setu-TS.

The framework has served traffic on Workers since the Hono migration, but had no way to reach the platform's own primitives. This plugin publishes a Worker's bindings under CAPABILITIES.CLOUDFLARE and, optionally, serves the committed cache, storage, queue, and messaging capabilities from KV, R2, and Cloudflare Queues. It also ships the queue and scheduled handler exports a Worker needs, and a response cache over the platform's own edge cache.

Zero npm dependencies. Nothing in this package imports cloudflare:workers — the application passes env in, which keeps the package type-checkable on Deno, Node, and Bun, and trivially testable with a plain object.

Installation

npm install @setu-ts/cloudflare-plugin
# Deno: deno add npm:@setu-ts/cloudflare-plugin

Usage

import { env, waitUntil } from 'cloudflare:workers';
import { createApplication } from '@setu-ts/kernel';
import { RuntimePlugin } from '@setu-ts/runtime';
import { CloudflarePlugin } from '@setu-ts/cloudflare-plugin';
import { CAPABILITIES, type ICacheStore } from '@setu-ts/common';
import type { ICloudflareBindings } from '@setu-ts/cloudflare-plugin';

const app = createApplication({
  plugins: [
    // Populates runtime.env with the Worker's string variables and secrets.
    RuntimePlugin({ env }),
    CloudflarePlugin({
      env,
      waitUntil,
      cache: { binding: 'CACHE_KV', prefix: 'cache:' },
      storage: { binding: 'UPLOADS' },
      requireBindings: ['CACHE_KV', 'UPLOADS'],
    }),
  ],
});

app.router.get('/settings', async (ctx) => {
  const cache = ctx.services.get<ICacheStore>(CAPABILITIES.CACHE);
  const cf = ctx.services.get<ICloudflareBindings>(CAPABILITIES.CLOUDFLARE);

  const cached = await cache.get<Settings>('settings');
  if (cached !== null) return ctx.response.json(cached);

  const settings = await loadSettings(cf.d1('DB'));
  await cache.set('settings', settings, 30);
  cf.waitUntil(recordCacheMiss()); // finishes after the response
  return ctx.response.json(settings);
});

export default { fetch: app.fetch };

setu new --runtime cloudflare-workers scaffolds the env wiring and a wrangler.toml with commented binding stanzas.

Requirements

compatibility_date must be 2025-08-08 or later — that is when Cloudflare shipped import { waitUntil } from 'cloudflare:workers'. env has been importable at module scope for longer; only waitUntil needs the newer date.

Options

| Option | Type | Default | Description | | -------------------------------- | ------------------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | env | Record<string,unknown> | — | Required. The Worker's env. | | waitUntil | WaitUntilHost | — | The platform sink. Omit off Workers. Without it on Workers, work passed to bindings.waitUntil() may be cut off when the response is sent, and one warning says so at startup — pass import { waitUntil } from 'cloudflare:workers' from your entry through the factory. | | requireBindings | string[] | [] | Bindings that must exist; register() throws naming absent ones. | | cache.binding | string | — | KV namespace serving CAPABILITIES.CACHE. | | cache.name | string | 'default' | Derives cache.<name> when not 'default'. | | cache.prefix | string | — | Key prefix. Required to call clear(). | | cache.defaultTtlSeconds | number | — | TTL applied when set omits one. | | storage.binding | string | — | R2 bucket serving CAPABILITIES.STORAGE. | | storage.name | string | 'default' | Derives storage.<name> when not 'default'. | | storage.prefix | string | — | Object-key prefix. | | queue.binding | string | — | Queues producer binding serving CAPABILITIES.QUEUE. | | queue.name | string | 'default' | Derives queue.<name> when not 'default'. | | queue.maxDelaySeconds | number | 86400 | A larger delayMs throws rather than being truncated. | | messaging.binding | string | — | Queues producer binding serving CAPABILITIES.MESSAGING. | | messaging.name | string | 'default' | Derives messaging.<name> when not 'default'. | | messaging.rpc.binding | string | — | DO namespace serving RPC reply inboxes. Absent, RPC throws. | | messaging.rpc.defaultTimeoutMs | number | 5000 | Reply budget when RequestOptions.timeoutMs is omitted. | | durableObject.binding | string | — | DO namespace serving CAPABILITIES.REALTIME_BACKPLANE. | | durableObject.name | string | 'default' | Derives realtime-backplane.<name> when not 'default'. | | durableObject.topic | string | 'realtime' | The object every replica shares. Differ it per application. |

Queues, Cron Triggers, and the edge cache

Cloudflare invokes queue and scheduled as module-level exports — fetch is not involved — so your Worker assembles them beside it:

import {
  cacheApiMiddleware,
  CloudflarePlugin,
  createQueueHandler,
  createScheduledHandler,
  WorkersCron,
} from '@setu-ts/cloudflare-plugin';
import { CAPABILITIES, type IQueue } from '@setu-ts/common';

const app = createApplication({
  plugins: [
    RuntimePlugin({ env }),
    CloudflarePlugin({ env, waitUntil, queue: { binding: 'JOBS' } }),
  ],
});

// A route cached in the datacenter it was served from.
app.router.get('/catalog', {
  handler: listCatalog,
  middleware: [cacheApiMiddleware({ ttlSeconds: 300 })],
});

await app.start();

const queue = app.services.get<IQueue>(CAPABILITIES.QUEUE);
queue.process<{ to: string }>(
  'send-welcome',
  async (job) => {
    await mailer.send({ to: job.data.to, subject: 'Welcome', text: 'Welcome!' });
  },
  { concurrency: 5 },
);

// The expression must match wrangler.toml `[triggers] crons` exactly.
// An unmatched trigger FAILS the invocation. A logger adds a structured
// warning naming the registered expressions; it never changes the outcome.
const cron = new WorkersCron({ logger });
cron.on('0 3 * * *', async () => {
  await queue.add('rebuild-reports', {});
});

export default {
  fetch: app.fetch,
  queue: createQueueHandler(app),
  scheduled: createScheduledHandler(cron),
};

wrangler.toml needs the matching stanzas:

[[queues.producers]]
queue = "jobs"
binding = "JOBS"

[[queues.consumers]]
queue = "jobs"

[triggers]
crons = ["0 3 * * *"]

Messaging

CAPABILITIES.MESSAGING over Cloudflare Queues, with request/reply through a Durable Object.

app.register(
  CloudflarePlugin({
    env,
    messaging: { binding: 'MESSAGES', rpc: { binding: 'REPLY_INBOX' } },
  }),
);

const broker = app.services.get<IMessageBroker>(CAPABILITIES.MESSAGING);
await broker.subscribe<{ id: string }>('user.created', async (user) => {
  /* … */
});
await broker.respond<number, number>('double', (n) => n * 2);

// Consuming is a MODULE export. Combine with a job queue by routing on the
// queue name, which your `wrangler.toml` owns and this package cannot see.
export default { fetch: app.fetch, queue: createMessagingHandler(app) };
[[queues.producers]]
binding = "MESSAGES"
queue = "messages"

# REQUIRED for request/reply: the platform default of 5s alone exhausts the
# default reply budget, so every request() would time out.
[[queues.consumers]]
queue = "messages"
max_batch_size = 1
max_batch_timeout = 0

[[durable_objects.bindings]]
name = "REPLY_INBOX"
class_name = "ReplyInboxObject"

[[migrations]]
tag = "v1"
new_classes = ["ReplyInboxObject"]

The rpc arm needs a Durable Object class your application exports, delegating to ReplyInboxObjectCore exactly as the realtime backplane's class does — see the class you export.

Three things to know before you rely on it:

  • publish reaches one consumer Worker, not every subscriber in the cluster. Cloudflare allows exactly one active consumer per queue. Fan-out happens across the handlers registered inside that consumer; cross-service fan-out means one queue per consuming service.
  • subscribe registers, it does not start receiving. Delivery begins when the Worker exports the handler above and the queue is declared under [[queues.consumers]].
  • A publish nobody subscribed to is acked, not retried. Retrying ordinary pub/sub would burn the queue's retry budget and dead-letter every fire-and-forget message. An unreadable body is still retried, and a subscriber that throws is retried.

Without the rpc arm, request and respond throw CloudflareUnsupportedError naming the binding to add — publish and subscribe are unaffected.

Sessions

SessionPluginOptions.store is read when the plugin is constructed, before any application exists, so a store published in the registry could never reach it. Build one yourself:

import { createRuntimeServices } from '@setu-ts/runtime';
import { SessionPlugin } from '@setu-ts/session-plugin';
import { type IKvNamespace, KvSessionStore } from '@setu-ts/cloudflare-plugin';

const runtime = createRuntimeServices({ env });

SessionPlugin({
  secret: String(env.SESSION_SECRET),
  mode: 'sign', // the payload lives in KV, so the cookie holds only an opaque id
  store: new KvSessionStore(env.SESSIONS as IKvNamespace, runtime),
});

D1

D1Adapter implements the committed IDatabaseAdapter, so a Worker serves CAPABILITIES.DATABASE through the ordinary repository and Unit-of-Work surface. Like the session store, you build it and hand it over — DatabasePlugin's options are read before any application exists.

import { DatabasePlugin } from '@setu-ts/database-plugin';
import { D1Adapter, type ID1Database } from '@setu-ts/cloudflare-plugin';

DatabasePlugin({
  type: 'custom',
  adapter: new D1Adapter(env.DB as ID1Database, {
    // Optional: entity name → table / primary key. Unmapped entities use their
    // own name as the table and `id` as the key.
    tables: { User: { table: 'users' } },
  }),
});

D1 has no interactive transaction. It rejects BEGIN TRANSACTION, and batch() is its only unit of atomicity. db.transaction(...) therefore buffers every write and flushes it as one batch() at commit — atomic, and rolled back whole if any statement fails. Two consequences to plan around:

  • Reads inside a transaction do not see that transaction's own writes. They run immediately against committed state. Read what you need first, then write.
  • create() inside a transaction needs an explicit primary key. A deferred INSERT cannot hand a generated key back to a caller awaiting create() before the flush, so it throws rather than returning a row with a missing id. Outside a transaction create() uses RETURNING * and gives you the real persisted row, generated columns included.

Values are always bound; table and column names are validated and quoted, and any statement that would exceed D1's 100-bound-parameter limit is refused with an error naming the count rather than failing inside D1.

Migrations stay a wrangler d1 migrations concern — the adapter has no migrate().

D1 honours the same two portable refusal rules @setu-ts/database-plugin answers: a primary-key change through update (or updateWhere) is refused with PrimaryKeyChangeError (400) instead of moving the row, and a cursor D1 cannot use — malformed, made under a different sort, or of the wrong shape for the target — is refused with InvalidCursorError (400) before any query runs. Both classes are exported from @setu-ts/common.

Durable Objects — realtime backplane and distributed lock

Both features need a Durable Object class your application exports, plus a wrangler stanza. No plugin option can export a class on your behalf, so this is wiring you write once.

The class you export

This package ships the behaviour as two plain cores. Your class extends DurableObject and delegates — that keeps cloudflare:workers out of the package, which matters because the specifier is unresolvable on every other runtime and would break type-checking everywhere.

import { DurableObject } from 'cloudflare:workers';
import { DistributedLockObjectCore, RealtimeBackplaneObjectCore } from '@setu-ts/cloudflare-plugin';

export class RealtimeBackplaneObject extends DurableObject {
  #core = new RealtimeBackplaneObjectCore(this.ctx);

  override fetch(request: Request): Promise<Response> {
    return this.#core.fetch(request);
  }
  webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void {
    this.#core.webSocketMessage(ws, message);
  }
  webSocketClose(ws: WebSocket, code: number, reason: string): void {
    this.#core.webSocketClose(ws, code, reason);
  }
  webSocketError(ws: WebSocket): void {
    this.#core.webSocketError(ws);
  }
}

export class DistributedLockObject extends DurableObject {
  #core = new DistributedLockObjectCore(this.ctx);

  override fetch(request: Request): Promise<Response> {
    return this.#core.fetch(request);
  }
}

The wrangler stanza

[[durable_objects.bindings]]
name = "REALTIME"
class_name = "RealtimeBackplaneObject"

[[durable_objects.bindings]]
name = "LOCKS"
class_name = "DistributedLockObject"

[exports.RealtimeBackplaneObject]
type = "durable-object"
storage = "sqlite"

[exports.DistributedLockObject]
type = "durable-object"
storage = "sqlite"

storage = "sqlite" is required on the Workers Free plan. The older migrations + new_sqlite_classes flow still works, but a Worker can use only one of the two flows — do not mix them.

Realtime backplane

CloudflarePlugin({ env, waitUntil, durableObject: { binding: 'REALTIME', topic: 'chat' } });

That registers CAPABILITIES.REALTIME_BACKPLANE, which websocket-plugin and sse-plugin resolve on their own — so ws.room('lobby') and sse.channel('news') start reaching clients on other replicas with no further wiring. Register either this arm or RealtimeBackplanePlugin, never both: the kernel rejects two providers of one capability token.

The guarantee is narrower than "a durable subscription", deliberately. A Worker isolate is evicted at Cloudflare's discretion and its outbound WebSockets go with it, so no Worker can hold a subscription indefinitely. What makes that sound rather than lossy is the coupling: the members the subscription serves are client WebSockets held by the same isolate, and an HTTP-triggered Worker stays alive as long as its clients stay connected — so losing the isolate loses the subscription and its members together. The socket opens lazily on first publish and reopens after any failure.

Two applications sharing one namespace must set different topic values, or each receives the other's frames.

Subscriber throws and returned promise rejections are isolated through common's shared dispatch. The default reporter reads the logger at call time: first failure per handler at warn, repeats at debug with a count, and recovery at info. Errors use describeError, so CR/LF cannot forge log lines; frame data is never logged. A directly constructed DurableObjectBackplane accepts onHandlerError(error, frame) in DurableObjectBackplaneOptions to receive every failure instead of default logging. Hook and logger failures are swallowed.

Distributed lock

DurableObjectLock implements scheduler-plugin's IDistributedLock seam, so the lock can be handed to SchedulerPlugin on a Node/Deno replica set — the composition it exists for. Do not register SchedulerPlugin on Workers: register() refuses there with a SchedulerUnavailableError, naming WorkersCron and [triggers] crons as the replacement, because timers cannot fire on an isolate that is evicted between invocations. Workers use WorkersCron, above.

// On a Node/Deno replica set — where SchedulerPlugin runs.
import { DurableObjectLock } from '@setu-ts/cloudflare-plugin';
import { SchedulerPlugin } from '@setu-ts/scheduler-plugin';

const lock = new DurableObjectLock(env.LOCKS as IDurableObjectNamespace, {
  runtime,
  keyPrefix: 'reports:',
});

// An injected lock wins outright — `enabled: true` is not needed.
app.register(SchedulerPlugin({ distributedLock: { lock } }));

One Durable Object per lock key. Correctness comes from the platform rather than an algorithm: a Durable Object processes one event at a time and holds back delivery while a storage operation runs, so the read-compare-write is atomic with no transaction and no Redlock-style quorum. The holder is persisted in the object's storage, never in memory, because an object is evicted after 70–140 seconds of inactivity and a lock TTL routinely outlives that.

A non-2xx from the object throws rather than reporting "not acquired" — a 404 means the binding points at the wrong class, and folding that into contention would silently disable every scheduled job.

Behaviour worth knowing before you deploy

  • KV rejects a TTL under 60 seconds. ICacheStore.set accepts any TTL, so the value carries its own deadline: a 5-second entry reads as a miss on time even though the key survives up to a minute longer. Short TTLs are correct; they are just not free of storage.
  • KV writes are eventually consistent. This suits read-heavy caching, not coordination. Reach for a Durable Object when you need a consistent read after write.
  • A read never deletes a key the store does not own, and a deliberately cached null survives a read — negative caching works. get still answers null for it, because ICacheStore has no way to distinguish a cached absence from a miss, but has and delete report it as present.
  • clear() needs a prefix and costs one delete per key. The binding has no bulk delete, so the sweep pages list (1000 keys at a time) and deletes each one. Without a prefix it would delete keys the store does not own, so it throws instead.
  • R2 cannot presign. getSignedUrl rejects. Serve the object through a route (getStream gives you a zero-copy download) or put a custom domain in front of the bucket.
  • Binding methods only work inside a request. Cloudflare prohibits I/O in global scope. The plugin holds bindings at register() and never reads through them there, and the health indicator performs no binding I/O.
  • An unroutable queue message is retried, never acked. A body that is not one of this package's job envelopes, or a job name with no registered processor, goes back for redelivery and is reported through the logger. Acking would discard it permanently and silently. Your queue's max_retries and dead-letter settings in wrangler.toml then decide what happens.
  • queue.addRecurring throws. Cloudflare Queues has no recurring message. Declare the schedule in [triggers] crons and let a WorkersCron handler enqueue the work.
  • There is no IScheduler on Workers, deliberately. every and delay arm timers, and the isolate is evicted between invocations, so they would silently never fire; pause/resume/ remove need state that does not survive an invocation; and getNextRun is owned by wrangler.toml. WorkersCron is a small honest surface instead of an IScheduler where six of eight methods throw.
  • A cron expression registered here but absent from wrangler.toml never fires. Nothing in the process can read that file. cron.expressions() lets you assert your own coverage. An unmatched trigger fails the invocation; the logger you pass to WorkersCron adds a structured warning naming the registered expressions, but the outcome is the same without one. Matching is exact — whitespace is not normalized.
  • Only GET requests touch the edge cache. The cache key is a URL, which the Cache API resolves as a GET request, so a POST to a cached path would otherwise be served the cached GET body and never reach your handler. Non-GET requests pass through with X-Cache-Api: BYPASS. This matters most if you add the middleware globally rather than to one GET route.
  • A failed cache write never fails your response. Cache.put rejects for an oversized body or a quota error; the response is already built by then, so the failure is reported and the request succeeds uncached.
  • The write lands with or without a waitUntil host. When the plugin received no host the middleware awaits Cache.put itself before the response returns — the write delays the response instead of being abandoned when the isolate is released (V9-23). Passing waitUntil moves the write off the response path; the plugin warns at startup when it is missing on Workers.
  • caches.default is per-datacenter. cacheApiMiddleware is a latency optimisation, not a shared store: a hit in one colo says nothing about another, and a delete does not evict globally. It reports under X-Cache-Api, so it composes with cache-plugin's cacheMiddleware (which reports under X-Cache and reads a store every colo shares) rather than colliding with it.
  • The default key carries the resolved tenant and locale. The key is a URL string the platform matches with no request headers, so Vary cannot separate entries here; ctx.request.tenant and ctx.request.locale are appended to the URL text as setu-cache-tenant and setu-cache-locale parameters instead. The middleware must run after the tenant and locale middleware (globally, at a higher priority number than theirs — 40 and 45 by default, or whatever middlewarePriority and middleware.priority are set to), and a request no key can keep apart from another — a fragment, un-normalized URL text, an unencodable id, or a reserved parameter on a request that lacks the value — is served uncached. A custom key must include both itself.
  • The edge cache refuses some responses, and the middleware skips them rather than failing. Non-GET, 206, Vary: *, and an uncleared Set-Cookie all make caches.default.put throw; those are checked first. Cache-Control: private=Set-Cookie is the platform's opt-in. Streaming responses are never cached.
  • A cache HIT is replayed as a stream, so app.inject() cannot read its body — drive cached routes in tests with app.fetch and a web Request, which is what a Worker invokes anyway.
  • Not verified against a live Worker. Every binding is exercised against a fake built from the documented signatures, including KV's 60-second floor and R2's void delete. CI has no Cloudflare account.

Not in this package

  • Hyperdrive, Vectorize, Workers AI, Analytics Engine. Reachable now through bindings.get<T>('NAME'); each becomes a first-class port only when an application needs one.

Documentation

License

MIT

Exports

| Export | Kind | | ----------------------------------------- | --------- | | assessCacheability | function | | asUpgradeResponse | function | | cacheApiMiddleware | function | | CloudflarePlugin | function | | createDefaultDurableObjectWebSocketHost | function | | createMessagingHandler | function | | createQueueHandler | function | | createScheduledHandler | function | | isD1Database | function | | isDurableObjectNamespace | function | | isKvNamespace | function | | isQueueProducer | function | | isR2Bucket | function | | CloudflareBindingMissingError | class | | CloudflareObjectNotFoundError | class | | CloudflareRemoteHandlerError | class | | CloudflareRequestTimeoutError | class | | CloudflareUnsupportedError | class | | D1Adapter | class | | DistributedLockObjectCore | class | | DurableObjectBackplane | class | | DurableObjectIdempotencyStore | class | | DurableObjectLock | class | | IdempotencyObjectCore | class | | KvCacheStore | class | | KvSessionStore | class | | R2Storage | class | | RealtimeBackplaneObjectCore | class | | ReplyInboxObjectCore | class | | WorkersBroker | class | | WorkersCron | class | | WorkersQueue | class | | BrokerRuntime | interface | | CacheabilityInput | interface | | CacheApiMiddlewareOptions | interface | | CacheClock | interface | | CloudflarePluginOptions | interface | | D1AdapterOptions | interface | | D1EntityMapping | interface | | D1Result | interface | | DistributedLockObjectCoreOptions | interface | | DurableObjectArm | interface | | DurableObjectBackplaneOptions | interface | | DurableObjectIdempotencyStoreOptions | interface | | DurableObjectLockOptions | interface | | DurableObjectMessageEvent | interface | | DurableObjectUpgradeResponse | interface | | DurableObjectWebSocketHost | interface | | DurableObjectWebSocketPair | interface | | ICacheApi | interface | | ICloudflareBindings | interface | | ID1Database | interface | | ID1PreparedStatement | interface | | IdempotencyObjectCoreOptions | interface | | IDurableObjectClientSocket | interface | | IDurableObjectNamespace | interface | | IDurableObjectState | interface | | IDurableObjectStorage | interface | | IDurableObjectWebSocket | interface | | IIdempotencyObjectState | interface | | IKvNamespace | interface | | IQueueMessage | interface | | IQueueMessageBatch | interface | | IQueueProducer | interface | | IR2Bucket | interface | | IR2Object | interface | | IR2ObjectBody | interface | | IScheduledController | interface | | IServiceBinding | interface | | JobIdSource | interface | | KvCacheOptions | interface | | KvCacheStoreOptions | interface | | KvListOptions | interface | | KvListResult | interface | | KvPutOptions | interface | | KvSessionStoreOptions | interface | | MessagingHandlerOptions | interface | | QueueHandlerOptions | interface | | QueueSendOptions | interface | | R2PutOptions | interface | | R2StorageArm | interface | | R2StorageOptions | interface | | RealtimeBackplaneObjectCoreOptions | interface | | ReplyInboxBinding | interface | | ReplyInboxObjectCoreOptions | interface | | WorkersBrokerOptions | interface | | WorkersCronOptions | interface | | WorkersMessagingArm | interface | | WorkersMessagingRpcArm | interface | | WorkersQueueArm | interface | | WorkersQueueOptions | interface | | CacheRefusal | type | | CloudflareWorkerEnv | type | | CronHandler | type | | LoggerSource | type | | MessagingHandler | type | | QueueHandler | type | | ScheduledHandler | type | | WaitUntilHost | type |

Generated from the package barrel by pnpm run docs:exports; pnpm run check:docs fails when it drifts.

The Durable Object idempotency store (M109a)

One Durable Object per idempotency store key — the tier-B store @setu-ts/idempotency-plugin can use on Workers. The application exports the DO class and hands the store to the plugin; the object owns the record, its lease and its alarm.

# wrangler.toml
[[durable_objects.bindings]]
name = "IDEMPOTENCY"
class_name = "IdempotencyObject"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["IdempotencyObject"]
import { DurableObject } from 'cloudflare:workers';
import { DurableObjectIdempotencyStore, IdempotencyObjectCore } from '@setu-ts/cloudflare-plugin';
import { IdempotencyPlugin } from '@setu-ts/idempotency-plugin';

// The Durable Object side — the class the binding names.
export class IdempotencyObject extends DurableObject {
  #core = new IdempotencyObjectCore(this.ctx);
  override fetch(request: Request): Promise<Response> {
    return this.#core.fetch(request);
  }
  override alarm(): Promise<void> {
    return this.#core.alarm();
  }
}

// The Worker side — one store, its `namespace` isolating this application's records.
const store = new DurableObjectIdempotencyStore(env.IDEMPOTENCY, { namespace: 'shop' });
app.register(IdempotencyPlugin({ store: { type: 'custom', store } }));

The store takes namespace (required), keyPrefix (default 'idempotency:'), binding (for error messages) and timeoutMs (default 5,000; 0 disables). It advertises maxRecordBytes of 120,000 — under the KV-backed Durable Object value limit — and has no isHealthy, because probing it would create and bill an object.

D1 payload refusals (M119e, 0.11.0)

D1 create, update and updateWhere refuse invalid payload identifiers or unknown payload columns with UnknownPayloadFieldError from @setu-ts/common (400). The SQLite unknown-column mapping applies only when the reported column belongs to the payload; precondition-column errors keep their existing behavior. A STRICT-table type refusal becomes InvalidFieldValueError (400). Both HTTP details are fixed and contain no field names or supplied values.