raf-effect-atom
Loader verbs
prefetch, ensure and fetch start atoms from route loaders before anything renders, so a page's requests run at the same time instead of one after another.
When a component starts its own data, nested components wait for each other: a page with an issue, its assignee and its comments makes three requests one after another, a request waterfall. React's guidance is to start fetching before rendering, in the router's loader. raf-effect-atom adds three verbs that start atoms there, in the registry the page renders from.
// Starts the atom and returns its handle at once.
const assigneeHandle = const atoms: Loader.Loaderatoms.Loader.prefetch: <(issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, issueId: number & Brand<"IssueId">) => Handle<(issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>prefetch(const assignee: (issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee, const id: number & Brand<"IssueId">id);
// Waits for a result, unless the atom already has one.
const issueHandle = await const atoms: Loader.Loaderatoms.Loader.ensure: <(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">) => Promise<LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>>ensure(const issue: (id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue, const id: number & Brand<"IssueId">id);
// Refreshes the atom if it has a result, and waits for the new one.
await const atoms: Loader.Loaderatoms.Loader.fetch: <(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">) => Promise<LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>>fetch(const issue: (id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue, const id: number & Brand<"IssueId">id);
Each verb takes a source, an atom family or one atom, and the family's arguments: atoms.ensure(issue, id) starts issue(id).
A source can be a derived atom with no serialization key that points at a serializable one with Effect's initialValueTarget, such as the issue's title below. The verbs start the atom it points at, the one useAtomResult reads it through: the lease, the handle's key and what the server sends are that atom's, and ensure and fetch wait for the derived atom's own result. The server sends a result still loading as a promise only when that atom holds an AsyncResult; one that holds another value, such as an Option snapshot, crosses with the value it has when the loaders finish, so point a derived atom at an AsyncResult atom, or ensure it.
To derive one, assign initialValueTarget to the atom it reads. Atom.map and Atom.mapResult set none, and Atom.transform's initialValueTarget option follows the atom's own target to the end of its chain, which for an AtomRpc or AtomHttpApi query with reactivityKeys is an inner atom without a key:
// The issue's title alone. The verbs start, and the server sends, issue(id).
const const issueTitle: (id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<string, RpcClientError | IssueNotFound | Unauthorized>> & {
initialValueTarget: Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>;
}
issueTitle = (id: number & Brand<"IssueId">id: type IssueId = number & Brand<"IssueId">IssueId) =>
var Object: ObjectConstructorObject.ObjectConstructor.assign<Atom.Atom<AsyncResult.AsyncResult<string, RpcClientError | IssueNotFound | Unauthorized>>, {
initialValueTarget: Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>;
}>(target: Atom.Atom<AsyncResult.AsyncResult<string, RpcClientError | IssueNotFound | Unauthorized>>, source: {
initialValueTarget: Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>;
}): Atom.Atom<...> & {
initialValueTarget: Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>;
} (+3 overloads)
assign(
import AtomAtom.const mapResult: <Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, string>(self: Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, f: (_: Issue) => string) => Atom.Atom<AsyncResult.AsyncResult<string, RpcClientError | IssueNotFound | Unauthorized>> (+1 overload)mapResult(function issue(id: IssueId): Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue(id: number & Brand<"IssueId">id), (found: Issuefound) => found: Issuefound.title: stringtitle),
{ initialValueTarget: Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>initialValueTarget: function issue(id: IssueId): Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue(id: number & Brand<"IssueId">id) },
);
const const titleHandle: Handle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<string, RpcClientError | IssueNotFound | Unauthorized>> & {
initialValueTarget: Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>;
}>
titleHandle = const atoms: Loader.Loaderatoms.Loader.prefetch: <(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<string, RpcClientError | IssueNotFound | Unauthorized>> & {
initialValueTarget: Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>;
}>(source: (id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<string, RpcClientError | IssueNotFound | Unauthorized>> & {
initialValueTarget: Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>;
}, id: number & Brand<...>) => Handle<...>
prefetch(const issueTitle: (id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<string, RpcClientError | IssueNotFound | Unauthorized>> & {
initialValueTarget: Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>;
}
issueTitle, const id: number & Brand<"IssueId">id);
Atom.swr points at the atom it wraps the same way, so over a query with reactivityKeys, assign initialValueTarget: query to what it returns. Atom.setIdleTTL, Atom.keepAlive and Atom.withLabel copy the atom, serialization key included. An atom that picks between a value the page already has and a live query takes Atom.serializable itself, and the server sends whichever it picked.
| Verb | Starts the atom | Waits | When the atom has a result | Returns |
|---|---|---|---|---|
prefetch | if it isn't in the registry | never | leaves it alone | a handle |
ensure | if it isn't in the registry | only while the atom has no result | returns it at once | a loaded handle |
fetch | yes, and refreshes it if it has a result | until the new result settles | refreshes it, then waits | a loaded handle |
- Use
prefetchfor data the route needs but its loader doesn't: it streams in while the page renders. - Use
ensurewhen the loader needs the result, or the page shouldn't render without it. - Use
fetchwhen the loader must not see a result from before it ran, such as after a write. - Use
prefetchInBrowserfor a read that needs what only the browser has, such as a token in the URL's fragment: the server leaves it to the browser. See Inputs only the browser has.
Below, three copies of an app differ only in the verb their issue route's loader calls. The router keeps the old page until the loader returns, so a verb that waits blocks the navigation, and a page whose atom has no result yet shows a skeleton until it streams in.
Handles
A handle is plain data, the arguments and the atom's key, that names the atom a verb started. The loader returns it and the page passes it to the component that reads the atom, as a prop: useAtomResult(issue, props.issue) reads issue(...props.issue.args). It crosses loader data and server component props as is. Its type names its source, so a component that takes a Handle<typeof assignee> can't be given an id or another family's handle, and can't start a request of its own. ensure and fetch return a LoadedHandle, for a component that should only get a handle whose atom had a result when the loader returned. See Reading atoms.
Typed failures are results
ensure and fetch don't reject on a typed failure: they return a handle whose atom failed, so the loader can decide what the failure means, and otherwise the page renders it through a boundary. Only a defect or an interruption rejects. Loader.result(handle) returns the settled AsyncResult in the loader that waited for it; it throws for a copy of the handle, such as one read back from loader data.
The result's error type lists every tag the atom can fail with, so a loader can decide tag by tag. Match.valueTags takes one handler per tag, and doesn't compile when one is missing.
// The result ensure or fetch waited for, in the loader that called it.
const const result: Loader.SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>result = import LoaderLoader.const result: <(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(handle: LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>) => Loader.SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>result(const issueHandle: LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issueHandle);
const error = import AsyncResultAsyncResult.const error: <Issue, RpcClientError | IssueNotFound | Unauthorized>(self: AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>) => Option.Option<RpcClientError | IssueNotFound | Unauthorized>error(const result: Loader.SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>result);if (import OptionOption.const isSome: <RpcClientError | IssueNotFound | Unauthorized>(self: Option.Option<RpcClientError | IssueNotFound | Unauthorized>) => self is Option.Some<RpcClientError | IssueNotFound | Unauthorized>isSome(const error: Option.Option<RpcClientError | IssueNotFound | Unauthorized>error)) {
// One case per tag the atom can fail with; a missing one doesn't compile.
import MatchMatch.const valueTags: <RpcClientError | IssueNotFound | Unauthorized, {
IssueNotFound: () => never;
Unauthorized: () => undefined;
RpcClientError: () => undefined;
}>(input: RpcClientError | IssueNotFound | Unauthorized, fields: {
IssueNotFound: () => never;
Unauthorized: () => undefined;
RpcClientError: () => undefined;
}) => undefined (+1 overload)
valueTags(const error: Option.Some<RpcClientError | IssueNotFound | Unauthorized>error.Some<RpcClientError | IssueNotFound | Unauthorized>.value: RpcClientError | IssueNotFound | Unauthorizedvalue, {
type IssueNotFound: () => neverIssueNotFound: () => {
throw function notFound(options?: NotFoundError): NotFoundErrornotFound();
},
// Left in the result: <AuthBoundary> and <AppBoundary> catch them
// when the page reads the atom.
type Unauthorized: () => undefinedUnauthorized: () => var undefinedundefined,
type RpcClientError: () => undefinedRpcClientError: () => var undefinedundefined,
});
}
Loader failures shows both: a notFound from the loader, and a failure caught by a boundary.
Leases
A loader runs before the route renders, and an atom nobody has mounted is evicted when it goes idle. Every verb takes a lease, which keeps the atom mounted until a component reads it with useAtomResult or useHandleValue, or for 30 seconds when none does. Loader.make(registry, { leaseTTL }) changes the time. While a component reads the atom, the verbs take no lease on it, so a loader that runs again for the same page, such as after a search param changes, doesn't keep the atom mounted after the page leaves it.
On the server
Each request has its own registry, so ensure waits like fetch and nothing refreshes in the background. Only atoms in the registry when the loaders finish are sent to the browser; an atom first read during render runs again there. Start every atom a page reads from its loader.
In development, a verb fails fast when a loader on the server starts an atom without a serialization key that doesn't point at one with initialValueTarget, since the browser would request it again.
Aborted requests
When a navigation is left while its loaders wait, because another one interrupted it or the browser closed the request, ensure and fetch stop waiting and reject with the abort reason, which the router ignores. The atoms they started keep loading, in the browser's registry or until the request's registry is disposed. Nothing changes in the loaders:
- On React Router, the middleware
atomsMiddlewaregives the verbs the server request'ssignal, andclientAtomsMiddlewaregives client loaders the navigation's. - On Next.js, the verbs take React's
cacheSignal, which aborts with the server component render. - Another integration passes its own:
Loader.make(registry, { signal }).
On TanStack Router, the router context's atoms serves every navigation, so its verbs can't see one navigation's signal, and a loader waits until the atom settles. To stop waiting, run the Effect verbs with run and the loader's abort signal:
export const const Route: Route<Register, RootRoute<Register, undefined, {
atoms: Loader.Loader;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$id", "/issues/$id", "/issues/$id", "/issues/$id", undefined, {
readonly id: number & Brand<"IssueId">;
}, AnyContext, AnyContext, AnyContext, {}, ({ abortController, context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
atoms: Loader.Loader;
}, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route = createFileRoute<"/issues/$id", RootRoute<Register, undefined, {
atoms: Loader.Loader;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$id", "/issues/$id", "/issues/$id">(path?: "/issues/$id" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & FilebaseRouteOptionsInterface<...> & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute("/issues/$id")({
params?: {
parse?: ParseParamsFn<"/issues/$id", {
readonly id: number & Brand<"IssueId">;
}> | undefined;
priority?: number;
stringify?: StringifyParamsFn<"/issues/$id", {
readonly id: number & Brand<"IssueId">;
}> | undefined;
} | undefined
params: {
parse?: ParseParamsFn<"/issues/$id", {
readonly id: number & Brand<"IssueId">;
}> | undefined
parse: (params: {
id: string;
}
params) => import SchemaSchema.function decodeUnknownSync<Schema.Struct<{
readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>>(schema: Schema.Struct<{
readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>, options?: ParseOptions): (input: unknown, options?: ParseOptions) => {
readonly id: number & Brand<"IssueId">;
}
decodeUnknownSync(const IssueParams: Schema.Struct<{
readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>
IssueParams)(params: {
id: string;
}
params),
stringify?: StringifyParamsFn<"/issues/$id", {
readonly id: number & Brand<"IssueId">;
}> | undefined
stringify: import SchemaSchema.const encodeSync: <Schema.Struct<{
readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>>(schema: Schema.Struct<{
readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>, options?: ParseOptions) => (input: {
readonly id: number & Brand<"IssueId">;
}, options?: ParseOptions) => {
readonly id: string;
}
encodeSync(const IssueParams: Schema.Struct<{
readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>
IssueParams),
},
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { atoms: Loader.Loader; }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, ... 12 more ..., undefined>.loader?: Constrain<({ abortController, context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
atoms: Loader.Loader;
}, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader: async ({ abortController: AbortControllerabortController, context: {
atoms: Loader.Loader;
}
context, params: {
readonly id: number & Brand<"IssueId">;
}
params }) => ({
// Stops waiting when another navigation supersedes this one.
issue: LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: await context: {
atoms: Loader.Loader;
}
context.atoms: Loader.Loaderatoms.Loader.run: <LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>, never>(effect: Effect.Effect<LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>, never, AtomRegistry>, options?: {
readonly signal?: AbortSignal | undefined;
}) => Promise<...>
run(import LoaderLoader.const ensure: <(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">) => Effect.Effect<LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>, never, AtomRegistry>ensure(const issue: (id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue, params: {
readonly id: number & Brand<"IssueId">;
}
params.id: number & Brand<"IssueId">id), {
signal?: AbortSignal | undefinedsignal: abortController: AbortControllerabortController.AbortController.signal: AbortSignalsignal,
}),
}),
});In Effect code
The verbs are Effects that need an AtomRegistry. The framework integrations put them on the router context as Promises, through Loader.make(registry), because loaders are async functions. Its run runs an Effect that needs the registry on the same registry, with the same lease and signal, and rejects with its failure: raf-effect-atom/rsc exports it as run.
const const program: Effect.Effect<{
assignee: Handle<(issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
issue: LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
}, never, AtomRegistry>
program = import EffectEffect.const gen: <Effect.Effect<Handle<(issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>, never, AtomRegistry> | Effect.Effect<LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>, never, AtomRegistry>, {
...;
}>(f: () => Generator<...>) => Effect.Effect<...> (+1 overload)
gen(function* () {
const const assigneeHandle: Handle<(issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assigneeHandle = yield* import LoaderLoader.const prefetch: <(issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, issueId: number & Brand<"IssueId">) => Effect.Effect<Handle<(issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>, never, AtomRegistry>prefetch(const assignee: (issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee, const id: number & Brand<"IssueId">id);
const const issueHandle: LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issueHandle = yield* import LoaderLoader.const ensure: <(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">) => Effect.Effect<LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>, never, AtomRegistry>ensure(const issue: (id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue, const id: number & Brand<"IssueId">id);
return { assignee: Handle<(issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee: const assigneeHandle: Handle<(issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assigneeHandle, issue: LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: const issueHandle: LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issueHandle };
});
// Runs it on the registry the Promise verbs use.
const handles = await const atoms: Loader.Loaderatoms.Loader.run: <{
assignee: Handle<(issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
issue: LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
}, never>(effect: Effect.Effect<{
assignee: Handle<(issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
issue: LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
}, never, AtomRegistry>, options?: {
readonly signal?: AbortSignal | undefined;
}) => Promise<...>
run(const program: Effect.Effect<{
assignee: Handle<(issueId: IssueId) => Atom.Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
issue: LoadedHandle<(id: IssueId) => Atom.Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
}, never, AtomRegistry>
program);