Skip to content

docs(store): explain OwnedEventStream as the on-ramp to futures::Stream — PR4 of arc-subscription refactor#180

Merged
joeldsouzax merged 1 commit into
mainfrom
refactor/arc-subscription-pr4
May 27, 2026
Merged

docs(store): explain OwnedEventStream as the on-ramp to futures::Stream — PR4 of arc-subscription refactor#180
joeldsouzax merged 1 commit into
mainfrom
refactor/arc-subscription-pr4

Conversation

@joeldsouzax

Copy link
Copy Markdown
Contributor

Summary

PR4 of the 4-PR arc-subscription refactor. Docs-only. No source code or trait changes.

The plan framed PR4 as "evaluate whether `OwnedEventStream` and `IntoStream` simplify or delete now that subscription cursors are Arc-based and `'static`." The evaluation concluded they do not: cursor structs became `'static`, but `EventStream::Item<'a>` is still a GAT and base-cursor items are still borrowing `PersistedEnvelope<'a, M>`. The witness `OwnedEventStream` carries — "this combinator's `Item<'a>` is, in fact, owned `T: 'static`" — cannot be expressed as a `for<'a> Item<'a>: 'static` HRTB on stable Rust. The sub-trait survives PR1–PR3 structurally unchanged.

What was missing was a record of design intent. Without it, the natural reading of `owned.rs` invites YAGNI-style deletion of `OwnedEventStream` impls whose chains have no current in-tree caller (`.try_map(...).map_err(...).into_stream()`, for instance). This commit documents the intent so future readers — human or AI — understand why every owning-combinator impl matters.

Changes

`crates/nexus-store/src/stream/owned.rs` (51 lines added, 3 changed):

  • New "Why the bridge exists" module-doc section — explains the on-ramp pattern: implement small owning combinators (`Map`, `TryMap`, `MapErr`) in the GAT world, then cross into `futures::Stream` via `.into_stream()` to ride the full `futures` combinator ecosystem (`.take_while`, `.filter`, `.chain`, `.then`, `.buffer_unordered`, `try_collect`, ...). Re-implementing those on lending iterators would duplicate years of upstream work; the bridge is the strategic choice to consume them instead.
  • New "Trait surface IS the contract" module-doc section — states that each `OwnedEventStream` impl is one lane on the on-ramp, and that an impl whose chain has no in-tree consumer is still load-bearing API. It defines what users can express via `.into_stream()`.
  • Existing "Implemented for [`Map`] and [`TryMap`]" bullet updated to mention `MapErr`, which has been an impl since PR feat(store): futures::Stream bridge for owning-output lending streams #171 (the doc fell out of sync at that time).
  • `IntoStream`'s `_marker: PhantomData<fn() -> M>` field gains an inline doc-comment — explains that `M` appears only in the trait bound `S: OwnedEventStream` and never in a field, so the marker is structurally needed for Rust's unused-type-parameter check. The contravariant `fn() -> M` form is the conventional "bound on this, don't store it" shape.

`docs/plans/2026-05-26-arc-subscription-deviations.md`:

  • Appended `[PR 4 | Task 1] — Decision REVISED` entry documenting the full evaluation. The initial controller decision (Option B: delete unused `MapErr` impl) was reversed after the design intent of the `stream/` module became clear — the entry records both the wrong call and why it was wrong, so the same mistake is not repeated.

Verification

  • `nix develop -c nix flake check` — all 8 checks green (toml-fmt, deny, audit, nextest, hakari, clippy, doc, fmt).
  • No source code or trait changes — only doc-comments and a markdown audit log.
  • `MapErr` import (line 37 of `owned.rs`) and `impl<S, F, E2, M> OwnedEventStream for MapErr<S, F, E2>` block (lines 118–146) are unchanged.

