electron-effect-rpc 0.9.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
1
  import * as S from "effect/Schema";
2
- import { Cause, Effect, Exit, Stream } from "effect";
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,15 +179,15 @@ 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
  };
186
- const streamEvent = (event) => Stream.asyncPush((emit) => Effect.acquireRelease(Effect.sync(() => subscribeEvent(event, (payload) => {
187
- emit.single(payload);
189
+ const streamEvent = (event) => Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => subscribeEvent(event, (payload) => {
190
+ Queue.offerUnsafe(queue, payload);
188
191
  })), (unsubscribe) => Effect.sync(unsubscribe)));
189
192
  const subscribeByName = (name, handler) => {
190
193
  const event = eventMap.get(name);
@@ -198,7 +201,7 @@ export function createEventSubscriber(contract, options) {
198
201
  decoded = decoder(payload);
199
202
  }
200
203
  catch (cause) {
201
- reportDecodeFailure(mode, name, payload, cause, options);
204
+ reportDecodeFailure(mode, name, payload, toDiagnosticCause(cause), options);
202
205
  return;
203
206
  }
204
207
  handler(decoded);
@@ -241,6 +244,15 @@ export function createStreamRpcClient(contract, options) {
241
244
  const diagnostics = options.diagnostics;
242
245
  const streamBuffer = options.streamBuffer ?? { bufferSize: "unbounded" };
243
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
+ };
244
256
  // Set up central frame listener
245
257
  const centralCleanup = onStreamFrame((raw) => {
246
258
  const frame = parseStreamFrame(raw);
@@ -262,8 +274,14 @@ export function createStreamRpcClient(contract, options) {
262
274
  return;
263
275
  }
264
276
  const handler = frameDispatcher.get(frame.streamId);
265
- if (!handler)
266
- 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
+ }
267
285
  switch (frame.type) {
268
286
  case "data":
269
287
  handler.data(frame.payload);
@@ -283,113 +301,146 @@ export function createStreamRpcClient(contract, options) {
283
301
  }
284
302
  });
285
303
  const streamMethods = contract.streamMethods ?? [];
286
- const client = Object.create(null);
287
- 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();
288
311
  for (const method of streamMethods) {
289
312
  const decodeChunk = S.decodeUnknownSync(method.chunk);
290
313
  const encodeInput = S.encodeSync(method.req);
291
314
  const decodeTypedError = isNoErrorSchema(method.err) ? null : S.decodeUnknownSync(method.err);
292
315
  const caller = (...args) => {
293
316
  const payload = args.length === 0 ? {} : args[0];
294
- return Stream.asyncPush((emit) => Effect.gen(function* () {
295
- const streamId = crypto.randomUUID();
296
- // Once main has sent a terminal frame (end/error/defect) there is
297
- // nothing left to cancel; the finalizer skips the cancel invoke.
298
- let serverTerminated = false;
299
- // 1. Register in dispatch map BEFORE calling invoke
300
- frameDispatcher.set(streamId, {
301
- data: (rawPayload) => {
302
- let decoded;
303
- try {
304
- decoded = decodeChunk(rawPayload);
305
- }
306
- catch (cause) {
307
- const context = {
308
- scope: "stream-chunk",
309
- name: method.name,
310
- payload: rawPayload,
311
- cause,
312
- };
313
- safelyCall(diagnostics?.onDecodeFailure, context);
314
- emit.fail(rpcDefect("stream_chunk_decode_failed", `Stream ${method.name} chunk decode failed: ${formatUnknown(cause)}`, cause));
315
- return;
316
- }
317
- // `emit.single(...) === false` indicates a closed emitter, not
318
- // bounded-buffer overflow.
319
- const accepted = emit.single(decoded);
320
- if (!accepted) {
321
- // Stream has already finished. Remove dispatcher entry on the
322
- // first post-close frame to avoid repeated diagnostics.
323
- frameDispatcher.delete(streamId);
324
- safelyCall(diagnostics?.onProtocolError, {
325
- method: method.name,
326
- response: rawPayload,
327
- cause: new Error(`Stream ${method.name} received a post-close data frame; frame ignored`),
328
- });
329
- }
330
- },
331
- end: () => {
332
- serverTerminated = true;
333
- emit.end();
334
- },
335
- error: (err) => {
336
- serverTerminated = true;
337
- if (!decodeTypedError) {
338
- emit.fail(rpcDefect("stream_error_decode_failed", `Stream ${method.name} received typed error but declares NoError`, err));
339
- return;
340
- }
341
- try {
342
- const decoded = decodeNonEmptyError(method.err, err.data);
343
- emit.fail(decoded);
344
- }
345
- catch (cause) {
346
- const errContext = {
347
- scope: "stream-error",
348
- name: method.name,
349
- payload: err,
350
- cause,
351
- };
352
- safelyCall(diagnostics?.onDecodeFailure, errContext);
353
- emit.fail(rpcDefect("stream_error_decode_failed", `Stream ${method.name} error decode failed: ${formatUnknown(cause)}`, cause));
354
- }
355
- },
356
- defect: (message, fromServer = false) => {
357
- if (fromServer) {
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) => {
358
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);
359
410
  }
360
- emit.fail(rpcDefect("remote_defect", message, undefined));
361
- },
362
- });
363
- // 2. Register cleanup finalizer
364
- yield* Effect.addFinalizer(() => Effect.sync(() => {
365
- frameDispatcher.delete(streamId);
366
- }).pipe(Effect.andThen(Effect.suspend(() => serverTerminated
367
- ? Effect.void
368
- : Effect.tryPromise(() => invoke(`stream-cancel`, { streamId })).pipe(Effect.ignore)))));
369
- // 3. Encode input
370
- const encodedInput = yield* Effect.try({
371
- try: () => encodeInput(payload),
372
- catch: (cause) => rpcDefect("request_encoding_failed", `Stream ${method.name} request encoding failed: ${formatUnknown(cause)}`, cause),
373
- });
374
- // 4. Initiate the stream on main
375
- const response = yield* Effect.tryPromise({
376
- try: () => invoke(`stream/${method.name}`, {
377
- data: encodedInput,
378
- streamId,
379
- }),
380
- catch: (cause) => rpcDefect("stream_invoke_failed", `Stream ${method.name} invoke failed: ${formatUnknown(cause)}`, cause),
381
- });
382
- // 5. Validate handshake response
383
- const envelope = parseRpcResponseEnvelope(response);
384
- if (envelope?.type === "defect") {
385
- return yield* Effect.fail(rpcDefect("remote_defect", envelope.message, envelope.cause));
386
- }
387
- if (!isStreamStartedResponse(response)) {
388
- return yield* Effect.fail(rpcDefect("stream_handshake_invalid", `Stream ${method.name} unexpected handshake response`, response));
389
- }
390
- }), 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);
391
442
  };
392
- clientRecord[method.name] = caller;
443
+ Object.assign(client, { [method.name]: caller });
393
444
  }
394
445
  function dispose() {
395
446
  // Fail all active streams so consumers don't hang
@@ -397,6 +448,7 @@ export function createStreamRpcClient(contract, options) {
397
448
  handler.defect("Stream client disposed");
398
449
  frameDispatcher.delete(streamId);
399
450
  }
451
+ recentlyClosedStreams.clear();
400
452
  centralCleanup();
401
453
  }
402
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,6 +1,7 @@
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";
6
7
  /**
@@ -20,14 +21,17 @@ export type StreamImplementations<C extends RpcContract<readonly AnyMethod[], re
20
21
  export type WebContentsLike = {
21
22
  readonly id: number;
22
23
  readonly isDestroyed: () => boolean;
23
- readonly send: (channel: string, payload: unknown) => void;
24
+ readonly send: (channel: string, payload: IpcEncodedValue) => void;
24
25
  /**
25
26
  * Optional EventEmitter surface (present on real Electron WebContents).
26
27
  * When available, active stream fibers are interrupted as soon as the
27
28
  * renderer is destroyed; otherwise termination happens on the next chunk.
28
29
  */
29
- readonly once?: (event: "destroyed", listener: () => void) => unknown;
30
- readonly removeListener?: (event: "destroyed", listener: () => void) => unknown;
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;
31
35
  };
32
36
  type IsEmptyObject<T> = T extends object ? keyof T extends never ? string extends T ? true : false : false : false;
33
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";
@@ -62,28 +66,28 @@ export type DecodeFailureScope = "rpc-request" | "rpc-response" | "event-payload
62
66
  export type DecodeFailureContext = {
63
67
  readonly scope: DecodeFailureScope;
64
68
  readonly name: string;
65
- readonly payload: unknown;
66
- readonly cause: unknown;
69
+ readonly payload: DiagnosticCause;
70
+ readonly cause: DiagnosticCause;
67
71
  };
68
72
  export type ProtocolErrorContext = {
69
73
  readonly method: string;
70
- readonly response: unknown;
71
- readonly cause: unknown;
74
+ readonly response: DiagnosticCause;
75
+ readonly cause: DiagnosticCause;
72
76
  };
73
77
  export type DispatchFailureContext = {
74
78
  readonly event: string;
75
- readonly payload: unknown;
76
- readonly cause: unknown;
79
+ readonly payload: DiagnosticCause;
80
+ readonly cause: DiagnosticCause;
77
81
  };
78
82
  export type DroppedEventReason = "queue_full" | "encoding_failed" | "window_unavailable" | "dispatch_failed";
79
83
  export type DroppedEventContext = {
80
84
  readonly event: string;
81
- readonly payload: unknown;
85
+ readonly payload: DiagnosticCause;
82
86
  readonly reason: DroppedEventReason;
83
87
  readonly queued: number;
84
88
  readonly dropped: number;
85
89
  };
86
- export type RpcInvoke = (method: string, payload: unknown) => Promise<unknown>;
90
+ export type RpcInvoke = (method: string, payload: IpcEncodedValue) => Promise<IpcEncodedValue>;
87
91
  export type RpcResponseDecodeMode = "envelope" | "dual";
88
92
  export type RpcClientDiagnostics = {
89
93
  readonly onDecodeFailure?: (context: DecodeFailureContext) => void;
@@ -98,9 +102,10 @@ export type RpcEndpointDiagnostics = {
98
102
  readonly onDecodeFailure?: (context: DecodeFailureContext) => void;
99
103
  readonly onProtocolError?: (context: ProtocolErrorContext) => void;
100
104
  };
105
+ export type IpcInvokeResult = IpcEncodedValue | Promise<IpcEncodedValue>;
101
106
  export type IpcMainLike = {
102
- readonly handle: (channel: string, listener: (event: unknown, payload: unknown) => unknown) => unknown;
103
- readonly removeHandler: (channel: string) => unknown;
107
+ readonly handle: (channel: string, listener: (event: IpcInvokeEvent, payload: IpcEncodedValue) => IpcInvokeResult) => void;
108
+ readonly removeHandler: (channel: string) => void;
104
109
  };
105
110
  export interface RpcEndpoint {
106
111
  readonly start: () => void;
@@ -110,7 +115,7 @@ export interface RpcEndpoint {
110
115
  }
111
116
  export type RpcEndpointOptions<C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[], readonly AnyStreamMethod[]> = RpcContract<readonly AnyMethod[], readonly AnyEvent[], readonly []>, R = never> = {
112
117
  readonly channelPrefix?: ChannelPrefix;
113
- readonly runtime: Runtime.Runtime<R>;
118
+ readonly context: Context.Context<R>;
114
119
  readonly diagnostics?: RpcEndpointDiagnostics;
115
120
  readonly streamHandlers?: StreamImplementations<C, R>;
116
121
  };
@@ -122,7 +127,7 @@ export type EventPublisherDiagnostics = {
122
127
  export type RendererWindowLike = {
123
128
  readonly isDestroyed: () => boolean;
124
129
  readonly webContents: {
125
- readonly send: (channel: string, payload: unknown) => void;
130
+ readonly send: (channel: string, payload: IpcEncodedValue) => void;
126
131
  };
127
132
  };
128
133
  export type EventPublisherOptions = {
@@ -147,13 +152,14 @@ export interface RpcEventPublisher<C extends RpcContract<readonly AnyMethod[], r
147
152
  * reaches two of three windows increments it once (per failed window),
148
153
  * as do queue evictions and encoding failures.
149
154
  */
150
- readonly stats: () => {
151
- readonly queued: number;
152
- readonly dropped: number;
153
- };
155
+ readonly stats: () => EventPublisherStats;
154
156
  }
155
157
  export type EventDecodeMode = "safe" | "strict";
156
- 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;
157
163
  export type EventSubscriberDiagnostics = {
158
164
  readonly onDecodeFailure?: (context: DecodeFailureContext) => void;
159
165
  };
@@ -164,7 +170,7 @@ export type EventSubscriberOptions = {
164
170
  };
165
171
  export interface EventSubscriber<C extends RpcContract<readonly AnyMethod[], readonly AnyEvent[], readonly AnyStreamMethod[]>> {
166
172
  readonly subscribe: <E extends C["events"][number]>(event: E, handler: (payload: RpcEventPayload<E>) => void) => () => void;
167
- readonly subscribeByName: (name: string, handler: (payload: unknown) => void) => () => void;
173
+ readonly subscribeByName: (name: string, handler: (payload: IpcEncodedValue) => void) => () => void;
168
174
  /**
169
175
  * Effect-native subscription: a Stream of decoded payloads that
170
176
  * subscribes when run and unsubscribes when its scope closes.
@@ -173,13 +179,13 @@ export interface EventSubscriber<C extends RpcContract<readonly AnyMethod[], rea
173
179
  readonly stream: <E extends C["events"][number]>(event: E) => Stream.Stream<RpcEventPayload<E>>;
174
180
  readonly dispose: () => void;
175
181
  }
176
- export type OnStreamFrame = (listener: (frame: unknown) => void) => () => void;
182
+ export type OnStreamFrame = (listener: (frame: IpcEncodedValue) => void) => () => void;
177
183
  /**
178
184
  * Stream chunk buffering policy in the renderer.
179
185
  * Prefer `bufferSize: "unbounded"` for lossless streams such as token deltas.
180
186
  *
181
- * Bounded buffers are lossy. With the current Effect `Stream.asyncPush`
182
- * 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.
183
189
  */
184
190
  export type StreamBufferOptions = {
185
191
  readonly bufferSize: "unbounded";
@@ -197,5 +203,6 @@ export interface StreamRpcClientHandle<C extends RpcContract<readonly AnyMethod[
197
203
  readonly client: StreamRpcClient<C>;
198
204
  readonly dispose: () => void;
199
205
  }
206
+ export type { DiagnosticCause, IpcEncodedRecord, IpcEncodedValue } from "./boundary.ts";
200
207
  export type { IpcBridge, IpcBridgeGlobal, IpcKit, IpcKitOptions, IpcMainHandle } from "./kit.ts";
201
208
  //# sourceMappingURL=types.d.ts.map