Skip to content

feat(client)!: deliver resource state events from one stream connector - #53

Merged
sunib merged 3 commits into
mainfrom
docs/proposal-0009-stream-editor-separation
Oct 5, 2026
Merged

sunib merged 3 commits into
mainfrom
docs/proposal-0009-stream-editor-separation

Conversation

@sunib

@sunib sunib commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Adds proposals 0009 (separating the browser stream from the editor) and 0010 (Go gateway API cleanup), and implements change 1 of proposal 0009: one public fetch connector that delivers protocol state events to a callback, so transport no longer depends on the editor store.

What changed

One connector. connectResourceStream(url, consume, options) is the former managed connector under its final name. The single-connection fetch loop is now internal (streamOnce in sse.ts). connectManagedResourceStream and connectWithEventSource are removed with no aliases. sse.ts and connection.ts import nothing from the store.

State events in. A new ResourceStateEvent union (reset, added/modified, deleted, synced; no seq, no transport error fields) is what the callback receives: synchronously, exactly once, in stream order. Unknown wire types still count in the sequence and are then ignored. Errors go only to onError. An upsert without object or a delete without identity.uid is dropped, as before.

Lifecycle through state + subscribe. onOpen, onSynced, onGap, onStateChange and onChange are removed; onError stays. The connector observes reset itself (it enters syncing and clears the health timer before the callback applies the event). It publishes live only after the callback has applied synced, and does so after every completed snapshot. A retrying state caused by a sequence gap carries gap: { expected, received }, which the next attempt clears.

Consumer exceptions. The exception is captured where it is thrown, including after close(). The reader, health timer and signal listener are released, closed is published, and handle.closed rejects with the original exception. It is not retried and not reported as a stream error.

Protocol header. If X-KRM-Stream-Protocol is present on a successful response and names a different version, the stream fails with a terminal local INTERNAL error: the body is cancelled, no event is applied, and nothing is retried. A missing header is accepted. HTTP refusals keep their classification.

Editor layer kept usable for now. applyStreamEvent(store, event) and StreamChange move into store.ts and take a ResourceStateEvent. The store's methods are otherwise unchanged; consolidating them is change 2.

Migrated: package tests, the Go-wire e2e, the Vue and conditional-save examples, the vanilla browser demo, both READMEs, and the adoption, auth, state-model and why-a-gateway guides. Spec §7 and consumer rule 9 are reworded where removing the EventSource connector made them false. The wording changes are implementation-neutral; no wire fields or error codes change.

Raw EventSource coverage kept. The browser suite reads the real replay gateway with a raw EventSource and test-owned handling. It checks framing, that the session cookie is sent on both connections, that a native reconnect gets a fresh seq: 1 snapshot, and that closing on a terminal error stops reconnects.

