electron-effect-rpc 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,21 @@ and let the workspace resolver handle the rest.
30
30
 
31
31
  ## Core Concepts
32
32
 
33
- ### Methods and events
33
+ ### Communication directions
34
+
35
+ This library provides two communication patterns for Electron IPC:
36
+
37
+ | Direction | Mechanism | Pattern | Use case |
38
+ |-----------|-----------|---------|----------|
39
+ | **Renderer → Main** | RPC methods | Request/response | Fetching data, triggering actions, calling main process APIs |
40
+ | **Main → Renderer** | Event bus | Push/broadcast | Progress updates, state changes, background task notifications |
41
+
42
+ **RPC methods** are for when the renderer needs something from the main process. The renderer calls a method and awaits a typed response.
43
+
44
+ **Events** are for when the main process needs to notify the renderer. The main process emits events whenever it wants, and the renderer subscribes to receive them.
45
+
46
+ ### Defining methods and events
47
+
34
48
  Define methods and events using schema-based helpers:
35
49
 
36
50
  ```ts
@@ -0,0 +1,40 @@
1
+ import * as S from "@effect/schema/Schema";
2
+ export type SchemaNoContext = S.Schema.AnyNoContext;
3
+ export declare const NoError: typeof S.Never;
4
+ export type NoError = typeof NoError;
5
+ export type ErrorSchema = SchemaNoContext | S.Schema<never, never, never>;
6
+ export interface RpcMethod<Name extends string, Req extends SchemaNoContext, Res extends SchemaNoContext, Err extends ErrorSchema = NoError> {
7
+ readonly name: Name;
8
+ readonly req: Req;
9
+ readonly res: Res;
10
+ readonly err: Err;
11
+ }
12
+ export declare function rpc<const Name extends string, Req extends SchemaNoContext, Res extends SchemaNoContext, Err extends ErrorSchema>(name: Name, req: Req, res: Res, err: Err): RpcMethod<Name, Req, Res, Err>;
13
+ export declare function rpc<const Name extends string, Req extends SchemaNoContext, Res extends SchemaNoContext>(name: Name, req: Req, res: Res): RpcMethod<Name, Req, Res, NoError>;
14
+ export interface RpcEvent<Payload extends SchemaNoContext, Context extends SchemaNoContext | null, Name extends string = string> {
15
+ readonly name: Name;
16
+ readonly payload: Payload;
17
+ readonly context: Context;
18
+ }
19
+ export declare function event<const Name extends string, Payload extends SchemaNoContext, Context extends SchemaNoContext>(name: Name, payload: Payload, context: Context): RpcEvent<Payload, Context, Name>;
20
+ export declare function event<const Name extends string, Payload extends SchemaNoContext>(name: Name, payload: Payload): RpcEvent<Payload, null, Name>;
21
+ export declare const exitSchemaFor: <Name extends string, Req extends SchemaNoContext, Res extends SchemaNoContext, Err extends ErrorSchema>(method: RpcMethod<Name, Req, Res, Err>) => S.Exit<Res, Err, S.Defect>;
22
+ export type AnyMethod = RpcMethod<string, SchemaNoContext, SchemaNoContext, ErrorSchema>;
23
+ export type AnyEvent = RpcEvent<SchemaNoContext, SchemaNoContext | null, string>;
24
+ export type RpcInput<M extends AnyMethod> = S.Schema.Type<M["req"]>;
25
+ export type RpcOutput<M extends AnyMethod> = S.Schema.Type<M["res"]>;
26
+ export type RpcError<M extends AnyMethod> = S.Schema.Type<M["err"]>;
27
+ export type RpcEventPayload<E extends AnyEvent> = S.Schema.Type<E["payload"]>;
28
+ /** Extract a method from a tuple by its name string literal. */
29
+ export type ExtractMethod<Methods extends readonly AnyMethod[], Name extends string> = Extract<Methods[number], {
30
+ readonly name: Name;
31
+ }>;
32
+ export interface RpcContract<Methods extends ReadonlyArray<AnyMethod>, Events extends ReadonlyArray<AnyEvent>> {
33
+ readonly methods: Methods;
34
+ readonly events: Events;
35
+ }
36
+ export declare const defineContract: <const Methods extends ReadonlyArray<AnyMethod>, const Events extends ReadonlyArray<AnyEvent>>(input: {
37
+ readonly methods: Methods;
38
+ readonly events: Events;
39
+ }) => RpcContract<Methods, Events>;
40
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import * as S from "@effect/schema/Schema";
2
+ export const NoError = S.Never;
3
+ export function rpc(name, req, res, err = NoError) {
4
+ return { name, req, res, err };
5
+ }
6
+ export function event(name, payload, context) {
7
+ return { name, payload, context: context ?? null };
8
+ }
9
+ export const exitSchemaFor = (method) => S.Exit({
10
+ success: method.res,
11
+ failure: method.err,
12
+ defect: S.Defect,
13
+ });
14
+ const collectDuplicates = (names) => {
15
+ const counts = new Map();
16
+ const duplicates = [];
17
+ for (const name of names) {
18
+ const next = (counts.get(name) ?? 0) + 1;
19
+ counts.set(name, next);
20
+ if (next === 2) {
21
+ duplicates.push(name);
22
+ }
23
+ }
24
+ return duplicates;
25
+ };
26
+ export const defineContract = (input) => {
27
+ const { methods, events } = input;
28
+ if (!Array.isArray(methods)) {
29
+ throw new Error("RPC contract methods must be an array.");
30
+ }
31
+ if (!Array.isArray(events)) {
32
+ throw new Error("RPC contract events must be an array.");
33
+ }
34
+ const duplicateMethods = collectDuplicates(methods.map((method) => method.name));
35
+ if (duplicateMethods.length > 0) {
36
+ throw new Error(`Duplicate RPC method name(s): ${duplicateMethods.join(", ")}`);
37
+ }
38
+ const duplicateEvents = collectDuplicates(events.map((event) => event.name));
39
+ if (duplicateEvents.length > 0) {
40
+ throw new Error(`Duplicate RPC event name(s): ${duplicateEvents.join(", ")}`);
41
+ }
42
+ return input;
43
+ };
package/dist/main.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { type RpcContract } from "./contract.ts";
2
+ import { type AnyEvent, type AnyMethod, type EventBusOptions, type Implementations, type IpcMainLike, type RpcEventBus, type RpcServerOptions } from "./types.ts";
3
+ export declare const createRpcServer: <const Methods extends ReadonlyArray<AnyMethod>, const Events extends ReadonlyArray<AnyEvent>, R = never>(contract: RpcContract<Methods, Events>, ipc: IpcMainLike, implementations: Implementations<RpcContract<Methods, Events>, R>, options?: RpcServerOptions<R>) => void;
4
+ export declare const createEventBus: <const Methods extends ReadonlyArray<AnyMethod>, const Events extends ReadonlyArray<AnyEvent>>(_contract: RpcContract<Methods, Events>, options: EventBusOptions) => RpcEventBus<RpcContract<Methods, Events>>;
5
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,WAAW,EAKjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAC;AAKpB,eAAO,MAAM,eAAe,GAC1B,KAAK,CAAC,OAAO,SAAS,aAAa,CAAC,SAAS,CAAC,EAC9C,KAAK,CAAC,MAAM,SAAS,aAAa,CAAC,QAAQ,CAAC,EAC5C,CAAC,GAAG,KAAK,EAET,UAAU,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EACtC,KAAK,WAAW,EAChB,iBAAiB,eAAe,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,EACjE,UAAU,gBAAgB,CAAC,CAAC,CAAC,KAC5B,IA6CF,CAAC;AAqCF,eAAO,MAAM,cAAc,GACzB,KAAK,CAAC,OAAO,SAAS,aAAa,CAAC,SAAS,CAAC,EAC9C,KAAK,CAAC,MAAM,SAAS,aAAa,CAAC,QAAQ,CAAC,EAE5C,WAAW,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EACvC,SAAS,eAAe,KACvB,WAAW,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CA8C1C,CAAC"}
package/dist/main.js ADDED
@@ -0,0 +1,72 @@
1
+ import * as S from "@effect/schema/Schema";
2
+ import { Effect, PubSub, Stream } from "effect";
3
+ import * as Runtime from "effect/Runtime";
4
+ import { exitSchemaFor, } from "./contract.js";
5
+ import { defaultChannelPrefix, } from "./types.js";
6
+ const resolveChannelPrefix = (prefix) => prefix ?? defaultChannelPrefix;
7
+ export const createRpcServer = (contract, ipc, implementations, options) => {
8
+ const channelPrefix = resolveChannelPrefix(options?.channelPrefix);
9
+ const runPromiseExit = (effect) => {
10
+ if (options?.runtime) {
11
+ return Runtime.runPromiseExit(options.runtime)(effect);
12
+ }
13
+ // @ts-expect-error -- default runtime only supports R=never when no runtime is provided
14
+ return Effect.runPromiseExit(effect);
15
+ };
16
+ const implementationsByName = implementations;
17
+ const methodNames = new Set(contract.methods.map((method) => method.name));
18
+ for (const name in implementations) {
19
+ if (!methodNames.has(name)) {
20
+ throw new Error(`Implementation provided for unknown RPC method: ${name}`);
21
+ }
22
+ }
23
+ contract.methods.forEach((method) => {
24
+ const impl = implementationsByName[method.name];
25
+ if (!isImplementation(impl)) {
26
+ throw new Error(`Missing implementation for RPC method: ${method.name}`);
27
+ }
28
+ const exitSchema = exitSchemaFor(method);
29
+ const encodeExit = S.encodeUnknownSync(exitSchema);
30
+ const decodeInput = S.decodeUnknownSync(method.req);
31
+ const channel = `${channelPrefix.rpc}${method.name}`;
32
+ ipc.handle(channel, async (_event, rawPayload) => {
33
+ let input;
34
+ try {
35
+ input = decodeInput(rawPayload);
36
+ }
37
+ catch (cause) {
38
+ const defectExit = await runPromiseExit(Effect.die(cause));
39
+ return encodeExit(defectExit);
40
+ }
41
+ const exit = await runPromiseExit(impl(input));
42
+ return encodeExit(exit);
43
+ });
44
+ });
45
+ };
46
+ const isImplementation = (value) => typeof value === "function";
47
+ const encodePayload = (event, payload) => Effect.try({
48
+ try: () => S.encodeSync(event.payload)(payload),
49
+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
50
+ });
51
+ const dispatchToRenderer = (getWindow, channelPrefix, event, encoded) => Effect.sync(() => {
52
+ const window = getWindow();
53
+ if (window && !window.isDestroyed()) {
54
+ const prefix = resolveChannelPrefix(channelPrefix);
55
+ window.webContents.send(`${prefix.event}${event.name}`, encoded);
56
+ }
57
+ });
58
+ export const createEventBus = (_contract, options) => {
59
+ const pubsub = Effect.runSync(PubSub.unbounded());
60
+ Effect.runFork(Effect.scoped(Effect.gen(function* () {
61
+ const dequeue = yield* PubSub.subscribe(pubsub);
62
+ yield* Stream.fromQueue(dequeue, { shutdown: true }).pipe(Stream.runForEach(({ event, payload }) => Effect.gen(function* () {
63
+ const encodeResult = yield* Effect.either(encodePayload(event, payload));
64
+ if (encodeResult._tag === "Left") {
65
+ return;
66
+ }
67
+ yield* dispatchToRenderer(options.getWindow, options.channelPrefix, event, encodeResult.right);
68
+ })));
69
+ })).pipe(Effect.catchAllCause(() => Effect.void), Effect.retry({ times: 3 }), Effect.catchAll(() => Effect.void)));
70
+ const emit = (event, payload) => Effect.flatMap(PubSub.publish(pubsub, { event, payload }), () => Effect.void);
71
+ return { emit };
72
+ };
@@ -0,0 +1,9 @@
1
+ import { type ChannelPrefix } from "./types.ts";
2
+ type BridgeOptions = {
3
+ readonly rpcGlobal?: string;
4
+ readonly eventsGlobal?: string;
5
+ readonly channelPrefix?: ChannelPrefix;
6
+ };
7
+ export declare const exposeRpcBridge: (options?: BridgeOptions) => void;
8
+ export {};
9
+ //# sourceMappingURL=preload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preload.d.ts","sourceRoot":"","sources":["../src/preload.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAItE,KAAK,aAAa,GAAG;IACnB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;CACxC,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,UAAU,aAAa,KAAG,IAqBzD,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { contextBridge, ipcRenderer } from "electron";
2
+ import { defaultChannelPrefix } from "./types.js";
3
+ export const exposeRpcBridge = (options) => {
4
+ const rpcGlobal = options?.rpcGlobal ?? "rpc";
5
+ const eventsGlobal = options?.eventsGlobal ?? "events";
6
+ const channelPrefix = options?.channelPrefix ?? defaultChannelPrefix;
7
+ const invoke = (method, payload) => ipcRenderer.invoke(`${channelPrefix.rpc}${method}`, payload);
8
+ const subscribe = (event, listener) => {
9
+ const wrapped = (_event, payload) => listener(payload);
10
+ ipcRenderer.on(`${channelPrefix.event}${event}`, wrapped);
11
+ return () => {
12
+ ipcRenderer.removeListener(`${channelPrefix.event}${event}`, wrapped);
13
+ };
14
+ };
15
+ contextBridge.exposeInMainWorld(rpcGlobal, { invoke });
16
+ contextBridge.exposeInMainWorld(eventsGlobal, { subscribe });
17
+ };
@@ -0,0 +1,6 @@
1
+ import { type RpcContract } from "./contract.ts";
2
+ import { type AnyEvent, type AnyMethod, type EventSubscriber, type EventSubscriberOptions, type RpcClient, type RpcClientOptions } from "./types.ts";
3
+ export declare const createRpcClient: <const Methods extends ReadonlyArray<AnyMethod>, const Events extends ReadonlyArray<AnyEvent>>(contract: RpcContract<Methods, Events>, options?: RpcClientOptions) => RpcClient<RpcContract<Methods, Events>>;
4
+ export declare const createEventSubscriber: <const Methods extends ReadonlyArray<AnyMethod>, const Events extends ReadonlyArray<AnyEvent>>(contract: RpcContract<Methods, Events>, options?: EventSubscriberOptions) => EventSubscriber<RpcContract<Methods, Events>>;
5
+ export { RpcDefectError } from "./types.ts";
6
+ //# sourceMappingURL=renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,WAAW,EAIjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAE3B,KAAK,SAAS,EACd,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAC;AAyBpB,eAAO,MAAM,eAAe,GAC1B,KAAK,CAAC,OAAO,SAAS,aAAa,CAAC,SAAS,CAAC,EAC9C,KAAK,CAAC,MAAM,SAAS,aAAa,CAAC,QAAQ,CAAC,EAE5C,UAAU,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EACtC,UAAU,gBAAgB,KACzB,SAAS,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAuExC,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,KAAK,CAAC,OAAO,SAAS,aAAa,CAAC,SAAS,CAAC,EAC9C,KAAK,CAAC,MAAM,SAAS,aAAa,CAAC,QAAQ,CAAC,EAE5C,UAAU,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EACtC,UAAU,sBAAsB,KAC/B,eAAe,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAoC9C,CAAC;AAEF,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,93 @@
1
+ import * as S from "@effect/schema/Schema";
2
+ import { Cause, Exit } from "effect";
3
+ import { exitSchemaFor, } from "./contract.js";
4
+ import { RpcDefectError, } from "./types.js";
5
+ const formatCause = (cause) => cause instanceof Error ? cause.message : String(cause);
6
+ const requireInvoke = (options) => {
7
+ if (!options?.invoke) {
8
+ throw new Error("RpcClientOptions.invoke is required.");
9
+ }
10
+ return options.invoke;
11
+ };
12
+ const requireSubscribe = (options) => {
13
+ if (!options?.subscribe) {
14
+ throw new Error("EventSubscriberOptions.subscribe is required.");
15
+ }
16
+ return options.subscribe;
17
+ };
18
+ export const createRpcClient = (contract, options) => {
19
+ const invoke = requireInvoke(options);
20
+ const call = async (method, input) => {
21
+ let encoded;
22
+ try {
23
+ encoded = S.encodeSync(method.req)(input);
24
+ }
25
+ catch (cause) {
26
+ throw new Error(`RPC ${method.name} request encoding failed: ${formatCause(cause)}`);
27
+ }
28
+ const raw = await invoke(method.name, encoded);
29
+ const exitSchema = exitSchemaFor(method);
30
+ const decodeExit = S.decodeUnknownSync(exitSchema);
31
+ let exit;
32
+ try {
33
+ exit = decodeExit(raw);
34
+ }
35
+ catch (cause) {
36
+ throw new Error(`RPC ${method.name} response decoding failed: ${formatCause(cause)}`);
37
+ }
38
+ if (Exit.isSuccess(exit)) {
39
+ return exit.value;
40
+ }
41
+ const cause = exit.cause;
42
+ const failureOption = Cause.failureOption(cause);
43
+ if (failureOption._tag === "Some") {
44
+ throw failureOption.value;
45
+ }
46
+ const defectOption = Cause.dieOption(cause);
47
+ if (defectOption._tag === "Some") {
48
+ const defect = defectOption.value;
49
+ if (defect instanceof Error) {
50
+ throw new RpcDefectError(defect.message, defect);
51
+ }
52
+ throw new RpcDefectError(String(defect), defect);
53
+ }
54
+ throw new RpcDefectError("RPC call was interrupted or failed unexpectedly", cause);
55
+ };
56
+ const client = Object.create(null);
57
+ const clientRecord = client;
58
+ contract.methods.forEach((method) => {
59
+ const caller = (input) => {
60
+ const payload = input ?? S.decodeUnknownSync(method.req)({});
61
+ return call(method, payload);
62
+ };
63
+ clientRecord[method.name] = caller;
64
+ });
65
+ return client;
66
+ };
67
+ export const createEventSubscriber = (contract, options) => {
68
+ const subscribe = requireSubscribe(options);
69
+ const eventMap = new Map();
70
+ for (const event of contract.events) {
71
+ eventMap.set(event.name, event);
72
+ }
73
+ const subscribeEvent = (event, handler) => {
74
+ const decoder = S.decodeUnknownSync(event.payload);
75
+ return subscribe(event.name, (payload) => {
76
+ const decoded = decoder(payload);
77
+ handler(decoded);
78
+ });
79
+ };
80
+ const subscribeByName = (name, handler) => {
81
+ const event = eventMap.get(name);
82
+ if (!event) {
83
+ throw new Error(`Unknown event: ${name}`);
84
+ }
85
+ const decoder = S.decodeUnknownSync(event.payload);
86
+ return subscribe(name, (payload) => handler(decoder(payload)));
87
+ };
88
+ return {
89
+ subscribe: subscribeEvent,
90
+ subscribeByName,
91
+ };
92
+ };
93
+ export { RpcDefectError } from "./types.js";
@@ -0,0 +1,15 @@
1
+ import type { RpcInvoke } from "./types.ts";
2
+ export type Invocation = {
3
+ readonly method: string;
4
+ readonly payload: unknown;
5
+ };
6
+ export type InvokeStub = RpcInvoke & {
7
+ readonly invocations: Invocation[];
8
+ };
9
+ export declare const createInvokeStub: (impl: RpcInvoke) => InvokeStub;
10
+ export declare const createDeferred: <T>() => {
11
+ promise: Promise<T>;
12
+ resolve: (value: T | PromiseLike<T>) => void;
13
+ reject: (reason?: unknown) => void;
14
+ };
15
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG;IAAE,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC;AAE5E,eAAO,MAAM,gBAAgB,GAAI,MAAM,SAAS,KAAG,UAYlD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,CAAC;;qBACT,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI;sBAC1B,OAAO,KAAK,IAAI;CAOvC,CAAC"}
@@ -0,0 +1,17 @@
1
+ export const createInvokeStub = (impl) => {
2
+ const invocations = [];
3
+ const wrapped = Object.assign(async (method, payload) => {
4
+ invocations.push({ method, payload });
5
+ return impl(method, payload);
6
+ }, { invocations });
7
+ return wrapped;
8
+ };
9
+ export const createDeferred = () => {
10
+ let resolve = () => { };
11
+ let reject = () => { };
12
+ const promise = new Promise((res, rej) => {
13
+ resolve = res;
14
+ reject = rej;
15
+ });
16
+ return { promise, resolve, reject };
17
+ };
@@ -0,0 +1,52 @@
1
+ import type * as Effect from "effect/Effect";
2
+ import type * as Runtime from "effect/Runtime";
3
+ import type { BrowserWindow } from "electron";
4
+ import type { AnyEvent, AnyMethod, ExtractMethod, RpcContract, RpcError, RpcEventPayload, RpcInput, RpcOutput } from "./contract.ts";
5
+ export type { AnyEvent, AnyMethod, ErrorSchema, ExtractMethod, RpcContract, RpcError, RpcEvent, RpcEventPayload, RpcInput, RpcMethod, RpcOutput, SchemaNoContext, } from "./contract.ts";
6
+ export type Implementations<C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[]>, R = never> = {
7
+ readonly [Name in C["methods"][number]["name"]]: (input: RpcInput<ExtractMethod<C["methods"], Name>>) => Effect.Effect<RpcOutput<ExtractMethod<C["methods"], Name>>, RpcError<ExtractMethod<C["methods"], Name>>, R>;
8
+ };
9
+ type IsEmptyObject<T> = keyof T extends never ? true : false;
10
+ export type RpcCaller<M extends AnyMethod> = IsEmptyObject<RpcInput<M>> extends true ? () => Promise<RpcOutput<M>> : (input: RpcInput<M>) => Promise<RpcOutput<M>>;
11
+ export type RpcClient<C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[]>> = {
12
+ readonly [Name in C["methods"][number]["name"]]: RpcCaller<ExtractMethod<C["methods"], Name>>;
13
+ };
14
+ export declare class RpcDefectError extends Error {
15
+ readonly cause: unknown;
16
+ readonly _tag = "RpcDefectError";
17
+ constructor(message: string, cause: unknown);
18
+ }
19
+ export type ChannelPrefix = {
20
+ readonly rpc: string;
21
+ readonly event: string;
22
+ };
23
+ export declare const defaultChannelPrefix: ChannelPrefix;
24
+ export type IpcMainLike = {
25
+ readonly handle: (channel: string, listener: (event: unknown, payload: unknown) => unknown) => unknown;
26
+ };
27
+ export type RpcInvoke = (method: string, payload: unknown) => Promise<unknown>;
28
+ /** Provide a Runtime when handlers require services (R). */
29
+ export type RpcServerOptions<R = never> = {
30
+ readonly channelPrefix?: ChannelPrefix;
31
+ readonly runtime?: Runtime.Runtime<R>;
32
+ };
33
+ export type RpcClientOptions = {
34
+ readonly invoke?: RpcInvoke;
35
+ readonly channelPrefix?: ChannelPrefix;
36
+ };
37
+ export type EventBusOptions = {
38
+ readonly channelPrefix?: ChannelPrefix;
39
+ readonly getWindow: () => BrowserWindow | null;
40
+ };
41
+ export type EventSubscriberOptions = {
42
+ readonly channelPrefix?: ChannelPrefix;
43
+ readonly subscribe?: (name: string, handler: (payload: unknown) => void) => () => void;
44
+ };
45
+ export interface RpcEventBus<C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[]>> {
46
+ readonly emit: <E extends C["events"][number]>(event: E, payload: RpcEventPayload<E>) => Effect.Effect<void, never>;
47
+ }
48
+ export interface EventSubscriber<C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[]>> {
49
+ readonly subscribe: <E extends C["events"][number]>(event: E, handler: (payload: RpcEventPayload<E>) => void) => () => void;
50
+ readonly subscribeByName: (name: C["events"][number]["name"], handler: (payload: unknown) => void) => () => void;
51
+ }
52
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ export class RpcDefectError extends Error {
2
+ constructor(message, cause) {
3
+ super(message);
4
+ this.cause = cause;
5
+ this._tag = "RpcDefectError";
6
+ this.name = "RpcDefectError";
7
+ }
8
+ }
9
+ export const defaultChannelPrefix = {
10
+ rpc: "rpc/",
11
+ event: "event/",
12
+ };
package/package.json CHANGED
@@ -1,20 +1,40 @@
1
1
  {
2
2
  "name": "electron-effect-rpc",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Typed IPC RPC for Electron, built on Effect and @effect/schema",
5
5
  "type": "module",
6
6
  "exports": {
7
- "./main": "./src/main.ts",
8
- "./renderer": "./src/renderer.ts",
9
- "./preload": "./src/preload.ts",
10
- "./contract": "./src/contract.ts",
11
- "./types": "./src/types.ts",
12
- "./testing": "./src/testing.ts"
7
+ "./main": {
8
+ "types": "./dist/main.d.ts",
9
+ "default": "./dist/main.js"
10
+ },
11
+ "./renderer": {
12
+ "types": "./dist/renderer.d.ts",
13
+ "default": "./dist/renderer.js"
14
+ },
15
+ "./preload": {
16
+ "types": "./dist/preload.d.ts",
17
+ "default": "./dist/preload.js"
18
+ },
19
+ "./contract": {
20
+ "types": "./dist/contract.d.ts",
21
+ "default": "./dist/contract.js"
22
+ },
23
+ "./types": {
24
+ "types": "./dist/types.d.ts",
25
+ "default": "./dist/types.js"
26
+ },
27
+ "./testing": {
28
+ "types": "./dist/testing.d.ts",
29
+ "default": "./dist/testing.js"
30
+ }
13
31
  },
14
32
  "files": [
15
- "src"
33
+ "dist"
16
34
  ],
17
35
  "scripts": {
36
+ "build": "tsc",
37
+ "prepublishOnly": "bun run build",
18
38
  "test": "bun test",
19
39
  "typecheck": "tsc --noEmit"
20
40
  },
package/src/contract.ts DELETED
@@ -1,165 +0,0 @@
1
- import * as S from "@effect/schema/Schema";
2
-
3
- export type SchemaNoContext = S.Schema.AnyNoContext;
4
-
5
- export const NoError = S.Never;
6
- export type NoError = typeof NoError;
7
-
8
- export type ErrorSchema = SchemaNoContext | S.Schema<never, never, never>;
9
-
10
- export interface RpcMethod<
11
- Name extends string,
12
- Req extends SchemaNoContext,
13
- Res extends SchemaNoContext,
14
- Err extends ErrorSchema = NoError
15
- > {
16
- readonly name: Name;
17
- readonly req: Req;
18
- readonly res: Res;
19
- readonly err: Err;
20
- }
21
-
22
- export function rpc<
23
- const Name extends string,
24
- Req extends SchemaNoContext,
25
- Res extends SchemaNoContext,
26
- Err extends ErrorSchema
27
- >(name: Name, req: Req, res: Res, err: Err): RpcMethod<Name, Req, Res, Err>;
28
-
29
- export function rpc<
30
- const Name extends string,
31
- Req extends SchemaNoContext,
32
- Res extends SchemaNoContext
33
- >(name: Name, req: Req, res: Res): RpcMethod<Name, Req, Res, NoError>;
34
-
35
- export function rpc<const Name extends string>(
36
- name: Name,
37
- req: SchemaNoContext,
38
- res: SchemaNoContext,
39
- err: ErrorSchema = NoError
40
- ): RpcMethod<Name, SchemaNoContext, SchemaNoContext, ErrorSchema> {
41
- return { name, req, res, err };
42
- }
43
-
44
- export interface RpcEvent<
45
- Payload extends SchemaNoContext,
46
- Context extends SchemaNoContext | null,
47
- Name extends string = string
48
- > {
49
- readonly name: Name;
50
- readonly payload: Payload;
51
- readonly context: Context;
52
- }
53
-
54
- export function event<
55
- const Name extends string,
56
- Payload extends SchemaNoContext,
57
- Context extends SchemaNoContext
58
- >(name: Name, payload: Payload, context: Context): RpcEvent<Payload, Context, Name>;
59
-
60
- export function event<const Name extends string, Payload extends SchemaNoContext>(
61
- name: Name,
62
- payload: Payload
63
- ): RpcEvent<Payload, null, Name>;
64
-
65
- export function event<const Name extends string>(
66
- name: Name,
67
- payload: SchemaNoContext,
68
- context?: SchemaNoContext | null
69
- ): RpcEvent<SchemaNoContext, SchemaNoContext | null, Name> {
70
- return { name, payload, context: context ?? null };
71
- }
72
-
73
- export const exitSchemaFor = <
74
- Name extends string,
75
- Req extends SchemaNoContext,
76
- Res extends SchemaNoContext,
77
- Err extends ErrorSchema
78
- >(
79
- method: RpcMethod<Name, Req, Res, Err>
80
- ) =>
81
- S.Exit({
82
- success: method.res,
83
- failure: method.err,
84
- defect: S.Defect,
85
- });
86
-
87
- export type AnyMethod = RpcMethod<
88
- string,
89
- SchemaNoContext,
90
- SchemaNoContext,
91
- ErrorSchema
92
- >;
93
-
94
- export type AnyEvent = RpcEvent<SchemaNoContext, SchemaNoContext | null, string>;
95
-
96
- export type RpcInput<M extends AnyMethod> = S.Schema.Type<M["req"]>;
97
-
98
- export type RpcOutput<M extends AnyMethod> = S.Schema.Type<M["res"]>;
99
-
100
- export type RpcError<M extends AnyMethod> = S.Schema.Type<M["err"]>;
101
-
102
- export type RpcEventPayload<E extends AnyEvent> = S.Schema.Type<E["payload"]>;
103
-
104
- /** Extract a method from a tuple by its name string literal. */
105
- export type ExtractMethod<
106
- Methods extends readonly AnyMethod[],
107
- Name extends string
108
- > = Extract<Methods[number], { readonly name: Name }>;
109
-
110
- export interface RpcContract<
111
- Methods extends ReadonlyArray<AnyMethod>,
112
- Events extends ReadonlyArray<AnyEvent>
113
- > {
114
- readonly methods: Methods;
115
- readonly events: Events;
116
- }
117
-
118
- const collectDuplicates = (names: ReadonlyArray<string>): Array<string> => {
119
- const counts = new Map<string, number>();
120
- const duplicates: string[] = [];
121
-
122
- for (const name of names) {
123
- const next = (counts.get(name) ?? 0) + 1;
124
- counts.set(name, next);
125
- if (next === 2) {
126
- duplicates.push(name);
127
- }
128
- }
129
-
130
- return duplicates;
131
- };
132
-
133
- export const defineContract = <
134
- const Methods extends ReadonlyArray<AnyMethod>,
135
- const Events extends ReadonlyArray<AnyEvent>
136
- >(input: {
137
- readonly methods: Methods;
138
- readonly events: Events;
139
- }): RpcContract<Methods, Events> => {
140
- const { methods, events } = input;
141
-
142
- if (!Array.isArray(methods)) {
143
- throw new Error("RPC contract methods must be an array.");
144
- }
145
-
146
- if (!Array.isArray(events)) {
147
- throw new Error("RPC contract events must be an array.");
148
- }
149
-
150
- const duplicateMethods = collectDuplicates(methods.map((method) => method.name));
151
- if (duplicateMethods.length > 0) {
152
- throw new Error(
153
- `Duplicate RPC method name(s): ${duplicateMethods.join(", ")}`
154
- );
155
- }
156
-
157
- const duplicateEvents = collectDuplicates(events.map((event) => event.name));
158
- if (duplicateEvents.length > 0) {
159
- throw new Error(
160
- `Duplicate RPC event name(s): ${duplicateEvents.join(", ")}`
161
- );
162
- }
163
-
164
- return input;
165
- };
package/src/main.ts DELETED
@@ -1,169 +0,0 @@
1
- import * as S from "@effect/schema/Schema";
2
- import { Effect, PubSub, Stream } from "effect";
3
- import * as Runtime from "effect/Runtime";
4
- import {
5
- exitSchemaFor,
6
- type RpcContract,
7
- type RpcError,
8
- type RpcEventPayload,
9
- type RpcInput,
10
- type RpcOutput,
11
- } from "./contract.ts";
12
- import {
13
- defaultChannelPrefix,
14
- type AnyEvent,
15
- type AnyMethod,
16
- type EventBusOptions,
17
- type Implementations,
18
- type IpcMainLike,
19
- type RpcEventBus,
20
- type RpcServerOptions,
21
- } from "./types.ts";
22
-
23
- const resolveChannelPrefix = (prefix: EventBusOptions["channelPrefix"]) =>
24
- prefix ?? defaultChannelPrefix;
25
-
26
- export const createRpcServer = <
27
- const Methods extends ReadonlyArray<AnyMethod>,
28
- const Events extends ReadonlyArray<AnyEvent>,
29
- R = never
30
- >(
31
- contract: RpcContract<Methods, Events>,
32
- ipc: IpcMainLike,
33
- implementations: Implementations<RpcContract<Methods, Events>, R>,
34
- options?: RpcServerOptions<R>
35
- ): void => {
36
- const channelPrefix = resolveChannelPrefix(options?.channelPrefix);
37
- const runPromiseExit = <A, E>(effect: Effect.Effect<A, E, R>) => {
38
- if (options?.runtime) {
39
- return Runtime.runPromiseExit(options.runtime)(effect);
40
- }
41
-
42
- // @ts-expect-error -- default runtime only supports R=never when no runtime is provided
43
- return Effect.runPromiseExit(effect);
44
- };
45
- const implementationsByName: Implementations<RpcContract<Methods, Events>, R> &
46
- Record<string, unknown> = implementations;
47
-
48
- const methodNames = new Set(contract.methods.map((method) => method.name));
49
-
50
- for (const name in implementations) {
51
- if (!methodNames.has(name)) {
52
- throw new Error(`Implementation provided for unknown RPC method: ${name}`);
53
- }
54
- }
55
-
56
- contract.methods.forEach((method: Methods[number]) => {
57
- const impl = implementationsByName[method.name];
58
- if (!isImplementation<typeof method, R>(impl)) {
59
- throw new Error(`Missing implementation for RPC method: ${method.name}`);
60
- }
61
-
62
- const exitSchema = exitSchemaFor(method);
63
- const encodeExit = S.encodeUnknownSync(exitSchema);
64
- const decodeInput = S.decodeUnknownSync(method.req);
65
- const channel = `${channelPrefix.rpc}${method.name}`;
66
-
67
- ipc.handle(channel, async (_event, rawPayload) => {
68
- let input: RpcInput<typeof method>;
69
- try {
70
- input = decodeInput(rawPayload);
71
- } catch (cause) {
72
- const defectExit = await runPromiseExit(Effect.die(cause));
73
- return encodeExit(defectExit);
74
- }
75
-
76
- const exit = await runPromiseExit(impl(input));
77
- return encodeExit(exit);
78
- });
79
- });
80
- };
81
-
82
- type Envelope<E extends AnyEvent> = {
83
- readonly event: E;
84
- readonly payload: RpcEventPayload<E>;
85
- };
86
-
87
- const isImplementation = <M extends AnyMethod, R>(
88
- value: unknown
89
- ): value is (
90
- input: RpcInput<M>
91
- ) => Effect.Effect<RpcOutput<M>, RpcError<M>, R> => typeof value === "function";
92
-
93
- const encodePayload = <E extends AnyEvent>(
94
- event: E,
95
- payload: RpcEventPayload<E>
96
- ) =>
97
- Effect.try({
98
- try: () => S.encodeSync(event.payload)(payload),
99
- catch: (cause) =>
100
- cause instanceof Error ? cause : new Error(String(cause)),
101
- });
102
-
103
- const dispatchToRenderer = <E extends AnyEvent>(
104
- getWindow: EventBusOptions["getWindow"],
105
- channelPrefix: EventBusOptions["channelPrefix"],
106
- event: E,
107
- encoded: unknown
108
- ) =>
109
- Effect.sync(() => {
110
- const window = getWindow();
111
- if (window && !window.isDestroyed()) {
112
- const prefix = resolveChannelPrefix(channelPrefix);
113
- window.webContents.send(`${prefix.event}${event.name}`, encoded);
114
- }
115
- });
116
-
117
- export const createEventBus = <
118
- const Methods extends ReadonlyArray<AnyMethod>,
119
- const Events extends ReadonlyArray<AnyEvent>
120
- >(
121
- _contract: RpcContract<Methods, Events>,
122
- options: EventBusOptions
123
- ): RpcEventBus<RpcContract<Methods, Events>> => {
124
- const pubsub = Effect.runSync(
125
- PubSub.unbounded<Envelope<Events[number]>>()
126
- );
127
-
128
- Effect.runFork(
129
- Effect.scoped(
130
- Effect.gen(function* () {
131
- const dequeue = yield* PubSub.subscribe(pubsub);
132
- yield* Stream.fromQueue(dequeue, { shutdown: true }).pipe(
133
- Stream.runForEach(({ event, payload }) =>
134
- Effect.gen(function* () {
135
- const encodeResult = yield* Effect.either(
136
- encodePayload(event, payload)
137
- );
138
-
139
- if (encodeResult._tag === "Left") {
140
- return;
141
- }
142
-
143
- yield* dispatchToRenderer(
144
- options.getWindow,
145
- options.channelPrefix,
146
- event,
147
- encodeResult.right
148
- );
149
- })
150
- )
151
- );
152
- })
153
- ).pipe(
154
- Effect.catchAllCause(() => Effect.void),
155
- Effect.retry({ times: 3 }),
156
- Effect.catchAll(() => Effect.void)
157
- )
158
- );
159
-
160
- const emit = <E extends Events[number]>(
161
- event: E,
162
- payload: RpcEventPayload<E>
163
- ) =>
164
- Effect.flatMap(PubSub.publish(pubsub, { event, payload }), () =>
165
- Effect.void
166
- );
167
-
168
- return { emit };
169
- };
package/src/preload.ts DELETED
@@ -1,33 +0,0 @@
1
- import { contextBridge, ipcRenderer, type IpcRendererEvent } from "electron";
2
- import { defaultChannelPrefix, type ChannelPrefix } from "./types.ts";
3
-
4
- type Listener = (payload: unknown) => void;
5
-
6
- type BridgeOptions = {
7
- readonly rpcGlobal?: string;
8
- readonly eventsGlobal?: string;
9
- readonly channelPrefix?: ChannelPrefix;
10
- };
11
-
12
- export const exposeRpcBridge = (options?: BridgeOptions): void => {
13
- const rpcGlobal = options?.rpcGlobal ?? "rpc";
14
- const eventsGlobal = options?.eventsGlobal ?? "events";
15
- const channelPrefix = options?.channelPrefix ?? defaultChannelPrefix;
16
-
17
- const invoke = (method: string, payload: unknown): Promise<unknown> =>
18
- ipcRenderer.invoke(`${channelPrefix.rpc}${method}`, payload);
19
-
20
- const subscribe = (event: string, listener: Listener): (() => void) => {
21
- const wrapped = (_event: IpcRendererEvent, payload: unknown) =>
22
- listener(payload);
23
-
24
- ipcRenderer.on(`${channelPrefix.event}${event}`, wrapped);
25
-
26
- return () => {
27
- ipcRenderer.removeListener(`${channelPrefix.event}${event}`, wrapped);
28
- };
29
- };
30
-
31
- contextBridge.exposeInMainWorld(rpcGlobal, { invoke });
32
- contextBridge.exposeInMainWorld(eventsGlobal, { subscribe });
33
- };
package/src/renderer.ts DELETED
@@ -1,167 +0,0 @@
1
- import * as S from "@effect/schema/Schema";
2
- import { Cause, Exit } from "effect";
3
- import {
4
- exitSchemaFor,
5
- type RpcContract,
6
- type RpcEventPayload,
7
- type RpcInput,
8
- type RpcOutput,
9
- } from "./contract.ts";
10
- import {
11
- RpcDefectError,
12
- type AnyEvent,
13
- type AnyMethod,
14
- type EventSubscriber,
15
- type EventSubscriberOptions,
16
- type RpcCaller,
17
- type RpcClient,
18
- type RpcClientOptions,
19
- } from "./types.ts";
20
-
21
- const formatCause = (cause: unknown): string =>
22
- cause instanceof Error ? cause.message : String(cause);
23
-
24
- const requireInvoke = (options?: RpcClientOptions) => {
25
- if (!options?.invoke) {
26
- throw new Error("RpcClientOptions.invoke is required.");
27
- }
28
- return options.invoke;
29
- };
30
-
31
- const requireSubscribe = (options?: EventSubscriberOptions) => {
32
- if (!options?.subscribe) {
33
- throw new Error("EventSubscriberOptions.subscribe is required.");
34
- }
35
- return options.subscribe;
36
- };
37
-
38
- type MutableRpcClient<
39
- C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[]>
40
- > = {
41
- -readonly [Name in keyof RpcClient<C>]: RpcClient<C>[Name];
42
- };
43
-
44
- export const createRpcClient = <
45
- const Methods extends ReadonlyArray<AnyMethod>,
46
- const Events extends ReadonlyArray<AnyEvent>
47
- >(
48
- contract: RpcContract<Methods, Events>,
49
- options?: RpcClientOptions
50
- ): RpcClient<RpcContract<Methods, Events>> => {
51
- const invoke = requireInvoke(options);
52
-
53
- const call = async <M extends Methods[number]>(
54
- method: M,
55
- input: RpcInput<M>
56
- ): Promise<RpcOutput<M>> => {
57
- let encoded: unknown;
58
- try {
59
- encoded = S.encodeSync(method.req)(input);
60
- } catch (cause) {
61
- throw new Error(
62
- `RPC ${method.name} request encoding failed: ${formatCause(cause)}`
63
- );
64
- }
65
-
66
- const raw = await invoke(method.name, encoded);
67
-
68
- const exitSchema = exitSchemaFor(method);
69
- const decodeExit = S.decodeUnknownSync(exitSchema);
70
- let exit: ReturnType<typeof decodeExit>;
71
-
72
- try {
73
- exit = decodeExit(raw);
74
- } catch (cause) {
75
- throw new Error(
76
- `RPC ${method.name} response decoding failed: ${formatCause(cause)}`
77
- );
78
- }
79
-
80
- if (Exit.isSuccess(exit)) {
81
- return exit.value;
82
- }
83
-
84
- const cause = exit.cause;
85
- const failureOption = Cause.failureOption(cause);
86
- if (failureOption._tag === "Some") {
87
- throw failureOption.value;
88
- }
89
-
90
- const defectOption = Cause.dieOption(cause);
91
- if (defectOption._tag === "Some") {
92
- const defect = defectOption.value;
93
- if (defect instanceof Error) {
94
- throw new RpcDefectError(defect.message, defect);
95
- }
96
- throw new RpcDefectError(String(defect), defect);
97
- }
98
-
99
- throw new RpcDefectError(
100
- "RPC call was interrupted or failed unexpectedly",
101
- cause
102
- );
103
- };
104
-
105
- const client: MutableRpcClient<RpcContract<Methods, Events>> =
106
- Object.create(null);
107
- const clientRecord: Record<string, unknown> = client;
108
-
109
- contract.methods.forEach((method: Methods[number]) => {
110
- const caller: RpcCaller<typeof method> = (
111
- input?: RpcInput<typeof method>
112
- ) => {
113
- const payload = input ?? S.decodeUnknownSync(method.req)({});
114
- return call(method, payload);
115
- };
116
-
117
- clientRecord[method.name] = caller;
118
- });
119
-
120
- return client;
121
- };
122
-
123
- export const createEventSubscriber = <
124
- const Methods extends ReadonlyArray<AnyMethod>,
125
- const Events extends ReadonlyArray<AnyEvent>
126
- >(
127
- contract: RpcContract<Methods, Events>,
128
- options?: EventSubscriberOptions
129
- ): EventSubscriber<RpcContract<Methods, Events>> => {
130
- const subscribe = requireSubscribe(options);
131
- const eventMap = new Map<string, Events[number]>();
132
-
133
- for (const event of contract.events) {
134
- eventMap.set(event.name, event);
135
- }
136
-
137
- const subscribeEvent = <E extends Events[number]>(
138
- event: E,
139
- handler: (payload: RpcEventPayload<E>) => void
140
- ) => {
141
- const decoder = S.decodeUnknownSync(event.payload);
142
- return subscribe(event.name, (payload) => {
143
- const decoded = decoder(payload);
144
- handler(decoded);
145
- });
146
- };
147
-
148
- const subscribeByName = (
149
- name: Events[number]["name"],
150
- handler: (payload: unknown) => void
151
- ) => {
152
- const event = eventMap.get(name);
153
- if (!event) {
154
- throw new Error(`Unknown event: ${name}`);
155
- }
156
-
157
- const decoder = S.decodeUnknownSync(event.payload);
158
- return subscribe(name, (payload) => handler(decoder(payload)));
159
- };
160
-
161
- return {
162
- subscribe: subscribeEvent,
163
- subscribeByName,
164
- };
165
- };
166
-
167
- export { RpcDefectError } from "./types.ts";
package/src/testing.ts DELETED
@@ -1,33 +0,0 @@
1
- import type { RpcInvoke } from "./types.ts";
2
-
3
- export type Invocation = {
4
- readonly method: string;
5
- readonly payload: unknown;
6
- };
7
-
8
- export type InvokeStub = RpcInvoke & { readonly invocations: Invocation[] };
9
-
10
- export const createInvokeStub = (impl: RpcInvoke): InvokeStub => {
11
- const invocations: Invocation[] = [];
12
-
13
- const wrapped = Object.assign(
14
- async (method: string, payload: unknown) => {
15
- invocations.push({ method, payload });
16
- return impl(method, payload);
17
- },
18
- { invocations }
19
- );
20
-
21
- return wrapped;
22
- };
23
-
24
- export const createDeferred = <T>() => {
25
- let resolve: (value: T | PromiseLike<T>) => void = () => {};
26
- let reject: (reason?: unknown) => void = () => {};
27
- const promise = new Promise<T>((res, rej) => {
28
- resolve = res;
29
- reject = rej;
30
- });
31
-
32
- return { promise, resolve, reject };
33
- };
package/src/types.ts DELETED
@@ -1,134 +0,0 @@
1
- import type * as Effect from "effect/Effect";
2
- import type * as Runtime from "effect/Runtime";
3
- import type { BrowserWindow } from "electron";
4
- import type {
5
- AnyEvent,
6
- AnyMethod,
7
- ErrorSchema,
8
- ExtractMethod,
9
- RpcContract,
10
- RpcError,
11
- RpcEvent,
12
- RpcEventPayload,
13
- RpcInput,
14
- RpcMethod,
15
- RpcOutput,
16
- SchemaNoContext,
17
- } from "./contract.ts";
18
-
19
- export type {
20
- AnyEvent,
21
- AnyMethod,
22
- ErrorSchema,
23
- ExtractMethod,
24
- RpcContract,
25
- RpcError,
26
- RpcEvent,
27
- RpcEventPayload,
28
- RpcInput,
29
- RpcMethod,
30
- RpcOutput,
31
- SchemaNoContext,
32
- } from "./contract.ts";
33
-
34
- export type Implementations<
35
- C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[]>,
36
- R = never
37
- > = {
38
- readonly [Name in C["methods"][number]["name"]]: (
39
- input: RpcInput<ExtractMethod<C["methods"], Name>>
40
- ) => Effect.Effect<
41
- RpcOutput<ExtractMethod<C["methods"], Name>>,
42
- RpcError<ExtractMethod<C["methods"], Name>>,
43
- R
44
- >;
45
- };
46
-
47
- type IsEmptyObject<T> = keyof T extends never ? true : false;
48
-
49
- export type RpcCaller<M extends AnyMethod> =
50
- IsEmptyObject<RpcInput<M>> extends true
51
- ? () => Promise<RpcOutput<M>>
52
- : (input: RpcInput<M>) => Promise<RpcOutput<M>>;
53
-
54
- export type RpcClient<
55
- C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[]>
56
- > = {
57
- readonly [Name in C["methods"][number]["name"]]: RpcCaller<
58
- ExtractMethod<C["methods"], Name>
59
- >;
60
- };
61
-
62
- export class RpcDefectError extends Error {
63
- readonly _tag = "RpcDefectError";
64
-
65
- constructor(
66
- message: string,
67
- public readonly cause: unknown
68
- ) {
69
- super(message);
70
- this.name = "RpcDefectError";
71
- }
72
- }
73
-
74
- export type ChannelPrefix = {
75
- readonly rpc: string;
76
- readonly event: string;
77
- };
78
-
79
- export const defaultChannelPrefix: ChannelPrefix = {
80
- rpc: "rpc/",
81
- event: "event/",
82
- };
83
-
84
- export type IpcMainLike = {
85
- readonly handle: (
86
- channel: string,
87
- listener: (event: unknown, payload: unknown) => unknown
88
- ) => unknown;
89
- };
90
-
91
- export type RpcInvoke = (method: string, payload: unknown) => Promise<unknown>;
92
-
93
- /** Provide a Runtime when handlers require services (R). */
94
- export type RpcServerOptions<R = never> = {
95
- readonly channelPrefix?: ChannelPrefix;
96
- readonly runtime?: Runtime.Runtime<R>;
97
- };
98
-
99
- export type RpcClientOptions = {
100
- readonly invoke?: RpcInvoke;
101
- readonly channelPrefix?: ChannelPrefix;
102
- };
103
-
104
- export type EventBusOptions = {
105
- readonly channelPrefix?: ChannelPrefix;
106
- readonly getWindow: () => BrowserWindow | null;
107
- };
108
-
109
- export type EventSubscriberOptions = {
110
- readonly channelPrefix?: ChannelPrefix;
111
- readonly subscribe?: (name: string, handler: (payload: unknown) => void) => () => void;
112
- };
113
-
114
- export interface RpcEventBus<
115
- C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[]>
116
- > {
117
- readonly emit: <E extends C["events"][number]>(
118
- event: E,
119
- payload: RpcEventPayload<E>
120
- ) => Effect.Effect<void, never>;
121
- }
122
-
123
- export interface EventSubscriber<
124
- C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[]>
125
- > {
126
- readonly subscribe: <E extends C["events"][number]>(
127
- event: E,
128
- handler: (payload: RpcEventPayload<E>) => void
129
- ) => () => void;
130
- readonly subscribeByName: (
131
- name: C["events"][number]["name"],
132
- handler: (payload: unknown) => void
133
- ) => () => void;
134
- }