Skip to content
Open
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
3 changes: 2 additions & 1 deletion apps/web/src/components/ThreadRouteView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useEnvironmentQuery } from "../state/query";
import { environmentShell } from "../state/shell";
import {
buildThreadRouteParams,
isThreadRouteSnapshotAuthoritative,
resolveThreadRouteRenderState,
type ThreadRouteTarget,
} from "../threadRoutes";
Expand Down Expand Up @@ -72,7 +73,7 @@ export function ThreadRouteView({ target }: { target: ThreadRouteTarget }) {
);
const serverThreadShell = serverThread;
const environmentThreadRefs = useEnvironmentThreadRefs(serverThreadRef?.environmentId ?? null);
const bootstrapComplete = shell.data?.snapshot._tag === "Some";
const bootstrapComplete = isThreadRouteSnapshotAuthoritative(shell.data?.status);
const draftThread = useComposerDraftStore((store) =>
serverThreadRef ? store.getDraftThreadByRef(serverThreadRef) : null,
);
Expand Down
38 changes: 38 additions & 0 deletions apps/web/src/threadRoutes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@ import { DraftId } from "./composerDraftStore";
import {
buildDraftThreadRouteParams,
buildThreadRouteParams,
isThreadRouteSnapshotAuthoritative,
resolveActiveThreadRouteRef,
resolveThreadRouteRenderState,
resolveThreadRouteRef,
resolveThreadRouteTarget,
} from "./threadRoutes";

describe("threadRoutes", () => {
it("does not treat cached thread lists as authoritative for deep links", () => {
expect(isThreadRouteSnapshotAuthoritative("empty")).toBe(false);
expect(isThreadRouteSnapshotAuthoritative("cached")).toBe(false);
expect(isThreadRouteSnapshotAuthoritative("synchronizing")).toBe(false);
expect(isThreadRouteSnapshotAuthoritative("live")).toBe(true);
});

it("builds canonical thread route params from a scoped ref", () => {
const ref = scopeThreadRef("env-1" as never, ThreadId.make("thread-1"));

Expand Down Expand Up @@ -124,6 +132,25 @@ describe("threadRoutes", () => {
).toBe("ready");
});

it.each([
{ status: "cached", source: "server thread" },
{ status: "synchronizing", source: "server thread" },
{ status: "cached", source: "draft" },
{ status: "synchronizing", source: "draft" },
] as const)(
"keeps available $source visible while the shell is $status",
({ status, source }) => {
expect(
resolveThreadRouteRenderState({
bootstrapComplete: isThreadRouteSnapshotAuthoritative(status),
serverThreadExists: source === "server thread",
serverThreadDeleted: false,
draftThreadExists: source === "draft",
}),
).toBe("ready");
},
);

it("distinguishes bootstrap loading from a missing thread", () => {
expect(
resolveThreadRouteRenderState({
Expand Down Expand Up @@ -153,4 +180,15 @@ describe("threadRoutes", () => {
}),
).toBe("missing");
});

it("waits for the live shell before redirecting a deleted thread", () => {
expect(
resolveThreadRouteRenderState({
bootstrapComplete: isThreadRouteSnapshotAuthoritative("cached"),
serverThreadExists: true,
serverThreadDeleted: true,
draftThreadExists: false,
}),
).toBe("loading");
});
});
18 changes: 11 additions & 7 deletions apps/web/src/threadRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { scopeThreadRef } from "@t3tools/client-runtime/environment";
import type { EnvironmentShellStatus } from "@t3tools/client-runtime/state/shell";
import type { EnvironmentId, ScopedThreadRef, ThreadId } from "@t3tools/contracts";
import type { DraftId } from "./composerDraftStore";

Expand All @@ -20,22 +21,25 @@ type DraftThreadRouteState = {

export type ThreadRouteRenderState = "loading" | "ready" | "missing";

export function isThreadRouteSnapshotAuthoritative(
status: EnvironmentShellStatus | undefined,
): boolean {
return status === "live";
}

export function resolveThreadRouteRenderState(input: {
bootstrapComplete: boolean;
serverThreadExists: boolean;
serverThreadDeleted: boolean;
draftThreadExists: boolean;
}): ThreadRouteRenderState {
if (!input.bootstrapComplete) {
return "loading";
}
if (input.draftThreadExists) {
if (input.draftThreadExists || (input.serverThreadExists && !input.serverThreadDeleted)) {
return "ready";
}
if (input.serverThreadDeleted) {
return "missing";
if (!input.bootstrapComplete) {
return "loading";
}
return input.serverThreadExists ? "ready" : "missing";
return "missing";
}

export function buildThreadRouteParams(ref: ScopedThreadRef): {
Expand Down
Loading