@blamejs/core 0.6.58 → 0.6.59

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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.6.x
10
10
 
11
+ - **0.6.59** (2026-05-03) — HTTP/2 session teardown deduplication + http-client sweep. v0.6.58's inline `session.close() + session.destroy()` block in `lib/log-stream-otlp-grpc.js` was the right fix for the OTLP-gRPC sink hang, but the same bug class lives in `lib/http-client.js` — the h2 transport pool had 5 call sites running the bare `session.close()` (`_resetTransports`, the ALPN-fallback path, the h2 connect-error path, the h2c connect-error path, the idle-timeout handler, and `_resetForTest`) all of which leak the underlying TCP socket on idle / error / fallback paths in exactly the same way. New `lib/http2-teardown.js` exports `tearDownH2Session(session)` which performs the close()-then-destroy() routine; `lib/http-client.js` and `lib/log-stream-otlp-grpc.js` both import it. The OTLP-gRPC v0.6.58 inline block is removed in favour of the shared helper. **Tests** — verified in a node:24-alpine docker container: smoke 7204 checks in 85.3 seconds. Wiki e2e 178 / eslint clean / shellcheck clean.
12
+
11
13
  - **0.6.58** (2026-05-03) — `b.logStream` OTLP-gRPC sink hang fix that has been silently breaking the npm-publish gate since v0.6.38. The sink's `close()` was calling `session.close()` (HTTP/2 *graceful* close — waits for in-flight streams before freeing the socket) but never `session.destroy()`. The graceful close completed but the underlying TCP socket stayed connected, blocking the test fixture's `server.close()` indefinitely on Linux CI runners. Same bug also added ~120 seconds of lingering latency to every local smoke run on Windows (smoke 196s → 76s after fix). The fix calls `session.close()` then `session.destroy()`; by the time we reach close(), all buffered records have been flushed via the awaited `inflightPromise` + final `_doExport`, so destroy() is structurally safe. **Operator impact** — the npm registry has been stuck at v0.6.37 since 2026-05-02; every tag from v0.6.38 → v0.6.57 timed out at the publish workflow's smoke step. With this fix the publish should reach `npm publish`. No layer-0 or integration test changes — the existing `log-stream-otlp-grpc.test.js` round-trip was passing locally because tests use small batches and the lingering socket happens to terminate before the test driver's overall timeout, but it was leaving the process unable to exit cleanly until the OS-level TCP timeout fired. Smoke 7204 / wiki e2e 178 / integration 16 files / eslint clean / shellcheck clean.
12
14
 
13
15
  - **0.6.57** (2026-05-03) — `b.safeBuffer.boundedChunkCollector` strict `maxBytes` validation. Pre-fix the validator was `typeof opts.maxBytes === "number" && opts.maxBytes > 0` which silently accepted `Infinity` (defeating the OOM-cap purpose entirely — a hostile 10-GB upstream would accumulate fully) and non-integer floats like `3.5` (which set a fractional cap that confused downstream `total + chunk.length > maxBytes` arithmetic). Now requires positive finite integer; everything else throws `buffer/bad-arg` at boot with the offending value in the message. Real-world consumers (`b.httpClient` request cap, `b.atomicFile.read`, `b.parsers.*`, multipart body parser) are unaffected because they pass real positive integers via `C.BYTES.*` helpers; the fix catches operator typos / misconfigured env-var coercions where `Number(env.MAX_BYTES)` produces NaN or Infinity. **Tests** — 7 new layer-0 boot-validation assertions in `test/00-primitives.js` (smoke 7197→7204). Wiki e2e 178 / eslint clean / shellcheck clean.
@@ -174,13 +174,18 @@ function configurePool(opts) {
174
174
  if (t && t.kind === "h1" && t.agent && typeof t.agent.destroy === "function") {
175
175
  try { t.agent.destroy(); } catch (_e) {}
176
176
  }
177
- if (t && t.kind === "h2" && t.session && typeof t.session.close === "function") {
178
- try { t.session.close(); } catch (_e) {}
177
+ if (t && t.kind === "h2" && t.session) {
178
+ _tearDownH2Session(t.session);
179
179
  }
180
180
  });
181
181
  _transports.clear();
182
182
  }
183
183
 
184
+ // HTTP/2 session teardown — see lib/http2-teardown.js for the full
185
+ // rationale. Centralised so any future sink / pool teardown gets the
186
+ // same close()-then-destroy() discipline.
187
+ var _tearDownH2Session = require("./http2-teardown").tearDownH2Session;
188
+
184
189
  // h2 session connect options. Same TLS posture as h1 Agent.
185
190
  var DEFAULT_H2_TLS_OPTS = {
186
191
  ALPNProtocols: ["h2", "http/1.1"],
@@ -254,11 +259,11 @@ function _connectHttpsWithAlpn(u, ips) {
254
259
  return;
255
260
  }
256
261
  // Server picked http/1.1 — close the h2 session, return h1 transport.
257
- try { session.close(); } catch (_e) {}
262
+ _tearDownH2Session(session);
258
263
  _done(_makeH1Transport(u, ips));
259
264
  });
260
265
  session.once("error", function (err) {
261
- try { session.close(); } catch (_e) {}
266
+ _tearDownH2Session(session);
262
267
  _fail(err);
263
268
  });
264
269
  });
@@ -277,7 +282,7 @@ function _connectH2c(u, ips) {
277
282
  resolve({ kind: "h2", session: session });
278
283
  });
