@dxos/rpc 0.10.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.
@@ -0,0 +1,948 @@
1
+ import * as RpcClient from "@effect/rpc/RpcClient";
2
+ import * as RpcClientError from "@effect/rpc/RpcClientError";
3
+ import * as RpcMessage from "@effect/rpc/RpcMessage";
4
+ import * as RpcSerialization from "@effect/rpc/RpcSerialization";
5
+ import * as RpcServer from "@effect/rpc/RpcServer";
6
+ import * as Duration from "effect/Duration";
7
+ import * as Effect from "effect/Effect";
8
+ import * as Layer from "effect/Layer";
9
+ import * as Mailbox from "effect/Mailbox";
10
+ import * as Option from "effect/Option";
11
+ import { log } from "@dxos/log";
12
+ import { StackTrace } from "@dxos/debug";
13
+ import { RpcClosedError, RpcNotOpenError, decodeError, encodeError } from "@dxos/protocols";
14
+ import { Event, Trigger, asyncTimeout, synchronized } from "@dxos/async";
15
+ import { Stream } from "@dxos/codec-protobuf";
16
+ import { ContextRpcCodec } from "@dxos/context";
17
+ import { invariant } from "@dxos/invariant";
18
+ import { schema } from "@dxos/protocols/proto";
19
+ import { exponentialBackoffInterval, isNode } from "@dxos/util";
20
+ import { MessageTrace } from "@dxos/protocols/proto/dxos/rpc";
21
+ //#region src/effect-rpc.ts
22
+ var __dxlog_file$2 = "/__w/dxos/dxos/packages/core/mesh/rpc/src/effect-rpc.ts";
23
+ /**
24
+ * Interval at which the client re-sends the initial Ping while waiting for the server to attach.
25
+ */
26
+ var HANDSHAKE_RETRY_INTERVAL = Duration.millis(50);
27
+ /**
28
+ * Effect RPC protocols over a {@link RpcPort} — a transport-agnostic, reliable, ordered,
29
+ * binary message channel. Message envelopes are framed with msgpack; RPC payloads are expected
30
+ * to already be binary-safe (e.g. protobuf-encoded by the payload schemas).
31
+ */
32
+ var subscribePort = (port) => Effect.gen(function* () {
33
+ const mailbox = yield* Mailbox.make();
34
+ const unsubscribe = port.subscribe((message) => {
35
+ mailbox.unsafeOffer(message);
36
+ });
37
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
38
+ unsubscribe?.();
39
+ }));
40
+ return mailbox;
41
+ });
42
+ var sendFrame = (port, frame) => frame === void 0 || typeof frame === "string" ? Effect.dieMessage("rpc-port protocol requires binary frames") : Effect.tryPromise({
43
+ try: async () => port.send(frame.slice()),
44
+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
45
+ });
46
+ /**
47
+ * Client-side effect-rpc protocol over an {@link RpcPort}.
48
+ *
49
+ * Performs a Ping/Pong handshake on construction: the server answers Pings as soon as it is
50
+ * running, so construction blocks until the peer is reachable and fails fast under an outer
51
+ * timeout instead of buffering requests towards a peer that never attaches.
52
+ */
53
+ var makeProtocolRpcPortClient = (port) => RpcClient.Protocol.make(Effect.fnUntraced(function* (writeResponse) {
54
+ const parser = RpcSerialization.msgPack.unsafeMake();
55
+ const mailbox = yield* subscribePort(port);
56
+ const decodeFrame = (frame) => Effect.try({
57
+ try: () => parser.decode(frame),
58
+ catch: (cause) => {
59
+ log.warn("rpc-port client: failed to decode frame", { cause }, {
60
+ "~LogMeta": "~LogMeta",
61
+ F: __dxlog_file$2,
62
+ L: 75,
63
+ S: this
64
+ });
65
+ return [];
66
+ }
67
+ }).pipe(Effect.merge);
68
+ const send = (request) => Effect.suspend(() => sendFrame(port, parser.encode(request))).pipe(Effect.mapError((cause) => new RpcClientError.RpcClientError({
69
+ reason: "Protocol",
70
+ message: "Failed to send message over RpcPort",
71
+ cause
72
+ })));
73
+ yield* Effect.gen(function* () {
74
+ let connected = false;
75
+ while (!connected) {
76
+ yield* send(RpcMessage.constPing);
77
+ const frame = yield* mailbox.take.pipe(Effect.timeoutOption(HANDSHAKE_RETRY_INTERVAL));
78
+ if (Option.isNone(frame)) continue;
79
+ for (const response of yield* decodeFrame(frame.value)) if (response._tag === "Pong") connected = true;
80
+ else yield* writeResponse(response);
81
+ }
82
+ }).pipe(Effect.orDie);
83
+ yield* mailbox.take.pipe(Effect.flatMap(decodeFrame), Effect.flatMap((responses) => Effect.forEach(responses, writeResponse, { discard: true })), Effect.forever, Effect.orDie, Effect.interruptible, Effect.forkScoped);
84
+ return {
85
+ send,
86
+ supportsAck: true,
87
+ supportsTransferables: false
88
+ };
89
+ }));
90
+ var layerProtocolRpcPortClient = (port) => Layer.scoped(RpcClient.Protocol, makeProtocolRpcPortClient(port));
91
+ /**
92
+ * Server-side effect-rpc protocol over an {@link RpcPort}.
93
+ * The port carries a single logical client for the lifetime of the protocol.
94
+ */
95
+ var makeProtocolRpcPortServer = (port) => RpcServer.Protocol.make(Effect.fnUntraced(function* (writeRequest) {
96
+ const parser = RpcSerialization.msgPack.unsafeMake();
97
+ const mailbox = yield* subscribePort(port);
98
+ const disconnects = yield* Mailbox.make();
99
+ const clientId = 0;
100
+ yield* mailbox.take.pipe(Effect.flatMap((frame) => Effect.try({
101
+ try: () => parser.decode(frame),
102
+ catch: (cause) => {
103
+ log.warn("rpc-port server: failed to decode frame", { cause }, {
104
+ "~LogMeta": "~LogMeta",
105
+ F: __dxlog_file$2,
106
+ L: 151,
107
+ S: this
108
+ });
109
+ return [];
110
+ }
111
+ }).pipe(Effect.merge)), Effect.flatMap((requests) => Effect.forEach(requests, (request) => writeRequest(clientId, request), { discard: true })), Effect.forever, Effect.interruptible, Effect.forkScoped);
112
+ return {
113
+ disconnects,
114
+ send: (_clientId, response) => Effect.suspend(() => sendFrame(port, parser.encode(response))).pipe(Effect.orDie),
115
+ end: (_clientId) => Effect.void,
116
+ clientIds: Effect.sync(() => /* @__PURE__ */ new Set([clientId])),
117
+ initialMessage: Effect.succeed(Option.none()),
118
+ supportsAck: true,
119
+ supportsTransferables: false,
120
+ supportsSpanPropagation: false
121
+ };
122
+ }));
123
+ var layerProtocolRpcPortServer = (port) => Layer.scoped(RpcServer.Protocol, makeProtocolRpcPortServer(port));
124
+ //#endregion
125
+ //#region src/errors.ts
126
+ var decodeRpcError = (err, rpcMethod) => decodeError(err, { appendStack: `\n at RPC ${rpcMethod} \n` + new StackTrace().getStack(1) });
127
+ //#endregion
128
+ //#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorate.js
129
+ function __decorate(decorators, target, key, desc) {
130
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
131
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
132
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
133
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
134
+ }
135
+ //#endregion
136
+ //#region src/rpc.ts
137
+ var __dxlog_file$1 = "/__w/dxos/dxos/packages/core/mesh/rpc/src/rpc.ts";
138
+ var DEFAULT_TIMEOUT = 3e4;
139
+ var BYE_SEND_TIMEOUT = 2e3;
140
+ var CLOSE_TIMEOUT = 3e3;
141
+ var PendingRpcRequest = class {
142
+ resolve;
143
+ reject;
144
+ stream;
145
+ constructor(resolve, reject, stream) {
146
+ this.resolve = resolve;
147
+ this.reject = reject;
148
+ this.stream = stream;
149
+ }
150
+ };
151
+ var RpcMessageCodec;
152
+ var getRpcMessageCodec = () => RpcMessageCodec ??= schema.getCodecForType("dxos.rpc.RpcMessage");
153
+ /**
154
+ * A remote procedure call peer.
155
+ *
156
+ * Provides a away to make RPC calls and get a response back as a promise.
157
+ * Does not handle encoding/decoding and only works with byte buffers.
158
+ * For type safe approach see `createRpcClient` and `createRpcServer`.
159
+ *
160
+ * Must be connected with another instance on the other side via `send`/`receive` methods.
161
+ * Both sides must be opened before making any RPC calls.
162
+ *
163
+ * Errors inside the handler get serialized and sent to the other side.
164
+ *
165
+ * Inspired by JSON-RPC 2.0 https://www.jsonrpc.org/specification.
166
+ */
167
+ var RpcPeer = class {
168
+ _params;
169
+ _outgoingRequests = /* @__PURE__ */ new Map();
170
+ _localStreams = /* @__PURE__ */ new Map();
171
+ _remoteOpenTrigger = new Trigger();
172
+ /**
173
+ * Triggered when the peer starts closing.
174
+ */
175
+ _closingTrigger = new Trigger();
176
+ /**
177
+ * Triggered when peer receives a bye message.
178
+ */
179
+ _byeTrigger = new Trigger();
180
+ _nextId = 0;
181
+ _state = "INITIAL";
182
+ _unsubscribeFromPort = void 0;
183
+ _clearOpenInterval = void 0;
184
+ constructor(params) {
185
+ this._params = {
186
+ timeout: void 0,
187
+ streamHandler: void 0,
188
+ noHandshake: false,
189
+ ...params
190
+ };
191
+ }
192
+ /**
193
+ * Open the peer. Required before making any calls.
194
+ *
195
+ * Will block before the other peer calls `open`.
196
+ */
197
+ async open() {
198
+ if (this._state !== "INITIAL") return;
199
+ this._unsubscribeFromPort = this._params.port.subscribe(async (msg) => {
200
+ try {
201
+ await this._receive(msg);
202
+ } catch (err) {
203
+ log.catch(err, void 0, {
204
+ "~LogMeta": "~LogMeta",
205
+ F: __dxlog_file$1,
206
+ L: 157,
207
+ S: this
208
+ });
209
+ }
210
+ });
211
+ this._state = "OPENING";
212
+ if (this._params.noHandshake) {
213
+ this._state = "OPENED";
214
+ this._remoteOpenTrigger.wake();
215
+ return;
216
+ }
217
+ log("sending open message", { state: this._state }, {
218
+ "~LogMeta": "~LogMeta",
219
+ F: __dxlog_file$1,
220
+ L: 169,
221
+ S: this
222
+ });
223
+ await this._sendMessage({ open: true });
224
+ if (this._state !== "OPENING") return;
225
+ this._clearOpenInterval = exponentialBackoffInterval(() => {
226
+ this._sendMessage({ open: true }).catch((err) => log.warn(err, void 0, {
227
+ "~LogMeta": "~LogMeta",
228
+ F: __dxlog_file$1,
229
+ L: 178,
230
+ S: this
231
+ }));
232
+ }, 50);
233
+ await Promise.race([this._remoteOpenTrigger.wait(), this._closingTrigger.wait()]);
234
+ this._clearOpenInterval?.();
235
+ if (this._state !== "OPENED") return;
236
+ log("resending open message", { state: this._state }, {
237
+ "~LogMeta": "~LogMeta",
238
+ F: __dxlog_file$1,
239
+ L: 192,
240
+ S: this
241
+ });
242
+ await this._sendMessage({ openAck: true });
243
+ }
244
+ /**
245
+ * Close the peer.
246
+ * Stop taking or making requests.
247
+ * Will wait for confirmation from the other side.
248
+ * Any responses for RPC calls made before close will be delivered.
249
+ */
250
+ async close({ timeout = CLOSE_TIMEOUT } = {}) {
251
+ if (this._state === "CLOSED") return;
252
+ this._abortRequests();
253
+ if (this._state === "OPENED" && !this._params.noHandshake) {
254
+ try {
255
+ this._state = "CLOSING";
256
+ await this._sendMessage({ bye: {} }, BYE_SEND_TIMEOUT);
257
+ } catch (err) {
258
+ log("error closing peer, sending bye", { err }, {
259
+ "~LogMeta": "~LogMeta",
260
+ F: __dxlog_file$1,
261
+ L: 214,
262
+ S: this
263
+ });
264
+ }
265
+ try {
266
+ log("closing waiting on bye", void 0, {
267
+ "~LogMeta": "~LogMeta",
268
+ F: __dxlog_file$1,
269
+ L: 217,
270
+ S: this
271
+ });
272
+ await this._byeTrigger.wait({ timeout });
273
+ } catch (err) {
274
+ log("error closing peer", { err }, {
275
+ "~LogMeta": "~LogMeta",
276
+ F: __dxlog_file$1,
277
+ L: 220,
278
+ S: this
279
+ });
280
+ return;
281
+ }
282
+ }
283
+ this._disposeAndClose();
284
+ }
285
+ /**
286
+ * Dispose the connection without waiting for the other side.
287
+ */
288
+ async abort() {
289
+ if (this._state === "CLOSED") return;
290
+ this._abortRequests();
291
+ this._disposeAndClose();
292
+ }
293
+ _abortRequests() {
294
+ this._clearOpenInterval?.();
295
+ this._closingTrigger.wake();
296
+ for (const req of this._outgoingRequests.values()) req.reject(new RpcClosedError());
297
+ this._outgoingRequests.clear();
298
+ }
299
+ _disposeAndClose() {
300
+ this._unsubscribeFromPort?.();
301
+ this._unsubscribeFromPort = void 0;
302
+ this._clearOpenInterval?.();
303
+ this._state = "CLOSED";
304
+ }
305
+ /**
306
+ * Handle incoming message. Should be called as the result of other peer's `send` callback.
307
+ */
308
+ async _receive(msg) {
309
+ const decoded = getRpcMessageCodec().decode(msg, { preserveAny: true });
310
+ log.trace("received message", { type: Object.keys(decoded)[0] }, {
311
+ "~LogMeta": "~LogMeta",
312
+ F: __dxlog_file$1,
313
+ L: 264,
314
+ S: this
315
+ });
316
+ if (decoded.request) {
317
+ if (this._state !== "OPENED" && this._state !== "OPENING") {
318
+ log("received request while closed", void 0, {
319
+ "~LogMeta": "~LogMeta",
320
+ F: __dxlog_file$1,
321
+ L: 268,
322
+ S: this
323
+ });
324
+ await this._sendMessage({ response: {
325
+ id: decoded.request.id,
326
+ error: encodeError(new RpcClosedError())
327
+ } });
328
+ return;
329
+ }
330
+ const req = decoded.request;
331
+ if (req.stream) {
332
+ log("stream request", { method: req.method }, {
333
+ "~LogMeta": "~LogMeta",
334
+ F: __dxlog_file$1,
335
+ L: 280,
336
+ S: this
337
+ });
338
+ this._callStreamHandler(req, (response) => {
339
+ log.trace("sending stream response", {
340
+ method: req.method,
341
+ response: response.payload?.type_url,
342
+ error: response.error,
343
+ close: response.close
344
+ }, {
345
+ "~LogMeta": "~LogMeta",
346
+ F: __dxlog_file$1,
347
+ L: 282,
348
+ S: this
349
+ });
350
+ this._sendMessage({ response }).catch((err) => {
351
+ log.warn("failed during close", err, {
352
+ "~LogMeta": "~LogMeta",
353
+ F: __dxlog_file$1,
354
+ L: 290,
355
+ S: this
356
+ });
357
+ });
358
+ });
359
+ } else {
360
+ log.trace("requesting...", { method: req.method }, {
361
+ "~LogMeta": "~LogMeta",
362
+ F: __dxlog_file$1,
363
+ L: 294,
364
+ S: this
365
+ });
366
+ const response = await this._callHandler(req);
367
+ log.trace("sending response", {
368
+ method: req.method,
369
+ response: response.payload?.type_url,
370
+ error: response.error
371
+ }, {
372
+ "~LogMeta": "~LogMeta",
373
+ F: __dxlog_file$1,
374
+ L: 297,
375
+ S: this
376
+ });
377
+ await this._sendMessage({ response });
378
+ }
379
+ } else if (decoded.response) {
380
+ if (this._state !== "OPENED") {
381
+ log("received response while closed", void 0, {
382
+ "~LogMeta": "~LogMeta",
383
+ F: __dxlog_file$1,
384
+ L: 306,
385
+ S: this
386
+ });
387
+ return;
388
+ }
389
+ const responseId = decoded.response.id;
390
+ invariant(typeof responseId === "number", void 0, {
391
+ "~LogMeta": "~LogMeta",
392
+ F: __dxlog_file$1,
393
+ L: 311,
394
+ S: this,
395
+ A: ["typeof responseId === 'number'", ""]
396
+ });
397
+ if (!this._outgoingRequests.has(responseId)) {
398
+ log.trace("received response with invalid id", { responseId }, {
399
+ "~LogMeta": "~LogMeta",
400
+ F: __dxlog_file$1,
401
+ L: 313,
402
+ S: this
403
+ });
404
+ return;
405
+ }
406
+ const item = this._outgoingRequests.get(responseId);
407
+ if (!item.stream) this._outgoingRequests.delete(responseId);
408
+ log.trace("response", { type_url: decoded.response.payload?.type_url }, {
409
+ "~LogMeta": "~LogMeta",
410
+ F: __dxlog_file$1,
411
+ L: 323,
412
+ S: this
413
+ });
414
+ item.resolve(decoded.response);
415
+ } else if (decoded.open) {
416
+ log("received open message", { state: this._state }, {
417
+ "~LogMeta": "~LogMeta",
418
+ F: __dxlog_file$1,
419
+ L: 326,
420
+ S: this
421
+ });
422
+ if (this._params.noHandshake) return;
423
+ await this._sendMessage({ openAck: true });
424
+ } else if (decoded.openAck) {
425
+ log("received openAck message", { state: this._state }, {
426
+ "~LogMeta": "~LogMeta",
427
+ F: __dxlog_file$1,
428
+ L: 333,
429
+ S: this
430
+ });
431
+ if (this._params.noHandshake) return;
432
+ this._state = "OPENED";
433
+ this._remoteOpenTrigger.wake();
434
+ } else if (decoded.streamClose) {
435
+ if (this._state !== "OPENED") {
436
+ log("received stream close while closed", void 0, {
437
+ "~LogMeta": "~LogMeta",
438
+ F: __dxlog_file$1,
439
+ L: 342,
440
+ S: this
441
+ });
442
+ return;
443
+ }
444
+ log("received stream close", { id: decoded.streamClose.id }, {
445
+ "~LogMeta": "~LogMeta",
446
+ F: __dxlog_file$1,
447
+ L: 346,
448
+ S: this
449
+ });
450
+ invariant(typeof decoded.streamClose.id === "number", void 0, {
451
+ "~LogMeta": "~LogMeta",
452
+ F: __dxlog_file$1,
453
+ L: 347,
454
+ S: this,
455
+ A: ["typeof decoded.streamClose.id === 'number'", ""]
456
+ });
457
+ const stream = this._localStreams.get(decoded.streamClose.id);
458
+ if (!stream) {
459
+ log("no local stream", { id: decoded.streamClose.id }, {
460
+ "~LogMeta": "~LogMeta",
461
+ F: __dxlog_file$1,
462
+ L: 350,
463
+ S: this
464
+ });
465
+ return;
466
+ }
467
+ this._localStreams.delete(decoded.streamClose.id);
468
+ await stream.close();
469
+ } else if (decoded.bye) {
470
+ this._byeTrigger.wake();
471
+ if (this._state !== "CLOSING" && this._state !== "CLOSED") {
472
+ log("replying to bye", void 0, {
473
+ "~LogMeta": "~LogMeta",
474
+ F: __dxlog_file$1,
475
+ L: 360,
476
+ S: this
477
+ });
478
+ this._state = "CLOSING";
479
+ await this._sendMessage({ bye: {} });
480
+ this._abortRequests();
481
+ this._disposeAndClose();
482
+ }
483
+ } else {
484
+ log.error("received malformed message", { msg }, {
485
+ "~LogMeta": "~LogMeta",
486
+ F: __dxlog_file$1,
487
+ L: 368,
488
+ S: this
489
+ });
490
+ throw new Error("Malformed message.");
491
+ }
492
+ }
493
+ /**
494
+ * Make RPC call. Will trigger a handler on the other side.
495
+ * Peer should be open before making this call.
496
+ */
497
+ async call(method, request, options) {
498
+ log.trace("calling...", { method }, {
499
+ "~LogMeta": "~LogMeta",
500
+ F: __dxlog_file$1,
501
+ L: 378,
502
+ S: this
503
+ });
504
+ throwIfNotOpen(this._state);
505
+ let response;
506
+ try {
507
+ const id = this._nextId++;
508
+ const responseReceived = new Promise((resolve, reject) => {
509
+ this._outgoingRequests.set(id, new PendingRpcRequest(resolve, reject, false));
510
+ });
511
+ let traceContext;
512
+ try {
513
+ traceContext = options?.ctx ? ContextRpcCodec.encode(options.ctx) : void 0;
514
+ } catch (err) {
515
+ log.warn("failed to encode trace context", { err }, {
516
+ "~LogMeta": "~LogMeta",
517
+ F: __dxlog_file$1,
518
+ L: 393,
519
+ S: this
520
+ });
521
+ }
522
+ const sending = this._sendMessage({ request: {
523
+ id,
524
+ method,
525
+ payload: request,
526
+ stream: false,
527
+ ...traceContext ? { traceContext } : {}
528
+ } });
529
+ const timeout = options?.timeout ?? this._params.timeout;
530
+ const waiting = timeout === 0 ? responseReceived : asyncTimeout(responseReceived, timeout ?? DEFAULT_TIMEOUT);
531
+ await Promise.race([sending, waiting]);
532
+ response = await waiting;
533
+ invariant(response.id === id, void 0, {
534
+ "~LogMeta": "~LogMeta",
535
+ F: __dxlog_file$1,
536
+ L: 414,
537
+ S: this,
538
+ A: ["response.id === id", ""]
539
+ });
540
+ } catch (err) {
541
+ if (err instanceof RpcClosedError) {
542
+ const error = new RpcClosedError();
543
+ error.stack += `\n\n info: RPC client was closed at:\n${err.stack?.split("\n").slice(1).join("\n")}`;
544
+ throw error;
545
+ }
546
+ throw err;
547
+ }
548
+ if (response.payload) return response.payload;
549
+ else if (response.error) throw decodeRpcError(response.error, method);
550
+ else throw new Error("Malformed response.");
551
+ }
552
+ /**
553
+ * Make RPC call with a streaming response.
554
+ * Will trigger a handler on the other side.
555
+ * Peer should be open before making this call.
556
+ */
557
+ callStream(method, request, options) {
558
+ throwIfNotOpen(this._state);
559
+ const id = this._nextId++;
560
+ return new Stream(({ ready, next, close }) => {
561
+ const onResponse = (response) => {
562
+ if (response.streamReady) ready();
563
+ else if (response.close) close();
564
+ else if (response.error) close(decodeRpcError(response.error, method));
565
+ else if (response.payload) next(response.payload);
566
+ else throw new Error("Malformed response.");
567
+ };
568
+ const stack = new StackTrace();
569
+ const closeStream = (err) => {
570
+ if (!err) close();
571
+ else {
572
+ err.stack += `\n\nError happened in the stream at:\n${stack.getStack()}`;
573
+ close(err);
574
+ }
575
+ };
576
+ this._outgoingRequests.set(id, new PendingRpcRequest(onResponse, closeStream, true));
577
+ let traceContext;
578
+ try {
579
+ traceContext = options?.ctx ? ContextRpcCodec.encode(options.ctx) : void 0;
580
+ } catch (err) {
581
+ log.warn("failed to encode trace context", { err }, {
582
+ "~LogMeta": "~LogMeta",
583
+ F: __dxlog_file$1,
584
+ L: 476,
585
+ S: this
586
+ });
587
+ }
588
+ try {
589
+ this._sendMessage({ request: {
590
+ id,
591
+ method,
592
+ payload: request,
593
+ stream: true,
594
+ ...traceContext ? { traceContext } : {}
595
+ } }).catch((err) => {
596
+ this._outgoingRequests.delete(id);
597
+ close(err);
598
+ });
599
+ } catch (err) {
600
+ this._outgoingRequests.delete(id);
601
+ throw err;
602
+ }
603
+ return () => {
604
+ this._sendMessage({ streamClose: { id } }).catch((err) => {
605
+ log.catch(err, void 0, {
606
+ "~LogMeta": "~LogMeta",
607
+ F: __dxlog_file$1,
608
+ L: 501,
609
+ S: this
610
+ });
611
+ });
612
+ this._outgoingRequests.delete(id);
613
+ };
614
+ });
615
+ }
616
+ async _sendMessage(message, timeout) {
617
+ log.trace("sending message", { type: Object.keys(message)[0] }, {
618
+ "~LogMeta": "~LogMeta",
619
+ F: __dxlog_file$1,
620
+ L: 509,
621
+ S: this
622
+ });
623
+ await this._params.port.send(getRpcMessageCodec().encode(message, { preserveAny: true }), timeout);
624
+ }
625
+ _getHandlerRpcOptions(req) {
626
+ let traceCtx;
627
+ if (req.traceContext) try {
628
+ traceCtx = ContextRpcCodec.decode(req.traceContext);
629
+ } catch (err) {
630
+ log.warn("failed to decode trace context", {
631
+ traceContext: req.traceContext,
632
+ err
633
+ }, {
634
+ "~LogMeta": "~LogMeta",
635
+ F: __dxlog_file$1,
636
+ L: 519,
637
+ S: this
638
+ });
639
+ }
640
+ if (!traceCtx && !this._params.handlerRpcOptions) return;
641
+ return {
642
+ ...this._params.handlerRpcOptions,
643
+ ...traceCtx ? { ctx: traceCtx } : {}
644
+ };
645
+ }
646
+ async _callHandler(req) {
647
+ try {
648
+ invariant(typeof req.id === "number", void 0, {
649
+ "~LogMeta": "~LogMeta",
650
+ F: __dxlog_file$1,
651
+ L: 530,
652
+ S: this,
653
+ A: ["typeof req.id === 'number'", ""]
654
+ });
655
+ invariant(req.payload, void 0, {
656
+ "~LogMeta": "~LogMeta",
657
+ F: __dxlog_file$1,
658
+ L: 531,
659
+ S: this,
660
+ A: ["req.payload", ""]
661
+ });
662
+ invariant(req.method, void 0, {
663
+ "~LogMeta": "~LogMeta",
664
+ F: __dxlog_file$1,
665
+ L: 532,
666
+ S: this,
667
+ A: ["req.method", ""]
668
+ });
669
+ const response = await this._params.callHandler(req.method, req.payload, this._getHandlerRpcOptions(req));
670
+ return {
671
+ id: req.id,
672
+ payload: response
673
+ };
674
+ } catch (err) {
675
+ return {
676
+ id: req.id,
677
+ error: encodeError(err)
678
+ };
679
+ }
680
+ }
681
+ _callStreamHandler(req, callback) {
682
+ try {
683
+ invariant(this._params.streamHandler, "Requests with streaming responses are not supported.", {
684
+ "~LogMeta": "~LogMeta",
685
+ F: __dxlog_file$1,
686
+ L: 549,
687
+ S: this,
688
+ A: ["this._params.streamHandler", "'Requests with streaming responses are not supported.'"]
689
+ });
690
+ invariant(typeof req.id === "number", void 0, {
691
+ "~LogMeta": "~LogMeta",
692
+ F: __dxlog_file$1,
693
+ L: 550,
694
+ S: this,
695
+ A: ["typeof req.id === 'number'", ""]
696
+ });
697
+ invariant(req.payload, void 0, {
698
+ "~LogMeta": "~LogMeta",
699
+ F: __dxlog_file$1,
700
+ L: 551,
701
+ S: this,
702
+ A: ["req.payload", ""]
703
+ });
704
+ invariant(req.method, void 0, {
705
+ "~LogMeta": "~LogMeta",
706
+ F: __dxlog_file$1,
707
+ L: 552,
708
+ S: this,
709
+ A: ["req.method", ""]
710
+ });
711
+ const responseStream = this._params.streamHandler(req.method, req.payload, this._getHandlerRpcOptions(req));
712
+ responseStream.onReady(() => {
713
+ callback({
714
+ id: req.id,
715
+ streamReady: true
716
+ });
717
+ });
718
+ responseStream.subscribe((msg) => {
719
+ callback({
720
+ id: req.id,
721
+ payload: msg
722
+ });
723
+ }, (error) => {
724
+ if (error) callback({
725
+ id: req.id,
726
+ error: encodeError(error)
727
+ });
728
+ else callback({
729
+ id: req.id,
730
+ close: true
731
+ });
732
+ });
733
+ this._localStreams.set(req.id, responseStream);
734
+ } catch (err) {
735
+ callback({
736
+ id: req.id,
737
+ error: encodeError(err)
738
+ });
739
+ }
740
+ }
741
+ };
742
+ __decorate([synchronized], RpcPeer.prototype, "open", null);
743
+ var throwIfNotOpen = (state) => {
744
+ switch (state) {
745
+ case "OPENED": return;
746
+ case "INITIAL": throw new RpcNotOpenError();
747
+ case "CLOSED": throw new RpcClosedError();
748
+ }
749
+ };
750
+ //#endregion
751
+ //#region src/service.ts
752
+ var __dxlog_file = "/__w/dxos/dxos/packages/core/mesh/rpc/src/service.ts";
753
+ /**
754
+ * Groups multiple services together to be served by a single RPC peer.
755
+ */
756
+ var createServiceBundle = (services) => services;
757
+ /**
758
+ * Type-safe RPC peer.
759
+ */
760
+ var ProtoRpcPeer = class {
761
+ rpc;
762
+ _peer;
763
+ constructor(rpc, _peer) {
764
+ this.rpc = rpc;
765
+ this._peer = _peer;
766
+ }
767
+ async open() {
768
+ await this._peer.open();
769
+ }
770
+ async close() {
771
+ await this._peer.close();
772
+ }
773
+ async abort() {
774
+ await this._peer.abort();
775
+ }
776
+ };
777
+ /**
778
+ * Create type-safe RPC peer from a service bundle.
779
+ * Can both handle and issue requests.
780
+ */
781
+ var createProtoRpcPeer = ({ requested, exposed, handlers, encodingOptions, ...rest }) => {
782
+ const exposedRpcs = {};
783
+ if (exposed) {
784
+ invariant(handlers, void 0, {
785
+ "~LogMeta": "~LogMeta",
786
+ F: __dxlog_file,
787
+ L: 93,
788
+ S: void 0,
789
+ A: ["handlers", ""]
790
+ });
791
+ for (const serviceName of Object.keys(exposed)) {
792
+ const serviceFqn = exposed[serviceName].serviceProto.fullName.slice(1);
793
+ const serviceProvider = handlers[serviceName];
794
+ exposedRpcs[serviceFqn] = exposed[serviceName].createServer(serviceProvider, encodingOptions);
795
+ }
796
+ }
797
+ const peer = new RpcPeer({
798
+ ...rest,
799
+ callHandler: (method, request, options) => {
800
+ const [serviceName, methodName] = parseMethodName(method);
801
+ if (!exposedRpcs[serviceName]) throw new Error(`Service not supported: ${serviceName}`);
802
+ return exposedRpcs[serviceName].call(methodName, request, options);
803
+ },
804
+ streamHandler: (method, request, options) => {
805
+ const [serviceName, methodName] = parseMethodName(method);
806
+ if (!exposedRpcs[serviceName]) throw new Error(`Service not supported: ${serviceName}`);
807
+ return exposedRpcs[serviceName].callStream(methodName, request, options);
808
+ }
809
+ });
810
+ const requestedRpcs = {};
811
+ if (requested) for (const serviceName of Object.keys(requested)) {
812
+ const serviceFqn = requested[serviceName].serviceProto.fullName.slice(1);
813
+ requestedRpcs[serviceName] = requested[serviceName].createClient({
814
+ call: (method, req, options) => peer.call(`${serviceFqn}.${method}`, req, options),
815
+ callStream: (method, req, options) => peer.callStream(`${serviceFqn}.${method}`, req, options)
816
+ }, encodingOptions);
817
+ }
818
+ return new ProtoRpcPeer(requestedRpcs, peer);
819
+ };
820
+ var parseMethodName = (method) => {
821
+ const separator = method.lastIndexOf(".");
822
+ const serviceName = method.slice(0, separator);
823
+ const methodName = method.slice(separator + 1);
824
+ if (serviceName.length === 0 || methodName.length === 0) throw new Error(`Invalid method: ${method}`);
825
+ return [serviceName, methodName];
826
+ };
827
+ /**
828
+ * Create a type-safe RPC client.
829
+ * @deprecated Use createProtoRpcPeer instead.
830
+ */
831
+ var createRpcClient = (serviceDef, options) => {
832
+ const peer = new RpcPeer({
833
+ ...options,
834
+ callHandler: () => {
835
+ throw new Error("Requests to client are not supported.");
836
+ }
837
+ });
838
+ return new ProtoRpcPeer(serviceDef.createClient({
839
+ call: peer.call.bind(peer),
840
+ callStream: peer.callStream.bind(peer)
841
+ }), peer);
842
+ };
843
+ /**
844
+ * Create a type-safe RPC server.
845
+ * @deprecated Use createProtoRpcPeer instead.
846
+ */
847
+ var createRpcServer = ({ service, handlers, ...rest }) => {
848
+ const server = service.createServer(handlers);
849
+ return new RpcPeer({
850
+ ...rest,
851
+ callHandler: server.call.bind(server),
852
+ streamHandler: server.callStream.bind(server)
853
+ });
854
+ };
855
+ /**
856
+ * Create type-safe RPC client from a service bundle.
857
+ * @deprecated Use createProtoRpcPeer instead.
858
+ */
859
+ var createBundledRpcClient = (descriptors, options) => {
860
+ return createProtoRpcPeer({
861
+ requested: descriptors,
862
+ ...options
863
+ });
864
+ };
865
+ /**
866
+ * Create type-safe RPC server from a service bundle.
867
+ * @deprecated Use createProtoRpcPeer instead.
868
+ */
869
+ var createBundledRpcServer = ({ services, handlers, ...rest }) => {
870
+ const rpc = {};
871
+ for (const serviceName of Object.keys(services)) {
872
+ const serviceFqn = services[serviceName].serviceProto.fullName.slice(1);
873
+ rpc[serviceFqn] = services[serviceName].createServer(handlers[serviceName]);
874
+ }
875
+ return new RpcPeer({
876
+ ...rest,
877
+ callHandler: (method, request) => {
878
+ const [serviceName, methodName] = parseMethodName(method);
879
+ if (!rpc[serviceName]) throw new Error(`Service not supported: ${serviceName}`);
880
+ return rpc[serviceName].call(methodName, request);
881
+ },
882
+ streamHandler: (method, request) => {
883
+ const [serviceName, methodName] = parseMethodName(method);
884
+ if (!rpc[serviceName]) throw new Error(`Service not supported: ${serviceName}`);
885
+ return rpc[serviceName].callStream(methodName, request);
886
+ }
887
+ });
888
+ };
889
+ //#endregion
890
+ //#region src/testing.ts
891
+ /**
892
+ * Create bi-directionally linked ports.
893
+ */
894
+ var createLinkedPorts = ({ delay } = {}) => {
895
+ let port1Received;
896
+ let port2Received;
897
+ const send = (handler, msg) => {
898
+ if (delay) setTimeout(() => handler?.(msg), delay);
899
+ else handler?.(msg);
900
+ };
901
+ return [{
902
+ send: (msg) => send(port2Received, msg),
903
+ subscribe: (cb) => {
904
+ port1Received = cb;
905
+ }
906
+ }, {
907
+ send: (msg) => send(port1Received, msg),
908
+ subscribe: (cb) => {
909
+ port2Received = cb;
910
+ }
911
+ }];
912
+ };
913
+ var encodeMessage = (msg) => isNode() ? Buffer.from(msg) : new TextEncoder().encode(msg);
914
+ //#endregion
915
+ //#region src/trace.ts
916
+ var PortTracer = class {
917
+ _wrappedPort;
918
+ message = new Event();
919
+ _port;
920
+ constructor(_wrappedPort) {
921
+ this._wrappedPort = _wrappedPort;
922
+ this._port = {
923
+ send: (msg) => {
924
+ this.message.emit({
925
+ direction: MessageTrace.Direction.OUTGOING,
926
+ data: msg
927
+ });
928
+ return this._wrappedPort.send(msg);
929
+ },
930
+ subscribe: (cb) => {
931
+ return this._wrappedPort.subscribe((msg) => {
932
+ this.message.emit({
933
+ direction: MessageTrace.Direction.INCOMING,
934
+ data: msg
935
+ });
936
+ cb(msg);
937
+ });
938
+ }
939
+ };
940
+ }
941
+ get port() {
942
+ return this._port;
943
+ }
944
+ };
945
+ //#endregion
946
+ export { PortTracer, ProtoRpcPeer, RpcPeer, createBundledRpcClient, createBundledRpcServer, createLinkedPorts, createProtoRpcPeer, createRpcClient, createRpcServer, createServiceBundle, decodeRpcError, encodeMessage, layerProtocolRpcPortClient, layerProtocolRpcPortServer, makeProtocolRpcPortClient, makeProtocolRpcPortServer, parseMethodName };
947
+
948
+ //# sourceMappingURL=index.mjs.map