@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 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
@@ -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
@@ -0,0 +1,4 @@
1
+ // @kyneta/bridge-transport — in-process transport for testing.
2
+
3
+ export type { BridgeTransportParams } from "./bridge.js"
4
+ export { Bridge, BridgeTransport, createBridgeTransport } from "./bridge.js"