Test plan

  • `nix develop -c nix flake check` passes locally
  • CI passes the same flake check on the PR branch
  • (Reviewer) Read the new "Why the bridge exists" and "Trait surface IS the contract" sections; confirm they capture the on-ramp design intent without anchoring to nexus-specific types (the doc-comment uses generic `T`/`E`/`M` language, not event-sourcing language)
  • (Reviewer) Confirm the `_marker` rationale matches the actual constraint (M unconstrained in fields)

Context

This concludes the 4-PR arc-subscription refactor (PR #177#178#179 → this PR). The four PRs:

  1. feat(store, fjall): SharedSubscription (Arc-based, 'static cursor) — PR1 of arc-subscription refactor #177 — Added `SharedSubscription` trait (Arc-based, `'static` cursor).
  2. refactor(framework): migrate projection runner to SharedSubscription — PR2 of arc-subscription refactor #178 — Migrated the projection runner to `SharedSubscription`.
  3. refactor(store, fjall, framework)!: delete borrowed Subscription; promote Arc-based shape — PR3 of arc-subscription refactor #179 — Deleted the borrowed `Subscription` trait; promoted `SharedSubscription` → `Subscription`.
  4. This PR — Documents the futures-bridge design intent. The bridge was added pre-arc-subscription (PR feat(store): futures::Stream bridge for owning-output lending streams #171); PR1–PR3 did not require changes to it. The "evaluate" framing in the original plan is resolved as docs-only.

…am — PR4 of arc-subscription refactor

PR4 was framed in the plan as "evaluate whether `OwnedEventStream`
and `IntoStream` simplify or delete now that subscription cursors
are Arc-based and 'static." The evaluation came back: no, they
don't. Cursor structs became 'static, but `EventStream::Item<'a>`
is still a GAT — `Item<'a>` is still a borrowing `PersistedEnvelope`
on the base cursors. The witness that `OwnedEventStream` carries
("this combinator's `Item<'a>` is, in fact, owned `T: 'static` —
here is the name of the type") cannot be expressed via a
`for<'a> Item<'a>: 'static` HRTB on stable Rust. The sub-trait
survives PR1–PR3 structurally unchanged.

What was missing was the design intent — *why* the bridge exists
at all, and *why* every owning-combinator impl matters even when
no in-tree caller exercises a particular chain. Without that
intent recorded, the natural reading invites YAGNI-style deletion
of impls whose chains are not yet exercised. This commit records
the intent so future readers (and reviewers) understand the
shape:

- Module-level doc gains a "Why the bridge exists" section
  explaining the on-ramp pattern: implement small owning
  combinators in the GAT world (`Map`, `TryMap`, `MapErr`), then
  cross into `futures::Stream` via `.into_stream()` to ride the
  full futures combinator ecosystem (`.take_while`, `.filter`,
  `.chain`, `.then`, `.buffer_unordered`, `try_collect`, ...).
- Module-level doc gains a "Trait surface IS the contract"
  section stating that each `OwnedEventStream` impl is one lane
  on the on-ramp and that an impl whose chain has no in-tree
  consumer is still load-bearing API — it defines what users
  *can* express via `.into_stream()`.
- The "Implemented for [`Map`] and [`TryMap`]" bullet is updated
  to mention `MapErr`, which has been an impl in this module
  since PR #171 (the doc fell out of sync at that time).
- `IntoStream`'s `_marker: PhantomData<fn() -> M>` field gains
  an inline doc-comment explaining why it is structurally needed
  (`M` appears only in the trait bound `S: OwnedEventStream<M>`,
  not in any field).

No source code changes. No trait shape changes. No impl additions
or removals. PR4 ships as docs-only.

See `docs/plans/2026-05-26-arc-subscription-deviations.md` under
"[PR 4 | Task 1] — Decision REVISED" for the full evaluation
history, including the controller's initial wrong call (Option B
with `MapErr` impl deletion on YAGNI grounds) and why it was
reversed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joeldsouzax joeldsouzax merged commit 0c2d0bc into main May 27, 2026
2 checks passed
@joeldsouzax joeldsouzax deleted the refactor/arc-subscription-pr4 branch May 27, 2026 09:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant