feat(store): phase-aware subscribe — Step on subscribe, decode composes (#250)#278
Merged
Merged
Conversation
Delete docs/plans/ (99 files) and docs/prompts/ (9 files) wholesale, and strip the 10 now-dangling `docs/plans/…` pointers from CLAUDE.md. The design rationale those pointers referenced is already captured inline in each CLAUDE.md module bullet, so nothing substantive is lost. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design/plan docs stay on disk for review + subagent execution but are never committed; the design intent that matters lands in CLAUDE.md and the PR body. Complements the earlier purge of the committed plan docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es (#250) The catch-up→live boundary is what makes a subscription a subscription, so `subscribe`/`subscribe_all` now yield `Step<PersistedEnvelope>` / `Step<(AllPosition, PersistedEnvelope)>` directly — the CaughtUp marker the live loop already detects rides all the way to the consumer. Phase and decode are orthogonal axes composed by combinators, never welded into a `subscribe_typed`/`cursor` method: StepStreamExt::events() — drop the phase (then DecodedStreamExt applies) StepStreamExt::decoded() — decode and KEEP the phase -> Step<Decoded<E>> Adds Step::map; deletes the internal events-only `live` filter (its logic is now the public `.events()`); subscribe builds on `live_stepped`. Migrates the #227 example off the magic count-drain + timeout_next onto CaughtUp. Existing events-only consumers gain `.events()`. Breaking change to raw subscribe + #249's `.decoded()` — both pre-freeze; the seam is fixed now because it is frozen forever at 1.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the 4 mandatory test categories across three layers: - phase_subscription_tests (InMemoryStore, end-to-end): the CaughtUp latch survives internal scan reopens (exactly one CaughtUp across a 2600-event, multi-chunk backlog, only after the final event); $all replays in position order with one CaughtUp; resume-from-checkpoint / resume-from-head phase semantics; .events() phase-strip + .events().decoded() composition; a decode error mid-backlog is never swallowed into a spurious CaughtUp; $all CaughtUp exactly once under concurrent multi-stream writers. - step_stream_ext_tests (combinator isolation, synthetic Step streams): .events() drops every marker (incl. multiple/interleaved) and passes Err through in place; .decoded() preserves phase, keeps the $all tag, and routes Read vs Decode to distinct variants; a proptest asserts .events() yields exactly the Event payloads for any interleaving. - nexus-fjall subscription_tests (real LSM adapter): CaughtUp survives a full close/reopen replay from disk, and exactly one CaughtUp across a >CATCHUP_CHUNK backlog that forces real ScanCursor reopens. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Resolves #250. Builds on #249 (now in main).
What & why
The catch-up→live boundary is what makes a subscription a subscription (a finite read has no such boundary). So
subscribe/subscribe_allnow yieldStep<PersistedEnvelope>/Step<(AllPosition, PersistedEnvelope)>directly — theCaughtUpmarker the live loop already detects rides all the way to the consumer, instead of being thrown away by rawsubscribeand re-added by a separatesubscribe_typed/cursormethod.Phase and decode are orthogonal axes, composed by combinators — never welded into a method name:
Consuming catch-up→live is now: match
Step::CaughtUp/Step::Event(d)— no magic count-drain, no hand-rolledtimeout_next, nexus-owned error.Changes
Step<T>(Event | CaughtUp) frozen two-variant phase marker +Step::map.StepStreamExt(.events()/.decoded()) reusing [freeze] Subscriptions force hand-decoding: no ergonomic typed view that reuses the configured codec #249'sRawItem/Decoded/DecodeStreamError;Step<I>deliberately not aRawItem(CaughtUp has no envelope), so the two.decoded()methods are non-overlapping.subscribe/subscribe_allre-shaped onto thelive_steppedcore loop; internal events-onlylivefilter deleted (its logic is now the public.events()).timeout_nextontoCaughtUp..events().Breaking to raw
subscribe+ #249's.decoded()— both pre-freeze/experimental; the seam is fixed now because it is frozen forever at 1.0.Tests (4 mandatory categories, three layers)
phase_subscription_tests(end-to-end): exactly-one-CaughtUpacross a 2600-event multi-chunk backlog (latch survives scan reopens);$allposition-order replay; resume-from-checkpoint/head;.events()strip + composition; decode error never swallowed into a spuriousCaughtUp;$allconcurrency.step_stream_ext_tests(combinator isolation + proptest): pathologicalStepsequences, error-domain routing, tag preservation.nexus-fjall(real LSM adapter):CaughtUpsurvives close/reopen replay and >CATCHUP_CHUNKScanCursorreopens.🤖 Generated with Claude Code