Skip to main content
AI operations take time. Streaming shows real-time progress as the agent works.

Event Types

type SSEEvent<T> = ProgressEvent | CompleteEvent<T> | ErrorEvent;

ProgressEvent

interface ProgressEvent {
  type: 'progress';
  percentage: number;
  message?: string;
  metadata?: Record<string, unknown>;
}

CompleteEvent

interface CompleteEvent<T> {
  type: 'complete';
  data: T;
}

ErrorEvent

interface ErrorEvent {
  type: 'error';
  error: string;
  code?: string;
  retryable?: boolean;
}

Async Iterator Pattern

generateGraphStream() returns an async iterator:
const stream = await ai.generateGraphStream({
  config,
  userPrompt: 'Add a trend line',
});

for await (const event of stream) {
  if (event.type === 'progress') {
    console.log(`${event.percentage}% - ${event.message}`);
  }

  if (event.type === 'complete') {
    return event.data.config;
  }

  if (event.type === 'error') {
    throw new Error(event.error);
  }
}

Cancellation

Pass an AbortSignal to cancel mid-operation:
const controller = new AbortController();

const stream = await ai.generateGraphStream(
  { config, userPrompt: 'Create a complex heatmap' },
  controller.signal
);

// Cancel from a button click
cancelButton.onclick = () => controller.abort();

try {
  for await (const event of stream) {
    if (event.type === 'complete') {
      return event.data;
    }
  }
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Cancelled by user');
    return;
  }
  throw error;
}

Progress Callback Alternative

If you want progress updates without the streaming API, use the onProgress callback with generateGraph():
const result = await ai.generateGraph(
  { config, userPrompt: 'Add a trend line' },
  (progress) => {
    setProgress(progress.percentage);
    setStatus(progress.message ?? '');
  }
);

// result contains the final GenerateGraphResponse
This collects the stream internally and returns the final result.

React Pattern

Store the abort controller in a ref for proper cleanup:
import { useState, useRef, useEffect } from 'react';
import { GraphyAiSdk } from '@graphysdk/ai';
import type { GraphConfig } from '@graphysdk/core';

interface Props {
  initialConfig: GraphConfig;
  onUpdate: (config: GraphConfig) => void;
}

function ChartEditor({ initialConfig, onUpdate }: Props) {
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const abortRef = useRef<AbortController | null>(null);

  const ai = useRef(
    new GraphyAiSdk({
      apiKey: process.env.NEXT_PUBLIC_GRAPHY_API_KEY,
      baseUrl: 'https://agents.graphy.dev',
    })
  ).current;

  const handleGenerate = async (prompt: string) => {
    // Cancel any in-flight request
    abortRef.current?.abort();
    abortRef.current = new AbortController();

    setIsLoading(true);
    setProgress(0);
    setError(null);

    try {
      const stream = await ai.generateGraphStream(
        { config: initialConfig, userPrompt: prompt },
        abortRef.current.signal
      );

      for await (const event of stream) {
        if (event.type === 'progress') {
          setProgress(event.percentage);
          setStatus(event.message ?? '');
        }

        if (event.type === 'complete') {
          onUpdate(event.data.config);
        }

        if (event.type === 'error') {
          setError(event.error);
        }
      }
    } catch (err) {
      if (err instanceof Error && err.name !== 'AbortError') {
        setError(err.message);
      }
    } finally {
      setIsLoading(false);
    }
  };

  // Cleanup on unmount
  useEffect(() => {
    return () => abortRef.current?.abort();
  }, []);

  return (
    <div>
      {isLoading && (
        <div>
          <progress value={progress} max={100} />
          <span>{status}</span>
          <button onClick={() => abortRef.current?.abort()}>Cancel</button>
        </div>
      )}
      {error && <div className="error">{error}</div>}
      <PromptInput onSubmit={handleGenerate} disabled={isLoading} />
    </div>
  );
}
Key patterns:
  1. Cancel previous requests — Abort any in-flight request before starting a new one
  2. Store in ref — The abort controller persists across renders
  3. Cleanup on unmount — Cancel pending requests when the component unmounts
  4. Handle AbortError — Don’t treat user cancellation as an error

Type Guards

The SDK exports type guards for narrowing event types:
import { ApiClient } from '@graphysdk/ai';

for await (const event of stream) {
  if (ApiClient.isProgressEvent(event)) {
    // event is ProgressEvent
  }
  if (ApiClient.isCompleteEvent(event)) {
    // event is CompleteEvent<T>
  }
  if (ApiClient.isErrorEvent(event)) {
    // event is ErrorEvent
  }
}

Error Handling in Streams

Errors can come from two sources:
  1. Error events — The API returns an error during processing
  2. Exceptions — Network failure, timeout, or abort
try {
  const stream = await ai.generateGraphStream({ config, userPrompt });

  for await (const event of stream) {
    if (event.type === 'error') {
      // API-level error
      if (event.retryable) {
        // Offer retry option
      }
      throw new Error(event.error);
    }

    if (event.type === 'complete') {
      return event.data;
    }
  }
} catch (error) {
  if (error.name === 'AbortError') {
    // User cancelled
    return;
  }
  // Network or other error
  console.error('Stream failed:', error);
}
See Error Handling for details on error codes and retry behavior.