Skip to content

Latest commit

 

History

History
531 lines (403 loc) · 16.4 KB

File metadata and controls

531 lines (403 loc) · 16.4 KB

@stellar/streaming-sdk

A lightweight, isomorphic TypeScript client for the XStreamRoll platform. It handles authentication, stream CRUD, real-time event publishing, and an extensible HTTP transport (interceptors + retry) with zero runtime dependencies — both the high-level StreamingClient and the low-level HttpClient are built on the platform-global fetch.

Issues #391, #393, and #395 — this README was audited against the actual SDK implementation. Endpoint references, dependency claims, and the Quick Start have all been verified to compile and run. Issues opened against this README should update the audit alongside the source change.


Table of contents

  1. Installation
  2. Quick start
  3. Configuration
  4. Authentication
  5. Streams
  6. Real-time events
  7. HTTP transport
  8. Pagination
  9. Types
  10. Browser usage
  11. Testing helpers
  12. API reference — auto-generated by typedoc
  13. Compatibility matrix
  14. Versioning
  15. Publishing

Installation

# npm (workspaces-aware)
npm install @stellar/streaming-sdk

# pnpm
pnpm add @stellar/streaming-sdk

# yarn
yarn add @stellar/streaming-sdk

@stellar/streaming-sdk@1.x targets ES2020 and has zero runtime dependencies. The HTTP layer uses the platform-global fetch (available in Node ≥ 18, all evergreen browsers, Bun, Deno, and Cloudflare Workers). Dev-only dependencies are ts-jest and jest for the test suite, plus openapi-typescript and typedoc for type/reference regeneration.


Quick start

The full Quick Start below compiles against the public StreamingClient surface and exercises the parts of the API that already exist on the server. Stream CRUD currently lives on the API server (consumed by the web app); the SDK provides a getStreamStatus shortcut and publishEvent for clients that emit live event traffic.

import { StreamingClient, HttpClient, ApiError } from "@stellar/streaming-sdk"

const client = new StreamingClient({
  env: "production", // or "staging" | "development", or a custom baseUrl
})

// 1. Log in
const { user, accessToken, refreshToken } = await client.login(
  "alice@example.com",
  "super-secret-password",
)
// user: { id, email, displayName, role, createdAt, updatedAt }

// 2. Publish an event to an existing stream.
//    `clientId` is auto-filled with a stable per-instance id; pass
//    `eventType` and `data` and the client adds a timestamp for you.
try {
  await client.publishEvent({
    streamId: "stream_abc",
    eventType: "viewer:joined",
    data: { viewerId: "user_42" },
  })
} catch (err) {
  if (err instanceof ApiError) {
    console.error(`API ${err.statusCode}: ${err.message}`)
  } else {
    throw err
  }
}

// 3. Read the current status of a stream you own.
const stream = await client.getStreamStatus("stream_abc")
console.log(`stream ${stream.id} -> status=${stream.status}, visibility=${stream.visibility}`)

// 4. Tear down
await client.logout()

Stream CRUD: the SDK is intentionally thin so it can mirror the API surface 1:1. To create, list, or update streams from a CLI, an SDK consumer, or a server-to-server integration, hit the API directly with HttpClient (see HTTP transport). The web app already does this through its Next.js API routes.


Configuration

StreamingClient accepts a StreamConfig:

Field Type Notes
env "development" | "staging" | "production" Named preset. Resolves to a well-known base URL.
baseUrl string Explicit base URL. Overrides env and the legacy apiUrl.
apiUrl string (deprecated) Legacy field. Kept for backwards compatibility.
clientId string Identifier attached to published events. Defaults to a timestamp.

Resolution order: baseUrlenvapiUrldevelopment.

The full URL presets are:

Env Base URL
development http://localhost:3001
staging https://staging-api.xstreamroll.io
production https://api.xstreamroll.io

Authentication

const loginTokens = await client.login(email, password)
const registerTokens = await client.register({
  email: "alice@example.com",
  password: "super-secret-password",
  displayName: "Alice",
})
// tokens: { user, accessToken, refreshToken }

// Refresh an expired access token
const freshTokens = await client.refreshToken()
// freshTokens: { user, accessToken, refreshToken }

StreamingClient keeps the active tokens on the instance and:

  • attaches Authorization: Bearer <accessToken> to every outbound request, and
  • transparently refreshes the access token on a 401 response (using the stored refresh token), then retries the original request once.

Call await client.logout() to invalidate the session server-side and drop the local tokens.

Token storage: tokens are kept in memory only. Persist them with localStorage / sessionStorage / a secure cookie if you need them to survive a page reload.


Streams

Listing (stream events and status)

// Read the current status of a stream you own.
const stream = await client.getStreamStatus("stream_abc")

