electron-effect-rpc 0.1.0 → 0.2.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 +62 -19
- package/dist/contract.d.ts +41 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +46 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +296 -0
- package/dist/preload.d.ts +15 -0
- package/dist/preload.d.ts.map +1 -0
- package/dist/preload.js +31 -0
- package/dist/protocol.d.ts +23 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +77 -0
- package/dist/renderer.d.ts +6 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +201 -0
- package/dist/testing.d.ts +15 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +17 -0
- package/dist/types.d.ts +124 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/package.json +28 -8
- package/src/contract.ts +0 -165
- package/src/main.ts +0 -169
- package/src/preload.ts +0 -33
- package/src/renderer.ts +0 -167
- package/src/testing.ts +0 -33
- package/src/types.ts +0 -134
package/README.md
CHANGED
|
@@ -11,9 +11,10 @@ uses Electron 38) and assumes ESM-capable bundling.
|
|
|
11
11
|
- Single shared contract for methods and events.
|
|
12
12
|
- End-to-end type safety using @effect/schema.
|
|
13
13
|
- Promise-based renderer client with typed errors.
|
|
14
|
-
- Effect-based main handlers with
|
|
15
|
-
-
|
|
16
|
-
-
|
|
14
|
+
- Effect-based main handlers with explicit runtime injection.
|
|
15
|
+
- Explicit lifecycle handles for main-process RPC and event publishing.
|
|
16
|
+
- Bounded event queue with drop-oldest backpressure.
|
|
17
|
+
- Structured diagnostics hooks for decode/protocol/dispatch failures.
|
|
17
18
|
|
|
18
19
|
## Requirements
|
|
19
20
|
- Electron with context isolation enabled.
|
|
@@ -30,7 +31,21 @@ and let the workspace resolver handle the rest.
|
|
|
30
31
|
|
|
31
32
|
## Core Concepts
|
|
32
33
|
|
|
33
|
-
###
|
|
34
|
+
### Communication directions
|
|
35
|
+
|
|
36
|
+
This library provides two communication patterns for Electron IPC:
|
|
37
|
+
|
|
38
|
+
| Direction | Mechanism | Pattern | Use case |
|
|
39
|
+
|-----------|-----------|---------|----------|
|
|
40
|
+
| **Renderer → Main** | RPC methods | Request/response | Fetching data, triggering actions, calling main process APIs |
|
|
41
|
+
| **Main → Renderer** | Event bus | Push/broadcast | Progress updates, state changes, background task notifications |
|
|
42
|
+
|
|
43
|
+
**RPC methods** are for when the renderer needs something from the main process. The renderer calls a method and awaits a typed response.
|
|
44
|
+
|
|
45
|
+
**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.
|
|
46
|
+
|
|
47
|
+
### Defining methods and events
|
|
48
|
+
|
|
34
49
|
Define methods and events using schema-based helpers:
|
|
35
50
|
|
|
36
51
|
```ts
|
|
@@ -85,36 +100,43 @@ export const ReadTextFile = rpc(
|
|
|
85
100
|
```ts
|
|
86
101
|
import { app, ipcMain } from "electron";
|
|
87
102
|
import { Effect } from "effect";
|
|
88
|
-
import
|
|
103
|
+
import * as Runtime from "effect/Runtime";
|
|
104
|
+
import { createRpcEndpoint, createEventPublisher } from "electron-effect-rpc/main";
|
|
89
105
|
import { contract, WorkUnitProgress } from "./contract.ts";
|
|
90
106
|
|
|
91
107
|
const implementations = {
|
|
92
108
|
GetAppVersion: () => Effect.succeed({ version: app.getVersion() }),
|
|
93
109
|
};
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
const endpoint = createRpcEndpoint(contract, ipcMain, implementations, {
|
|
112
|
+
runtime: Runtime.defaultRuntime,
|
|
113
|
+
});
|
|
114
|
+
endpoint.start();
|
|
96
115
|
|
|
97
|
-
const
|
|
116
|
+
const publisher = createEventPublisher(contract, {
|
|
98
117
|
getWindow: () => mainWindow,
|
|
99
118
|
});
|
|
119
|
+
publisher.start();
|
|
100
120
|
|
|
101
|
-
|
|
121
|
+
publisher.publish(WorkUnitProgress, {
|
|
102
122
|
requestId: "req-1",
|
|
103
123
|
chunk: "working...",
|
|
104
124
|
done: false,
|
|
105
125
|
});
|
|
106
126
|
```
|
|
107
127
|
|
|
108
|
-
|
|
128
|
+
Pass the runtime used to execute handler effects:
|
|
109
129
|
|
|
110
130
|
```ts
|
|
111
131
|
import * as Runtime from "effect/Runtime";
|
|
112
|
-
import {
|
|
132
|
+
import { createRpcEndpoint } from "electron-effect-rpc/main";
|
|
113
133
|
import { contract } from "./contract.ts";
|
|
114
134
|
|
|
115
|
-
|
|
135
|
+
const endpoint = createRpcEndpoint(contract, ipcMain, implementations, {
|
|
116
136
|
runtime: Runtime.defaultRuntime,
|
|
117
137
|
});
|
|
138
|
+
|
|
139
|
+
endpoint.start();
|
|
118
140
|
```
|
|
119
141
|
|
|
120
142
|
### Preload: expose bridge globals
|
|
@@ -138,13 +160,25 @@ exposeRpcBridge({
|
|
|
138
160
|
});
|
|
139
161
|
```
|
|
140
162
|
|
|
163
|
+
Or use low-level bridge adapters for custom exposure:
|
|
164
|
+
```ts
|
|
165
|
+
import { createBridgeAdapters } from "electron-effect-rpc/preload";
|
|
166
|
+
|
|
167
|
+
const bridge = createBridgeAdapters();
|
|
168
|
+
// bridge.invoke(method, payload)
|
|
169
|
+
// bridge.subscribe(name, handler)
|
|
170
|
+
```
|
|
171
|
+
|
|
141
172
|
### Renderer: create client and subscriber
|
|
142
173
|
```ts
|
|
143
174
|
import { createRpcClient, createEventSubscriber } from "electron-effect-rpc/renderer";
|
|
144
175
|
import { contract, WorkUnitProgress } from "./contract.ts";
|
|
145
176
|
|
|
146
177
|
const client = createRpcClient(contract, { invoke: window.rpc.invoke });
|
|
147
|
-
const events = createEventSubscriber(contract, {
|
|
178
|
+
const events = createEventSubscriber(contract, {
|
|
179
|
+
subscribe: window.events.subscribe,
|
|
180
|
+
decodeMode: "safe", // default
|
|
181
|
+
});
|
|
148
182
|
|
|
149
183
|
const { version } = await client.GetAppVersion();
|
|
150
184
|
|
|
@@ -180,8 +214,10 @@ import { createInvokeStub } from "electron-effect-rpc/testing";
|
|
|
180
214
|
import { contract } from "./contract.ts";
|
|
181
215
|
|
|
182
216
|
const invoke = createInvokeStub(async (method, payload) => {
|
|
183
|
-
|
|
184
|
-
|
|
217
|
+
return {
|
|
218
|
+
type: "success",
|
|
219
|
+
data: { version: "1.0.0" },
|
|
220
|
+
};
|
|
185
221
|
});
|
|
186
222
|
|
|
187
223
|
const client = createRpcClient(contract, { invoke });
|
|
@@ -196,7 +232,8 @@ expect(invoke.invocations).toEqual([
|
|
|
196
232
|
You can stub `IpcMainLike` and collect registered handlers:
|
|
197
233
|
|
|
198
234
|
```ts
|
|
199
|
-
import
|
|
235
|
+
import * as Runtime from "effect/Runtime";
|
|
236
|
+
import { createRpcEndpoint } from "electron-effect-rpc/main";
|
|
200
237
|
import type { IpcMainLike } from "electron-effect-rpc/types";
|
|
201
238
|
import { contract } from "./contract.ts";
|
|
202
239
|
|
|
@@ -205,9 +242,15 @@ const ipcMainStub: IpcMainLike = {
|
|
|
205
242
|
handle: (channel, handler) => {
|
|
206
243
|
handlers.set(channel, handler);
|
|
207
244
|
},
|
|
245
|
+
removeHandler: (channel) => {
|
|
246
|
+
handlers.delete(channel);
|
|
247
|
+
},
|
|
208
248
|
};
|
|
209
249
|
|
|
210
|
-
|
|
250
|
+
const endpoint = createRpcEndpoint(contract, ipcMainStub, implementations, {
|
|
251
|
+
runtime: Runtime.defaultRuntime,
|
|
252
|
+
});
|
|
253
|
+
endpoint.start();
|
|
211
254
|
```
|
|
212
255
|
|
|
213
256
|
## Error Handling
|
|
@@ -222,13 +265,13 @@ Entry points:
|
|
|
222
265
|
- `electron-effect-rpc/contract`
|
|
223
266
|
- `rpc`, `event`, `defineContract`, `exitSchemaFor`, `SchemaNoContext`, `NoError`
|
|
224
267
|
- `electron-effect-rpc/types`
|
|
225
|
-
- Type aliases such as `Implementations`, `RpcClient`, `
|
|
268
|
+
- Type aliases such as `Implementations`, `RpcClient`, `RpcEventPublisher`, `IpcMainLike`
|
|
226
269
|
- `electron-effect-rpc/main`
|
|
227
|
-
- `
|
|
270
|
+
- `createRpcEndpoint`, `createEventPublisher`
|
|
228
271
|
- `electron-effect-rpc/renderer`
|
|
229
272
|
- `createRpcClient`, `createEventSubscriber`, `RpcDefectError`
|
|
230
273
|
- `electron-effect-rpc/preload`
|
|
231
|
-
- `exposeRpcBridge`
|
|
274
|
+
- `exposeRpcBridge`, `createBridgeAdapters`
|
|
232
275
|
- `electron-effect-rpc/testing`
|
|
233
276
|
- `createInvokeStub`, `createDeferred`
|
|
234
277
|
|
|
@@ -0,0 +1,41 @@
|
|
|
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 | NoError;
|
|
6
|
+
export declare function isNoErrorSchema(schema: ErrorSchema): schema is NoError;
|
|
7
|
+
export interface RpcMethod<Name extends string, Req extends SchemaNoContext, Res extends SchemaNoContext, Err extends ErrorSchema = NoError> {
|
|
8
|
+
readonly name: Name;
|
|
9
|
+
readonly req: Req;
|
|
10
|
+
readonly res: Res;
|
|
11
|
+
readonly err: Err;
|
|
12
|
+
}
|
|
13
|
+
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>;
|
|
14
|
+
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>;
|
|
15
|
+
export interface RpcEvent<Payload extends SchemaNoContext, Context extends SchemaNoContext | null, Name extends string = string> {
|
|
16
|
+
readonly name: Name;
|
|
17
|
+
readonly payload: Payload;
|
|
18
|
+
readonly context: Context;
|
|
19
|
+
}
|
|
20
|
+
export declare function event<const Name extends string, Payload extends SchemaNoContext, Context extends SchemaNoContext>(name: Name, payload: Payload, context: Context): RpcEvent<Payload, Context, Name>;
|
|
21
|
+
export declare function event<const Name extends string, Payload extends SchemaNoContext>(name: Name, payload: Payload): RpcEvent<Payload, null, Name>;
|
|
22
|
+
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>;
|
|
23
|
+
export type AnyMethod = RpcMethod<string, SchemaNoContext, SchemaNoContext, ErrorSchema>;
|
|
24
|
+
export type AnyEvent = RpcEvent<SchemaNoContext, SchemaNoContext | null, string>;
|
|
25
|
+
export type RpcInput<M extends AnyMethod> = S.Schema.Type<M["req"]>;
|
|
26
|
+
export type RpcOutput<M extends AnyMethod> = S.Schema.Type<M["res"]>;
|
|
27
|
+
export type RpcError<M extends AnyMethod> = S.Schema.Type<M["err"]>;
|
|
28
|
+
export type RpcEventPayload<E extends AnyEvent> = S.Schema.Type<E["payload"]>;
|
|
29
|
+
/** Extract a method from a tuple by its name string literal. */
|
|
30
|
+
export type ExtractMethod<Methods extends readonly AnyMethod[], Name extends string> = Extract<Methods[number], {
|
|
31
|
+
readonly name: Name;
|
|
32
|
+
}>;
|
|
33
|
+
export interface RpcContract<Methods extends ReadonlyArray<AnyMethod>, Events extends ReadonlyArray<AnyEvent>> {
|
|
34
|
+
readonly methods: Methods;
|
|
35
|
+
readonly events: Events;
|
|
36
|
+
}
|
|
37
|
+
export declare function defineContract<const Methods extends ReadonlyArray<AnyMethod>, const Events extends ReadonlyArray<AnyEvent>>(input: {
|
|
38
|
+
readonly methods: Methods;
|
|
39
|
+
readonly events: Events;
|
|
40
|
+
}): RpcContract<Methods, Events>;
|
|
41
|
+
//# 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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"}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as S from "@effect/schema/Schema";
|
|
2
|
+
export const NoError = S.Never;
|
|
3
|
+
export function isNoErrorSchema(schema) {
|
|
4
|
+
return schema === NoError;
|
|
5
|
+
}
|
|
6
|
+
export function rpc(name, req, res, err = NoError) {
|
|
7
|
+
return { name, req, res, err };
|
|
8
|
+
}
|
|
9
|
+
export function event(name, payload, context) {
|
|
10
|
+
return { name, payload, context: context ?? null };
|
|
11
|
+
}
|
|
12
|
+
export const exitSchemaFor = (method) => S.Exit({
|
|
13
|
+
success: method.res,
|
|
14
|
+
failure: method.err,
|
|
15
|
+
defect: S.Defect,
|
|
16
|
+
});
|
|
17
|
+
function collectDuplicates(names) {
|
|
18
|
+
const counts = new Map();
|
|
19
|
+
const duplicates = [];
|
|
20
|
+
for (const name of names) {
|
|
21
|
+
const next = (counts.get(name) ?? 0) + 1;
|
|
22
|
+
counts.set(name, next);
|
|
23
|
+
if (next === 2) {
|
|
24
|
+
duplicates.push(name);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return duplicates;
|
|
28
|
+
}
|
|
29
|
+
export function defineContract(input) {
|
|
30
|
+
const { methods, events } = input;
|
|
31
|
+
if (!Array.isArray(methods)) {
|
|
32
|
+
throw new Error("RPC contract methods must be an array.");
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(events)) {
|
|
35
|
+
throw new Error("RPC contract events must be an array.");
|
|
36
|
+
}
|
|
37
|
+
const duplicateMethods = collectDuplicates(methods.map((method) => method.name));
|
|
38
|
+
if (duplicateMethods.length > 0) {
|
|
39
|
+
throw new Error(`Duplicate RPC method name(s): ${duplicateMethods.join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
const duplicateEvents = collectDuplicates(events.map((event) => event.name));
|
|
42
|
+
if (duplicateEvents.length > 0) {
|
|
43
|
+
throw new Error(`Duplicate RPC event name(s): ${duplicateEvents.join(", ")}`);
|
|
44
|
+
}
|
|
45
|
+
return input;
|
|
46
|
+
}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RpcContract } from "./contract.ts";
|
|
2
|
+
import { type AnyEvent, type AnyMethod, type EventPublisherOptions, type Implementations, type IpcMainLike, type RpcEndpoint, type RpcEndpointOptions, type RpcEventPublisher } from "./types.ts";
|
|
3
|
+
export declare function createRpcEndpoint<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: RpcEndpointOptions<R>): RpcEndpoint;
|
|
4
|
+
export declare function createEventPublisher<const Methods extends ReadonlyArray<AnyMethod>, const Events extends ReadonlyArray<AnyEvent>>(_contract: RpcContract<Methods, Events>, options: EventPublisherOptions): RpcEventPublisher<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,KAAK,EACV,WAAW,EAKZ,MAAM,eAAe,CAAC;AAQvB,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,SAAS,EAEd,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAC;AAmBpB,wBAAgB,iBAAiB,CAC/B,KAAK,CAAC,OAAO,SAAS,aAAa,CAAC,SAAS,CAAC,EAC9C,KAAK,CAAC,MAAM,SAAS,aAAa,CAAC,QAAQ,CAAC,EAC5C,CAAC,GAAG,KAAK,EAET,QAAQ,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EACtC,GAAG,EAAE,WAAW,EAChB,eAAe,EAAE,eAAe,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,EACjE,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC7B,WAAW,CA4Kb;AAmBD,wBAAgB,oBAAoB,CAClC,KAAK,CAAC,OAAO,SAAS,aAAa,CAAC,SAAS,CAAC,EAC9C,KAAK,CAAC,MAAM,SAAS,aAAa,CAAC,QAAQ,CAAC,EAE5C,SAAS,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EACvC,OAAO,EAAE,qBAAqB,GAC7B,iBAAiB,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CA8KjD"}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import * as S from "@effect/schema/Schema";
|
|
2
|
+
import { Cause, Effect, Exit } from "effect";
|
|
3
|
+
import * as Runtime from "effect/Runtime";
|
|
4
|
+
import { isNoErrorSchema } from "./contract.js";
|
|
5
|
+
import { extractErrorTag, safelyCall, toDefectEnvelope, } from "./protocol.js";
|
|
6
|
+
import { defaultChannelPrefix, } from "./types.js";
|
|
7
|
+
function resolveChannelPrefix(prefix) {
|
|
8
|
+
return prefix ?? defaultChannelPrefix;
|
|
9
|
+
}
|
|
10
|
+
function isImplementation(value) {
|
|
11
|
+
return typeof value === "function";
|
|
12
|
+
}
|
|
13
|
+
export function createRpcEndpoint(contract, ipc, implementations, options) {
|
|
14
|
+
const channelPrefix = resolveChannelPrefix(options.channelPrefix);
|
|
15
|
+
const diagnostics = options.diagnostics;
|
|
16
|
+
const runPromiseExit = Runtime.runPromiseExit(options.runtime);
|
|
17
|
+
const implementationsByName = implementations;
|
|
18
|
+
const methodNames = new Set(contract.methods.map((method) => method.name));
|
|
19
|
+
for (const name in implementations) {
|
|
20
|
+
if (!methodNames.has(name)) {
|
|
21
|
+
throw new Error(`Implementation provided for unknown RPC method: ${name}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function reportProtocolError(method, response, cause) {
|
|
25
|
+
safelyCall(diagnostics?.onProtocolError, {
|
|
26
|
+
method,
|
|
27
|
+
response,
|
|
28
|
+
cause,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const listeners = new Map();
|
|
32
|
+
for (const method of contract.methods) {
|
|
33
|
+
const impl = implementationsByName[method.name];
|
|
34
|
+
if (!isImplementation(impl)) {
|
|
35
|
+
throw new Error(`Missing implementation for RPC method: ${method.name}`);
|
|
36
|
+
}
|
|
37
|
+
const decodeInput = S.decodeUnknownSync(method.req);
|
|
38
|
+
const encodeSuccess = S.encodeSync(method.res);
|
|
39
|
+
const encodeFailure = isNoErrorSchema(method.err)
|
|
40
|
+
? null
|
|
41
|
+
: S.encodeSync(method.err);
|
|
42
|
+
const channel = `${channelPrefix.rpc}${method.name}`;
|
|
43
|
+
listeners.set(channel, async function handleRpcRequest(_event, rawPayload) {
|
|
44
|
+
let input;
|
|
45
|
+
try {
|
|
46
|
+
input = decodeInput(rawPayload);
|
|
47
|
+
}
|
|
48
|
+
catch (cause) {
|
|
49
|
+
safelyCall(diagnostics?.onDecodeFailure, {
|
|
50
|
+
scope: "rpc-request",
|
|
51
|
+
name: method.name,
|
|
52
|
+
payload: rawPayload,
|
|
53
|
+
cause,
|
|
54
|
+
});
|
|
55
|
+
return toDefectEnvelope(cause, `RPC ${method.name} request decode failed`);
|
|
56
|
+
}
|
|
57
|
+
let effect;
|
|
58
|
+
try {
|
|
59
|
+
effect = impl(input);
|
|
60
|
+
}
|
|
61
|
+
catch (cause) {
|
|
62
|
+
return toDefectEnvelope(cause, `RPC ${method.name} implementation threw`);
|
|
63
|
+
}
|
|
64
|
+
const exit = await runPromiseExit(effect);
|
|
65
|
+
if (Exit.isSuccess(exit)) {
|
|
66
|
+
try {
|
|
67
|
+
return {
|
|
68
|
+
type: "success",
|
|
69
|
+
data: encodeSuccess(exit.value),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (cause) {
|
|
73
|
+
reportProtocolError(method.name, exit.value, cause);
|
|
74
|
+
return toDefectEnvelope(cause, `RPC ${method.name} success encoding failed`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const failure = Cause.failureOption(exit.cause);
|
|
78
|
+
if (failure._tag === "Some") {
|
|
79
|
+
if (!encodeFailure) {
|
|
80
|
+
return toDefectEnvelope(failure.value, `RPC ${method.name} returned a typed failure, but method declares NoError`);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return {
|
|
84
|
+
type: "failure",
|
|
85
|
+
error: {
|
|
86
|
+
tag: extractErrorTag(failure.value),
|
|
87
|
+
data: encodeFailure(failure.value),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (cause) {
|
|
92
|
+
reportProtocolError(method.name, failure.value, cause);
|
|
93
|
+
return toDefectEnvelope(cause, `RPC ${method.name} failure encoding failed`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const defect = Cause.dieOption(exit.cause);
|
|
97
|
+
if (defect._tag === "Some") {
|
|
98
|
+
return toDefectEnvelope(defect.value, `RPC ${method.name} defect`);
|
|
99
|
+
}
|
|
100
|
+
return toDefectEnvelope(exit.cause, `RPC ${method.name} interrupted`);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
let running = false;
|
|
104
|
+
let disposed = false;
|
|
105
|
+
function start() {
|
|
106
|
+
if (disposed) {
|
|
107
|
+
throw new Error("RPC endpoint has already been disposed.");
|
|
108
|
+
}
|
|
109
|
+
if (running) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
for (const [channel, listener] of listeners) {
|
|
113
|
+
ipc.handle(channel, listener);
|
|
114
|
+
}
|
|
115
|
+
running = true;
|
|
116
|
+
}
|
|
117
|
+
function stop() {
|
|
118
|
+
if (!running) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
for (const channel of listeners.keys()) {
|
|
122
|
+
ipc.removeHandler(channel);
|
|
123
|
+
}
|
|
124
|
+
running = false;
|
|
125
|
+
}
|
|
126
|
+
function dispose() {
|
|
127
|
+
if (disposed) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
stop();
|
|
131
|
+
disposed = true;
|
|
132
|
+
}
|
|
133
|
+
function isRunning() {
|
|
134
|
+
return running;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
start,
|
|
138
|
+
stop,
|
|
139
|
+
dispose,
|
|
140
|
+
isRunning,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function clampQueueSize(maxQueueSize) {
|
|
144
|
+
if (maxQueueSize === undefined) {
|
|
145
|
+
return 1000;
|
|
146
|
+
}
|
|
147
|
+
if (!Number.isFinite(maxQueueSize) || maxQueueSize < 1) {
|
|
148
|
+
throw new Error("Event publisher maxQueueSize must be a positive finite number.");
|
|
149
|
+
}
|
|
150
|
+
return Math.floor(maxQueueSize);
|
|
151
|
+
}
|
|
152
|
+
export function createEventPublisher(_contract, options) {
|
|
153
|
+
const channelPrefix = resolveChannelPrefix(options.channelPrefix);
|
|
154
|
+
const diagnostics = options.diagnostics;
|
|
155
|
+
const maxQueueSize = clampQueueSize(options.maxQueueSize);
|
|
156
|
+
const queue = [];
|
|
157
|
+
let dropped = 0;
|
|
158
|
+
let running = false;
|
|
159
|
+
let disposed = false;
|
|
160
|
+
let draining = false;
|
|
161
|
+
let drainScheduled = false;
|
|
162
|
+
function scheduleDrain() {
|
|
163
|
+
if (!running || disposed || draining || drainScheduled) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
drainScheduled = true;
|
|
167
|
+
queueMicrotask(() => {
|
|
168
|
+
drainScheduled = false;
|
|
169
|
+
drain();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function dispatch(item) {
|
|
173
|
+
let encoded;
|
|
174
|
+
try {
|
|
175
|
+
encoded = S.encodeSync(item.event.payload)(item.payload);
|
|
176
|
+
}
|
|
177
|
+
catch (cause) {
|
|
178
|
+
dropped += 1;
|
|
179
|
+
safelyCall(diagnostics?.onDecodeFailure, {
|
|
180
|
+
scope: "event-payload",
|
|
181
|
+
name: item.event.name,
|
|
182
|
+
payload: item.payload,
|
|
183
|
+
cause,
|
|
184
|
+
});
|
|
185
|
+
safelyCall(diagnostics?.onDroppedEvent, {
|
|
186
|
+
event: item.event.name,
|
|
187
|
+
payload: item.payload,
|
|
188
|
+
reason: "encoding_failed",
|
|
189
|
+
queued: queue.length,
|
|
190
|
+
dropped,
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const window = options.getWindow();
|
|
195
|
+
if (!window || window.isDestroyed()) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
window.webContents.send(`${channelPrefix.event}${item.event.name}`, encoded);
|
|
200
|
+
}
|
|
201
|
+
catch (cause) {
|
|
202
|
+
safelyCall(diagnostics?.onDispatchFailure, {
|
|
203
|
+
event: item.event.name,
|
|
204
|
+
payload: item.payload,
|
|
205
|
+
cause,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function drain() {
|
|
210
|
+
if (!running || disposed || draining) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
draining = true;
|
|
214
|
+
try {
|
|
215
|
+
while (running && !disposed && queue.length > 0) {
|
|
216
|
+
const next = queue.shift();
|
|
217
|
+
if (!next) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
dispatch(next);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
draining = false;
|
|
225
|
+
if (running && !disposed && queue.length > 0) {
|
|
226
|
+
scheduleDrain();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function enqueue(item) {
|
|
231
|
+
if (queue.length >= maxQueueSize) {
|
|
232
|
+
const evicted = queue.shift();
|
|
233
|
+
dropped += 1;
|
|
234
|
+
if (evicted) {
|
|
235
|
+
safelyCall(diagnostics?.onDroppedEvent, {
|
|
236
|
+
event: evicted.event.name,
|
|
237
|
+
payload: evicted.payload,
|
|
238
|
+
reason: "queue_full",
|
|
239
|
+
queued: queue.length,
|
|
240
|
+
dropped,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
queue.push(item);
|
|
245
|
+
scheduleDrain();
|
|
246
|
+
}
|
|
247
|
+
function publish(event, payload) {
|
|
248
|
+
return Effect.sync(() => {
|
|
249
|
+
if (disposed) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
enqueue({ event, payload });
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function start() {
|
|
256
|
+
if (disposed) {
|
|
257
|
+
throw new Error("Event publisher has already been disposed.");
|
|
258
|
+
}
|
|
259
|
+
if (running) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
running = true;
|
|
263
|
+
scheduleDrain();
|
|
264
|
+
}
|
|
265
|
+
function stop() {
|
|
266
|
+
if (!running) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
running = false;
|
|
270
|
+
}
|
|
271
|
+
function dispose() {
|
|
272
|
+
if (disposed) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
stop();
|
|
276
|
+
queue.length = 0;
|
|
277
|
+
disposed = true;
|
|
278
|
+
}
|
|
279
|
+
function isRunning() {
|
|
280
|
+
return running;
|
|
281
|
+
}
|
|
282
|
+
function stats() {
|
|
283
|
+
return {
|
|
284
|
+
queued: queue.length,
|
|
285
|
+
dropped,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
publish,
|
|
290
|
+
start,
|
|
291
|
+
stop,
|
|
292
|
+
dispose,
|
|
293
|
+
isRunning,
|
|
294
|
+
stats,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ChannelPrefix, type EventSubscribe, type RpcInvoke } from "./types.ts";
|
|
2
|
+
export type BridgeAdapters = {
|
|
3
|
+
readonly invoke: RpcInvoke;
|
|
4
|
+
readonly subscribe: EventSubscribe;
|
|
5
|
+
};
|
|
6
|
+
export type BridgeAdaptersOptions = {
|
|
7
|
+
readonly channelPrefix?: ChannelPrefix;
|
|
8
|
+
};
|
|
9
|
+
export type BridgeExposureOptions = BridgeAdaptersOptions & {
|
|
10
|
+
readonly rpcGlobal?: string;
|
|
11
|
+
readonly eventsGlobal?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function createBridgeAdapters(options?: BridgeAdaptersOptions): BridgeAdapters;
|
|
14
|
+
export declare function exposeRpcBridge(options?: BridgeExposureOptions): void;
|
|
15
|
+
//# sourceMappingURL=preload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preload.d.ts","sourceRoot":"","sources":["../src/preload.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,qBAAqB,GAAG;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,wBAAgB,oBAAoB,CAClC,OAAO,CAAC,EAAE,qBAAqB,GAC9B,cAAc,CAsBhB;AAED,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAerE"}
|
package/dist/preload.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { contextBridge, ipcRenderer } from "electron";
|
|
2
|
+
import { defaultChannelPrefix, } from "./types.js";
|
|
3
|
+
export function createBridgeAdapters(options) {
|
|
4
|
+
const channelPrefix = options?.channelPrefix ?? defaultChannelPrefix;
|
|
5
|
+
const invoke = (method, payload) => ipcRenderer.invoke(`${channelPrefix.rpc}${method}`, payload);
|
|
6
|
+
const subscribe = (event, listener) => {
|
|
7
|
+
const wrapped = (_event, payload) => listener(payload);
|
|
8
|
+
const channel = `${channelPrefix.event}${event}`;
|
|
9
|
+
ipcRenderer.on(channel, wrapped);
|
|
10
|
+
return () => {
|
|
11
|
+
ipcRenderer.removeListener(channel, wrapped);
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
invoke,
|
|
16
|
+
subscribe,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function exposeRpcBridge(options) {
|
|
20
|
+
const rpcGlobal = options?.rpcGlobal ?? "rpc";
|
|
21
|
+
const eventsGlobal = options?.eventsGlobal ?? "events";
|
|
22
|
+
const adapters = createBridgeAdapters({
|
|
23
|
+
channelPrefix: options?.channelPrefix,
|
|
24
|
+
});
|
|
25
|
+
contextBridge.exposeInMainWorld(rpcGlobal, {
|
|
26
|
+
invoke: adapters.invoke,
|
|
27
|
+
});
|
|
28
|
+
contextBridge.exposeInMainWorld(eventsGlobal, {
|
|
29
|
+
subscribe: adapters.subscribe,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type RpcSuccessEnvelope = {
|
|
2
|
+
readonly type: "success";
|
|
3
|
+
readonly data: unknown;
|
|
4
|
+
};
|
|
5
|
+
export type RpcFailureEnvelope = {
|
|
6
|
+
readonly type: "failure";
|
|
7
|
+
readonly error: {
|
|
8
|
+
readonly tag: string;
|
|
9
|
+
readonly data: unknown;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export type RpcDefectEnvelope = {
|
|
13
|
+
readonly type: "defect";
|
|
14
|
+
readonly message: string;
|
|
15
|
+
readonly cause?: unknown;
|
|
16
|
+
};
|
|
17
|
+
export type RpcResponseEnvelope = RpcSuccessEnvelope | RpcFailureEnvelope | RpcDefectEnvelope;
|
|
18
|
+
export declare function formatUnknown(value: unknown): string;
|
|
19
|
+
export declare function extractErrorTag(error: unknown): string;
|
|
20
|
+
export declare function toDefectEnvelope(cause: unknown, prefix?: string): RpcDefectEnvelope;
|
|
21
|
+
export declare function safelyCall<T>(callback: ((context: T) => void) | undefined, context: T): void;
|
|
22
|
+
export declare function parseRpcResponseEnvelope(value: unknown): RpcResponseEnvelope | null;
|
|
23
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;KACxB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAC3B,kBAAkB,GAClB,kBAAkB,GAClB,iBAAiB,CAAC;AAUtB,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUtD;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,OAAO,EACd,MAAM,CAAC,EAAE,MAAM,GACd,iBAAiB,CAOnB;AAED,wBAAgB,UAAU,CAAC,CAAC,EAC1B,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,EAC5C,OAAO,EAAE,CAAC,GACT,IAAI,CAUN;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,GACb,mBAAmB,GAAG,IAAI,CA+C5B"}
|