v2.6.0
A performance-focused minor release: three throughput/efficiency improvements to the JetStream, KV, Object Store, and request/reply paths. No breaking API changes (one account-permission note below).
Added
- Configurable transport read chunk size —
NatsOptions::$readChunkSizeBytes(default 128 KiB, up from Amp's 8 KiB) caps a single socket read on both the TCP and WebSocket transports, so large inbound payloads (e.g. an Object Store download) arrive in far fewer chunks — dividing the per-chunk syscall + fiber-spawn + parser-push overhead ~8–32×. It raises only the maximum a read may return; small-message behavior is unchanged (#119). readIncoming()onNatsClient/NatsConnectionexposes one read-and-dispatch cycle as anIncomingChunkResult(frame count plus whether bytes were consumed) — the progress signal the wait loops use.processIncoming()is unchanged (#119).
Changed (all performance, behavior-compatible)
- Muxed request inbox (#118) —
request()/requestMany()now share ONE long-lived wildcard subscription_INBOX.<base>.*per connection instead of a fresh subscribe + unsubscribe per call; each request publishes reply-to_INBOX.<base>.<token>and the reply is routed back by token. This removes two control-plane frames and a server-side interest change per request — a throughput win for JetStream, KV, Object Store, and BatchPublisher, which all funnel throughrequest(). Reply delivery, timeouts, no-responders handling, cancellation, andrequestManysemantics are unchanged; a delayed/duplicate reply for a completed request can never reach a later one, and the shared reply queue is exempt from the slow-consumer drop. Permission note: the mux inbox subscribes to a wildcard under the inbox prefix, so an account permissioned to subscribe only to exact_INBOX.<...>subjects (not_INBOX.>) will have the mux subscription rejected and request/reply will time out — grant_INBOX.>subscribe permission (the same requirement as nats.go's muxed responder; see #167). - Batched, chunked KV/Object Store enumeration (#110) —
keys()/listKeys()enumerate names via a headers-onlylast_per_subjectconsumer instead of downloading every value;getAll()and Object Storelist()fetch every record with one batchedmulti_lastDirect Get (ADR-31) on NATS 2.11+ (with a version-gated fallback to the per-subject fan-out), chunked by the server's 1024-result cap and bymax_payloadso large buckets are enumerated across several requests rather than one the server would reject. - Read-path idle-sleep removed on partial-frame progress (#119) — the read-path wait loops (
flush(),drain(),request(),requestMany(), JetStream pull-fetch and direct-get-batch,KeyValueBucket::history()/keys(),SubscriptionQueue,Service::run()) no longer pay a 1 ms idle sleep per partial socket chunk. A frame spanning N chunks previously cost ~N ms (~125 ms/MB at the old 8 KiB default); the loops now sleep only on a genuinely idle read. Delivery is byte-for-byte identical.
Full CI green: PHPStan level 8, unit tests on PHP 8.2–8.5, E2E + coverage on a live NATS 2.12 server, examples, and ≥90% mutation MSI.