| 1 | import * as Schema from "effect/Schema"; |
| 2 | import type * as SchemaIssue from "effect/SchemaIssue"; |
| 3 | |
| 4 | import * as AcpSchema from "./_generated/schema.gen.ts"; |
| 5 | |
| 6 | const AcpRequestOperation = Schema.Literals([ |
| 7 | "decode-extension-request-payload", |
| 8 | "encode-extension-response", |
| 9 | "handle-request", |
| 10 | "handle-extension-request", |
| 11 | "receive-response", |
| 12 | "receive-streaming-response", |
| 13 | ]); |
| 14 | type AcpRequestOperation = typeof AcpRequestOperation.Type; |
| 15 | |
| 16 | export const AcpRequestId = Schema.Union([Schema.String, Schema.Number]); |
| 17 | export type AcpRequestId = typeof AcpRequestId.Type; |
| 18 | |
| 19 | const AcpSchemaIssueKind = Schema.Literals([ |
| 20 | "Filter", |
| 21 | "Encoding", |
| 22 | "Pointer", |
| 23 | "Composite", |
| 24 | "AnyOf", |
| 25 | "InvalidType", |
| 26 | "InvalidValue", |
| 27 | "MissingKey", |
| 28 | "UnexpectedKey", |
| 29 | "Forbidden", |
| 30 | "OneOf", |
| 31 | ]); |
| 32 | type AcpSchemaIssueKind = typeof AcpSchemaIssueKind.Type; |
| 33 | |
| 34 | interface AcpSchemaIssueDiagnostics { |
| 35 | readonly issueCount: number; |
| 36 | readonly issueKinds: ReadonlyArray<AcpSchemaIssueKind>; |
| 37 | readonly maximumPathDepth: number; |
| 38 | } |
| 39 | |
| 40 | const schemaIssueDiagnostics = (root: SchemaIssue.Issue): AcpSchemaIssueDiagnostics => { |
| 41 | let issueCount = 0; |
| 42 | let maximumPathDepth = 0; |
| 43 | const issueKinds = new Set<AcpSchemaIssueKind>(); |
| 44 | |
| 45 | const visit = (issue: SchemaIssue.Issue, pathDepth: number): void => { |
| 46 | issueCount += 1; |
| 47 | issueKinds.add(issue._tag); |
| 48 | maximumPathDepth = Math.max(maximumPathDepth, pathDepth); |
| 49 | switch (issue._tag) { |
| 50 | case "Filter": |
| 51 | case "Encoding": |
| 52 | visit(issue.issue, pathDepth); |
| 53 | break; |
| 54 | case "Pointer": |
| 55 | visit(issue.issue, pathDepth + issue.path.length); |
| 56 | break; |
| 57 | case "Composite": |
| 58 | case "AnyOf": |
| 59 | for (const child of issue.issues) visit(child, pathDepth); |
| 60 | break; |
| 61 | } |
| 62 | }; |
| 63 | |
| 64 | visit(root, 0); |
| 65 | return { |
| 66 | issueCount, |
| 67 | issueKinds: [...issueKinds], |
| 68 | maximumPathDepth, |
| 69 | }; |
| 70 | }; |
| 71 | |
| 72 | export interface AcpRequestDiagnostics { |
| 73 | readonly method?: string; |
| 74 | readonly requestId?: AcpRequestId; |
| 75 | readonly operation?: AcpRequestOperation; |
| 76 | readonly cause?: unknown; |
| 77 | readonly issueCount?: number; |
| 78 | readonly issueKinds?: ReadonlyArray<AcpSchemaIssueKind>; |
| 79 | readonly maximumPathDepth?: number; |
| 80 | } |
| 81 | |
| 82 | export class AcpSpawnError extends Schema.TaggedError<AcpSpawnError>()("AcpSpawnError", { |
| 83 | command: Schema.optional(Schema.String), |
| 84 | cause: Schema.Defect(), |
| 85 | }) { |
| 86 | override get message() { |
| 87 | return this.command |
| 88 | ? `Failed to spawn ACP process for command: ${this.command}` |
| 89 | : "Failed to spawn ACP process"; |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | export class AcpProcessExitedError extends Schema.TaggedError<AcpProcessExitedError>()( |
| 94 | "AcpProcessExitedError", |
| 95 | { |
| 96 | code: Schema.optional(Schema.Number), |
| 97 | pid: Schema.optionalKey(Schema.Int), |
| 98 | stderr: Schema.optionalKey(Schema.String), |
| 99 | cause: Schema.optional(Schema.Defect()), |
| 100 | }, |
| 101 | ) { |
| 102 | override get message() { |
| 103 | const base = |
| 104 | this.code === undefined ? "ACP process exited" : `ACP process exited with code ${this.code}`; |
| 105 | const excerpt = this.stderr?.trim(); |
| 106 | return excerpt && excerpt.length > 0 ? `${base}\n${excerpt}` : base; |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | export const AcpProtocolParseOperation = Schema.Literals([ |
| 111 | "encode-message", |
| 112 | "decode-wire-message", |
| 113 | "decode-notification-payload", |
| 114 | ]); |
| 115 | export type AcpProtocolParseOperation = typeof AcpProtocolParseOperation.Type; |
| 116 | |
| 117 | export class AcpProtocolParseError extends Schema.TaggedError<AcpProtocolParseError>()( |
| 118 | "AcpProtocolParseError", |
| 119 | { |
| 120 | operation: AcpProtocolParseOperation, |
| 121 | method: Schema.optionalKey(Schema.String), |
| 122 | requestId: Schema.optionalKey(AcpRequestId), |
| 123 | issueCount: Schema.optionalKey(Schema.Number), |
| 124 | issueKinds: Schema.optionalKey(Schema.Array(AcpSchemaIssueKind)), |
| 125 | maximumPathDepth: Schema.optionalKey(Schema.Number), |
| 126 | cause: Schema.Defect(), |
| 127 | }, |
| 128 | ) { |
| 129 | override get message() { |
| 130 | const method = this.method === undefined ? "" : ` for method '${this.method}'`; |
| 131 | return `ACP protocol operation '${this.operation}' failed${method}.`; |
| 132 | } |
| 133 | |
| 134 | static fromSchemaError( |
| 135 | operation: AcpProtocolParseOperation, |
| 136 | method: string, |
| 137 | cause: Schema.SchemaError, |
| 138 | ) { |
| 139 | return new AcpProtocolParseError({ |
| 140 | operation, |
| 141 | method, |
| 142 | ...schemaIssueDiagnostics(cause.issue), |
| 143 | cause, |
| 144 | }); |
| 145 | } |
| 146 | |
| 147 | static fromEncodingError( |
| 148 | method: string | undefined, |
| 149 | requestId: AcpRequestId | undefined, |
| 150 | cause: unknown, |
| 151 | ) { |
| 152 | return new AcpProtocolParseError({ |
| 153 | operation: "encode-message", |
| 154 | ...(method === undefined ? {} : { method }), |
| 155 | ...(requestId === undefined ? {} : { requestId }), |
| 156 | cause, |
| 157 | }); |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | export class AcpTransportError extends Schema.TaggedError<AcpTransportError>()( |
| 162 | "AcpTransportError", |
| 163 | { |
| 164 | operation: Schema.optional( |
| 165 | Schema.Literals(["call-rpc", "read-input-stream", "read-process-exit-status"]), |
| 166 | ), |
| 167 | method: Schema.optional(Schema.String), |
| 168 | detail: Schema.optional(Schema.String), |
| 169 | pid: Schema.optionalKey(Schema.Int), |
| 170 | cause: Schema.Defect(), |
| 171 | }, |
| 172 | ) { |
| 173 | override get message() { |
| 174 | const method = this.method ? ` for method ${this.method}` : ""; |
| 175 | return this.operation |
| 176 | ? `ACP transport operation ${this.operation} failed${method}.` |
| 177 | : "ACP transport operation failed."; |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | export class AcpInputStreamEndedError extends Schema.TaggedError<AcpInputStreamEndedError>()( |
| 182 | "AcpInputStreamEndedError", |
| 183 | {}, |
| 184 | ) { |
| 185 | override get message() { |
| 186 | return "ACP input stream ended."; |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | export class AcpRequestError extends Schema.TaggedError<AcpRequestError>()("AcpRequestError", { |
| 191 | code: AcpSchema.ErrorCode, |
| 192 | errorMessage: Schema.String, |
| 193 | data: Schema.optional(Schema.Unknown), |
| 194 | method: Schema.optionalKey(Schema.String), |
| 195 | requestId: Schema.optionalKey(AcpRequestId), |
| 196 | operation: Schema.optionalKey(AcpRequestOperation), |
| 197 | issueCount: Schema.optionalKey(Schema.Number), |
| 198 | issueKinds: Schema.optionalKey(Schema.Array(AcpSchemaIssueKind)), |
| 199 | maximumPathDepth: Schema.optionalKey(Schema.Number), |
| 200 | cause: Schema.optionalKey(Schema.Defect()), |
| 201 | }) { |
| 202 | override get message() { |
| 203 | return this.errorMessage; |
| 204 | } |
| 205 | |
| 206 | static fromProtocolError( |
| 207 | error: AcpSchema.Error, |
| 208 | context: { |
| 209 | readonly method: string; |
| 210 | readonly requestId?: AcpRequestId; |
| 211 | readonly cause?: unknown; |
| 212 | }, |
| 213 | ) { |
| 214 | return new AcpRequestError({ |
| 215 | code: error.code, |
| 216 | errorMessage: error.message, |
| 217 | ...(error.data !== undefined ? { data: error.data } : {}), |
| 218 | method: context.method, |
| 219 | ...(context.requestId === undefined ? {} : { requestId: context.requestId }), |
| 220 | operation: "receive-response", |
| 221 | cause: context.cause ?? error, |
| 222 | }); |
| 223 | } |
| 224 | |
| 225 | static fromExtensionResponseFailure(method: string, requestId: AcpRequestId, cause: unknown) { |
| 226 | return AcpRequestError.internalError("Extension request failed", undefined, { |
| 227 | method, |
| 228 | requestId, |
| 229 | operation: "receive-response", |
| 230 | cause, |
| 231 | }); |
| 232 | } |
| 233 | |
| 234 | static fromExtensionResponseEncodingError( |
| 235 | method: string, |
| 236 | requestId: AcpRequestId, |
| 237 | cause: AcpProtocolParseError, |
| 238 | ) { |
| 239 | return AcpRequestError.internalError("Internal error", undefined, { |
| 240 | method, |
| 241 | requestId, |
| 242 | operation: "encode-extension-response", |
| 243 | cause, |
| 244 | }); |
| 245 | } |
| 246 | |
| 247 | static unsupportedStreamingResponse(method: string, requestId: AcpRequestId) { |
| 248 | return AcpRequestError.internalError( |
| 249 | "Streaming extension responses are not supported", |
| 250 | undefined, |
| 251 | { |
| 252 | method, |
| 253 | requestId, |
| 254 | operation: "receive-streaming-response", |
| 255 | }, |
| 256 | ); |
| 257 | } |
| 258 | |
| 259 | static fromCoreHandlerError(error: AcpError, method: string) { |
| 260 | if (error._tag === "AcpRequestError") { |
| 261 | return error; |
| 262 | } |
| 263 | return AcpRequestError.internalError( |
| 264 | `ACP request handler failed for method '${method}'`, |
| 265 | undefined, |
| 266 | { |
| 267 | method, |
| 268 | operation: "handle-request", |
| 269 | cause: error, |
| 270 | }, |
| 271 | ); |
| 272 | } |
| 273 | |
| 274 | static fromExtensionHandlerError(error: AcpError, method: string) { |
| 275 | if (error._tag === "AcpRequestError") { |
| 276 | return error; |
| 277 | } |
| 278 | return AcpRequestError.internalError( |
| 279 | `ACP extension request handler failed for method '${method}'`, |
| 280 | undefined, |
| 281 | { |
| 282 | method, |
| 283 | operation: "handle-extension-request", |
| 284 | cause: error, |
| 285 | }, |
| 286 | ); |
| 287 | } |
| 288 | |
| 289 | static parseError(message = "Parse error", data?: unknown) { |
| 290 | return new AcpRequestError({ |
| 291 | code: -32700, |
| 292 | errorMessage: message, |
| 293 | ...(data !== undefined ? { data } : {}), |
| 294 | }); |
| 295 | } |
| 296 | |
| 297 | static invalidRequest(message = "Invalid request", data?: unknown) { |
| 298 | return new AcpRequestError({ |
| 299 | code: -32600, |
| 300 | errorMessage: message, |
| 301 | ...(data !== undefined ? { data } : {}), |
| 302 | }); |
| 303 | } |
| 304 | |
| 305 | static methodNotFound(method: string) { |
| 306 | return new AcpRequestError({ |
| 307 | code: -32601, |
| 308 | errorMessage: `Method not found: ${method}`, |
| 309 | }); |
| 310 | } |
| 311 | |
| 312 | static invalidParams(message = "Invalid params", data?: unknown) { |
| 313 | return new AcpRequestError({ |
| 314 | code: -32602, |
| 315 | errorMessage: message, |
| 316 | ...(data !== undefined ? { data } : {}), |
| 317 | }); |
| 318 | } |
| 319 | |
| 320 | static invalidExtensionPayload(method: string, cause: Schema.SchemaError) { |
| 321 | const diagnostics = schemaIssueDiagnostics(cause.issue); |
| 322 | return new AcpRequestError({ |
| 323 | code: -32602, |
| 324 | errorMessage: `Invalid payload for ACP extension method '${method}'.`, |
| 325 | data: diagnostics, |
| 326 | method, |
| 327 | operation: "decode-extension-request-payload", |
| 328 | ...diagnostics, |
| 329 | cause, |
| 330 | }); |
| 331 | } |
| 332 | |
| 333 | static internalError( |
| 334 | message = "Internal error", |
| 335 | data?: unknown, |
| 336 | diagnostics: AcpRequestDiagnostics = {}, |
| 337 | ) { |
| 338 | return new AcpRequestError({ |
| 339 | code: -32603, |
| 340 | errorMessage: message, |
| 341 | ...(data !== undefined ? { data } : {}), |
| 342 | ...diagnostics, |
| 343 | }); |
| 344 | } |
| 345 | |
| 346 | static authRequired(message = "Authentication required", data?: unknown) { |
| 347 | return new AcpRequestError({ |
| 348 | code: -32000, |
| 349 | errorMessage: message, |
| 350 | ...(data !== undefined ? { data } : {}), |
| 351 | }); |
| 352 | } |
| 353 | |
| 354 | static resourceNotFound(message = "Resource not found", data?: unknown) { |
| 355 | return new AcpRequestError({ |
| 356 | code: -32002, |
| 357 | errorMessage: message, |
| 358 | ...(data !== undefined ? { data } : {}), |
| 359 | }); |
| 360 | } |
| 361 | |
| 362 | toProtocolError() { |
| 363 | return AcpSchema.Error.make({ |
| 364 | code: this.code, |
| 365 | message: this.errorMessage, |
| 366 | ...(this.data !== undefined ? { data: this.data } : {}), |
| 367 | }); |
| 368 | } |
| 369 | } |
| 370 | |
| 371 | export const AcpError = Schema.Union([ |
| 372 | AcpRequestError, |
| 373 | AcpSpawnError, |
| 374 | AcpProcessExitedError, |
| 375 | AcpProtocolParseError, |
| 376 | AcpTransportError, |
| 377 | AcpInputStreamEndedError, |
| 378 | ]); |
| 379 | |
| 380 | export type AcpError = typeof AcpError.Type; |