electron-effect-rpc 0.4.0 → 0.6.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/main.js CHANGED
@@ -1,15 +1,30 @@
1
1
  import * as S from "@effect/schema/Schema";
2
- import { Cause, Effect, Exit } from "effect";
2
+ import { Cause, Effect, Exit, FiberId, Stream } from "effect";
3
3
  import * as Runtime from "effect/Runtime";
4
4
  import { isNoErrorSchema } from "./contract.js";
5
- import { extractErrorTag, safelyCall, toDefectEnvelope, } from "./protocol.js";
5
+ import { extractErrorTag, formatUnknown, isRecord, safelyCall, toDefectEnvelope, } from "./protocol.js";
6
6
  import { defaultChannelPrefix, } from "./types.js";
7
7
  function resolveChannelPrefix(prefix) {
8
8
  return prefix ?? defaultChannelPrefix;
9
9
  }
10
+ function isWebContentsLike(value) {
11
+ if (!isRecord(value))
12
+ return false;
13
+ return (typeof value.id === "number" &&
14
+ typeof value.isDestroyed === "function" &&
15
+ typeof value.send === "function");
16
+ }
17
+ function extractSender(event) {
18
+ if (!isRecord(event))
19
+ return null;
20
+ return isWebContentsLike(event.sender) ? event.sender : null;
21
+ }
10
22
  function isImplementation(value) {
11
23
  return typeof value === "function";
12
24
  }
