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 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 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.
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
- ### Methods and events
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 { createRpcServer, createEventBus } from "electron-effect-rpc/main";
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
- createRpcServer(contract, ipcMain, implementations);
111
+ const endpoint = createRpcEndpoint(contract, ipcMain, implementations, {
112
+ runtime: Runtime.defaultRuntime,
113
+ });
114
+ endpoint.start();
96
115
 
97
- const eventBus = createEventBus(contract, {
116
+ const publisher = createEventPublisher(contract, {
98
117
  getWindow: () => mainWindow,
99
118
  });
119
+ publisher.start();
100
120
 
101
- eventBus.emit(WorkUnitProgress, {
121
+ publisher.publish(WorkUnitProgress, {
102
122
  requestId: "req-1",
103
123
  chunk: "working...",
104
124
  done: false,
105
125
  });
106
126
  ```
107
127
 
108
- If your handlers require services in the Effect environment, provide a runtime:
128
+ Pass the runtime used to execute handler effects:
109
129
 
110
130
  ```ts
111
131
  import * as Runtime from "effect/Runtime";
112
- import { createRpcServer } from "electron-effect-rpc/main";
132
+ import { createRpcEndpoint } from "electron-effect-rpc/main";
113
133
  import { contract } from "./contract.ts";
114
134
 
115
- createRpcServer(contract, ipcMain, implementations, {
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, { subscribe: window.events.subscribe });
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
- // return encoded Exit values from your handler logic
184
- return payload;
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 { createRpcServer } from "electron-effect-rpc/main";
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
- createRpcServer(contract, ipcMainStub, implementations);
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`, `RpcEventBus`, `IpcMainLike`
268
+ - Type aliases such as `Implementations`, `RpcClient`, `RpcEventPublisher`, `IpcMainLike`
226
269
  - `electron-effect-rpc/main`
227
- - `createRpcServer`, `createEventBus`
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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"}
@@ -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"}
@@ -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"}