socketduct 0.1.15

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 (138) hide show
  1. package/.dockerignore +11 -0
  2. package/.env.example +131 -0
  3. package/AGENTS.md +168 -0
  4. package/Dockerfile +30 -0
  5. package/README.md +134 -0
  6. package/changelog.d/20260716083144-v2-ack-wire-format.md +3 -0
  7. package/changelog.d/20260716092044-v2-only-stream-framing.md +1 -0
  8. package/changelog.d/20260727113000-relay-connect-timeout-destroy.md +1 -0
  9. package/changelog.d/20260813093000-private-relay-hardening.md +1 -0
  10. package/changelog.d/20260824183412-initial-target-recovery-https.md +1 -0
  11. package/changelog.d/20260824230000-tls-relay-session-close.md +1 -0
  12. package/changelog.d/20260825120000-reconnect-close-target-timeout.md +1 -0
  13. package/changelog.d/20260829120000-protocol-v3-multiplexing.md +1 -0
  14. package/changelog.d/20260829160000-v3-review-corrections.md +1 -0
  15. package/changelog.d/20260830000000-fix-socket-idle-timeout.md +1 -0
  16. package/changelog.d/20260830090000-multi-gateway-transport-set.md +1 -0
  17. package/changelog.d/20260830093000-relay-close-settlement.md +1 -0
  18. package/changelog.d/20260830120000-immutable-gateway-image.md +1 -0
  19. package/changelog.d/20260902210000-native-reverse-gateway.md +1 -0
  20. package/changelog.d/20260902230000-relay-resume-terminal-order.md +1 -0
  21. package/changelog.d/20260902230708-multiplexed-open-ownership.md +1 -0
  22. package/changelog.d/20260902231541-multiplexed-terminal-close.md +1 -0
  23. package/changelog.d/20260902231542-reverse-close-position.md +1 -0
  24. package/changelog.d/20260902231543-reverse-terminal-connect.md +1 -0
  25. package/changelog.d/20260902233000-reverse-gateway-bounds.md +1 -0
  26. package/changelog.d/20260903001000-reverse-data-ordering.md +1 -0
  27. package/changelog.d/20260903003000-reverse-hub-restart-window.md +1 -0
  28. package/changelog.d/20260903010000-reverse-terminal-lifecycle.md +1 -0
  29. package/changelog.d/20260904142743-transport-set-readiness-wait.md +1 -0
  30. package/changelog.d/20260904152855-deferred-open-snapshots-binary-views.md +1 -0
  31. package/changelog.d/20260904163041-tensorbuzz-container-workflow.md +1 -0
  32. package/changelog.d/20260904174955-transport-set-terminal-causes.md +1 -0
  33. package/changelog.d/20260905081000-contain-reverse-auth-write-errors.md +1 -0
  34. package/changelog.d/20260905100520-reverse-hub-close-race.md +1 -0
  35. package/changelog.d/20260905120000-persistent-jsonl-logging.md +1 -0
  36. package/changelog.d/20260908071009-jsonl-partial-append-recovery.md +1 -0
  37. package/changelog.d/20260908074916-private-log-directory-setup.md +1 -0
  38. package/changelog.d/20260909093057-reverse-large-write-backpressure.md +1 -0
  39. package/changelog.d/20260909135830-actionable-error-diagnostics.md +1 -0
  40. package/changelog.d/20260909150000-transport-set-error-redaction.md +1 -0
  41. package/docker-compose.dev.yml +19 -0
  42. package/docker-compose.reverse-gateway.yml +36 -0
  43. package/docker-compose.yml +73 -0
  44. package/docs/architecture.md +264 -0
  45. package/docs/docker-topology.md +164 -0
  46. package/docs/docker.md +281 -0
  47. package/docs/logging.md +185 -0
  48. package/docs/plans/initial-implementation.md +205 -0
  49. package/docs/plans/socket-docker-integration.md +221 -0
  50. package/docs/protocol.md +558 -0
  51. package/docs/releasing.md +18 -0
  52. package/docs/reverse-gateway.md +186 -0
  53. package/docs/runbooks/jsonl-integrity-rollout.md +144 -0
  54. package/docs/running.md +215 -0
  55. package/docs/security.md +258 -0
  56. package/docs/session-lifecycle.md +127 -0
  57. package/docs/socket-api.md +448 -0
  58. package/eslint.config.js +36 -0
  59. package/package.json +44 -0
  60. package/scripts/deploy-gateway.js +194 -0
  61. package/scripts/test.js +28 -0
  62. package/src/auth/passwords.js +38 -0
  63. package/src/auth/static-token-store.js +41 -0
  64. package/src/auth/user-store.js +139 -0
  65. package/src/client/connect.js +76 -0
  66. package/src/client/deferred-virtual-socket.js +312 -0
  67. package/src/client/http-agent.js +101 -0
  68. package/src/client/https-agent.js +366 -0
  69. package/src/client/multiplexed-connection.js +1504 -0
  70. package/src/client/multiplexed-transport-set.js +917 -0
  71. package/src/client/multiplexed.js +3 -0
  72. package/src/client/socketduct-socket.js +1174 -0
  73. package/src/client/virtual-socket.js +256 -0
  74. package/src/config/environment.js +400 -0
  75. package/src/edge/tcp-edge.js +82 -0
  76. package/src/index.js +101 -0
  77. package/src/logging/structured-logger.js +745 -0
  78. package/src/protocol/compression.js +325 -0
  79. package/src/protocol/frame.js +341 -0
  80. package/src/protocol/v3-compression.js +44 -0
  81. package/src/protocol/v3-frame.js +209 -0
  82. package/src/proxy/multiplexed-relay.js +798 -0
  83. package/src/proxy/tcp-relay.js +1191 -0
  84. package/src/relay.js +3 -0
  85. package/src/reverse/client.js +721 -0
  86. package/src/reverse/common.js +88 -0
  87. package/src/reverse/gateway.js +663 -0
  88. package/src/reverse/hub.js +563 -0
  89. package/src/reverse/index.js +3 -0
  90. package/src/reverse/runtime.js +78 -0
  91. package/src/server/control-handler.js +170 -0
  92. package/src/server/primary.js +64 -0
  93. package/src/server/relay-runtime.js +124 -0
  94. package/src/server/worker.js +39 -0
  95. package/src/sessions/session.js +212 -0
  96. package/src/spool/spool-file.js +451 -0
  97. package/tensorbuzz.yml +36 -0
  98. package/test/e2e/gateway-process.test.js +262 -0
  99. package/test/e2e/reverse-gateway-process.test.js +285 -0
  100. package/test/fixtures/logging/enospc-matrix.mjs +238 -0
  101. package/test/fixtures/logging/enospc-recovery.mjs +146 -0
  102. package/test/fixtures/logging/process-writer.mjs +20 -0
  103. package/test/helpers/tls-certificates.js +115 -0
  104. package/test/integration/initial-target-recovery.test.js +518 -0
  105. package/test/integration/logging-recovery.test.js +163 -0
  106. package/test/integration/multiplexed-compatibility.test.js +254 -0
  107. package/test/integration/multiplexed-corrections.test.js +551 -0
  108. package/test/integration/multiplexed-failures.test.js +339 -0
  109. package/test/integration/multiplexed-transport-set.test.js +456 -0
  110. package/test/integration/multiplexed-transport.test.js +208 -0
  111. package/test/integration/reverse-gateway.test.js +1293 -0
  112. package/test/integration/socketduct-http-agent.test.js +435 -0
  113. package/test/integration/socketduct-https-agent.test.js +669 -0
  114. package/test/integration/stream-compression.test.js +1267 -0
  115. package/test/integration/tcp-forwarding.test.js +2878 -0
  116. package/test/integration/tls-forwarding.test.js +748 -0
  117. package/test/unit/auth/auth.test.js +64 -0
  118. package/test/unit/auth/static-token-store.test.js +14 -0
  119. package/test/unit/client/connect.test.js +58 -0
  120. package/test/unit/client/multiplexed-spool-budget.test.js +309 -0
  121. package/test/unit/client/multiplexed-transport-set.test.js +906 -0
  122. package/test/unit/client/socketduct-socket.test.js +1124 -0
  123. package/test/unit/client/virtual-socket.test.js +98 -0
  124. package/test/unit/config/docker-gateway-files.test.js +274 -0
  125. package/test/unit/config/environment.test.js +507 -0
  126. package/test/unit/index.test.js +78 -0
  127. package/test/unit/logging/structured-logger.test.js +674 -0
  128. package/test/unit/protocol/compression.test.js +421 -0
  129. package/test/unit/protocol/frame.test.js +336 -0
  130. package/test/unit/protocol/v3-compression.test.js +31 -0
  131. package/test/unit/protocol/v3-frame.test.js +57 -0
  132. package/test/unit/release-configuration.test.js +21 -0
  133. package/test/unit/server/control-handler.test.js +291 -0
  134. package/test/unit/server/primary.test.js +59 -0
  135. package/test/unit/server/relay-runtime.test.js +434 -0
  136. package/test/unit/sessions/session.test.js +84 -0
  137. package/test/unit/spool/spool-file.test.js +295 -0
  138. package/tsconfig.json +20 -0
