socketduct 0.1.25 → 0.1.26

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 (56) hide show
  1. package/AGENTS.md +2 -2
  2. package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
  3. package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
  4. package/package.json +2 -2
  5. package/scripts/test.js +9 -14
  6. package/src/logging/compressed-log-writer.js +1 -1
  7. package/src/logging/zstd-frame-codec.js +15 -1
  8. package/test/e2e/gateway-process.test.js +93 -92
  9. package/test/e2e/logger-shutdown-process.test.js +40 -37
  10. package/test/e2e/reverse-client-recovery-process.test.js +107 -103
  11. package/test/e2e/reverse-gateway-process.test.js +328 -318
  12. package/test/integration/initial-target-recovery.test.js +333 -334
  13. package/test/integration/logging-cli.test.js +121 -119
  14. package/test/integration/logging-compressed-recovery.test.js +96 -95
  15. package/test/integration/multiplexed-compatibility.test.js +178 -177
  16. package/test/integration/multiplexed-corrections.test.js +360 -359
  17. package/test/integration/multiplexed-failures.test.js +253 -251
  18. package/test/integration/multiplexed-transport-set.test.js +334 -322
  19. package/test/integration/multiplexed-transport.test.js +135 -134
  20. package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
  21. package/test/integration/reverse-gateway.test.js +2309 -2218
  22. package/test/integration/reverse-resume-continuity.test.js +219 -218
  23. package/test/integration/reverse-route-class.test.js +358 -341
  24. package/test/integration/socketduct-http-agent.test.js +314 -312
  25. package/test/integration/socketduct-https-agent.test.js +516 -485
  26. package/test/integration/stream-compression.test.js +862 -824
  27. package/test/integration/tcp-forwarding.test.js +2550 -2447
  28. package/test/integration/tls-forwarding.test.js +593 -592
  29. package/test/unit/auth/auth.test.js +43 -44
  30. package/test/unit/auth/static-token-store.test.js +9 -8
  31. package/test/unit/client/connect.test.js +41 -40
  32. package/test/unit/client/deferred-virtual-socket.test.js +95 -94
  33. package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
  34. package/test/unit/client/multiplexed-transport-set.test.js +830 -748
  35. package/test/unit/client/socketduct-socket.test.js +768 -771
  36. package/test/unit/client/virtual-socket.test.js +198 -168
  37. package/test/unit/config/docker-gateway-files.test.js +224 -224
  38. package/test/unit/config/environment.test.js +472 -492
  39. package/test/unit/index.test.js +53 -52
  40. package/test/unit/logging/compressed-log-worker.test.js +23 -22
  41. package/test/unit/logging/compressed-log-writer.test.js +155 -154
  42. package/test/unit/logging/log-retention.test.js +52 -47
  43. package/test/unit/logging/log-segment-index.test.js +84 -81
  44. package/test/unit/logging/zstd-frame-codec.test.js +28 -19
  45. package/test/unit/protocol/compression.test.js +254 -298
  46. package/test/unit/protocol/frame.test.js +280 -276
  47. package/test/unit/protocol/v3-compression.test.js +21 -23
  48. package/test/unit/protocol/v3-frame.test.js +42 -41
  49. package/test/unit/release-configuration.test.js +15 -12
  50. package/test/unit/reverse/disconnect-attribution.test.js +65 -64
  51. package/test/unit/reverse-terminal-outcomes.test.js +53 -50
  52. package/test/unit/server/control-handler.test.js +232 -231
  53. package/test/unit/server/primary.test.js +51 -49
  54. package/test/unit/server/relay-runtime.test.js +280 -294
  55. package/test/unit/sessions/session.test.js +48 -62
  56. package/test/unit/spool/spool-file.test.js +401 -385
@@ -1,7 +1,6 @@
1
1
  /* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param-type, jsdoc/require-returns */
2
- import assert from "node:assert/strict"
2
+ import {describe, expect, it, withTestCleanup} from "@velocious/testing"
3
3
  import {createServer} from "node:net"
4
- import test from "node:test"
5
4
 
6
5
  import {StaticTokenStore} from "../../src/auth/static-token-store.js"
7
6
  import {MultiplexedSocketductConnection} from "../../src/client/multiplexed-connection.js"
