| ... | ... | @@ -9,11 +9,9 @@ import { |
| 9 | 9 | } from "@t3tools/contracts"; |
| 10 | 10 | import * as Console from "effect/Console"; |
| 11 | 11 | import * as Crypto from "effect/Crypto"; |
| 12 | | import * as Data from "effect/Data"; |
| 13 | 12 | import * as DateTime from "effect/DateTime"; |
| 14 | 13 | import * as Duration from "effect/Duration"; |
| 15 | 14 | import * as Effect from "effect/Effect"; |
| 16 | | import * as Exit from "effect/Exit"; |
| 17 | 15 | import * as FileSystem from "effect/FileSystem"; |
| 18 | 16 | import * as Layer from "effect/Layer"; |
| 19 | 17 | import * as Option from "effect/Option"; |
| ... | ... | @@ -52,16 +50,64 @@ type ProjectCliDispatchCommand = Extract< |
| 52 | 50 | { type: "project.create" | "project.meta.update" | "project.delete" } |
| 53 | 51 | >; |
| 54 | 52 | |
| 55 | | class ProjectCommandError extends Data.TaggedError("ProjectCommandError")<{ |
| 56 | | readonly message: string; |
| 57 | | }> {} |
| 53 | const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); |
| 54 | const ProjectCommandOperation = Schema.Literals([ |
| 55 | "generateProjectCommandId", |
| 56 | "callLiveServer", |
| 57 | "validateProjectTitle", |
| 58 | "resolveProjectTarget", |
| 59 | "addProject", |
| 60 | ]); |
| 61 | |
| 62 | export class ProjectCommandError extends Schema.TaggedErrorClass<ProjectCommandError>()( |
| 63 | "ProjectCommandError", |
| 64 | { |
| 65 | operation: ProjectCommandOperation, |
| 66 | detail: Schema.String, |
| 67 | code: Schema.optional(Schema.String), |
| 68 | traceId: Schema.optional(Schema.String), |
| 69 | status: Schema.optional(Schema.Number), |
| 70 | cause: Schema.optional(Schema.Defect()), |
| 71 | }, |
| 72 | ) { |
| 73 | static fromLiveServerRequest(cause: unknown): ProjectCommandError { |
| 74 | if (isEnvironmentHttpCommonError(cause)) { |
| 75 | return new ProjectCommandError({ |
| 76 | operation: "callLiveServer", |
| 77 | detail: `Server request failed (${cause.code}, trace ${cause.traceId}).`, |
| 78 | code: cause.code, |
| 79 | traceId: cause.traceId, |
| 80 | cause, |
| 81 | }); |
| 82 | } |
| 83 | if (HttpClientError.isHttpClientError(cause) && cause.response !== undefined) { |
| 84 | return new ProjectCommandError({ |
| 85 | operation: "callLiveServer", |
| 86 | detail: `Server request failed with undeclared status ${cause.response.status}.`, |
| 87 | status: cause.response.status, |
| 88 | cause, |
| 89 | }); |
| 90 | } |
| 91 | return new ProjectCommandError({ |
| 92 | operation: "callLiveServer", |
| 93 | detail: "Failed to call the running server.", |
| 94 | cause, |
| 95 | }); |
| 96 | } |
| 97 | |
| 98 | override get message(): string { |
| 99 | return this.detail; |
| 100 | } |
| 101 | } |
| 58 | 102 | |
| 59 | 103 | const projectCommandUuid = Crypto.Crypto.pipe( |
| 60 | 104 | Effect.flatMap((crypto) => crypto.randomUUIDv4), |
| 61 | 105 | Effect.mapError( |
| 62 | | () => |
| 106 | (cause) => |
| 63 | 107 | new ProjectCommandError({ |
| 64 | | message: "Failed to generate a project command identifier.", |
| 108 | operation: "generateProjectCommandId", |
| 109 | detail: "Failed to generate a project command identifier.", |
| 110 | cause, |
| 65 | 111 | }), |
| 66 | 112 | ), |
| 67 | 113 | ); |
| ... | ... | @@ -75,7 +121,6 @@ const ProjectCliRuntimeLive = Layer.mergeAll( |
| 75 | 121 | ); |
| 76 | 122 | |
| 77 | 123 | const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1); |
| 78 | | const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); |
| 79 | 124 | const withProjectCliSessionToken = <A, E, R>( |
| 80 | 125 | environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"], |
| 81 | 126 | run: (token: string) => Effect.Effect<A, E, R>, |
| ... | ... | @@ -92,28 +137,6 @@ const withProjectCliSessionToken = <A, E, R>( |
| 92 | 137 | const withProjectCliLiveServerTimeout = <A, E, R>(effect: Effect.Effect<A, E, R>) => |
| 93 | 138 | effect.pipe(Effect.timeout(PROJECT_CLI_LIVE_SERVER_TIMEOUT)); |
| 94 | 139 | |
| 95 | | const failLiveServerRequest = (cause: unknown) => { |
| 96 | | if (isEnvironmentHttpCommonError(cause)) { |
| 97 | | return Effect.fail( |
| 98 | | new ProjectCommandError({ |
| 99 | | message: `Server request failed (${cause.code}, trace ${cause.traceId}).`, |
| 100 | | }), |
| 101 | | ); |
| 102 | | } |
| 103 | | if (HttpClientError.isHttpClientError(cause) && cause.response !== undefined) { |
| 104 | | return Effect.fail( |
| 105 | | new ProjectCommandError({ |
| 106 | | message: `Server request failed with undeclared status ${cause.response.status}.`, |
| 107 | | }), |
| 108 | | ); |
| 109 | | } |
| 110 | | return Effect.fail( |
| 111 | | new ProjectCommandError({ |
| 112 | | message: `Failed to call running server: ${String(cause)}.`, |
| 113 | | }), |
| 114 | | ); |
| 115 | | }; |
| 116 | | |
| 117 | 140 | const makeLiveServerClient = (origin: string) => |
| 118 | 141 | HttpApiClient.make(EnvironmentHttpApi, { |
| 119 | 142 | baseUrl: origin, |
| ... | ... | @@ -135,7 +158,10 @@ const resolveProjectTitle = Effect.fn("resolveProjectTitle")(function* ( |
| 135 | 158 | if (trimmed.length > 0) { |
| 136 | 159 | return trimmed; |
| 137 | 160 | } |
| 138 | | return yield* new ProjectCommandError({ message: "Project title cannot be empty." }); |
| 161 | return yield* new ProjectCommandError({ |
| 162 | operation: "validateProjectTitle", |
| 163 | detail: "Project title cannot be empty.", |
| 164 | }); |
| 139 | 165 | } |
| 140 | 166 | |
| 141 | 167 | const path = yield* Path.Path; |
| ... | ... | @@ -149,7 +175,10 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* ( |
| 149 | 175 | }) { |
| 150 | 176 | const trimmedIdentifier = input.identifier.trim(); |
| 151 | 177 | if (trimmedIdentifier.length === 0) { |
| 152 | | return yield* new ProjectCommandError({ message: "Project identifier cannot be empty." }); |
| 178 | return yield* new ProjectCommandError({ |
| 179 | operation: "resolveProjectTarget", |
| 180 | detail: "Project identifier cannot be empty.", |
| 181 | }); |
| 153 | 182 | } |
| 154 | 183 | |
| 155 | 184 | const activeProjects = input.snapshot.projects.filter((project) => project.deletedAt === null); |
| ... | ... | @@ -162,12 +191,11 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* ( |
| 162 | 191 | } satisfies ProjectMutationTarget; |
| 163 | 192 | } |
| 164 | 193 | |
| 165 | | const normalizedWorkspaceRootResult = yield* Effect.exit( |
| 194 | const normalizedWorkspaceRootResult = yield* Effect.result( |
| 166 | 195 | normalizeWorkspaceRootForProjectCommand(trimmedIdentifier), |
| 167 | 196 | ); |
| 168 | | const normalizedWorkspaceRoot = Exit.isSuccess(normalizedWorkspaceRootResult) |
| 169 | | ? normalizedWorkspaceRootResult.value |
| 170 | | : null; |
| 197 | const normalizedWorkspaceRoot = |
| 198 | normalizedWorkspaceRootResult._tag === "Success" ? normalizedWorkspaceRootResult.success : null; |
| 171 | 199 | |
| 172 | 200 | const exactWorkspaceMatch = |
| 173 | 201 | normalizedWorkspaceRoot === null |
| ... | ... | @@ -177,7 +205,11 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* ( |
| 177 | 205 | const resolved = exactWorkspaceMatch; |
| 178 | 206 | if (!resolved) { |
| 179 | 207 | return yield* new ProjectCommandError({ |
| 180 | | message: `No active project found for '${trimmedIdentifier}'.`, |
| 208 | operation: "resolveProjectTarget", |
| 209 | detail: `No active project found for '${trimmedIdentifier}'.`, |
| 210 | ...(normalizedWorkspaceRootResult._tag === "Failure" |
| 211 | ? { cause: normalizedWorkspaceRootResult.failure } |
| 212 | : {}), |
| 181 | 213 | }); |
| 182 | 214 | } |
| 183 | 215 | |
| ... | ... | @@ -194,7 +226,10 @@ const fetchLiveOrchestrationSnapshot = (origin: string, bearerToken: string) => |
| 194 | 226 | return yield* client.orchestration.snapshot({ |
| 195 | 227 | headers: { authorization: `Bearer ${bearerToken}` }, |
| 196 | 228 | }); |
| 197 | | }).pipe(withProjectCliLiveServerTimeout, Effect.catch(failLiveServerRequest)); |
| 229 | }).pipe( |
| 230 | withProjectCliLiveServerTimeout, |
| 231 | Effect.mapError(ProjectCommandError.fromLiveServerRequest), |
| 232 | ); |
| 198 | 233 | |
| 199 | 234 | const dispatchLiveOrchestrationCommand = ( |
| 200 | 235 | origin: string, |
| ... | ... | @@ -207,7 +242,10 @@ const dispatchLiveOrchestrationCommand = ( |
| 207 | 242 | headers: { authorization: `Bearer ${bearerToken}` }, |
| 208 | 243 | payload: command, |
| 209 | 244 | } as Parameters<typeof client.orchestration.dispatch>[0]); |
| 210 | | }).pipe(withProjectCliLiveServerTimeout, Effect.catch(failLiveServerRequest)); |
| 245 | }).pipe( |
| 246 | withProjectCliLiveServerTimeout, |
| 247 | Effect.mapError(ProjectCommandError.fromLiveServerRequest), |
| 248 | ); |
| 211 | 249 | |
| 212 | 250 | const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () { |
| 213 | 251 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; |
| ... | ... | @@ -232,11 +270,15 @@ const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecu |
| 232 | 270 | ), |
| 233 | 271 | ); |
| 234 | 272 | |
| 235 | | const attempted = yield* Effect.exit(attempt); |
| 236 | | if (Exit.isSuccess(attempted)) { |
| 237 | | return Option.some(attempted.value); |
| 273 | const attempted = yield* Effect.result(attempt); |
| 274 | if (attempted._tag === "Success") { |
| 275 | return Option.some(attempted.success); |
| 238 | 276 | } |
| 239 | 277 | |
| 278 | yield* Effect.logDebug("Failed to connect to the persisted project CLI server.", { |
| 279 | origin: runtimeState.value.origin, |
| 280 | cause: attempted.failure, |
| 281 | }); |
| 240 | 282 | yield* clearPersistedServerRuntimeState(config.serverRuntimeStatePath); |
| 241 | 283 | return Option.none<{ readonly origin: string }>(); |
| 242 | 284 | }, |
| ... | ... | @@ -335,7 +377,8 @@ const projectAddCommand = Command.make("add", { |
| 335 | 377 | ); |
| 336 | 378 | if (existingProject) { |
| 337 | 379 | return yield* new ProjectCommandError({ |
| 338 | | message: `An active project already exists for '${workspaceRoot}'.`, |
| 380 | operation: "addProject", |
| 381 | detail: `An active project already exists for '${workspaceRoot}'.`, |
| 339 | 382 | }); |
| 340 | 383 | } |
| 341 | 384 | |