| 1 | import * as Context from "effect/Context"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; |
| 4 | import * as Schema from "effect/Schema"; |
| 5 | import * as Scope from "effect/Scope"; |
| 6 | |
| 7 | export interface DesktopIpcInvokeEvent { |
| 8 | readonly sender: { readonly id: number }; |
| 9 | readonly senderFrame?: { readonly url: string; readonly parent: unknown } | null; |
| 10 | } |
| 11 | |
| 12 | export interface DesktopIpcSyncEvent { |
| 13 | returnValue: unknown; |
| 14 | } |
| 15 | |
| 16 | export type DesktopIpcHandleListener = ( |
| 17 | event: DesktopIpcInvokeEvent, |
| 18 | raw: unknown, |
| 19 | ) => unknown | Promise<unknown>; |
| 20 | |
| 21 | export type DesktopIpcSyncListener = (event: DesktopIpcSyncEvent) => void; |
| 22 | |
| 23 | export 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 | |
| 30 | export 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 | |
| 43 | export 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 | |
| 56 | export const DesktopIpcError = Schema.Union([ |
| 57 | DesktopIpcRegistrationError, |
| 58 | DesktopIpcUnregistrationError, |
| 59 | ]); |
| 60 | export type DesktopIpcError = typeof DesktopIpcError.Type; |
| 61 | |
| 62 | export interface DesktopIpcMethod<E, R> { |
| 63 | readonly channel: string; |
| 64 | readonly handler: (raw: unknown, event?: DesktopIpcInvokeEvent) => Effect.Effect<unknown, E, R>; |
| 65 | } |
| 66 | |
| 67 | export interface DesktopSyncIpcMethod<E, R> { |
| 68 | readonly channel: string; |
| 69 | readonly handler: () => Effect.Effect<unknown, E, R>; |
| 70 | } |
| 71 | |
| 72 | export 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 | |
| 84 | export 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 | |
| 156 | export const layer = (ipcMain: DesktopIpcMain) => Layer.succeed(DesktopIpc, make(ipcMain)); |
| 157 | |
| 158 | /** |
| 159 | * Convenience helpers for creating IPC methods |
| 160 | */ |
| 161 | |
| 162 | export 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 | |
| 190 | export 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 | |
| 232 | export 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 | |
| 250 | export 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 | }; |