Skip to content
IC Reactor

@ic-reactor/core

@ic-reactor/core is the framework-agnostic foundation of IC Reactor. It gives you the runtime building blocks for agent management, identity wiring, TanStack Query integration, and typed canister interaction without depending on React.

  • You need ClientManager, Reactor, or DisplayReactor in non-React code.
  • You want to use IC Reactor in loaders, services, scripts, or framework adapters.
  • You are building your own abstractions on top of IC Reactor’s runtime layer.
Terminal window
pnpm add @ic-reactor/core @icp-sdk/core @tanstack/query-core
import { ClientManager, Reactor } from "@ic-reactor/core"
import { QueryClient } from "@tanstack/query-core"
import { canisterId, idlFactory, type _SERVICE } from "./declarations/backend"
const queryClient = new QueryClient()
export const clientManager = new ClientManager({
queryClient,
})
await clientManager.initialize()
export const backend = new Reactor<_SERVICE>({
clientManager,
idlFactory,
name: "backend",
// Required in Node, where the constructor otherwise throws. A browser page
// may omit it only when the ic_env cookie is trusted there (a local replica,
// or `allowEnvConfig: true`).
canisterId,
})
const greeting = await backend.fetchQuery({
functionName: "greet",
args: ["World"],
})

Whether a failed call is worth retrying is decided by what failed, and most failures are deterministic. @ic-reactor/core exports the classification (@ic-reactor/react re-exports these functions):

import { reactorRetry, isRetryableReactorError } from "@ic-reactor/core"
import { QueryClient } from "@tanstack/query-core"
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: reactorRetry } },
})

isRetryableReactorError(error) returns true only for a CallError whose cause is an agent-level fault — a transport, protocol, or certificate failure, an HTTP 5xx, 408 or 429 answer, or a SysTransient/SysUnknown replica rejection. A canister Err variant (CanisterError), a ValidationError, a Candid encode failure (no request was ever made), and a decode failure on the reply are never retryable: identical input — or the identical reply — produces the identical failure. Neither is any other HTTP 4xx answer, such as the 400 for an expired delegation or a 403: it refuses the request itself, and the agent has already sent it retryTimes more times (3 by default) before reporting it. The status is read from the agent’s HttpErrorCode by its name and status, so an error from another copy of @icp-sdk/core classifies the same way. Within agent errors the bias is toward retrying — an unrecognized kind, or a rejection whose code cannot be read, still retries, so an unfamiliar transport-level fault is never silently made fatal.

reactorRetry(failureCount, error) is a ready-made TanStack Query retry predicate on top of it — up to 3 retries (4 attempts in total) for retryable errors, none otherwise, and always false on the server so a failed prefetch keeps failing fast. defineReactor in @ic-reactor/react already installs it as the query default on the QueryClient it creates; a QueryClient you construct yourself opts in as shown above.

An update call is different: each attempt is a new call that the canister runs, so only a failure that proves the first attempt never ran is safe to retry. isRetryableUpdateError(error) accepts just that, a SysTransient rejection, and reactorUpdateRetry(failureCount, error) retries it up to 3 times in a browser. A query of an update method with no retry of its own retries this way within the QueryClient’s retry default, as Reactor.getQueryRetry(functionName, queryKey) returns; getQueryOptions and fetchQuery apply it. See Update Methods in Queries.

Identity attributes are not part of @ic-reactor/core. IdentityAttributesManager in @ic-reactor/react requests signed OpenID identity attributes through @icp-sdk/auth v8 or v10:

import {
AuthenticationManager,
IdentityAttributesManager,
identityAttributeKeys,
} from "@ic-reactor/react"
// The `clientManager` and `backend` the Quick Start's module exports
import { backend, clientManager } from "./reactor"
const authentication = new AuthenticationManager({ clientManager })
const identityAttributes = new IdentityAttributesManager(authentication)
const result = await identityAttributes.requestOpenId({
// Pass the nonce as a callback: awaiting it first ends the user gesture
// and the browser blocks the Internet Identity window.
nonce: () => backend.callMethod({ functionName: "register_begin" }),
openIdProvider: "microsoft",
keys: ["email", "name"],
})
await backend.callMethod({
functionName: "register_finish",
args: [
{
data: result.signedAttributes.data,
signature: result.signedAttributes.signature,
},
],
})

Use openIdProvider: "google", "apple", "microsoft", or a provider issuer URL. For lower-level requests, build scoped keys with identityAttributeKeys() and pass them to identityAttributes.request().

@ic-reactor/core/testing is a separate entry point for tests. It installs a fake replica that answers your agent with canisters written as typed handlers, so ClientManager, Reactor and DisplayReactor run unmodified with no replica:

import {
createTestCanister,
installFakeReplica,
} from "@ic-reactor/core/testing"
import { canisterId, idlFactory, type _SERVICE } from "./declarations/backend"
const replica = installFakeReplica({
canisters: {
[canisterId]: createTestCanister<_SERVICE>(idlFactory, {
greet: ([name]) => `Hello, ${name}!`,
}),
},
})
// Build the ClientManager afterwards, with agentOptions: { host: replica.host }.

The main entry never imports it. See the Testing guide.