@aiwerk/mcp-bridge 1.0.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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/bin/mcp-bridge.js +9 -0
  4. package/bin/mcp-bridge.ts +335 -0
  5. package/package.json +42 -0
  6. package/scripts/install-server.ps1 +300 -0
  7. package/scripts/install-server.sh +357 -0
  8. package/servers/apify/README.md +40 -0
  9. package/servers/apify/config.json +13 -0
  10. package/servers/apify/env_vars +1 -0
  11. package/servers/apify/install.ps1 +3 -0
  12. package/servers/apify/install.sh +4 -0
  13. package/servers/candidates.md +13 -0
  14. package/servers/github/README.md +40 -0
  15. package/servers/github/config.json +21 -0
  16. package/servers/github/env_vars +1 -0
  17. package/servers/github/install.ps1 +3 -0
  18. package/servers/github/install.sh +4 -0
  19. package/servers/google-maps/README.md +40 -0
  20. package/servers/google-maps/config.json +17 -0
  21. package/servers/google-maps/env_vars +1 -0
  22. package/servers/google-maps/install.ps1 +3 -0
  23. package/servers/google-maps/install.sh +4 -0
  24. package/servers/hetzner/README.md +41 -0
  25. package/servers/hetzner/config.json +16 -0
  26. package/servers/hetzner/env_vars +1 -0
  27. package/servers/hetzner/install.ps1 +3 -0
  28. package/servers/hetzner/install.sh +4 -0
  29. package/servers/hostinger/README.md +40 -0
  30. package/servers/hostinger/config.json +17 -0
  31. package/servers/hostinger/env_vars +1 -0
  32. package/servers/hostinger/install.ps1 +3 -0
  33. package/servers/hostinger/install.sh +4 -0
  34. package/servers/index.json +125 -0
  35. package/servers/linear/README.md +40 -0
  36. package/servers/linear/config.json +16 -0
  37. package/servers/linear/env_vars +1 -0
  38. package/servers/linear/install.ps1 +3 -0
  39. package/servers/linear/install.sh +4 -0
  40. package/servers/miro/README.md +40 -0
  41. package/servers/miro/config.json +19 -0
  42. package/servers/miro/env_vars +1 -0
  43. package/servers/miro/install.ps1 +3 -0
  44. package/servers/miro/install.sh +4 -0
  45. package/servers/notion/README.md +42 -0
  46. package/servers/notion/config.json +17 -0
  47. package/servers/notion/env_vars +1 -0
  48. package/servers/notion/install.ps1 +3 -0
  49. package/servers/notion/install.sh +4 -0
  50. package/servers/stripe/README.md +40 -0
  51. package/servers/stripe/config.json +19 -0
  52. package/servers/stripe/env_vars +1 -0
  53. package/servers/stripe/install.ps1 +3 -0
  54. package/servers/stripe/install.sh +4 -0
  55. package/servers/tavily/README.md +40 -0
  56. package/servers/tavily/config.json +17 -0
  57. package/servers/tavily/env_vars +1 -0
  58. package/servers/tavily/install.ps1 +3 -0
  59. package/servers/tavily/install.sh +4 -0
  60. package/servers/todoist/README.md +40 -0
  61. package/servers/todoist/config.json +17 -0
  62. package/servers/todoist/env_vars +1 -0
  63. package/servers/todoist/install.ps1 +3 -0
  64. package/servers/todoist/install.sh +4 -0
  65. package/servers/wise/README.md +41 -0
  66. package/servers/wise/config.json +16 -0
  67. package/servers/wise/env_vars +1 -0
  68. package/servers/wise/install.ps1 +3 -0
  69. package/servers/wise/install.sh +4 -0
  70. package/src/config.ts +168 -0
  71. package/src/index.ts +44 -0
  72. package/src/mcp-router.ts +366 -0
  73. package/src/protocol.ts +69 -0
  74. package/src/schema-convert.ts +178 -0
  75. package/src/standalone-server.ts +385 -0
  76. package/src/tool-naming.ts +51 -0
  77. package/src/transport-base.ts +199 -0
  78. package/src/transport-sse.ts +230 -0
  79. package/src/transport-stdio.ts +312 -0
  80. package/src/transport-streamable-http.ts +188 -0
  81. package/src/types.ts +88 -0
  82. package/src/update-checker.ts +155 -0
  83. package/tests/collision.test.ts +60 -0
  84. package/tests/env-resolve.test.ts +68 -0
  85. package/tests/mcp-router.test.ts +301 -0
  86. package/tests/schema-convert.test.ts +70 -0
  87. package/tests/transport-base.test.ts +214 -0
  88. package/tsconfig.json +15 -0
