Skip to content

Commit e608fdd

Browse files
donnesclaude
andauthored
fix(web): unreachable environments no longer keep Automations and Notes loading (#17)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 1701041 commit e608fdd

4 files changed

Lines changed: 20 additions & 7 deletions

File tree

‎apps/mobile/src/state/notes.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useAtomValue } from "@effect/atom-react";
22
import { createNotesEnvironmentAtoms } from "@t3tools/client-runtime/state/notes";
33
import type { EnvironmentId, NoteSummary } from "@t3tools/contracts";
4+
import { isAnswerExpected } from "@t3tools/client-runtime/connection";
45
import * as Option from "effect/Option";
56
import { AsyncResult, Atom } from "effect/unstable/reactivity";
67
import { connectionAtomRuntime } from "../connection/runtime";
@@ -15,11 +16,11 @@ const allNotesAtom = Atom.family((query: string) =>
1516
Atom.make((get) => {
1617
const notes: EnvironmentNote[] = [];
1718
let isPending = false;
18-
for (const environmentId of get(environmentPresentations.presentationsAtom).keys()) {
19+
for (const [environmentId, presentation] of get(environmentPresentations.presentationsAtom)) {
1920
const result = get(notesEnvironment.list({ environmentId, input: query ? { query } : {} }));
2021
const value = Option.getOrNull(AsyncResult.value(result));
2122
if (value === null) {
22-
isPending ||= result._tag !== "Failure";
23+
isPending ||= result._tag !== "Failure" && isAnswerExpected(presentation);
2324
continue;
2425
}
2526
notes.push(...value.notes.map((note) => ({ ...note, environmentId })));

‎apps/web/src/state/automations.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
createEnvironmentRpcCommand,
1111
createEnvironmentRpcSubscriptionAtomFamily,
1212
} from "@t3tools/client-runtime/state/runtime";
13+
import { isAnswerExpected } from "@t3tools/client-runtime/connection";
1314
import * as Option from "effect/Option";
1415
import { AsyncResult, Atom } from "effect/unstable/reactivity";
1516

@@ -47,18 +48,19 @@ export const runAutomationNow = createEnvironmentRpcCommand(connectionAtomRuntim
4748

4849
interface AutomationsView {
4950
readonly automations: readonly EnvironmentAutomation[];
50-
/** True until every environment has answered at least once. */
51+
/** True while a reachable environment has not answered yet. */
5152
readonly isPending: boolean;
5253
}
5354

5455
const allAutomationsAtom = Atom.make((get): AutomationsView => {
5556
const automations: EnvironmentAutomation[] = [];
5657
let isPending = false;
57-
for (const environmentId of get(environmentPresentations.presentationsAtom).keys()) {
58+
for (const [environmentId, presentation] of get(environmentPresentations.presentationsAtom)) {
5859
const result = get(automationsByEnvironment({ environmentId, input: {} }));
5960
const snapshot = Option.getOrNull(AsyncResult.value(result));
6061
if (snapshot === null) {
61-
isPending ||= result._tag !== "Failure";
62+
// An unreachable environment never answers; it must not hold the whole list in loading.
63+
isPending ||= result._tag !== "Failure" && isAnswerExpected(presentation);
6264
continue;
6365
}
6466
for (const automation of snapshot.automations) {

‎apps/web/src/state/notes.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useAtomValue } from "@effect/atom-react";
22
import { createNotesEnvironmentAtoms } from "@t3tools/client-runtime/state/notes";
33
import type { EnvironmentId, NoteSummary } from "@t3tools/contracts";
4+
import { isAnswerExpected } from "@t3tools/client-runtime/connection";
45
import * as Option from "effect/Option";
56
import { AsyncResult, Atom } from "effect/unstable/reactivity";
67
import { connectionAtomRuntime } from "../connection/runtime";
@@ -15,11 +16,11 @@ const allNotesAtom = Atom.family((query: string) =>
1516
Atom.make((get) => {
1617
const notes: EnvironmentNote[] = [];
1718
let isPending = false;
18-
for (const environmentId of get(environmentPresentations.presentationsAtom).keys()) {
19+
for (const [environmentId, presentation] of get(environmentPresentations.presentationsAtom)) {
1920
const result = get(notesEnvironment.list({ environmentId, input: query ? { query } : {} }));
2021
const snapshot = Option.getOrNull(AsyncResult.value(result));
2122
if (snapshot === null) {
22-
isPending ||= result._tag !== "Failure";
23+
isPending ||= result._tag !== "Failure" && isAnswerExpected(presentation);
2324
continue;
2425
}
2526
notes.push(...snapshot.notes.map((note) => ({ ...note, environmentId })));

‎packages/client-runtime/src/connection/presentation.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ export interface EnvironmentPresentation {
2525
readonly serverConfig: ServerConfig | null;
2626
}
2727

28+
/**
29+
* Whether a pending subscription on this environment is worth showing as loading.
30+
* Offline or failing environments never answer and must not hold a merged list in loading.
31+
*/
32+
export function isAnswerExpected(presentation: EnvironmentPresentation): boolean {
33+
const { phase } = presentation.connection;
34+
return phase === "connected" || phase === "connecting";
35+
}
36+
2837
export function presentConnectionState(
2938
state: SupervisorConnectionState,
3039
): EnvironmentConnectionPresentation {

0 commit comments

Comments
 (0)