@@ -17,7 +16,7 @@ async function listen(server) {
17
16
  })
18
17
  })
19
18
  const address = server.address()
20
- assert(address !== null && typeof address !== "string")
19
+ if (!address || typeof address === "string") throw new Error("expected AddressInfo")
21
20
  return address.port
22
21
  }
23
22
 
@@ -50,7 +49,7 @@ async function readExactly(socket, expected) {
50
49
  if (value.length < expected.length) return
51
50
  cleanup()
52
51
  try {
53
- assert.equal(value, expected)
52
+ expect(value).toBe(expected)
54
53
  resolve(undefined)
55
54
  } catch (error) {
56
55
  reject(error)
@@ -65,144 +64,146 @@ async function readExactly(socket, expected) {
65
64
  })
66
65
  }
67
66
 
68
- test("two virtual sockets use one authenticated physical transport", async (t) => {
69
- let targetAccepts = 0
70
- const target = createServer((socket) => {
71
- targetAccepts += 1
72
- socket.on("data", (chunk) => socket.write(chunk))
73
- })
74
- const targetPort = await listen(target)
75
- t.after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
76
-
77
- const relay = await startMultiplexedTcpRelay({
78
- tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
79
- authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort
80
- })
81
- t.after(() => relay.close())
67
+ describe("multiplexed transport", () => {
68
+ it("two virtual sockets use one authenticated physical transport", async () => withTestCleanup(async (after) => {
69
+ let targetAccepts = 0
70
+ const target = createServer((socket) => {
71
+ targetAccepts += 1
72
+ socket.on("data", (chunk) => socket.write(chunk))
73
+ })
74
+ const targetPort = await listen(target)
75
+ after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
82
76
 
83
- const connection = new MultiplexedSocketductConnection({
84
- relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}
85
- })
86
- t.after(() => connection.close())
77
+ const relay = await startMultiplexedTcpRelay({
78
+ tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
79
+ authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort
80
+ })
81
+ after(() => relay.close())
87
82
 
88
- const first = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "first"})
89
- const second = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "second"})
90
- await Promise.all([onceEvent(first, "connect"), onceEvent(second, "connect")])
83
+ const connection = new MultiplexedSocketductConnection({
84
+ relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}
85
+ })
86
+ after(() => connection.close())
91
87
 
92
- first.write("one")
93
- second.write("two")
94
- await Promise.all([readExactly(first, "one"), readExactly(second, "two")])
88
+ const first = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "first"})
89
+ const second = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "second"})
90
+ await Promise.all([onceEvent(first, "connect"), onceEvent(second, "connect")])
95
91
 
96
- assert.equal(relay.status().physicalAccepts, 1)
97
- assert.equal(relay.status().authenticatedConnections, 1)
98
- assert.equal(targetAccepts, 2)
99
- assert.equal(connection.status().virtualStreams, 2)
92
+ first.write("one")
93
+ second.write("two")
94
+ await Promise.all([readExactly(first, "one"), readExactly(second, "two")])
100
95
 
101
- first.destroy()
102
- assert.equal(second.destroyed, false)
103
- second.destroy()
104
- })
96
+ expect(relay.status().physicalAccepts).toBe(1)
97
+ expect(relay.status().authenticatedConnections).toBe(1)
98
+ expect(targetAccepts).toBe(2)
99
+ expect(connection.status().virtualStreams).toBe(2)
105
100
 