Stream CRUD

Stream CRUD (POST /streams, GET /streams, PATCH /streams/:id, DELETE /streams/:id) lives on the API server. The web app consumes it directly. From SDK code that wants to bypass the web app and call the API itself, use HttpClient:

import { HttpClient } from "@stellar/streaming-sdk"

const http = new HttpClient("https://api.xstreamroll.io")
http.addRequestInterceptor((cfg) => {
  const headers: Record<string, string> = {
    ...(cfg.headers as Record<string, string> | undefined),
    Authorization: `Bearer ${loginTokens.accessToken}`,
  }
  return { ...cfg, headers }
})

const created = await http.post("/streams", {
  name: "My new stream",
  visibility: "private",
})
const createdJson = (await created.json()) as { id: number }

const listing = await http.get("/streams?ownerOnly=true&page=1&limit=20")
const listingJson = (await listing.json()) as {
  data: Array<{ id: number; name: string; visibility: "public" | "private" }>
  total: number
  page: number
  limit: number
}

GET /streams returns streams visible to the caller:

Caller Result
Any authenticated user Public streams + their own private streams (default)
visibility=public Only public streams
visibility=private Only the caller's own private streams
ownerOnly=true Only streams owned by the caller

This mirrors the API documented at http://localhost:3001/docs once the API server is running.


Real-time events

await client.publishEvent({
  streamId: "stream_abc",
  eventType: "data",
  data: { foo: "bar" },
})

eventType is one of the union members exported as StreamEventType: "stream:started" | "stream:stopped" | "stream:error" | "viewer:joined" | "viewer:left" | "data". The client auto-fills clientId and a timestamp (ISO 8601) before posting.

