@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 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 produce alias-aware encoded bytes via the wire-layer
9
- * transformer and call `routeBytes`.
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 alias-aware pipeline end-to-end.
39
- * Tests that use this transport exercise the same wire path as
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 aliasStateByChannel;
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
- * Decodes via `decodeWireMessage`, applies the inbound alias
69
- * transformer, and delivers each resolved message asynchronously
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.
@@ -1 +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,iBAAiB;AAAA;AAAA,KAGP,qBAAA;EAbU,+EAepB,WAAA;EAfuB;AAGxB;;;EAiBC,aAAA;EACA,MAAA,EAAQ,MAAM;AAAA;;;;;;;;;AAAA;AAgBhB;;;;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,gBAAgB"}
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 produce alias-aware encoded bytes via the wire-layer
9
- * transformer and call `routeBytes`.
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 alias-aware pipeline end-to-end.
38
- * Tests that use this transport exercise the same wire path as
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
- aliasStateByChannel = /* @__PURE__ */ new Map();
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 { 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);
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
- this.removeChannel(channelId);
86
- this.aliasStateByChannel.delete(channelId);
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.aliasStateByChannel.set(channel.channelId, emptyAliasState());
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.aliasStateByChannel.delete(channelId);
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
- * Decodes via `decodeWireMessage`, applies the inbound alias
111
- * transformer, and delivers each resolved message asynchronously
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 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;
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.6.0",
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.6.0",
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/schema": "^1.6.0",
36
- "@kyneta/wire": "^1.6.0",
37
- "@kyneta/transport": "^1.6.0"
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 codec-faithful + alias-aware in-process tests.
1
+ // BridgeTransport integration tests lifecycle, channel handshake,
2
+ // and fragmentation through the wire pipeline.
2
3
 
3
- import type { TransportContext } from "@kyneta/transport"
4
- import { describe, expect, it, vi } from "vitest"
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
- 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
- }
9
+ const createTransportContext = createTestTransportContext
19
10
 
20
11
  describe("BridgeTransport", () => {
21
- it("two adapters exchange messages through a Bridge", async () => {
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("codec-faithful: messages round-trip bit-perfectly via the bridge codec", async () => {
75
+ it("fragments and reassembles large messages through the bridge", async () => {
85
76
  const bridge = new Bridge()
86
77
 
87
- const ctxA = createTransportContext({
88
- identity: { peerId: "peer-a", type: "user" },
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
- expect(bridge.transports.size).toBe(2)
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 alias-aware delivery.
1
+ // bridge-adapter — in-process transport with Pipeline-based delivery.
2
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
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 produce alias-aware encoded bytes via the wire-layer
39
- * transformer and call `routeBytes`.
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 alias-aware pipeline end-to-end.
98
- * Tests that use this transport exercise the same wire path as
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 alias state. Created with the channel; lives until removal.
108
+ // Per-channel pipeline. Created with the channel; lives until removal.
117
109
  // Keyed by channelId.
118
- private aliasStateByChannel = new Map<ChannelId, AliasState>()
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
- 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
- )
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: Create all channels (no establishment yet).
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: Only the joining transport initiates establishment.
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.aliasStateByChannel.set(channel.channelId, emptyAliasState())
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.aliasStateByChannel.delete(channelId)
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
- * Decodes via `decodeWireMessage`, applies the inbound alias
209
- * transformer, and delivers each resolved message asynchronously
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
- 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
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