1import type * as Context from "effect/Context";
2import * as Layer from "effect/Layer";
3import type * as Rpc from "effect/rpc/Rpc";
4import * as RpcClient from "effect/rpc/RpcClient";
5import type { RpcClientError } from "effect/rpc/RpcClientError";
6import type * as RpcGroup from "effect/rpc/RpcGroup";
7import * as RpcSerialization from "effect/rpc/RpcSerialization";
8import * as Socket from "effect/socket/Socket";
9
10export interface LayerOptions {
11 /** Options passed to Effect's WebSocket transport. */
12 readonly socket?: Parameters<typeof Socket.layerWebSocket>[1];
13 /** Options passed to Effect's socket RPC protocol. */
14 readonly protocol?: Parameters<typeof RpcClient.layerProtocolSocket>[0];
15 /** Options passed to the grouped Effect RPC client. */
16 readonly client?: Omit<
17 NonNullable<Parameters<typeof RpcClient.make>[1]>,
18 "flatten"
19 >;
20 /**
21 * Serialization matching the server. Alchemy's RpcDurableObject uses JSON.
22 * @default RpcSerialization.json
23 */
24 readonly serialization?: RpcSerialization.RpcSerialization["Service"];
25}
26
27/**
28 * Provide a typed RPC client and its WebSocket connection for the Layer's lifetime.
29 *
30 * Import from `alchemy/Cloudflare/RpcWebSocketClient` in browsers to avoid
31 * Cloudflare server modules. Provide `Socket.layerWebSocketConstructorGlobal`
32 * in a browser, or the corresponding constructor Layer for another platform.
33 * RPC client middleware remains an explicit dependency.
34 *
35 * Provide this Layer around the application or session that shares the client.
36 * Releasing the Layer closes the connection on success, failure, or interruption.
37 * Ordinary calls need no caller Scope; streaming calls using `{ asQueue: true }`
38 * still require a Scope for the queue consumer. Inside a Cloudflare Worker,
39 * provide this Layer per request, not in the isolate-scoped initializer.
40 *
41 * ### Browser Client
42 * **Example:** Provide a client for the application's program
43 * ```typescript
44 * import * as RpcWebSocketClient from "alchemy/Cloudflare/RpcWebSocketClient";
45 * import { Context, Effect, Layer } from "effect";
46 * import type * as RpcClient from "effect/rpc/RpcClient";
47 * import type { RpcClientError } from "effect/rpc/RpcClientError";
48 * import * as Socket from "effect/socket/Socket";
49 * import { CounterRpcs } from "./rpcs.ts";
50 *
51 * class CounterClient extends Context.Service<
52 * CounterClient,
53 * RpcClient.FromGroup<typeof CounterRpcs, RpcClientError>
54 * >()("CounterClient") {}
55 *
56 * const ClientLive = RpcWebSocketClient.layer(
57 * CounterClient,
58 * CounterRpcs,
59 * "wss://example.com/counters/alice",
60 * ).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal));
61 *
62 * const program = Effect.gen(function* () {
63 * const counter = yield* CounterClient;
64 * return yield* counter.increment();
65 * }).pipe(Effect.provide(ClientLive));
66 * ```
67 *
68 * @layer
69 * @provides service
70 * @product Workers
71 * @category Workers & Compute
72 */
73export const layer = <I, Rpcs extends Rpc.Any>(
74 service: Context.Key<I, RpcClient.RpcClient<Rpcs, RpcClientError>>,
75 group: RpcGroup.RpcGroup<Rpcs>,
76 url: Parameters<typeof Socket.layerWebSocket>[0],
77 options: LayerOptions = {},
78): Layer.Layer<
79 I,
80 never,
81 Socket.WebSocketConstructor | Rpc.MiddlewareClient<Rpcs>
82> =>
83 Layer.effect(service, RpcClient.make(group, options.client)).pipe(
84 Layer.provide(
85 RpcClient.layerProtocolSocket(options.protocol).pipe(
86 Layer.provide(
87 Layer.mergeAll(
88 Socket.layerWebSocket(url, options.socket),
89 Layer.succeed(
90 RpcSerialization.RpcSerialization,
91 options.serialization ?? RpcSerialization.json,
92 ),
93 ),
94 ),
95 ),
96 ),
97 );