electron-effect-rpc 0.1.0
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 +241 -0
- package/package.json +47 -0
- package/src/contract.ts +165 -0
- package/src/main.ts +169 -0
- package/src/preload.ts +33 -0
- package/src/renderer.ts +167 -0
- package/src/testing.ts +33 -0
- package/src/types.ts +134 -0
package/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# electron-effect-rpc
|
|
2
|
+
|
|
3
|
+
Typed IPC RPC for Electron, built on Effect and @effect/schema. This library
|
|
4
|
+
lets you define a shared contract, generate a typed RPC client in the renderer,
|
|
5
|
+
register handlers in the main process, and stream typed events across processes.
|
|
6
|
+
|
|
7
|
+
This package is ESM-only. It targets modern Electron runtimes (current project
|
|
8
|
+
uses Electron 38) and assumes ESM-capable bundling.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
- Single shared contract for methods and events.
|
|
12
|
+
- End-to-end type safety using @effect/schema.
|
|
13
|
+
- Promise-based renderer client with typed errors.
|
|
14
|
+
- Effect-based main handlers with optional runtime injection.
|
|
15
|
+
- Event bus and subscriber for typed renderer events.
|
|
16
|
+
- No protocol handshake or versioning; schema decoding is the source of truth.
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
- Electron with context isolation enabled.
|
|
20
|
+
- ESM-capable build pipeline.
|
|
21
|
+
- Peer dependencies: `effect`, `@effect/schema`, `electron`.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
```sh
|
|
25
|
+
bun add electron-effect-rpc effect @effect/schema
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If you are in a monorepo workspace, add the dependency to the target package
|
|
29
|
+
and let the workspace resolver handle the rest.
|
|
30
|
+
|
|
31
|
+
## Core Concepts
|
|
32
|
+
|
|
33
|
+
### Methods and events
|
|
34
|
+
Define methods and events using schema-based helpers:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import * as S from "@effect/schema/Schema";
|
|
38
|
+
import { defineContract, event, rpc } from "electron-effect-rpc/contract";
|
|
39
|
+
|
|
40
|
+
export const GetAppVersion = rpc(
|
|
41
|
+
"GetAppVersion",
|
|
42
|
+
S.Struct({}),
|
|
43
|
+
S.Struct({ version: S.String })
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export const WorkUnitProgress = event(
|
|
47
|
+
"WorkUnitProgress",
|
|
48
|
+
S.Struct({
|
|
49
|
+
requestId: S.String,
|
|
50
|
+
chunk: S.String,
|
|
51
|
+
done: S.Boolean,
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const methods = [GetAppVersion] as const;
|
|
56
|
+
const events = [WorkUnitProgress] as const;
|
|
57
|
+
|
|
58
|
+
export const contract = defineContract({ methods, events });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Errors
|
|
62
|
+
Error schemas should be `Schema.TaggedError` classes. If a method does not
|
|
63
|
+
declare an error schema, it uses `NoError` and the error channel is `never`.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import * as S from "@effect/schema/Schema";
|
|
67
|
+
import { rpc } from "electron-effect-rpc/contract";
|
|
68
|
+
|
|
69
|
+
export class FileReadError extends S.TaggedError<FileReadError>()("FileReadError", {
|
|
70
|
+
message: S.String,
|
|
71
|
+
path: S.String,
|
|
72
|
+
}) {}
|
|
73
|
+
|
|
74
|
+
export const ReadTextFile = rpc(
|
|
75
|
+
"ReadTextFile",
|
|
76
|
+
S.Struct({ path: S.String }),
|
|
77
|
+
S.Struct({ content: S.String }),
|
|
78
|
+
FileReadError
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
### Main process: register handlers
|
|
85
|
+
```ts
|
|
86
|
+
import { app, ipcMain } from "electron";
|
|
87
|
+
import { Effect } from "effect";
|
|
88
|
+
import { createRpcServer, createEventBus } from "electron-effect-rpc/main";
|
|
89
|
+
import { contract, WorkUnitProgress } from "./contract.ts";
|
|
90
|
+
|
|
91
|
+
const implementations = {
|
|
92
|
+
GetAppVersion: () => Effect.succeed({ version: app.getVersion() }),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
createRpcServer(contract, ipcMain, implementations);
|
|
96
|
+
|
|
97
|
+
const eventBus = createEventBus(contract, {
|
|
98
|
+
getWindow: () => mainWindow,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
eventBus.emit(WorkUnitProgress, {
|
|
102
|
+
requestId: "req-1",
|
|
103
|
+
chunk: "working...",
|
|
104
|
+
done: false,
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If your handlers require services in the Effect environment, provide a runtime:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import * as Runtime from "effect/Runtime";
|
|
112
|
+
import { createRpcServer } from "electron-effect-rpc/main";
|
|
113
|
+
import { contract } from "./contract.ts";
|
|
114
|
+
|
|
115
|
+
createRpcServer(contract, ipcMain, implementations, {
|
|
116
|
+
runtime: Runtime.defaultRuntime,
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Preload: expose bridge globals
|
|
121
|
+
```ts
|
|
122
|
+
import { exposeRpcBridge } from "electron-effect-rpc/preload";
|
|
123
|
+
|
|
124
|
+
exposeRpcBridge();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Defaults:
|
|
128
|
+
- RPC global: `window.rpc.invoke(method, payload)`
|
|
129
|
+
- Events global: `window.events.subscribe(name, handler)`
|
|
130
|
+
- Channel prefix: `rpc/` and `event/`
|
|
131
|
+
|
|
132
|
+
You can override globals and prefixes:
|
|
133
|
+
```ts
|
|
134
|
+
exposeRpcBridge({
|
|
135
|
+
rpcGlobal: "rpcApi",
|
|
136
|
+
eventsGlobal: "rpcEvents",
|
|
137
|
+
channelPrefix: { rpc: "rpc/", event: "events/" },
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Renderer: create client and subscriber
|
|
142
|
+
```ts
|
|
143
|
+
import { createRpcClient, createEventSubscriber } from "electron-effect-rpc/renderer";
|
|
144
|
+
import { contract, WorkUnitProgress } from "./contract.ts";
|
|
145
|
+
|
|
146
|
+
const client = createRpcClient(contract, { invoke: window.rpc.invoke });
|
|
147
|
+
const events = createEventSubscriber(contract, { subscribe: window.events.subscribe });
|
|
148
|
+
|
|
149
|
+
const { version } = await client.GetAppVersion();
|
|
150
|
+
|
|
151
|
+
events.subscribe(WorkUnitProgress, (payload) => {
|
|
152
|
+
console.log(payload.chunk);
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Window type augmentation
|
|
157
|
+
If you expose globals in preload, add a local `globals.d.ts`:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
declare global {
|
|
161
|
+
interface Window {
|
|
162
|
+
rpc: {
|
|
163
|
+
invoke: (method: string, payload: unknown) => Promise<unknown>;
|
|
164
|
+
};
|
|
165
|
+
events: {
|
|
166
|
+
subscribe: (name: string, handler: (payload: unknown) => void) => () => void;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Testing
|
|
173
|
+
|
|
174
|
+
### Renderer client tests
|
|
175
|
+
Use the testing helpers to stub invoke behavior:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { createRpcClient } from "electron-effect-rpc/renderer";
|
|
179
|
+
import { createInvokeStub } from "electron-effect-rpc/testing";
|
|
180
|
+
import { contract } from "./contract.ts";
|
|
181
|
+
|
|
182
|
+
const invoke = createInvokeStub(async (method, payload) => {
|
|
183
|
+
// return encoded Exit values from your handler logic
|
|
184
|
+
return payload;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const client = createRpcClient(contract, { invoke });
|
|
188
|
+
await client.GetAppVersion();
|
|
189
|
+
|
|
190
|
+
expect(invoke.invocations).toEqual([
|
|
191
|
+
{ method: "GetAppVersion", payload: {} },
|
|
192
|
+
]);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Main process tests
|
|
196
|
+
You can stub `IpcMainLike` and collect registered handlers:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { createRpcServer } from "electron-effect-rpc/main";
|
|
200
|
+
import type { IpcMainLike } from "electron-effect-rpc/types";
|
|
201
|
+
import { contract } from "./contract.ts";
|
|
202
|
+
|
|
203
|
+
const handlers = new Map<string, (event: unknown, payload: unknown) => unknown>();
|
|
204
|
+
const ipcMainStub: IpcMainLike = {
|
|
205
|
+
handle: (channel, handler) => {
|
|
206
|
+
handlers.set(channel, handler);
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
createRpcServer(contract, ipcMainStub, implementations);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Error Handling
|
|
214
|
+
- If a handler fails with a typed domain error, the renderer client rejects
|
|
215
|
+
with that error instance.
|
|
216
|
+
- If a handler dies or throws a defect, the renderer client rejects with
|
|
217
|
+
`RpcDefectError`.
|
|
218
|
+
|
|
219
|
+
## API Surface
|
|
220
|
+
|
|
221
|
+
Entry points:
|
|
222
|
+
- `electron-effect-rpc/contract`
|
|
223
|
+
- `rpc`, `event`, `defineContract`, `exitSchemaFor`, `SchemaNoContext`, `NoError`
|
|
224
|
+
- `electron-effect-rpc/types`
|
|
225
|
+
- Type aliases such as `Implementations`, `RpcClient`, `RpcEventBus`, `IpcMainLike`
|
|
226
|
+
- `electron-effect-rpc/main`
|
|
227
|
+
- `createRpcServer`, `createEventBus`
|
|
228
|
+
- `electron-effect-rpc/renderer`
|
|
229
|
+
- `createRpcClient`, `createEventSubscriber`, `RpcDefectError`
|
|
230
|
+
- `electron-effect-rpc/preload`
|
|
231
|
+
- `exposeRpcBridge`
|
|
232
|
+
- `electron-effect-rpc/testing`
|
|
233
|
+
- `createInvokeStub`, `createDeferred`
|
|
234
|
+
|
|
235
|
+
## Conventions
|
|
236
|
+
- Relative imports use `.ts` extensions.
|
|
237
|
+
- Package imports are extensionless.
|
|
238
|
+
- No `index.ts` barrel files.
|
|
239
|
+
|
|
240
|
+
## License
|
|
241
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "electron-effect-rpc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed IPC RPC for Electron, built on Effect and @effect/schema",
|
|
5
|
+
"type": "module",
|
|
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"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"electron",
|
|
23
|
+
"effect",
|
|
24
|
+
"rpc",
|
|
25
|
+
"ipc",
|
|
26
|
+
"typed",
|
|
27
|
+
"schema"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/joaoeira/electron-effect-rpc"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@effect/schema": ">=0.69.0",
|
|
36
|
+
"effect": ">=3.0.0",
|
|
37
|
+
"electron": ">=28.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@effect/schema": "^0.69.0",
|
|
41
|
+
"@types/node": "^22.0.0",
|
|
42
|
+
"bun-types": "^1.1.0",
|
|
43
|
+
"effect": "^3.11.0",
|
|
44
|
+
"electron": "^38.0.0",
|
|
45
|
+
"typescript": "^5.7.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/contract.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
}
|