Streaming routes
Read streaming routes as async iterables through the DALP TypeScript SDK, with decoded events, cancellation, and error handling.
Overview
Some things do not fit in one response. A deployment reports progress across several minutes; a transaction moves through states; an activity feed has no last item. Those routes answer with a stream of events rather than a body, and the platform sends them as server-sent events.
In the SDK they are ordinary nodes on the same tree. What differs is what comes back: an async iterable of decoded frames instead of a promise of one payload.
for await (const frame of dalp.api.transactionsV2.statusStream({ transactionId })) {
console.log(frame);
}Streaming operations have an async-iterable return type in the route tree.
Reading a stream
The loop is the whole interface. Frames arrive in order and are parsed as JSON when they carry JSON.
async function watchDeployment(deploymentId: string) {
for await (const frame of dalp.api.platform.organizations.organizationsDeployStream({ deploymentId })) {
render(frame);
}
// The loop ends when the platform closes the stream.
}Breaking out of the loop closes the connection. So does an early return or a thrown error inside the body, because the iterator's cleanup runs either way and cancels the underlying read. You do not need to close anything by hand.
Cancelling
Pass an AbortSignal the same way you would on any other call:
const controller = new AbortController();
for await (const frame of dalp.api.assetPortfolioV2.v2.actions.stream({}, { signal: controller.signal })) {
render(frame);
}In React, abort in the effect's cleanup so a component that unmounts mid-stream does not hold a connection open:
useEffect(() => {
const controller = new AbortController();
void (async () => {
try {
for await (const frame of dalp.api.assetPortfolioV2.v2.actions.stream({}, { signal: controller.signal })) {
setLatest(frame);
}
} catch (error) {
if (!controller.signal.aborted) setError(error);
}
})();
return () => controller.abort();
}, []);The aborted check matters. An aborted stream rejects with the abort reason, and treating your own cleanup as a failure is how an unmount shows an error toast.
What the SDK does not do
It does not reconnect. One call opens one connection. When the stream ends, it has ended, and if it ended before the terminal event you were waiting for, that is a fact your code sees rather than a gap it never learns about.
If the connection ends before a terminal event, read the current status before deciding whether to reconnect. A missed event does not establish the transaction outcome.
This matters on a settlement stream. A transaction that reaches COMPLETED while your connection is down, followed by a reconnect that resumes from the live edge, leaves you waiting forever for an event that has already been sent. That is why the reconnecting wrapper below re-reads rather than resumes, and why a stream is a way to watch progress rather than the thing you trust for the verdict: the status route is.
It does not classify frames. A frame is data. Whether a given frame is terminal, and whether terminal means success, is domain knowledge the route defines and your code applies.
Reconnecting
When you want a stream that survives a dropped connection, wrap the call. The SDK ships the wrapper the Console uses:
import { consumeSseUntilTerminal, SseLifecycleError } from "@settlemint/dalp-sdk";
const outcome = await consumeSseUntilTerminal({
connect: async (signal) => ({
stream: dalp.api.transactionsV2.statusStream({ transactionId }, { signal }),
}),
classify: (frame) => {
if (frame.status === "COMPLETED") return "success";
if (frame.status === "FAILED") return "failure";
return "continue";
},
signal: outerSignal,
});It reconnects with backoff while frames are non-terminal, stops on the verdict classify returns, and throws SseLifecycleError when it gives up, with a reason distinguishing an abort from a terminal failure from a transport failure.
The reconnect budget is wall-clock rather than a count of attempts, and that choice is worth understanding before you tune it. Counting attempts sounds equivalent and is not: when every failure is instant, four attempts is about six seconds of tolerance, so a backend stalling for ten seconds exhausts the budget before it has recovered. A window measures the thing you actually care about. The window is anchored at the first failure of a run, and restarts only once a connection has stayed up long enough to count as recovered. A long-lived stream that dies late therefore gets a full window, rather than the remains of one spent hours earlier.
Receiving events is deliberately not the test for recovery. A stream that replays its opening frame on every subscription would clear the failure streak on each attempt and reconnect forever, which is the failure mode this rule exists to prevent.
Which routes stream
The typed tree is the answer: a streaming route is typed as returning an async iterable, so calling one and awaiting it as a body is a type error rather than a runtime surprise. As a rough map, streams exist for deployment progress, migration progress, transaction status, block progress, the actions feed, log tailing, index snapshots, and token and venue creation.
The full list, with each route's frames, is in the API reference, where every streaming route is documented beside the routes that return a body.
Next
Wiring calls into a React application: TanStack Query.
Mutations and settlement
A write that touches the chain returns the settled resource or the platform's 202 handle; retry safety, the acting context, and errors are yours to handle.
TanStack Query
Every route carries its own query and mutation factories, so a cache key derives from route identity and canonicalized input instead of hand-written keys.