import type { QueryPersistence } from "./queryPersistence.ts"; import { createCommandPermissions } from "./commandPermissions.ts"; import { followStreamInEnvironment } from "./environmentStreams.ts"; import { reuseUnchanged } from "./serverConfigProjection.ts"; export { runStreamInEnvironment, followStreamInEnvironment } from "./environmentStreams.ts"; import { type ClientGuardedRpcTag, EnvironmentId, type EnvironmentId as EnvironmentIdType, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity"; import type { ConnectionAttemptError, SupervisorConnectionState } from "../connection/model.ts"; import * as EnvironmentRegistry from "../connection/registry.ts"; import { type EnvironmentRpcInput, type EnvironmentRpcSuccess, type EnvironmentRpcFailure, type EnvironmentRpcStreamFailure, type EnvironmentRpcStreamValue, type EnvironmentSubscriptionRpcTag, type EnvironmentUnaryRpcTag, EnvironmentRpcUnavailableError, request, requestGuarded, RpcPermissionGuard, subscribe, } from "../rpc/client.ts"; import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import type * as RpcSession from "../rpc/session.ts"; const isEnvironmentRpcUnavailable = Schema.is(EnvironmentRpcUnavailableError); /** * Holds back the unavailable failure of a query whose session went away. The * query atom reruns when it sees that session change, so settling first would * show a failure for a query that is about to retry. While `session` is still * current no rerun is coming, and the failure stands. */ const pendingOnceSessionReplaced = (session: RpcSession.RpcSession) => (effect: Effect.Effect) => effect.pipe( // The query's error type is generic, so its tags are unknown to `catchTags`. Effect.catchIf(isEnvironmentRpcUnavailable, (error) => EnvironmentSupervisor.EnvironmentSupervisor.pipe( Effect.flatMap((supervisor) => SubscriptionRef.get(supervisor.session)), Effect.flatMap((current) => Option.isSome(current) && current.value === session ? Effect.fail(error) : Effect.never, ), ), ), ); interface EnvironmentAtomOptions { readonly label: string; readonly execute: (input: Input) => Effect.Effect; readonly scheduler?: AtomCommandScheduler; readonly concurrency?: AtomCommandConcurrency<{ readonly environmentId: EnvironmentIdType; readonly input: Input; }>; } interface EnvironmentCommandAtomOptions extends Omit< EnvironmentAtomOptions, "execute" > { readonly execute: ( input: Input, registry: AtomRegistry.AtomRegistry, environmentId: EnvironmentIdType, ) => Effect.Effect; } interface EnvironmentQueryAtomOptions extends Omit< EnvironmentAtomOptions, "execute" > { /** * `emit` shows a provisional value while the query keeps waiting. The settled * result replaces it, so catch failures that should keep it on screen. */ readonly execute: ( input: Input, emit: (value: A) => Effect.Effect, ) => Effect.Effect; readonly persistence?: QueryPersistence; readonly staleTimeMs?: number; readonly idleTtlMs?: number; readonly refreshIntervalMs?: number; readonly refreshTrigger?: (target: { readonly environmentId: EnvironmentIdType; readonly input: Input; }) => Atom.Atom | undefined; readonly isFinal?: (value: A) => boolean; /** * Runs where each server answer lands, before readers see it, so derived records (summaries, * snapshots, watches) come from the producer instead of from components watching the data. * Cached and emitted provisional values are not answers. */ readonly onAnswer?: QueryAnswerHandler; } export type QueryAnswerHandler = ( target: { readonly environmentId: EnvironmentIdType; readonly input: Input }, value: A, registry: AtomRegistry.AtomRegistry, ) => Effect.Effect; interface EnvironmentSubscriptionAtomOptions { readonly sensitiveInput?: boolean; readonly label: string; readonly subscribe: (input: Input) => Stream.Stream; readonly completeWhen?: (value: A) => boolean; readonly idleTtlMs?: number; } export type SettledAsyncResult = AsyncResult.Success | AsyncResult.Failure; export type AtomCommandResult = SettledAsyncResult; export type AtomCommandSuccess = R extends AtomCommandResult ? A : never; export type AtomCommandFailure = R extends AtomCommandResult ? E : never; export interface AtomCommandOptions { readonly label?: string; readonly reportFailure?: boolean; readonly reportDefect?: boolean; } export interface AtomCommandReporter { readonly warn: (message: string, cause: Cause.Cause) => void; readonly error: (message: string, cause: Cause.Cause) => void; } export interface AtomCommand { readonly label: string; readonly run: (registry: AtomRegistry.AtomRegistry, input: W) => Promise>; } export type AtomCommandConcurrency = | { readonly mode: "parallel" } | { readonly mode: "serial" | "singleFlight" | "latest"; readonly key: (input: W) => string; }; interface AtomCommandSchedulerState { readonly serial: Map>; readonly singleFlight: Map>; readonly latest: Map; } interface AtomCommandLatestBatch { execute: () => Promise>; readonly resolve: Array<(result: AtomCommandResult) => void>; } interface AtomCommandLatestLane { running: boolean; pending: AtomCommandLatestBatch | undefined; } export interface AtomCommandScheduler { readonly schedule: ( registry: AtomRegistry.AtomRegistry, concurrency: AtomCommandConcurrency, input: W, execute: () => Promise>, ) => Promise>; } async function settleAtomCommandResult( execute: () => Promise>, ): Promise> { try { return await execute(); } catch (defect) { return AsyncResult.failure(Cause.die(defect)); } } export function createAtomCommandScheduler(): AtomCommandScheduler { const registryStates = new WeakMap(); const stateFor = (registry: AtomRegistry.AtomRegistry): AtomCommandSchedulerState => { const existing = registryStates.get(registry); if (existing !== undefined) { return existing; } const state: AtomCommandSchedulerState = { serial: new Map(), singleFlight: new Map(), latest: new Map(), }; registryStates.set(registry, state); return state; }; return { schedule: ( registry: AtomRegistry.AtomRegistry, concurrency: AtomCommandConcurrency, input: W, execute: () => Promise>, ): Promise> => { if (concurrency.mode === "parallel") { return execute(); } const key = concurrency.key(input); const state = stateFor(registry); if (concurrency.mode === "singleFlight") { const existing = state.singleFlight.get(key) as | Promise> | undefined; if (existing !== undefined) { return existing; } const current = execute(); state.singleFlight.set(key, current); void current.then( () => { if (state.singleFlight.get(key) === current) { state.singleFlight.delete(key); } }, () => { if (state.singleFlight.get(key) === current) { state.singleFlight.delete(key); } }, ); return current; } if (concurrency.mode === "serial") { const previous = state.serial.get(key); const current = previous === undefined ? execute() : previous.then(execute, execute); state.serial.set(key, current); void current.then( () => { if (state.serial.get(key) === current) { state.serial.delete(key); } }, () => { if (state.serial.get(key) === current) { state.serial.delete(key); } }, ); return current; } let lane = state.latest.get(key); if (lane === undefined) { lane = { running: false, pending: undefined }; state.latest.set(key, lane); } const activeLane = lane; const result = new Promise>((resolve) => { if (activeLane.pending === undefined) { activeLane.pending = { execute: execute as () => Promise>, resolve: [resolve as (result: AtomCommandResult) => void], }; return; } activeLane.pending.execute = execute as () => Promise>; activeLane.pending.resolve.push( resolve as (result: AtomCommandResult) => void, ); }); if (!activeLane.running) { activeLane.running = true; void (async () => { while (activeLane.pending !== undefined) { const batch = activeLane.pending; activeLane.pending = undefined; let batchResult: AtomCommandResult; try { batchResult = await batch.execute(); } catch (defect) { batchResult = AsyncResult.failure(Cause.die(defect)); } for (const resolve of batch.resolve) { resolve(batchResult); } } activeLane.running = false; if (state.latest.get(key) === activeLane) { state.latest.delete(key); } })(); } return result; }, }; } export const scheduleAtomCommandEffect = /* Every atom command passes through this scheduler; the command owns its tracing boundary. */ Effect.fnUntraced(function* ( registry: AtomRegistry.AtomRegistry, scheduler: AtomCommandScheduler, concurrency: AtomCommandConcurrency, input: W, effect: Effect.Effect, ): Effect.fn.Return { const context = yield* Effect.context(); const result = yield* Effect.promise((signal) => scheduler.schedule(registry, concurrency, input, async () => { const exit = await Effect.runPromiseExitWith(context)(effect, { signal }); return Exit.isSuccess(exit) ? AsyncResult.success(exit.value) : AsyncResult.failure(exit.cause); }), ); return result._tag === "Success" ? result.value : yield* Effect.failCause(result.cause); }); export async function runAtomCommand( registry: AtomRegistry.AtomRegistry, command: AtomCommand, input: W, options: AtomCommandOptions = {}, reporter: AtomCommandReporter = console, ): Promise> { const result = await settleAtomCommandResult(() => command.run(registry, input)); reportAtomCommandResult(result, { ...options, label: options.label ?? command.label }, reporter); return result; } export function mapAtomCommandResult( result: AtomCommandResult, map: (value: A) => B, ): AtomCommandResult { return result._tag === "Success" ? AsyncResult.success(map(result.value)) : AsyncResult.failure(result.cause); } export function isAtomCommandInterrupted(result: AtomCommandResult): boolean { return result._tag === "Failure" && Cause.hasInterruptsOnly(result.cause); } export function squashAtomCommandFailure(result: { readonly cause: Cause.Cause; }): unknown { return Cause.squash(result.cause); } export async function settleAsyncResult( execute: () => Promise>, ): Promise> { try { return AsyncResult.fromExit(await execute()); } catch (defect) { return AsyncResult.failure(Cause.die(defect)); } } async function executeAtomCommand( execute: () => Promise>, options: AtomCommandOptions = {}, reporter: AtomCommandReporter = console, ): Promise> { const result = await settleAsyncResult(execute); reportAtomCommandResult(result, options, reporter); return result; } export interface AtomQueryOptions extends AtomCommandOptions { /** * Force a fresh execution instead of accepting a value the atom already * holds (e.g. an SWR-cached result within its stale window). Used by * verification flows where a cached failure must not satisfy a retry. */ readonly refresh?: boolean; readonly signal?: AbortSignal; } export async function executeAtomQuery( registry: AtomRegistry.AtomRegistry, atom: Atom.Atom>, options: AtomQueryOptions = {}, reporter: AtomCommandReporter = console, ): Promise> { const query = Effect.scoped( Effect.gen(function* () { yield* AtomRegistry.mount(registry, atom); if (options.refresh) { yield* Effect.sync(() => { const current = registry.get(atom); if (current._tag !== "Initial" && !current.waiting) { registry.refresh(atom); } }); } return yield* AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true, }); }), ); return executeAtomCommand( () => Effect.runPromiseExit(query, { signal: options.signal }), options, reporter, ); } export function createRuntimeCommand( runtime: Atom.AtomRuntime, options: { readonly label: string; readonly execute: (input: W, registry: AtomRegistry.AtomRegistry) => Effect.Effect; readonly scheduler?: AtomCommandScheduler; readonly concurrency?: AtomCommandConcurrency; }, ): AtomCommand { const scheduler = options.scheduler ?? createAtomCommandScheduler(); const concurrency = options.concurrency ?? { mode: "parallel" as const }; return { label: options.label, run: (registry, input) => settleAtomCommandResult(() => scheduler.schedule(registry, concurrency, input, () => { const atom = runtime .atom(options.execute(input, registry)) .pipe(Atom.withLabel(options.label)); return executeAtomQuery(registry, atom, { reportDefect: false, reportFailure: false }); }), ), }; } export function reportAtomCommandResult( result: AtomCommandResult, options: AtomCommandOptions = {}, reporter: AtomCommandReporter = console, ): void { if (AsyncResult.isSuccess(result) || Cause.hasInterruptsOnly(result.cause)) { return; } const label = options.label ?? "atom command"; if (Cause.hasDies(result.cause)) { if (options.reportDefect ?? true) { reporter.error(`[atom-command] ${label} defected`, result.cause); } } else if (options.reportFailure ?? true) { reporter.warn(`[atom-command] ${label} failed`, result.cause); } } export async function settlePromise( execute: () => Promise, ): Promise> { try { return AsyncResult.success(await execute()); } catch (defect) { return AsyncResult.failure(Cause.die(defect)); } } function environmentRpcKey(target: { readonly environmentId: EnvironmentIdType; readonly input: Input; }): string { return JSON.stringify([target.environmentId, target.input]); } function parseEnvironmentRpcKey(key: string): { readonly environmentId: EnvironmentIdType; readonly input: Input; } { const decoded = JSON.parse(key) as [EnvironmentIdType, Input]; return { environmentId: EnvironmentId.make(decoded[0]), input: decoded[1], }; } function runInEnvironment( environmentId: EnvironmentIdType, effect: Effect.Effect, ): Effect.Effect< A, E | EnvironmentRegistry.EnvironmentNotRegisteredError, EnvironmentRegistry.EnvironmentRegistry | Exclude > { return EnvironmentRegistry.EnvironmentRegistry.pipe( Effect.flatMap((registry) => registry.run(environmentId, effect)), ); } /** * `Atom.makeRefreshOnSignal` subscribes to the signal, so a query that outlives its view on an * idle TTL still re-runs on every change. Reading the signal as a dependency instead lets the * registry re-run a mounted query right away and only mark an idle one stale, which then * refreshes on its next read. */ const refreshOnSignalWhileRead = (signal: Atom.Atom, isFinal: (value: V) => boolean) => (self: Atom.Atom): Atom.Atom => { // The signal value each registry's data was read under. Held outside the node because the // registry can sweep this node while `self` keeps the data, and the rebuilt node must still // see a change it missed. const readUnder = new WeakMap(); return Atom.transform( self, (get) => { const current = get(signal); get.subscribe(self, (value) => get.setSelf(value)); const changed = readUnder.has(get.registry) && !Object.is(readUnder.get(get.registry), current); readUnder.set(get.registry, current); if (changed && !isFinal(get.once(self))) get.refresh(self); return get.once(self); }, { initialValueTarget: self }, ); }; const refreshEveryUntilFinal = (intervalMs: number, isFinal: (value: V) => boolean) => (self: Atom.Atom): Atom.Atom => Atom.transform( self, (get) => { const value = get(self); if (!isFinal(value)) { // @effect-diagnostics-next-line globalTimers:off - Mirrors `Atom.withRefresh`, outside an Effect runtime. const handle = setTimeout(() => get.refresh(self), intervalMs); get.addFinalizer(() => clearTimeout(handle)); } return value; }, { initialValueTarget: self }, ); /** * A query result's data alone. A refetch always publishes a new result (a new * timestamp, `waiting` on and off), while equal data keeps its identity, so a * reader of only this skips the refetches that changed nothing. */ export const selectEnvironmentQueryData = (result: AsyncResult.AsyncResult): A | null => AsyncResult.getOrElse(result, () => null); type EnvironmentConnection = readonly [ SupervisorConnectionState, Option.Option, ]; const connectionAtomsByRuntime = new WeakMap< object, ( environmentId: EnvironmentIdType, ) => Atom.Atom> >(); /** * One connection atom per environment for every query on a runtime, so a reader of the session * generation and a query it re-runs see the same connection change at once. */ function environmentConnectionAtoms( runtime: Atom.AtomRuntime, ) { const existing = connectionAtomsByRuntime.get(runtime); if (existing !== undefined) return existing; const family = Atom.family((environmentId: EnvironmentIdType) => runtime.atom( followStreamInEnvironment( environmentId, Stream.unwrap( EnvironmentSupervisor.EnvironmentSupervisor.pipe( Effect.map((supervisor) => SubscriptionRef.changes(supervisor.state).pipe( Stream.zipLatest(SubscriptionRef.changes(supervisor.session)), ), ), ), ), ), { initialValue: null }, ), ) as ( environmentId: EnvironmentIdType, ) => Atom.Atom>; connectionAtomsByRuntime.set(runtime, family); return family; } const sessionGenerationAtomsByRuntime = new WeakMap< object, (environmentId: EnvironmentIdType) => Atom.Atom >(); /** * Counts established sessions; it changes only when a new one comes up, so failed one-shot reads * can watch it to run again. Keyed on the session object because the supervisor's `generation` * restarts when the registry installs a new supervisor (re-pairing, route edits). */ export function environmentSessionGenerationAtom( runtime: Atom.AtomRuntime, environmentId: EnvironmentIdType, ): Atom.Atom { let family = sessionGenerationAtomsByRuntime.get(runtime); if (family === undefined) { const connectionAtom = environmentConnectionAtoms(runtime); const sessionsSeen = Atom.family((id: EnvironmentIdType) => Atom.make( (get): { readonly session: RpcSession.RpcSession | null; readonly count: number } => { const previous = Option.getOrElse( get.self<{ readonly session: RpcSession.RpcSession | null; readonly count: number }>(), () => ({ session: null, count: 0 }), ); const connection = AsyncResult.getOrElse(get(connectionAtom(id)), () => null); if (connection === null || connection[0].phase !== "connected") return previous; const session = Option.getOrNull(connection[1]); return session === null || session === previous.session ? previous : { session, count: previous.count + 1 }; }, ), ); family = Atom.family((id: EnvironmentIdType) => Atom.make((get) => get(sessionsSeen(id)).count).pipe( Atom.withLabel(`environment-session-generation:${id}`), ), ); sessionGenerationAtomsByRuntime.set(runtime, family); } return family(environmentId); } export function createEnvironmentQueryAtomFamily( runtime: Atom.AtomRuntime, options: EnvironmentQueryAtomOptions< Input, A, E, | EnvironmentSupervisor.EnvironmentSupervisor | EnvironmentRegistry.EnvironmentRegistry | AtomRegistry.AtomRegistry | R >, ): (target: { readonly environmentId: EnvironmentIdType; readonly input: Input; }) => Atom.Atom> { const connectionAtom = environmentConnectionAtoms(runtime); const family = Atom.family((key: string) => { const target = parseEnvironmentRpcKey(key); const idleTtlMs = options.idleTtlMs ?? 5 * 60_000; const queryAtom = runtime .atom< A, | E | ConnectionAttemptError | EnvironmentRegistry.EnvironmentNotRegisteredError | EnvironmentRpcUnavailableError >((get) => { const connection = AsyncResult.getOrElse( get(connectionAtom(target.environmentId)), () => null, ); // A refetch that returns equal content keeps the previous value's // references, so readers of the data skip re-rendering on idle polls. const share = (value: A): A => reuseUnchanged( Option.getOrUndefined( Option.flatMap(get.self>(), AsyncResult.value), ), value, ); const emit = (value: A) => Effect.sync(() => get.setSelf(AsyncResult.success(share(value), { waiting: true }))).pipe( // A value set while the atom builds wins over the build's own // result, so a query that then settled synchronously would stay // waiting forever. Suspend on a promise (`yieldNow` is drained // inside the build) so the settled result arrives asynchronously. Effect.andThen(Effect.promise(() => Promise.resolve())), ); const execute = (): Effect.Effect< A, | E | ConnectionAttemptError | EnvironmentRegistry.EnvironmentNotRegisteredError | EnvironmentRpcUnavailableError, | EnvironmentRegistry.EnvironmentRegistry | AtomRegistry.AtomRegistry | Exclude > => { if (connection === null) return Effect.never; const [connectionState, session] = connection; switch (connectionState.phase) { case "connected": return Option.isSome(session) ? runInEnvironment( target.environmentId, options .execute(target.input, emit) .pipe(pendingOnceSessionReplaced(session.value)), ) : Effect.never; case "connecting": case "backoff": // A retry can take minutes. A query with an answer keeps showing it while it // waits; one with nothing to show reports the failure the retry is recovering // from, so its reader stops loading, and re-runs when the connection settles. return connectionState.lastFailure !== null && Option.isNone( Option.flatMap( get.self>(), AsyncResult.value, ), ) ? Effect.fail(connectionState.lastFailure) : Effect.never; case "available": case "offline": case "blocked": if (connectionState.lastFailure !== null) { return Effect.fail(connectionState.lastFailure); } return Effect.fail( new EnvironmentRpcUnavailableError({ environmentId: target.environmentId, message: `Environment ${target.environmentId} is ${ connectionState.phase === "available" ? "not connected" : connectionState.phase }.`, }), ); } }; const onAnswer = options.onAnswer; const answer = () => onAnswer === undefined ? execute().pipe(Effect.map(share)) : execute().pipe( Effect.map(share), Effect.tap((value) => onAnswer(target, value, get.registry)), ); if (!options.persistence) return answer(); const persistence = options.persistence; const cacheKey = JSON.stringify([options.label, target.input]); const previous = Option.flatMap( get.self>(), AsyncResult.value, ); return Effect.gen(function* () { const cached = Option.isSome(previous) ? previous : yield* persistence.load(target.environmentId, cacheKey); if ( Option.isSome(cached) && (connection === null || connection[0].phase !== "connected") ) { return cached.value; } if (Option.isNone(previous) && Option.isSome(cached)) yield* emit(cached.value); return yield* answer().pipe( Effect.tap((value) => persistence.save(target.environmentId, cacheKey, value)), ); }); }) .pipe( Atom.swr({ staleTime: options.staleTimeMs ?? 30_000, revalidateOnMount: true, }), Atom.setIdleTTL(idleTtlMs), ); const isFinal = (result: AsyncResult.AsyncResult) => options.isFinal !== undefined && Option.exists(AsyncResult.value(result), options.isFinal); const intervalQuery = options.refreshIntervalMs === undefined ? queryAtom : queryAtom.pipe(refreshEveryUntilFinal(options.refreshIntervalMs, isFinal)); const refreshTrigger = options.refreshTrigger?.(target); return ( refreshTrigger === undefined ? intervalQuery : intervalQuery.pipe(refreshOnSignalWhileRead(refreshTrigger, isFinal)) ).pipe(Atom.setIdleTTL(idleTtlMs), Atom.withLabel(`${options.label}:${key}`)); }); return (target) => family(environmentRpcKey(target)); } export function createEnvironmentSubscriptionAtomFamily( runtime: Atom.AtomRuntime, options: EnvironmentSubscriptionAtomOptions< Input, A, E, EnvironmentSupervisor.EnvironmentSupervisor | R >, ) { const family = Atom.family((key: string) => { const target = parseEnvironmentRpcKey(key); return runtime .atom( Stream.suspend(() => followStreamInEnvironment(target.environmentId, options.subscribe(target.input)), ).pipe(options.completeWhen ? Stream.takeUntil(options.completeWhen) : (stream) => stream), ) .pipe( Atom.setIdleTTL(options.idleTtlMs ?? 5 * 60_000), Atom.withLabel( options.sensitiveInput ? `${options.label}:${target.environmentId}` : `${options.label}:${key}`, ), ); }); return (target: { readonly environmentId: EnvironmentIdType; readonly input: Input }) => family(environmentRpcKey(target)); } export function createEnvironmentCommand( runtime: Atom.AtomRuntime, options: EnvironmentCommandAtomOptions< Input, A, E, EnvironmentSupervisor.EnvironmentSupervisor | EnvironmentRegistry.EnvironmentRegistry | R >, ) { return createRuntimeCommand(runtime, { label: options.label, ...(options.scheduler === undefined ? {} : { scheduler: options.scheduler }), ...(options.concurrency === undefined ? {} : { concurrency: options.concurrency }), execute: (target, registry) => runInEnvironment( target.environmentId, options.execute(target.input, registry, target.environmentId), ), }); } export function createEnvironmentRpcQueryAtomFamily< R, ER, TTag extends Exclude, >( runtime: Atom.AtomRuntime, options: { readonly label: string; readonly tag: TTag; readonly execute?: ( input: EnvironmentRpcInput, emit: (value: EnvironmentRpcSuccess) => Effect.Effect, ) => Effect.Effect< EnvironmentRpcSuccess, EnvironmentRpcFailure | EnvironmentRpcUnavailableError, EnvironmentSupervisor.EnvironmentSupervisor | EnvironmentRegistry.EnvironmentRegistry >; readonly persistence?: QueryPersistence>; readonly staleTimeMs?: number; readonly idleTtlMs?: number; readonly refreshIntervalMs?: number; readonly refreshTrigger?: (target: { readonly environmentId: EnvironmentIdType; readonly input: EnvironmentRpcInput; }) => Atom.Atom | undefined; readonly isFinal?: (value: EnvironmentRpcSuccess) => boolean; readonly onAnswer?: QueryAnswerHandler, EnvironmentRpcSuccess>; }, ) { return createEnvironmentQueryAtomFamily(runtime, { label: options.label, ...(options.persistence === undefined ? {} : { persistence: options.persistence }), ...(options.staleTimeMs === undefined ? {} : { staleTimeMs: options.staleTimeMs }), ...(options.idleTtlMs === undefined ? {} : { idleTtlMs: options.idleTtlMs }), ...(options.refreshIntervalMs === undefined ? {} : { refreshIntervalMs: options.refreshIntervalMs }), ...(options.refreshTrigger === undefined ? {} : { refreshTrigger: options.refreshTrigger }), ...(options.isFinal === undefined ? {} : { isFinal: options.isFinal }), ...(options.onAnswer === undefined ? {} : { onAnswer: options.onAnswer }), execute: ( input: EnvironmentRpcInput, emit: (value: EnvironmentRpcSuccess) => Effect.Effect, ) => options.execute?.(input, emit) ?? request(options.tag, input), }); } export function createEnvironmentRpcSubscriptionAtomFamily< R, ER, TTag extends EnvironmentSubscriptionRpcTag, B = EnvironmentRpcStreamValue, >( runtime: Atom.AtomRuntime, options: { readonly label: string; readonly tag: TTag; readonly idleTtlMs?: number; readonly completeWhen?: (value: B) => boolean; readonly transform?: ( stream: Stream.Stream< EnvironmentRpcStreamValue, EnvironmentRpcStreamFailure, EnvironmentSupervisor.EnvironmentSupervisor | R >, ) => Stream.Stream< B, EnvironmentRpcStreamFailure, EnvironmentSupervisor.EnvironmentSupervisor | R >; }, ) { return createEnvironmentSubscriptionAtomFamily(runtime, { label: options.label, ...(options.completeWhen === undefined ? {} : { completeWhen: options.completeWhen }), ...(options.idleTtlMs === undefined ? {} : { idleTtlMs: options.idleTtlMs }), subscribe: (input: EnvironmentRpcInput) => { const stream = subscribe(options.tag, input); return options.transform === undefined ? (stream as Stream.Stream< B, EnvironmentRpcStreamFailure, EnvironmentSupervisor.EnvironmentSupervisor | R >) : options.transform(stream); }, }); } export function createEnvironmentRpcCommand< R, ER, TTag extends EnvironmentUnaryRpcTag, Input extends EnvironmentRpcInput = EnvironmentRpcInput, >( runtime: Atom.AtomRuntime, options: { readonly label: string; readonly tag: TTag; readonly execute?: ( input: Input, ) => Effect.Effect< EnvironmentRpcSuccess, EnvironmentRpcFailure | EnvironmentRpcUnavailableError, EnvironmentSupervisor.EnvironmentSupervisor | EnvironmentRegistry.EnvironmentRegistry >; readonly scheduler?: AtomCommandScheduler; readonly concurrency?: AtomCommandConcurrency<{ readonly environmentId: EnvironmentIdType; readonly input: NoInfer; }>; readonly onSuccess?: ( target: { readonly environmentId: EnvironmentIdType; readonly input: NoInfer; }, registry: AtomRegistry.AtomRegistry, result: EnvironmentRpcSuccess, ) => Effect.Effect; readonly onSettled?: ( target: { readonly environmentId: EnvironmentIdType; readonly input: NoInfer; }, registry: AtomRegistry.AtomRegistry, ) => Effect.Effect; }, ) { const permissions = createCommandPermissions(runtime, options.tag); const command = createEnvironmentCommand(runtime, { label: options.label, ...(options.scheduler === undefined ? {} : { scheduler: options.scheduler }), ...(options.concurrency === undefined ? {} : { concurrency: options.concurrency }), execute: (input: Input, registry, environmentId) => { const target = { environmentId, input, }; // Routing requires consent on the origin as well as the actual destination. // The transport check below deliberately checks the destination again. return permissions.authorize(registry, environmentId, input).pipe( Effect.andThen(() => options.execute?.(input) ?? requestGuarded(options.tag, input)), Effect.provideService(RpcPermissionGuard, { authorize: (id, method, payload) => createCommandPermissions(runtime, method).authorize(registry, id, payload), }), Effect.tap((result) => options.onSuccess?.(target, registry, result) ?? Effect.void), Effect.ensuring(options.onSettled?.(target, registry) ?? Effect.void), ); }, }); return { ...command, requiredScopes: permissions.requiredScopes, permissionAtom: permissions.permissionAtom, }; }