1import { EnvironmentId } from "@t3tools/contracts";
2import * as Schema from "effect/Schema";
3
4const ConnectionTargetBase = {
5 environmentId: EnvironmentId,
6 label: Schema.String,
7};
8
9export class PrimaryConnectionTarget extends Schema.TaggedClass<PrimaryConnectionTarget>()(
10 "PrimaryConnectionTarget",
11 {
12 ...ConnectionTargetBase,
13 httpBaseUrl: Schema.String,
14 wsBaseUrl: Schema.String,
15 },
16) {}
17
18export class BearerConnectionTarget extends Schema.TaggedClass<BearerConnectionTarget>()(
19 "BearerConnectionTarget",
20 {
21 ...ConnectionTargetBase,
22 connectionId: Schema.String,
23 },
24) {}
25
26export class RelayConnectionTarget extends Schema.TaggedClass<RelayConnectionTarget>()(
27 "RelayConnectionTarget",
28 {
29 ...ConnectionTargetBase,
30 },
31) {}
32
33export class SshConnectionTarget extends Schema.TaggedClass<SshConnectionTarget>()(
34 "SshConnectionTarget",
35 {
36 ...ConnectionTargetBase,
37 connectionId: Schema.String,
38 },
39) {}
40
41export const ConnectionTarget = Schema.Union([
42 PrimaryConnectionTarget,
43 BearerConnectionTarget,
44 RelayConnectionTarget,
45 SshConnectionTarget,
46]);
47export type ConnectionTarget = typeof ConnectionTarget.Type;
48
49export const PersistedConnectionTarget = Schema.Union([
50 BearerConnectionTarget,
51 RelayConnectionTarget,
52 SshConnectionTarget,
53]);
54export type PersistedConnectionTarget = typeof PersistedConnectionTarget.Type;
55
56export type ConnectionTargetKind = ConnectionTarget["_tag"];
57
58export type NetworkStatus = "unknown" | "offline" | "online";
59
60export const ConnectionTransientReason = Schema.Literals([
61 "network",
62 "timeout",
63 "transport",
64 "endpoint-unavailable",
65 "relay-unavailable",
66 "remote-unavailable",
67]);
68export type ConnectionTransientReason = typeof ConnectionTransientReason.Type;
69
70export const ConnectionBlockedReason = Schema.Literals([
71 "authentication",
72 "configuration",
73 "permission",
74 "unsupported",
75]);
76export type ConnectionBlockedReason = typeof ConnectionBlockedReason.Type;
77
78export class ConnectionTransientError extends Schema.TaggedError<ConnectionTransientError>()(
79 "ConnectionTransientError",
80 {
81 reason: ConnectionTransientReason,
82 detail: Schema.String,
83 traceId: Schema.optionalKey(Schema.String),
84 },
85) {
86 override get message(): string {
87 return this.detail;
88 }
89}
90
91export class ConnectionBlockedError extends Schema.TaggedError<ConnectionBlockedError>()(
92 "ConnectionBlockedError",
93 {
94 reason: ConnectionBlockedReason,
95 detail: Schema.String,
96 traceId: Schema.optionalKey(Schema.String),
97 /** The host speaks an older orchestration protocol; updating it restores the connection. */
98 serverUpdateRequired: Schema.optionalKey(Schema.Boolean),
99 },
100) {
101 override get message(): string {
102 return this.detail;
103 }
104}
105
106export type ConnectionAttemptError = ConnectionTransientError | ConnectionBlockedError;
107
108export const DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS = 60_000;
109
110export type PreparedHttpAuthorization =
111 | {
112 readonly _tag: "Bearer";
113 readonly token: string;
114 }
115 | {
116 readonly _tag: "Dpop";
117 readonly accessToken: string;
118 readonly expiresAtEpochMs: number;
119 };
120
121export interface PreparedConnection {
122 readonly environmentId: EnvironmentId;
123 readonly label: string;
124 readonly httpBaseUrl: string;
125 readonly socketUrl: string;
126 readonly httpAuthorization: PreparedHttpAuthorization | null;
127 readonly target: ConnectionTarget;
128}
129
130export type SupervisorConnectionPhase =
131 | "available"
132 | "offline"
133 | "connecting"
134 | "backoff"
135 | "connected"
136 | "blocked";
137
138export type ConnectionAttemptStage = "preparing" | "opening" | "synchronizing";
139
140export interface SupervisorConnectionState {
141 readonly desired: boolean;
142 readonly network: NetworkStatus;
143 readonly phase: SupervisorConnectionPhase;
144 readonly stage: ConnectionAttemptStage | null;
145 readonly attempt: number;
146 readonly generation: number;
147 readonly lastFailure: ConnectionAttemptError | null;
148 readonly retryAt: number | null;
149}
150
151export type ConnectionProjectionPhase = "disconnected" | "synchronizing" | "ready";
152
153export function connectionProjectionPhase(
154 state: SupervisorConnectionState,
155): ConnectionProjectionPhase {
156 switch (state.phase) {
157 case "connecting":
158 return "synchronizing";
159 case "connected":
160 return "ready";
161 case "available":
162 case "offline":
163 case "backoff":
164 case "blocked":
165 return "disconnected";
166 }
167}
168
169export const AVAILABLE_CONNECTION_STATE: SupervisorConnectionState = Object.freeze({
170 desired: false,
171 network: "unknown",
172 phase: "available",
173 stage: null,
174 attempt: 0,
175 generation: 0,
176 lastFailure: null,
177 retryAt: null,
178});