socketduct 0.1.25 → 0.1.27

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 (58) hide show
  1. package/AGENTS.md +2 -2
  2. package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
  3. package/changelog.d/20261025070000-reclaim-unreadable-retention-lock.md +14 -0
  4. package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
  5. package/package.json +2 -2
  6. package/scripts/test.js +9 -14
  7. package/src/logging/compressed-log-writer.js +1 -1
  8. package/src/logging/log-retention.js +36 -1
  9. package/src/logging/zstd-frame-codec.js +15 -1
  10. package/test/e2e/gateway-process.test.js +93 -92
  11. package/test/e2e/logger-shutdown-process.test.js +40 -37
  12. package/test/e2e/reverse-client-recovery-process.test.js +107 -103
  13. package/test/e2e/reverse-gateway-process.test.js +328 -318
  14. package/test/integration/initial-target-recovery.test.js +333 -334
  15. package/test/integration/logging-cli.test.js +121 -119
  16. package/test/integration/logging-compressed-recovery.test.js +96 -95
  17. package/test/integration/multiplexed-compatibility.test.js +178 -177
  18. package/test/integration/multiplexed-corrections.test.js +360 -359
  19. package/test/integration/multiplexed-failures.test.js +253 -251
  20. package/test/integration/multiplexed-transport-set.test.js +334 -322
  21. package/test/integration/multiplexed-transport.test.js +135 -134
  22. package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
  23. package/test/integration/reverse-gateway.test.js +2309 -2218
  24. package/test/integration/reverse-resume-continuity.test.js +219 -218
  25. package/test/integration/reverse-route-class.test.js +358 -341
  26. package/test/integration/socketduct-http-agent.test.js +314 -312
  27. package/test/integration/socketduct-https-agent.test.js +516 -485
  28. package/test/integration/stream-compression.test.js +862 -824
  29. package/test/integration/tcp-forwarding.test.js +2550 -2447
  30. package/test/integration/tls-forwarding.test.js +593 -592
  31. package/test/unit/auth/auth.test.js +43 -44
  32. package/test/unit/auth/static-token-store.test.js +9 -8
  33. package/test/unit/client/connect.test.js +41 -40
  34. package/test/unit/client/deferred-virtual-socket.test.js +95 -94
  35. package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
  36. package/test/unit/client/multiplexed-transport-set.test.js +830 -748
  37. package/test/unit/client/socketduct-socket.test.js +768 -771
  38. package/test/unit/client/virtual-socket.test.js +198 -168
  39. package/test/unit/config/docker-gateway-files.test.js +224 -224
  40. package/test/unit/config/environment.test.js +472 -492
  41. package/test/unit/index.test.js +53 -52
  42. package/test/unit/logging/compressed-log-worker.test.js +23 -22
  43. package/test/unit/logging/compressed-log-writer.test.js +155 -154
  44. package/test/unit/logging/log-retention.test.js +71 -48
  45. package/test/unit/logging/log-segment-index.test.js +84 -81
  46. package/test/unit/logging/zstd-frame-codec.test.js +28 -19
  47. package/test/unit/protocol/compression.test.js +254 -298
  48. package/test/unit/protocol/frame.test.js +280 -276
  49. package/test/unit/protocol/v3-compression.test.js +21 -23
  50. package/test/unit/protocol/v3-frame.test.js +42 -41
  51. package/test/unit/release-configuration.test.js +15 -12
  52. package/test/unit/reverse/disconnect-attribution.test.js +65 -64
  53. package/test/unit/reverse-terminal-outcomes.test.js +53 -50
  54. package/test/unit/server/control-handler.test.js +232 -231
  55. package/test/unit/server/primary.test.js +51 -49
  56. package/test/unit/server/relay-runtime.test.js +280 -294
  57. package/test/unit/sessions/session.test.js +48 -62
  58. 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
  })