25
+ function isStreamImplementation(value) {
26
+ return typeof value === "function";
27
+ }
13
28
  export function createRpcEndpoint(contract, ipc, implementations, options) {
14
29
  const channelPrefix = resolveChannelPrefix(options.channelPrefix);
15
30
  const diagnostics = options.diagnostics;
@@ -36,9 +51,7 @@ export function createRpcEndpoint(contract, ipc, implementations, options) {
36
51
  }
37
52
  const decodeInput = S.decodeUnknownSync(method.req);
38
53
  const encodeSuccess = S.encodeSync(method.res);
39
- const encodeFailure = isNoErrorSchema(method.err)
40
- ? null
41
- : S.encodeSync(method.err);
54
+ const encodeFailure = isNoErrorSchema(method.err) ? null : S.encodeSync(method.err);
42
55
  const channel = `${channelPrefix.rpc}${method.name}`;
43
56
  listeners.set(channel, async function handleRpcRequest(_event, rawPayload) {
44
57
  let input;
@@ -100,6 +113,133 @@ export function createRpcEndpoint(contract, ipc, implementations, options) {
100
113
  return toDefectEnvelope(exit.cause, `RPC ${method.name} interrupted`);
101
114
  });
102
115
  }
116
+ const activeStreams = new Map();
117
+ const streamListeners = new Map();
118
+ const streamMethods = contract.streamMethods ?? [];
119
+ const streamHandlerImpls = options.streamHandlers;
120
+ const cancelChannel = `${channelPrefix.rpc}stream-cancel`;
121
+ if (streamMethods.length > 0 && !streamHandlerImpls) {
122
+ throw new Error("Contract defines stream methods but no streamHandlers were provided.");
123
+ }
124
+ if (streamMethods.length > 0 && streamHandlerImpls) {
125
+ const streamMethodNames = new Set(streamMethods.map((m) => m.name));
126
+ for (const name in streamHandlerImpls) {
127
+ if (!streamMethodNames.has(name)) {
128
+ throw new Error(`Stream implementation provided for unknown stream method: ${name}`);
129
+ }
130
+ }
131
+ const runFork = Runtime.runFork(options.runtime);
132
+ for (const method of streamMethods) {
133
+ const impl = streamHandlerImpls[method.name];
134
+ if (!isStreamImplementation(impl)) {
135
+ throw new Error(`Missing implementation for stream method: ${method.name}`);
136
+ }
137
+ const decodeInput = S.decodeUnknownSync(method.req);
138
+ const encodeChunk = S.encodeSync(method.chunk);
139
+ const encodeFailure = isNoErrorSchema(method.err) ? null : S.encodeSync(method.err);
140
+ const channel = `${channelPrefix.rpc}stream/${method.name}`;
141
+ streamListeners.set(channel, async function handleStreamRequest(event, rawPayload) {
142
+ if (!isRecord(rawPayload)) {
143
+ return toDefectEnvelope("Stream request payload must be an object");
144
+ }
145
+ const streamId = rawPayload.streamId;
146
+ const rawData = rawPayload.data;
147
+ if (typeof streamId !== "string" || streamId.length === 0) {
148
+ return toDefectEnvelope("Invalid streamId");
149
+ }
150
+ if (activeStreams.has(streamId)) {
151
+ return toDefectEnvelope("Duplicate streamId");
152
+ }
153
+ const sender = extractSender(event);
154
+ if (!sender) {
155
+ return toDefectEnvelope("Stream handler requires sender with webContents");
156
+ }
157
+ let input;
158
+ try {
159
+ input = decodeInput(rawData);
160
+ }
161
+ catch (cause) {
162
+ const decodeCtx = {
163
+ scope: "stream-request",
164
+ name: method.name,
165
+ payload: rawData,
166
+ cause,
167
+ };
168
+ safelyCall(diagnostics?.onDecodeFailure, decodeCtx);
169
+ return toDefectEnvelope(cause, `Stream ${method.name} request decode failed`);
170
+ }
171
+ const sfChannel = `${channelPrefix.rpc}sf`;
172
+ const trySend = (frame) => Effect.try({
173
+ try: () => {
174
+ if (!sender.isDestroyed()) {
175
+ sender.send(sfChannel, frame);
176
+ }
177
+ },
178
+ catch: () => undefined,
179
+ }).pipe(Effect.ignore);
180
+ let handlerStream;
181
+ try {
182
+ handlerStream = impl(input);
183
+ }
184
+ catch (cause) {
185
+ return toDefectEnvelope(cause, `Stream ${method.name} implementation threw`);
186
+ }
187
+ const buildTerminalFrame = (cause) => {
188
+ const failure = Cause.failureOption(cause);
189
+ if (failure._tag === "Some") {
190
+ if (encodeFailure) {
191
+ try {
192
+ return {
193
+ type: "error",
194
+ streamId,
195
+ error: {
196
+ tag: extractErrorTag(failure.value),
197
+ data: encodeFailure(failure.value),
198
+ },
199
+ };
200
+ }
201
+ catch {
202
+ // encoding failed, fall through to defect
203
+ }
204
+ }
205
+ return { type: "defect", streamId, message: formatUnknown(failure.value) };
206
+ }
207
+ if (Cause.isInterruptedOnly(cause)) {
208
+ return { type: "end", streamId };
209
+ }
210
+ const defect = Cause.dieOption(cause);
211
+ return {
212
+ type: "defect",
213
+ streamId,
214
+ message: defect._tag === "Some" ? formatUnknown(defect.value) : "Stream failed unexpectedly",
215
+ };
216
+ };
217
+ const streamEffect = handlerStream.pipe(Stream.mapEffect((chunk) => Effect.try({
218
+ try: () => {
219
+ if (sender.isDestroyed())
220
+ return;
221
+ sender.send(sfChannel, { type: "data", streamId, payload: encodeChunk(chunk) });
222
+ },
223
+ catch: () => undefined,
224
+ }).pipe(Effect.ignore)), Stream.runDrain, Effect.andThen(() => trySend({ type: "end", streamId })), Effect.catchAllCause((cause) => sender.isDestroyed() ? Effect.void : trySend(buildTerminalFrame(cause))), Effect.ensuring(Effect.sync(() => {
225
+ activeStreams.delete(streamId);
226
+ })));
227
+ // Reserve entry BEFORE forking
228
+ const entry = {
229
+ fiber: null,
230
+ senderId: sender.id,
231
+ };
232
+ activeStreams.set(streamId, entry);
233
+ const fiber = runFork(streamEffect);
234
+ entry.fiber = fiber;
235
+ const response = {
236
+ type: "success",
237
+ data: { type: "stream_started" },
238
+ };
239
+ return response;
240
+ });
241
+ }
242
+ }
103
243
  let running = false;
104
244
  let disposed = false;
105
245
  function start() {
@@ -115,6 +255,35 @@ export function createRpcEndpoint(contract, ipc, implementations, options) {
115
255
  ipc.handle(channel, listener);
116
256
  registeredChannels.push(channel);
117
257
  }
258
+ // Register stream handlers
259
+ for (const [channel, listener] of streamListeners) {
260
+ ipc.handle(channel, listener);
261
+ registeredChannels.push(channel);
262
+ }
263
+ // Register cancel handler if we have stream methods
264
+ if (streamMethods.length > 0) {
265
+ ipc.handle(cancelChannel, (event, rawPayload) => {
266
+ if (!isRecord(rawPayload)) {
267
+ return { cancelled: false };
268
+ }
269
+ const streamId = rawPayload.streamId;
270
+ if (typeof streamId !== "string")
271
+ return { cancelled: false };
272
+ const entry = activeStreams.get(streamId);
273
+ if (!entry)
274
+ return { cancelled: false };
275
+ // Validate sender identity
276
+ const sender = extractSender(event);
277
+ if (!sender || sender.id !== entry.senderId)
278
+ return { cancelled: false };
279
+ // Interrupt the fiber
280
+ if (entry.fiber) {
281
+ entry.fiber.unsafeInterruptAsFork(FiberId.none);
282
+ }
283
+ return { cancelled: true };
284
+ });
285
+ registeredChannels.push(cancelChannel);
286
+ }
118
287
  }
119
288
  catch (cause) {
120
289
  for (const channel of registeredChannels) {
@@ -133,6 +302,13 @@ export function createRpcEndpoint(contract, ipc, implementations, options) {
133
302
  if (!running) {
134
303
  return;
135
304
  }
305
+ // Interrupt all active stream fibers first
306
+ for (const entry of activeStreams.values()) {
307
+ if (entry.fiber) {
308
+ entry.fiber.unsafeInterruptAsFork(FiberId.none);
309
+ }
310
+ }
311
+ activeStreams.clear();
136
312
  let firstError;
137
313
  for (const channel of listeners.keys()) {
138
314
  try {
@@ -142,6 +318,22 @@ export function createRpcEndpoint(contract, ipc, implementations, options) {
142
318
  firstError ??= cause;
143
319
  }
144
320
  }
321
+ for (const channel of streamListeners.keys()) {
322
+ try {
323
+ ipc.removeHandler(channel);
324
+ }
325
+ catch (cause) {
326
+ firstError ??= cause;
327
+ }
328
+ }
329
+ if (streamMethods.length > 0) {
330
+ try {
331
+ ipc.removeHandler(cancelChannel);
332
+ }
333
+ catch (cause) {
334
+ firstError ??= cause;
335
+ }
336
+ }
145
337
  running = false;
146
338
  if (firstError !== undefined) {
147
339
  throw firstError;
@@ -224,8 +416,8 @@ export function createEventPublisher(_contract, options) {
224
416
  });
225
417
  return;
226
418
  }
227
- const window = options.getWindow();
228
- if (!window || window.isDestroyed()) {
419
+ const windows = options.getWindows();
420
+ if (windows.length === 0) {
229
421
  dropped += 1;
230
422
  safelyCall(diagnostics?.onDroppedEvent, {
231
423
  event: item.event.name,
@@ -236,23 +428,37 @@ export function createEventPublisher(_contract, options) {
236
428
  });
237
429
  return;
238
430
  }
239
- try {
240
- window.webContents.send(`${channelPrefix.event}${item.event.name}`, encoded);
241
- }
242
- catch (cause) {
243
- dropped += 1;
244
- safelyCall(diagnostics?.onDispatchFailure, {
245
- event: item.event.name,
246
- payload: item.payload,
247
- cause,
248
- });
249
- safelyCall(diagnostics?.onDroppedEvent, {
250
- event: item.event.name,
251
- payload: item.payload,
252
- reason: "dispatch_failed",
253
- queued: queue.length,
254
- dropped,
255
- });
431
+ const channel = `${channelPrefix.event}${item.event.name}`;
432
+ for (const window of windows) {
433
+ if (window.isDestroyed()) {
434
+ dropped += 1;
435
+ safelyCall(diagnostics?.onDroppedEvent, {
436
+ event: item.event.name,
437
+ payload: item.payload,
438
+ reason: "window_unavailable",
439
+ queued: queue.length,
440
+ dropped,
441
+ });
442
+ continue;
443
+ }
444
+ try {
445
+ window.webContents.send(channel, encoded);
446
+ }
447
+ catch (cause) {
448
+ dropped += 1;
449
+ safelyCall(diagnostics?.onDispatchFailure, {
450
+ event: item.event.name,
451
+ payload: item.payload,
452
+ cause,
453
+ });
454
+ safelyCall(diagnostics?.onDroppedEvent, {
455
+ event: item.event.name,
456
+ payload: item.payload,
457
+ reason: "dispatch_failed",
458
+ queued: queue.length,
459
+ dropped,
460
+ });
461
+ }
256
462
  }
257
463
  }
258
464
  function drain() {
package/dist/preload.d.ts CHANGED
@@ -2,6 +2,7 @@ import { type ChannelPrefix, type EventSubscribe, type RpcInvoke } from "./types
2
2
  export type BridgeAdapters = {
3
3
  readonly invoke: RpcInvoke;
4
4
  readonly subscribe: EventSubscribe;
5
+ readonly onStreamFrame: (listener: (frame: unknown) => void) => () => void;
5
6
  };
6
7
  export type BridgeAdaptersOptions = {
7
8
  readonly channelPrefix?: ChannelPrefix;
@@ -1 +1 @@
1
- {"version":3,"file":"preload.d.ts","sourceRoot":"","sources":["../src/preload.ts"],"names":[],"mappings":"AAEA,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,MAAM,MAAM,wBAAwB,GAAG,qBAAqB,GAAG;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAwCF,wBAAgB,oBAAoB,CAClC,OAAO,CAAC,EAAE,qBAAqB,GAC9B,cAAc,CAuBhB;AAED,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAgBrE;AAED,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,IAAI,CAYxE"}
1
+ {"version":3,"file":"preload.d.ts","sourceRoot":"","sources":["../src/preload.ts"],"names":[],"mappings":"AAGA,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;IACnC,QAAQ,CAAC,aAAa,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;CAC5E,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,MAAM,MAAM,wBAAwB,GAAG,qBAAqB,GAAG;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAoDF,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,cAAc,CAgCpF;AAED,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAiBrE;AAED,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,IAAI,CAaxE"}
package/dist/preload.js CHANGED
@@ -1,13 +1,24 @@
1
1
  import * as electronModule from "electron";
2
+ import { isRecord } from "./protocol.js";
2
3
  import { defaultChannelPrefix, } from "./types.js";
4
+ function isContextBridgeLike(value) {
5
+ return isRecord(value) && typeof value.exposeInMainWorld === "function";
6
+ }
7
+ function isIpcRendererLike(value) {
8
+ return (isRecord(value) &&
9
+ typeof value.invoke === "function" &&
10
+ typeof value.on === "function" &&
11
+ typeof value.removeListener === "function");
12
+ }
3
13
  function resolveElectronRendererBindings() {
4
- const moduleDefault = electronModule.default;
5
- const source = moduleDefault && typeof moduleDefault === "object"
6
- ? moduleDefault
7
- : electronModule;
8
- const contextBridge = source.contextBridge;
9
- const ipcRenderer = source.ipcRenderer;
10
- if (!contextBridge || !ipcRenderer) {
14
+ const mod = electronModule;
15
+ const moduleDefault = isRecord(mod) ? mod.default : undefined;
16
+ const source = isRecord(moduleDefault) ? moduleDefault : isRecord(mod) ? mod : undefined;
17
+ if (!source) {
18
+ throw new Error("electron-effect-rpc/preload requires Electron preload runtime bindings.");
19
+ }
20
+ const { contextBridge, ipcRenderer } = source;
21
+ if (!isContextBridgeLike(contextBridge) || !isIpcRendererLike(ipcRenderer)) {
11
22
  throw new Error("electron-effect-rpc/preload requires Electron preload runtime bindings.");
12
23
  }
13
24
  return { contextBridge, ipcRenderer };
@@ -24,9 +35,18 @@ export function createBridgeAdapters(options) {
24
35
  ipcRenderer.removeListener(channel, wrapped);
25
36
  };
26
37
  };
38
+ const onStreamFrame = (listener) => {
39
+ const channel = `${channelPrefix.rpc}sf`;
40
+ const wrapped = (_event, frame) => listener(frame);
41
+ ipcRenderer.on(channel, wrapped);
42
+ return () => {
43
+ ipcRenderer.removeListener(channel, wrapped);
44
+ };
45
+ };
27
46
  return {
28
47
  invoke,
29
48
  subscribe,
49
+ onStreamFrame,
30
50
  };
31
51
  }
32
52
  export function exposeRpcBridge(options) {
@@ -38,6 +58,7 @@ export function exposeRpcBridge(options) {
38
58
  });
39
59
  contextBridge.exposeInMainWorld(rpcGlobal, {
40
60
  invoke: adapters.invoke,
61
+ onStreamFrame: adapters.onStreamFrame,
41
62
  });
42
63
  contextBridge.exposeInMainWorld(eventsGlobal, {
43
64
  subscribe: adapters.subscribe,
@@ -52,5 +73,6 @@ export function exposeIpcBridge(options) {
52
73
  contextBridge.exposeInMainWorld(global, {
53
74
  invoke: adapters.invoke,
54
75
  subscribe: adapters.subscribe,
76
+ onStreamFrame: adapters.onStreamFrame,
55
77
  });
56
78
  }
@@ -15,9 +15,35 @@ export type RpcDefectEnvelope = {
15
15
  readonly cause?: unknown;
16
16
  };
17
17
  export type RpcResponseEnvelope = RpcSuccessEnvelope | RpcFailureEnvelope | RpcDefectEnvelope;
18
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
18
19
  export declare function formatUnknown(value: unknown): string;
19
20
  export declare function extractErrorTag(error: unknown): string;
20
21
  export declare function toDefectEnvelope(cause: unknown, prefix?: string): RpcDefectEnvelope;
21
22
  export declare function safelyCall<T>(callback: ((context: T) => void) | undefined, context: T): void;
23
+ export type StreamDataFrame = {
24
+ readonly type: "data";
25
+ readonly streamId: string;
26
+ readonly payload: unknown;
27
+ };
28
+ export type StreamEndFrame = {
29
+ readonly type: "end";
30
+ readonly streamId: string;
31
+ };
32
+ export type StreamErrorFrame = {
33
+ readonly type: "error";
34
+ readonly streamId: string;
35
+ readonly error: {
36
+ readonly tag: string;
37
+ readonly data: unknown;
38
+ };
39
+ };
40
+ export type StreamDefectFrame = {
41
+ readonly type: "defect";
42
+ readonly streamId: string;
43
+ readonly message: string;
44
+ };
45
+ export type StreamFrame = StreamDataFrame | StreamEndFrame | StreamErrorFrame | StreamDefectFrame;
46
+ export declare function extractStreamIdFromRaw(value: unknown): string | null;
47
+ export declare function parseStreamFrame(value: unknown): StreamFrame | null;
22
48
  export declare function parseRpcResponseEnvelope(value: unknown): RpcResponseEnvelope | null;
23
49
  //# sourceMappingURL=protocol.d.ts.map
@@ -1 +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"}
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,GAAG,kBAAkB,GAAG,kBAAkB,GAAG,iBAAiB,CAAC;AAM9F,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUtD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAOnF;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,CAU5F;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,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,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,cAAc,GAAG,gBAAgB,GAAG,iBAAiB,CAAC;AAElG,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKpE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI,CA4CnE;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CA+CnF"}
package/dist/protocol.js CHANGED
@@ -1,7 +1,7 @@
1
1
  function hasOwn(record, key) {
2
2
  return Object.prototype.hasOwnProperty.call(record, key);
3
3
  }
4
- function isRecord(value) {
4
+ export function isRecord(value) {
5
5
  return typeof value === "object" && value !== null;
6
6
  }
7
7
  export function formatUnknown(value) {
@@ -35,6 +35,48 @@ export function safelyCall(callback, context) {
35
35
  // Diagnostics hooks must never crash transport internals.
36
36
  }
37
37
  }
38
+ export function extractStreamIdFromRaw(value) {
39
+ if (!isRecord(value) || typeof value.streamId !== "string") {
40
+ return null;
41
+ }
42
+ return value.streamId;
43
+ }
44
+ export function parseStreamFrame(value) {
45
+ if (!isRecord(value) || typeof value.type !== "string") {
46
+ return null;
47
+ }
48
+ if (typeof value.streamId !== "string") {
49
+ return null;
50
+ }
51
+ const streamId = value.streamId;
52
+ switch (value.type) {
53
+ case "data":
54
+ if (!hasOwn(value, "payload")) {
55
+ return null;
56
+ }
57
+ return { type: "data", streamId, payload: value.payload };
58
+ case "end":
59
+ return { type: "end", streamId };
60
+ case "error":
61
+ if (!isRecord(value.error) ||
62
+ typeof value.error.tag !== "string" ||
63
+ !hasOwn(value.error, "data")) {
64
+ return null;
65
+ }
66
+ return {
67
+ type: "error",
68
+ streamId,
69
+ error: { tag: value.error.tag, data: value.error.data },
70
+ };
71
+ case "defect":
72
+ if (typeof value.message !== "string") {
73
+ return null;
74
+ }
75
+ return { type: "defect", streamId, message: value.message };
76
+ default:
77
+ return null;
78
+ }
79
+ }
38
80
  export function parseRpcResponseEnvelope(value) {
39
81
  if (!isRecord(value) || typeof value.type !== "string") {
40
82
  return null;
@@ -1,6 +1,7 @@
1
- import { type RpcContract } from "./contract.ts";
2
- import { type AnyEvent, type AnyMethod, type EventSubscriber, type EventSubscriberOptions, type RpcClient, type RpcClientOptions } from "./types.ts";
3
- export declare function createRpcClient<const Methods extends ReadonlyArray<AnyMethod>, const Events extends ReadonlyArray<AnyEvent>>(contract: RpcContract<Methods, Events>, options: RpcClientOptions): RpcClient<RpcContract<Methods, Events>>;
4
- export declare function createEventSubscriber<const Methods extends ReadonlyArray<AnyMethod>, const Events extends ReadonlyArray<AnyEvent>>(contract: RpcContract<Methods, Events>, options: EventSubscriberOptions): EventSubscriber<RpcContract<Methods, Events>>;
1
+ import { type AnyStreamMethod, type RpcContract } from "./contract.ts";
2
+ import { type AnyEvent, type AnyMethod, type EventSubscriber, type EventSubscriberOptions, type RpcClient, type RpcClientOptions, type StreamRpcClientHandle, type StreamRpcClientOptions } from "./types.ts";
3
+ export declare function createRpcClient<const Methods extends ReadonlyArray<AnyMethod>, const Events extends ReadonlyArray<AnyEvent>, const StreamMethods extends ReadonlyArray<AnyStreamMethod> = readonly []>(contract: RpcContract<Methods, Events, StreamMethods>, options: RpcClientOptions): RpcClient<RpcContract<Methods, Events, StreamMethods>>;
4
+ export declare function createEventSubscriber<const Methods extends ReadonlyArray<AnyMethod>, const Events extends ReadonlyArray<AnyEvent>, const StreamMethods extends ReadonlyArray<AnyStreamMethod> = readonly []>(contract: RpcContract<Methods, Events, StreamMethods>, options: EventSubscriberOptions): EventSubscriber<RpcContract<Methods, Events, StreamMethods>>;
5
+ export declare function createStreamRpcClient<const Methods extends ReadonlyArray<AnyMethod>, const Events extends ReadonlyArray<AnyEvent>, const StreamMethods extends ReadonlyArray<AnyStreamMethod>>(contract: RpcContract<Methods, Events, StreamMethods>, options: StreamRpcClientOptions): StreamRpcClientHandle<RpcContract<Methods, Events, StreamMethods>>;
5
6
  export { RpcDefectError } from "./types.ts";
6
7
  //# sourceMappingURL=renderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,WAAW,EAIjB,MAAM,eAAe,CAAC;AAOvB,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,SAAS,EAGd,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAE3B,KAAK,SAAS,EACd,KAAK,gBAAgB,EAGtB,MAAM,YAAY,CAAC;AAuEpB,wBAAgB,eAAe,CAC7B,KAAK,CAAC,OAAO,SAAS,aAAa,CAAC,SAAS,CAAC,EAC9C,KAAK,CAAC,MAAM,SAAS,aAAa,CAAC,QAAQ,CAAC,EAE5C,QAAQ,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EACtC,OAAO,EAAE,gBAAgB,GACxB,SAAS,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CA0KzC;AAqBD,wBAAgB,qBAAqB,CACnC,KAAK,CAAC,OAAO,SAAS,aAAa,CAAC,SAAS,CAAC,EAC9C,KAAK,CAAC,MAAM,SAAS,aAAa,CAAC,QAAQ,CAAC,EAE5C,QAAQ,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EACtC,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAyF/C;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,WAAW,EAMjB,MAAM,eAAe,CAAC;AAUvB,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;AAsEpB,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,CA0JxD;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,CAsF9D;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,CA2NpE;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}