Jun 12, 7-8 PM (12)
Jun 12, 8-9 PM (17)
Jun 12, 9-10 PM (5)
Jun 12, 10-11 PM (30)
Jun 12, 11-12 AM (6)
Jun 13, 12-1 AM (6)
Jun 13, 1-2 AM (2)
Jun 13, 2-3 AM (0)
Jun 13, 3-4 AM (3)
Jun 13, 4-5 AM (0)
Jun 13, 5-6 AM (3)
Jun 13, 6-7 AM (7)
Jun 13, 7-8 AM (5)
Jun 13, 8-9 AM (6)
Jun 13, 9-10 AM (14)
Jun 13, 10-11 AM (12)
Jun 13, 11-12 PM (2)
Jun 13, 12-1 PM (23)
Jun 13, 1-2 PM (21)
Jun 13, 2-3 PM (8)
Jun 13, 3-4 PM (1)
Jun 13, 4-5 PM (4)
Jun 13, 5-6 PM (4)
Jun 13, 6-7 PM (3)
Jun 13, 7-8 PM (3)
Jun 13, 8-9 PM (7)
Jun 13, 9-10 PM (16)
Jun 13, 10-11 PM (19)
Jun 13, 11-12 AM (24)
Jun 14, 12-1 AM (18)
Jun 14, 1-2 AM (0)
Jun 14, 2-3 AM (0)
Jun 14, 3-4 AM (0)
Jun 14, 4-5 AM (2)
Jun 14, 5-6 AM (0)
Jun 14, 6-7 AM (2)
Jun 14, 7-8 AM (3)
Jun 14, 8-9 AM (0)
Jun 14, 9-10 AM (1)
Jun 14, 10-11 AM (2)
Jun 14, 11-12 PM (10)
Jun 14, 12-1 PM (8)
Jun 14, 1-2 PM (4)
Jun 14, 2-3 PM (8)
Jun 14, 3-4 PM (2)
Jun 14, 4-5 PM (1)
Jun 14, 5-6 PM (1)
Jun 14, 6-7 PM (0)
Jun 14, 7-8 PM (11)
Jun 14, 8-9 PM (1)
Jun 14, 9-10 PM (13)
Jun 14, 10-11 PM (29)
Jun 14, 11-12 AM (23)
Jun 15, 12-1 AM (8)
Jun 15, 1-2 AM (10)
Jun 15, 2-3 AM (4)
Jun 15, 3-4 AM (4)
Jun 15, 4-5 AM (1)
Jun 15, 5-6 AM (4)
Jun 15, 6-7 AM (6)
Jun 15, 7-8 AM (41)
Jun 15, 8-9 AM (26)
Jun 15, 9-10 AM (11)
Jun 15, 10-11 AM (35)
Jun 15, 11-12 PM (25)
Jun 15, 12-1 PM (40)
Jun 15, 1-2 PM (26)
Jun 15, 2-3 PM (21)
Jun 15, 3-4 PM (24)
Jun 15, 4-5 PM (21)
Jun 15, 5-6 PM (13)
Jun 15, 6-7 PM (13)
Jun 15, 7-8 PM (7)
Jun 15, 8-9 PM (26)
Jun 15, 9-10 PM (20)
Jun 15, 10-11 PM (22)
Jun 15, 11-12 AM (39)
Jun 16, 12-1 AM (11)
Jun 16, 1-2 AM (5)
Jun 16, 2-3 AM (1)
Jun 16, 3-4 AM (9)
Jun 16, 4-5 AM (6)
Jun 16, 5-6 AM (1)
Jun 16, 6-7 AM (16)
Jun 16, 7-8 AM (81)
Jun 16, 8-9 AM (18)
Jun 16, 9-10 AM (28)
Jun 16, 10-11 AM (22)
Jun 16, 11-12 PM (31)
Jun 16, 12-1 PM (37)
Jun 16, 1-2 PM (49)
Jun 16, 2-3 PM (34)
Jun 16, 3-4 PM (28)
Jun 16, 4-5 PM (37)
Jun 16, 5-6 PM (17)
Jun 16, 6-7 PM (26)
Jun 16, 7-8 PM (9)
Jun 16, 8-9 PM (11)
Jun 16, 9-10 PM (4)
Jun 16, 10-11 PM (31)
Jun 16, 11-12 AM (9)
Jun 17, 12-1 AM (8)
Jun 17, 1-2 AM (8)
Jun 17, 2-3 AM (11)
Jun 17, 3-4 AM (4)
Jun 17, 4-5 AM (1)
Jun 17, 5-6 AM (6)
Jun 17, 6-7 AM (99)
Jun 17, 7-8 AM (33)
Jun 17, 8-9 AM (22)
Jun 17, 9-10 AM (56)
Jun 17, 10-11 AM (18)
Jun 17, 11-12 PM (19)
Jun 17, 12-1 PM (57)
Jun 17, 1-2 PM (28)
Jun 17, 2-3 PM (37)
Jun 17, 3-4 PM (26)
Jun 17, 4-5 PM (19)
Jun 17, 5-6 PM (16)
Jun 17, 6-7 PM (10)
Jun 17, 7-8 PM (14)
Jun 17, 8-9 PM (12)
Jun 17, 9-10 PM (37)
Jun 17, 10-11 PM (29)
Jun 17, 11-12 AM (14)
Jun 18, 12-1 AM (12)
Jun 18, 1-2 AM (8)
Jun 18, 2-3 AM (5)
Jun 18, 3-4 AM (11)
Jun 18, 4-5 AM (11)
Jun 18, 5-6 AM (11)
Jun 18, 6-7 AM (9)
Jun 18, 7-8 AM (19)
Jun 18, 8-9 AM (83)
Jun 18, 9-10 AM (45)
Jun 18, 10-11 AM (51)
Jun 18, 11-12 PM (23)
Jun 18, 12-1 PM (67)
Jun 18, 1-2 PM (14)
Jun 18, 2-3 PM (53)
Jun 18, 3-4 PM (44)
Jun 18, 4-5 PM (64)
Jun 18, 5-6 PM (24)
Jun 18, 6-7 PM (21)
Jun 18, 7-8 PM (13)
Jun 18, 8-9 PM (17)
Jun 18, 9-10 PM (23)
Jun 18, 10-11 PM (30)
Jun 18, 11-12 AM (26)
Jun 19, 12-1 AM (13)
Jun 19, 1-2 AM (9)
Jun 19, 2-3 AM (5)
Jun 19, 3-4 AM (2)
Jun 19, 4-5 AM (11)
Jun 19, 5-6 AM (4)
Jun 19, 6-7 AM (92)
Jun 19, 7-8 AM (18)
Jun 19, 8-9 AM (37)
Jun 19, 9-10 AM (39)
Jun 19, 10-11 AM (27)
Jun 19, 11-12 PM (30)
Jun 19, 12-1 PM (53)
Jun 19, 1-2 PM (66)
Jun 19, 2-3 PM (32)
Jun 19, 3-4 PM (61)
Jun 19, 4-5 PM (7)
Jun 19, 5-6 PM (4)
Jun 19, 6-7 PM (4)
Jun 19, 7-8 PM (3)
3,144 commits this week Jun 12, 2026 - Jun 19, 2026
chore: target Node 22 LTS (drop EOL Node 18) + required compat fixes
The project was pinned to the now-EOL Node 18.12.0 (.nvmrc + all CI workflows)
with a stale `engines: >=16.20.2`. Move to the Node 22 LTS line, tracked by
major so it doesn't go stale again:

