Skip to main content
0.0.x

Consume durable streams in Expo

Use the SDK's focused consumer entries to render server-owned execution state in React Native. The phone reads the durable log; workers, sagas, and triggers continue on the server when it disconnects.

Connect a typed collection

Import createStreamCollectionV1 from @netscript/sdk/streams/collections. Pass the full stream URL, an injected streaming fetch, a State Protocol type, an entity validator (parse), and a primary-key extractor (getKey). Use @netscript/sdk/streams/react's useStreamLiveQueryV1 inside a React component with binding.collection. It subscribes through TanStack’s collection live-query overload.

The binding uses the same TanStack DB engine as the web streams surface. Committed entity changes update live queries directly, without query invalidation, refetch, or polling. It expects full entity state for insert, update, and upsert operations. Delete changes omit value. Events for other collection types are ignored. Parsing and key agreement finish before writes begin; invalid entities stop the source and reject binding.done.

Keep the binding in a screen-owned effect, and call await binding.dispose() during teardown. Dispose is idempotent, closes the source, cancels transport work, and releases the collection. Observe binding.done for terminal consumption failures. Before readiness, collection.preload() rejects with a fatal source or entity-validation failure instead of waiting forever. The failed collection is cleaned up and its status becomes cleaned-up; collection.utils.getError() retains the error, and the React hook returns status: 'error', isLoading: false, and error. The hook can render that error after a remount. Calling cleanup() or dispose() ends the single-use binding: starting sync or subscribing again requires a new binding. Successful readiness still waits for a control reporting upToDate or streamClosed.

StreamCollectionV1<T> is the SDK's typed read/lifecycle port over the upstream collection. Before using TanStack's query builder, narrow it with isCollection and retain the validated entity type in a typed local. Filtered and projected queries then retain their types without assertions:

import type { StreamCollectionV1 } from '@netscript/sdk/streams/collections';
import { createLiveQueryCollection, eq, isCollection } from '@tanstack/db';
import type { Collection } from '@tanstack/db';

declare const executions: StreamCollectionV1<{ id: string; status: string }>;
if (!isCollection(executions)) throw new TypeError('Expected a TanStack collection');
const collection: Collection<{ id: string; status: string }, string> = executions;
const running = createLiveQueryCollection({
  query: (q) =>
    q.from({ execution: collection })
      .where(({ execution }) => eq(execution.status, 'running'))
      .select(({ execution }) => ({ id: execution.id, status: execution.status })),
});
await running.cleanup();

Choose the materializer

The Expo binding and Fresh StreamDB are separate adapters over TanStack DB. Sharing the query engine does not make their schemas, transports, or failure states interchangeable:

Concern SDK createStreamCollectionV1 Fresh createNetScriptStreamDB
Input contract One State Protocol type, full-entity parse, and getKey; other types ignored Multi-type StateSchema passed to @durable-streams/state createStreamDB
Transport Existing DOM-independent SDK fetch source and stream-core v1 framing/binding; full URL and host fetch required @durable-streams/client transport with NetScript URL/auth resolution and the Fresh recovery adapter
Recovery Non-success HTTP responses retry with refreshed credentials until cancellation; finite buffer and delay caps A finite outage retry budget; permanent auth/protocol failures stop immediately
Failure done and pending preload() reject; collection is cleaned up; hook exposes error and leaves loading Recovery adapter exposes failed; collections belong to the upstream StreamDB
Lifecycle binding.dispose() is awaitable and idempotent; a cleaned-up collection cannot restart its source Database-level preload, status, and stop/dispose hooks own the default consumer

Use the SDK adapter for an injected Expo fetch and a single validated entity type. Use Fresh StreamDB for its schema-based web database and recovery contract. Both support TanStack query composition; neither adapter supplies the other adapter's lifecycle or recovery policy. This documented boundary is part of the API contract, rather than a claim that the two materializers are identical.

Inject Expo fetch

The Expo reference app contains the complete screen, typed execution contract, and a checked expo/fetch wrapper. Its data layer needs only NetScript imports. The host wrapper forwards GET, headers, and the abort signal; Expo's request body type is narrower than WHATWG RequestInit, so do not forward arbitrary request bodies. FetchResponse satisfies the SDK's StreamFetchResponseV1 subset without a cast and its body is read incrementally. See Expo's streaming fetch documentation.

Use authHeaders to resolve credentials on every connection. The underlying injected source keeps finite heartbeat, framing, pending-event, and reconnect-delay budgets. Persistent non-success HTTP responses retry until cancellation; callers should abort when their session expires.

Resume and readiness

Data waits for a validated control before materialization. Reconnect uses the last committed opaque offset, plus SSE Last-Event-ID when present. A disconnect before control discards the incomplete batch; replay re-delivers it once the server honors that offset. The collection stays mounted across connections. binding.snapshot() exposes replay progress after successful batch application.

A new app instance starts at offset=-1 to reconstruct the collection. Reusing a cursor without its materialized rows skips earlier state. Retain rows and cursor together if you add persistence. This binding does not synthesize state missing from a server's retained log.

Verify native portability

The reference includes an executable Hermes probe for AbortSignal.throwIfAborted, AbortSignal.reason, streaming TextDecoder, URL.searchParams including set, and ReadableStream.getReader. Run the actual native app, inspect its probe output, and force a stream disconnect while observing execution updates. Deno tests prove the binding works without EventSource; they do not prove native runtime behavior. Native Hermes and Expo network acceptance remain pending until that device run is recorded.