socketduct 0.1.27 → 0.1.28

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
+ A hub route-class unavailability report now lasts for the life of the connection instead of five seconds, so a transport set stops re-selecting a route class the hub already declared unserviceable and no longer fails the caller's first open on it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socketduct",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
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",
@@ -44,7 +44,7 @@ const GATEWAY_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{1,63}$/
44
44
  * @property {import('../logging/structured-logger.js').DiagnosticLogger} [logger] - Optional privacy-safe structured logger shared by the set and its members.
45
45
  * @typedef {object} MemberConnectionStatus
46
46
  * @property {string} physicalState - Member lifecycle.
47
- * @property {boolean} [gatewayUnavailable] - Hub recently reported no selectable gateway for this member's route class.
47
+ * @property {boolean} [gatewayUnavailable] - Hub reported no selectable gateway for this member's route class on the current connection.
48
48
  * @property {number} connectionGeneration - Current authenticated generation.
49
49
  * @property {number} physicalConnects - Authenticated physical generations.
50
50
  * @property {number} reconnectAttempts - Physical reconnect attempts.
@@ -32,7 +32,6 @@ import {attachDisconnectAttribution, createDisconnectAttribution, parseDisconnec
32
32
  const DEFAULT_STREAM_BUFFER_BYTES = 4 * 1024 * 1024
33
33
  const DEFAULT_CONNECTION_BUFFER_BYTES = 32 * 1024 * 1024
34
34
  const MANIFEST_PAGE_STREAMS = 64
35
- const GATEWAY_UNAVAILABLE_TTL_MS = 5_000
36
35
 
37
36
  /**
38
37
  * @typedef {object} ReverseGatewayConnectionOptions
@@ -373,13 +372,18 @@ export class ReverseGatewayConnection extends EventEmitter {
373
372
  }
374
373
 
375
374
  /**
376
- * Whether the last hub-reported gateway state for this member was unavailable
377
- * and the report is still fresh. A stale report never pins the member out.
378
- * @returns {boolean} True while a fresh hub unavailability report is active.
375
+ * Whether the hub last reported this member's route class as having no selectable
376
+ * gateway. The report is a property of the live connection, not a short-lived cache:
377
+ * it arrives once per connection at selection time and only changes when the hub says
378
+ * otherwise or an open succeeds. Expiring it on a timer made the transport set pick a
379
+ * route class the hub had already declared unserviceable, and the reset that followed
380
+ * failed the caller's operation — a build lost its first transport open this way.
381
+ * The transport set still falls back to marked members when every member is marked, so
382
+ * a mark can never wedge the set.
383
+ * @returns {boolean} True while the hub reports no selectable gateway for this member.
379
384
  */
380
385
  #gatewayUnavailable() {
381
386
  return this.#gatewayUnavailableSince !== undefined
382
- && Date.now() - this.#gatewayUnavailableSince < GATEWAY_UNAVAILABLE_TTL_MS
383
387
  }
384
388
 
385
389
  status() {
@@ -436,6 +436,12 @@ describe("reverse gateway route-class", () => {
436
436
  expect(client.status().physicalState).toBe("ready")
437
437
  expect(client.status().gatewayUnavailable).toBe(true)
438
438
 
439
+ // The report belongs to the live connection: it must survive any short window a caller
440
+ // might wait between the announce and its first open. A timer-bound mark made the transport
441
+ // set pick this route class again and the resulting reset failed the caller's operation.
442
+ await new Promise((resolve) => setTimeout(resolve, 5200))
443
+ expect({value: client.status().gatewayUnavailable, context: "a hub unavailability report must not expire on a timer"}).toMatchObject({value: true})
444
+
439
445
  // Once the gateway registers, a replacement selection announces selectable=true and clears the mark.
440
446
  const gatewayA = new OutboundReverseGateway({
441
447
  gatewayId: "server-a-starlink",