@kyneta/bridge-transport 1.6.0 → 1.7.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/index.d.ts +11 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -31
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
- package/src/__tests__/bridge.test.ts +66 -27
- package/src/bridge.ts +63 -56
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { GeneratedChannel, Transport, TransportFactory } from "@kyneta/transport
|
|
|
5
5
|
* In-process byte router connecting multiple `BridgeTransport`s,
|
|
6
6
|
* keyed by each transport's unique `transportId`.
|
|
7
7
|
*
|
|
8
|
-
* Channel-level sends
|
|
9
|
-
*
|
|
8
|
+
* Channel-level sends route through per-channel `Pipeline<"binary">`
|
|
9
|
+
* instances and call `routeBytes`.
|
|
10
10
|
*/
|
|
11
11
|
declare class Bridge {
|
|
12
12
|
readonly transports: Map<string, BridgeTransport>;
|
|
@@ -19,7 +19,7 @@ declare class Bridge {
|
|
|
19
19
|
*
|
|
20
20
|
* Used by `BridgeTransport`'s channel send path.
|
|
21
21
|
*/
|
|
22
|
-
routeBytes(fromTransportId: string, toTransportId: string, bytes: Uint8Array): void;
|
|
22
|
+
routeBytes(fromTransportId: string, toTransportId: string, bytes: Uint8Array<ArrayBuffer>): void;
|
|
23
23
|
get transportIds(): Set<string>;
|
|
24
24
|
}
|
|
25
25
|
type BridgeTransportContext = {
|
|
@@ -35,9 +35,9 @@ type BridgeTransportParams = {
|
|
|
35
35
|
bridge: Bridge;
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
|
-
* In-memory transport that runs the
|
|
39
|
-
* Tests that use this
|
|
40
|
-
* production transports.
|
|
38
|
+
* In-memory transport that runs the full wire pipeline end-to-end
|
|
39
|
+
* via per-channel `Pipeline<"binary">` instances. Tests that use this
|
|
40
|
+
* transport exercise the same wire path as production transports.
|
|
41
41
|
*
|
|
42
42
|
* @example
|
|
43
43
|
* ```typescript
|
|
@@ -51,7 +51,7 @@ declare class BridgeTransport extends Transport<BridgeTransportContext> {
|
|
|
51
51
|
readonly bridge: Bridge;
|
|
52
52
|
private channelToAdapter;
|
|
53
53
|
private adapterToChannel;
|
|
54
|
-
private
|
|
54
|
+
private pipelineByChannel;
|
|
55
55
|
constructor({
|
|
56
56
|
transportId,
|
|
57
57
|
transportType,
|
|
@@ -65,11 +65,11 @@ declare class BridgeTransport extends Transport<BridgeTransportContext> {
|
|
|
65
65
|
/**
|
|
66
66
|
* Deliver encoded bytes to the appropriate channel.
|
|
67
67
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* via `queueMicrotask()`.
|
|
68
|
+
* Routes through the per-channel `Pipeline.receive()` which handles
|
|
69
|
+
* decoding, deframing, reassembly, and alias resolution. Delivers
|
|
70
|
+
* each resolved message asynchronously via `queueMicrotask()`.
|
|
71
71
|
*/
|
|
72
|
-
deliverBytes(fromTransportId: string, bytes: Uint8Array): void;
|
|
72
|
+
deliverBytes(fromTransportId: string, bytes: Uint8Array<ArrayBuffer>): void;
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
75
|
* Create a BridgeTransport factory for in-process testing.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/bridge.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/bridge.ts"],"mappings":";;;;AAgCA;;;;;;cAAa,MAAA;EAAA,SACF,UAAA,EAAU,GAAA,SAAA,eAAA;EAEnB,YAAA,CAAa,SAAA,EAAW,eAAA;EAOxB,eAAA,CAAgB,WAAA;EAqBO;;;;;;;EAVvB,UAAA,CACE,eAAA,UACA,aAAA,UACA,KAAA,EAAO,UAAA,CAAW,WAAA;EAAA,IAOhB,YAAA,CAAA,GAAgB,GAAA;AAAA;AAAA,KASjB,sBAAA;EACH,iBAAiB;AAAA;AAAA,KAGP,qBAAA;EApBR,+EAsBF,WAAA;EAfoB;;AAAG;AAGxB;EAiBC,aAAA;EACA,MAAA,EAAQ,MAAM;AAAA;AAXG;AAGnB;;;;;;;;;AAQgB;AAgBhB;;AA3BmB,cA2BN,eAAA,SAAwB,SAAA,CAAU,sBAAA;EAAA,SACpC,MAAA,EAAQ,MAAA;EAAA,QAGT,gBAAA;EAAA,QACA,gBAAA;EAAA,QAIA,iBAAA;;IAEM,WAAA;IAAa,aAAA;IAAe;EAAA,GAAU,qBAAA;EAKpD,QAAA,CAAS,OAAA,EAAS,sBAAA,GAAyB,gBAAA;EAwBrC,OAAA,CAAA,GAAW,OAAA;EA4BX,MAAA,CAAA,GAAU,OAAA;EAmBhB,eAAA,CAAgB,iBAAA;EAkBhB,eAAA,CAAgB,iBAAA;EAkBwC;;;;;;;EAAxD,YAAA,CAAa,eAAA,UAAyB,KAAA,EAAO,UAAA,CAAW,WAAA;AAAA;;;;;;;;;;;;iBAiC1C,qBAAA,CACd,MAAA,EAAQ,qBAAA,GACP,gBAAgB"}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { Transport } from "@kyneta/transport";
|
|
2
|
-
import { applyInboundAliasing, applyOutboundAliasing, decodeWireMessage, emptyAliasState, encodeWireMessage } from "@kyneta/wire";
|
|
1
|
+
import { Pipeline, Transport } from "@kyneta/transport";
|
|
3
2
|
//#region src/bridge.ts
|
|
4
3
|
/**
|
|
5
4
|
* In-process byte router connecting multiple `BridgeTransport`s,
|
|
6
5
|
* keyed by each transport's unique `transportId`.
|
|
7
6
|
*
|
|
8
|
-
* Channel-level sends
|
|
9
|
-
*
|
|
7
|
+
* Channel-level sends route through per-channel `Pipeline<"binary">`
|
|
8
|
+
* instances and call `routeBytes`.
|
|
10
9
|
*/
|
|
11
10
|
var Bridge = class {
|
|
12
11
|
transports = /* @__PURE__ */ new Map();
|
|
@@ -34,9 +33,9 @@ var Bridge = class {
|
|
|
34
33
|
}
|
|
35
34
|
};
|
|
36
35
|
/**
|
|
37
|
-
* In-memory transport that runs the
|
|
38
|
-
* Tests that use this
|
|
39
|
-
* production transports.
|
|
36
|
+
* In-memory transport that runs the full wire pipeline end-to-end
|
|
37
|
+
* via per-channel `Pipeline<"binary">` instances. Tests that use this
|
|
38
|
+
* transport exercise the same wire path as production transports.
|
|
40
39
|
*
|
|
41
40
|
* @example
|
|
42
41
|
* ```typescript
|
|
@@ -50,7 +49,7 @@ var BridgeTransport = class extends Transport {
|
|
|
50
49
|
bridge;
|
|
51
50
|
channelToAdapter = /* @__PURE__ */ new Map();
|
|
52
51
|
adapterToChannel = /* @__PURE__ */ new Map();
|
|
53
|
-
|
|
52
|
+
pipelineByChannel = /* @__PURE__ */ new Map();
|
|
54
53
|
constructor({ transportId, transportType, bridge }) {
|
|
55
54
|
super({
|
|
56
55
|
transportType: transportType ?? "bridge",
|
|
@@ -64,10 +63,9 @@ var BridgeTransport = class extends Transport {
|
|
|
64
63
|
send: (msg) => {
|
|
65
64
|
const channelId = this.adapterToChannel.get(context.targetTransportId);
|
|
66
65
|
if (channelId === void 0) return;
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
this.bridge.routeBytes(this.transportId, context.targetTransportId, bytes);
|
|
66
|
+
const pipeline = this.pipelineByChannel.get(channelId);
|
|
67
|
+
if (!pipeline) return;
|
|
68
|
+
for (const r of pipeline.send(msg)) if (r.ok) this.bridge.routeBytes(this.transportId, context.targetTransportId, r.value);
|
|
71
69
|
},
|
|
72
70
|
stop: () => {}
|
|
73
71
|
};
|
|
@@ -81,10 +79,9 @@ var BridgeTransport = class extends Transport {
|
|
|
81
79
|
async onStop() {
|
|
82
80
|
for (const [transportId, adapter] of this.bridge.transports) if (transportId !== this.transportId) adapter.removeChannelTo(this.transportId);
|
|
83
81
|
this.bridge.removeTransport(this.transportId);
|
|
84
|
-
for (const channelId of this.channelToAdapter.keys())
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
82
|
+
for (const channelId of this.channelToAdapter.keys()) this.removeChannel(channelId);
|
|
83
|
+
for (const pipeline of this.pipelineByChannel.values()) pipeline.dispose();
|
|
84
|
+
this.pipelineByChannel.clear();
|
|
88
85
|
this.channelToAdapter.clear();
|
|
89
86
|
this.adapterToChannel.clear();
|
|
90
87
|
}
|
|
@@ -93,7 +90,13 @@ var BridgeTransport = class extends Transport {
|
|
|
93
90
|
const channel = this.addChannel({ targetTransportId });
|
|
94
91
|
this.channelToAdapter.set(channel.channelId, targetTransportId);
|
|
95
92
|
this.adapterToChannel.set(targetTransportId, channel.channelId);
|
|
96
|
-
this.
|
|
93
|
+
this.pipelineByChannel.set(channel.channelId, new Pipeline({
|
|
94
|
+
send: "binary",
|
|
95
|
+
opts: {
|
|
96
|
+
threshold: 100 * 1024,
|
|
97
|
+
onError: (e, dir) => console.warn(`[BridgeTransport] wire error (${dir}):`, e)
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
97
100
|
}
|
|
98
101
|
removeChannelTo(targetTransportId) {
|
|
99
102
|
const channelId = this.adapterToChannel.get(targetTransportId);
|
|
@@ -101,32 +104,30 @@ var BridgeTransport = class extends Transport {
|
|
|
101
104
|
this.removeChannel(channelId);
|
|
102
105
|
this.channelToAdapter.delete(channelId);
|
|
103
106
|
this.adapterToChannel.delete(targetTransportId);
|
|
104
|
-
this.
|
|
107
|
+
this.pipelineByChannel.get(channelId)?.dispose();
|
|
108
|
+
this.pipelineByChannel.delete(channelId);
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
/**
|
|
108
112
|
* Deliver encoded bytes to the appropriate channel.
|
|
109
113
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* via `queueMicrotask()`.
|
|
114
|
+
* Routes through the per-channel `Pipeline.receive()` which handles
|
|
115
|
+
* decoding, deframing, reassembly, and alias resolution. Delivers
|
|
116
|
+
* each resolved message asynchronously via `queueMicrotask()`.
|
|
113
117
|
*/
|
|
114
118
|
deliverBytes(fromTransportId, bytes) {
|
|
115
119
|
const channelId = this.adapterToChannel.get(fromTransportId);
|
|
116
120
|
if (channelId === void 0) return;
|
|
117
121
|
const channel = this.channels.get(channelId);
|
|
118
122
|
if (!channel) return;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
const pipeline = this.pipelineByChannel.get(channelId);
|
|
124
|
+
if (!pipeline) return;
|
|
125
|
+
for (const r of pipeline.receive(bytes)) if (r.ok) {
|
|
126
|
+
const msg = r.value;
|
|
127
|
+
queueMicrotask(() => {
|
|
128
|
+
channel.onReceive(msg);
|
|
129
|
+
});
|
|
125
130
|
}
|
|
126
|
-
const msg = result.msg;
|
|
127
|
-
queueMicrotask(() => {
|
|
128
|
-
channel.onReceive(msg);
|
|
129
|
-
});
|
|
130
131
|
}
|
|
131
132
|
};
|
|
132
133
|
/**
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/bridge.ts"],"sourcesContent":["// bridge-adapter — in-process transport with alias-aware delivery.\n//\n// BridgeTransport is a real transport that runs the alias-aware pipeline\n// end-to-end and applies the docId/schemaHash alias transformer at the\n// channel send/receive boundary — exactly like every other binary\n// transport. Async delivery is preserved via `queueMicrotask()` to keep\n// test behavior representative of real network adapters.\n//\n// Usage:\n// const bridge = new Bridge()\n// const exchangeA = new Exchange({\n// transports: [createBridgeTransport({ transportId: \"peer-a\", bridge })],\n// })\n\nimport type {\n ChannelId,\n GeneratedChannel,\n TransportFactory,\n} from \"@kyneta/transport\"\nimport { Transport } from \"@kyneta/transport\"\nimport {\n type AliasState,\n applyInboundAliasing,\n applyOutboundAliasing,\n decodeWireMessage,\n emptyAliasState,\n encodeWireMessage,\n} from \"@kyneta/wire\"\n\n// ---------------------------------------------------------------------------\n// Bridge — message router connecting multiple BridgeTransports in-process\n// ---------------------------------------------------------------------------\n\n/**\n * In-process byte router connecting multiple `BridgeTransport`s,\n * keyed by each transport's unique `transportId`.\n *\n * Channel-level sends produce alias-aware encoded bytes via the wire-layer\n * transformer and call `routeBytes`.\n */\nexport class Bridge {\n readonly transports = new Map<string, BridgeTransport>()\n\n addTransport(transport: BridgeTransport): void {\n if (!transport.transportId) {\n throw new Error(\"can't add transport without transport id\")\n }\n this.transports.set(transport.transportId, transport)\n }\n\n removeTransport(transportId: string): void {\n this.transports.delete(transportId)\n }\n\n /**\n * Route already-encoded bytes from one transport to another. The\n * receiving transport's `deliverBytes` is responsible for decoding\n * and applying the inbound alias transformer.\n *\n * Used by `BridgeTransport`'s channel send path.\n */\n routeBytes(\n fromTransportId: string,\n toTransportId: string,\n bytes: Uint8Array,\n ): void {\n const toTransport = this.transports.get(toTransportId)\n if (!toTransport) return\n toTransport.deliverBytes(fromTransportId, bytes)\n }\n\n get transportIds(): Set<string> {\n return new Set(this.transports.keys())\n }\n}\n\n// ---------------------------------------------------------------------------\n// BridgeTransport — in-process network adapter for testing\n// ---------------------------------------------------------------------------\n\ntype BridgeTransportContext = {\n targetTransportId: string\n}\n\nexport type BridgeTransportParams = {\n /** Unique identifier for this transport instance (e.g. \"peer-a\", \"server\"). */\n transportId: string\n /**\n * Transport type category. Defaults to \"bridge\".\n * Stored in ChannelMeta for informational purposes.\n */\n transportType?: string\n bridge: Bridge\n}\n\n/**\n * In-memory transport that runs the alias-aware pipeline end-to-end.\n * Tests that use this transport exercise the same wire path as\n * production transports.\n *\n * @example\n * ```typescript\n * const bridge = new Bridge()\n * const exchangeA = new Exchange({\n * transports: [createBridgeTransport({ transportId: \"peer-a\", bridge })],\n * })\n * ```\n */\nexport class BridgeTransport extends Transport<BridgeTransportContext> {\n readonly bridge: Bridge\n\n // Track which remote transport each channel connects to.\n private channelToAdapter = new Map<ChannelId, string>()\n private adapterToChannel = new Map<string, ChannelId>()\n\n // Per-channel alias state. Created with the channel; lives until removal.\n // Keyed by channelId.\n private aliasStateByChannel = new Map<ChannelId, AliasState>()\n\n constructor({ transportId, transportType, bridge }: BridgeTransportParams) {\n super({ transportType: transportType ?? \"bridge\", transportId })\n this.bridge = bridge\n }\n\n generate(context: BridgeTransportContext): GeneratedChannel {\n return {\n transportType: this.transportType,\n send: msg => {\n const channelId = this.adapterToChannel.get(context.targetTransportId)\n if (channelId === undefined) return\n\n const state =\n this.aliasStateByChannel.get(channelId) ?? emptyAliasState()\n const { state: nextState, wire } = applyOutboundAliasing(state, msg)\n this.aliasStateByChannel.set(channelId, nextState)\n\n const bytes = encodeWireMessage(wire)\n this.bridge.routeBytes(\n this.transportId,\n context.targetTransportId,\n bytes,\n )\n },\n stop: () => {\n // Cleanup handled by removeChannel.\n },\n }\n }\n\n async onStart(): Promise<void> {\n this.bridge.addTransport(this)\n\n // Phase 1: Create all channels (no establishment yet).\n for (const [transportId, adapter] of this.bridge.transports) {\n if (transportId !== this.transportId) {\n adapter.createChannelTo(this.transportId)\n }\n }\n for (const transportId of this.bridge.transports.keys()) {\n if (transportId !== this.transportId) {\n this.createChannelTo(transportId)\n }\n }\n\n // Phase 2: Only the joining transport initiates establishment.\n for (const channelId of this.adapterToChannel.values()) {\n this.establishChannel(channelId)\n }\n }\n\n async onStop(): Promise<void> {\n for (const [transportId, adapter] of this.bridge.transports) {\n if (transportId !== this.transportId) {\n adapter.removeChannelTo(this.transportId)\n }\n }\n this.bridge.removeTransport(this.transportId)\n\n for (const channelId of this.channelToAdapter.keys()) {\n this.removeChannel(channelId)\n this.aliasStateByChannel.delete(channelId)\n }\n this.channelToAdapter.clear()\n this.adapterToChannel.clear()\n }\n\n createChannelTo(targetTransportId: string): void {\n if (this.adapterToChannel.has(targetTransportId)) return\n const channel = this.addChannel({ targetTransportId })\n this.channelToAdapter.set(channel.channelId, targetTransportId)\n this.adapterToChannel.set(targetTransportId, channel.channelId)\n this.aliasStateByChannel.set(channel.channelId, emptyAliasState())\n }\n\n removeChannelTo(targetTransportId: string): void {\n const channelId = this.adapterToChannel.get(targetTransportId)\n if (channelId !== undefined) {\n this.removeChannel(channelId)\n this.channelToAdapter.delete(channelId)\n this.adapterToChannel.delete(targetTransportId)\n this.aliasStateByChannel.delete(channelId)\n }\n }\n\n /**\n * Deliver encoded bytes to the appropriate channel.\n *\n * Decodes via `decodeWireMessage`, applies the inbound alias\n * transformer, and delivers each resolved message asynchronously\n * via `queueMicrotask()`.\n */\n deliverBytes(fromTransportId: string, bytes: Uint8Array): void {\n const channelId = this.adapterToChannel.get(fromTransportId)\n if (channelId === undefined) return\n\n const channel = this.channels.get(channelId)\n if (!channel) return\n\n const wire = decodeWireMessage(bytes)\n const state = this.aliasStateByChannel.get(channelId) ?? emptyAliasState()\n const result = applyInboundAliasing(state, wire)\n this.aliasStateByChannel.set(channelId, result.state)\n\n if (result.error || !result.msg) {\n console.warn(\"[BridgeTransport] alias resolution failed:\", result.error)\n return\n }\n const msg = result.msg\n queueMicrotask(() => {\n channel.onReceive(msg)\n })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory function\n// ---------------------------------------------------------------------------\n\n/**\n * Create a BridgeTransport factory for in-process testing.\n *\n * @example\n * ```typescript\n * const bridge = new Bridge()\n * const exchangeA = new Exchange({\n * transports: [createBridgeTransport({ transportId: \"peer-a\", bridge })],\n * })\n * ```\n */\nexport function createBridgeTransport(\n params: BridgeTransportParams,\n): TransportFactory {\n return () => new BridgeTransport(params)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/bridge.ts"],"sourcesContent":["// bridge-adapter — in-process transport with Pipeline-based delivery.\n//\n// BridgeTransport is a real transport that runs the full wire pipeline\n// (aliasing, framing, fragmentation) end-to-end via per-channel\n// Pipeline<\"binary\"> instances — exactly like every other binary\n// transport. Async delivery is preserved via `queueMicrotask()` to keep\n// test behavior representative of real network adapters.\n//\n// Usage:\n// const bridge = new Bridge()\n// const exchangeA = new Exchange({\n// transports: [createBridgeTransport({ transportId: \"peer-a\", bridge })],\n// })\n\nimport type {\n ChannelId,\n GeneratedChannel,\n TransportFactory,\n} from \"@kyneta/transport\"\nimport { Pipeline, Transport } from \"@kyneta/transport\"\n\n// ---------------------------------------------------------------------------\n// Bridge — message router connecting multiple BridgeTransports in-process\n// ---------------------------------------------------------------------------\n\n/**\n * In-process byte router connecting multiple `BridgeTransport`s,\n * keyed by each transport's unique `transportId`.\n *\n * Channel-level sends route through per-channel `Pipeline<\"binary\">`\n * instances and call `routeBytes`.\n */\nexport class Bridge {\n readonly transports = new Map<string, BridgeTransport>()\n\n addTransport(transport: BridgeTransport): void {\n if (!transport.transportId) {\n throw new Error(\"can't add transport without transport id\")\n }\n this.transports.set(transport.transportId, transport)\n }\n\n removeTransport(transportId: string): void {\n this.transports.delete(transportId)\n }\n\n /**\n * Route already-encoded bytes from one transport to another. The\n * receiving transport's `deliverBytes` is responsible for decoding\n * and applying the inbound alias transformer.\n *\n * Used by `BridgeTransport`'s channel send path.\n */\n routeBytes(\n fromTransportId: string,\n toTransportId: string,\n bytes: Uint8Array<ArrayBuffer>,\n ): void {\n const toTransport = this.transports.get(toTransportId)\n if (!toTransport) return\n toTransport.deliverBytes(fromTransportId, bytes)\n }\n\n get transportIds(): Set<string> {\n return new Set(this.transports.keys())\n }\n}\n\n// ---------------------------------------------------------------------------\n// BridgeTransport — in-process network adapter for testing\n// ---------------------------------------------------------------------------\n\ntype BridgeTransportContext = {\n targetTransportId: string\n}\n\nexport type BridgeTransportParams = {\n /** Unique identifier for this transport instance (e.g. \"peer-a\", \"server\"). */\n transportId: string\n /**\n * Transport type category. Defaults to \"bridge\".\n * Stored in ChannelMeta for informational purposes.\n */\n transportType?: string\n bridge: Bridge\n}\n\n/**\n * In-memory transport that runs the full wire pipeline end-to-end\n * via per-channel `Pipeline<\"binary\">` instances. Tests that use this\n * transport exercise the same wire path as production transports.\n *\n * @example\n * ```typescript\n * const bridge = new Bridge()\n * const exchangeA = new Exchange({\n * transports: [createBridgeTransport({ transportId: \"peer-a\", bridge })],\n * })\n * ```\n */\nexport class BridgeTransport extends Transport<BridgeTransportContext> {\n readonly bridge: Bridge\n\n // Track which remote transport each channel connects to.\n private channelToAdapter = new Map<ChannelId, string>()\n private adapterToChannel = new Map<string, ChannelId>()\n\n // Per-channel pipeline. Created with the channel; lives until removal.\n // Keyed by channelId.\n private pipelineByChannel = new Map<ChannelId, Pipeline<\"binary\">>()\n\n constructor({ transportId, transportType, bridge }: BridgeTransportParams) {\n super({ transportType: transportType ?? \"bridge\", transportId })\n this.bridge = bridge\n }\n\n generate(context: BridgeTransportContext): GeneratedChannel {\n return {\n transportType: this.transportType,\n send: msg => {\n const channelId = this.adapterToChannel.get(context.targetTransportId)\n if (channelId === undefined) return\n const pipeline = this.pipelineByChannel.get(channelId)\n if (!pipeline) return\n for (const r of pipeline.send(msg)) {\n if (r.ok) {\n this.bridge.routeBytes(\n this.transportId,\n context.targetTransportId,\n r.value,\n )\n }\n }\n },\n stop: () => {\n // Cleanup handled by removeChannel.\n },\n }\n }\n\n async onStart(): Promise<void> {\n this.bridge.addTransport(this)\n\n // Phase 1: create channels on both sides (no establish yet).\n // Doing remote-side and local-side creation separately ensures both\n // peers' `adapterToChannel` maps are populated before the joining\n // side initiates the handshake — otherwise the joining side's\n // establish message would arrive at a remote that hasn't routed\n // bytes back yet.\n for (const [transportId, adapter] of this.bridge.transports) {\n if (transportId !== this.transportId) {\n adapter.createChannelTo(this.transportId)\n }\n }\n for (const transportId of this.bridge.transports.keys()) {\n if (transportId !== this.transportId) {\n this.createChannelTo(transportId)\n }\n }\n\n // Phase 2: only the joining transport initiates establish. The\n // already-started side learns the joining peer's identity from\n // the establish handshake it echoes back.\n for (const channelId of this.adapterToChannel.values()) {\n this.establishChannel(channelId)\n }\n }\n\n async onStop(): Promise<void> {\n for (const [transportId, adapter] of this.bridge.transports) {\n if (transportId !== this.transportId) {\n adapter.removeChannelTo(this.transportId)\n }\n }\n this.bridge.removeTransport(this.transportId)\n\n for (const channelId of this.channelToAdapter.keys()) {\n this.removeChannel(channelId)\n }\n for (const pipeline of this.pipelineByChannel.values()) {\n pipeline.dispose()\n }\n this.pipelineByChannel.clear()\n this.channelToAdapter.clear()\n this.adapterToChannel.clear()\n }\n\n createChannelTo(targetTransportId: string): void {\n if (this.adapterToChannel.has(targetTransportId)) return\n const channel = this.addChannel({ targetTransportId })\n this.channelToAdapter.set(channel.channelId, targetTransportId)\n this.adapterToChannel.set(targetTransportId, channel.channelId)\n this.pipelineByChannel.set(\n channel.channelId,\n new Pipeline({\n send: \"binary\",\n opts: {\n threshold: 100 * 1024,\n onError: (e, dir) =>\n console.warn(`[BridgeTransport] wire error (${dir}):`, e),\n },\n }),\n )\n }\n\n removeChannelTo(targetTransportId: string): void {\n const channelId = this.adapterToChannel.get(targetTransportId)\n if (channelId !== undefined) {\n this.removeChannel(channelId)\n this.channelToAdapter.delete(channelId)\n this.adapterToChannel.delete(targetTransportId)\n this.pipelineByChannel.get(channelId)?.dispose()\n this.pipelineByChannel.delete(channelId)\n }\n }\n\n /**\n * Deliver encoded bytes to the appropriate channel.\n *\n * Routes through the per-channel `Pipeline.receive()` which handles\n * decoding, deframing, reassembly, and alias resolution. Delivers\n * each resolved message asynchronously via `queueMicrotask()`.\n */\n deliverBytes(fromTransportId: string, bytes: Uint8Array<ArrayBuffer>): void {\n const channelId = this.adapterToChannel.get(fromTransportId)\n if (channelId === undefined) return\n const channel = this.channels.get(channelId)\n if (!channel) return\n const pipeline = this.pipelineByChannel.get(channelId)\n if (!pipeline) return\n for (const r of pipeline.receive(bytes)) {\n if (r.ok) {\n const msg = r.value\n queueMicrotask(() => {\n channel.onReceive(msg)\n })\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory function\n// ---------------------------------------------------------------------------\n\n/**\n * Create a BridgeTransport factory for in-process testing.\n *\n * @example\n * ```typescript\n * const bridge = new Bridge()\n * const exchangeA = new Exchange({\n * transports: [createBridgeTransport({ transportId: \"peer-a\", bridge })],\n * })\n * ```\n */\nexport function createBridgeTransport(\n params: BridgeTransportParams,\n): TransportFactory {\n return () => new BridgeTransport(params)\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyneta/bridge-transport",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "In-process transport for @kyneta/exchange — codec-faithful + alias-aware delivery for testing multi-peer scenarios in a single process",
|
|
5
5
|
"author": "Duane Johnson",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,20 +21,18 @@
|
|
|
21
21
|
".": {
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
23
|
"import": "./dist/index.js"
|
|
24
|
-
}
|
|
25
|
-
"./src/*": "./src/*"
|
|
24
|
+
}
|
|
26
25
|
},
|
|
27
26
|
"peerDependencies": {
|
|
28
|
-
"@kyneta/transport": "^1.
|
|
29
|
-
"@kyneta/wire": "^1.6.0"
|
|
27
|
+
"@kyneta/transport": "^1.7.0"
|
|
30
28
|
},
|
|
31
29
|
"devDependencies": {
|
|
32
30
|
"tsdown": "^0.22.0",
|
|
33
31
|
"typescript": "^5.9.2",
|
|
34
32
|
"vitest": "^4.0.17",
|
|
35
|
-
"@kyneta/
|
|
36
|
-
"@kyneta/
|
|
37
|
-
"@kyneta/
|
|
33
|
+
"@kyneta/transport": "^1.7.0",
|
|
34
|
+
"@kyneta/schema": "^1.7.0",
|
|
35
|
+
"@kyneta/wire": "^1.7.0"
|
|
38
36
|
},
|
|
39
37
|
"scripts": {
|
|
40
38
|
"build": "tsdown",
|
|
@@ -1,24 +1,15 @@
|
|
|
1
|
-
// BridgeTransport
|
|
1
|
+
// BridgeTransport integration tests — lifecycle, channel handshake,
|
|
2
|
+
// and fragmentation through the wire pipeline.
|
|
2
3
|
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
4
|
+
import type { ChannelMsg, OfferMsg } from "@kyneta/transport"
|
|
5
|
+
import { createTestTransportContext } from "@kyneta/transport/testing"
|
|
6
|
+
import { describe, expect, it } from "vitest"
|
|
5
7
|
import { Bridge, BridgeTransport } from "../bridge.js"
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
overrides: Partial<TransportContext> = {},
|
|
9
|
-
): TransportContext {
|
|
10
|
-
return {
|
|
11
|
-
identity: { peerId: "test-peer", type: "user" },
|
|
12
|
-
onChannelReceive: vi.fn(),
|
|
13
|
-
onChannelAdded: vi.fn(),
|
|
14
|
-
onChannelRemoved: vi.fn(),
|
|
15
|
-
onChannelEstablish: vi.fn(),
|
|
16
|
-
...overrides,
|
|
17
|
-
}
|
|
18
|
-
}
|
|
9
|
+
const createTransportContext = createTestTransportContext
|
|
19
10
|
|
|
20
11
|
describe("BridgeTransport", () => {
|
|
21
|
-
it("two adapters
|
|
12
|
+
it("two adapters register in a shared Bridge", async () => {
|
|
22
13
|
const bridge = new Bridge()
|
|
23
14
|
|
|
24
15
|
const ctxA = createTransportContext({
|
|
@@ -31,9 +22,9 @@ describe("BridgeTransport", () => {
|
|
|
31
22
|
const adapterA = new BridgeTransport({ transportId: "peer-a", bridge })
|
|
32
23
|
const adapterB = new BridgeTransport({ transportId: "peer-b", bridge })
|
|
33
24
|
|
|
34
|
-
adapterA._initialize(ctxA)
|
|
25
|
+
await adapterA._initialize(ctxA)
|
|
35
26
|
await adapterA._start()
|
|
36
|
-
adapterB._initialize(ctxB)
|
|
27
|
+
await adapterB._initialize(ctxB)
|
|
37
28
|
await adapterB._start()
|
|
38
29
|
|
|
39
30
|
expect(bridge.transports.size).toBe(2)
|
|
@@ -43,7 +34,7 @@ describe("BridgeTransport", () => {
|
|
|
43
34
|
const bridge = new Bridge()
|
|
44
35
|
|
|
45
36
|
const adapterA = new BridgeTransport({ transportId: "peer-a", bridge })
|
|
46
|
-
adapterA._initialize(createTransportContext())
|
|
37
|
+
await adapterA._initialize(createTransportContext())
|
|
47
38
|
await adapterA._start()
|
|
48
39
|
|
|
49
40
|
expect(bridge.transports.size).toBe(1)
|
|
@@ -60,7 +51,7 @@ describe("BridgeTransport", () => {
|
|
|
60
51
|
const adapterA = new BridgeTransport({ transportId: "peer-a", bridge })
|
|
61
52
|
const adapterB = new BridgeTransport({ transportId: "peer-b", bridge })
|
|
62
53
|
|
|
63
|
-
adapterA._initialize(
|
|
54
|
+
await adapterA._initialize(
|
|
64
55
|
createTransportContext({
|
|
65
56
|
identity: { peerId: "peer-a", type: "user" },
|
|
66
57
|
onChannelEstablish: channel =>
|
|
@@ -69,7 +60,7 @@ describe("BridgeTransport", () => {
|
|
|
69
60
|
)
|
|
70
61
|
await adapterA._start()
|
|
71
62
|
|
|
72
|
-
adapterB._initialize(
|
|
63
|
+
await adapterB._initialize(
|
|
73
64
|
createTransportContext({
|
|
74
65
|
identity: { peerId: "peer-b", type: "user" },
|
|
75
66
|
onChannelEstablish: channel =>
|
|
@@ -81,24 +72,72 @@ describe("BridgeTransport", () => {
|
|
|
81
72
|
expect(establishedChannels.length).toBeGreaterThan(0)
|
|
82
73
|
})
|
|
83
74
|
|
|
84
|
-
it("
|
|
75
|
+
it("fragments and reassembles large messages through the bridge", async () => {
|
|
85
76
|
const bridge = new Bridge()
|
|
86
77
|
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
// 200KB payload — exceeds the 100KB fragmentation threshold
|
|
79
|
+
const largeData = new Uint8Array(200 * 1024)
|
|
80
|
+
for (let i = 0; i < largeData.length; i++) {
|
|
81
|
+
largeData[i] = i % 256
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const offer: OfferMsg = {
|
|
85
|
+
type: "offer",
|
|
86
|
+
docId: "doc-large",
|
|
87
|
+
payload: { kind: "entirety", encoding: "binary", data: largeData },
|
|
88
|
+
version: "1",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const received: ChannelMsg[] = []
|
|
92
|
+
let resolveReceived!: () => void
|
|
93
|
+
const receivedPromise = new Promise<void>(resolve => {
|
|
94
|
+
resolveReceived = resolve
|
|
89
95
|
})
|
|
96
|
+
|
|
90
97
|
const ctxB = createTransportContext({
|
|
91
98
|
identity: { peerId: "peer-b", type: "user" },
|
|
99
|
+
onChannelReceive: (_channelId, message) => {
|
|
100
|
+
received.push(message)
|
|
101
|
+
resolveReceived()
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const ctxA = createTransportContext({
|
|
106
|
+
identity: { peerId: "peer-a", type: "user" },
|
|
92
107
|
})
|
|
93
108
|
|
|
94
109
|
const adapterA = new BridgeTransport({ transportId: "peer-a", bridge })
|
|
95
110
|
const adapterB = new BridgeTransport({ transportId: "peer-b", bridge })
|
|
96
111
|
|
|
97
|
-
adapterA._initialize(ctxA)
|
|
112
|
+
await adapterA._initialize(ctxA)
|
|
98
113
|
await adapterA._start()
|
|
99
|
-
adapterB._initialize(ctxB)
|
|
114
|
+
await adapterB._initialize(ctxB)
|
|
100
115
|
await adapterB._start()
|
|
101
116
|
|
|
102
|
-
|
|
117
|
+
// Get the channelId for A→B
|
|
118
|
+
const channelIds: number[] = []
|
|
119
|
+
for (const ch of adapterA.channels) {
|
|
120
|
+
channelIds.push(ch.channelId)
|
|
121
|
+
}
|
|
122
|
+
// Send the large offer through A's channel to B
|
|
123
|
+
adapterA._send({
|
|
124
|
+
toChannelIds: channelIds,
|
|
125
|
+
message: offer,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
await receivedPromise
|
|
129
|
+
|
|
130
|
+
expect(received).toHaveLength(1)
|
|
131
|
+
const msg = received[0]
|
|
132
|
+
expect(msg).toBeDefined()
|
|
133
|
+
if (msg === undefined) throw new Error("unreachable")
|
|
134
|
+
expect(msg.type).toBe("offer")
|
|
135
|
+
if (msg.type !== "offer") throw new Error("unreachable")
|
|
136
|
+
expect(msg.docId).toBe("doc-large")
|
|
137
|
+
expect(msg.payload.data).toBeInstanceOf(Uint8Array)
|
|
138
|
+
if (!(msg.payload.data instanceof Uint8Array))
|
|
139
|
+
throw new Error("unreachable")
|
|
140
|
+
expect(msg.payload.data.length).toBe(largeData.length)
|
|
141
|
+
expect(new Uint8Array(msg.payload.data)).toEqual(largeData)
|
|
103
142
|
})
|
|
104
143
|
})
|
package/src/bridge.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// bridge-adapter — in-process transport with
|
|
1
|
+
// bridge-adapter — in-process transport with Pipeline-based delivery.
|
|
2
2
|
//
|
|
3
|
-
// BridgeTransport is a real transport that runs the
|
|
4
|
-
// end-to-end
|
|
5
|
-
//
|
|
3
|
+
// BridgeTransport is a real transport that runs the full wire pipeline
|
|
4
|
+
// (aliasing, framing, fragmentation) end-to-end via per-channel
|
|
5
|
+
// Pipeline<"binary"> instances — exactly like every other binary
|
|
6
6
|
// transport. Async delivery is preserved via `queueMicrotask()` to keep
|
|
7
7
|
// test behavior representative of real network adapters.
|
|
8
8
|
//
|
|
@@ -17,15 +17,7 @@ import type {
|
|
|
17
17
|
GeneratedChannel,
|
|
18
18
|
TransportFactory,
|
|
19
19
|
} from "@kyneta/transport"
|
|
20
|
-
import { Transport } from "@kyneta/transport"
|
|
21
|
-
import {
|
|
22
|
-
type AliasState,
|
|
23
|
-
applyInboundAliasing,
|
|
24
|
-
applyOutboundAliasing,
|
|
25
|
-
decodeWireMessage,
|
|
26
|
-
emptyAliasState,
|
|
27
|
-
encodeWireMessage,
|
|
28
|
-
} from "@kyneta/wire"
|
|
20
|
+
import { Pipeline, Transport } from "@kyneta/transport"
|
|
29
21
|
|
|
30
22
|
// ---------------------------------------------------------------------------
|
|
31
23
|
// Bridge — message router connecting multiple BridgeTransports in-process
|
|
@@ -35,8 +27,8 @@ import {
|
|
|
35
27
|
* In-process byte router connecting multiple `BridgeTransport`s,
|
|
36
28
|
* keyed by each transport's unique `transportId`.
|
|
37
29
|
*
|
|
38
|
-
* Channel-level sends
|
|
39
|
-
*
|
|
30
|
+
* Channel-level sends route through per-channel `Pipeline<"binary">`
|
|
31
|
+
* instances and call `routeBytes`.
|
|
40
32
|
*/
|
|
41
33
|
export class Bridge {
|
|
42
34
|
readonly transports = new Map<string, BridgeTransport>()
|
|
@@ -62,7 +54,7 @@ export class Bridge {
|
|
|
62
54
|
routeBytes(
|
|
63
55
|
fromTransportId: string,
|
|
64
56
|
toTransportId: string,
|
|
65
|
-
bytes: Uint8Array
|
|
57
|
+
bytes: Uint8Array<ArrayBuffer>,
|
|
66
58
|
): void {
|
|
67
59
|
const toTransport = this.transports.get(toTransportId)
|
|
68
60
|
if (!toTransport) return
|
|
@@ -94,9 +86,9 @@ export type BridgeTransportParams = {
|
|
|
94
86
|
}
|
|
95
87
|
|
|
96
88
|
/**
|
|
97
|
-
* In-memory transport that runs the
|
|
98
|
-
* Tests that use this
|
|
99
|
-
* production transports.
|
|
89
|
+
* In-memory transport that runs the full wire pipeline end-to-end
|
|
90
|
+
* via per-channel `Pipeline<"binary">` instances. Tests that use this
|
|
91
|
+
* transport exercise the same wire path as production transports.
|
|
100
92
|
*
|
|
101
93
|
* @example
|
|
102
94
|
* ```typescript
|
|
@@ -113,9 +105,9 @@ export class BridgeTransport extends Transport<BridgeTransportContext> {
|
|
|
113
105
|
private channelToAdapter = new Map<ChannelId, string>()
|
|
114
106
|
private adapterToChannel = new Map<string, ChannelId>()
|
|
115
107
|
|
|
116
|
-
// Per-channel
|
|
108
|
+
// Per-channel pipeline. Created with the channel; lives until removal.
|
|
117
109
|
// Keyed by channelId.
|
|
118
|
-
private
|
|
110
|
+
private pipelineByChannel = new Map<ChannelId, Pipeline<"binary">>()
|
|
119
111
|
|
|
120
112
|
constructor({ transportId, transportType, bridge }: BridgeTransportParams) {
|
|
121
113
|
super({ transportType: transportType ?? "bridge", transportId })
|
|
@@ -128,18 +120,17 @@ export class BridgeTransport extends Transport<BridgeTransportContext> {
|
|
|
128
120
|
send: msg => {
|
|
129
121
|
const channelId = this.adapterToChannel.get(context.targetTransportId)
|
|
130
122
|
if (channelId === undefined) return
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
123
|
+
const pipeline = this.pipelineByChannel.get(channelId)
|
|
124
|
+
if (!pipeline) return
|
|
125
|
+
for (const r of pipeline.send(msg)) {
|
|
126
|
+
if (r.ok) {
|
|
127
|
+
this.bridge.routeBytes(
|
|
128
|
+
this.transportId,
|
|
129
|
+
context.targetTransportId,
|
|
130
|
+
r.value,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
143
134
|
},
|
|
144
135
|
stop: () => {
|
|
145
136
|
// Cleanup handled by removeChannel.
|
|
@@ -150,7 +141,12 @@ export class BridgeTransport extends Transport<BridgeTransportContext> {
|
|
|
150
141
|
async onStart(): Promise<void> {
|
|
151
142
|
this.bridge.addTransport(this)
|
|
152
143
|
|
|
153
|
-
// Phase 1:
|
|
144
|
+
// Phase 1: create channels on both sides (no establish yet).
|
|
145
|
+
// Doing remote-side and local-side creation separately ensures both
|
|
146
|
+
// peers' `adapterToChannel` maps are populated before the joining
|
|
147
|
+
// side initiates the handshake — otherwise the joining side's
|
|
148
|
+
// establish message would arrive at a remote that hasn't routed
|
|
149
|
+
// bytes back yet.
|
|
154
150
|
for (const [transportId, adapter] of this.bridge.transports) {
|
|
155
151
|
if (transportId !== this.transportId) {
|
|
156
152
|
adapter.createChannelTo(this.transportId)
|
|
@@ -162,7 +158,9 @@ export class BridgeTransport extends Transport<BridgeTransportContext> {
|
|
|
162
158
|
}
|
|
163
159
|
}
|
|
164
160
|
|
|
165
|
-
// Phase 2:
|
|
161
|
+
// Phase 2: only the joining transport initiates establish. The
|
|
162
|
+
// already-started side learns the joining peer's identity from
|
|
163
|
+
// the establish handshake it echoes back.
|
|
166
164
|
for (const channelId of this.adapterToChannel.values()) {
|
|
167
165
|
this.establishChannel(channelId)
|
|
168
166
|
}
|
|
@@ -178,8 +176,11 @@ export class BridgeTransport extends Transport<BridgeTransportContext> {
|
|
|
178
176
|
|
|
179
177
|
for (const channelId of this.channelToAdapter.keys()) {
|
|
180
178
|
this.removeChannel(channelId)
|
|
181
|
-
this.aliasStateByChannel.delete(channelId)
|
|
182
179
|
}
|
|
180
|
+
for (const pipeline of this.pipelineByChannel.values()) {
|
|
181
|
+
pipeline.dispose()
|
|
182
|
+
}
|
|
183
|
+
this.pipelineByChannel.clear()
|
|
183
184
|
this.channelToAdapter.clear()
|
|
184
185
|
this.adapterToChannel.clear()
|
|
185
186
|
}
|
|
@@ -189,7 +190,17 @@ export class BridgeTransport extends Transport<BridgeTransportContext> {
|
|
|
189
190
|
const channel = this.addChannel({ targetTransportId })
|
|
190
191
|
this.channelToAdapter.set(channel.channelId, targetTransportId)
|
|
191
192
|
this.adapterToChannel.set(targetTransportId, channel.channelId)
|
|
192
|
-
this.
|
|
193
|
+
this.pipelineByChannel.set(
|
|
194
|
+
channel.channelId,
|
|
195
|
+
new Pipeline({
|
|
196
|
+
send: "binary",
|
|
197
|
+
opts: {
|
|
198
|
+
threshold: 100 * 1024,
|
|
199
|
+
onError: (e, dir) =>
|
|
200
|
+
console.warn(`[BridgeTransport] wire error (${dir}):`, e),
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
)
|
|
193
204
|
}
|
|
194
205
|
|
|
195
206
|
removeChannelTo(targetTransportId: string): void {
|
|
@@ -198,37 +209,33 @@ export class BridgeTransport extends Transport<BridgeTransportContext> {
|
|
|
198
209
|
this.removeChannel(channelId)
|
|
199
210
|
this.channelToAdapter.delete(channelId)
|
|
200
211
|
this.adapterToChannel.delete(targetTransportId)
|
|
201
|
-
this.
|
|
212
|
+
this.pipelineByChannel.get(channelId)?.dispose()
|
|
213
|
+
this.pipelineByChannel.delete(channelId)
|
|
202
214
|
}
|
|
203
215
|
}
|
|
204
216
|
|
|
205
217
|
/**
|
|
206
218
|
* Deliver encoded bytes to the appropriate channel.
|
|
207
219
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* via `queueMicrotask()`.
|
|
220
|
+
* Routes through the per-channel `Pipeline.receive()` which handles
|
|
221
|
+
* decoding, deframing, reassembly, and alias resolution. Delivers
|
|
222
|
+
* each resolved message asynchronously via `queueMicrotask()`.
|
|
211
223
|
*/
|
|
212
|
-
deliverBytes(fromTransportId: string, bytes: Uint8Array): void {
|
|
224
|
+
deliverBytes(fromTransportId: string, bytes: Uint8Array<ArrayBuffer>): void {
|
|
213
225
|
const channelId = this.adapterToChannel.get(fromTransportId)
|
|
214
226
|
if (channelId === undefined) return
|
|
215
|
-
|
|
216
227
|
const channel = this.channels.get(channelId)
|
|
217
228
|
if (!channel) return
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
const pipeline = this.pipelineByChannel.get(channelId)
|
|
230
|
+
if (!pipeline) return
|
|
231
|
+
for (const r of pipeline.receive(bytes)) {
|
|
232
|
+
if (r.ok) {
|
|
233
|
+
const msg = r.value
|
|
234
|
+
queueMicrotask(() => {
|
|
235
|
+
channel.onReceive(msg)
|
|
236
|
+
})
|
|
237
|
+
}
|
|
227
238
|
}
|
|
228
|
-
const msg = result.msg
|
|
229
|
-
queueMicrotask(() => {
|
|
230
|
-
channel.onReceive(msg)
|
|
231
|
-
})
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
241
|
|