authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:05:24-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:05:24-07:00
log8112aff7c2dc8a82447920e6fde232565149d32c
tree574e792925e4c3153705b6b2090b655d97b2f0ed
parentb9e22de6a354a97c2ecc772ad00754bfbd7333e6
signature Signed by PGP key B5690EEEBB952194

[codex] Structure relay install confirmation conflicts (#3365)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 55 insertions(+), 4 deletions(-)

apps/web/src/cloud/relayClientInstallDialog.test.ts+25
......@@ -4,6 +4,7 @@ import {
44 completeRelayClientInstallDialogClose,
55 finishRelayClientInstall,
66 readRelayClientInstallDialogState,
7 RelayClientInstallConfirmationConflictError,
78 reportRelayClientInstallProgress,
89 requestRelayClientInstallConfirmation,
910 resetRelayClientInstallDialogForTests,
......@@ -67,4 +68,28 @@ describe("relay client install dialog coordinator", () => {
6768 completeRelayClientInstallDialogClose();
6869 expect(readRelayClientInstallDialogState()).toEqual({ status: "idle" });
6970 });
71
72 it("rejects concurrent confirmation with the active install state", async () => {
73 const confirmation = requestRelayClientInstallConfirmation("2026.5.2");
74 respondToRelayClientInstallConfirmation(true);
75 await expect(confirmation).resolves.toBe(true);
76 reportRelayClientInstallProgress({ type: "progress", stage: "downloading" });
77
78 const error = await requestRelayClientInstallConfirmation("2026.6.0").then(
79 () => undefined,
80 (cause: unknown) => cause,
81 );
82
83 expect(error).toBeInstanceOf(RelayClientInstallConfirmationConflictError);
84 expect(error).toMatchObject({
85 requestedVersion: "2026.6.0",
86 activeVersion: "2026.5.2",
87 activeDialogStatus: "installing",
88 activeInstallStage: "downloading",
89 });
90 expect(error).not.toHaveProperty("cause");
91 expect((error as Error).message).toBe(
92 "Cannot confirm relay client installation 2026.6.0; installation 2026.5.2 has dialog status installing.",
93 );
94 });
7095});
apps/web/src/cloud/relayClientInstallDialog.ts+30-4
......@@ -1,7 +1,23 @@
1import type {
2 RelayClientInstallProgressEvent,
3 RelayClientInstallProgressStage,
1import {
2 RelayClientInstallProgressStageSchema,
3 type RelayClientInstallProgressEvent,
4 type RelayClientInstallProgressStage,
45} from "@t3tools/contracts";
6import * as Schema from "effect/Schema";
7
8export class RelayClientInstallConfirmationConflictError extends Schema.TaggedErrorClass<RelayClientInstallConfirmationConflictError>()(
9 "RelayClientInstallConfirmationConflictError",
10 {
11 requestedVersion: Schema.String,
12 activeVersion: Schema.String,
13 activeDialogStatus: Schema.Literals(["confirming", "installing", "closing"]),
14 activeInstallStage: Schema.optional(RelayClientInstallProgressStageSchema),
15 },
16) {
17 override get message(): string {
18 return `Cannot confirm relay client installation ${this.requestedVersion}; installation ${this.activeVersion} has dialog status ${this.activeDialogStatus}.`;
19 }
20}
521
622export type RelayClientInstallDialogState =
723 | { readonly status: "idle" }
......@@ -47,7 +63,17 @@ export function subscribeRelayClientInstallDialog(listener: () => void): () => v
4763
4864export function requestRelayClientInstallConfirmation(version: string): Promise<boolean> {
4965 if (state.status !== "idle") {
50 return Promise.reject(new Error("A relay client installation is already in progress."));
66 const activeInstall = state.status === "closing" ? state.view : state;
67 return Promise.reject(
68 new RelayClientInstallConfirmationConflictError({
69 requestedVersion: version,
70 activeVersion: activeInstall.version,
71 activeDialogStatus: state.status,
72 ...(activeInstall.status === "installing"
73 ? { activeInstallStage: activeInstall.stage }
74 : {}),
75 }),
76 );
5177 }
5278
5379 publish({ status: "confirming", version });