Query once, then follow live
A normal query followed by a separate stream connection has a race: a change
can commit after the query but before the stream starts. queryAndFollow()
returns the snapshot and its opaque change boundary atomically. Passing that
boundary to subscribe({ after }) closes the gap.
First follow the source installation, including the SDK setup. Stop any other node using API port 8080 before this example. The supplied profile retains the latest USDC/WETH pool state and a bounded change stream; its public Xatu history needs no provider key:
LEANI_SDK_DIR="$(mktemp -d)"cp "$LEANI_SOURCE/examples/sdk-stream/node.toml" "$LEANI_SDK_DIR/leani.toml"cd "$LEANI_SDK_DIR"leani doctor --jsonleani backfill --processor usdc-weth-latest --from-block 17000000 --to-block 17000999leani serveIn another terminal, from the repository root:
cd "$LEANI_SOURCE"bun examples/sdk-stream/index.tsExpect Loaded 1 pools. The process then waits for more changes; this
historical-only profile has no live source, so silence after the seed is expected.
Press Ctrl-C to stop. The example keeps the price map in memory; for a durable
destination, continue with PostgreSQL.
This historical-only profile lets you inspect the seed and resume boundary. For ongoing P2P updates, enable live execution and verified finality as described in the Uniswap subscription guide.
import { applyEntityChange, createLeaniClient, isLiveGapChange, LeaniError, type GenericSnapshotPage, type LeaniClient, type UniswapPoolPrice,} from "@smart-byte/leani-sdk";
// Run examples/sdk-stream/node.toml first (see the guide).export async function followPrices( leani: LeaniClient = createLeaniClient({ baseUrl: "http://127.0.0.1:8080" }), prices = new Map<string, UniswapPoolPrice>(), signal?: AbortSignal,): Promise<void> { const processor = "usdc-weth-latest"; const collection = "uniswap.pools.current"; const target = { put: (key: string, value: UniswapPoolPrice) => { prices.set(key, value); }, delete: (key: string) => { prices.delete(key); }, }; while (!signal?.aborted) { try { const snapshot = await leani.processors.queryAndFollow<UniswapPoolPrice>( processor, collection, { signal }, ); const seed = new Map<string, UniswapPoolPrice>(); try { let page: GenericSnapshotPage<UniswapPoolPrice> = snapshot; while (true) { for (const entity of page.data) seed.set(entity.key, entity.data); if (!page.nextCursor) break; page = await leani.processors.queryEntities<UniswapPoolPrice>( processor, collection, { cursor: page.nextCursor, signal }, ); } } finally { // Cleanup gets its own deadline even if the subscription was cancelled. await leani.processors.releaseSnapshot(processor, snapshot.snapshotId) .catch((error: unknown) => { if (!(error instanceof LeaniError && error.status === 404)) throw error; }); } prices.clear(); for (const [key, value] of seed) prices.set(key, value); console.log(`Loaded ${prices.size} pools`); for await (const change of leani.processors.subscribe<UniswapPoolPrice>( processor, { after: snapshot.boundaryCursor, signal }, )) { // Both apply and undo carry the entity mutation to perform. await applyEntityChange(target, change); if (change.operation === "finalized" && !isLiveGapChange(change)) { console.log("finalized through", change.data.throughBlock); } } } catch (error) { if (signal?.aborted) return; // ResetRequiredError from SSE is also a LeaniError with cursor_expired. if (!(error instanceof LeaniError) || !["cursor_expired", "query_snapshot_expired"].includes(error.code)) throw error; console.log("Retention moved; rebuilding the snapshot"); } }}
if (import.meta.main) await followPrices();- Follow every
nextCursorto materialize the complete stable snapshot. - Release the snapshot when the local seed is committed.
- Start the resumable stream at
boundaryCursor. - Apply both
applyandundo; an undo envelope carries the inverse entity mutation. - Treat
finalizedas an explicit transition andreset_requiredas a rebuild instruction. The SDK throwsResetRequiredErroron an SSE reset; expired HTTP cursors throwLeaniErrorwithcode: "cursor_expired". Never infer either transition from elapsed time.
The page limit only controls transport size. Creating the first page copies
the entire matching collection under the store writer lock. Defaults cap each
snapshot at 100,000 rows / 64 MiB and all outstanding snapshots together at 32
snapshots / 128 MiB (including key and row overhead). A request over the
per-snapshot caps or outside coverage is rejected before it takes the writer
lock. Physical store admission also applies. Release snapshots promptly; the
node expires them after five minutes and cleans expired rows every 30 seconds.
query_snapshot_capacity returns retryable HTTP 503 when aggregate admission
is full. Very slow readers can outlive the retained stream and must rebuild, as
the example does.
Built by Smart-Byte · Legal notice (Impressum) · Privacy