- .nvmrc: 22 · all 9 CI workflows: node-version 22 · Dockerfile: NODEJS_MAJOR_VERSION=22
- engines.node: >=22 across root + every workspace manifest
- @types/node: ^18 -> ^22

Node 22 / @types/node 22 compatibility fixes (folded in so every commit builds):
- cardano-services WsServer: type heartbeat/stake intervals as NodeJS.Timeout
  (setInterval no longer returns the legacy NodeJS.Timer; clearInterval rejects it)
- e2e measurement-util: PerformanceEntry no longer carries `detail` (now on
  PerformanceMark/Measure) — cast the entry and narrow the mark filter
- shared jest base config: fakeTimers `doNotFake: ['performance']` — Node 22's
  read-only global `performance` cannot be replaced by the fake-timers impl
  (was crashing many suites across cardano-services/ogmios/web-extension/etc.)
- TypeormStakePoolProvider util test: assert `toThrow(SyntaxError)` instead of a
  V8 JSON.parse message string that differs across Node versions

Unblocks the Node-20+ dependency majors that couldn't pass CI on Node 18.

Validated: clean full build green on @types/node 22 (all 21 workspaces);
core/crypto/key-management + cardano-services unit suites green except
DB-backed tests (no local DB) and the in-process HTTP TxSubmit tests (local
sockets) — both validated by CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
fix(bootstrap): download snapshots via ranged ring buffer
Bootstrapping from the Cloudflare R2 snapshot bucket intermittently failed
with "error decoding response body / operation timed out" while unpacking a
segment. The old path streamed a single HTTP response directly into the tar
extractor, so the connection stayed open for the entire multi-GB transfer and
its lifetime was coupled to disk-write backpressure. R2 tears down such
long-lived, slowly-drained responses where S3 tolerated them, and any
transient stall on that one connection was fatal and unrecoverable.

