feat: add refuse() calculator and session.complete()#5
Open
Conversation
Adds two primitives for spec-compliant 204 responses. refuse: subscription config predicate that runs before session creation. Returning true responds with 204. session.complete(): mid-stream method that writes a final event with session.id, records the id in a hapi server cache, then closes. The next reconnect with that id in Last-Event-ID gets 204. Completion cache is configurable via the completion plugin option (cache, segment, expiresIn). Default: hapi's in-process cache, 5 minute TTL.
- Drop unreachable defensive check in #onKeepAlive: the keep-alive timer is cleared in close() before #closed flips, so the callback cannot run after close. - Extract readLastEventId helper and reuse it in the subscription handler so the existing array-header unit test covers the parsing branch. - Add a test for session.close() called from onSubscribe (the remaining path that hits the !session.isOpen short-circuit before initialize).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4.
Summary
Two primitives for spec-compliant
HTTP 204 No Contentresponses, each addressing a different use case.refuse(subscription config)A predicate that runs before the session is created. Server-state checks like shutdown, overload, or open circuit breakers. Returning
trueresponds with 204; the EventSource stops reconnecting.Sync or async. Throwing a Boom error returns that HTTP error.
session.complete()(session method)A mid-stream method for when the stream's work is done. Writes a final event of type
completewithsession.id(a UUID generated at construction) as the SSEidfield, records the id in a hapi server cache, then closes the connection. When the EventSource reconnects with that id inLast-Event-ID, the plugin responds 204.Why two primitives
The WHATWG SSE spec says only that a compliant client must not reconnect after receiving 204. It doesn't define when or why a server should send one. The semantics are open to interpretation, and there's no established pattern across implementations.
Looking at how 204 is used in the wild, two distinct patterns emerge:
The Quarkus discussion at quarkusio/quarkus#22762 lands on a similar reading: 204 is semantically a "completion" signal, and the moment the server decides to send it is usually mid-stream rather than at connection setup.
I checked Go, Python, and Rust SSE libraries while researching. None ship a first-class 204 API; every one expects the application to write 204 at the route level when needed. The call sites split cleanly into the two cases above. Two distinct primitives is this plugin's interpretation:
refuseis a config predicate, evaluated per request before the session exists.session.complete()is a method on a live session, called when the work it represents is finished.How completion works internally
Each
Sessiongets a UUID at construction (exposed assession.id). Whencomplete()runs, the plugin's hapi server cache records the id. On reconnect, the plugin checksLast-Event-IDagainst the cache and responds 204 if matched. Tokens are consumed on first redemption and otherwise expire by TTL.The cache lives on the plugin's realm (
server.realm.plugins['@hapi/sse']), so the Session reaches it viathis.request.route.realm.plugins['@hapi/sse']. No callback wiring.Configurability
The
completionplugin option accepts the relevant subset ofserver.cache()options:Default: hapi's in-process cache with a 5 minute TTL.
The override matters under horizontal scale. Behind a load balancer, a client whose session was completed on server A might reconnect to server B. With the in-process default, server B has no record of the completion token and would let the connection through. Pointing
cacheat a shared engine like redis (configured at server creation viaHapi.server({ cache: [...] })) lets every node behind the LB honor a completion regardless of where it originated.Test plan
complete()writes a final event, recordssession.idin the realm completion store, closescomplete()is a no-op when never initialized or already closedrefusereturningtrueresponds 204 before any session is createdrefusereturningfalseallows the connection throughrefuseis awaited and receives the requestcomplete()causes 204 on the next reconnect viaLast-Event-ID; tokens are consumed@hapi/catbox-memoryengineEventSourceclient reconnects aftersession.close()(validates the docs claim)EventSourceclient stops reconnecting aftersession.complete()(validates spec compliance end-to-end)