import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; export interface DesktopIpcInvokeEvent {} export interface DesktopIpcSyncEvent { returnValue: unknown; } export type DesktopIpcHandleListener = ( event: DesktopIpcInvokeEvent, raw: unknown, ) => unknown | Promise; export type DesktopIpcSyncListener = (event: DesktopIpcSyncEvent) => void; export interface DesktopIpcMain { removeHandler(channel: string): void; handle(channel: string, listener: DesktopIpcHandleListener): void; removeAllListeners(channel: string): void; on(channel: string, listener: DesktopIpcSyncListener): void; } export class DesktopIpcRegistrationError extends Schema.TaggedErrorClass()( "DesktopIpcRegistrationError", { handlerKind: Schema.Literals(["invoke", "sync"]), channel: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to register the ${this.handlerKind} IPC handler for ${this.channel}.`; } } export class DesktopIpcUnregistrationError extends Schema.TaggedErrorClass()( "DesktopIpcUnregistrationError", { handlerKind: Schema.Literals(["invoke", "sync"]), channel: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to unregister the ${this.handlerKind} IPC handler for ${this.channel}.`; } } export const DesktopIpcError = Schema.Union([ DesktopIpcRegistrationError, DesktopIpcUnregistrationError, ]); export type DesktopIpcError = typeof DesktopIpcError.Type; export const isDesktopIpcError = Schema.is(DesktopIpcError); export interface DesktopIpcMethod { readonly channel: string; readonly handler: (raw: unknown) => Effect.Effect; } export interface DesktopSyncIpcMethod { readonly channel: string; readonly handler: () => Effect.Effect; } export class DesktopIpc extends Context.Service< DesktopIpc, { readonly handle: ( input: DesktopIpcMethod, ) => Effect.Effect; readonly handleSync: ( input: DesktopSyncIpcMethod, ) => Effect.Effect; } >()("@t3tools/desktop/ipc/DesktopIpc") {} export const make = (ipcMain: DesktopIpcMain): DesktopIpc["Service"] => DesktopIpc.of({ handle: Effect.fn("desktop.ipc.registerInvoke")(function* ({ channel, handler, }: DesktopIpcMethod) { yield* Effect.annotateCurrentSpan({ channel }); const context = yield* Effect.context(); const runPromise = Effect.runPromiseWith(context); yield* Effect.acquireRelease( Effect.try({ try: () => { ipcMain.removeHandler(channel); ipcMain.handle(channel, (_event, raw) => runPromise( Effect.gen(function* () { yield* Effect.annotateCurrentSpan({ channel }); return yield* handler(raw); }).pipe(Effect.annotateLogs({ channel }), Effect.withSpan("desktop.ipc.invoke")), ), ); }, catch: (cause) => new DesktopIpcRegistrationError({ handlerKind: "invoke", channel, cause }), }), () => Effect.try({ try: () => ipcMain.removeHandler(channel), catch: (cause) => new DesktopIpcUnregistrationError({ handlerKind: "invoke", channel, cause }), }).pipe(Effect.orDie), ); }), handleSync: Effect.fn("desktop.ipc.registerSync")(function* ({ channel, handler, }: DesktopSyncIpcMethod) { yield* Effect.annotateCurrentSpan({ channel }); const context = yield* Effect.context(); const runSync = Effect.runSyncWith(context); yield* Effect.acquireRelease( Effect.try({ try: () => { ipcMain.removeAllListeners(channel); ipcMain.on(channel, (event) => { event.returnValue = runSync( Effect.gen(function* () { yield* Effect.annotateCurrentSpan({ channel }); return yield* handler(); }).pipe( Effect.annotateLogs({ channel }), Effect.withSpan("desktop.ipc.invokeSync"), ), ); }); }, catch: (cause) => new DesktopIpcRegistrationError({ handlerKind: "sync", channel, cause }), }), () => Effect.try({ try: () => ipcMain.removeAllListeners(channel), catch: (cause) => new DesktopIpcUnregistrationError({ handlerKind: "sync", channel, cause }), }).pipe(Effect.orDie), ); }), }); export const layer = (ipcMain: DesktopIpcMain) => Layer.succeed(DesktopIpc, make(ipcMain)); /** * Convenience helpers for creating IPC methods */ export interface DesktopIpcMethodRegistration< Payload, EncodedPayload, Result, EncodedResult, E, R, PayloadDecodingServices = never, PayloadEncodingServices = never, ResultDecodingServices = never, ResultEncodingServices = never, > { readonly channel: string; readonly payload: Schema.Codec< Payload, EncodedPayload, PayloadDecodingServices, PayloadEncodingServices >; readonly result: Schema.Codec< Result, EncodedResult, ResultDecodingServices, ResultEncodingServices >; readonly handler: (input: Payload) => Effect.Effect; } export const makeIpcMethod = < Payload, EncodedPayload, Result, EncodedResult, E, R, PayloadDecodingServices = never, PayloadEncodingServices = never, ResultDecodingServices = never, ResultEncodingServices = never, >( method: DesktopIpcMethodRegistration< Payload, EncodedPayload, Result, EncodedResult, E, R, PayloadDecodingServices, PayloadEncodingServices, ResultDecodingServices, ResultEncodingServices >, ): DesktopIpcMethod< E | Schema.SchemaError, R | PayloadDecodingServices | ResultEncodingServices > => { const decode = Schema.decodeUnknownEffect(method.payload); const encode = Schema.encodeUnknownEffect(method.result); return { channel: method.channel, handler: (raw) => decode(raw).pipe( Effect.flatMap(method.handler), Effect.flatMap(encode), Effect.withSpan("desktop.ipc.method", { attributes: { channel: method.channel } }), ), }; }; export interface DesktopSyncIpcMethodRegistration< Result, EncodedResult, E, R, ResultDecodingServices = never, ResultEncodingServices = never, > { readonly channel: string; readonly result: Schema.Codec< Result, EncodedResult, ResultDecodingServices, ResultEncodingServices >; readonly handler: () => Effect.Effect; } export const makeSyncIpcMethod = < Result, EncodedResult, E, R, ResultDecodingServices = never, ResultEncodingServices = never, >( method: DesktopSyncIpcMethodRegistration< Result, EncodedResult, E, R, ResultDecodingServices, ResultEncodingServices >, ): DesktopSyncIpcMethod => { const encode = Schema.encodeUnknownEffect(method.result); return { channel: method.channel, handler: () => method .handler() .pipe( Effect.flatMap(encode), Effect.withSpan("desktop.ipc.method", { attributes: { channel: method.channel } }), ), }; };