Skip to content
Next — unreleased

Polling works, and for most applications it is enough. When it is not, the context can subscribe to account updates instead.

Subscriptions are context-owned, which is the important detail: an update arriving over a subscription goes through the same validation and lands in the same cache as a normal read. A subscribed fleet and a fetched fleet are the same cached snapshot, not two views that can disagree.

Inject an application-owned Kit RpcSubscriptions<AccountNotificationsApi> into createRpcSubscriptionProvider from @aephia/atlas-kit/client. The factory does not create a socket, accept a URL, or retain credentials. Nothing subscribes until you explicitly call ctx.data.subscribe() or a capability’s watcher.

Illustrative setup with an existing Kit connection and account provider:

import type { AccountNotificationsApi, RpcSubscriptions } from '@solana/kit';
import {
createRpcSubscriptionProvider,
createSageContext,
} from '@aephia/atlas-kit/client';
import type { AccountProvider } from '@aephia/atlas-kit/client';
function createLiveContext(
rpcSubscriptions: RpcSubscriptions<AccountNotificationsApi>,
provider: AccountProvider,
) {
return createSageContext({
cluster: 'zink-ptr',
provider,
subscriptions: createRpcSubscriptionProvider({
rpcSubscriptions,
identity: { kind: 'websocket', name: 'application-live' },
backoff: { initialMs: 250, maxMs: 30_000 },
}),
});
}
// Pass your existing connections to createLiveContext(), then register through
// ctx.data.subscribe() or watchFleet(). Call ctx.dispose() on teardown.

The identity is optional and defaults to websocket / rpc-subscriptions. Use only a non-secret label: identities are public provenance, never a place for endpoints or authentication material. Backoff defaults to 250–30,000 ms; supplied values must be positive safe integers with maxMs >= initialMs or the factory throws synchronously.

The adapter requests base64 data at the registration commitment (confirmed by default). Initial registration failure rejects with a sanitized ProviderError and no automatic retry. Once registered, stream termination or failure starts one retry loop per registration. Each wait uses equal jitter between half and all of the capped exponential delay, rounded down to at least 1 ms. Successful re-registration resets the delay and emits reconnected before account events. Failed attempts never emit it. Transport exceptions and abort reasons are not copied into errors or diagnostics. Listener exceptions do not trigger retries.

Explicit unsubscribe, caller abort, and context disposal cancel pending waits and release each Kit registration via its abort signal. Iterator cleanup is best-effort, so a transport that ignores cancellation cannot block teardown or deliver later updates. No transport can guarantee updates during an outage; the context closes that gap with its existing direct-read resynchronization.

Bring your own AccountSubscriptionProvider if your transport needs a different integration. It must honor cancellation and emit reconnected only after a successful registration; keep validation, caching, and resynchronization in the context rather than duplicating them in the provider.

Without shared identity, a live update and a fetch race each other, and which one your application sees depends on timing. Here they cannot diverge: both write to the same cache slot under the same key.

Updates are translated the same way reads are, so a subscription delivers the SDK’s own snapshot types rather than raw account bytes.

Connections drop. When one does, the SDK resynchronises rather than resuming blindly, because the gap between disconnect and reconnect is exactly where missed updates hide.

Reconnects coalesce: a burst of drops produces one resynchronisation rather than one per drop, and a reconnect that arrives while another is in flight is folded into it rather than queued behind it.

Subscriptions are cancellable, and disposing a context cancels everything it owns. That matters in a browser, where a component unmounting must not leave a socket delivering updates into a cache nobody reads.

Prefer polling with a sensible maxAge unless you have a reason not to. It is simpler, it degrades more gracefully, and for state that changes on the scale of minutes it is indistinguishable to a user.

Subscribe when you are tracking something that genuinely moves — an active fleet, an order book — and staleness is visible.