279
284
  session.once("error", function (err) {
280
- try { session.close(); } catch (_e) {}
285
+ _tearDownH2Session(session);
281
286
  reject(err);
282
287
  });
283
288
  });
@@ -286,7 +291,7 @@ function _connectH2c(u, ips) {
286
291
  // Common h2 session wiring — idle close + cache eviction on error/close.
287
292
  function _wireH2Session(session, key) {
288
293
  session.setTimeout(H2_SESSION_IDLE_TIMEOUT_MS, function () {
289
- try { session.close(); } catch (_e) {}
294
+ _tearDownH2Session(session);
290
295
  });
291
296
  session.once("close", function () { _transports.delete(key); });
292
297
  session.once("error", function () { _transports.delete(key); });
@@ -1132,7 +1137,7 @@ function _resetForTest() {
1132
1137
  try { t.agent.destroy(); } catch (_e) {}
1133
1138
  }
1134
1139
  if (t && t.kind === "h2" && t.session) {
1135
- try { t.session.close(); } catch (_e) {}
1140
+ _tearDownH2Session(t.session);
1136
1141
  }
1137
1142
  });
1138
1143
  _transports.clear();
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ /**
3
+ * HTTP/2 session teardown — graceful close *then* force-destroy.
4
+ *
5
+ * `Http2Session.close()` is the *graceful* close: it returns synchronously
6
+ * while letting in-flight streams complete on their own, but it does NOT
7
+ * free the underlying TCP socket until those streams complete (or the
8
+ * peer disconnects). On idle / error / fallback paths — where we
9
+ * explicitly DON'T want the session anymore — that means the socket
10
+ * lingers until the OS-level TCP timeout fires. In a test process the
11
+ * mock-server's `server.close()` then waits for that lingering socket
12
+ * to release, which on Linux can be tens of minutes. v0.6.58 hit
13
+ * exactly this in the OTLP-gRPC sink and timed out the npm-publish
14
+ * workflow on every tag from v0.6.38 → v0.6.57.
15
+ *
16
+ * The fix is structural: every call site that wants the session GONE
17
+ * routes through this helper, which calls close() (best-effort drain)
18
+ * then destroy() (force socket teardown). Used by `lib/http-client.js`
19
+ * (h2 transport pool — fallback, error, idle-timeout, reset) and by
20
+ * `lib/log-stream-otlp-grpc.js` (sink shutdown after final flush).
21
+ *
22
+ * No-op on a null / undefined session. Wraps each call in try/catch so
23
+ * a partially-torn-down session can't throw and cancel the second call.
24
+ */
25
+
26
+ function tearDownH2Session(session) {
27
+ if (!session) return;
28
+ try { if (typeof session.close === "function") session.close(); }
29
+ catch (_e1) { /* best-effort graceful */ }
30
+ try { if (typeof session.destroy === "function") session.destroy(); }
31
+ catch (_e2) { /* best-effort socket teardown */ }
32
+ }
33
+
34
+ module.exports = { tearDownH2Session: tearDownH2Session };
@@ -29,6 +29,7 @@ var http2 = require("node:http2");
29
29
  var C = require("./constants");
30
30
  var pb = require("./protobuf-encoder");
31
31
  var safeUrl = require("./safe-url");
32
+ var { tearDownH2Session } = require("./http2-teardown");
32
33
  var { LogStreamError } = require("./framework-error");
33
34
 
34
35
  var _err = LogStreamError.factory;
@@ -372,22 +373,12 @@ function create(config) {
372
373
  _emitDrop("send-failed", pending, e);
373
374
  }
374
375
  }
375
- // Tear down the HTTP/2 session. `session.close()` is the *graceful*
376
- // close it waits for in-flight streams to complete on their own
377
- // and won't free the underlying socket while any stream is still
378
- // open. By this point we've already awaited `inflightPromise` and
379
- // run a final `_doExport` for any buffered records, so there's
380
- // nothing left to drain; `session.destroy()` is the structurally
381
- // correct call. (Calling only close() left the test fixture's
382
- // server.close() hanging indefinitely on Linux CI runners — the
383
- // close() was returning while the underlying TCP socket stayed
384
- // connected, blocking the server-side close from completing.)
385
- try {
386
- if (session) {
387
- try { session.close(); } catch (_e1) { /* best-effort graceful */ }
388
- try { session.destroy(); } catch (_e2) { /* socket teardown */ }
389
- }
390
- } catch (_e) {}
376
+ // Tear down the HTTP/2 session via the shared close+destroy helper.
377
+ // By this point we've already awaited inflightPromise and run a
378
+ // final _doExport for any buffered records, so there's nothing left
379
+ // to drain. See lib/http2-teardown.js for the rationale on why
380
+ // close() alone leaves the underlying socket connected on Linux.
381
+ tearDownH2Session(session);
391
382
  session = null;
392
383
  }
393
384
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.6.58",
3
+ "version": "0.6.59",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:246b27c1-6c5e-4ffc-9840-0275a3ed556a",
5
+ "serialNumber": "urn:uuid:f1dea294-4db6-4cb4-9820-6958e7939dac",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-03T13:19:55.918Z",
8
+ "timestamp": "2026-05-03T13:30:21.472Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.6.58",
22
+ "bom-ref": "@blamejs/core@0.6.59",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.6.58",
25
+ "version": "0.6.59",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.6.58",
29
+ "purl": "pkg:npm/%40blamejs/core@0.6.59",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.6.58",
57
+ "ref": "@blamejs/core@0.6.59",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]