electron-effect-rpc 0.8.0 → 0.10.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/dist/renderer.js CHANGED
@@ -1,7 +1,8 @@
1
- import * as S from "@effect/schema/Schema";
2
- import { Cause, Effect, Exit, Stream } from "effect";
1
+ import * as S from "effect/Schema";
2
+ import { Cause, Effect, Exit, Queue, Result, Stream } from "effect";
3
+ import { isRecord, toDiagnosticCause, } from "./boundary.js";
3
4
  import { exitSchemaFor, isNoErrorSchema, } from "./contract.js";
4
- import { extractStreamIdFromRaw, formatUnknown, isRecord, parseRpcResponseEnvelope, parseStreamFrame, safelyCall, } from "./protocol.js";
5
+ import { extractStreamIdFromRaw, formatUnknown, parseRpcResponseEnvelope, parseStreamFrame, safelyCall, } from "./protocol.js";
5
6
  import { RpcDefectError, } from "./types.js";
6
7
  function requireInvoke(options) {
7
8
  if (!options?.invoke) {
@@ -15,6 +16,9 @@ function requireSubscribe(options) {
15
16
  }
16
17
  return options.subscribe;
17
18
  }
19
+ function emptyRpcClient() {
20
+ return Object.create(null);
21
+ }
18
22
  function rpcDefect(code, message, cause) {
19
23
  return new RpcDefectError(code, message, cause);
20
24
  }
@@ -32,13 +36,13 @@ function decodeLegacyExit(method, raw) {
32
36
  if (Exit.isSuccess(exit)) {
33
37
  return Effect.succeed(exit.value);
34
38
  }
35
- const failureOption = Cause.failureOption(exit.cause);
39
+ const failureOption = Cause.findErrorOption(exit.cause);
36
40
  if (failureOption._tag === "Some") {
37
41
  return Effect.fail(failureOption.value);
38
42
  }
39
- const defectOption = Cause.dieOption(exit.cause);
40
- if (defectOption._tag === "Some") {
41
- const defect = defectOption.value;
43
+ const defectResult = Cause.findDefect(exit.cause);
44
+ if (Result.isSuccess(defectResult)) {
45
+ const defect = defectResult.success;
42
46
  const message = defect instanceof Error ? defect.message : String(defect);
43
47
  return Effect.fail(rpcDefect("remote_defect", message, defect));
44
48
  }
@@ -59,7 +63,7 @@ export function createRpcClient(contract, options) {
59
63
  scope: "rpc-response",
60
64
  name: method.name,
61
65
  payload: envelope.data,
62
- cause,
66
+ cause: toDiagnosticCause(cause),
63
67
  });
64
68
  return rpcDefect("success_payload_decoding_failed", `RPC ${method.name} success payload decoding failed: ${formatUnknown(cause)}`, cause);
65
69
  },
@@ -75,7 +79,7 @@ export function createRpcClient(contract, options) {
75
79
  scope: "rpc-response",
76
80
  name: method.name,
77
81
  payload: envelope.error,
78
- cause,
82
+ cause: toDiagnosticCause(cause),
79
83
  });
80
84
  return rpcDefect("failure_payload_decoding_failed", `RPC ${method.name} failure payload decoding failed: ${formatUnknown(cause)}`, cause);
81
85
  },
@@ -90,8 +94,8 @@ export function createRpcClient(contract, options) {
90
94
  safelyCall(diagnostics?.onDecodeFailure, {
91
95
  scope: "rpc-request",
92
96
  name: method.name,
93
- payload: input,
94
- cause,
97
+ payload: toDiagnosticCause(input),
98
+ cause: toDiagnosticCause(cause),
95
99
  });
96
100
  return rpcDefect("request_encoding_failed", `RPC ${method.name} request encoding failed: ${formatUnknown(cause)}`, cause);
97
101
  },
@@ -101,7 +105,7 @@ export function createRpcClient(contract, options) {
101
105
  safelyCall(diagnostics?.onProtocolError, {
102
106
  method: method.name,
103
107
  response: undefined,
104
- cause,
108
+ cause: toDiagnosticCause(cause),
105
109
  });
106
110
  return rpcDefect("invoke_failed", `RPC ${method.name} invoke failed: ${formatUnknown(cause)}`, cause);
107
111
  },
