Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ function workspaceState(overrides: Partial<WorkspaceState> = {}): WorkspaceState
connectionState: "connected",
connectionError: null,
shellSnapshotError: null,
latestCachedSnapshotReceivedAt: null,
networkStatus: "online",
...overrides,
};
Expand Down
3 changes: 1 addition & 2 deletions apps/mobile/src/state/workspace-connection-atoms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ describe("workspace connection subscriptions", () => {
hasCachedShell: false,
hasLiveShell: true,
firstError: null,
latestSnapshotUpdatedAt: null,
});
const broadState = Atom.make((get) =>
projectWorkspaceState({
Expand All @@ -75,7 +74,7 @@ describe("workspace connection subscriptions", () => {
for (let index = 0; index < 20; index++) {
h.registry.set(shell, {
...h.registry.get(shell),
latestSnapshotUpdatedAt: new Date(index * 1000).toISOString(),
hasSynchronizingShell: index % 2 === 0,
});
h.registry.get(broadState);
h.registry.get(h.stateAtom);
Expand Down
2 changes: 0 additions & 2 deletions apps/mobile/src/state/workspaceModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,13 @@ const EMPTY_SHELL_SUMMARY: EnvironmentShellSummary = {
hasCachedShell: false,
hasLiveShell: false,
firstError: null,
latestSnapshotUpdatedAt: null,
};

const CACHED_SHELL_SUMMARY: EnvironmentShellSummary = {
...EMPTY_SHELL_SUMMARY,
hasSnapshot: true,
hasSynchronizingShell: true,
hasCachedShell: true,
latestSnapshotUpdatedAt: "2026-06-07T00:00:00.000Z",
};

describe("mobile workspace projection", () => {
Expand Down
2 changes: 0 additions & 2 deletions apps/mobile/src/state/workspaceModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export interface WorkspaceState extends WorkspaceConnectionState {
readonly hasLoadedShellSnapshot: boolean;
readonly hasPendingShellSnapshot: boolean;
readonly shellSnapshotError: string | null;
readonly latestCachedSnapshotReceivedAt: string | null;
}

function overallConnectionState(
Expand Down Expand Up @@ -101,7 +100,6 @@ export function projectWorkspaceState(input: {
hasLoadedShellSnapshot: input.shellSummary.hasSnapshot,
hasPendingShellSnapshot: input.shellSummary.hasSynchronizingShell,
shellSnapshotError: input.shellSummary.firstError,
latestCachedSnapshotReceivedAt: input.shellSummary.latestSnapshotUpdatedAt,
};
}

Expand Down
97 changes: 96 additions & 1 deletion packages/client-runtime/src/state/shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function makeHarness() {
});

return {
catalogValueAtom,
registry: AtomRegistry.make(),
shellStateAtom: shellStateAtoms,
configAtom: configAtoms,
Expand All @@ -107,7 +108,6 @@ describe("environment shell projections", () => {
hasCachedShell: true,
hasLiveShell: false,
firstError: "Retrying.",
latestSnapshotUpdatedAt: "2026-06-02T00:00:00.000Z",
});

harness.registry.set(
Expand All @@ -122,6 +122,101 @@ describe("environment shell projections", () => {
expect(harness.registry.get(harness.summaryAtom)).toBe(summary);
});

it("does not notify summary subscribers when thread timestamps advance", () => {
const harness = makeHarness();
const initial = harness.registry.get(harness.summaryAtom);
let changes = 0;
const unsubscribe = harness.registry.subscribe(harness.summaryAtom, () => changes++);
try {
for (let index = 0; index < 20; index++) {
harness.registry.set(
harness.shellStateAtom(ENVIRONMENT_ID),
shellState({
status: "cached",
updatedAt: `2026-07-01T00:00:${String(index).padStart(2, "0")}.000Z`,
snapshotSequence: index + 2,
}),
);
expect(harness.registry.get(harness.summaryAtom)).toBe(initial);
}
expect(changes).toBe(0);
} finally {
unsubscribe();
harness.registry.dispose();
}
});

it("notifies for shell availability, status, errors, and enabled environment changes", () => {
const harness = makeHarness();
let changes = 0;
const unsubscribe = harness.registry.subscribe(harness.summaryAtom, () => changes++);
const setCatalog = (entries: ReturnType<typeof environmentEntry>[]) => {
harness.registry.set(harness.catalogValueAtom, {
isReady: true,
entries: new Map(entries.map((entry) => [entry.target.environmentId, entry])),
});
return harness.registry.get(harness.summaryAtom);
};
try {
expect(setCatalog([])).toEqual({
hasSnapshot: false,
hasSynchronizingShell: false,
hasCachedShell: false,
hasLiveShell: false,
firstError: null,
});
expect(setCatalog([environmentEntry(ENVIRONMENT_ID, "Environment")])).toMatchObject({
hasSnapshot: true,
hasCachedShell: true,
});
harness.registry.set(harness.shellStateAtom(ENVIRONMENT_ID), shellState({ status: "empty" }));
expect(harness.registry.get(harness.summaryAtom)).toMatchObject({
hasSnapshot: false,
hasCachedShell: false,
});
harness.registry.set(
harness.shellStateAtom(ENVIRONMENT_ID),
shellState({ status: "synchronizing", error: "Retrying." }),
);
expect(harness.registry.get(harness.summaryAtom)).toMatchObject({
hasSnapshot: false,
hasSynchronizingShell: true,
firstError: "Retrying.",
});
harness.registry.set(
harness.shellStateAtom(ENVIRONMENT_ID),
shellState({ status: "live", updatedAt: "2026-07-01T00:00:00.000Z" }),
);
expect(harness.registry.get(harness.summaryAtom)).toMatchObject({
hasSnapshot: true,
hasSynchronizingShell: false,
hasLiveShell: true,
firstError: null,
});
harness.registry.set(
harness.shellStateAtom(ENVIRONMENT_ID),
shellState({
status: "live",
updatedAt: "2026-07-01T00:00:00.000Z",
error: "Disconnected.",
}),
);
expect(harness.registry.get(harness.summaryAtom).firstError).toBe("Disconnected.");
harness.registry.set(
harness.shellStateAtom(ENVIRONMENT_ID),
shellState({ status: "live", updatedAt: "2026-07-01T00:00:00.000Z" }),
);
expect(harness.registry.get(harness.summaryAtom).firstError).toBeNull();
expect(
setCatalog([{ ...environmentEntry(ENVIRONMENT_ID, "Environment"), enabled: false }]),
).toMatchObject({ hasSnapshot: false, hasLiveShell: false });
expect(changes).toBe(8);
} finally {
unsubscribe();
harness.registry.dispose();
}
});

it("preserves server-config map identity until a config reference changes", () => {
const harness = makeHarness();
const empty = harness.registry.get(harness.serverConfigsAtom);
Expand Down
24 changes: 1 addition & 23 deletions packages/client-runtime/src/state/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
type ServerConfig,
} from "@t3tools/contracts";
import * as Cause from "effect/Cause";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
import * as Queue from "effect/Queue";
Expand Down Expand Up @@ -321,7 +320,6 @@ export interface EnvironmentShellSummary {
readonly hasCachedShell: boolean;
readonly hasLiveShell: boolean;
readonly firstError: string | null;
readonly latestSnapshotUpdatedAt: string | null;
}

const EMPTY_ENVIRONMENT_SHELL_SUMMARY: EnvironmentShellSummary = Object.freeze({
Expand All @@ -330,7 +328,6 @@ const EMPTY_ENVIRONMENT_SHELL_SUMMARY: EnvironmentShellSummary = Object.freeze({
hasCachedShell: false,
hasLiveShell: false,
firstError: null,
latestSnapshotUpdatedAt: null,
});

const EMPTY_SERVER_CONFIGS: ReadonlyMap<EnvironmentId, ServerConfig> = new Map();
Expand All @@ -344,8 +341,7 @@ function shellSummariesEqual(
left.hasSynchronizingShell === right.hasSynchronizingShell &&
left.hasCachedShell === right.hasCachedShell &&
left.hasLiveShell === right.hasLiveShell &&
left.firstError === right.firstError &&
left.latestSnapshotUpdatedAt === right.latestSnapshotUpdatedAt
left.firstError === right.firstError
);
}

Expand All @@ -372,7 +368,6 @@ export function createEnvironmentShellSummaryAtom(input: {
let hasCachedShell = false;
let hasLiveShell = false;
let firstError: string | null = null;
let latestSnapshotUpdatedAt: string | null = null;

for (const environmentId of enabledEnvironmentIds(get(input.catalogValueAtom))) {
const state = get(input.shellStateValueAtom(environmentId));
Expand All @@ -386,22 +381,6 @@ export function createEnvironmentShellSummaryAtom(input: {
continue;
}
hasSnapshot = true;
const snapshot = state.snapshot.value;
const updatedAt = snapshot.threads.concat(snapshot.archivedThreads).reduce<string | null>(
(latest, thread) => {
const value = DateTime.formatIso(thread.updatedAt);
return latest === null || value > latest ? value : latest;
},
snapshot.projects.reduce<string | null>((latest, project) => {
return latest === null || project.updatedAt > latest ? project.updatedAt : latest;
}, null),
);
if (
updatedAt !== null &&
(latestSnapshotUpdatedAt === null || updatedAt > latestSnapshotUpdatedAt)
) {
latestSnapshotUpdatedAt = updatedAt;
}
}

const next: EnvironmentShellSummary = {
Expand All @@ -410,7 +389,6 @@ export function createEnvironmentShellSummaryAtom(input: {
hasCachedShell,
hasLiveShell,
firstError,
latestSnapshotUpdatedAt,
};
if (shellSummariesEqual(previousSummary, next)) {
return previousSummary;
Expand Down
Loading