From 5b1cc3501e226ac5c41247694800780c931d8cbd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 01:46:20 +0000 Subject: [PATCH] docs(spec): state IPubSub's real delivery guarantee on the contract docblock The IPubSub docblock claimed "At-least-once delivery; handlers MUST be idempotent". No shipped driver provides that, and the repo's own measured statements said so elsewhere: content/docs/kernel/cluster.mdx 4.2 ("No shipped driver provides this yet."), service-cluster-redis's publish docblock (at-most-once, fire-and-forget, no persistence), and the authz.invalidated channel module in @objectstack/core, which recorded the contradiction inline rather than resolving it in the wrong direction. The interface docblock is the load-bearing one for the hazard it invites: it is what a consumer's editor shows at the call site. "At-least-once" tells that author their only obligation is to tolerate duplicates, while the transport's actual failure mode is a LOST message with no replay and no upper bound on how long a node that was down at publish time stays wrong. State it driver-relatively instead -- delivery is whatever the configured driver declares, and no shipped driver exceeds at-most-once -- so a future durable driver lands without rewriting the contract. A missed message is expected; handlers must be idempotent AND loss-tolerant; the staleness bound lives outside the bus; deliverySemantics is named as the per-channel surface, along with what it is (what a channel asks for, not what it gets). The driver-reach sentence is unchanged -- it was never part of the false claim. Prose only: no schema, no type, no accept/reject behaviour change. Co-authored-by: Claude --- .../ipubsub-delivery-guarantee-docblock.md | 38 +++++++++++++++++++ .../spec/src/contracts/cluster-service.ts | 23 +++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 .changeset/ipubsub-delivery-guarantee-docblock.md diff --git a/.changeset/ipubsub-delivery-guarantee-docblock.md b/.changeset/ipubsub-delivery-guarantee-docblock.md new file mode 100644 index 0000000000..4892f1ca59 --- /dev/null +++ b/.changeset/ipubsub-delivery-guarantee-docblock.md @@ -0,0 +1,38 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): state `IPubSub`'s real delivery guarantee on the interface docblock (#12651) + +The `IPubSub` contract docblock claimed **"At-least-once delivery; handlers MUST +be idempotent"**. No shipped driver provides that, and the repo's own measured +statements said so elsewhere: `content/docs/kernel/cluster.mdx` §4.2 ("**No +shipped driver provides this yet.**"), `@objectstack/service-cluster-redis`'s +`publish` docblock (plain Redis pub/sub — at-most-once, fire-and-forget, no +persistence, "acceptable **only** for events that are pure cache-invalidation +hints, never the source of truth"), and the `authz.invalidated` channel module +in `@objectstack/core`, which recorded the contradiction inline rather than +resolving it in the wrong direction. + +The interface docblock is the load-bearing one for the hazard it invites: it is +what a consumer's editor shows at the call site, before they write a subscriber. +"At-least-once" tells that author their only obligation is to tolerate +**duplicates**, while the transport's actual failure mode is the opposite — a +**lost** message, with no replay and no upper bound on how long a node that was +down at publish time stays wrong. An author who designs for duplicates and not +for loss has designed for the wrong hazard, and neither the type nor the tests +contradict them. + +The docblock now states the shipped guarantee **driver-relatively** — delivery +is whatever the configured driver declares, and no shipped driver exceeds +at-most-once — so a future durable driver lands without rewriting the contract. +It says a missed message is expected, requires handlers to be idempotent **and** +loss-tolerant, puts the staleness bound outside the bus (a TTL, a reload, a +durable outbox of its own), and points at `deliverySemantics` as the per-channel +surface while naming what that surface is: what a channel *asks* for, not what +it gets. The existing driver-reach sentence (memory = synchronous in-process; +remote drivers cross nodes) is unchanged — it was never part of the false claim. + +Prose only. No schema, no type, no accept/reject behaviour changes; the shipped +`.d.ts` carries the corrected text to consumers' editors, which is the whole +point of the repair. diff --git a/packages/spec/src/contracts/cluster-service.ts b/packages/spec/src/contracts/cluster-service.ts index 01d7b3883b..2ee7f58f29 100644 --- a/packages/spec/src/contracts/cluster-service.ts +++ b/packages/spec/src/contracts/cluster-service.ts @@ -70,10 +70,25 @@ export interface SubscribeOptions { } /** - * Fan-out messaging primitive. At-least-once delivery; handlers MUST be - * idempotent. The memory driver delivers synchronously within a process; - * remote drivers (redis pub/sub, postgres LISTEN/NOTIFY, nats) deliver - * across nodes. + * Fan-out messaging primitive. + * + * **Delivery is whatever the configured driver declares, and no shipped + * driver exceeds at-most-once.** A missed message is EXPECTED: there is no + * persistence, no replay, and no upper bound on how long a node that was down + * or slow at publish time stays wrong. Handlers MUST therefore be idempotent + * **and** tolerate loss — designing only for duplicates is designing for the + * wrong hazard. Whatever bounds staleness for a consumer (a TTL, a reload, + * a durable outbox of its own) has to live outside this bus. + * + * The per-channel surface is `deliverySemantics` on the event's cluster + * metadata (`kernel/cluster.zod.ts`, `content/docs/kernel/cluster.mdx` §4.2) + * — it states what a channel *asks* for, not what it gets. `at-least-once` + * parses there and is the default for `cluster` / `tenant` scope, but no + * shipped driver provides it yet, so read the driver you actually run before + * treating a declaration as a guarantee. + * + * The memory driver delivers synchronously within a process; remote drivers + * (redis pub/sub, postgres LISTEN/NOTIFY, nats) deliver across nodes. */ export interface IPubSub { publish(