Idempotency Store
createIdempotencyStore(redis | pg, options?) is the cluster-wide store behind live.idempotent and the task runner. Three-state acquire(idempotencyKey):
acquired- first caller; run the handler.pending- another caller is currently running the handler. Await its result.result- the handler already completed. Return the cached result.
Setup
// Redis
import { createRedisClient } from 'svelte-adapter-uws-extensions/redis';
import { createIdempotencyStore } from 'svelte-adapter-uws-extensions/redis/idempotency';
const redis = createRedisClient();
const store = createIdempotencyStore(redis); // Postgres
import { createPgClient } from 'svelte-adapter-uws-extensions/postgres';
import { createIdempotencyStore } from 'svelte-adapter-uws-extensions/postgres/idempotency';
const pg = createPgClient();
const store = createIdempotencyStore(pg); Both implementations expose the same interface.
Wire into live.idempotent
import { live } from 'svelte-realtime/server';
export const charge = live.idempotent(
async (ctx, amount) => stripe.charges.create({ amount, customer: ctx.user.id }),
{
keyFrom: (ctx, amount) => `${ctx.user.id}:${amount}`,
store
}
); The wrapper namespaces the cache key as 'rpc:' + path + ':' + userKey before calling store.acquire(...).
API
| Method | Description |
|---|---|
acquire(idempotencyKey, opts?) | Returns { status: 'acquired' \| 'pending' \| 'result', result?, release? }. |
purge(idempotencyKey) | Force-evict the cached entry. |
ready() | Resolves once migrations are applied. |
release(value, error?) is included on acquired. Call it with the handler’s return value (or error) to commit the result so subsequent retries see it.
Acquire shape
const slot = await store.acquire(key, { acquireTtlMs: 60_000, ttlMs: 48 * 60 * 60_000 });
if (slot.status === 'acquired') {
try {
const result = await runHandler();
await slot.release(result);
return result;
} catch (err) {
await slot.release(undefined, err);
throw err;
}
}
if (slot.status === 'pending') {
return await slot.wait();
}
if (slot.status === 'result') {
return slot.result;
} Options
| Option | Default | Description |
|---|---|---|
acquireTtlMs | 60_000 | How long another caller waits on pending before falling through. |
ttlMs | 48 * 60 * 60 * 1000 (48 h) | How long result lives before eviction. |
maxResultBytes | 256 * 1024 (256 KB) | Reject release(result) payloads larger than this with IdempotencyResultTooLargeError. Pass Infinity to disable. |
table (Postgres) | 'svti_idempotency' | Override the table name. |
Caps
idempotencyKey longer than 1024 characters throws (defense in depth). The wrappers calling into the store (live.idempotent, tasks.run / tasks.enqueue) apply a tighter 256-char cap at the API boundary.
release(result) payloads larger than maxResultBytes (default 256 KB) are rejected with a typed IdempotencyResultTooLargeError BEFORE writing to storage, leaving the slot in its pending state. The caller can then abort() to release immediately, or let acquireTtlMs expire it.
import { IdempotencyResultTooLargeError } from 'svelte-adapter-uws-extensions/redis/idempotency';
// or postgres/idempotency - the error class is re-exported from both
try {
await slot.release(hugeResult);
} catch (err) {
if (err instanceof IdempotencyResultTooLargeError) {
// err.code === 'IDEMPOTENCY_RESULT_TOO_LARGE'
// err.bytes <- the actual size
// err.maxBytes <- the configured cap
await slot.abort();
throw new Error('handler returned too large; cannot cache for retry');
}
throw err;
} The 256 KB default matches the operational shape both backends handle cleanly without metastable behavior (Redis pubsub fragmentation, Postgres NOTIFY caps, bus envelope cap). Past a few hundred KB the cache becomes a liability rather than an asset: handlers that legitimately return blobs that large should write them to object storage and cache the resulting URL instead.
Upgrading from 0.4.x: The first-arg parameter name is now
idempotencyKey(waskey). Transparent at positional call sites; code that destructures the parameter name from a wrapper signature must update. See Migration 0.4 to 0.5.
See also
live.idempotent- realtime wrapper with auto-namespacing.- Task runner - durable runner that uses this store internally.
Was this page helpful?