> ## Documentation Index
> Fetch the complete documentation index at: https://docs.promptjuggler.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Browser SDK

> Subscribe to live token streams from your end users' browsers — no API key required, or allowed.

`@promptjuggler/browser` is the client for [streaming](/concepts/streaming): it opens the
SSE connection, speaks the whole protocol — segments, resets, resume, staleness — and
hands you one maintained text per run. Everything in the package is safe to ship to an
end user's browser; it authenticates with a thread-scoped credential and never sees your
API key.

## Installation

```bash theme={null}
npm install @promptjuggler/browser
```

It runs in every modern browser (`fetch` + `ReadableStream`) and in Node.js 22+.

## The credential flow

Your backend mints the credential with a server SDK and returns it verbatim; the browser
hands it to `getToken`. That one function is the entire auth story:

```ts Your backend (Node example) theme={null}
import { PromptJuggler } from '@promptjuggler/sdk';

const pj = new PromptJuggler(process.env.PROMPTJUGGLER_API_KEY);

app.get('/api/stream-token', async (req, res) => {
  // Authenticate YOUR user first — the credential grants read access to the thread.
  res.json(await pj.createStreamToken(req.query.thread));
});
```

```ts Browser theme={null}
import { PromptJugglerStream } from '@promptjuggler/browser';

const stream = new PromptJugglerStream({
  // Called on connect and on every reconnect, so token expiry renews itself.
  getToken: async () => {
    const res = await fetch(`/api/stream-token?thread=${threadId}`);
    return res.json(); // { token, url } — createStreamToken's response, verbatim
  },
});
```

## Events

```ts theme={null}
const finished = new Set<string>();

stream.on('text', ({ runId, text }) => render(runId, text));
stream.on('done', ({ runId, gapped }) => {
  finished.add(runId);
  if (gapped) refetch(runId);
});
// gap can arrive even after a clean done — a late token proving the finished
// text incomplete — and no second done follows. Refetch settled runs it names.
stream.on('gap', ({ runId }) => {
  if (finished.has(runId)) refetch(runId);
});
stream.on('failure', ({ runId, message }) => showError(runId, message));
stream.connect();
```

| Event                        | Payload highlights                      | When                                                                               |
| ---------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------- |
| `text`                       | `runId`, `text`, `gapped`               | The run's full text so far, maintained for you — subscribe to this to just render. |
| `done` / `failure`           | `runId`, `gapped` (+ `code`, `message`) | The run reached its terminal state.                                                |
| `gap`                        | `runId`, `segment`                      | Part of this run's text is missing (e.g. you joined mid-run).                      |
| `stale`                      | —                                       | The server lost history; runs still streaming should be treated as gapped.         |
| `token` / `reset`            | raw protocol events                     | For append-style UIs that assemble text themselves.                                |
| `connected` / `disconnected` | —                                       | Connection lifecycle.                                                              |

The one flag that matters is **`gapped`**: when a terminal event carries `gapped: true`,
the streamed text is (or may be) incomplete — fetch the run through your backend for the
authoritative result. When it is `false`, the text you rendered is the full output and no
fetch is needed — with one edge for raw-event consumers: a `gap` arriving *after* a clean
`done` retroactively proves that text incomplete (as in the sample above). The React hook
folds all of this into `runs[runId].gapped`, which is why it is the recommended surface.

Reconnection is automatic — exponential backoff after failures, immediate when the
server hands the connection off during a deploy — and resumes exactly where it left off.
`disconnect()` stops everything.

## React

```tsx theme={null}
import { usePromptJugglerStream } from '@promptjuggler/browser/react';

function Chat({ threadId }: { threadId: string }) {
  const { connected, runs } = usePromptJugglerStream(
    { getToken: () => fetch(`/api/stream-token?thread=${threadId}`).then(r => r.json()) },
    [threadId], // what identifies the subscription: change it, and the hook resubscribes
  );

  return Object.entries(runs).map(([runId, run]) => (
    <Message key={runId} text={run.text} pending={run.status === 'streaming'} />
  ));
}
```

`runs[runId]` is `{ text, channel, status: 'streaming' | 'done' | 'failed', gapped?,
error? }` — render `text` and you have a chatbot. The options object is read through a
ref, so inline literals never cause reconnects; only the `deps` array does.

## Angular

The same surface as signals, for Angular 17+:

```ts theme={null}
import { Component, signal } from '@angular/core';
import { injectPromptJugglerStream } from '@promptjuggler/browser/angular';

@Component({
  selector: 'app-chat',
  template: `
    @for (run of stream.runs() | keyvalue; track run.key) {
      <p>{{ run.value.text }}</p>
    }
  `,
})
export class ChatComponent {
  readonly threadId = signal('0198…');

  readonly stream = injectPromptJugglerStream(() => {
    // Read the signal HERE, in the factory itself — that read is what the
    // adapter tracks. Inside getToken it would run too late to be seen.
    const thread = this.threadId();
    return {
      getToken: () => fetch(`/api/stream-token?thread=${thread}`).then((r) => r.json()),
    };
  });
}
```

`stream.connected` and `stream.runs` are signals holding the same shapes as the React
hook. Reactivity follows Angular's grain: any signal read **during the factory call** —
here `threadId`, hoisted into a local — is tracked, and changing it drops the old
subscription (and its runs) and connects a fresh one. Reads deferred into `getToken`
happen after tracking ends, so hoist them as above. Call it in an injection context
(a field initializer or constructor); the subscription disconnects when the component is
destroyed. Signals-only, so it works in zoneless applications.

## Workflows

Every prompt node of a workflow streams into the same connection, keyed by `runId` and
tagged with its [channel](/concepts/memory-and-threads#channels). Follow only the
user-facing lanes with `channels: ['support']`.

See the [chatbot guide](/guides/building-a-chatbot) for the full end-to-end walkthrough.