@@ -116,7 +120,7 @@ export function createRpcClient(contract, options) {
116
120
  return Effect.sync(() => safelyCall(diagnostics?.onProtocolError, {
117
121
  method: method.name,
118
122
  response: raw,
119
- cause,
123
+ cause: toDiagnosticCause(cause),
120
124
  }));
121
125
  }
122
126
  return Effect.void;
@@ -126,18 +130,17 @@ export function createRpcClient(contract, options) {
126
130
  safelyCall(diagnostics?.onProtocolError, {
127
131
  method: method.name,
128
132
  response: raw,
129
- cause,
133
+ cause: toDiagnosticCause(cause),
130
134
  });
131
135
  return Effect.fail(cause);
132
136
  }));
133
- const client = Object.create(null);
134
- const clientRecord = client;
137
+ const client = emptyRpcClient();
135
138
  for (const method of contract.methods) {
136
139
  const caller = (...args) => {
137
140
  const payload = args.length === 0 ? {} : args[0];
138
141
  return call(method, payload);
139
142
  };
140
- clientRecord[method.name] = caller;
143
+ Object.assign(client, { [method.name]: caller });
141
144
  }
142
145
  return client;
143
146
  }
@@ -176,13 +179,16 @@ export function createEventSubscriber(contract, options) {
176
179
  decoded = decoder(payload);
177
180
  }
178
181
  catch (cause) {
179
- reportDecodeFailure(mode, event.name, payload, cause, options);
182
+ reportDecodeFailure(mode, event.name, payload, toDiagnosticCause(cause), options);
180
183
  return;
181
184
  }
182
185
  handler(decoded);
183
186
  });
184
187
  return registerUnsubscribe(unsubscribe);
185
188
  };
