Skip to content

Durable Streams ​

Durable streams let you abort a running stream and resume it later without losing any data. This is useful for long AI responses where users might navigate away and come back.

How It Works ​

  1. Server assigns each stream a unique ID via SSE
  2. Stream chunks are stored in a StreamStore (in-memory or custom)
  3. When client calls stop(), the server pauses (not aborts) the stream
  4. Client calls resume() — server replays missed chunks then continues

Enabling Resumable Streams ​

Server ​

ts
// hooks.server.ts (SvelteKit)
import { createStreamHandler } from "@aibind/sveltekit/server";
import { models } from "./models.server";

export const handle = createStreamHandler({
  models,
  resumable: true, // Enable durable streams
});

Client ​

svelte
<script lang="ts">
  import { Stream } from "@aibind/sveltekit";

  const stream = new Stream({ model: "fast" });
</script>

<button onclick={() => stream.send("Write a long essay...")}>Start</button>

{#if stream.loading}
  <button onclick={() => stream.stop()}>Pause</button>
{/if}

{#if stream.canResume}
  <button onclick={() => stream.resume()}>Resume</button>
{/if}

<div>{stream.text}</div>
tsx
"use client";

import { useStream } from "@aibind/nextjs";

function DurableChat() {
  const { text, loading, canResume, send, stop, resume } = useStream({
    model: "fast",
  });

  return (
    <div>
      <button onClick={() => send("Write a long essay...")}>Start</button>
      {loading && <button onClick={stop}>Pause</button>}
      {canResume && <button onClick={resume}>Resume</button>}
      <div>{text}</div>
    </div>
  );
}
vue
<script setup lang="ts">
import { useStream } from "@aibind/nuxt";

const { text, loading, canResume, send, stop, resume } = useStream({
  model: "fast",
});
</script>

<template>
  <button @click="send('Write a long essay...')">Start</button>
  <button v-if="loading" @click="stop()">Pause</button>
  <button v-if="canResume" @click="resume()">Resume</button>
  <div>{{ text }}</div>
</template>
tsx
import { useStream } from "@aibind/solidstart";

function DurableChat() {
  const { text, loading, canResume, send, stop, resume } = useStream({
    model: "fast",
  });

  return (
    <div>
      <button onClick={() => send("Write a long essay...")}>Start</button>
      <Show when={loading()}>
        <button onClick={stop}>Pause</button>
      </Show>
      <Show when={canResume()}>
        <button onClick={resume}>Resume</button>
      </Show>
      <div>{text()}</div>
    </div>
  );
}
tsx
import { useStream } from "@aibind/tanstack-start";

function DurableChat() {
  const { text, loading, canResume, send, stop, resume } = useStream({
    model: "fast",
  });

  return (
    <div>
      <button onClick={() => send("Write a long essay...")}>Start</button>
      {loading && <button onClick={stop}>Pause</button>}
      {canResume && <button onClick={resume}>Resume</button>}
      <div>{text}</div>
    </div>
  );
}

Stream Status ​

StatusDescription
idleNo stream active
streamingActively receiving chunks
stoppedPaused, can be resumed
resumingReplaying missed chunks

Custom Stream Store ​

By default, streams are stored in memory. For production, implement a custom StreamStore:

ts
import type { StreamStore, StreamChunk } from "@aibind/core";

class RedisStreamStore implements StreamStore {
  async append(streamId: string, chunk: StreamChunk) {
    /* ... */
  }
  async getChunks(streamId: string, afterIndex: number) {
    /* ... */
  }
  async getStatus(streamId: string) {
    /* ... */
  }
  async setStatus(streamId: string, status: DurableStreamStatus) {
    /* ... */
  }
}

Released under the MIT License.