socketduct 0.1.15 → 0.1.16

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.
@@ -0,0 +1 @@
1
+ Fixed a crash in `DeferredVirtualSocket` where a non-function callback value (e.g. `null`) passed to `write()` or `end()` threw `selectedCallback is not a function` instead of failing gracefully.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socketduct",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Resilient authenticated tunnel/proxy for keeping socket streams alive across flaky links.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -259,7 +259,7 @@ export class DeferredVirtualSocket extends VirtualSocket {
259
259
  */
260
260
  write(chunk, encoding, callback) {
261
261
  const selectedEncoding = typeof encoding === "string" ? encoding : undefined
262
- const selectedCallback = typeof encoding === "function" ? encoding : callback
262
+ const selectedCallback = typeof encoding === "function" ? encoding : typeof callback === "function" ? callback : undefined
263
263
  if (typeof chunk !== "string" && !ArrayBuffer.isView(chunk)) {
264
264
  return selectedEncoding === undefined
265
265
  ? super.write(chunk, selectedCallback)
@@ -302,7 +302,7 @@ export class DeferredVirtualSocket extends VirtualSocket {
302
302
  */
303
303
  end(chunk, encoding, callback) {
304
304
  if (typeof chunk === "function") return super.end(chunk)
305
- const selectedCallback = typeof encoding === "function" ? encoding : callback
305
+ const selectedCallback = typeof encoding === "function" ? encoding : typeof callback === "function" ? callback : undefined
306
306
  if (chunk !== undefined) {
307
307
  if (typeof encoding === "string") this.write(chunk, encoding)
308
308
  else this.write(chunk)
@@ -0,0 +1,170 @@
1
+ /* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param-type, jsdoc/require-returns */
2
+ import assert from "node:assert/strict"
3
+ import test from "node:test"
4
+
5
+ import {DeferredVirtualSocket} from "../../../src/client/deferred-virtual-socket.js"
6
+ import {VirtualSocket} from "../../../src/client/virtual-socket.js"
7
+
8
+ /**
9
+ * @param {{reserveMemoryError?: Error}} [options] - Optional rejection to inject into the byte budget.
10
+ * @returns {{reservedBytes: () => number, socket: import('../../../src/client/deferred-virtual-socket.js').DeferredVirtualSocket}} Deferred socket fixture with reserved-byte accounting.
11
+ */
12
+ function deferredSocket(options = {}) {
13
+ let reservedBytes = 0
14
+ const socket = new DeferredVirtualSocket({host: "127.0.0.1", port: 4793}, {
15
+ budget: {
16
+ reserveMemory(bytes) {
17
+ if (options.reserveMemoryError) throw options.reserveMemoryError
18
+ reservedBytes += bytes
19
+ },
20
+ releaseMemory(bytes) {
21
+ reservedBytes -= bytes
22
+ }
23
+ },
24
+ onCancel: () => {},
25
+ ref: () => {},
26
+ unref: () => {}
27
+ })
28
+ return {reservedBytes: () => reservedBytes, socket}
29
+ }
30
+
31
+ /** Fake member connection that settles write and final callbacks on the next macrotask. */
32
+ function memberConnection() {
33
+ /** @type {Buffer[]} */
34
+ const writes = []
35
+ let ended = 0
36
+ return {
37
+ connection: /** @type {import('../../../src/client/virtual-socket.js').VirtualSocketOwner} */ (/** @type {unknown} */ ({
38
+ /**
39
+ * @param {number} _id @param {Buffer} data @param {(error?: Error | null) => void} callback
40
+ * @param data
41
+ * @param callback
42
+ */
43
+ _writeStream: (_id, data, callback) => {
44
+ writes.push(data)
45
+ setImmediate(callback)
46
+ },
47
+ /**
48
+ * @param {number} _id @param {(error?: Error | null) => void} callback
49
+ * @param callback
50
+ */
51
+ _endStream: (_id, callback) => {
52
+ ended += 1
53
+ callback()
54
+ },
55
+ _destroyStream: () => {},
56
+ _resumeReadable: () => {},
57
+ physicalAddress: () => ({address: "127.0.0.1", port: 43210}),
58
+ ref: () => {},
59
+ unref: () => {}
60
+ })),
61
+ ended: () => ended,
62
+ writes
63
+ }
64
+ }
65
+
66
+ test("DeferredVirtualSocket write ignores non-function callback values in both parameter slots", async () => {
67
+ const first = deferredSocket()
68
+ // @ts-expect-error Deliberate non-function callback value; the socket must ignore it.
69
+ assert.equal(first.socket.write("hello", null), true)
70
+ const closed = new Promise((resolve) => first.socket.once("close", resolve))
71
+ first.socket.destroy()
72
+ await closed
73
+ assert.equal(first.socket.destroyed, true)
74
+
75
+ const second = deferredSocket()
76
+ // @ts-expect-error Deliberate non-function callback value; the socket must ignore it.
77
+ assert.equal(second.socket.write("hello", "utf8", null), true)
78
+ const closedAgain = new Promise((resolve) => second.socket.once("close", resolve))
79
+ assert.doesNotThrow(() => second.socket.destroy())
80
+ await closedAgain
81
+ assert.equal(second.socket.destroyed, true)
82
+ })
83
+
84
+ test("DeferredVirtualSocket write budget failure destroys the socket without crashing on non-function callback values", async () => {
85
+ const failure = new Error("budget exhausted")
86
+ const {socket} = deferredSocket({reserveMemoryError: failure})
87
+ const error = new Promise((resolve) => socket.once("error", resolve))
88
+ // @ts-expect-error Deliberate non-function callback value; the socket must ignore it.
89
+ assert.equal(socket.write("hello", "utf8", null), false)
90
+ assert.equal(await error, failure)
91
+ assert.equal(socket.destroyed, true)
92
+ })
93
+
94
+ test("DeferredVirtualSocket write budget failure invokes the completion callback with the failure", async () => {
95
+ const failure = new Error("budget exhausted")
96
+ const {reservedBytes, socket} = deferredSocket({reserveMemoryError: failure})
97
+ /** @type {Error | null | undefined} */
98
+ let callbackError
99
+ const error = new Promise((resolve) => socket.once("error", resolve))
100
+ assert.equal(socket.write("hello", "utf8", (received) => {
101
+ callbackError = received
102
+ }), false)
103
+ await error
104
+ assert.equal(callbackError, failure)
105
+ assert.equal(reservedBytes(), 0)
106
+ assert.equal(socket.destroyed, true)
107
+ })
108
+
109
+ test("DeferredVirtualSocket write completion after member assignment ignores non-function callback values", async () => {
110
+ const {socket} = deferredSocket()
111
+ const member = memberConnection()
112
+ socket.assign(new VirtualSocket(member.connection, 1, {host: "127.0.0.1", port: 4793}), "gateway-1")
113
+ // @ts-expect-error Deliberate non-function callback value; the socket must ignore it.
114
+ assert.equal(socket.write("hello", "utf8", null), true)
115
+ await new Promise((resolve) => setImmediate(resolve))
116
+ const written = member.writes[0]
117
+ assert(written !== undefined)
118
+ assert.equal(written.toString(), "hello")
119
+ })
120
+
121
+ test("DeferredVirtualSocket end ignores non-function callback values in both parameter slots", async () => {
122
+ const first = deferredSocket()
123
+ assert.doesNotThrow(() => {
124
+ // @ts-expect-error Deliberate non-function callback value; the socket must ignore it.
125
+ first.socket.end("hello", null)
126
+ })
127
+ assert.equal(first.socket.writableEnded, true)
128
+ const closed = new Promise((resolve) => first.socket.once("close", resolve))
129
+ first.socket.destroy()
130
+ await closed
131
+ assert.equal(first.socket.destroyed, true)
132
+
133
+ const second = deferredSocket()
134
+ assert.doesNotThrow(() => {
135
+ // @ts-expect-error Deliberate non-function callback value; the socket must ignore it.
136
+ second.socket.end("hello", "utf8", null)
137
+ })
138
+ assert.equal(second.socket.writableEnded, true)
139
+ const closedAgain = new Promise((resolve) => second.socket.once("close", resolve))
140
+ second.socket.destroy()
141
+ await closedAgain
142
+ assert.equal(second.socket.destroyed, true)
143
+ })
144
+
145
+ test("DeferredVirtualSocket end does not forward non-function callback values to the stream", async () => {
146
+ const {socket} = deferredSocket()
147
+ assert.doesNotThrow(() => {
148
+ // @ts-expect-error Deliberate non-function callback value; the socket must ignore it.
149
+ socket.end("hello", "utf8", 42)
150
+ })
151
+ assert.equal(socket.writableEnded, true)
152
+ const closed = new Promise((resolve) => socket.once("close", resolve))
153
+ socket.destroy()
154
+ await closed
155
+ })
156
+
157
+ test("DeferredVirtualSocket end completion after member assignment invokes the callback on finish", async () => {
158
+ const {socket} = deferredSocket()
159
+ const member = memberConnection()
160
+ socket.assign(new VirtualSocket(member.connection, 1, {host: "127.0.0.1", port: 4793}), "gateway-1")
161
+ let finishedCallback = false
162
+ const finished = new Promise((resolve) => socket.once("finish", resolve))
163
+ socket.end("hello", "utf8", () => {
164
+ finishedCallback = true
165
+ })
166
+ await finished
167
+ assert.equal(finishedCallback, true)
168
+ assert.equal(socket.writableFinished, true)
169
+ assert.equal(member.ended(), 1)
170
+ })
@@ -41,7 +41,9 @@ test("TensorBuzz exclusively owns verification and container build validation",
41
41
  assert.match(tensorbuzz, /privileged: true/)
42
42
  assert.match(tensorbuzz, /docker build --tag socketduct-ci \./)
43
43
  assert.doesNotMatch(tensorbuzz, /docker (?:login|push)/)
44
- assert.equal(packageMetadata.private, true)
44
+ // release-patch refuses to publish a package marked "private": true, so the
45
+ // published UNLICENSED npm release requires the flag to be absent or false.
46
+ assert.notEqual(packageMetadata.private, true)
45
47
  assert.equal(packageMetadata.license, "UNLICENSED")
46
48
  })
47
49