Repository navigation
Phase 7b: server-sent events — documentation and phase record - #83
Merged
Merged
Conversation
Phase 7b, chapter 13 (SSE-1 through SSE-40; SSE-41 declined for v1). Nine new files under lib/dexpace/sse/ with sig/ mirrors: the namespace with its three limits (MAX_LINE_BYTES 1 MiB, MAX_EVENT_BYTES 8 MiB, MAX_RETRY_MS 2^31-1) and the two frozen Sentinel singletons SKIP and DONE; LineReader, the WHATWG line machine over BufferedSource#getbyte with a one-byte pushback, built there and not over #read_line_utf8 because IO-14 keeps a lone CR as content where SSE-2 terminates on it (P7-20); the immutable five-field Event (Data plus Model); Reader, the field machine whose one persistent state is the BOM flag; Stream, the resource-owning single-pass facade over Closeable's latch, built through .open/.owning/.borrowing with logger: on each; TypedStream over a caller-supplied mapper; and the two namespaced errors. The design's Signal is Sentinel: a bare Signal inside module Dexpace::SSE would shadow Ruby's ::Signal (P7-81). _ByteSource has four methods, close included, because the reader closes its peek view (P7-82). SSE-30's swallowed release failure is reported through Dexpace.close_quietly(self, logger:) as an http.instrumentation.close diagnostic (P7-83). gates:serde_boundary is the eighteenth gate: tools/serde_boundary.rb scans lib/dexpace/sse.rb, lib/dexpace/sse/** and their sig/ mirrors with a parsed scan (prism for every require spelling and every constant read or path, the RBS lexer for type names), asserts every guarded glob matches a file, and prints the pagination layer's globs as PENDING until 7c lands (P7-84). Wired into DEFAULT_GATES, gates:list, the default task's pin and CI's once-per-run gates job, with fixtures under test/fixtures/gates/serde_boundary/. Also: the smoke suite's SSE_LAYER and PhaseSevenLayers case, the surface manifest regenerated once (1137 to 1180 rows, every row read against the object model), and the #read_line_utf8 YARD in io/typed_reads.rb corrected (SSE-11 to SSE-19, and its false premise replaced).
…xits
Review round 0 of phase 7b, findings R0-1 and R0-2.
Reader#dispatch returned nil for a block with no field seen before
reset_block ran, so the block's byte total carried over every blank
line that closed a fieldless block. A run of unknown-field keep-alives,
NUL ids or rejected retries between real events accumulated into
SSE-19's event cap and tore the stream down with a spurious
LimitExceededError(kind: :event) once their sum crossed
max_event_bytes -- at the shipped 8 MiB, after 8 MiB of keep-alives.
SSE-1 makes a blank line the end of a block whether or not it
dispatched, so dispatch now resets the block either way; the Event is
built by a private build_event only when a field was seen, and the
attr_reader's YARD says "reset at every blank line, dispatching or not".
TypedStream#drive_values had the rescue Stream#drive has and not its
ensure, so an Enumerable method that stops early on #values -- first(n),
take, find, each { break } -- left the resource unreleased (closes 0)
where the same call on Stream#events released (closes 1): the raw
enumerator it drives is parked mid-#next and never reaches the Stream's
own ensure. The design names the two shapes as mirrors, and the plan's
own fence drove Stream#drive and inherited its ensure. drive_values now
closes in an ensure of its own: a loud release on the block-form exit,
a no-op on the clean end, a DONE and both failure paths, whose latch has
already flipped. No public name or signature changes; reader.rbs gains
the one private build_event line.
Fourteen suites under gems/dexpace-core/test/dexpace/sse/ plus the namespace file's mirror, one per lib/ file and three with no mirror: matrix_facts_test.rb (the design's eight facts and the build's, a standing test on every CI row), boundaries_test.rb (SSE-37's and SSE-38's absences) and documentation_test.rb (the line-cap closure's prose on both the SSE constants and #read_line_utf8). The grammar battery is the chapter's own conformance fixtures, the terminator property test is seeded, every encoding assertion is non-ASCII, the two caps are proven at a small value and at their real values (over the duck source, whose #getbyte is a third the cost of BufferedSource's), and SSE-31's two shapes are both written and deterministic, the blocked-read one over BufferedSource.wrapping of an IO.pipe. Three top-level doubles, one object per file: SSEFixtures (the shared stream_over helper and its fixtures), ScriptedChunked (a chunked body whose script raises where an Exception sits) and FakeByteSource (the _ByteSource duck, used to keep P7-27's claim behavioural). The tree's own FakeResponseBody, FakeChunked, RecoveryFixtures and RecordingSink answered the plan's other doubles without a new file. test/gates/serde_boundary_test.rb drives the gate against twenty-one fixtures and two fixture workspaces: every require spelling, every constant spelling including ::JSON and self::JSON, a serde type in an .rbs, a comment and a string that must pass, an unparseable file, a non-literal feature, and a GUARDED glob that matches nothing.
…rror
Review round 0 of phase 7b, findings R0-1 through R0-3.
reader_test.rb's EventCapTest gains the case the fix for R0-1 needed:
three 5-byte unknown-field blocks, six NUL-id blocks and six rejected-
retry blocks under a cap each block is far below must not add up to a
LimitExceededError across the blank lines that separate them, while the
same lines in ONE block still trip it. Red against the previous
dispatch (an error, kind :event under the 12-byte cap), green now.
typed_stream_test.rb's ViewTest gains two: first(1), take(2), find and
an each { break } on #values each release the resource once, as the
same call on #events does, and a release failure on that exit
propagates as the raw form's does (R0-2). Red against the previous
drive_values (closes 0 in both), green now. stream_test.rb's
LifecycleTest pins the raw half, events.first(2), which sse.md
documented and nothing asserted.
stream_state_error_test.rb is the one-class error's own mirror, which
the checklist cited and the tree did not hold (R0-3): a Dexpace::Error
and a StandardError caught through Module#===, and the two messages
naming SSE-26 and SSE-27. Every non-private lib file under sse/ now has
its test mirror, as the count sentence in CLAUDE.md says.
The checklist, written from the build: forty-one own rows (forty implemented, SSE-41 declined for v1), the cross-reference rows, the matrix facts on every interpreter, the guard battery (forty-six mutations, forty-six caught on 3.2.11 and forty-five on 4.0.6, the one survivor an equivalent mutant), the audit groups, thirty-six departures from the plan's text, the findings routed and the postponed work. The design's ledger gains an As-built addendum, P7-81 through P7-86. docs/sdk-documentation/sse.md is the as-built page, every example run on 4.0.6 and 3.2.11; io.md's line-cap sentence and quality-gates.md's table (row 9, eighteen gates) are corrected; architecture.md, the core README, README.md and docs/README.md point at the new page. docs/knowledge/notes/sse-streaming.md files two supersessions and three references. CLAUDE.md's claim sentences are re-derived from the tree (193 lib files, fifteen checklists, eighteen gates) and its constraints list gains four lines. The roadmap gains the 2026-09-20 status note and two phase-10 inbound bullets: BufferedSource.over's enumerator restarts after a mid-stream failure, and the per-byte read path's cost.
The checklist's SSE-1, SSE-19 and SSE-25 rows say what the code now does: the block resets at every blank line, dispatching or not, and an early-stopping Enumerable method on the typed #values releases the resource as the same call on #events does; guards 51 and 52 are the round's two, each the reviewer's control mutation turned red on both rows against the pre-fix lib. The NFR-13 row and "What was built" carry the corrected counts -- forty-six new .rb files, twelve suites, twenty Ruby fixtures, docs/first-release.md untouched -- and Deviations 16, 36 and the new 37 record the missing error-test file, the typed ensure and the round's fixes per branch. The design's As-built addendum and P7-86 say the byte total resets at every blank line and that drive_values carries Stream#drive's ensure; the roadmap's 7b note gains the round's paragraph. sse.md's two blocks that read `e` without binding it now show the rescue that binds it, the Stream and TypedStream sections state the early-exit mirror, and the typed example demonstrates values.first(1) releasing; every `# =>` on the page was re-run on 4.0.6 (55 matched) and 3.2.11 (54, plus the documented NoMethodError wording). CLAUDE.md's SSE constraint names the typed drive's own ensure.
Contributor
Author
Review record for the phase 7b stack (#81 → #82 → #83)2 independent reviews, each by a fresh agent with no memory of the previous one, each re-running every gate itself on every tip (4.0.6 every gate individually at the code tip and the full
Nothing was skipped. Round 0 → fixed in round 1
Round 1 (final) — approve
What the final reviewer verified by experiment, both interpreters
Tips reviewed at the final round: code |
Wahbeh-Mohammad
changed the base branch from
27-phase-7b-server-sent-events-tests
to
main
September 20, 2026 13:58
This was referenced Sep 20, 2026
Closed
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 #27. Third PR of phase 7b's stack — the phase record and the documentation — on top of #82.
What lands
12 files, +1,165 / −41.
docs/work/mvp/phase7/phase7b/2026-09-10-phase7b-server-sent-events-checklist.md— 41 own rows: 40 ✅, 1 ⏳ (SSE-41, MAY, declined for v1 —docs/first-release.md§ What v1 ships without, whose entry is true as written and untouched), plus 20 cross-reference rows; the roadmap's legend verbatim; sections: requirement rows, what was built, guards run red (50), audit groups run, deviations from the plan (36), findings routed, postponed work._ByteSource;logger:and theclose_quietlyroute forSSE-30; the eighteenth gate; the 43 public names as P7-27 amended;TypedStream's two drive shapes and the facade's one failure path), beside the design's own P7-20–P7-27, which stand. The§10.18amendment for the two cap constants and the consolidation into design §10 are a human's —docs/sdk-design-ruby/is frozen — and the addendum says so.docs/sdk-documentation/sse.md(new; every example executed on 4.0.6 and 3.2.11 before it was written down),architecture.md,io.md(the#read_line_utf8line-cap sentence corrected beside the YARD),quality-gates.md(the eighteenth gate's row), the core README,README.md,docs/README.md.docs/knowledge/notes/sse-streaming.md(new) — supersedessse-streaming/2dba42b0, corrects/8c25db7dand/e98a0668, three reference entries (thePAGE-14attribution, theSSE-11/SSE-19attribution, the enumerator restart);ruby scripts/verify_knowledge_structure.rbOK.CLAUDE.md— the built-phases sentence gains 7b; the opening paragraph gains the SSE layer; the lib-file count and the mirror clauses re-derived from the tree; fifteen checklists; eighteen gates everywhere seventeen was counted; one line under "Constraints that will bite".#getbytethroughput).docs/product-spec/,docs/sdk-design-ruby/,docs/knowledge/harvested/,docs/deviations.mdand every other phase's documents are untouched.Concurrency note for the merge
Three sibling lanes (7a #26, 7c #28, 8a #30) are built off the same
mainand edit the same count sentences,lib/dexpace.rb, the LAYERS table, the manifest andtasks/gates.rake(8a'sclean_bundle_checkedit; 7b's task block is appended). Whichever stack merges second rebases and re-derives the counts; thegates:serde_boundarypage/**row flips from PENDING to GUARDED on the first tip that holds both 7b and 7c.Verification
Docs tip:
bundle exec rake(eighteen gates) green on 4.0.6;ruby .claude/skills/housekeeping/probe.rbexit 0 (all eight checks);ruby scripts/verify_knowledge_structure.rbOK (2,166 harvested, 61 notes); the housekeeping and knowledge tooling suites green; everysse.mdblock run on both interpreters.Known follow-ups
R1-1 (nit) —
sse.md:107's "copies it again" wording; see the code PR's list.