diff --git a/apps/mobile/src/state/notes.ts b/apps/mobile/src/state/notes.ts index 68ee42ea1aa2..10b70d1fa4a2 100644 --- a/apps/mobile/src/state/notes.ts +++ b/apps/mobile/src/state/notes.ts @@ -1,6 +1,7 @@ import { useAtomValue } from "@effect/atom-react"; import { createNotesEnvironmentAtoms } from "@t3tools/client-runtime/state/notes"; import type { EnvironmentId, NoteSummary } from "@t3tools/contracts"; +import { isAnswerExpected } from "@t3tools/client-runtime/connection"; import * as Option from "effect/Option"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { connectionAtomRuntime } from "../connection/runtime"; @@ -15,11 +16,11 @@ const allNotesAtom = Atom.family((query: string) => Atom.make((get) => { const notes: EnvironmentNote[] = []; let isPending = false; - for (const environmentId of get(environmentPresentations.presentationsAtom).keys()) { + for (const [environmentId, presentation] of get(environmentPresentations.presentationsAtom)) { const result = get(notesEnvironment.list({ environmentId, input: query ? { query } : {} })); const value = Option.getOrNull(AsyncResult.value(result)); if (value === null) { - isPending ||= result._tag !== "Failure"; + isPending ||= result._tag !== "Failure" && isAnswerExpected(presentation); continue; } notes.push(...value.notes.map((note) => ({ ...note, environmentId }))); diff --git a/apps/web/src/state/automations.ts b/apps/web/src/state/automations.ts index 4752d96d1a3d..70a415d03b67 100644 --- a/apps/web/src/state/automations.ts +++ b/apps/web/src/state/automations.ts @@ -10,6 +10,7 @@ import { createEnvironmentRpcCommand, createEnvironmentRpcSubscriptionAtomFamily, } from "@t3tools/client-runtime/state/runtime"; +import { isAnswerExpected } from "@t3tools/client-runtime/connection"; import * as Option from "effect/Option"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; @@ -47,18 +48,19 @@ export const runAutomationNow = createEnvironmentRpcCommand(connectionAtomRuntim interface AutomationsView { readonly automations: readonly EnvironmentAutomation[]; - /** True until every environment has answered at least once. */ + /** True while a reachable environment has not answered yet. */ readonly isPending: boolean; } const allAutomationsAtom = Atom.make((get): AutomationsView => { const automations: EnvironmentAutomation[] = []; let isPending = false; - for (const environmentId of get(environmentPresentations.presentationsAtom).keys()) { + for (const [environmentId, presentation] of get(environmentPresentations.presentationsAtom)) { const result = get(automationsByEnvironment({ environmentId, input: {} })); const snapshot = Option.getOrNull(AsyncResult.value(result)); if (snapshot === null) { - isPending ||= result._tag !== "Failure"; + // An unreachable environment never answers; it must not hold the whole list in loading. + isPending ||= result._tag !== "Failure" && isAnswerExpected(presentation); continue; } for (const automation of snapshot.automations) { diff --git a/apps/web/src/state/notes.ts b/apps/web/src/state/notes.ts index c83e8741c458..7f1fc17e67e5 100644 --- a/apps/web/src/state/notes.ts +++ b/apps/web/src/state/notes.ts @@ -1,6 +1,7 @@ import { useAtomValue } from "@effect/atom-react"; import { createNotesEnvironmentAtoms } from "@t3tools/client-runtime/state/notes"; import type { EnvironmentId, NoteSummary } from "@t3tools/contracts"; +import { isAnswerExpected } from "@t3tools/client-runtime/connection"; import * as Option from "effect/Option"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { connectionAtomRuntime } from "../connection/runtime"; @@ -15,11 +16,11 @@ const allNotesAtom = Atom.family((query: string) => Atom.make((get) => { const notes: EnvironmentNote[] = []; let isPending = false; - for (const environmentId of get(environmentPresentations.presentationsAtom).keys()) { + for (const [environmentId, presentation] of get(environmentPresentations.presentationsAtom)) { const result = get(notesEnvironment.list({ environmentId, input: query ? { query } : {} })); const snapshot = Option.getOrNull(AsyncResult.value(result)); if (snapshot === null) { - isPending ||= result._tag !== "Failure"; + isPending ||= result._tag !== "Failure" && isAnswerExpected(presentation); continue; } notes.push(...snapshot.notes.map((note) => ({ ...note, environmentId }))); diff --git a/packages/client-runtime/src/connection/presentation.ts b/packages/client-runtime/src/connection/presentation.ts index f7586c5e3dbf..463920269c44 100644 --- a/packages/client-runtime/src/connection/presentation.ts +++ b/packages/client-runtime/src/connection/presentation.ts @@ -25,6 +25,15 @@ export interface EnvironmentPresentation { readonly serverConfig: ServerConfig | null; } +/** + * Whether a pending subscription on this environment is worth showing as loading. + * Offline or failing environments never answer and must not hold a merged list in loading. + */ +export function isAnswerExpected(presentation: EnvironmentPresentation): boolean { + const { phase } = presentation.connection; + return phase === "connected" || phase === "connecting"; +} + export function presentConnectionState( state: SupervisorConnectionState, ): EnvironmentConnectionPresentation {