106
- test("one transport drop has one reconnect owner and resumes sibling streams without gaps", async (t) => {
107
- /** @type {Array<{event: string} & Record<string, unknown>>} */
108
- const lifecycle = []
109
- const logger = {log: (/** @type {string} */ event, /** @type {Record<string, unknown>} */ fields = {}) => lifecycle.push({event, ...fields})}
110
- const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
111
- const targetPort = await listen(target)
112
- t.after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
113
- const relay = await startMultiplexedTcpRelay({
114
- tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
115
- authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort
116
- })
117
- t.after(() => relay.close())
118
- const connection = new MultiplexedSocketductConnection({
119
- relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
120
- reconnectDelayMs: 10,
121
- logger
122
- })
123
- t.after(() => connection.close())
124
- const first = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "resume-a"})
125
- const second = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "resume-b"})
126
- await Promise.all([onceEvent(first, "connect"), onceEvent(second, "connect")])
127
- first.write("before-a")
128
- second.write("before-b")
129
- await Promise.all([readExactly(first, "before-a"), readExactly(second, "before-b")])
130
-
131
- const ready = Promise.all([onceEvent(first, "ready"), onceEvent(second, "ready")])
132
- await relay.closeActiveTransport()
133
- first.write("after-a")
134
- second.write("after-b")
135
- await ready
136
- await Promise.all([readExactly(first, "after-a"), readExactly(second, "after-b")])
137
-
138
- assert.equal(relay.status().physicalAccepts, 2)
139
- assert.equal(connection.status().reconnectAttempts, 1)
140
- assert.equal(connection.status().reconnectSuccesses, 1)
141
- assert.equal(relay.status().resumeSuccesses, 2)
142
- const closed = Promise.all([onceEvent(first, "close"), onceEvent(second, "close")])
143
- first.destroy()
144
- second.destroy()
145
- await closed
146
- for (const event of [
147
- "transport.connecting",
148
- "transport.connected",
149
- "transport.disconnected",
150
- "transport.reconnect_scheduled",
151
- "transport.reconnect_succeeded",
152
- "virtual_stream.opening",
153
- "virtual_stream.opened",
154
- "virtual_stream.closed"
155
- ]) assert(lifecycle.some((entry) => entry.event === event), `missing ${event}`)
156
- assert.doesNotMatch(JSON.stringify(lifecycle), /valid-token|resume-a|resume-b|127\.0\.0\.1/)
157
- })
101
+ first.destroy()
102
+ expect(second.destroyed).toBe(false)
103
+ second.destroy()
104
+ }))
158
105
 
159
- test("100 concurrent build-like streams create zero extra physical accepts", async (t) => {
160
- let targetAccepts = 0
161
- const target = createServer((socket) => {
162
- targetAccepts += 1
163
- socket.on("data", (chunk) => {
164
- const split = Math.max(1, Math.floor(chunk.length / 2))
165
- socket.write(chunk.subarray(0, split))
166
- socket.write(chunk.subarray(split))
106
+ it("one transport drop has one reconnect owner and resumes sibling streams without gaps", async () => withTestCleanup(async (after) => {
107
+ /** @type {Array<{event: string} & Record<string, unknown>>} */
108
+ const lifecycle = []
109
+ const logger = {log: (/** @type {string} */ event, /** @type {Record<string, unknown>} */ fields = {}) => lifecycle.push({event, ...fields})}
110
+ const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
111
+ const targetPort = await listen(target)
112
+ after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
113
+ const relay = await startMultiplexedTcpRelay({
114
+ tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
115
+ authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort
167
116
  })
168
- })
169
- const targetPort = await listen(target)
170
- t.after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
171
- const relay = await startMultiplexedTcpRelay({
172
- tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
173
- authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort,
174
- maxStreams: 128
175
- })
176
- t.after(() => relay.close())
177
- const connection = new MultiplexedSocketductConnection({
178
- relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
179
- maxStreams: 128
180
- })
181
- t.after(() => connection.close())
117
+ after(() => relay.close())
118
+ const connection = new MultiplexedSocketductConnection({
119
+ relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
120
+ reconnectDelayMs: 10,
121
+ logger
122
+ })
123
+ after(() => connection.close())
124
+ const first = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "resume-a"})
125
+ const second = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "resume-b"})
126
+ await Promise.all([onceEvent(first, "connect"), onceEvent(second, "connect")])
127
+ first.write("before-a")
128
+ second.write("before-b")
129
+ await Promise.all([readExactly(first, "before-a"), readExactly(second, "before-b")])
130
+
131
+ const ready = Promise.all([onceEvent(first, "ready"), onceEvent(second, "ready")])
132
+ await relay.closeActiveTransport()
133
+ first.write("after-a")
134
+ second.write("after-b")
135
+ await ready
136
+ await Promise.all([readExactly(first, "after-a"), readExactly(second, "after-b")])
137
+
138
+ expect(relay.status().physicalAccepts).toBe(2)
139
+ expect(connection.status().reconnectAttempts).toBe(1)
140
+ expect(connection.status().reconnectSuccesses).toBe(1)
141
+ expect(relay.status().resumeSuccesses).toBe(2)
142
+ const closed = Promise.all([onceEvent(first, "close"), onceEvent(second, "close")])
143
+ first.destroy()
144
+ second.destroy()
145
+ await closed
146
+ for (const event of [
147
+ "transport.connecting",
148
+ "transport.connected",
149
+ "transport.disconnected",
150
+ "transport.reconnect_scheduled",
151
+ "transport.reconnect_succeeded",
152
+ "virtual_stream.opening",
153
+ "virtual_stream.opened",
154
+ "virtual_stream.closed"
155
+ ]) expect(lifecycle.some((entry) => entry.event === event)).toBeTruthy() // missing ${event}
156
+ expect(JSON.stringify(lifecycle)).not.toMatch(/valid-token|resume-a|resume-b|127\.0\.0\.1/)
157
+ }))
182
158
 
