/** * Durable-membership registry — read/write helpers over the per-space members KV bucket * (`cotal_members_`). One {@link MembershipRecord} per (concrete channel, owner) under * {@link memberKey}. This is the Plane-3 source of truth for `channelMembers()` and the fan-out's * member list, **moved off JetStream consumer topology** (core-sub joins create no consumer to * enumerate — the migration trap). * * Writes are **privileged** (the manager / open-mode self-write); agent-authored membership is * forbidden — it would self-authorize durable-backstop delivery + reads. Every write is guarded * two ways: a **generation** monotonicity check (a stale control reply with an older generation is * rejected, so it can't clobber a newer tombstone or rejoin) and a KV **revision CAS** (a concurrent * same-generation write is retried against the fresh revision). Eligibility is always by CHAT stream * **sequence** (`joinCursor`/`leaveCursor`), never wall-clock. */ import { type KV } from "@nats-io/kv"; import type { MembershipRecord } from "./types.js"; /** Thrown when a write would regress membership generation — a stale/late control reply. Callers * treat this as "a newer membership change already won", not an error to retry. */ export declare class StaleMembershipWrite extends Error { constructor(channel: string, owner: string, attempted: number, current: number); } /** Open the members registry bucket. Auth mode OPENs the bucket pre-created at `cotal up`; open dev * mode lazily CREATEs it. Mirrors {@link openChannelRegistry}. */ export declare function openMembersRegistry(nc: import("@nats-io/transport-node").NatsConnection, space: string, opts?: { create?: boolean; }): Promise; /** Read one membership record (incl. a tombstone — `leaveCursor` set), or undefined if no record / * the key was deleted. The CAS revision is returned alongside so a caller can do its own * read-modify-write; most callers use {@link commitMember}/{@link tombstoneMember} instead. */ export declare function readMember(kv: KV, channel: string, owner: string, lifecycleUid: string): Promise<{ record: MembershipRecord; revision: number; } | undefined>; /** * Commit a membership record with the generation guard + revision CAS. `next` is the full intended * record (the caller has already validated the channel ⊆ ACL, concrete, etc.). Returns the committed * record. Throws {@link StaleMembershipWrite} if `next.generation` is older than what's stored. * Retries a revision conflict (a concurrent same-or-newer write) by re-reading; if the re-read shows * a newer generation, that surfaces as `StaleMembershipWrite` too — last writer by generation wins, * deterministically. */ export declare function commitMember(kv: KV, next: MembershipRecord): Promise; /** * Tombstone a membership at `leaveCursor` (leave). Reads the current record and writes it back with * `leaveCursor` set + `state: "live-confirmed"` (the durable backstop is closed), keeping its * generation — so a later rejoin (a NEWER generation) wins, and a stale leave reply (an OLDER * generation than what's stored, e.g. the agent already rejoined) is rejected. A no-op if there is * no record (already gone) or it is already tombstoned at/below this cursor. */ export declare function tombstoneMember(kv: KV, channel: string, owner: string, lifecycleUid: string, leaveCursor: number, writerIdentity: string, expectedGeneration?: number): Promise; /** * Complete an activation: flip a pending join (generation `expectedGeneration`, `joinCursor` = * `expectedJoinCursor`, `activated:false`, open) to `activated:true`. ATOMIC via revision CAS, and * REFUSES (returns undefined) if the record is no longer that exact open pending join — a concurrent * SAME-generation LEAVE (tombstone) or a rejoin could have superseded it while catch-up ran. This is the * guard {@link commitMember}'s generation check can't provide: a same-generation activation write would * otherwise CLOBBER a same-generation tombstone (clear its `leaveCursor`, resurrect the membership) and * reopen the SPEC §7 leave boundary. Idempotent: an already-activated open record at the same generation * is returned unchanged. */ export declare function activateMember(kv: KV, channel: string, owner: string, lifecycleUid: string, expectedGeneration: number, expectedJoinCursor: number): Promise; /** Permanently remove a membership record (GC / footprint deletion — revocation deletes the footprint * AFTER invalidating creds). Distinct from {@link tombstoneMember}, which keeps the record so late * durable entries are denied by the cursor; only call this past the retention horizon. */ export declare function deleteMember(kv: KV, channel: string, owner: string, lifecycleUid: string): Promise; /** * Scan the registry, yielding every live (non-deleted) record matching the filter. `channel` → * that channel's members (fan-out's per-channel list); `owner` → that owner's memberships. With no * filter, every record. Tombstones (with `leaveCursor`) ARE yielded — a caller that wants only * currently-open memberships filters on `leaveCursor === undefined`. * * ONE pass. This was a `keys()` scan plus a sequential per-key `get()`, which cost one round trip * per surviving key; a filtered caller paid for the keys that matched, an unfiltered one paid for * all of them. The filtering is still done in code (the registry stays the single canonical source; * a derived channel→members index remains a separate, deferred decision), but the READ is now * independent of record count. */ export declare function listMembers(kv: KV, filter?: { channel?: string; owner?: string; }): Promise; /** True if a record makes the owner an **eligible durable recipient** for a CHAT message at `seq`: * the membership interval `joinCursor < seq <= leaveCursor` (open leave ⇒ no upper bound). The single * interval rule shared by fan-out routing and the trusted reader's re-auth (SPEC §7 L355-356) so they * can't drift. A tombstone stays interval-eligible for its PRE-leave window (`seq <= leaveCursor`) — * "leave is a hard read boundary" is the leaveCursor cutoff, not a drop of in-interval entries. * * This is a pure DELIVERY predicate, deliberately INDEPENDENT of `activated`. `activated` is a * COMPLETENESS/reporting flag (it gates `durableJoin`'s return value + `channelMembers`), NOT a * delivery gate: a `durable-active` record is committed `activated:false` and routes in-interval * *immediately* so no live message published during activation catch-up is lost — only the *report* * (durable:true / member listing) waits for the catch-up to confirm. Gating delivery on `activated` * instead dropped the very catch-up + post-fence messages activation exists to deliver (the * activation race): the trusted reader ack-dropped catch-up dinbox entries and fan-out skipped * post-fence/pre-activation messages, both before the flip. */ export declare function durableEligible(rec: MembershipRecord, seq: number): boolean; //# sourceMappingURL=members.d.ts.map