1import * as Cause from "effect/Cause";
2import * as Effect from "effect/Effect";
3import * as Schema from "effect/Schema";
4import { RpcClientError } from "effect/rpc";
5
6import * as AcpSchema from "../_generated/schema.gen.ts";
7import * as AcpError from "../errors.ts";
8const isError = Schema.is(AcpSchema.Error);
9const isAcpError = Schema.is(AcpError.AcpError);
10
11export const callRpc = <A>(
12 method: string,
13 effect: Effect.Effect<A, RpcClientError.RpcClientError | AcpSchema.Error>,
14): Effect.Effect<A, AcpError.AcpError> =>
15 effect.pipe(
16 Effect.catchIf(isError, (error) =>
17 Effect.fail(AcpError.AcpRequestError.fromProtocolError(error, { method })),
18 ),
19 Effect.catchTags({
20 RpcClientError: (cause) =>
21 Effect.fail(
22 cause.reason._tag === "RpcClientDefect" && isAcpError(cause.reason.cause)
23 ? cause.reason.cause
24 : new AcpError.AcpTransportError({
25 operation: "call-rpc",
26 method,
27 cause,
28 }),
29 ),
30 }),
31 );
32
33/**
34 * Runs a notification handler so it cannot stop the caller: a typed failure is
35 * dropped, as notifications have no reply, and a defect is logged.
36 */
37export const isolateNotificationHandler = <E, R>(effect: Effect.Effect<void, E, R>) =>
38 effect.pipe(
39 Effect.catchCause((cause) =>
40 Cause.hasDies(cause)
41 ? Effect.logError("ACP notification handler failed", cause)
42 : Effect.void,
43 ),
44 );
45
46export const runHandler = Effect.fnUntraced(function* <A, B, Args extends ReadonlyArray<unknown>>(
47 handler: ((payload: A, ...args: Args) => Effect.Effect<B, AcpError.AcpError>) | undefined,
48 payload: A,
49 method: string,
50 ...args: Args
51) {
52 if (!handler) {
53 return yield* Effect.fail(AcpError.AcpRequestError.methodNotFound(method).toProtocolError());
54 }
55 return yield* handler(payload, ...args).pipe(
56 Effect.tapDefect((defect) =>
57 Effect.logError(`ACP request handler failed for '${method}'`, defect),
58 ),
59 Effect.mapError((error) =>
60 AcpError.AcpRequestError.fromCoreHandlerError(error, method).toProtocolError(),
61 ),
62 );
63});
64
65export function decodeExtRequestRegistration<A, I>(
66 method: string,
67 payload: Schema.Codec<A, I>,
68 handler: (
69 payload: A,
70 context: { readonly requestId: string; readonly method: string },
71 ) => Effect.Effect<unknown, AcpError.AcpError>,
72) {
73 return (
74 params: unknown,
75 context: { readonly requestId: string; readonly method: string },
76 ): Effect.Effect<unknown, AcpError.AcpError> =>
77 Schema.decodeUnknownEffect(payload)(params).pipe(
78 Effect.mapError((error) => AcpError.AcpRequestError.invalidExtensionPayload(method, error)),
79 Effect.flatMap((decoded) => handler(decoded, context)),
80 );
81}
82
83export function decodeExtNotificationRegistration<A, I>(
84 method: string,
85 payload: Schema.Codec<A, I>,
86 handler: (payload: A) => Effect.Effect<void, AcpError.AcpError>,
87) {
88 return (params: unknown): Effect.Effect<void, AcpError.AcpError> =>
89 Schema.decodeUnknownEffect(payload)(params).pipe(
90 Effect.mapError((error) =>
91 AcpError.AcpProtocolParseError.fromSchemaError(
92 "decode-notification-payload",
93 method,
94 error,
95 ),
96 ),
97 Effect.flatMap((decoded) => handler(decoded)),
98 );
99}
100
101const encoder = new TextEncoder();
102
103const JsonRpcId = Schema.Union([Schema.Number, Schema.String]);
104const JsonRpcHeaders = Schema.Array(Schema.Unknown);
105
106export const jsonRpcRequest = <A, I>(method: string, params: Schema.Codec<A, I>) =>
107 Schema.Struct({
108 jsonrpc: Schema.Literal("2.0"),
109 id: JsonRpcId,
110 method: Schema.Literal(method),
111 params,
112 headers: JsonRpcHeaders.pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
113 });
114
115export const jsonRpcNotification = <A, I>(method: string, params: Schema.Codec<A, I>) =>
116 Schema.Struct({
117 jsonrpc: Schema.Literal("2.0"),
118 method: Schema.Literal(method),
119 params,
120 });
121
122export const jsonRpcResponse = <A, I>(result: Schema.Codec<A, I>) =>
123 Schema.Struct({
124 jsonrpc: Schema.Literal("2.0"),
125 id: JsonRpcId,
126 result,
127 });
128
129export const encodeJsonl = <A, I>(schema: Schema.Codec<A, I>, value: A) =>
130 Effect.map(Schema.encodeEffect(Schema.fromJsonString(schema))(value), (encoded) =>
131 encoder.encode(`${encoded}\n`),
132 );