Skip to content

releaseStreamGrants

releaseStreamGrants(options, claims, token, now): Promise<{ released: number; } | { refused: string; }>

Defined in: packages/server/src/circuits/subscribe.ts:704

Give back the engine claims a token’s grants acquired — the close half of subscribe.

Why this route exists. A native POST /shapes with a definition the engine already holds does not create a second shape: it JOINS the existing one, under a claim of its own. Live claims are load-bearing rather than bookkeeping — one live claim blocks both dormancy and eviction, precisely because native durable-streams reads bypass the engine entirely, so a claim is the only thing that tells it a reader it cannot see still exists. Without a release, a shape waits out its whole lease window before it can go dormant, every time.

The token IS the request, exactly as on the re-mint path: its signature proves this control plane minted these grants, so nothing has to be stored server-side to know what was acquired. Verified with allowExpired, and that is not a weakening — a session releases at CLOSE, which is routinely past a 5-minute TTL, and expiry bounds how long a grant keeps working, not what it proves was issued. A release grants its bearer nothing; the only thing it can do is give away claims that this token’s own subject holds.

ONE release per GRANT, deliberately not deduplicated by shapeId. Two grants naming one shape are two joins — the engine deduplicated identical definitions and counted both (ADR-0055 decision 6) — so collapsing them here would return one of two claims and leak the other permanently. Each grant releases its OWN claim (DELETE /shapes/{id}?subscription=<claim>), which is what makes one release distinguishable from the other at all.

Idempotent and retry-safe (fork ADR-0008). A claim is named, so releasing it twice is releasing it once: the second DELETE is a no-op 200 and cannot touch another subscriber’s claim. That is a change in kind from the anonymous refcount decrement this route was first written against, and it collapses the old asymmetry — a client, a proxy or an operator may repeat a release freely. A release that never arrives at all is not a permanent leak either: the claim is a LEASE, renewed on the token re-mint, so a session that crashed or was unloaded has its claims reclaimed by the engine within leaseSeconds. What this route buys is promptness, not correctness.

Refusals are refusals, not no-ops: a bad signature or a mismatched subject answers an error rather than { released: 0 }, because a token presented by anyone but its own subject is not a release, it is an attempt to drop the victim’s claims.

{ createShape: Promise<CircuitsShapeHandle>; releaseShape: Promise<void>; replicationState: Promise<CircuitsReplicationState>; }

CryptoKey

{[key: string]: unknown; app_metadata?: {[key: string]: unknown; roles?: string[]; }; sub?: string; } | null

string

number

Promise<{ released: number; } | { refused: string; }>