183
- const sockets = Array.from({length: 100}, (_, index) => connection.createConnection({
184
- host: "127.0.0.1",
185
- port: targetPort,
186
- sessionName: `build-flow-${index + 1}`
159
+ it("100 concurrent build-like streams create zero extra physical accepts", async () => withTestCleanup(async (after) => {
160
+ let targetAccepts = 0
161
+ const target = createServer((socket) => {
162
+ targetAccepts += 1
163
+ socket.on("data", (chunk) => {
164
+ const split = Math.max(1, Math.floor(chunk.length / 2))
165
+ socket.write(chunk.subarray(0, split))
166
+ socket.write(chunk.subarray(split))
167
+ })
168
+ })
169
+ const targetPort = await listen(target)
170
+ after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
171
+ const relay = await startMultiplexedTcpRelay({
172
+ tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
173
+ authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort,
174
+ maxStreams: 128
175
+ })
176
+ after(() => relay.close())
177
+ const connection = new MultiplexedSocketductConnection({
178
+ relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
179
+ maxStreams: 128
180
+ })
181
+ after(() => connection.close())
182
+
183
+ const sockets = Array.from({length: 100}, (_, index) => connection.createConnection({
184
+ host: "127.0.0.1",
185
+ port: targetPort,
186
+ sessionName: `build-flow-${index + 1}`
187
+ }))
188
+ await Promise.all(sockets.map((socket) => onceEvent(socket, "connect")))
189
+ const reads = sockets.map((socket, index) => readExactly(socket, `flow-${index + 1}:${"x".repeat(1024)}`))
190
+ sockets.forEach((socket, index) => socket.write(`flow-${index + 1}:${"x".repeat(1024)}`))
191
+ await Promise.all(reads)
192
+
193
+ expect(relay.status().physicalAccepts).toBe(1)
194
+ expect(relay.status().physicalConnections).toBe(1)
195
+ expect(targetAccepts).toBe(100)
196
+ expect(connection.status().virtualStreams).toBe(100)
197
+
198
+ const resumed = Promise.all(sockets.map((socket) => onceEvent(socket, "ready")))
199
+ await relay.closeActiveTransport()
200
+ await resumed
201
+ const resumedReads = sockets.map((socket, index) => readExactly(socket, `resumed-${index + 1}`))
202
+ sockets.forEach((socket, index) => socket.write(`resumed-${index + 1}`))
203
+ await Promise.all(resumedReads)
204
+ expect(relay.status().physicalAccepts).toBe(2)
205
+ expect(relay.status().resumeSuccesses).toBe(100)
206
+ expect(connection.status().reconnectAttempts).toBe(1)
207
+ sockets.forEach((socket) => socket.destroy())
187
208
  }))
188
- await Promise.all(sockets.map((socket) => onceEvent(socket, "connect")))
189
- const reads = sockets.map((socket, index) => readExactly(socket, `flow-${index + 1}:${"x".repeat(1024)}`))
190
- sockets.forEach((socket, index) => socket.write(`flow-${index + 1}:${"x".repeat(1024)}`))
191
- await Promise.all(reads)
192
-
193
- assert.equal(relay.status().physicalAccepts, 1)
194
- assert.equal(relay.status().physicalConnections, 1)
195
- assert.equal(targetAccepts, 100)
196
- assert.equal(connection.status().virtualStreams, 100)
197
-
198
- const resumed = Promise.all(sockets.map((socket) => onceEvent(socket, "ready")))
199
- await relay.closeActiveTransport()
200
- await resumed
201
- const resumedReads = sockets.map((socket, index) => readExactly(socket, `resumed-${index + 1}`))
202
- sockets.forEach((socket, index) => socket.write(`resumed-${index + 1}`))
203
- await Promise.all(resumedReads)
204
- assert.equal(relay.status().physicalAccepts, 2)
205
- assert.equal(relay.status().resumeSuccesses, 100)
206
- assert.equal(connection.status().reconnectAttempts, 1)
207
- sockets.forEach((socket) => socket.destroy())
208
209
  })
@@ -1,9 +1,8 @@
1
- import assert from "node:assert/strict"
1
+ import {describe, expect, it, withTestCleanup} from "@velocious/testing"
2
2
  import {mkdtemp, rm} from "node:fs/promises"
3
3
  import {createServer} from "node:net"
4
4
  import {tmpdir} from "node:os"
5
5
  import {join} from "node:path"
6
- import test from "node:test"
7
6
 
8
7
  import {StaticTokenStore} from "../../src/auth/static-token-store.js"
9
8
  import {withTimeout, waitUntil} from "awaitery"
@@ -11,99 +10,101 @@ import {OutboundReverseGateway, ReverseGatewayConnection, startReverseGatewayHub
11
10
  import {generateTestCertificates} from "../helpers/tls-certificates.js"
12
11
  import {listen, onceEvent} from "../helpers/test-async.js"
13
12
 
14
- test("gateway resets its reverse stream high-water on fresh registration after a hub restart", async (t) => {
15
- const root = await mkdtemp(join(tmpdir(), "socketduct-reverse-streamid-"))
16
- t.after(() => rm(root, {recursive: true, force: true}))
17
- const certificates = generateTestCertificates(root)
13
+ describe("reverse gateway stream id reset", () => {
14
+ it("gateway resets its reverse stream high-water on fresh registration after a hub restart", async () => withTestCleanup(async (after) => {
15
+ const root = await mkdtemp(join(tmpdir(), "socketduct-reverse-streamid-"))
16
+ after(() => rm(root, {recursive: true, force: true}))
17
+ const certificates = generateTestCertificates(root)
18
18
 
19
- let targetAccepts = 0
20
- const targetSockets = new Set()
21
- const target = createServer((socket) => {
22
- targetAccepts += 1
23
- targetSockets.add(socket)
24
- socket.once("close", () => targetSockets.delete(socket))
25
- socket.on("data", (chunk) => socket.write(chunk))
26
- })
27
- const targetPort = await listen(target)
28
- t.after(() => {
29
- for (const socket of targetSockets) socket.destroy()
30
- return new Promise((resolve) => target.close(() => resolve(undefined)))
31
- })
19
+ let targetAccepts = 0
20
+ const targetSockets = new Set()
21
+ const target = createServer((socket) => {
22
+ targetAccepts += 1
23
+ targetSockets.add(socket)
24
+ socket.once("close", () => targetSockets.delete(socket))
25
+ socket.on("data", (chunk) => socket.write(chunk))
26
+ })
27
+ const targetPort = await listen(target)
28
+ after(() => {
29
+ for (const socket of targetSockets) socket.destroy()
30
+ return new Promise((resolve) => target.close(() => resolve(undefined)))
31
+ })
32
32
 
33
- const tokens = new StaticTokenStore([
34
- {username: "home-o2", token: "gateway-token"},
35
- {username: "tensorbuzz", token: "client-token"}
36
- ])
37
- const hubOptions = {
38
- tokens,
39
- tls: {certFile: certificates.serverCertFile, keyFile: certificates.serverKeyFile},
40
- knownGatewayIds: ["o2"],
41
- authorizeGateway: (/** @type {{principal: {username: string}, gatewayId: string}} */ {principal, gatewayId}) => principal.username === "home-o2" && gatewayId === "o2",
42
- authorizeClientGateway: (/** @type {{principal: {username: string}, gatewayId: string}} */ {principal, gatewayId}) => principal.username === "tensorbuzz" && gatewayId === "o2",
43
- authorizeOpen: (/** @type {{principal: {username: string}, gatewayId: string, target: {host: string, port: number}}} */ {principal, gatewayId, target: requested}) => principal.username === "tensorbuzz" && gatewayId === "o2" && requested.host === "127.0.0.1" && requested.port === targetPort
44
- }
45
- let hub = await startReverseGatewayHub(hubOptions)
46
- t.after(() => hub.close())
33
+ const tokens = new StaticTokenStore([
34
+ {username: "home-o2", token: "gateway-token"},
35
+ {username: "tensorbuzz", token: "client-token"}
36
+ ])
37
+ const hubOptions = {
38
+ tokens,
39
+ tls: {certFile: certificates.serverCertFile, keyFile: certificates.serverKeyFile},
40
+ knownGatewayIds: ["o2"],
41
+ authorizeGateway: (/** @type {{principal: {username: string}, gatewayId: string}} */ {principal, gatewayId}) => principal.username === "home-o2" && gatewayId === "o2",
42
+ authorizeClientGateway: (/** @type {{principal: {username: string}, gatewayId: string}} */ {principal, gatewayId}) => principal.username === "tensorbuzz" && gatewayId === "o2",
43
+ authorizeOpen: (/** @type {{principal: {username: string}, gatewayId: string, target: {host: string, port: number}}} */ {principal, gatewayId, target: requested}) => principal.username === "tensorbuzz" && gatewayId === "o2" && requested.host === "127.0.0.1" && requested.port === targetPort
44
+ }
45
+ let hub = await startReverseGatewayHub(hubOptions)
46
+ after(() => hub.close())
47
47
 
48
- const gateway = new OutboundReverseGateway({
49
- gatewayId: "o2",
50
- connectionId: "streamid_gateway",
51
- hub: {host: "127.0.0.1", port: hub.port, token: "gateway-token"},
52
- tls: {caFile: certificates.caCertFile, servername: "localhost"},
53
- authorizeTarget: (requested) => requested.host === "127.0.0.1" && requested.port === targetPort,
54
- reconnectDelayMs: 10,
55
- maxReconnectDelayMs: 20
56
- })
57
- t.after(() => gateway.close())
58
- await withTimeout(gateway.start(), "gateway registration")
48
+ const gateway = new OutboundReverseGateway({
49
+ gatewayId: "o2",
50
+ connectionId: "streamid_gateway",
51
+ hub: {host: "127.0.0.1", port: hub.port, token: "gateway-token"},
52
+ tls: {caFile: certificates.caCertFile, servername: "localhost"},
53
+ authorizeTarget: (requested) => requested.host === "127.0.0.1" && requested.port === targetPort,
54
+ reconnectDelayMs: 10,
55
+ maxReconnectDelayMs: 20
56
+ })
57
+ after(() => gateway.close())
58
+ await withTimeout(gateway.start(), "gateway registration")
59
59
 
60
- const client = new ReverseGatewayConnection({
61
- gatewayId: "o2",
62
- connectionId: "streamid_client",
63
- hub: {host: "127.0.0.1", port: hub.port, token: "client-token"},
64
- tls: {caFile: certificates.caCertFile, servername: "localhost"},
65
- reconnectDelayMs: 10,
66
- maxReconnectDelayMs: 20
67
- })
68
- t.after(() => client.close())
69
- await withTimeout(client.connect(), "client authentication")
60
+ const client = new ReverseGatewayConnection({
61
+ gatewayId: "o2",
62
+ connectionId: "streamid_client",
63
+ hub: {host: "127.0.0.1", port: hub.port, token: "client-token"},
64
+ tls: {caFile: certificates.caCertFile, servername: "localhost"},
65
+ reconnectDelayMs: 10,
66
+ maxReconnectDelayMs: 20
67
+ })
68
+ after(() => client.close())
69
+ await withTimeout(client.connect(), "client authentication")
70
70
 
71
- // Open the first stream: this raises the gateway's process-lifetime
72
- // #lastStreamId high-water mark above the fresh registration's start.
73
- const first = client.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "before-restart"})
74
- await withTimeout(onceEvent(first, "connect"), "first reverse stream opens")
75
- first.write("first")
76
- await new Promise((resolve, reject) => {
77
- first.once("data", (/** @type {Buffer} */ chunk) => { assert.equal(chunk.toString(), "first"); resolve(undefined) })
78
- first.once("error", reject)
79
- })
80
- const firstClosed = onceEvent(first, "close")
81
- first.end()
82
- await withTimeout(firstClosed, "first stream close")
83
- // Fully retire the first stream on every peer before the hub restarts so
84
- // the only effect of the restart is the fresh (non-resumed) registration.
85
- await waitUntil(() => client.status().virtualStreams === 0 && gateway.status().virtualStreams === 0 && hub.status().virtualStreams === 0, "first stream retired")
71
+ // Open the first stream: this raises the gateway's process-lifetime
72
+ // #lastStreamId high-water mark above the fresh registration's start.
73
+ const first = client.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "before-restart"})
74
+ await withTimeout(onceEvent(first, "connect"), "first reverse stream opens")
75
+ first.write("first")
76
+ await new Promise((resolve, reject) => {
77
+ first.once("data", (/** @type {Buffer} */ chunk) => { expect(chunk.toString()).toBe("first"); resolve(undefined) })
78
+ first.once("error", reject)
79
+ })
80
+ const firstClosed = onceEvent(first, "close")
81
+ first.end()
82
+ await withTimeout(firstClosed, "first stream close")
83
+ // Fully retire the first stream on every peer before the hub restarts so
84
+ // the only effect of the restart is the fresh (non-resumed) registration.
85
+ await waitUntil(() => client.status().virtualStreams === 0 && gateway.status().virtualStreams === 0 && hub.status().virtualStreams === 0, "first stream retired")
86
86
 
87
- // Restart the hub so the gateway reconnects with a fresh (non-resumed)
88
- // registration whose stream ids restart from the low end.
89
- const port = hub.port
90
- await withTimeout(hub.close(), "hub close")
91
- hub = await startReverseGatewayHub({...hubOptions, port})
92
- t.after(() => hub.close())
93
- await waitUntil(() => gateway.status().physicalState === "ready" && client.status().physicalState === "ready" && gateway.status().physicalConnects >= 2 && client.status().physicalConnects >= 2, "peers after hub restart")
87
+ // Restart the hub so the gateway reconnects with a fresh (non-resumed)
88
+ // registration whose stream ids restart from the low end.
89
+ const port = hub.port
90
+ await withTimeout(hub.close(), "hub close")
91
+ hub = await startReverseGatewayHub({...hubOptions, port})
92
+ after(() => hub.close())
93
+ await waitUntil(() => gateway.status().physicalState === "ready" && client.status().physicalState === "ready" && gateway.status().physicalConnects >= 2 && client.status().physicalConnects >= 2, "peers after hub restart")
94
94
 
95
- // Without the high-water reset, this OPEN is rejected as
96
- // "Invalid reverse gateway stream ID" and the gateway wedges.
97
- const second = client.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "after-restart"})
98
- await withTimeout(onceEvent(second, "connect"), "second reverse stream opens after hub restart")
99
- second.write("second")
100
- await new Promise((resolve, reject) => {
101
- second.once("data", (/** @type {Buffer} */ chunk) => { assert.equal(chunk.toString(), "second"); resolve(undefined) })
102
- second.once("error", reject)
103
- })
104
- const secondClosed = onceEvent(second, "close")
105
- second.end()
106
- await withTimeout(secondClosed, "second stream close")
95
+ // Without the high-water reset, this OPEN is rejected as
96
+ // "Invalid reverse gateway stream ID" and the gateway wedges.
97
+ const second = client.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "after-restart"})
98
+ await withTimeout(onceEvent(second, "connect"), "second reverse stream opens after hub restart")
99
+ second.write("second")
100
+ await new Promise((resolve, reject) => {
101
+ second.once("data", (/** @type {Buffer} */ chunk) => { expect(chunk.toString()).toBe("second"); resolve(undefined) })
102
+ second.once("error", reject)
103
+ })
104
+ const secondClosed = onceEvent(second, "close")
105
+ second.end()
106
+ await withTimeout(secondClosed, "second stream close")
107
107
 
108
- assert.ok(targetAccepts >= 2, `expected at least two target connections, got ${targetAccepts}`)
108
+ expect(targetAccepts).toBeGreaterThanOrEqual(2) // expected at least two target connections, got ${targetAccepts}
109
+ }))
109
110
  })