Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion apps/website/content/docs-v2/api/fetch-stream-transport.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

`FetchStreamTransport` is the production-ready transport that opens a real server-sent event connection using the browser's `fetch` API and reads a `ReadableStream` response body. It is the default transport you register with `provideStreamResource` in production builds.

You rarely need to interact with `FetchStreamTransport` directly — simply provide it once at the application level and every `streamResource` will use it automatically. You would reach for it explicitly only when constructing a resource outside the normal DI tree or when you need to override the transport for a single resource while keeping the global default intact.
## When you interact with it directly

In most apps you will never import or inject `FetchStreamTransport` by name — you register it once in `provideStreamResource` and forget about it. The two cases where you reach for it explicitly are:

1. **Per-resource override** — you want one resource to use a different transport than the global default while everything else stays on `FetchStreamTransport`.
2. **Outside the DI tree** — you are constructing a resource in a context where global providers are not available and you need to supply the transport manually.

```ts
import { inject } from '@angular/core';
Expand All @@ -15,10 +20,46 @@ const events = streamResource<Event>({
});
```

## How it works

`FetchStreamTransport` makes a `fetch` call to the given URL and expects the server to respond with `Content-Type: text/event-stream`. It then reads the `ReadableStream` body line-by-line, parses SSE `data:` fields, and emits each parsed JSON value into the resource signal.

The transport handles:

- **Backpressure** — reads chunks at the pace the browser delivers them
- **Cancellation** — aborts the underlying `fetch` when `interrupt()` is called or the resource is destroyed
- **Error propagation** — network errors and non-2xx responses surface through `resource.error()`

<Callout type="tip" title="Transport interface">
`FetchStreamTransport` implements the `StreamTransport` interface. You can
create custom transports (e.g. WebSocket-backed) by implementing the same
interface and providing them in place of this class.
</Callout>

## What's Next

<CardGroup cols={3}>
<Card
title="Streaming Guide"
icon="wave-sine"
href="/docs-v2/guides/streaming"
>
Learn how the SSE lifecycle maps to resource signals and how to handle reconnects.
</Card>
<Card
title="Deployment"
icon="server"
href="/docs-v2/guides/deployment"
>
Server configuration for SSE: headers, timeouts, and edge runtime considerations.
</Card>
<Card
title="MockStreamTransport"
icon="flask"
href="/docs-v2/api/mock-stream-transport"
>
The test-time counterpart — push values synchronously without a real server.
</Card>
</CardGroup>

{/* Auto-rendered from api-docs.json — see page component */}
87 changes: 78 additions & 9 deletions apps/website/content/docs-v2/api/mock-stream-transport.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,100 @@

`MockStreamTransport` is a test-friendly transport that replaces real network calls with an in-memory event emitter. Use it in unit and component tests to push values on demand and assert against your component's reactive state without a running server.

## Complete test example

The pattern below covers the full lifecycle: configure the transport in `TestBed`, create the component, emit values, and assert signal state.

```ts
import { Component, inject } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
provideStreamResource,
MockStreamTransport,
streamResource,
} from '@cacheplane/stream-resource';

beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideStreamResource({ transport: MockStreamTransport })],
@Component({ template: '' })
class RepoComponent {
readonly repo = streamResource<{ name: string }>({
url: () => '/api/repos/42',
});
});
}

it('reflects streamed value', () => {
const transport = TestBed.inject(MockStreamTransport);
// Emit a value into the stream
transport.emit('/api/repos/42', { id: 42, name: 'my-repo' });
// Assert your component's signal updated accordingly
describe('RepoComponent', () => {
let transport: MockStreamTransport;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [RepoComponent],
providers: [provideStreamResource({ transport: MockStreamTransport })],
});
transport = TestBed.inject(MockStreamTransport);
});

it('reflects the streamed value', () => {
const fixture = TestBed.createComponent(RepoComponent);
fixture.detectChanges();

// Push a value into the stream — synchronous, no fakeAsync needed
transport.emit('/api/repos/42', { name: 'my-repo' });
fixture.detectChanges();

expect(fixture.componentInstance.repo.value()).toEqual({ name: 'my-repo' });
expect(fixture.componentInstance.repo.status()).toBe('streaming');
});

it('surfaces errors through the error signal', () => {
const fixture = TestBed.createComponent(RepoComponent);
fixture.detectChanges();

transport.error('/api/repos/42', new Error('not found'));
fixture.detectChanges();

expect(fixture.componentInstance.repo.status()).toBe('error');
expect(fixture.componentInstance.repo.error()).toBeInstanceOf(Error);
});
});
```

## MockStreamTransport API

| Method | Description |
|--------|-------------|
| `emit(url, value)` | Pushes a single value into the stream at the given URL path. |
| `error(url, err)` | Triggers an error on the stream at the given URL path. |
| `complete(url)` | Closes the stream cleanly, as if the server sent the final event. |

<Callout type="tip" title="Deterministic tests">
Because `MockStreamTransport` is synchronous by default, you can emit values
and assert state changes in the same test tick — no `fakeAsync` or `tick`
required.
</Callout>

## What's Next

<CardGroup cols={3}>
<Card
title="Testing Guide"
icon="flask"
href="/docs-v2/guides/testing"
>
Full testing patterns including component harnesses and multi-stream scenarios.
</Card>
<Card
title="FetchStreamTransport"
icon="globe"
href="/docs-v2/api/fetch-stream-transport"
>
The production transport that MockStreamTransport replaces in tests.
</Card>
<Card
title="streamResource() API"
icon="code"
href="/docs-v2/api/stream-resource"
>
Full reference for the primitive you are testing against.
</Card>
</CardGroup>