@@ -0,0 +1,312 @@
1
+ /* eslint-disable jsdoc/escape-inline-tags, jsdoc/no-undefined-types, jsdoc/require-returns */
2
+ import {SocketductStreamError, VirtualSocket} from "./virtual-socket.js"
3
+
4
+ /**
5
+ * @typedef {object} PendingByteBudget
6
+ * @property {(bytes: number) => void} reserveMemory - Reserve pending application bytes.
7
+ * @property {(bytes: number) => void} releaseMemory - Release pending application bytes.
8
+ * @typedef {object} DeferredVirtualSocketOptions
9
+ * @property {PendingByteBudget} budget - Set-wide byte budget.
10
+ * @property {() => void} onCancel - Remove an unassigned socket from the set queue.
11
+ * @property {() => void} ref - Reference set resources.
12
+ * @property {() => void} unref - Unreference set resources.
13
+ * @typedef {object} WriteReservation
14
+ * @property {number} bytes - Reserved bytes.
15
+ * @property {boolean} held - Whether the reservation is still charged.
16
+ */
17
+
18
+ /** Bridges one not-yet-assigned socket into its eventual member VirtualSocket. */
19
+ class DeferredSocketOwner {
20
+ /** @type {PendingByteBudget} */
21
+ #budget
22
+
23
+ /** @type {() => void} */
24
+ #onCancel
25
+
26
+ /** @type {() => void} */
27
+ #ref
28
+
29
+ /** @type {() => void} */
30
+ #unref
31
+
32
+ /** @type {DeferredVirtualSocket | undefined} */
33
+ #outer
34
+
35
+ /** @type {VirtualSocket | undefined} */
36
+ #inner
37
+
38
+ /** @type {WriteReservation[]} */
39
+ #writeReservations = []
40
+
41
+ /** @type {{data: Buffer, callback: (error?: Error | null) => void, reservation: WriteReservation} | undefined} */
42
+ #activeWrite
43
+
44
+ /** @type {((error?: Error | null) => void) | undefined} */
45
+ #activeFinal
46
+
47
+ /** @type {boolean} */
48
+ #cancelled = false
49
+
50
+ /** @param {DeferredVirtualSocketOptions} options - Set-owned deferred socket policy. */
51
+ constructor(options) {
52
+ this.#budget = options.budget
53
+ this.#onCancel = options.onCancel
54
+ this.#ref = options.ref
55
+ this.#unref = options.unref
56
+ }
57
+
58
+ /** @param {DeferredVirtualSocket} socket - Public socket facade. */
59
+ attach(socket) {
60
+ this.#outer = socket
61
+ }
62
+
63
+ /** @param {number} bytes - Application bytes entering the pending socket. @returns {WriteReservation} */
64
+ reserveWrite(bytes) {
65
+ this.#budget.reserveMemory(bytes)
66
+ const reservation = {bytes, held: true}
67
+ this.#writeReservations.push(reservation)
68
+ return reservation
69
+ }
70
+
71
+ /** @param {WriteReservation} reservation - Reservation whose write settled or was rejected. */
72
+ releaseWrite(reservation) {
73
+ if (!reservation.held) return
74
+ reservation.held = false
75
+ this.#budget.releaseMemory(reservation.bytes)
76
+ }
77
+
78
+ /** @param {WriteReservation} reservation - Reservation for a write rejected before entering the stream. */
79
+ rollbackWrite(reservation) {
80
+ const index = this.#writeReservations.indexOf(reservation)
81
+ if (index !== -1) this.#writeReservations.splice(index, 1)
82
+ this.releaseWrite(reservation)
83
+ }
84
+
85
+ /**
86
+ * Assigns exactly one authenticated ready-member socket.
87
+ * @param {VirtualSocket} inner - Selected member socket.
88
+ * @returns {boolean} Whether the facade accepted the member.
89
+ */
90
+ assign(inner) {
91
+ if (this.#inner !== undefined) throw new Error("Deferred Socketduct socket is already assigned")
92
+ if (this.#cancelled || this.#outer === undefined || this.#outer.destroyed) {
93
+ inner.destroy()
94
+ return false
95
+ }
96
+ const outer = this.#outer
97
+ this.#inner = inner
98
+ inner.on("data", (chunk) => {
99
+ if (!outer._receive(Buffer.from(chunk))) inner.pause()
100
+ })
101
+ inner.once("end", () => outer._receiveEnd())
102
+ inner.on("error", (error) => outer._receiveError(error))
103
+ inner.once("close", () => {
104
+ if (outer.destroyed) return
105
+ if (outer.connecting) outer._receiveError(new SocketductStreamError("Selected Socketduct member closed before connecting", "ERR_SOCKET_CLOSED_BEFORE_CONNECTION"))
106
+ else outer.destroy()
107
+ })
108
+ inner.on("ready", () => outer._markConnected())
109
+ if (!inner.connecting) queueMicrotask(() => outer._markConnected())
110
+ if (this.#activeWrite !== undefined) {
111
+ const active = this.#activeWrite
112
+ this.#activeWrite = undefined
113
+ this.releaseWrite(active.reservation)
114
+ inner.write(active.data, active.callback)
115
+ }
116
+ if (this.#activeFinal !== undefined) {
117
+ const callback = this.#activeFinal
118
+ this.#activeFinal = undefined
119
+ inner.end(callback)
120
+ }
121
+ return true
122
+ }
123
+
124
+ /** @returns {{address?: string, port?: number}} Physical endpoint metadata. */
125
+ physicalAddress() {
126
+ if (this.#inner === undefined) return {}
127
+ const address = this.#inner.address()
128
+ return "address" in address ? {address: address.address, port: address.port} : {}
129
+ }
130
+
131
+ /** @returns {number} Assigned protocol stream ID, or zero while unassigned. */
132
+ streamId() {
133
+ return this.#inner?.streamId ?? 0
134
+ }
135
+
136
+ /**
137
+ * @param {number} _streamId - Facade stream ID.
138
+ * @param {Buffer} data - Application bytes.
139
+ * @param {(error?: Error | null) => void} callback - Completion.
140
+ */
141
+ _writeStream(_streamId, data, callback) {
142
+ const reservation = this.#writeReservations.shift()
143
+ if (reservation === undefined) throw new Error("Deferred Socketduct write has no byte reservation")
144
+ if (this.#inner === undefined) {
145
+ this.#activeWrite = {data, callback, reservation}
146
+ return
147
+ }
148
+ this.releaseWrite(reservation)
149
+ this.#inner.write(data, callback)
150
+ }
151
+
152
+ /**
153
+ * @param {number} _streamId - Facade stream ID.
154
+ * @param {(error?: Error | null) => void} callback - Completion.
155
+ */
156
+ _endStream(_streamId, callback) {
157
+ if (this.#inner === undefined) {
158
+ this.#activeFinal = callback
159
+ return
160
+ }
161
+ this.#inner.end(callback)
162
+ }
163
+
164
+ /**
165
+ * @param {number} _streamId - Facade stream ID.
166
+ * @param {Error | null} error - Destroy cause.
167
+ */
168
+ _destroyStream(_streamId, error) {
169
+ if (this.#cancelled) return
170
+ this.#cancelled = true
171
+ this.#onCancel()
172
+ for (const reservation of this.#writeReservations) this.releaseWrite(reservation)
173
+ this.#writeReservations = []
174
+ if (this.#activeWrite !== undefined) {
175
+ const active = this.#activeWrite
176
+ this.#activeWrite = undefined
177
+ this.releaseWrite(active.reservation)
178
+ active.callback(error ?? new SocketductStreamError("Deferred Socketduct socket was destroyed", "ERR_STREAM_DESTROYED"))
179
+ }
180
+ if (this.#activeFinal !== undefined) {
181
+ const callback = this.#activeFinal
182
+ this.#activeFinal = undefined
183
+ callback(error ?? new SocketductStreamError("Deferred Socketduct socket was destroyed", "ERR_STREAM_DESTROYED"))
184
+ }
185
+ if (this.#inner !== undefined && !this.#inner.destroyed) this.#inner.destroy(error ?? undefined)
186
+ }
187
+
188
+ /** @param {number} _streamId - Facade stream ID. */
189
+ _resumeReadable(_streamId) {
190
+ void _streamId
191
+ if (this.#inner !== undefined) this.#inner.resume()
192
+ }
193
+
194
+ /** References the assigned member or the whole set while pending. */
195
+ ref() {
196
+ if (this.#inner === undefined) this.#ref()
197
+ else this.#inner.ref()
198
+ }
199
+
200
+ /** Unreferences the assigned member or the whole set while pending. */
201
+ unref() {
202
+ if (this.#inner === undefined) this.#unref()
203
+ else this.#inner.unref()
204
+ }
205
+ }
206
+
207
+ /** A normal VirtualSocket that delays member ownership until readiness. */
208
+ export class DeferredVirtualSocket extends VirtualSocket {
209
+ /** @type {DeferredSocketOwner} */
210
+ #owner
211
+
212
+ /** @type {string | undefined} */
213
+ #gatewayId
214
+
215
+ /**
216
+ * @param {{host: string, port: number, allowHalfOpen?: boolean, readableHighWaterMark?: number, writableHighWaterMark?: number}} socketOptions - Target/socket options.
217
+ * @param {DeferredVirtualSocketOptions} deferredOptions - Set-owned wait policy.
218
+ */
219
+ constructor(socketOptions, deferredOptions) {
220
+ const owner = new DeferredSocketOwner(deferredOptions)
221
+ super(/** @type {import('./virtual-socket.js').VirtualSocketOwner} */ (owner), 0, socketOptions)
222
+ this.#owner = owner
223
+ owner.attach(this)
224
+ Object.defineProperty(this, "gatewayId", {get: () => this.#gatewayId, enumerable: true, configurable: false})
225
+ }
226
+
227
+ /** @returns {string | undefined} Immutable assignment selected only after readiness. */
228
+ get gatewayId() {
229
+ return this.#gatewayId
230
+ }
231
+
232
+ /**
233
+ * @override
234
+ * @returns {number} Assigned protocol stream ID, or zero while unassigned.
235
+ */
236
+ get streamId() {
237
+ return this.#owner.streamId()
238
+ }
239
+
240
+ /**
241
+ * @param {VirtualSocket} socket - Selected ready-member socket.
242
+ * @param {string} gatewayId - Stable selected gateway ID.
243
+ * @returns {boolean} Whether assignment succeeded before cancellation.
244
+ */
245
+ assign(socket, gatewayId) {
246
+ if (this.#gatewayId !== undefined) throw new Error("Deferred Socketduct socket gateway is already assigned")
247
+ if (!this.#owner.assign(socket)) return false
248
+ this.#gatewayId = gatewayId
249
+ return true
250
+ }
251
+
252
+ /**
253
+ * Hard-bounds bytes retained before member assignment while retaining Writable backpressure.
254
+ * @override
255
+ * @param {string | NodeJS.ArrayBufferView} chunk - Application bytes.
256
+ * @param {BufferEncoding | ((error?: Error | null) => void)} [encoding] - Encoding or completion.
257
+ * @param {(error?: Error | null) => void} [callback] - Completion.
258
+ * @returns {boolean} Writable backpressure state.
259
+ */
260
+ write(chunk, encoding, callback) {
261
+ const selectedEncoding = typeof encoding === "string" ? encoding : undefined
262
+ const selectedCallback = typeof encoding === "function" ? encoding : callback
263
+ if (typeof chunk !== "string" && !ArrayBuffer.isView(chunk)) {
264
+ return selectedEncoding === undefined
265
+ ? super.write(chunk, selectedCallback)
266
+ : super.write(chunk, selectedEncoding, selectedCallback)
267
+ }
268
+ const bytes = typeof chunk === "string" ? Buffer.byteLength(chunk, selectedEncoding) : chunk.byteLength
269
+ /** @type {WriteReservation} */
270
+ let reservation
271
+ try {
272
+ reservation = this.#owner.reserveWrite(bytes)
273
+ } catch (error) {
274
+ const failure = error instanceof Error ? error : new Error(String(error))
275
+ queueMicrotask(() => {
276
+ if (selectedCallback !== undefined) selectedCallback(failure)
277
+ this.destroy(failure)
278
+ })
279
+ return false
280
+ }
281
+ const completed = (/** @type {Error | null | undefined} */ error) => {
282
+ this.#owner.releaseWrite(reservation)
283
+ if (selectedCallback !== undefined) selectedCallback(error)
284
+ }
285
+ try {
286
+ return selectedEncoding === undefined
287
+ ? super.write(chunk, completed)
288
+ : super.write(chunk, selectedEncoding, completed)
289
+ } catch (error) {
290
+ this.#owner.rollbackWrite(reservation)
291
+ throw error
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Routes final application bytes through the same bounded reservation as write().
297
+ * @override
298
+ * @param {string | NodeJS.ArrayBufferView | (() => void)} [chunk] - Final application bytes or completion.
299
+ * @param {BufferEncoding | (() => void)} [encoding] - Encoding or completion.
300
+ * @param {() => void} [callback] - Completion after finish.
301
+ * @returns {this} This socket.
302
+ */
303
+ end(chunk, encoding, callback) {
304
+ if (typeof chunk === "function") return super.end(chunk)
305
+ const selectedCallback = typeof encoding === "function" ? encoding : callback
306
+ if (chunk !== undefined) {
307
+ if (typeof encoding === "string") this.write(chunk, encoding)
308
+ else this.write(chunk)
309
+ }
310
+ return selectedCallback === undefined ? super.end() : super.end(selectedCallback)
311
+ }
312
+ }
@@ -0,0 +1,101 @@
1
+ import {Agent} from "node:http"
2
+
3
+ import {connectSocketduct} from "./socketduct-socket.js"
4
+
5
+ /**
6
+ * @typedef {import('./socketduct-socket.js').ConnectSocketductOptions} ConnectSocketductOptions
7
+ * @typedef {import('./socketduct-socket.js').SocketductSocket} SocketductSocket
8
+ * @typedef {import('./socketduct-socket.js').SocketductTlsOptions} SocketductTlsOptions
9
+ * @typedef {object} SocketductHttpAgentOptions
10
+ * @property {{host: string, port: number, token: string}} relay - Relay connection options.
11
+ * @property {{host: string, port: number}} target - Target requested for every HTTP connection.
12
+ * @property {string} spoolDirectory - Spool directory.
13
+ * @property {{initialDelayMs?: number}} [reconnect] - Reconnect options.
14
+ * @property {string} [sessionNamePrefix] - Session name prefix.
15
+ * @property {string} [sessionName] - Fixed session name.
16
+ * @property {boolean} [keepAlive] - Keep sockets alive between HTTP requests.
17
+ * @property {number} [maxSockets] - Maximum sockets.
18
+ * @property {number} [maxFreeSockets] - Maximum free sockets.
19
+ * @property {SocketductTlsOptions} [tls] - TLS client options.
20
+ * @property {number} [maxBytes] - Maximum total spool bytes per session.
21
+ * @property {number} [maxAgeMs] - Maximum age of a spool session before operations are rejected.
22
+ * @property {import('../logging/structured-logger.js').DiagnosticLogger} [logger] - Optional privacy-safe structured diagnostic logger.
23
+ */
24
+
25
+ export class SocketductHttpAgent extends Agent {
26
+ /** @type {SocketductHttpAgentOptions} */
27
+ #options
28
+
29
+ /** @type {number} */
30
+ #connectionId = 0
31
+
32
+ /** @param {SocketductHttpAgentOptions} options - Agent options. */
33
+ constructor(options) {
34
+ super({
35
+ keepAlive: options.keepAlive,
36
+ maxSockets: options.maxSockets,
37
+ maxFreeSockets: options.maxFreeSockets
38
+ })
39
+ this.#options = options
40
+ }
41
+
42
+ /**
43
+ * @override
44
+ * @param {import('node:http').ClientRequestArgs} _options - HTTP connection options supplied by Node.
45
+ * @param {(error: Error | null, socket: SocketductSocket) => void} [callback] - Optional socket callback.
46
+ * @returns {SocketductSocket} - Socketduct-backed HTTP socket.
47
+ */
48
+ createConnection(_options, callback) {
49
+ this.#connectionId += 1
50
+ const sessionName = this.#options.sessionName
51
+ ?? `${this.#options.sessionNamePrefix ?? "socketduct-http"}-${this.#connectionId}`
52
+ /** @type {ConnectSocketductOptions} */
53
+ const socketOptions = {
54
+ relay: this.#options.relay,
55
+ sessionName,
56
+ target: this.#options.target,
57
+ spoolDirectory: this.#options.spoolDirectory
58
+ }
59
+ if (this.#options.reconnect !== undefined) {
60
+ socketOptions.reconnect = this.#options.reconnect
61
+ }
62
+ if (this.#options.tls !== undefined) {
63
+ socketOptions.tls = this.#options.tls
64
+ }
65
+ if (this.#options.maxBytes !== undefined) {
66
+ socketOptions.maxBytes = this.#options.maxBytes
67
+ }
68
+ if (this.#options.maxAgeMs !== undefined) {
69
+ socketOptions.maxAgeMs = this.#options.maxAgeMs
70
+ }
71
+ if (this.#options.logger !== undefined) {
72
+ socketOptions.logger = this.#options.logger
73
+ }
74
+ /** @type {SocketductSocket} */
75
+ const socket = connectSocketduct(socketOptions)
76
+ if (callback !== undefined) {
77
+ let settled = false
78
+ /**
79
+ * @param {Error | null} error - Connection result.
80
+ * @returns {void}
81
+ */
82
+ const settle = (error) => {
83
+ if (settled) return
84
+ settled = true
85
+ socket.off("connect", onConnect)
86
+ socket.off("error", onError)
87
+ callback(error, socket)
88
+ }
89
+ /** @returns {void} */
90
+ const onConnect = () => settle(null)
91
+ /**
92
+ * @param {Error} error - Pre-connect Socketduct failure.
93
+ * @returns {void}
94
+ */
95
+ const onError = (error) => settle(error)
96
+ socket.once("connect", onConnect)
97
+ socket.once("error", onError)
98
+ }
99
+ return socket
100
+ }
101
+ }