1import {
2 RelayClientInstallProgressStageSchema,
3 type RelayClientInstallProgressEvent,
4 type RelayClientInstallProgressStage,
5} from "@t3tools/contracts";
6import * as Schema from "effect/Schema";
7
8export class RelayClientInstallConfirmationConflictError extends Schema.TaggedError<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}
21
22export type RelayClientInstallDialogState =
23 | { readonly status: "idle" }
24 | { readonly status: "confirming"; readonly version: string }
25 | {
26 readonly status: "installing";
27 readonly version: string;
28 readonly stage: RelayClientInstallProgressStage;
29 }
30 | {
31 readonly status: "closing";
32 readonly view:
33 | { readonly status: "confirming"; readonly version: string }
34 | {
35 readonly status: "installing";
36 readonly version: string;
37 readonly stage: RelayClientInstallProgressStage;
38 };
39 };
40
41const idleState: RelayClientInstallDialogState = { status: "idle" };
42let state: RelayClientInstallDialogState = idleState;
43let resolveConfirmation: ((confirmed: boolean) => void) | null = null;
44const listeners = new Set<() => void>();
45
46function publish(next: RelayClientInstallDialogState) {
47 state = next;
48 for (const listener of listeners) {
49 listener();
50 }
51}
52
53export function readRelayClientInstallDialogState(): RelayClientInstallDialogState {
54 return state;
55}
56
57export function subscribeRelayClientInstallDialog(listener: () => void): () => void {
58 listeners.add(listener);
59 return () => {
60 listeners.delete(listener);
61 };
62}
63
64export function requestRelayClientInstallConfirmation(version: string): Promise<boolean> {
65 if (state.status !== "idle") {
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 );
77 }
78
79 publish({ status: "confirming", version });
80 return new Promise<boolean>((resolve) => {
81 resolveConfirmation = resolve;
82 });
83}
84
85export function respondToRelayClientInstallConfirmation(confirmed: boolean): void {
86 if (state.status !== "confirming" || !resolveConfirmation) {
87 return;
88 }
89
90 const resolve = resolveConfirmation;
91 resolveConfirmation = null;
92 publish(
93 confirmed
94 ? { status: "installing", version: state.version, stage: "checking" }
95 : { status: "closing", view: state },
96 );
97 resolve(confirmed);
98}
99
100export function reportRelayClientInstallProgress(event: RelayClientInstallProgressEvent): void {
101 if (state.status !== "installing" || event.type !== "progress") {
102 return;
103 }
104 publish({ ...state, stage: event.stage });
105}
106
107export function finishRelayClientInstall(): void {
108 resolveConfirmation?.(false);
109 resolveConfirmation = null;
110 if (state.status === "confirming" || state.status === "installing") {
111 publish({ status: "closing", view: state });
112 }
113}
114
115export function completeRelayClientInstallDialogClose(): void {
116 if (state.status === "closing") {
117 publish(idleState);
118 }
119}
120
121export function resetRelayClientInstallDialogForTests(): void {
122 resolveConfirmation?.(false);
123 resolveConfirmation = null;
124 publish(idleState);
125 listeners.clear();
126}