{/* Auto-rendered from api-docs.json — see page component */}
48 changes: 47 additions & 1 deletion apps/website/content/docs-v2/api/provide-stream-resource.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# provideStreamResource()

`provideStreamResource` is the provider factory that registers `stream-resource` in Angular's dependency injection system. Call it inside `bootstrapApplication` (or an `ApplicationConfig`) to configure the transport and any global defaults used by every `streamResource` in your app.
`provideStreamResource` is the provider factory that registers `stream-resource` in Angular's dependency injection system. Call it once inside `bootstrapApplication` (or an `ApplicationConfig`) to configure the transport and any global defaults used by every `streamResource` in your app.

This is the single configuration point for the entire library. Rather than configuring each resource individually, you declare your transport here and every `streamResource` call throughout the app inherits it automatically.

```ts
import { bootstrapApplication } from '@angular/platform-browser';
Expand All @@ -19,10 +21,54 @@ bootstrapApplication(AppComponent, {
});
```

## Global configuration

| Option | Type | Description |
|--------|------|-------------|
| `transport` | `Type<StreamTransport>` | The transport class to inject when resources request `StreamTransport`. Required. |

## Swapping transports by environment

Because `provideStreamResource` accepts a class token, you can vary the transport based on your environment without touching any component code:

```ts
// main.ts — production
provideStreamResource({ transport: FetchStreamTransport })

// main.spec.ts / TestBed — tests
provideStreamResource({ transport: MockStreamTransport })
```

<Callout type="info" title="One provider, any transport">
Swap `FetchStreamTransport` for `MockStreamTransport` (or any custom class
implementing the `StreamTransport` interface) to change the transport for all
resources at once — useful for testing or SSR.
</Callout>

## What's Next

<CardGroup cols={3}>
<Card
title="Installation"
icon="package"
href="/docs-v2/installation"
>
Step-by-step setup guide including peer dependencies and NgModule support.
</Card>
<Card
title="Deployment"
icon="server"
href="/docs-v2/guides/deployment"
>
Configure transports for production, SSR, and edge runtimes.
</Card>
<Card
title="streamResource() API"
icon="code"
href="/docs-v2/api/stream-resource"
>
Full reference for the core primitive you configure here.
</Card>
</CardGroup>

{/* Auto-rendered from api-docs.json — see page component */}
53 changes: 51 additions & 2 deletions apps/website/content/docs-v2/api/stream-resource.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

`streamResource` is the core primitive of the library. It creates a reactive resource that opens a server-sent event stream, tracks loading and error states, and exposes the latest emitted value — all within Angular's signal-based reactivity model.

When the `url` signal changes, the resource tears down the previous connection and opens a fresh one automatically. You never write subscription management or cleanup logic yourself.

```ts
import { streamResource } from '@cacheplane/stream-resource';

Expand All @@ -12,15 +14,62 @@ const repo = streamResource<Repository>({
});

// Use in template
// repo.value() — latest emitted value (or undefined)
// repo.status() — 'idle' | 'loading' | 'streaming' | 'error'
// repo.value() — latest emitted value (or undefined)
// repo.status() — 'idle' | 'loading' | 'streaming' | 'error'
// repo.error() — the thrown error, when status is 'error'
// repo.interrupt() — call to cancel the stream immediately
```

## Key signals

| Signal | Type | Description |
|--------|------|-------------|
| `value()` | `T \| undefined` | The latest value emitted by the stream. Starts as `undefined` and updates with each SSE event. |
| `status()` | `'idle' \| 'loading' \| 'streaming' \| 'error'` | Lifecycle state of the current connection. |
| `error()` | `unknown` | The error thrown when `status()` is `'error'`. `undefined` otherwise. |
| `interrupt()` | `() => void` | Closes the active stream without an error — useful for user-initiated cancellation. |

## When to use

Use `streamResource` whenever your UI needs to react to a live data stream from the server:

- **AI / LLM responses** — stream tokens into a chat bubble as they arrive
- **Live feeds** — stock tickers, activity logs, or progress updates
- **Long-running jobs** — subscribe to backend task progress over SSE

For plain HTTP requests that return a single value and complete, Angular's built-in `resource()` or `httpResource()` is a better fit.

<Callout type="warning" title="Injection context required">
`streamResource` must be called during construction, inside an injection
context (e.g. a component constructor, field initializer, or a function
passed to `runInInjectionContext`). Calling it outside an injection context
will throw.
</Callout>

## What's Next

<CardGroup cols={3}>
<Card
title="Quickstart"
icon="rocket"
href="/docs-v2/quickstart"
>
Build your first streaming component end-to-end in under five minutes.
</Card>
<Card
title="Streaming Guide"
icon="wave-sine"
href="/docs-v2/guides/streaming"
>
Deep dive into SSE lifecycle, error handling, and reconnect strategies.
</Card>
<Card
title="Angular Signals"
icon="bolt"
href="/docs-v2/concepts/angular-signals"
>
Understand how stream-resource integrates with Angular's reactivity model.
</Card>
</CardGroup>

{/* Auto-rendered from api-docs.json — see page component */}
Loading