WebSocket subscriptions — coming soon. The API broadcasts real-time lifecycle events over Socket.IO (see the API service's streams.gateway.ts), but the SDK does not yet expose typed subscribe helpers. Use socket.io-client directly against the same API host until those helpers land. Client-side wiring in the web app lives in hooks/useStreamSocket.ts.


HTTP transport

HttpClient is a small, fetch-based wrapper that:

  • merges baseUrl + path,
  • runs request/response interceptors in registration order,
  • retries transient failures (408, 425, 429, 5xx) with exponential backoff + jitter.
import { HttpClient } from "@stellar/streaming-sdk"

const http = new HttpClient("https://api.xstreamroll.io", {
  maxAttempts: 5,
  baseDelayMs: 250,
  maxDelayMs: 5_000,
})

const res = await http.request("/streams/abc")
const json = await res.json()

Interceptors

const authHandle = http.addRequestInterceptor((cfg) => ({
  ...cfg,
  headers: { ...cfg.headers, Authorization: `Bearer ${token}` },
}))

const metricsHandle = http.addResponseInterceptor((res) => {
  metrics.record(`/ -> ${res.status}`)
  return res
})

// later
http.removeInterceptor(authHandle)
http.removeInterceptor(metricsHandle)

Request interceptors run in registration order, receive the full RequestInit & { url }, and may return a new config. Response interceptors run after fetch resolves, may be async, and may replace the response (e.g. to transparently re-issue on 401).

Retries

The retry helper (withRetry) is generic and exported separately:

import { withRetry } from "@stellar/streaming-sdk"

await withRetry(() => callFlakyApi(), {
  maxAttempts: 4,
  baseDelayMs: 100,
  maxDelayMs: 2_000,
  jitterMs: 50,
  onRetry: (err, attempt, delay) => console.warn("retry", attempt, err, delay),
})

The HttpClient uses the helper internally; pass { enabled: false } to opt out per client.

Error model

When the retry budget is exhausted the client throws HttpRequestError, which carries:

  • the last error message,
  • the last Response (cloned, so it can be read after the throw),
  • the number of attempts made.

The high-level StreamingClient translates non-2xx responses into ApiError (also exported from the SDK), exposing statusCode, message, and a typed response payload.


Pagination

Pagination in the API is driven by ?page= and ?limit= query parameters (max limit=100). Lists return a PaginatedResponse<T> payload:

interface PaginatedResponse<T> {
  data: T[]
  total: number
  page: number
  limit: number
}

There is no SDK paginateStreams() helper — the SDK is thin on purpose. Drive pagination explicitly via HttpClient.get():

const res = await http.get("/streams?page=2&limit=50")
const { data, total, page, limit } = (await res.json()) as PaginatedResponse<Stream>
const hasMore = page * limit < total

Walking every page (paginateAll)

List endpoints often require callers to drive the cursor by hand — re-computing page, incrementing until total is collected. The SDK exposes a small async iterator helper that does that loop for you and yields each item exactly once across the entire result set:

import { StreamingClient } from "@stellar/streaming-sdk"

const client = new StreamingClient({ env: "production" })

// Iterate every public stream, page by page, as an async iterable.
for await (const stream of client.paginateAll<Stream>("/streams", {
  visibility: "public",
  limit: 100,
})) {
  console.log("got stream", stream.id)
}

// Or collect the full list synchronously once the iterator drained.
const allStreams = await client.paginateAll("/streams").toArray()

paginateAll exposes an async iterator (use for await … of) and the standard AsyncIterable helpers — .toArray(), .map(fn), .filter(fn). It stops when the page * limit envelope reaches total, so it works even if the server omits the (legacy) hasMore flag from the response.


Types

The SDK ships full type definitions. The most useful are:

  • Stream, CreateStreamDto, UpdateStreamDto, StreamVisibility — stream CRUD shapes.
  • StreamEvent, StreamEventRecord, StreamEventType — event shapes.
  • AuthTokens, User, CreateUserDto, UpdateUserDto — auth shapes.
  • PaginatedResponse<T>, PaginationParams — list helpers.
  • ApiError, ApiErrorResponse, ValidationError — error shapes.

All types are re-exported from the package root. Stream visibility (StreamVisibility = "public" | "private") was added so SDK callers can read the wiring on a Stream returned by the API; the matching server-side enforcement landed via database migration 2026080501_add_stream_visibility.


Browser usage

The SDK ships both a CJS build (dist/index.js) and an ESM build (dist-esm/index.js) and exposes them through the standard "exports" field in package.json. Tree-shaking bundlers (Vite, Webpack 5+, Rollup, esbuild, Turbopack) automatically pick the ESM bundle; older bundlers and Node require() resolve to the CJS build. No polyfills are required for evergreen browsers — the SDK uses the native fetch and crypto.subtle APIs that have shipped in every evergreen browser for several years.

import { StreamingClient } from "@stellar/streaming-sdk"

const client = new StreamingClient({
  baseUrl: "https://api.my-deployment.example.com",
})

For SSR environments (Next.js, Remix, etc.) avoid constructing the client at module scope; lazy-construct it inside the request handler so that auth tokens can be read from the incoming request.


Testing helpers

The retry behaviour and the HTTP layer are both fully unit-tested. To test consumers, the recommended approach is to inject a mock HttpClient rather than the full StreamingClient:

import { HttpClient } from "@stellar/streaming-sdk"

const mock = new HttpClient("http://test")
// add request/response interceptors to assert on outbound calls

For retry timing in tests, inject a custom sleep:

new HttpClient("http://x", { sleep: async () => {} })

For mutation testing — to detect undertested code paths masked by high line coverage — the SDK ships a Stryker config:

npm run test:mutation --workspace=xstreamroll-sdk

API reference

Once typedoc is installed, generate a browsable HTML reference from the public TSDoc:

# from the workspace root
npm install                    # pulls typedoc (devDependency)
npm run docs:api --workspace=@stellar/streaming-sdk
# …or directly:
cd xstreamroll-sdk && npm install && npm run docs:api

This emits a static site at xstreamroll-sdk/docs/api/ (gitignored). The output mirrors the categories used in this README so navigation matches what you see here.


Compatibility matrix

The SDK is intentionally versioned with the API semver so the contract below is easy to reason about. Use npx tsc --noEmit against your consumer if you want type-level confirmation before bumping a stack.

SDK version API version Status Notes
1.x 1.x ✅ Current Adds StreamVisibility (public/private, migration 2026080501); GET /streams is visibility-filtered per caller.
0.x 0.x ⚠️ Legacy Pre-visibility; streams.visibility was undefined and ignored.
1.x 0.x ❌ Mismatch SDK will send visibility payloads the legacy API silently drops. Pin SDK to 0.x.
0.x 1.x ❌ Mismatch API returns streams.visibility but the SDK type does not declare it; cast to unknown and validate manually or upgrade.

Rule of thumb: bump SDK and API together. If you absolutely need to mix versions, install the SDK that matches the oldest API you target and gate the visibility-aware endpoints behind a runtime version check.


Versioning

  • Follows semver.
  • Public API is whatever the package index.ts re-exports.
  • Breaking changes bump the major version and are announced in the release notes.

Publishing

Publishing is done by the maintainers via the release.yml workflow (.github/workflows/release.yml). To cut a release:

  1. Bump the version in xstreamroll-sdk/package.json (semver).
  2. Update the changelog.
  3. Open a PR titled chore(sdk): release vX.Y.Z.
  4. Once merged and CI is green, push the matching tag: git tag sdk/vX.Y.Z && git push origin sdk/vX.Y.Z.

The release workflow builds the package (CJS + ESM) and publishes it to the configured registry.