189
+ const streamEvent = (event) => Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => subscribeEvent(event, (payload) => {
190
+ Queue.offerUnsafe(queue, payload);
191
+ })), (unsubscribe) => Effect.sync(unsubscribe)));
186
192
  const subscribeByName = (name, handler) => {
187
193
  const event = eventMap.get(name);
188
194
  if (!event) {
@@ -195,7 +201,7 @@ export function createEventSubscriber(contract, options) {
195
201
  decoded = decoder(payload);
196
202
  }
197
203
  catch (cause) {
198
- reportDecodeFailure(mode, name, payload, cause, options);
204
+ reportDecodeFailure(mode, name, payload, toDiagnosticCause(cause), options);
199
205
  return;
200
206
  }
201
207
  handler(decoded);
@@ -220,6 +226,7 @@ export function createEventSubscriber(contract, options) {
220
226
  return {
221
227
  subscribe: subscribeEvent,
222
228
  subscribeByName,
229
+ stream: streamEvent,
223
230
  dispose,
224
231
  };
225
232
  }
@@ -237,6 +244,15 @@ export function createStreamRpcClient(contract, options) {
237
244
  const diagnostics = options.diagnostics;
238
245
  const streamBuffer = options.streamBuffer ?? { bufferSize: "unbounded" };
239
246
  const frameDispatcher = new Map();
247
+ const recentlyClosedStreams = new Map();
248
+ const rememberRecentlyClosed = (streamId, report) => {
249
+ recentlyClosedStreams.set(streamId, report);
250
+ queueMicrotask(() => {
251
+ if (recentlyClosedStreams.get(streamId) === report) {
252
+ recentlyClosedStreams.delete(streamId);
253
+ }
254
+ });
255
+ };
240
256
  // Set up central frame listener
241
257
  const centralCleanup = onStreamFrame((raw) => {
242
258
  const frame = parseStreamFrame(raw);
@@ -258,8 +274,14 @@ export function createStreamRpcClient(contract, options) {
258
274
  return;
259
275
  }
260
276
  const handler = frameDispatcher.get(frame.streamId);
261
- if (!handler)
262
- return; // stale frame for completed/cancelled stream
277
+ if (!handler) {
278
+ const report = recentlyClosedStreams.get(frame.streamId);
279
+ if (report) {
280
+ recentlyClosedStreams.delete(frame.streamId);
281
+ report(raw);
282
+ }
283
+ return;
284
+ }
263
285
  switch (frame.type) {
264
286
  case "data":
265
287
  handler.data(frame.payload);
@@ -273,105 +295,152 @@ export function createStreamRpcClient(contract, options) {
273
295
  frameDispatcher.delete(frame.streamId);
274
296
  break;
275
297
  case "defect":
276
- handler.defect(frame.message);
298
+ handler.defect(frame.message, true);
277
299
  frameDispatcher.delete(frame.streamId);
278
300
  break;
279
301
  }
280
302
  });
281
303
  const streamMethods = contract.streamMethods ?? [];
282
- const client = Object.create(null);
283
- const clientRecord = client;
304
+ const callbackOptions = streamBuffer.bufferSize === "unbounded"
305
+ ? undefined
306
+ : { bufferSize: streamBuffer.bufferSize, strategy: streamBuffer.strategy };
307
+ function emptyStreamRpcClient() {
308
+ return Object.create(null);
309
+ }
310
+ const client = emptyStreamRpcClient();
284
311
  for (const method of streamMethods) {
285
312
  const decodeChunk = S.decodeUnknownSync(method.chunk);
286
313
  const encodeInput = S.encodeSync(method.req);
287
314
  const decodeTypedError = isNoErrorSchema(method.err) ? null : S.decodeUnknownSync(method.err);
288
315
  const caller = (...args) => {
289
316
  const payload = args.length === 0 ? {} : args[0];
290
- return Stream.asyncPush((emit) => Effect.gen(function* () {
291
- const streamId = crypto.randomUUID();
292
- // 1. Register in dispatch map BEFORE calling invoke
293
- frameDispatcher.set(streamId, {
294
- data: (rawPayload) => {
295
- let decoded;
296
- try {
297
- decoded = decodeChunk(rawPayload);
298
- }
299
- catch (cause) {
300
- const context = {
301
- scope: "stream-chunk",
302
- name: method.name,
303
- payload: rawPayload,
304
- cause,
305
- };
306
- safelyCall(diagnostics?.onDecodeFailure, context);
307
- emit.fail(rpcDefect("stream_chunk_decode_failed", `Stream ${method.name} chunk decode failed: ${formatUnknown(cause)}`, cause));
308
- return;
309
- }
310
- // `emit.single(...) === false` indicates a closed emitter, not
311
- // bounded-buffer overflow.
312
- const accepted = emit.single(decoded);
313
- if (!accepted) {
314
- // Stream has already finished. Remove dispatcher entry on the
315
- // first post-close frame to avoid repeated diagnostics.
316
- frameDispatcher.delete(streamId);
317
- safelyCall(diagnostics?.onProtocolError, {
318
- method: method.name,
319
- response: rawPayload,
320
- cause: new Error(`Stream ${method.name} received a post-close data frame; frame ignored`),
321
- });
322
- }
323
- },
324
- end: () => emit.end(),
325
- error: (err) => {
326
- if (!decodeTypedError) {
327
- emit.fail(rpcDefect("stream_error_decode_failed", `Stream ${method.name} received typed error but declares NoError`, err));
328
- return;
329
- }
330
- try {
331
- const decoded = decodeNonEmptyError(method.err, err.data);
332
- emit.fail(decoded);
333
- }
334
- catch (cause) {
335
- const errContext = {
336
- scope: "stream-error",
337
- name: method.name,
338
- payload: err,
339
- cause,
340
- };
341
- safelyCall(diagnostics?.onDecodeFailure, errContext);
342
- emit.fail(rpcDefect("stream_error_decode_failed", `Stream ${method.name} error decode failed: ${formatUnknown(cause)}`, cause));
317
+ return Stream.callback((queue) => {
318
+ let queueClosed = false;
319
+ let postCloseReported = false;
320
+ const reportPostClose = (response) => {
321
+ if (postCloseReported)
322
+ return;
323
+ postCloseReported = true;
324
+ safelyCall(diagnostics?.onProtocolError, {
325
+ method: method.name,
326
+ response,
327
+ cause: new Error(`Stream ${method.name} received a post-close data frame; frame ignored`),
328
+ });
329
+ };
330
+ const failQueue = (error) => {
331
+ if (queueClosed)
332
+ return;
333
+ queueClosed = true;
334
+ Queue.failCauseUnsafe(queue, Cause.fail(error));
335
+ };
336
+ const endQueue = () => {
337
+ if (queueClosed)
338
+ return;
339
+ queueClosed = true;
340
+ Queue.endUnsafe(queue);
341
+ };
342
+ return Effect.gen(function* () {
343
+ const streamId = crypto.randomUUID();
344
+ // Once main has sent a terminal frame (end/error/defect) there is
345
+ // nothing left to cancel; the finalizer skips the cancel invoke.
346
+ let serverTerminated = false;
347
+ // 1. Register in dispatch map BEFORE calling invoke
348
+ frameDispatcher.set(streamId, {
349
+ data: (rawPayload) => {
350
+ let decoded;
351
+ try {
352
+ decoded = decodeChunk(rawPayload);
353
+ }
354
+ catch (cause) {
355
+ const context = {
356
+ scope: "stream-chunk",
357
+ name: method.name,
358
+ payload: rawPayload,
359
+ cause: toDiagnosticCause(cause),
360
+ };
361
+ safelyCall(diagnostics?.onDecodeFailure, context);
362
+ failQueue(rpcDefect("stream_chunk_decode_failed", `Stream ${method.name} chunk decode failed: ${formatUnknown(cause)}`, toDiagnosticCause(cause)));
363
+ return;
364
+ }
365
+ const accepted = !queueClosed && Queue.offerUnsafe(queue, decoded);
366
+ if (!accepted && (queueClosed || queue.state._tag !== "Open")) {
367
+ // Stream has already finished. Remove dispatcher entry on the
368
+ // first post-close frame to avoid repeated diagnostics.
369
+ frameDispatcher.delete(streamId);
370
+ reportPostClose(rawPayload);
371
+ }
372
+ },
373
+ end: () => {
374
+ serverTerminated = true;
375
+ endQueue();
376
+ },
377
+ error: (err) => {
378
+ serverTerminated = true;
379
+ if (!decodeTypedError) {
380
+ failQueue(rpcDefect("stream_error_decode_failed", `Stream ${method.name} received typed error but declares NoError`, err));
381
+ return;
382
+ }
383
+ try {
384
+ const decoded = decodeNonEmptyError(method.err, err.data);
385
+ failQueue(decoded);
386
+ }
387
+ catch (cause) {
388
+ const errContext = {
389
+ scope: "stream-error",
390
+ name: method.name,
391
+ payload: err,
392
+ cause: toDiagnosticCause(cause),
393
+ };
394
+ safelyCall(diagnostics?.onDecodeFailure, errContext);
395
+ failQueue(rpcDefect("stream_error_decode_failed", `Stream ${method.name} error decode failed: ${formatUnknown(cause)}`, toDiagnosticCause(cause)));
396
+ }
397
+ },
398
+ defect: (message, fromServer = false) => {
399
+ if (fromServer) {
400
+ serverTerminated = true;
401
+ }
402
+ failQueue(rpcDefect("remote_defect", message, undefined));
403
+ },
404
+ });
405
+ // 2. Register cleanup finalizer
406
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
407
+ frameDispatcher.delete(streamId);
408
+ if (queueClosed && !postCloseReported) {
409
+ rememberRecentlyClosed(streamId, reportPostClose);
343
410
  }
344
- },
345
- defect: (message) => emit.fail(rpcDefect("remote_defect", message, undefined)),
346
- });
347
- // 2. Register cleanup finalizer
348
- yield* Effect.addFinalizer(() => Effect.sync(() => {
349
- frameDispatcher.delete(streamId);
350
- }).pipe(Effect.andThen(Effect.tryPromise(() => invoke(`stream-cancel`, { streamId })).pipe(Effect.ignore))));
351
- // 3. Encode input
352
- const encodedInput = yield* Effect.try({
353
- try: () => encodeInput(payload),
354
- catch: (cause) => rpcDefect("request_encoding_failed", `Stream ${method.name} request encoding failed: ${formatUnknown(cause)}`, cause),
355
- });
356
- // 4. Initiate the stream on main
357
- const response = yield* Effect.tryPromise({
358
- try: () => invoke(`stream/${method.name}`, {
359
- data: encodedInput,
360
- streamId,
361
- }),
362
- catch: (cause) => rpcDefect("stream_invoke_failed", `Stream ${method.name} invoke failed: ${formatUnknown(cause)}`, cause),
363
- });
364
- // 5. Validate handshake response
365
- const envelope = parseRpcResponseEnvelope(response);
366
- if (envelope?.type === "defect") {
367
- return yield* Effect.fail(rpcDefect("remote_defect", envelope.message, envelope.cause));
368
- }
369
- if (!isStreamStartedResponse(response)) {
370
- return yield* Effect.fail(rpcDefect("stream_handshake_invalid", `Stream ${method.name} unexpected handshake response`, response));
371
- }
372
- }), streamBuffer);
411
+ }).pipe(Effect.andThen(Effect.suspend(() => serverTerminated
412
+ ? Effect.void
413
+ : Effect.tryPromise(() => invoke(`stream-cancel`, { streamId })).pipe(Effect.ignore)))));
414
+ // 3. Encode input
415
+ const encodedInput = yield* Effect.try({
416
+ try: () => encodeInput(payload),
417
+ catch: (cause) => rpcDefect("request_encoding_failed", `Stream ${method.name} request encoding failed: ${formatUnknown(cause)}`, cause),
418
+ });
419
+ // 4. Initiate the stream on main
420
+ const response = yield* Effect.tryPromise({
421
+ try: () => invoke(`stream/${method.name}`, {
422
+ data: encodedInput,
423
+ streamId,
424
+ }),
425
+ catch: (cause) => rpcDefect("stream_invoke_failed", `Stream ${method.name} invoke failed: ${formatUnknown(cause)}`, cause),
426
+ });
427
+ // 5. Validate handshake response
428
+ const envelope = parseRpcResponseEnvelope(response);
429
+ if (envelope?.type === "defect") {
430
+ return yield* Effect.fail(rpcDefect("remote_defect", envelope.message, envelope.cause));
431
+ }
432
+ if (!isStreamStartedResponse(response)) {
433
+ return yield* Effect.fail(rpcDefect("stream_handshake_invalid", `Stream ${method.name} unexpected handshake response`, response));
434
+ }
435
+ }).pipe(Effect.catchCause((cause) => Effect.sync(() => {
436
+ if (queueClosed)
437
+ return;
438
+ queueClosed = true;
439
+ Queue.failCauseUnsafe(queue, cause);
440
+ })));
441
+ }, callbackOptions);
373
442
  };
374
- clientRecord[method.name] = caller;
443
+ Object.assign(client, { [method.name]: caller });
375
444
  }
376
445
  function dispose() {
377
446
  // Fail all active streams so consumers don't hang
@@ -379,6 +448,7 @@ export function createStreamRpcClient(contract, options) {
379
448
  handler.defect("Stream client disposed");
380
449
  frameDispatcher.delete(streamId);
381
450
  }
451
+ recentlyClosedStreams.clear();
382
452
  centralCleanup();
383
453
  }
384
454
  return {
package/dist/testing.d.ts CHANGED
@@ -1,15 +1,16 @@
1
+ import type { IpcEncodedValue } from "./boundary.ts";
1
2
  import type { RpcInvoke } from "./types.ts";
2
3
  export type Invocation = {
3
4
  readonly method: string;
4
- readonly payload: unknown;
5
+ readonly payload: IpcEncodedValue;
5
6
  };
6
7
  export type InvokeStub = RpcInvoke & {
7
8
  readonly invocations: Invocation[];
8
9
  };
9
10
  export declare const createInvokeStub: (impl: RpcInvoke) => InvokeStub;
10
- export declare const createDeferred: <T>() => {
11
+ export declare const createDeferred: <T, E = Error>() => {
11
12
  promise: Promise<T>;
12
13
  resolve: (value: T | PromiseLike<T>) => void;
13
- reject: (reason?: unknown) => void;
14
+ reject: (reason?: E) => void;
14
15
  };
15
16
  //# sourceMappingURL=testing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG;IAAE,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC;AAE5E,eAAO,MAAM,gBAAgB,GAAI,MAAM,SAAS,KAAG,UAYlD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,CAAC;;qBACT,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI;sBAC1B,OAAO,KAAK,IAAI;CAOvC,CAAC"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG;IAAE,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC;AAE5E,eAAO,MAAM,gBAAgB,GAAI,MAAM,SAAS,KAAG,UAYlD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,CAAC,GAAG,KAAK;;qBACpB,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI;sBAC1B,CAAC,KAAK,IAAI;CAOjC,CAAC"}
package/dist/types.d.ts CHANGED
@@ -1,20 +1,39 @@
1
+ import type * as Context from "effect/Context";
1
2
  import type * as Effect from "effect/Effect";
2
- import type * as Runtime from "effect/Runtime";
3
3
  import type * as Stream from "effect/Stream";
4
+ import type { DiagnosticCause, IpcEncodedValue } from "./boundary.ts";
4
5
  import type { AnyEvent, AnyMethod, AnyStreamMethod, ExtractMethod, ExtractStreamMethod, RpcContract, RpcError, RpcEventPayload, RpcInput, RpcOutput, StreamChunk, StreamError, StreamInput } from "./contract.ts";
5
6
  export type { AnyEvent, AnyMethod, AnyStreamMethod, ErrorSchema, ExtractMethod, ExtractStreamMethod, RpcContract, RpcError, RpcEvent, RpcEventPayload, RpcInput, RpcMethod, RpcOutput, SchemaNoContext, StreamChunk, StreamError, StreamInput, StreamRpcMethod, } from "./contract.ts";
7
+ /**
8
+ * Per-request context passed as the second argument to handlers.
9
+ * Handlers that don't need it can simply omit the parameter.
10
+ */
11
+ export type RpcHandlerContext = {
12
+ /** The webContents that sent the request, when the transport exposes it. */
13
+ readonly sender: WebContentsLike | null;
14
+ };
6
15
  export type Implementations<C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[], readonly AnyStreamMethod[]>, R = never> = {
7
- readonly [Name in C["methods"][number]["name"]]: (input: RpcInput<ExtractMethod<C["methods"], Name>>) => Effect.Effect<RpcOutput<ExtractMethod<C["methods"], Name>>, RpcError<ExtractMethod<C["methods"], Name>>, R>;
16
+ readonly [Name in C["methods"][number]["name"]]: (input: RpcInput<ExtractMethod<C["methods"], Name>>, context: RpcHandlerContext) => Effect.Effect<RpcOutput<ExtractMethod<C["methods"], Name>>, RpcError<ExtractMethod<C["methods"], Name>>, R>;
8
17
  };
9
18
  export type StreamImplementations<C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[], readonly AnyStreamMethod[]>, R = never> = {
10
- readonly [Name in C["streamMethods"][number]["name"]]: (input: StreamInput<ExtractStreamMethod<C["streamMethods"], Name>>) => Stream.Stream<StreamChunk<ExtractStreamMethod<C["streamMethods"], Name>>, StreamError<ExtractStreamMethod<C["streamMethods"], Name>>, R>;
19
+ readonly [Name in C["streamMethods"][number]["name"]]: (input: StreamInput<ExtractStreamMethod<C["streamMethods"], Name>>, context: RpcHandlerContext) => Stream.Stream<StreamChunk<ExtractStreamMethod<C["streamMethods"], Name>>, StreamError<ExtractStreamMethod<C["streamMethods"], Name>>, R>;
11
20
  };
12
21
  export type WebContentsLike = {
13
22
  readonly id: number;
14
23
  readonly isDestroyed: () => boolean;
15
- readonly send: (channel: string, payload: unknown) => void;
16
- };
17
- type IsEmptyObject<T> = T extends object ? (keyof T extends never ? true : false) : false;
24
+ readonly send: (channel: string, payload: IpcEncodedValue) => void;
25
+ /**
26
+ * Optional EventEmitter surface (present on real Electron WebContents).
27
+ * When available, active stream fibers are interrupted as soon as the
28
+ * renderer is destroyed; otherwise termination happens on the next chunk.
29
+ */
30
+ readonly once?: (event: "destroyed", listener: () => void) => void;
31
+ readonly removeListener?: (event: "destroyed", listener: () => void) => void;
32
+ };
33
+ export type IpcInvokeEvent = {
34
+ readonly sender?: WebContentsLike;
35
+ };
36
+ type IsEmptyObject<T> = T extends object ? keyof T extends never ? string extends T ? true : false : false : false;
18
37
  export type RpcDefectCode = "request_encoding_failed" | "invoke_failed" | "success_payload_decoding_failed" | "failure_payload_decoding_failed" | "noerror_contract_violation" | "invalid_response_envelope" | "legacy_decode_failed" | "remote_defect" | "stream_invoke_failed" | "stream_handshake_invalid" | "stream_chunk_decode_failed" | "stream_error_decode_failed";
19
38
  export declare class RpcDefectError extends Error {
20
39
  readonly code: RpcDefectCode;
@@ -37,32 +56,38 @@ export type ChannelPrefix = {
37
56
  readonly event: string;
38
57
  };
39
58
  export declare const defaultChannelPrefix: ChannelPrefix;
59
+ /**
60
+ * RPC and event prefixes must differ: with identical prefixes an event named
61
+ * like a method (or named "sf"/"stream-cancel") would collide with RPC and
62
+ * stream transport channels.
63
+ */
64
+ export declare function assertValidChannelPrefix(prefix: ChannelPrefix): ChannelPrefix;
40
65
  export type DecodeFailureScope = "rpc-request" | "rpc-response" | "event-payload" | "stream-request" | "stream-chunk" | "stream-error";
41
66
  export type DecodeFailureContext = {
42
67
  readonly scope: DecodeFailureScope;
43
68
  readonly name: string;
44
- readonly payload: unknown;
45
- readonly cause: unknown;
69
+ readonly payload: DiagnosticCause;
70
+ readonly cause: DiagnosticCause;
46
71
  };
47
72
  export type ProtocolErrorContext = {
48
73
  readonly method: string;
49
- readonly response: unknown;
50
- readonly cause: unknown;
74
+ readonly response: DiagnosticCause;
75
+ readonly cause: DiagnosticCause;
51
76
  };
52
77
  export type DispatchFailureContext = {
53
78
  readonly event: string;
54
- readonly payload: unknown;
55
- readonly cause: unknown;
79
+ readonly payload: DiagnosticCause;
80
+ readonly cause: DiagnosticCause;
56
81
  };
57
82
  export type DroppedEventReason = "queue_full" | "encoding_failed" | "window_unavailable" | "dispatch_failed";
58
83
  export type DroppedEventContext = {
59
84
  readonly event: string;
60
- readonly payload: unknown;
85
+ readonly payload: DiagnosticCause;
61
86
  readonly reason: DroppedEventReason;
62
87
  readonly queued: number;
63
88
  readonly dropped: number;
64
89
  };
65
- export type RpcInvoke = (method: string, payload: unknown) => Promise<unknown>;
90
+ export type RpcInvoke = (method: string, payload: IpcEncodedValue) => Promise<IpcEncodedValue>;
66
91
  export type RpcResponseDecodeMode = "envelope" | "dual";
67
92
  export type RpcClientDiagnostics = {
68
93
  readonly onDecodeFailure?: (context: DecodeFailureContext) => void;
@@ -77,9 +102,10 @@ export type RpcEndpointDiagnostics = {
77
102
  readonly onDecodeFailure?: (context: DecodeFailureContext) => void;
78
103
  readonly onProtocolError?: (context: ProtocolErrorContext) => void;
79
104
  };
105
+ export type IpcInvokeResult = IpcEncodedValue | Promise<IpcEncodedValue>;
80
106
  export type IpcMainLike = {
81
- readonly handle: (channel: string, listener: (event: unknown, payload: unknown) => unknown) => unknown;
82
- readonly removeHandler: (channel: string) => unknown;
107
+ readonly handle: (channel: string, listener: (event: IpcInvokeEvent, payload: IpcEncodedValue) => IpcInvokeResult) => void;
108
+ readonly removeHandler: (channel: string) => void;
83
109
  };
84
110
  export interface RpcEndpoint {
85
111
  readonly start: () => void;
@@ -89,7 +115,7 @@ export interface RpcEndpoint {
89
115
  }
90
116
  export type RpcEndpointOptions<C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[], readonly AnyStreamMethod[]> = RpcContract<readonly AnyMethod[], readonly AnyEvent[], readonly []>, R = never> = {
91
117
  readonly channelPrefix?: ChannelPrefix;
92
- readonly runtime: Runtime.Runtime<R>;
118
+ readonly context: Context.Context<R>;
93
119
  readonly diagnostics?: RpcEndpointDiagnostics;
94
120
  readonly streamHandlers?: StreamImplementations<C, R>;
95
121
  };
@@ -101,7 +127,7 @@ export type EventPublisherDiagnostics = {
101
127
  export type RendererWindowLike = {
102
128
  readonly isDestroyed: () => boolean;
103
129
  readonly webContents: {
104
- readonly send: (channel: string, payload: unknown) => void;
130
+ readonly send: (channel: string, payload: IpcEncodedValue) => void;
105
131
  };
106
132
  };
107
133
  export type EventPublisherOptions = {
@@ -111,18 +137,29 @@ export type EventPublisherOptions = {
111
137
  readonly diagnostics?: EventPublisherDiagnostics;
112
138
  };
113
139
  export interface RpcEventPublisher<C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[], readonly AnyStreamMethod[]>> {
140
+ /**
141
+ * Enqueue an event for delivery. Never fails: after `dispose()` the event
142
+ * is silently discarded, and delivery problems surface only through the
143
+ * diagnostics hooks and `stats()`.
144
+ */
114
145
  readonly publish: <E extends C["events"][number]>(event: E, payload: RpcEventPayload<E>) => Effect.Effect<void, never>;
115
146
  readonly start: () => void;
116
147
  readonly stop: () => void;
117
148
  readonly dispose: () => void;
118
149
  readonly isRunning: () => boolean;
119
- readonly stats: () => {
120
- readonly queued: number;
121
- readonly dropped: number;
122
- };
150
+ /**
151
+ * `dropped` counts failed deliveries, not whole events: an event that
152
+ * reaches two of three windows increments it once (per failed window),
153
+ * as do queue evictions and encoding failures.
154
+ */
155
+ readonly stats: () => EventPublisherStats;
123
156
  }
124
157
  export type EventDecodeMode = "safe" | "strict";
125
- export type EventSubscribe = (name: string, handler: (payload: unknown) => void) => () => void;
158
+ export type EventPublisherStats = {
159
+ readonly queued: number;
160
+ readonly dropped: number;
161
+ };
162
+ export type EventSubscribe = (name: string, handler: (payload: IpcEncodedValue) => void) => () => void;
126
163
  export type EventSubscriberDiagnostics = {
127
164
  readonly onDecodeFailure?: (context: DecodeFailureContext) => void;
128
165
  };
@@ -133,16 +170,22 @@ export type EventSubscriberOptions = {
133
170
  };
134
171
  export interface EventSubscriber<C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[], readonly AnyStreamMethod[]>> {
135
172
  readonly subscribe: <E extends C["events"][number]>(event: E, handler: (payload: RpcEventPayload<E>) => void) => () => void;
136
- readonly subscribeByName: (name: string, handler: (payload: unknown) => void) => () => void;
173
+ readonly subscribeByName: (name: string, handler: (payload: IpcEncodedValue) => void) => () => void;
174
+ /**
175
+ * Effect-native subscription: a Stream of decoded payloads that
176
+ * subscribes when run and unsubscribes when its scope closes.
177
+ * Payloads that fail to decode are skipped (reported via diagnostics).
178
+ */
179
+ readonly stream: <E extends C["events"][number]>(event: E) => Stream.Stream<RpcEventPayload<E>>;
137
180
  readonly dispose: () => void;
138
181
  }
139
- export type OnStreamFrame = (listener: (frame: unknown) => void) => () => void;
182
+ export type OnStreamFrame = (listener: (frame: IpcEncodedValue) => void) => () => void;
140
183
  /**
141
184
  * Stream chunk buffering policy in the renderer.
142
185
  * Prefer `bufferSize: "unbounded"` for lossless streams such as token deltas.
143
186
  *
144
- * Bounded buffers are lossy. With the current Effect `Stream.asyncPush`
145
- * internals, terminal signals can also be lost under sustained pressure.
187
+ * Bounded buffers are lossy for chunks. Effect v4's callback queue still
188
+ * preserves terminal completion and failure signals.
146
189
  */
147
190
  export type StreamBufferOptions = {
148
191
  readonly bufferSize: "unbounded";
@@ -160,5 +203,6 @@ export interface StreamRpcClientHandle<C extends RpcContract<readonly AnyMethod[
160
203
  readonly client: StreamRpcClient<C>;
161
204
  readonly dispose: () => void;
162
205
  }
206
+ export type { DiagnosticCause, IpcEncodedRecord, IpcEncodedValue } from "./boundary.ts";
163
207
  export type { IpcBridge, IpcBridgeGlobal, IpcKit, IpcKitOptions, IpcMainHandle } from "./kit.ts";
164
208
  //# sourceMappingURL=types.d.ts.map