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
14 changes: 14 additions & 0 deletions .changeset/add-use-event-stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@perstack/react": patch
---

feat(react): add useEventStream hook for consuming PerstackEvent streams

Added a new `useEventStream` hook that provides automatic connection management for PerstackEvent streams. The hook is API-agnostic and accepts an `EventSourceFactory` function, allowing it to work with any backend that provides PerstackEvent streams.

Features:
- Automatic connection lifecycle management (connect/disconnect based on `enabled` flag)
- Event processing through `useRun` hook internally
- Connection state tracking (`isConnected`, `error`)
- Proper cleanup on unmount or when disabled
- AbortSignal support for stream cancellation
56 changes: 56 additions & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,41 @@ function MyComponent() {
}
```

### useEventStream

A hook for consuming PerstackEvent streams with automatic connection management. This hook is API-agnostic and accepts a factory function that creates the event source.

```tsx
import { useEventStream } from "@perstack/react"

function JobActivityView({ jobId, isRunning }: { jobId: string; isRunning: boolean }) {
const { activities, streaming, isConnected, isComplete, error } = useEventStream({
enabled: isRunning,
createEventSource: async ({ signal }) => {
const response = await fetch(`/api/jobs/${jobId}/stream`, { signal })
// Return an async generator of PerstackEvent
return parseSSEStream(response.body)
},
})

return (
<div>
{isConnected && <span>Live</span>}
{activities.map((activity) => (
<ActivityCard key={activity.id} activity={activity} />
))}
{Object.entries(streaming.runs).map(([runId, run]) => (
<div key={runId}>
{run.isReasoningActive && <div>Thinking: {run.reasoning}</div>}
{run.isRunResultActive && <div>Generating: {run.runResult}</div>}
</div>
))}
{error && <div>Error: {error.message}</div>}
</div>
)
}
```

### useRuntimeState

A lower-level hook for managing RuntimeState separately.
Expand Down Expand Up @@ -107,6 +142,27 @@ Returns an object with:

**Note:** Logs are append-only and never cleared. This is required for compatibility with Ink's `<Static>` component.

### useEventStream(options)

Options:

- `enabled`: Whether the stream should be active
- `createEventSource`: Factory function that returns an async generator of `PerstackEvent`

Returns an object with:

- `activities`: Array of `ActivityOrGroup` from processed events
- `streaming`: Current `StreamingState` for real-time display
- `isConnected`: Whether currently connected to the event source
- `isComplete`: Whether the run has completed
- `error`: Last error encountered, if any

The hook automatically:
- Connects when `enabled` is `true` and `createEventSource` is provided
- Disconnects and aborts when `enabled` becomes `false` or on unmount
- Processes events through `useRun` internally
- Clears error state on reconnection

### useRuntimeState()

Returns an object with:
Expand Down
7 changes: 7 additions & 0 deletions packages/react/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
export {
type EventSourceFactory,
type EventStreamOptions,
type EventStreamState,
type UseEventStreamOptions,
useEventStream,
} from "./use-event-stream.js"
export { type ActivityProcessState, type RunResult, useRun } from "./use-run.js"
export { type RuntimeResult, useRuntime } from "./use-runtime.js"
Loading