1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Schema from "effect/Schema";
5import * as Scope from "effect/Scope";
6
7export interface DesktopIpcInvokeEvent {
8 readonly sender: { readonly id: number };
9 readonly senderFrame?: { readonly url: string; readonly parent: unknown } | null;
10}
11
12export interface DesktopIpcSyncEvent {
13 returnValue: unknown;
14}
15
16export type DesktopIpcHandleListener = (
17 event: DesktopIpcInvokeEvent,
18 raw: unknown,
19) => unknown | Promise<unknown>;
20
21export type DesktopIpcSyncListener = (event: DesktopIpcSyncEvent) => void;
22
23export interface DesktopIpcMain {
24 removeHandler(channel: string): void;
25 handle(channel: string, listener: DesktopIpcHandleListener): void;
26 removeAllListeners(channel: string): void;
27 on(channel: string, listener: DesktopIpcSyncListener): void;
28}
29
30export class DesktopIpcRegistrationError extends Schema.TaggedError<DesktopIpcRegistrationError>()(
31 "DesktopIpcRegistrationError",
32 {
33 handlerKind: Schema.Literals(["invoke", "sync"]),
34 channel: Schema.String,
35 cause: Schema.Defect(),
36 },
37) {
38 override get message(): string {
39 return `Failed to register the ${this.handlerKind} IPC handler for ${this.channel}.`;
40 }
41}
42
43export class DesktopIpcUnregistrationError extends Schema.TaggedError<DesktopIpcUnregistrationError>()(
44 "DesktopIpcUnregistrationError",
45 {
46 handlerKind: Schema.Literals(["invoke", "sync"]),
47 channel: Schema.String,
48 cause: Schema.Defect(),
49 },
50) {
51 override get message(): string {
52 return `Failed to unregister the ${this.handlerKind} IPC handler for ${this.channel}.`;
53 }
54}
55
56export const DesktopIpcError = Schema.Union([
57 DesktopIpcRegistrationError,
58 DesktopIpcUnregistrationError,
59]);
60export type DesktopIpcError = typeof DesktopIpcError.Type;
61
62export interface DesktopIpcMethod<E, R> {
63 readonly channel: string;
64 readonly handler: (raw: unknown, event?: DesktopIpcInvokeEvent) => Effect.Effect<unknown, E, R>;
65}
66
67export interface DesktopSyncIpcMethod<E, R> {
68 readonly channel: string;
69 readonly handler: () => Effect.Effect<unknown, E, R>;
70}
71
72export class DesktopIpc extends Context.Service<
73 DesktopIpc,
74 {
75 readonly handle: <E, R>(
76 input: DesktopIpcMethod<E, R>,
77 ) => Effect.Effect<void, DesktopIpcRegistrationError, R | Scope.Scope>;
78 readonly handleSync: <E, R>(
79 input: DesktopSyncIpcMethod<E, R>,
80 ) => Effect.Effect<void, DesktopIpcRegistrationError, R | Scope.Scope>;
81 }
82>()("@t3tools/desktop/ipc/DesktopIpc") {}
83
84export const make = (ipcMain: DesktopIpcMain): DesktopIpc["Service"] =>
85 DesktopIpc.of({
86 handle: Effect.fn("desktop.ipc.registerInvoke")(function* <E, R>({
87 channel,
88 handler,
89 }: DesktopIpcMethod<E, R>) {
90 yield* Effect.annotateCurrentSpan({ channel });
91 const context = yield* Effect.context<R>();
92 const runPromise = Effect.runPromiseWith(context);
93
94 yield* Effect.acquireRelease(
95 Effect.try({
96 try: () => {
97 ipcMain.removeHandler(channel);
98 ipcMain.handle(channel, (event, raw) =>
99 runPromise(
100 Effect.gen(function* () {
101 yield* Effect.annotateCurrentSpan({ channel });
102 return yield* handler(raw, event);
103 }).pipe(Effect.annotateLogs({ channel }), Effect.withSpan("desktop.ipc.invoke")),
104 ),
105 );
106 },
107 catch: (cause) =>
108 new DesktopIpcRegistrationError({ handlerKind: "invoke", channel, cause }),
109 }),
110 () =>
111 Effect.try({
112 try: () => ipcMain.removeHandler(channel),
113 catch: (cause) =>
114 new DesktopIpcUnregistrationError({ handlerKind: "invoke", channel, cause }),
115 }).pipe(Effect.orDie),
116 );
117 }),
118
119 handleSync: Effect.fn("desktop.ipc.registerSync")(function* <E, R>({
120 channel,
121 handler,
122 }: DesktopSyncIpcMethod<E, R>) {
123 yield* Effect.annotateCurrentSpan({ channel });
124 const context = yield* Effect.context<R>();
125 const runSync = Effect.runSyncWith(context);
126
127 yield* Effect.acquireRelease(
128 Effect.try({
129 try: () => {
130 ipcMain.removeAllListeners(channel);
131 ipcMain.on(channel, (event) => {
132 event.returnValue = runSync(
133 Effect.gen(function* () {
134 yield* Effect.annotateCurrentSpan({ channel });
135 return yield* handler();
136 }).pipe(
137 Effect.annotateLogs({ channel }),
138 Effect.withSpan("desktop.ipc.invokeSync"),
139 ),
140 );
141 });
142 },
143 catch: (cause) =>
144 new DesktopIpcRegistrationError({ handlerKind: "sync", channel, cause }),
145 }),
146 () =>
147 Effect.try({
148 try: () => ipcMain.removeAllListeners(channel),
149 catch: (cause) =>
150 new DesktopIpcUnregistrationError({ handlerKind: "sync", channel, cause }),
151 }).pipe(Effect.orDie),
152 );
153 }),
154 });
155
156export const layer = (ipcMain: DesktopIpcMain) => Layer.succeed(DesktopIpc, make(ipcMain));
157
158/**
159 * Convenience helpers for creating IPC methods
160 */
161
162export interface DesktopIpcMethodRegistration<
163 Payload,
164 EncodedPayload,
165 Result,
166 EncodedResult,
167 E,
168 R,
169 PayloadDecodingServices = never,
170 PayloadEncodingServices = never,
171 ResultDecodingServices = never,
172 ResultEncodingServices = never,
173> {
174 readonly channel: string;
175 readonly payload: Schema.Codec<
176 Payload,
177 EncodedPayload,
178 PayloadDecodingServices,
179 PayloadEncodingServices
180 >;
181 readonly result: Schema.Codec<
182 Result,
183 EncodedResult,
184 ResultDecodingServices,
185 ResultEncodingServices
186 >;
187 readonly handler: (input: Payload, event?: DesktopIpcInvokeEvent) => Effect.Effect<Result, E, R>;
188}
189
190export const makeIpcMethod = <
191 Payload,
192 EncodedPayload,
193 Result,
194 EncodedResult,
195 E,
196 R,
197 PayloadDecodingServices = never,
198 PayloadEncodingServices = never,
199 ResultDecodingServices = never,
200 ResultEncodingServices = never,
201>(
202 method: DesktopIpcMethodRegistration<
203 Payload,
204 EncodedPayload,
205 Result,
206 EncodedResult,
207 E,
208 R,
209 PayloadDecodingServices,
210 PayloadEncodingServices,
211 ResultDecodingServices,
212 ResultEncodingServices
213 >,
214): DesktopIpcMethod<
215 E | Schema.SchemaError,
216 R | PayloadDecodingServices | ResultEncodingServices
217> => {
218 const decode = Schema.decodeUnknownEffect(method.payload);
219 const encode = Schema.encodeUnknownEffect(method.result);
220
221 return {
222 channel: method.channel,
223 handler: (raw, event) =>
224 decode(raw).pipe(
225 Effect.flatMap((input) => method.handler(input, event)),
226 Effect.flatMap(encode),
227 Effect.withSpan("desktop.ipc.method", { attributes: { channel: method.channel } }),
228 ),
229 };
230};
231
232export interface DesktopSyncIpcMethodRegistration<
233 Result,
234 EncodedResult,
235 E,
236 R,
237 ResultDecodingServices = never,
238 ResultEncodingServices = never,
239> {
240 readonly channel: string;
241 readonly result: Schema.Codec<
242 Result,
243 EncodedResult,
244 ResultDecodingServices,
245 ResultEncodingServices
246 >;
247 readonly handler: () => Effect.Effect<Result, E, R>;
248}
249
250export const makeSyncIpcMethod = <
251 Result,
252 EncodedResult,
253 E,
254 R,
255 ResultDecodingServices = never,
256 ResultEncodingServices = never,
257>(
258 method: DesktopSyncIpcMethodRegistration<
259 Result,
260 EncodedResult,
261 E,
262 R,
263 ResultDecodingServices,
264 ResultEncodingServices
265 >,
266): DesktopSyncIpcMethod<E | Schema.SchemaError, R | ResultEncodingServices> => {
267 const encode = Schema.encodeUnknownEffect(method.result);
268
269 return {
270 channel: method.channel,
271 handler: () =>
272 method
273 .handler()
274 .pipe(
275 Effect.flatMap(encode),
276 Effect.withSpan("desktop.ipc.method", { attributes: { channel: method.channel } }),
277 ),
278 };
279};