@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 +2 -0
- package/lib/http-client.js +12 -7
- package/lib/http2-teardown.js +34 -0
- package/lib/log-stream-otlp-grpc.js +7 -16
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
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.
|
package/lib/http-client.js
CHANGED
|
@@ -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
|
|
178
|
-
|
|
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
|
-
|
|
262
|
+
_tearDownH2Session(session);
|
|
258
263
|
_done(_makeH1Transport(u, ips));
|
|
259
264
|
});
|
|
260
265
|
session.once("error", function (err) {
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
|
|
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
package/sbom.cyclonedx.json
CHANGED
|
@@ -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:
|
|
5
|
+
"serialNumber": "urn:uuid:f1dea294-4db6-4cb4-9820-6958e7939dac",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-03T13:
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.6.59",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.6.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.6.59",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|