electron-effect-rpc 0.9.0 → 0.11.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/CHANGELOG.md +33 -11
- package/README.md +88 -38
- package/dist/boundary.d.ts +17 -0
- package/dist/boundary.d.ts.map +1 -0
- package/dist/boundary.js +89 -0
- package/dist/contract.d.ts +3 -3
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +4 -7
- package/dist/kit.d.ts +6 -8
- package/dist/kit.d.ts.map +1 -1
- package/dist/kit.js +9 -4
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +64 -59
- package/dist/preload-bridge.d.ts +36 -8
- package/dist/preload-bridge.d.ts.map +1 -1
- package/dist/preload-bridge.js +22 -20
- package/dist/protocol.d.ts +12 -12
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +35 -53
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +185 -121
- package/dist/testing.d.ts +4 -3
- package/dist/testing.d.ts.map +1 -1
- package/dist/types.d.ts +32 -25
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -5
package/dist/renderer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,WAAW,EAMjB,MAAM,eAAe,CAAC;AASvB,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,SAAS,EAGd,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAE3B,KAAK,SAAS,EACd,KAAK,gBAAgB,EAMrB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,YAAY,CAAC;AA4EpB,wBAAgB,eAAe,CAC7B,KAAK,CAAC,OAAO,SAAS,aAAa,CAAC,SAAS,CAAC,EAC9C,KAAK,CAAC,MAAM,SAAS,aAAa,CAAC,QAAQ,CAAC,EAC5C,KAAK,CAAC,aAAa,SAAS,aAAa,CAAC,eAAe,CAAC,GAAG,SAAS,EAAE,EAExE,QAAQ,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EACrD,OAAO,EAAE,gBAAgB,GACxB,SAAS,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC,CAyJxD;AAqBD,wBAAgB,qBAAqB,CACnC,KAAK,CAAC,OAAO,SAAS,aAAa,CAAC,SAAS,CAAC,EAC9C,KAAK,CAAC,MAAM,SAAS,aAAa,CAAC,QAAQ,CAAC,EAC5C,KAAK,CAAC,aAAa,SAAS,aAAa,CAAC,eAAe,CAAC,GAAG,SAAS,EAAE,EAExE,QAAQ,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EACrD,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC,CA2G9D;AAUD,wBAAgB,qBAAqB,CACnC,KAAK,CAAC,OAAO,SAAS,aAAa,CAAC,SAAS,CAAC,EAC9C,KAAK,CAAC,MAAM,SAAS,aAAa,CAAC,QAAQ,CAAC,EAC5C,KAAK,CAAC,aAAa,SAAS,aAAa,CAAC,eAAe,CAAC,EAE1D,QAAQ,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EACrD,OAAO,EAAE,sBAAsB,GAC9B,qBAAqB,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC,CAkUpE;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
|
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,
|
|
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.
|
|
39
|
+
const failureOption = Cause.findErrorOption(exit.cause);
|
|
36
40
|
if (failureOption._tag === "Some") {
|
|
37
41
|
return Effect.fail(failureOption.value);
|
|
38
42
|
}
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
const defect =
|
|
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 =
|
|
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
|
-
|
|
143
|
+
Object.assign(client, { [method.name]: caller });
|
|
141
144
|
}
|
|
142
145
|
return client;
|
|
143
146
|
}
|
|
@@ -176,16 +179,21 @@ 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.
|
|
187
|
-
|
|
188
|
-
})), (unsubscribe) => Effect.sync(unsubscribe))
|
|
189
|
+
const streamEvent = (event) => Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => subscribeEvent(event, (payload) => {
|
|
190
|
+
Queue.offerUnsafe(queue, payload);
|
|
191
|
+
})), (unsubscribe) => Effect.sync(unsubscribe)).pipe(
|
|
192
|
+
// Callback acquisition runs in a producer fiber; relay its failure to
|
|
193
|
+
// the queue so the consumer cannot remain blocked waiting for events.
|
|
194
|
+
Effect.catchCause((cause) => Effect.sync(() => {
|
|
195
|
+
Queue.failCauseUnsafe(queue, cause);
|
|
196
|
+
}))));
|
|
189
197
|
const subscribeByName = (name, handler) => {
|
|
190
198
|
const event = eventMap.get(name);
|
|
191
199
|
if (!event) {
|
|
@@ -198,7 +206,7 @@ export function createEventSubscriber(contract, options) {
|
|
|
198
206
|
decoded = decoder(payload);
|
|
199
207
|
}
|
|
200
208
|
catch (cause) {
|
|
201
|
-
reportDecodeFailure(mode, name, payload, cause, options);
|
|
209
|
+
reportDecodeFailure(mode, name, payload, toDiagnosticCause(cause), options);
|
|
202
210
|
return;
|
|
203
211
|
}
|
|
204
212
|
handler(decoded);
|
|
@@ -241,6 +249,15 @@ export function createStreamRpcClient(contract, options) {
|
|
|
241
249
|
const diagnostics = options.diagnostics;
|
|
242
250
|
const streamBuffer = options.streamBuffer ?? { bufferSize: "unbounded" };
|
|
243
251
|
const frameDispatcher = new Map();
|
|
252
|
+
const recentlyClosedStreams = new Map();
|
|
253
|
+
const rememberRecentlyClosed = (streamId, report) => {
|
|
254
|
+
recentlyClosedStreams.set(streamId, report);
|
|
255
|
+
queueMicrotask(() => {
|
|
256
|
+
if (recentlyClosedStreams.get(streamId) === report) {
|
|
257
|
+
recentlyClosedStreams.delete(streamId);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
};
|
|
244
261
|
// Set up central frame listener
|
|
245
262
|
const centralCleanup = onStreamFrame((raw) => {
|
|
246
263
|
const frame = parseStreamFrame(raw);
|
|
@@ -262,8 +279,14 @@ export function createStreamRpcClient(contract, options) {
|
|
|
262
279
|
return;
|
|
263
280
|
}
|
|
264
281
|
const handler = frameDispatcher.get(frame.streamId);
|
|
265
|
-
if (!handler)
|
|
266
|
-
|
|
282
|
+
if (!handler) {
|
|
283
|
+
const report = recentlyClosedStreams.get(frame.streamId);
|
|
284
|
+
if (report) {
|
|
285
|
+
recentlyClosedStreams.delete(frame.streamId);
|
|
286
|
+
report(raw);
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
267
290
|
switch (frame.type) {
|
|
268
291
|
case "data":
|
|
269
292
|
handler.data(frame.payload);
|
|
@@ -283,113 +306,153 @@ export function createStreamRpcClient(contract, options) {
|
|
|
283
306
|
}
|
|
284
307
|
});
|
|
285
308
|
const streamMethods = contract.streamMethods ?? [];
|
|
286
|
-
const
|
|
287
|
-
|
|
309
|
+
const callbackOptions = streamBuffer.bufferSize === "unbounded"
|
|
310
|
+
? undefined
|
|
311
|
+
: { bufferSize: streamBuffer.bufferSize, strategy: streamBuffer.strategy };
|
|
312
|
+
function emptyStreamRpcClient() {
|
|
313
|
+
return Object.create(null);
|
|
314
|
+
}
|
|
315
|
+
const client = emptyStreamRpcClient();
|
|
288
316
|
for (const method of streamMethods) {
|
|
289
317
|
const decodeChunk = S.decodeUnknownSync(method.chunk);
|
|
290
318
|
const encodeInput = S.encodeSync(method.req);
|
|
291
319
|
const decodeTypedError = isNoErrorSchema(method.err) ? null : S.decodeUnknownSync(method.err);
|
|
292
320
|
const caller = (...args) => {
|
|
293
321
|
const payload = args.length === 0 ? {} : args[0];
|
|
294
|
-
return Stream.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
},
|
|
356
|
-
defect: (message, fromServer = false) => {
|
|
357
|
-
if (fromServer) {
|
|
322
|
+
return Stream.callback((queue) => {
|
|
323
|
+
let queueClosed = false;
|
|
324
|
+
let postCloseReported = false;
|
|
325
|
+
const reportPostClose = (response) => {
|
|
326
|
+
if (postCloseReported)
|
|
327
|
+
return;
|
|
328
|
+
postCloseReported = true;
|
|
329
|
+
safelyCall(diagnostics?.onProtocolError, {
|
|
330
|
+
method: method.name,
|
|
331
|
+
response,
|
|
332
|
+
cause: new Error(`Stream ${method.name} received a post-close data frame; frame ignored`),
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
const failQueue = (error) => {
|
|
336
|
+
if (queueClosed)
|
|
337
|
+
return;
|
|
338
|
+
queueClosed = true;
|
|
339
|
+
Queue.failCauseUnsafe(queue, Cause.fail(error));
|
|
340
|
+
};
|
|
341
|
+
const endQueue = () => {
|
|
342
|
+
if (queueClosed)
|
|
343
|
+
return;
|
|
344
|
+
queueClosed = true;
|
|
345
|
+
Queue.endUnsafe(queue);
|
|
346
|
+
};
|
|
347
|
+
return Effect.gen(function* () {
|
|
348
|
+
const streamId = crypto.randomUUID();
|
|
349
|
+
// Once main has sent a terminal frame (end/error/defect) there is
|
|
350
|
+
// nothing left to cancel; the finalizer skips the cancel invoke.
|
|
351
|
+
let serverTerminated = false;
|
|
352
|
+
// 1. Register in dispatch map BEFORE calling invoke
|
|
353
|
+
frameDispatcher.set(streamId, {
|
|
354
|
+
data: (rawPayload) => {
|
|
355
|
+
let decoded;
|
|
356
|
+
try {
|
|
357
|
+
decoded = decodeChunk(rawPayload);
|
|
358
|
+
}
|
|
359
|
+
catch (cause) {
|
|
360
|
+
const context = {
|
|
361
|
+
scope: "stream-chunk",
|
|
362
|
+
name: method.name,
|
|
363
|
+
payload: rawPayload,
|
|
364
|
+
cause: toDiagnosticCause(cause),
|
|
365
|
+
};
|
|
366
|
+
safelyCall(diagnostics?.onDecodeFailure, context);
|
|
367
|
+
failQueue(rpcDefect("stream_chunk_decode_failed", `Stream ${method.name} chunk decode failed: ${formatUnknown(cause)}`, toDiagnosticCause(cause)));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const accepted = !queueClosed && Queue.offerUnsafe(queue, decoded);
|
|
371
|
+
if (!accepted && (queueClosed || queue.state._tag !== "Open")) {
|
|
372
|
+
// Stream has already finished. Remove dispatcher entry on the
|
|
373
|
+
// first post-close frame to avoid repeated diagnostics.
|
|
374
|
+
frameDispatcher.delete(streamId);
|
|
375
|
+
reportPostClose(rawPayload);
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
end: () => {
|
|
379
|
+
serverTerminated = true;
|
|
380
|
+
endQueue();
|
|
381
|
+
},
|
|
382
|
+
error: (err) => {
|
|
358
383
|
serverTerminated = true;
|
|
384
|
+
if (!decodeTypedError) {
|
|
385
|
+
failQueue(rpcDefect("stream_error_decode_failed", `Stream ${method.name} received typed error but declares NoError`, err));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const decoded = decodeNonEmptyError(method.err, err.data);
|
|
390
|
+
failQueue(decoded);
|
|
391
|
+
}
|
|
392
|
+
catch (cause) {
|
|
393
|
+
const errContext = {
|
|
394
|
+
scope: "stream-error",
|
|
395
|
+
name: method.name,
|
|
396
|
+
payload: err,
|
|
397
|
+
cause: toDiagnosticCause(cause),
|
|
398
|
+
};
|
|
399
|
+
safelyCall(diagnostics?.onDecodeFailure, errContext);
|
|
400
|
+
failQueue(rpcDefect("stream_error_decode_failed", `Stream ${method.name} error decode failed: ${formatUnknown(cause)}`, toDiagnosticCause(cause)));
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
defect: (message, fromServer = false) => {
|
|
404
|
+
if (fromServer) {
|
|
405
|
+
serverTerminated = true;
|
|
406
|
+
}
|
|
407
|
+
failQueue(rpcDefect("remote_defect", message, undefined));
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
// 2. Register cleanup finalizer
|
|
411
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
412
|
+
frameDispatcher.delete(streamId);
|
|
413
|
+
if (queueClosed && !postCloseReported) {
|
|
414
|
+
rememberRecentlyClosed(streamId, reportPostClose);
|
|
359
415
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
416
|
+
}).pipe(Effect.andThen(Effect.suspend(() => serverTerminated
|
|
417
|
+
? Effect.void
|
|
418
|
+
: Effect.tryPromise({
|
|
419
|
+
try: () => invoke(`stream-cancel`, { streamId }),
|
|
420
|
+
catch: toDiagnosticCause,
|
|
421
|
+
}).pipe(Effect.tapError((cause) => Effect.sync(() => safelyCall(diagnostics?.onProtocolError, {
|
|
422
|
+
method: `stream-cancel/${method.name}`,
|
|
423
|
+
response: { streamId },
|
|
424
|
+
cause,
|
|
425
|
+
}))), Effect.ignore)))));
|
|
426
|
+
// 3. Encode input
|
|
427
|
+
const encodedInput = yield* Effect.try({
|
|
428
|
+
try: () => encodeInput(payload),
|
|
429
|
+
catch: (cause) => rpcDefect("request_encoding_failed", `Stream ${method.name} request encoding failed: ${formatUnknown(cause)}`, cause),
|
|
430
|
+
});
|
|
431
|
+
// 4. Initiate the stream on main
|
|
432
|
+
const response = yield* Effect.tryPromise({
|
|
433
|
+
try: () => invoke(`stream/${method.name}`, {
|
|
434
|
+
data: encodedInput,
|
|
435
|
+
streamId,
|
|
436
|
+
}),
|
|
437
|
+
catch: (cause) => rpcDefect("stream_invoke_failed", `Stream ${method.name} invoke failed: ${formatUnknown(cause)}`, cause),
|
|
438
|
+
});
|
|
439
|
+
// 5. Validate handshake response
|
|
440
|
+
const envelope = parseRpcResponseEnvelope(response);
|
|
441
|
+
if (envelope?.type === "defect") {
|
|
442
|
+
return yield* Effect.fail(rpcDefect("remote_defect", envelope.message, envelope.cause));
|
|
443
|
+
}
|
|
444
|
+
if (!isStreamStartedResponse(response)) {
|
|
445
|
+
return yield* Effect.fail(rpcDefect("stream_handshake_invalid", `Stream ${method.name} unexpected handshake response`, response));
|
|
446
|
+
}
|
|
447
|
+
}).pipe(Effect.catchCause((cause) => Effect.sync(() => {
|
|
448
|
+
if (queueClosed)
|
|
449
|
+
return;
|
|
450
|
+
queueClosed = true;
|
|
451
|
+
Queue.failCauseUnsafe(queue, cause);
|
|
452
|
+
})));
|
|
453
|
+
}, callbackOptions);
|
|
391
454
|
};
|
|
392
|
-
|
|
455
|
+
Object.assign(client, { [method.name]: caller });
|
|
393
456
|
}
|
|
394
457
|
function dispose() {
|
|
395
458
|
// Fail all active streams so consumers don't hang
|
|
@@ -397,6 +460,7 @@ export function createStreamRpcClient(contract, options) {
|
|
|
397
460
|
handler.defect("Stream client disposed");
|
|
398
461
|
frameDispatcher.delete(streamId);
|
|
399
462
|
}
|
|
463
|
+
recentlyClosedStreams.clear();
|
|
400
464
|
centralCleanup();
|
|
401
465
|
}
|
|
402
466
|
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:
|
|
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?:
|
|
14
|
+
reject: (reason?: E) => void;
|
|
14
15
|
};
|
|
15
16
|
//# sourceMappingURL=testing.d.ts.map
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -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,
|
|
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:
|
|
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) =>
|
|
30
|
-
readonly removeListener?: (event: "destroyed", listener: () => void) =>
|
|
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:
|
|
66
|
-
readonly cause:
|
|
69
|
+
readonly payload: DiagnosticCause;
|
|
70
|
+
readonly cause: DiagnosticCause;
|
|
67
71
|
};
|
|
68
72
|
export type ProtocolErrorContext = {
|
|
69
73
|
readonly method: string;
|
|
70
|
-
readonly response:
|
|
71
|
-
readonly cause:
|
|
74
|
+
readonly response: DiagnosticCause;
|
|
75
|
+
readonly cause: DiagnosticCause;
|
|
72
76
|
};
|
|
73
77
|
export type DispatchFailureContext = {
|
|
74
78
|
readonly event: string;
|
|
75
|
-
readonly payload:
|
|
76
|
-
readonly cause:
|
|
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:
|
|
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:
|
|
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:
|
|
103
|
-
readonly removeHandler: (channel: string) =>
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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.
|
|
182
|
-
*
|
|
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
|