Replace the single stream with a ranged ring buffer (new `ranged` module):

- A background thread downloads the snapshot in bounded 64 MiB byte ranges,
  staging a small fixed-size window (4 chunks, ~256 MiB) on disk ahead of the
  extractor. Backpressure is applied *before* a request is issued (via a permit
  pool), so the server never sees an idle/slow-drained connection.
- Each chunk is short-lived and retried with exponential backoff on failure,
  making transient stalls recoverable instead of fatal.
- A HEAD probe selects the ranged path when the endpoint advertises
  `Accept-Ranges: bytes`; otherwise it falls back to the original single-stream
  download (with its own untimed client, since an overall timeout must not cap
  a full-body stream).

Verified end-to-end against the live R2 endpoint with an ignored integration
test that downloads a prefix through the ring buffer and asserts byte-exactness
versus a direct ranged fetch, plus window backpressure and staging cleanup.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
fix(cardano-services): type WsServer intervals as NodeJS.Timeout for @types/node 22
@types/node@22 has setInterval return NodeJS.Timeout and clearInterval no
longer accept the legacy NodeJS.Timer, so `clearInterval(this.heartbeatInterval)`
failed to type-check (WsServer/server.ts). Retype the heartbeatInterval and
stakeInterval fields as NodeJS.Timeout | undefined.

(Masked locally by incremental tsc --build; caught on a clean CI compile.)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
chore: target Node 22 LTS (drop EOL Node 18)
The project was pinned to Node 18.12.0 (.nvmrc + all CI workflows) with a
stale `engines: >=16.20.2` — both Node 16 and 18 are EOL. Move to the current
Node 22 LTS line, tracked by major (not a stuck patch pin like 18.12.0) so it
doesn't go stale again:

- .nvmrc: 22
- all 9 CI workflows: node-version 22
- Dockerfile: NODEJS_MAJOR_VERSION=22
- engines.node: >=22 across root + all workspace manifests
- @types/node: ^18 -> ^22

Fix surfaced by @types/node@22: PerformanceEntry no longer carries `detail`
(it's on PerformanceMark/Measure) — measurement-util now casts the entry to
read it, and the mark filter is a type predicate so the target type narrows.

Unblocks the Node-20+ dependency majors (e.g. uuid@14) that previously
couldn't pass CI on Node 18.

Validated: full build green on @types/node@22; core/crypto/key-management
suites pass (sandbox runs Node 24, i.e. >=22).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>