@@ -0,0 +1,301 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { McpRouter } from "../src/mcp-router.ts";
4
+ import type { McpRequest, McpResponse, McpServerConfig, McpTransport, McpTool } from "../src/types.ts";
5
+
6
+ type Behavior = {
7
+ tools: McpTool[];
8
+ callResult?: any;
9
+ callError?: { code: number; message: string };
10
+ connectError?: Error;
11
+ };
12
+
13
+ class MockTransport implements McpTransport {
14
+ static behaviors = new Map<string, Behavior>();
15
+ static instances = new Map<string, MockTransport>();
16
+
17
+ static reset(): void {
18
+ this.behaviors.clear();
19
+ this.instances.clear();
20
+ }
21
+
22
+ connected = false;
23
+ requests: McpRequest[] = [];
24
+ connectCount = 0;
25
+ disconnectCount = 0;
26
+ private readonly key: string;
27
+
28
+ constructor(config: McpServerConfig, _clientConfig?: any, _logger?: any, _onReconnected?: () => Promise<void>) {
29
+ this.key = config.url || config.command || "default";
30
+ MockTransport.instances.set(this.key, this);
31
+ }
32
+
33
+ async connect(): Promise<void> {
34
+ this.connectCount += 1;
35
+ const behavior = MockTransport.behaviors.get(this.key);
36
+ if (behavior?.connectError) {
37
+ throw behavior.connectError;
38
+ }
39
+ this.connected = true;
40
+ }
41
+
42
+ async disconnect(): Promise<void> {
43
+ this.disconnectCount += 1;
44
+ this.connected = false;
45
+ }
46
+
47
+ async sendRequest(request: McpRequest): Promise<McpResponse> {
48
+ this.requests.push(request);
49
+ const behavior = MockTransport.behaviors.get(this.key);
50
+
51
+ if (request.method === "initialize") {
52
+ return { jsonrpc: "2.0", id: 1, result: {} };
53
+ }
54
+
55
+ if (request.method === "tools/list") {
56
+ return { jsonrpc: "2.0", id: 2, result: { tools: behavior?.tools || [] } };
57
+ }
58
+
59
+ if (request.method === "tools/call") {
60
+ if (behavior?.callError) {
61
+ return {
62
+ jsonrpc: "2.0",
63
+ id: 3,
64
+ error: {
65
+ code: behavior.callError.code,
66
+ message: behavior.callError.message
67
+ }
68
+ };
69
+ }
70
+ return { jsonrpc: "2.0", id: 3, result: behavior?.callResult || { ok: true } };
71
+ }
72
+
73
+ return { jsonrpc: "2.0", id: 4, result: {} };
74
+ }
75
+
76
+ async sendNotification(_notification: any): Promise<void> {
77
+ return;
78
+ }
79
+
80
+ isConnected(): boolean {
81
+ return this.connected;
82
+ }
83
+ }
84
+
85
+ function makeLogger() {
86
+ return {
87
+ info: () => {},
88
+ warn: () => {},
89
+ error: () => {},
90
+ debug: () => {}
91
+ };
92
+ }
93
+
94
+ function makeRouter(
95
+ servers: Record<string, McpServerConfig>,
96
+ overrides: { routerIdleTimeoutMs?: number; routerMaxConcurrent?: number } = {}
97
+ ): McpRouter {
98
+ return new McpRouter(
99
+ servers,
100
+ {
101
+ servers,
102
+ routerIdleTimeoutMs: overrides.routerIdleTimeoutMs,
103
+ routerMaxConcurrent: overrides.routerMaxConcurrent
104
+ },
105
+ makeLogger(),
106
+ {
107
+ sse: MockTransport,
108
+ stdio: MockTransport,
109
+ streamableHttp: MockTransport
110
+ }
111
+ );
112
+ }
113
+
114
+ test.beforeEach(() => {
115
+ MockTransport.reset();
116
+ });
117
+
118
+ test("dispatch returns unknown_server error for missing server", async () => {
119
+ const router = makeRouter({
120
+ alpha: { transport: "sse", url: "mock://alpha" }
121
+ });
122
+
123
+ const result = await router.dispatch("missing", "list");
124
+ assert.equal("error" in result ? result.error : "", "unknown_server");
125
+ });
126
+
127
+ test("dispatch action=list returns cached tool list", async () => {
128
+ const server = { transport: "sse" as const, url: "mock://cache" };
129
+ MockTransport.behaviors.set("mock://cache", {
130
+ tools: [
131
+ {
132
+ name: "create_server",
133
+ description: "Create server",
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: { name: { type: "string" } },
137
+ required: ["name"]
138
+ }
139
+ }
140
+ ]
141
+ });
142
+
143
+ const router = makeRouter({ cache: server });
144
+ const first = await router.dispatch("cache", "list");
145
+ const second = await router.dispatch("cache", "list");
146
+
147
+ assert.equal("error" in first, false);
148
+ assert.equal("error" in second, false);
149
+ if (!("error" in first) && first.action === "list") {
150
+ assert.deepEqual(first.tools, [
151
+ {
152
+ name: "create_server",
153
+ description: "Create server",
154
+ requiredParams: ["name"]
155
+ }
156
+ ]);
157
+ }
158
+
159
+ const instance = MockTransport.instances.get("mock://cache");
160
+ assert.ok(instance);
161
+ const listCalls = instance!.requests.filter((req) => req.method === "tools/list");
162
+ assert.equal(listCalls.length, 1);
163
+ });
164
+
165
+ test("dispatch action=call proxies to transport", async () => {
166
+ MockTransport.behaviors.set("mock://call", {
167
+ tools: [{ name: "list_servers", description: "List", inputSchema: { type: "object" } }],
168
+ callResult: { servers: [{ id: "1" }] }
169
+ });
170
+
171
+ const router = makeRouter({
172
+ call: { transport: "sse", url: "mock://call" }
173
+ });
174
+
175
+ const result = await router.dispatch("call", "call", "list_servers", { region: "eu-central" });
176
+ assert.equal("error" in result, false);
177
+ if (!("error" in result) && result.action === "call") {
178
+ assert.deepEqual(result.result, { servers: [{ id: "1" }] });
179
+ }
180
+
181
+ const instance = MockTransport.instances.get("mock://call");
182
+ assert.ok(instance);
183
+ const callRequest = instance!.requests.find((req) => req.method === "tools/call");
184
+ assert.ok(callRequest);
185
+ assert.deepEqual(callRequest!.params, {
186
+ name: "list_servers",
187
+ arguments: { region: "eu-central" }
188
+ });
189
+ });
190
+
191
+ test("evicts least recently used connection when max concurrent exceeded", async () => {
192
+ const servers = {
193
+ a: { transport: "sse" as const, url: "mock://a" },
194
+ b: { transport: "sse" as const, url: "mock://b" },
195
+ c: { transport: "sse" as const, url: "mock://c" }
196
+ };
197
+
198
+ for (const key of Object.keys(servers)) {
199
+ MockTransport.behaviors.set(`mock://${key}`, {
200
+ tools: [{ name: "ping", description: "Ping", inputSchema: { type: "object" } }]
201
+ });
202
+ }
203
+
204
+ const router = makeRouter(servers, { routerMaxConcurrent: 2, routerIdleTimeoutMs: 60_000 });
205
+
206
+ await router.dispatch("a", "list");
207
+ await new Promise((resolve) => setTimeout(resolve, 5));
208
+ await router.dispatch("b", "list");
209
+ await new Promise((resolve) => setTimeout(resolve, 5));
210
+ await router.dispatch("c", "list");
211
+
212
+ assert.equal(MockTransport.instances.get("mock://a")?.disconnectCount, 1);
213
+ assert.equal(MockTransport.instances.get("mock://b")?.isConnected(), true);
214
+ assert.equal(MockTransport.instances.get("mock://c")?.isConnected(), true);
215
+ });
216
+
217
+ test("disconnects idle connection after timeout", async () => {
218
+ MockTransport.behaviors.set("mock://idle", {
219
+ tools: [{ name: "ping", description: "Ping", inputSchema: { type: "object" } }]
220
+ });
221
+
222
+ const router = makeRouter(
223
+ { idle: { transport: "sse", url: "mock://idle" } },
224
+ { routerIdleTimeoutMs: 25, routerMaxConcurrent: 5 }
225
+ );
226
+
227
+ await router.dispatch("idle", "list");
228
+ await new Promise((resolve) => setTimeout(resolve, 80));
229
+
230
+ const instance = MockTransport.instances.get("mock://idle");
231
+ assert.ok(instance);
232
+ assert.equal(instance!.disconnectCount >= 1, true);
233
+ assert.equal(instance!.isConnected(), false);
234
+ });
235
+
236
+ test("generateDescription includes configured servers", () => {
237
+ const description = McpRouter.generateDescription({
238
+ hetzner: { transport: "sse", url: "mock://h" },
239
+ github: { transport: "sse", url: "mock://g" }
240
+ });
241
+
242
+ assert.match(description, /hetzner/);
243
+ assert.match(description, /github/);
244
+ assert.match(description, /action='list'/);
245
+ assert.match(description, /action='call'/);
246
+ assert.match(description, /action='refresh'/);
247
+ });
248
+
249
+ test("status action returns all servers with connection state", async () => {
250
+ const servers = {
251
+ alpha: { transport: "stdio" as const, command: "node", args: ["fake.js"] },
252
+ beta: { transport: "sse" as const, url: "http://localhost:9999/sse" }
253
+ };
254
+ const router = makeRouter(servers);
255
+
256
+ const result = await router.dispatch(undefined, "status");
257
+ assert.equal("action" in result && result.action, "status");
258
+ if ("servers" in result) {
259
+ assert.equal(result.servers.length, 2);
260
+ const alpha = result.servers.find(s => s.name === "alpha");
261
+ assert.ok(alpha);
262
+ assert.equal(alpha!.status, "disconnected");
263
+ assert.equal(alpha!.tools, 0);
264
+ assert.equal(alpha!.transport, "stdio");
265
+ const beta = result.servers.find(s => s.name === "beta");
266
+ assert.ok(beta);
267
+ assert.equal(beta!.status, "disconnected");
268
+ assert.equal(beta!.transport, "sse");
269
+ }
270
+ });
271
+
272
+ test("status action shows connected server after list", async () => {
273
+ const servers = {
274
+ alpha: { transport: "stdio" as const, command: "node", args: ["fake.js"] }
275
+ };
276
+ MockTransport.behaviors.set("node", {
277
+ tools: [
278
+ { name: "tool_a", description: "A", inputSchema: { type: "object" } },
279
+ { name: "tool_b", description: "B", inputSchema: { type: "object" } }
280
+ ]
281
+ });
282
+ const router = makeRouter(servers);
283
+
284
+ // List triggers connection + tool fetch
285
+ await router.dispatch("alpha", "list");
286
+
287
+ const result = await router.dispatch(undefined, "status");
288
+ if ("servers" in result) {
289
+ const alpha = result.servers.find(s => s.name === "alpha");
290
+ assert.ok(alpha);
291
+ assert.equal(alpha!.status, "connected");
292
+ assert.equal(alpha!.tools, 2);
293
+ }
294
+ });
295
+
296
+ test("generateDescription includes status action", () => {
297
+ const description = McpRouter.generateDescription({
298
+ test: { transport: "stdio", command: "node", args: [] }
299
+ });
300
+ assert.match(description, /action='status'/);
301
+ });
@@ -0,0 +1,70 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ convertJsonSchemaToTypeBox,
5
+ createToolParameters,
6
+ setTypeBoxLoader
7
+ } from "../src/schema-convert.ts";
8
+
9
+ test("converts string schema", async () => {
10
+ const schema = await convertJsonSchemaToTypeBox({ type: "string", minLength: 1 });
11
+ assert.equal(schema.type, "string");
12
+ assert.equal(schema.minLength, 1);
13
+ });
14
+
15
+ test("converts number schema", async () => {
16
+ const schema = await convertJsonSchemaToTypeBox({ type: "number", minimum: 2, maximum: 5 });
17
+ assert.equal(schema.type, "number");
18
+ assert.equal(schema.minimum, 2);
19
+ assert.equal(schema.maximum, 5);
20
+ });
21
+
22
+ test("converts object schema", async () => {
23
+ const schema = await convertJsonSchemaToTypeBox({
24
+ type: "object",
25
+ properties: {
26
+ name: { type: "string" },
27
+ age: { type: "number" }
28
+ },
29
+ required: ["name"]
30
+ });
31
+
32
+ assert.equal(schema.type, "object");
33
+ assert.ok(schema.properties.name);
34
+ assert.ok(schema.properties.age);
35
+ });
36
+
37
+ test("converts array schema", async () => {
38
+ const schema = await convertJsonSchemaToTypeBox({ type: "array", items: { type: "string" } });
39
+ assert.equal(schema.type, "array");
40
+ assert.equal(schema.items.type, "string");
41
+ });
42
+
43
+ test("converts anyOf schema", async () => {
44
+ const schema = await convertJsonSchemaToTypeBox({
45
+ anyOf: [{ type: "string" }, { type: "number" }]
46
+ });
47
+
48
+ assert.ok(Array.isArray(schema.anyOf));
49
+ assert.equal(schema.anyOf.length, 2);
50
+ assert.equal(schema.anyOf[0].type, "string");
51
+ assert.equal(schema.anyOf[1].type, "number");
52
+ });
53
+
54
+ test("falls back when TypeBox is missing", async () => {
55
+ // Inject a loader that simulates missing TypeBox
56
+ setTypeBoxLoader(async () => null);
57
+
58
+ try {
59
+ const schema = await convertJsonSchemaToTypeBox({ type: "string" });
60
+ const params = await createToolParameters({ type: "object" });
61
+
62
+ // convertJsonSchemaToTypeBox fallback: empty schema {}
63
+ assert.deepStrictEqual(schema, {});
64
+ // createToolParameters fallback: returns raw inputSchema as-is
65
+ assert.deepStrictEqual(params, { type: "object" });
66
+ } finally {
67
+ // Restore default loader
68
+ setTypeBoxLoader(null);
69
+ }
70
+ });
@@ -0,0 +1,214 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { BaseTransport } from "../src/transport-base.ts";
4
+ import type { McpRequest, McpResponse, McpServerConfig } from "../src/types.ts";
5
+
6
+ class TestableTransport extends BaseTransport {
7
+ protected get transportName(): string {
8
+ return "test";
9
+ }
10
+
11
+ async connect(): Promise<void> {
12
+ this.connected = true;
13
+ }
14
+
15
+ async disconnect(): Promise<void> {
16
+ this.connected = false;
17
+ }
18
+
19
+ async sendRequest(_request: McpRequest): Promise<McpResponse> {
20
+ return { jsonrpc: "2.0", id: 0, result: {} };
21
+ }
22
+
23
+ async sendNotification(_notification: any): Promise<void> {
24
+ return;
25
+ }
26
+
27
+ testHandleMessage(message: any): void {
28
+ this.handleMessage(message);
29
+ }
30
+
31
+ testScheduleReconnect(): void {
32
+ this.scheduleReconnect();
33
+ }
34
+
35
+ testRejectAllPending(reason: string): void {
36
+ this.rejectAllPending(reason);
37
+ }
38
+
39
+ testCleanupReconnectTimer(): void {
40
+ this.cleanupReconnectTimer();
41
+ }
42
+
43
+ getPendingRequests() {
44
+ return this.pendingRequests;
45
+ }
46
+
47
+ setConnected(value: boolean): void {
48
+ this.connected = value;
49
+ }
50
+
51
+ getReconnectTimer() {
52
+ return this.reconnectTimer;
53
+ }
54
+ }
55
+
56
+ function makeLogger() {
57
+ return {
58
+ debugCalls: [] as string[],
59
+ errorCalls: [] as any[],
60
+ infoCalls: [] as string[],
61
+ debug(msg: string) {
62
+ this.debugCalls.push(msg);
63
+ },
64
+ error(...args: any[]) {
65
+ this.errorCalls.push(args);
66
+ },
67
+ info(msg: string) {
68
+ this.infoCalls.push(msg);
69
+ },
70
+ };
71
+ }
72
+
73
+ function makeTransport(onReconnected?: () => Promise<void>) {
74
+ const config: McpServerConfig = { transport: "sse", url: "http://localhost:1234/sse" };
75
+ const clientConfig = { reconnectIntervalMs: 10_000 };
76
+ const logger = makeLogger();
77
+ const transport = new TestableTransport(config, clientConfig, logger, onReconnected);
78
+ return { transport, logger };
79
+ }
80
+
81
+ test("handleMessage resolves pending request when response.id matches", async () => {
82
+ const { transport } = makeTransport();
83
+
84
+ let resolved: any;
85
+ const promise = new Promise((resolve, reject) => {
86
+ const timeout = setTimeout(() => reject(new Error("timeout")), 1000);
87
+ transport.getPendingRequests().set(1, { resolve, reject, timeout });
88
+ });
89
+
90
+ transport.testHandleMessage({ jsonrpc: "2.0", id: 1, result: { ok: true } });
91
+
92
+ resolved = await promise;
93
+ assert.deepEqual(resolved, { jsonrpc: "2.0", id: 1, result: { ok: true } });
94
+ assert.equal(transport.getPendingRequests().size, 0);
95
+ });
96
+
97
+ test("handleMessage rejects pending request when response has error", async () => {
98
+ const { transport } = makeTransport();
99
+
100
+ const promise = new Promise((resolve, reject) => {
101
+ const timeout = setTimeout(() => reject(new Error("timeout")), 1000);
102
+ transport.getPendingRequests().set(2, { resolve, reject, timeout });
103
+ });
104
+
105
+ transport.testHandleMessage({ jsonrpc: "2.0", id: 2, error: { code: -1, message: "boom" } });
106
+
107
+ await assert.rejects(promise, /boom/);
108
+ assert.equal(transport.getPendingRequests().size, 0);
109
+ });
110
+
111
+ test("handleMessage ignores messages with unknown id (no crash)", () => {
112
+ const { transport } = makeTransport();
113
+
114
+ assert.doesNotThrow(() => {
115
+ transport.testHandleMessage({ jsonrpc: "2.0", id: 999, result: { ok: true } });
116
+ });
117
+
118
+ assert.equal(transport.getPendingRequests().size, 0);
119
+ });
120
+
121
+ test("handleMessage calls onReconnected on notifications/tools/list_changed", async () => {
122
+ let called = 0;
123
+ const { transport } = makeTransport(async () => {
124
+ called += 1;
125
+ });
126
+
127
+ transport.testHandleMessage({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
128
+
129
+ await new Promise((resolve) => setImmediate(resolve));
130
+ assert.equal(called, 1);
131
+ });
132
+
133
+ test("handleMessage logs debug for unknown notifications", () => {
134
+ const { transport, logger } = makeTransport();
135
+
136
+ assert.doesNotThrow(() => {
137
+ transport.testHandleMessage({ jsonrpc: "2.0", method: "notifications/other" });
138
+ });
139
+
140
+ assert.equal(logger.debugCalls.length, 1);
141
+ assert.match(logger.debugCalls[0], /Unhandled test notification: notifications\/other/);
142
+ });
143
+
144
+ test("rejectAllPending rejects all pending and clears the map", async () => {
145
+ const { transport } = makeTransport();
146
+
147
+ const p1 = new Promise((resolve, reject) => {
148
+ const timeout = setTimeout(() => reject(new Error("timeout")), 1000);
149
+ transport.getPendingRequests().set(1, { resolve, reject, timeout });
150
+ });
151
+ const p2 = new Promise((resolve, reject) => {
152
+ const timeout = setTimeout(() => reject(new Error("timeout")), 1000);
153
+ transport.getPendingRequests().set(2, { resolve, reject, timeout });
154
+ });
155
+
156
+ transport.testRejectAllPending("cancelled");
157
+
158
+ await assert.rejects(p1, /cancelled/);
159
+ await assert.rejects(p2, /cancelled/);
160
+ assert.equal(transport.getPendingRequests().size, 0);
161
+ });
162
+
163
+ test("rejectAllPending with empty map doesn't crash", () => {
164
+ const { transport } = makeTransport();
165
+
166
+ assert.doesNotThrow(() => {
167
+ transport.testRejectAllPending("anything");
168
+ });
169
+
170
+ assert.equal(transport.getPendingRequests().size, 0);
171
+ });
172
+
173
+ test("scheduleReconnect sets connected=false and rejects pending", async () => {
174
+ const { transport } = makeTransport();
175
+ transport.setConnected(true);
176
+
177
+ const pending = new Promise((resolve, reject) => {
178
+ const timeout = setTimeout(() => reject(new Error("timeout")), 1000);
179
+ transport.getPendingRequests().set(10, { resolve, reject, timeout });
180
+ });
181
+
182
+ transport.testScheduleReconnect();
183
+
184
+ assert.equal(transport.isConnected(), false);
185
+ await assert.rejects(pending, /Connection lost, request cancelled/);
186
+ assert.equal(transport.getPendingRequests().size, 0);
187
+
188
+ transport.testCleanupReconnectTimer();
189
+ });
190
+
191
+ test("scheduleReconnect doesn't schedule twice (idempotent)", () => {
192
+ const { transport } = makeTransport();
193
+
194
+ transport.testScheduleReconnect();
195
+ const firstTimer = transport.getReconnectTimer();
196
+
197
+ transport.testScheduleReconnect();
198
+ const secondTimer = transport.getReconnectTimer();
199
+
200
+ assert.ok(firstTimer);
201
+ assert.equal(firstTimer, secondTimer);
202
+
203
+ transport.testCleanupReconnectTimer();
204
+ });
205
+
206
+ test("cleanupReconnectTimer clears the timer", () => {
207
+ const { transport } = makeTransport();
208
+
209
+ transport.testScheduleReconnect();
210
+ assert.ok(transport.getReconnectTimer());
211
+
212
+ transport.testCleanupReconnectTimer();
213
+ assert.equal(transport.getReconnectTimer(), null);
214
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": ".",
11
+ "declaration": true
12
+ },
13
+ "include": ["src/**/*.ts", "bin/**/*.ts"],
14
+ "exclude": ["node_modules", "dist", "tests"]
15
+ }