@kyneta/bridge-transport 1.5.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/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
- package/src/__tests__/bridge.test.ts +104 -0
- package/src/bridge.ts +254 -0
- package/src/index.ts +4 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Duane Johnson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @kyneta/bridge-transport
|
|
2
|
+
|
|
3
|
+
In-process transport for testing — alias-aware delivery.
|
|
4
|
+
|
|
5
|
+
`BridgeTransport` is a real transport that runs the production alias transformer
|
|
6
|
+
and `WireMessage` pipeline end-to-end and applies the docId/schemaHash alias
|
|
7
|
+
transformer at the channel send/receive boundary — exactly like every other binary
|
|
8
|
+
transport. Async delivery is preserved via `queueMicrotask()` to keep test behavior
|
|
9
|
+
representative of real network adapters.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Bridge, createBridgeTransport } from "@kyneta/bridge-transport"
|
|
15
|
+
import { Exchange } from "@kyneta/exchange"
|
|
16
|
+
|
|
17
|
+
const bridge = new Bridge()
|
|
18
|
+
|
|
19
|
+
const exchangeA = new Exchange({
|
|
20
|
+
transports: [createBridgeTransport({ transportId: "peer-a", bridge })],
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const exchangeB = new Exchange({
|
|
24
|
+
transports: [createBridgeTransport({ transportId: "peer-b", bridge })],
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Peer Dependencies
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@kyneta/transport": "^1.4.0",
|
|
34
|
+
"@kyneta/wire": "^1.4.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`BridgeTransport` extends the `Transport<G>` base class from
|
|
40
|
+
`@kyneta/transport` and uses the alias transformer + `WireMessage` pipeline from
|
|
41
|
+
`@kyneta/wire` to exercise the production wire path in tests.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { GeneratedChannel, Transport, TransportFactory } from "@kyneta/transport";
|
|
2
|
+
|
|
3
|
+
//#region src/bridge.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* In-process byte router connecting multiple `BridgeTransport`s,
|
|
6
|
+
* keyed by each transport's unique `transportId`.
|
|
7
|
+
*
|
|
8
|
+
* Channel-level sends produce alias-aware encoded bytes via the wire-layer
|
|
9
|
+
* transformer and call `routeBytes`.
|
|
10
|
+
*/
|
|
11
|
+
declare class Bridge {
|
|
12
|
+
readonly transports: Map<string, BridgeTransport>;
|
|
13
|
+
addTransport(transport: BridgeTransport): void;
|
|
14
|
+
removeTransport(transportId: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Route already-encoded bytes from one transport to another. The
|
|
17
|
+
* receiving transport's `deliverBytes` is responsible for decoding
|
|
18
|
+
* and applying the inbound alias transformer.
|
|
19
|
+
*
|
|
20
|
+
* Used by `BridgeTransport`'s channel send path.
|
|
21
|
+
*/
|
|
22
|
+
routeBytes(fromTransportId: string, toTransportId: string, bytes: Uint8Array): void;
|
|
23
|
+
get transportIds(): Set<string>;
|
|
24
|
+
}
|
|
25
|
+
type BridgeTransportContext = {
|
|
26
|
+
targetTransportId: string;
|
|
27
|
+
};
|
|
28
|
+
type BridgeTransportParams = {
|
|
29
|
+
/** Unique identifier for this transport instance (e.g. "peer-a", "server"). */transportId: string;
|
|
30
|
+
/**
|
|
31
|
+
* Transport type category. Defaults to "bridge".
|
|
32
|
+
* Stored in ChannelMeta for informational purposes.
|
|
33
|
+
*/
|
|
34
|
+
transportType?: string;
|
|
35
|
+
bridge: Bridge;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* In-memory transport that runs the alias-aware pipeline end-to-end.
|
|
39
|
+
* Tests that use this transport exercise the same wire path as
|
|
40
|
+
* production transports.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const bridge = new Bridge()
|
|
45
|
+
* const exchangeA = new Exchange({
|
|
46
|
+
* transports: [createBridgeTransport({ transportId: "peer-a", bridge })],
|
|
47
|
+
* })
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
declare class BridgeTransport extends Transport<BridgeTransportContext> {
|
|
51
|
+
readonly bridge: Bridge;
|
|
52
|
+
private channelToAdapter;
|
|
53
|
+
private adapterToChannel;
|
|
54
|
+
private aliasStateByChannel;
|
|
55
|
+
constructor({
|
|
56
|
+
transportId,
|
|
57
|
+
transportType,
|
|
58
|
+
bridge
|
|
59
|
+
}: BridgeTransportParams);
|
|
60
|
+
generate(context: BridgeTransportContext): GeneratedChannel;
|
|
61
|
+
onStart(): Promise<void>;
|
|
62
|
+
onStop(): Promise<void>;
|
|
63
|
+
createChannelTo(targetTransportId: string): void;
|
|
64
|
+
removeChannelTo(targetTransportId: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* Deliver encoded bytes to the appropriate channel.
|
|
67
|
+
*
|
|
68
|
+
* Decodes via `decodeWireMessage`, applies the inbound alias
|
|
69
|
+
* transformer, and delivers each resolved message asynchronously
|
|
70
|
+
* via `queueMicrotask()`.
|
|
71
|
+
*/
|
|
72
|
+
deliverBytes(fromTransportId: string, bytes: Uint8Array): void;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create a BridgeTransport factory for in-process testing.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const bridge = new Bridge()
|
|
80
|
+
* const exchangeA = new Exchange({
|
|
81
|
+
* transports: [createBridgeTransport({ transportId: "peer-a", bridge })],
|
|
82
|
+
* })
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function createBridgeTransport(params: BridgeTransportParams): TransportFactory;
|
|
86
|
+
//#endregion
|
|
87
|
+
export { Bridge, BridgeTransport, type BridgeTransportParams, createBridgeTransport };
|
|
88
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/bridge.ts"],"mappings":";;;;AAwCA;;;;;;cAAa,MAAA;EAAA,SACF,UAAA,EAAU,GAAA,SAAA,eAAA;EAEnB,YAAA,CAAa,SAAA,EAAW,eAAA;EAOxB,eAAA,CAAgB,WAAA;EATP;;;;;;;EAoBT,UAAA,CACE,eAAA,UACA,aAAA,UACA,KAAA,EAAO,UAAA;EAAA,IAOL,YAAA,CAAA,GAAgB,GAAA;AAAA;AAAA,KASjB,sBAAA;EACH,iBAAA;AAAA;AAAA,KAGU,qBAAA;EAbU,+EAepB,WAAA;EAfuB;AAGxB;;;EAiBC,aAAA;EACA,MAAA,EAAQ,MAAA;AAAA;;;;;;;;;;AAgBV;;;;cAAa,eAAA,SAAwB,SAAA,CAAU,sBAAA;EAAA,SACpC,MAAA,EAAQ,MAAA;EAAA,QAGT,gBAAA;EAAA,QACA,gBAAA;EAAA,QAIA,mBAAA;;IAEM,WAAA;IAAa,aAAA;IAAe;EAAA,GAAU,qBAAA;EAKpD,QAAA,CAAS,OAAA,EAAS,sBAAA,GAAyB,gBAAA;EAyBrC,OAAA,CAAA,GAAW,OAAA;EAqBX,MAAA,CAAA,GAAU,OAAA;EAgBhB,eAAA,CAAgB,iBAAA;EAQhB,eAAA,CAAgB,iBAAA;EAtF4B;;;;;;;EAuG5C,YAAA,CAAa,eAAA,UAAyB,KAAA,EAAO,UAAA;AAAA;;;;;;;;;;;;iBAsC/B,qBAAA,CACd,MAAA,EAAQ,qBAAA,GACP,gBAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Transport } from "@kyneta/transport";
|
|
2
|
+
import { applyInboundAliasing, applyOutboundAliasing, decodeWireMessage, emptyAliasState, encodeWireMessage } from "@kyneta/wire";
|
|
3
|
+
//#region src/bridge.ts
|
|
4
|
+
/**
|
|
5
|
+
* In-process byte router connecting multiple `BridgeTransport`s,
|
|
6
|
+
* keyed by each transport's unique `transportId`.
|
|
7
|
+
*
|
|
8
|
+
* Channel-level sends produce alias-aware encoded bytes via the wire-layer
|
|
9
|
+
* transformer and call `routeBytes`.
|
|
10
|
+
*/
|
|
11
|
+
var Bridge = class {
|
|
12
|
+
transports = /* @__PURE__ */ new Map();
|
|
13
|
+
addTransport(transport) {
|
|
14
|
+
if (!transport.transportId) throw new Error("can't add transport without transport id");
|
|
15
|
+
this.transports.set(transport.transportId, transport);
|
|
16
|
+
}
|
|
17
|
+
removeTransport(transportId) {
|
|
18
|
+
this.transports.delete(transportId);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Route already-encoded bytes from one transport to another. The
|
|
22
|
+
* receiving transport's `deliverBytes` is responsible for decoding
|
|
23
|
+
* and applying the inbound alias transformer.
|
|
24
|
+
*
|
|
25
|
+
* Used by `BridgeTransport`'s channel send path.
|
|
26
|
+
*/
|
|
27
|
+
routeBytes(fromTransportId, toTransportId, bytes) {
|
|
28
|
+
const toTransport = this.transports.get(toTransportId);
|
|
29
|
+
if (!toTransport) return;
|
|
30
|
+
toTransport.deliverBytes(fromTransportId, bytes);
|
|
31
|
+
}
|
|
32
|
+
get transportIds() {
|
|
33
|
+
return new Set(this.transports.keys());
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* In-memory transport that runs the alias-aware pipeline end-to-end.
|
|
38
|
+
* Tests that use this transport exercise the same wire path as
|
|
39
|
+
* production transports.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* const bridge = new Bridge()
|
|
44
|
+
* const exchangeA = new Exchange({
|
|
45
|
+
* transports: [createBridgeTransport({ transportId: "peer-a", bridge })],
|
|
46
|
+
* })
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
var BridgeTransport = class extends Transport {
|
|
50
|
+
bridge;
|
|
51
|
+
channelToAdapter = /* @__PURE__ */ new Map();
|
|
52
|
+
adapterToChannel = /* @__PURE__ */ new Map();
|
|
53
|
+
aliasStateByChannel = /* @__PURE__ */ new Map();
|
|
54
|
+
constructor({ transportId, transportType, bridge }) {
|
|
55
|
+
super({
|
|
56
|
+
transportType: transportType ?? "bridge",
|
|
57
|
+
transportId
|
|
58
|
+
});
|
|
59
|
+
this.bridge = bridge;
|
|
60
|
+
}
|
|
61
|
+
generate(context) {
|
|
62
|
+
return {
|
|
63
|
+
transportType: this.transportType,
|
|
64
|
+
send: (msg) => {
|
|
65
|
+
const channelId = this.adapterToChannel.get(context.targetTransportId);
|
|
66
|
+
if (channelId === void 0) return;
|
|
67
|
+
const { state: nextState, wire } = applyOutboundAliasing(this.aliasStateByChannel.get(channelId) ?? emptyAliasState(), msg);
|
|
68
|
+
this.aliasStateByChannel.set(channelId, nextState);
|
|
69
|
+
const bytes = encodeWireMessage(wire);
|
|
70
|
+
this.bridge.routeBytes(this.transportId, context.targetTransportId, bytes);
|
|
71
|
+
},
|
|
72
|
+
stop: () => {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async onStart() {
|
|
76
|
+
this.bridge.addTransport(this);
|
|
77
|
+
for (const [transportId, adapter] of this.bridge.transports) if (transportId !== this.transportId) adapter.createChannelTo(this.transportId);
|
|
78
|
+
for (const transportId of this.bridge.transports.keys()) if (transportId !== this.transportId) this.createChannelTo(transportId);
|
|
79
|
+
for (const channelId of this.adapterToChannel.values()) this.establishChannel(channelId);
|
|
80
|
+
}
|
|
81
|
+
async onStop() {
|
|
82
|
+
for (const [transportId, adapter] of this.bridge.transports) if (transportId !== this.transportId) adapter.removeChannelTo(this.transportId);
|
|
83
|
+
this.bridge.removeTransport(this.transportId);
|
|
84
|
+
for (const channelId of this.channelToAdapter.keys()) {
|
|
85
|
+
this.removeChannel(channelId);
|
|
86
|
+
this.aliasStateByChannel.delete(channelId);
|
|
87
|
+
}
|
|
88
|
+
this.channelToAdapter.clear();
|
|
89
|
+
this.adapterToChannel.clear();
|
|
90
|
+
}
|
|
91
|
+
createChannelTo(targetTransportId) {
|
|
92
|
+
if (this.adapterToChannel.has(targetTransportId)) return;
|
|
93
|
+
const channel = this.addChannel({ targetTransportId });
|
|
94
|
+
this.channelToAdapter.set(channel.channelId, targetTransportId);
|
|
95
|
+
this.adapterToChannel.set(targetTransportId, channel.channelId);
|
|
96
|
+
this.aliasStateByChannel.set(channel.channelId, emptyAliasState());
|
|
97
|
+
}
|
|
98
|
+
removeChannelTo(targetTransportId) {
|
|
99
|
+
const channelId = this.adapterToChannel.get(targetTransportId);
|
|
100
|
+
if (channelId !== void 0) {
|
|
101
|
+
this.removeChannel(channelId);
|
|
102
|
+
this.channelToAdapter.delete(channelId);
|
|
103
|
+
this.adapterToChannel.delete(targetTransportId);
|
|
104
|
+
this.aliasStateByChannel.delete(channelId);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Deliver encoded bytes to the appropriate channel.
|
|
109
|
+
*
|
|
110
|
+
* Decodes via `decodeWireMessage`, applies the inbound alias
|
|
111
|
+
* transformer, and delivers each resolved message asynchronously
|
|
112
|
+
* via `queueMicrotask()`.
|
|
113
|
+
*/
|
|
114
|
+
deliverBytes(fromTransportId, bytes) {
|
|
115
|
+
const channelId = this.adapterToChannel.get(fromTransportId);
|
|
116
|
+
if (channelId === void 0) return;
|
|
117
|
+
const channel = this.channels.get(channelId);
|
|
118
|
+
if (!channel) return;
|
|
119
|
+
const wire = decodeWireMessage(bytes);
|
|
120
|
+
const result = applyInboundAliasing(this.aliasStateByChannel.get(channelId) ?? emptyAliasState(), wire);
|
|
121
|
+
this.aliasStateByChannel.set(channelId, result.state);
|
|
122
|
+
if (result.error || !result.msg) {
|
|
123
|
+
console.warn("[BridgeTransport] alias resolution failed:", result.error);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const msg = result.msg;
|
|
127
|
+
queueMicrotask(() => {
|
|
128
|
+
channel.onReceive(msg);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Create a BridgeTransport factory for in-process testing.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```typescript
|
|
137
|
+
* const bridge = new Bridge()
|
|
138
|
+
* const exchangeA = new Exchange({
|
|
139
|
+
* transports: [createBridgeTransport({ transportId: "peer-a", bridge })],
|
|
140
|
+
* })
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
function createBridgeTransport(params) {
|
|
144
|
+
return () => new BridgeTransport(params);
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
export { Bridge, BridgeTransport, createBridgeTransport };
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kyneta/bridge-transport",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "In-process transport for @kyneta/exchange — codec-faithful + alias-aware delivery for testing multi-peer scenarios in a single process",
|
|
5
|
+
"author": "Duane Johnson",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/halecraft/kyneta",
|
|
10
|
+
"directory": "packages/exchange/transports/bridge"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@kyneta/transport": "^1.5.0",
|
|
29
|
+
"@kyneta/wire": "^1.5.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tsdown": "^0.21.9",
|
|
33
|
+
"typescript": "^5.9.2",
|
|
34
|
+
"vitest": "^4.0.17",
|
|
35
|
+
"@kyneta/transport": "^1.5.0",
|
|
36
|
+
"@kyneta/schema": "^1.5.0",
|
|
37
|
+
"@kyneta/wire": "^1.5.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown",
|
|
41
|
+
"test": "verify logic",
|
|
42
|
+
"verify": "verify"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// BridgeTransport — codec-faithful + alias-aware in-process tests.
|
|
2
|
+
|
|
3
|
+
import type { TransportContext } from "@kyneta/transport"
|
|
4
|
+
import { describe, expect, it, vi } from "vitest"
|
|
5
|
+
import { Bridge, BridgeTransport } from "../bridge.js"
|
|
6
|
+
|
|
7
|
+
function createTransportContext(
|
|
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
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("BridgeTransport", () => {
|
|
21
|
+
it("two adapters exchange messages through a Bridge", async () => {
|
|
22
|
+
const bridge = new Bridge()
|
|
23
|
+
|
|
24
|
+
const ctxA = createTransportContext({
|
|
25
|
+
identity: { peerId: "peer-a", type: "user" },
|
|
26
|
+
})
|
|
27
|
+
const ctxB = createTransportContext({
|
|
28
|
+
identity: { peerId: "peer-b", type: "user" },
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const adapterA = new BridgeTransport({ transportId: "peer-a", bridge })
|
|
32
|
+
const adapterB = new BridgeTransport({ transportId: "peer-b", bridge })
|
|
33
|
+
|
|
34
|
+
adapterA._initialize(ctxA)
|
|
35
|
+
await adapterA._start()
|
|
36
|
+
adapterB._initialize(ctxB)
|
|
37
|
+
await adapterB._start()
|
|
38
|
+
|
|
39
|
+
expect(bridge.transports.size).toBe(2)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("stops cleanly and removes from bridge", async () => {
|
|
43
|
+
const bridge = new Bridge()
|
|
44
|
+
|
|
45
|
+
const adapterA = new BridgeTransport({ transportId: "peer-a", bridge })
|
|
46
|
+
adapterA._initialize(createTransportContext())
|
|
47
|
+
await adapterA._start()
|
|
48
|
+
|
|
49
|
+
expect(bridge.transports.size).toBe(1)
|
|
50
|
+
|
|
51
|
+
await adapterA._stop()
|
|
52
|
+
expect(bridge.transports.size).toBe(0)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("channel lifecycle: connected → established via handshake", async () => {
|
|
56
|
+
const bridge = new Bridge()
|
|
57
|
+
|
|
58
|
+
const establishedChannels: number[] = []
|
|
59
|
+
|
|
60
|
+
const adapterA = new BridgeTransport({ transportId: "peer-a", bridge })
|
|
61
|
+
const adapterB = new BridgeTransport({ transportId: "peer-b", bridge })
|
|
62
|
+
|
|
63
|
+
adapterA._initialize(
|
|
64
|
+
createTransportContext({
|
|
65
|
+
identity: { peerId: "peer-a", type: "user" },
|
|
66
|
+
onChannelEstablish: channel =>
|
|
67
|
+
establishedChannels.push(channel.channelId),
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
await adapterA._start()
|
|
71
|
+
|
|
72
|
+
adapterB._initialize(
|
|
73
|
+
createTransportContext({
|
|
74
|
+
identity: { peerId: "peer-b", type: "user" },
|
|
75
|
+
onChannelEstablish: channel =>
|
|
76
|
+
establishedChannels.push(channel.channelId),
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
await adapterB._start()
|
|
80
|
+
|
|
81
|
+
expect(establishedChannels.length).toBeGreaterThan(0)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("codec-faithful: messages round-trip bit-perfectly via the bridge codec", async () => {
|
|
85
|
+
const bridge = new Bridge()
|
|
86
|
+
|
|
87
|
+
const ctxA = createTransportContext({
|
|
88
|
+
identity: { peerId: "peer-a", type: "user" },
|
|
89
|
+
})
|
|
90
|
+
const ctxB = createTransportContext({
|
|
91
|
+
identity: { peerId: "peer-b", type: "user" },
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const adapterA = new BridgeTransport({ transportId: "peer-a", bridge })
|
|
95
|
+
const adapterB = new BridgeTransport({ transportId: "peer-b", bridge })
|
|
96
|
+
|
|
97
|
+
adapterA._initialize(ctxA)
|
|
98
|
+
await adapterA._start()
|
|
99
|
+
adapterB._initialize(ctxB)
|
|
100
|
+
await adapterB._start()
|
|
101
|
+
|
|
102
|
+
expect(bridge.transports.size).toBe(2)
|
|
103
|
+
})
|
|
104
|
+
})
|
package/src/bridge.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// bridge-adapter — in-process transport with alias-aware delivery.
|
|
2
|
+
//
|
|
3
|
+
// BridgeTransport is a real transport that runs the alias-aware pipeline
|
|
4
|
+
// end-to-end and applies the docId/schemaHash alias transformer at the
|
|
5
|
+
// channel send/receive boundary — exactly like every other binary
|
|
6
|
+
// transport. Async delivery is preserved via `queueMicrotask()` to keep
|
|
7
|
+
// test behavior representative of real network adapters.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// const bridge = new Bridge()
|
|
11
|
+
// const exchangeA = new Exchange({
|
|
12
|
+
// transports: [createBridgeTransport({ transportId: "peer-a", bridge })],
|
|
13
|
+
// })
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
ChannelId,
|
|
17
|
+
GeneratedChannel,
|
|
18
|
+
TransportFactory,
|
|
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"
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Bridge — message router connecting multiple BridgeTransports in-process
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* In-process byte router connecting multiple `BridgeTransport`s,
|
|
36
|
+
* keyed by each transport's unique `transportId`.
|
|
37
|
+
*
|
|
38
|
+
* Channel-level sends produce alias-aware encoded bytes via the wire-layer
|
|
39
|
+
* transformer and call `routeBytes`.
|
|
40
|
+
*/
|
|
41
|
+
export class Bridge {
|
|
42
|
+
readonly transports = new Map<string, BridgeTransport>()
|
|
43
|
+
|
|
44
|
+
addTransport(transport: BridgeTransport): void {
|
|
45
|
+
if (!transport.transportId) {
|
|
46
|
+
throw new Error("can't add transport without transport id")
|
|
47
|
+
}
|
|
48
|
+
this.transports.set(transport.transportId, transport)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
removeTransport(transportId: string): void {
|
|
52
|
+
this.transports.delete(transportId)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Route already-encoded bytes from one transport to another. The
|
|
57
|
+
* receiving transport's `deliverBytes` is responsible for decoding
|
|
58
|
+
* and applying the inbound alias transformer.
|
|
59
|
+
*
|
|
60
|
+
* Used by `BridgeTransport`'s channel send path.
|
|
61
|
+
*/
|
|
62
|
+
routeBytes(
|
|
63
|
+
fromTransportId: string,
|
|
64
|
+
toTransportId: string,
|
|
65
|
+
bytes: Uint8Array,
|
|
66
|
+
): void {
|
|
67
|
+
const toTransport = this.transports.get(toTransportId)
|
|
68
|
+
if (!toTransport) return
|
|
69
|
+
toTransport.deliverBytes(fromTransportId, bytes)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get transportIds(): Set<string> {
|
|
73
|
+
return new Set(this.transports.keys())
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// BridgeTransport — in-process network adapter for testing
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
type BridgeTransportContext = {
|
|
82
|
+
targetTransportId: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type BridgeTransportParams = {
|
|
86
|
+
/** Unique identifier for this transport instance (e.g. "peer-a", "server"). */
|
|
87
|
+
transportId: string
|
|
88
|
+
/**
|
|
89
|
+
* Transport type category. Defaults to "bridge".
|
|
90
|
+
* Stored in ChannelMeta for informational purposes.
|
|
91
|
+
*/
|
|
92
|
+
transportType?: string
|
|
93
|
+
bridge: Bridge
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* In-memory transport that runs the alias-aware pipeline end-to-end.
|
|
98
|
+
* Tests that use this transport exercise the same wire path as
|
|
99
|
+
* production transports.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* const bridge = new Bridge()
|
|
104
|
+
* const exchangeA = new Exchange({
|
|
105
|
+
* transports: [createBridgeTransport({ transportId: "peer-a", bridge })],
|
|
106
|
+
* })
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export class BridgeTransport extends Transport<BridgeTransportContext> {
|
|
110
|
+
readonly bridge: Bridge
|
|
111
|
+
|
|
112
|
+
// Track which remote transport each channel connects to.
|
|
113
|
+
private channelToAdapter = new Map<ChannelId, string>()
|
|
114
|
+
private adapterToChannel = new Map<string, ChannelId>()
|
|
115
|
+
|
|
116
|
+
// Per-channel alias state. Created with the channel; lives until removal.
|
|
117
|
+
// Keyed by channelId.
|
|
118
|
+
private aliasStateByChannel = new Map<ChannelId, AliasState>()
|
|
119
|
+
|
|
120
|
+
constructor({ transportId, transportType, bridge }: BridgeTransportParams) {
|
|
121
|
+
super({ transportType: transportType ?? "bridge", transportId })
|
|
122
|
+
this.bridge = bridge
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
generate(context: BridgeTransportContext): GeneratedChannel {
|
|
126
|
+
return {
|
|
127
|
+
transportType: this.transportType,
|
|
128
|
+
send: msg => {
|
|
129
|
+
const channelId = this.adapterToChannel.get(context.targetTransportId)
|
|
130
|
+
if (channelId === undefined) return
|
|
131
|
+
|
|
132
|
+
const state =
|
|
133
|
+
this.aliasStateByChannel.get(channelId) ?? emptyAliasState()
|
|
134
|
+
const { state: nextState, wire } = applyOutboundAliasing(state, msg)
|
|
135
|
+
this.aliasStateByChannel.set(channelId, nextState)
|
|
136
|
+
|
|
137
|
+
const bytes = encodeWireMessage(wire)
|
|
138
|
+
this.bridge.routeBytes(
|
|
139
|
+
this.transportId,
|
|
140
|
+
context.targetTransportId,
|
|
141
|
+
bytes,
|
|
142
|
+
)
|
|
143
|
+
},
|
|
144
|
+
stop: () => {
|
|
145
|
+
// Cleanup handled by removeChannel.
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async onStart(): Promise<void> {
|
|
151
|
+
this.bridge.addTransport(this)
|
|
152
|
+
|
|
153
|
+
// Phase 1: Create all channels (no establishment yet).
|
|
154
|
+
for (const [transportId, adapter] of this.bridge.transports) {
|
|
155
|
+
if (transportId !== this.transportId) {
|
|
156
|
+
adapter.createChannelTo(this.transportId)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const transportId of this.bridge.transports.keys()) {
|
|
160
|
+
if (transportId !== this.transportId) {
|
|
161
|
+
this.createChannelTo(transportId)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Phase 2: Only the joining transport initiates establishment.
|
|
166
|
+
for (const channelId of this.adapterToChannel.values()) {
|
|
167
|
+
this.establishChannel(channelId)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async onStop(): Promise<void> {
|
|
172
|
+
for (const [transportId, adapter] of this.bridge.transports) {
|
|
173
|
+
if (transportId !== this.transportId) {
|
|
174
|
+
adapter.removeChannelTo(this.transportId)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
this.bridge.removeTransport(this.transportId)
|
|
178
|
+
|
|
179
|
+
for (const channelId of this.channelToAdapter.keys()) {
|
|
180
|
+
this.removeChannel(channelId)
|
|
181
|
+
this.aliasStateByChannel.delete(channelId)
|
|
182
|
+
}
|
|
183
|
+
this.channelToAdapter.clear()
|
|
184
|
+
this.adapterToChannel.clear()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
createChannelTo(targetTransportId: string): void {
|
|
188
|
+
if (this.adapterToChannel.has(targetTransportId)) return
|
|
189
|
+
const channel = this.addChannel({ targetTransportId })
|
|
190
|
+
this.channelToAdapter.set(channel.channelId, targetTransportId)
|
|
191
|
+
this.adapterToChannel.set(targetTransportId, channel.channelId)
|
|
192
|
+
this.aliasStateByChannel.set(channel.channelId, emptyAliasState())
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
removeChannelTo(targetTransportId: string): void {
|
|
196
|
+
const channelId = this.adapterToChannel.get(targetTransportId)
|
|
197
|
+
if (channelId !== undefined) {
|
|
198
|
+
this.removeChannel(channelId)
|
|
199
|
+
this.channelToAdapter.delete(channelId)
|
|
200
|
+
this.adapterToChannel.delete(targetTransportId)
|
|
201
|
+
this.aliasStateByChannel.delete(channelId)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Deliver encoded bytes to the appropriate channel.
|
|
207
|
+
*
|
|
208
|
+
* Decodes via `decodeWireMessage`, applies the inbound alias
|
|
209
|
+
* transformer, and delivers each resolved message asynchronously
|
|
210
|
+
* via `queueMicrotask()`.
|
|
211
|
+
*/
|
|
212
|
+
deliverBytes(fromTransportId: string, bytes: Uint8Array): void {
|
|
213
|
+
const channelId = this.adapterToChannel.get(fromTransportId)
|
|
214
|
+
if (channelId === undefined) return
|
|
215
|
+
|
|
216
|
+
const channel = this.channels.get(channelId)
|
|
217
|
+
if (!channel) return
|
|
218
|
+
|
|
219
|
+
const wire = decodeWireMessage(bytes)
|
|
220
|
+
const state = this.aliasStateByChannel.get(channelId) ?? emptyAliasState()
|
|
221
|
+
const result = applyInboundAliasing(state, wire)
|
|
222
|
+
this.aliasStateByChannel.set(channelId, result.state)
|
|
223
|
+
|
|
224
|
+
if (result.error || !result.msg) {
|
|
225
|
+
console.warn("[BridgeTransport] alias resolution failed:", result.error)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
const msg = result.msg
|
|
229
|
+
queueMicrotask(() => {
|
|
230
|
+
channel.onReceive(msg)
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Factory function
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create a BridgeTransport factory for in-process testing.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* const bridge = new Bridge()
|
|
245
|
+
* const exchangeA = new Exchange({
|
|
246
|
+
* transports: [createBridgeTransport({ transportId: "peer-a", bridge })],
|
|
247
|
+
* })
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
export function createBridgeTransport(
|
|
251
|
+
params: BridgeTransportParams,
|
|
252
|
+
): TransportFactory {
|
|
253
|
+
return () => new BridgeTransport(params)
|
|
254
|
+
}
|
package/src/index.ts
ADDED