Breaking changes

  • connectManagedResourceStream(url, store, opts) → connectResourceStream(url, event => applyStreamEvent(store, event), opts). The old single-connection connectResourceStream and connectWithEventSource are gone.
  • ManagedStreamOptions/StreamOptions → ResourceStreamOptions; ManagedStreamHandle/StreamHandle → ResourceStreamHandle.
  • onOpen, onSynced, onGap, onStateChange, onChange are removed. Use state/subscribe, the callback (render from applyStreamEvent's return value), and state.gap.
  • applyStreamEvent takes a ResourceStateEvent: no seq, no error events. StreamChange.type no longer includes "error".
  • closed rejects when the callback throws. A mismatched protocol header is terminal.

Verification

Run on 42c745c:

Command Result
task fixtures-check (after task --force fixtures) pass, goldens unchanged
task lint pass (Go vet, golangci-lint, tsc ×2, biome, actionlint, hadolint)
task test pass. Client 129/129, Vue 2/2. Go packages were served from Go's test cache; no Go source changed
task e2e-wire pass, 14 fixtures over a real socket
task e2e-browser pass, 20/20 in Chromium (per-module and bundle)
task pack-client pass. The tarball and bundle export connectResourceStream/applyStreamEvent, and neither removed name

The new connection tests were checked by breaking the implementation on purpose. Each of these made a test fail: ignoring a callback exception after close(), publishing live before synced is applied, dropping gap context, skipping the protocol check, leaking the health timer or the signal listener, observing reset after the callback, and delivering events after a close.

Not run: task release-rehearsal (part of task verify; it covers the Go adapter release and this PR doesn't touch it), and the cluster tasks (not a gate for browser separation).

Remaining (proposal 0009 changes 2–3)

  • Change 2: a bound applyStreamEvent method on the stores, with beginSnapshot/endSnapshot/applyServerEvent/removeResource made private. Remove the standalone helper, ApplyResult and ApplyOptions. Extract the shared SnapshotTracker, add ResourceStore, remove adoptSaved, takeTheirs, status() and readOnlyPolicy, and simplify ReconciliationOptions. Add the /stream and /editor entry points, trim exports (including the internal SSEDecoder, StreamSequence, path/deep helpers and PROTOCOL_VERSION), and assert the emitted module graph.
  • Change 3: the read-only example, editor/create/delete guidance, package metadata and remaining protocol wording, and browser/Go-wire coverage of both stores.

BREAKING CHANGE: connectManagedResourceStream is renamed connectResourceStream(url, consume, options), and the old single-connection connectResourceStream and connectWithEventSource are removed without aliases. ManagedStreamOptions/StreamOptions become ResourceStreamOptions and ManagedStreamHandle/StreamHandle become ResourceStreamHandle; onOpen, onSynced, onGap, onStateChange and onChange are removed in favour of state, subscribe and the callback. applyStreamEvent takes a ResourceStateEvent, without seq or error events. closed now rejects with a callback's exception, and a mismatched X-KRM-Stream-Protocol header is terminal.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • API Changes
    • Replaced the managed stream connector with connectResourceStream, which delivers resource events to a callback. Apply events to your store explicitly.
    • Connection handles now provide status updates, error reporting, and a promise that settles when the connection ends. Recoverable failures trigger reconnection; terminal errors stop the stream.
    • The native EventSource connector is no longer available. Browser examples use fetch-based streaming.
  • Documentation
    • Updated guides and examples with the event-handling workflow, connection monitoring, and recovery behavior.

sunib and others added 3 commits October 4, 2026 19:28
Add proposal 0009, which makes the browser connectors deliver protocol
events to a callback, adds a read-only ResourceStore for lists and
viewers, and keeps LiveResourceStore as the optional editing layer. It
opens with an overview of why live watches and three-way merge belong
together. Point proposal 0006 at it and split 0006's remaining work into
editor and stream tracks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tighten proposal 0009's connector contract (state events, lifecycle
through state and subscribe, consumer exceptions, protocol header
diagnostics) and its three-change delivery order. Add proposal 0010 for
the separate Go API cleanup, and point proposal 0006 at both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Implement change 1 of proposal 0009. connectResourceStream is now the
only public connector: the managed fetch connector, taking a synchronous
callback for ResourceStateEvent instead of a LiveResourceStore. The
single-connection fetch loop is internal, and transport no longer
imports the editor store.

- The connector detects reset and synced from protocol events itself,
  applies synced through the callback before publishing live, and
  reports lifecycle only through state and subscribe (plus onError).
- A retrying state caused by a sequence gap carries
  gap: { expected, received }; the next attempt clears it.
- A callback exception, including one thrown after close(), stops the
  stream without retrying, publishes closed and rejects closed with the
  original exception once reader, timers and listeners are released.
- A present X-KRM-Stream-Protocol header naming another version is a
  terminal INTERNAL error before any event is applied; an absent header
  is accepted. HTTP refusals keep their classification.
- applyStreamEvent and StreamChange move to the editor store module and
  take ResourceStateEvent.
- Tests, the Go-wire e2e, examples, guides and spec §7 / consumer rule 9
  use the one connector. The browser suite keeps raw EventSource checks
  of the gateway: framing, cookies, a fresh snapshot on native reconnect
  and test-owned terminal shutdown.

BREAKING CHANGE: connectManagedResourceStream is renamed connectResourceStream(url, consume, options), and the old single-connection connectResourceStream and connectWithEventSource are removed without aliases. ManagedStreamOptions/StreamOptions become ResourceStreamOptions and ManagedStreamHandle/StreamHandle become ResourceStreamHandle; onOpen, onSynced, onGap, onStateChange and onChange are removed in favour of state, subscribe and the callback. applyStreamEvent takes a ResourceStateEvent, without seq or error events. closed now rejects with a callback's exception, and a mismatched X-KRM-Stream-Protocol header is terminal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 304dd1f3-ad12-436a-b3e5-8e923a69bf98
📥 Commits

Reviewing files that changed from the base of the PR and between 42c745c and 42c745c.

📒 Files selected for processing (1)
  • examples/vanilla-browser/index.html

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The client package replaces managed and native EventSource connectors with a fetch-based connector that delivers resource state events to a consumer. Store application, connection lifecycle handling, browser examples, tests, and documentation are updated. A separate proposal describes potential Go gateway API cleanup.

Changes

Resource stream client

Layer / File(s) Summary
Resource events and store application
packages/krm-stream/src/types.ts, packages/krm-stream/src/store.ts, packages/krm-stream/src/sse.ts, packages/krm-stream/test/*
The package defines ResourceStateEvent and StreamChange. applyStreamEvent dispatches state events to LiveResourceStore. Tests use a helper to convert wire events before applying them.
Fetch transport and connection lifecycle
packages/krm-stream/src/connection.ts, packages/krm-stream/src/sse.ts, packages/krm-stream/test/connection.test.ts, packages/krm-stream/e2e/wire.ts
connectResourceStream delivers recognized state events through a synchronous consumer. The fetch transport checks sequence numbers and protocol versions. The connection handle reports state, retries recoverable failures, and closes on terminal errors or consumer exceptions. Tests cover delivery, recovery, closure, and cleanup.
Browser integration and API guidance
packages/krm-stream/src/index.ts, packages/krm-stream/README.md, README.md, docs/*, examples/*, spec/v1.md
Exports, examples, and guidance switch to the callback-based connector and explicit store application. Browser examples subscribe to connection state and report closure errors. Proposal 0009 describes the proposed stream and editor separation.

Gateway API cleanup proposal

Layer / File(s) Summary
Proposed gateway API and test-harness changes
docs/proposals/0010-gateway-api-cleanup.md
The proposal describes projection-aware stream methods, shared stream configuration, internal test utilities, and migration and acceptance requirements. It states that the document does not change Go APIs.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant connectResourceStream
  participant streamOnce
  participant Consumer
  participant LiveResourceStore
  Client->>connectResourceStream: open URL with event consumer
  connectResourceStream->>streamOnce: start fetch attempt
  streamOnce->>Consumer: deliver recognized resource state event
  Consumer->>LiveResourceStore: applyStreamEvent
  LiveResourceStore-->>Consumer: return StreamChange
  streamOnce-->>connectResourceStream: report attempt outcome
  connectResourceStream-->>Client: publish connection state
Loading

Merge Risk: ⚪ Minimal · up to 42c74

No merge-blocking issue is identified in the reviewed change; it is ready for normal merge checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 42c74

The reviewed browser caller preserves its existing same-origin credential behavior, and the connector contains sequence gaps and consumer failures. No introduced security vulnerability was established. Gateway authentication and identity enforcement remain unverified, so the assessment is not minimal.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The reviewed changes affect client consumers and their streamed resource state. The example uses a fixed same-origin endpoint with query-derived fixture and pace parameters. The maximum accessible tenant, asset, or datastore scope cannot be established without gateway authorization evidence.

Trust Boundaries and Controls

  • observed — The connector propagates browser credentials but does not itself establish authenticated identity. The session-cookie browser test asserts Cookie headers and stream snapshots using raw EventSource; it does not assert fetch-cookie authentication or binding to an authorized identity.

Resilience and Maintainability Implications

  • observed — HTTP 401 and 403 refusals are terminal rather than retried. Retry attempts are serialized and bounded. Consumer exceptions stop further delivery, release the reader, and take the connection into its closed failure path rather than automatic retry.

Hardening Proposals

  • proposed — Resolve the authentication proof gap with gateway-backed fetch checks that assert authenticated identity and rejection of invalid or unauthorized sessions. For supported cross-origin deployments, also verify credentialed CORS and redirect policy rather than inferring enforcement from Cookie delivery.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: delivering resource state events through one stream connector.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 17 files. (1 skipped: …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

{
maxRetries: connections - 1,
retryDelayMs: 0,
fetch: (_input, init) => fetch(url(f.id, conn++), init),

try {
if (controller.signal.aborted) return undefined;
const res = await (opts.fetch ?? globalThis.fetch)(url, {
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.

2 participants