This is an automated email from the ASF dual-hosted git repository.

Cole-Greer pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/master by this push:
     new a8c9d85bf3 Standardized `gremlin-js` connection options (#3472)
a8c9d85bf3 is described below

commit a8c9d85bf3f82b2f42f62adf5d9662d7b68218fb
Author: Guian Gumpac <[email protected]>
AuthorDate: Thu Jul 2 11:01:07 2026 -0700

    Standardized `gremlin-js` connection options (#3472)
    
    Implements the JavaScript portion of the TinkerPop 4.x GLV 
connection-options standardization. Adopts `undici` as a pinned dependency and 
builds an explicit dispatcher so the standardized options take effect, adds a 
set of new options, removes dead WebSocket-era fields, and removes `headers`. 
JavaScript driver changes only; the other GLVs follow in separate PRs.
    
    **Proposal:** 
https://lists.apache.org/thread/yqtr2wnb1kq2pqqq4002cz511q5o0bkg
    
    ## New options
    * **`maxConnections`** (128) - caps concurrent connections per origin on 
the default dispatcher (previously uncapped). *(breaking)*
    * **`readTimeoutMillis`** - per-read idle (body) timeout in ms (undici 
`bodyTimeout`); resets per chunk, so it is streaming-safe.
    * **`maxResponseHeaderBytes`** - maximum response header size in bytes 
(undici `maxHeaderSize`).
    * **`keepAliveTimeMillis`** (30000 ms) - idle time before TCP keep-alive 
probes begin; enables `SO_KEEPALIVE` via a custom undici connector. Set `0` to 
disable.
    * **`proxy`** - HTTP proxy URI; routes requests through an undici 
`ProxyAgent`.
    * **`compression`** (`'none'`/`'deflate'`, default `'deflate'`) - the wire 
compression negotiated with the server. *(breaking)*
    * **`batchSize`** (64) - connection-level default that fills a request's 
`batchSize` when unset.
    * **`bulkResults`** (default false) - connection-level default for 
`bulkResults`, applied to every request unless overridden per-request. The 
`DriverRemoteConnection` traversal path defaults to `true` regardless of this 
setting.
    * **`logger`** - a logger object (with `debug`/`info`/`warn`/`error` 
methods) or a `(level, message, ...args)` callback. Disabled when unset.
    
    ## Behavior changes (breaking)
    * Adopted **`undici`** as a pinned dependency (`6.27.0`). The driver now 
builds and owns an explicit undici dispatcher from the discrete options above; 
previously the options were declared but not wired to anything.
    * **`compression`** defaults to `'deflate'` (on), so the driver sends 
`Accept-Encoding: deflate` by default. Set `compression: 'none'` to disable; 
this also suppresses the auto-injected `Accept-Encoding` so compression is not 
silently negotiated. Defaulting on is a deliberate deviation from the 
proposal's agreed default-off, applied consistently across the GLVs by later 
agreement.
    * **`maxConnections`** now caps concurrent connections at 128 (was 
uncapped).
    
    ## Removed (breaking)
    * **`agent`**, **`ca`**, **`cert`**, **`pfx`**, and 
**`rejectUnauthorized`** TLS fields - they were declared (WebSocket-era 
leftovers) but never wired to the HTTP transport. TLS is now configured through 
the Node/undici runtime (`NODE_EXTRA_CA_CERTS`, `NODE_TLS_REJECT_UNAUTHORIZED`).
    * **`headers`** - removed as a deprecated option.
    
    ## Bug fix
    * Fixed `Client.submit()` so that an explicit `bulkResults: false` request 
option is forwarded to the server instead of being silently dropped.
    
    ## Notes
    * Per the proposal, JS does not expose 
`connectTimeout`/`idleTimeout`/`writeTimeout` as driver options (the undici 
runtime defaults are used), and TLS is runtime-delegated rather than a driver 
option. No public `dispatcher` option is exposed; the dispatcher is built 
internally from the discrete options.
    
    ## Testing
    * `gremlin-javascript` builds (dual CJS/ESM) and all unit tests pass, 
including new `client`, `connection`, `dispatcher`, and `logger` suites 
covering the option-to-undici mapping, compression negotiation, keep-alive 
wiring, and `bulkResults` precedence.
    
    Assisted-by: Kiro: Claude Opus 4.8
---
 CHANGELOG.asciidoc                                 |  58 +----
 docs/src/reference/gremlin-variants.asciidoc       |  34 ++-
 docs/src/upgrade/release-4.x.x.asciidoc            | 270 ++++++---------------
 gremlin-examples/gremlin-javascript/connections.js |  11 +-
 gremlin-js/gremlin-javascript/lib/driver/client.ts |  11 +-
 .../gremlin-javascript/lib/driver/connection.ts    | 114 +++++++--
 .../lib/driver/dispatcher.browser.ts               |  79 ++++++
 .../gremlin-javascript/lib/driver/dispatcher.ts    | 124 ++++++++++
 gremlin-js/gremlin-javascript/lib/driver/logger.ts |  71 ++++++
 .../lib/driver/request-message.ts                  |  19 ++
 gremlin-js/gremlin-javascript/package.json         |   7 +-
 gremlin-js/gremlin-javascript/test/helper.js       |   2 -
 .../gremlin-javascript/test/unit/client-test.js    |  96 ++++++++
 .../test/unit/connection-test.js                   |  67 ++++-
 .../test/unit/dispatcher-browser-test.js           |  59 +++++
 .../test/unit/dispatcher-test.js                   | 187 ++++++++++++++
 .../gremlin-javascript/test/unit/logger-test.js    |  67 +++++
 gremlin-js/gremlin-mcp/src/gremlin/connection.ts   |  15 +-
 gremlin-js/package-lock.json                       |  12 +-
 19 files changed, 1007 insertions(+), 296 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index b1456d0ec1..c58d88da0f 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -25,62 +25,22 @@ 
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 [[release-4-0-0]]
 === TinkerPop 4.0.0 (Release Date: NOT OFFICIALLY RELEASED YET)
 
-* Standardized `gremlin-dotnet` connection options per the TinkerPop 4.x GLV 
proposal:
-** Renamed the `Auth.BasicAuth`/`Auth.SigV4Auth` factory methods to 
`Auth.Basic`/`Auth.Sigv4` (breaking; the old methods have been removed). 
*(breaking)*
-** Renamed `MaxConnectionsPerServer` to `MaxConnections`, 
`IdleConnectionTimeout` to `IdleTimeout`, and `KeepAliveInterval` to 
`KeepAliveTime` (now wired to a real TCP keep-alive socket option rather than 
the inert HTTP/2 ping timeout, enabling `SO_KEEPALIVE` and setting the 
per-socket idle time on Windows, Linux, and macOS, with a no-op fallback to the 
OS default idle time on other platforms); the old property names have been 
removed. *(breaking)*
-** Renamed `ConnectionTimeout` to `ConnectTimeout` (default lowered from 15s 
to 5s; old name removed). *(breaking)*
-** Renamed `EnableCompression` to `Compression`, now a `{None, Deflate}` enum 
defaulting to `Deflate` (compression on by default; set `None` to disable). The 
old `EnableCompression` `bool` has been removed. *(breaking)*
-** Added `Ssl` (an `SslClientAuthenticationOptions`; 
`SkipCertificateValidation` is applied to an internal copy rather than mutating 
the caller's options).
-** Added `BatchSize` (default 64), a connection-level default that fills the 
per-request `batchSize` when unset.
-** Added `MaxResponseHeaderBytes`, exposing the handler's maximum response 
header size.
-** Added `ReadTimeout`, a per-read idle timeout applied to each read of the 
response stream.
-** Each timeout option is also settable in milliseconds via an `int` companion 
property (`ConnectTimeoutMillis`, `IdleTimeoutMillis`, `ReadTimeoutMillis`, 
`KeepAliveTimeMillis`); the unsuffixed `TimeSpan` property remains the 
idiomatic form and both reflect the same value.
-** Added `Proxy`, routing connections through an `IWebProxy`.
+* Standardized connection options across all GLVs (Java, Python, .NET, Go, 
JavaScript) per the TinkerPop 4.x GLV proposal; see the upgrade docs for the 
full per-driver table. *(breaking)*
+** Aligned option names across drivers: `maxConnections` (128), 
`connectTimeoutMillis` (5000), `readTimeoutMillis` (off), `idleTimeoutMillis` 
(180000), `keepAliveTimeMillis` (30000), `compression` (on/`deflate`), 
`batchSize` (64), `bulkResults` (false), `maxResponseHeaderBytes`, `proxy`, and 
`ssl`. Timeouts use a millisecond-suffixed canonical name with an idiomatic 
duration companion (Java `Duration`, Go `time.Duration`, .NET `TimeSpan`, 
Python seconds); JavaScript uses milliseconds only.
+** Java: renamed 
`maxConnectionPoolSize`/`connectionSetupTimeoutMillis`/`idleConnectionTimeoutMillis`/`resultIterationBatchSize`
 and `RequestOptions.addG`->`traversalSource`; removed 
`maxResponseContentLength` (responses now stream); added `readTimeoutMillis`, 
`keepAliveTimeMillis`, `maxResponseHeaderBytes`, `proxy`, `url(String)`, 
`ssl(SslContext)`. *(breaking)*
+** Python: renamed `pool_size`->`max_connections` (8->128) and 
`ssl_options`->`ssl`; removed `headers` (use interceptors) and 
`max_content_length`; added the timeout, `compression`, `batch_size`, `proxy`, 
and `trust_env` options; requires `aiohttp>=3.11`. *(breaking)*
+** .NET: renamed 
`MaxConnectionsPerServer`/`ConnectionTimeout`/`IdleConnectionTimeout`/`KeepAliveInterval`/`EnableCompression`
 and `Auth.BasicAuth`/`Auth.SigV4Auth`->`Auth.Basic`/`Auth.Sigv4`; 
`KeepAliveTime` now uses a real TCP keep-alive socket option; added 
`ReadTimeout`, `MaxResponseHeaderBytes`, `Proxy`, `Ssl`, `BulkResults`. 
*(breaking)*
+** Go: renamed 
`MaximumConcurrentConnections`/`IdleConnectionTimeout`/`KeepAliveInterval`/`ConnectionTimeout`/`TlsConfig`/`RequestInterceptors`/`EnableCompression`;
 moved auth helpers into an `auth` sub-package 
(`auth.Basic`/`auth.SigV4`/`auth.SigV4WithCredentials`); added `ReadTimeout`, 
`MaxResponseHeaderBytes`, `Proxy`, `BatchSize`, `BulkResults`. *(breaking)*
+** JavaScript: adopted `undici` for the default dispatcher; added 
`readTimeoutMillis`, `keepAliveTimeMillis`, `maxResponseHeaderBytes`, `proxy`, 
`compression`, `batchSize`, `bulkResults`, `logger`; removed `headers` (use 
interceptors) and `ca`/`cert`/`pfx`/`rejectUnauthorized`/`agent` (TLS via the 
Node/undici runtime); undici is swapped out in browser bundles. *(breaking)*
+** All drivers: compression now defaults on; `connectTimeout` lowered to 5s 
and applied to transport establishment; `readTimeout` is a streaming-safe 
idle-read timeout (off by default); `idleTimeout` reaps only pooled connections.
+* Fixed `gremlin-javascript` `Client.submit()` so that an explicit 
`bulkResults: false` request option is forwarded to the server instead of being 
silently dropped.
 * Fixed `gremlin-dotnet` deflate response decompression, which threw on the 
server's zlib-framed output because it used `DeflateStream` (raw DEFLATE, RFC 
1951) instead of `ZLibStream` (zlib, RFC 1950); the bug was previously masked 
because compression was off by default.
 * Fixed `gremlin-dotnet` SSL options cloning (used on the 
skip-certificate-validation path) to copy `ClientCertificateContext` and 
`AllowTlsResume`, which were previously dropped, breaking mTLS client 
certificates and silently re-enabling TLS resumption.
-* Standardized `gremlin-python` connection options per the TinkerPop 4.x GLV 
proposal:
-** Renamed `pool_size` to `max_connections` (breaking; the old name has been 
removed) and changed the default from 8 to 128; `max_connections` is now also 
applied to the aiohttp `TCPConnector` `limit` so the transport layer reflects 
the option in addition to sizing the Connection pool.
-** Renamed `ssl_options` to `ssl` accepting an `ssl.SSLContext` (breaking; the 
old name has been removed).
-** Added `connect_timeout` (default 5s) and rewired the existing 
`read_timeout` into a single aiohttp `ClientTimeout` 
(`sock_connect`/`sock_read`); the socket-level knobs are used rather than a 
whole-request `total` so long but legitimate streaming responses are not 
aborted, while a stalled server no longer hangs forever.
-** Added `idle_timeout` (default 180s) mapped to the aiohttp `TCPConnector` 
keep-alive timeout.
-** Added `keep_alive_time` (default 30s) enabling TCP keep-alive probes via 
the connector socket factory (`TCP_KEEPIDLE`/`TCP_KEEPALIVE` guarded by 
platform availability); this required raising the minimum `aiohttp` to `3.11` 
(the `socket_factory` floor). *(breaking)*
-** Each timeout option is named with a millisecond suffix as the primary form 
(`connect_timeout_millis`, `idle_timeout_millis`, `read_timeout_millis`, 
`keep_alive_time_millis`); the unsuffixed canonical name (`connect_timeout`, 
etc.) accepts the idiomatic seconds value. Both resolve to seconds internally 
for aiohttp.
-** Added `compression` accepting `'none'`/`'deflate'`, default `'deflate'` 
(on), advertising `Accept-Encoding: deflate` by default; when set to `'none'`, 
aiohttp's automatic `Accept-Encoding` injection is suppressed so compression is 
not silently negotiated.
-** Added `batch_size` (default 64) connection-level default that fills the 
per-request `batchSize` when unset.
-** Added explicit HTTP `proxy` and `trust_env` options surfaced on the aiohttp 
`ClientSession`.
-** Added a credentials-provider variant to `auth.sigv4` that accepts an 
optional credentials provider or callable, falling back to the AWS environment 
variables.
-** Removed the `max_content_length` kwarg from the `gremlin-python` driver 
(previously accepted but discarded).
-** Removed the standalone `headers` kwarg from `gremlin-python` 
`Client`/`DriverRemoteConnection`; custom headers must now be set via 
interceptors.
-** Fixed `gremlin-python` `Client.submit`/`submit_async` mutating a 
caller-supplied `RequestMessage` in place; the message fields are now cloned 
before applying `request_options`/`batch_size`, so resubmitting the same 
message no longer accumulates state, matching the no-mutate contract of the 
.NET/JS drivers.
-** Fixed `gremlin-python` `Client.submit`/`submit_async` mutating a 
caller-supplied `RequestMessage` in place; the message fields are now cloned 
before applying `request_options`/`default_batch_size`, so resubmitting the 
same message no longer accumulates state, matching the no-mutate contract of 
the .NET/JS drivers.
-* Standardized `gremlin-driver` (Java) connection options
-** Renamed the `connectionSetupTimeoutMillis` builder option to 
`connectTimeout` (default lowered from 15s to 5s) and actually wired it to 
`ChannelOption.CONNECT_TIMEOUT_MILLIS` on the connection bootstrap; it 
previously was validated but never applied. The old name is removed. Settable 
as `connectTimeout(Duration)` or `connectTimeoutMillis(int)` (YAML key 
`connectionPool.connectTimeoutMillis`). *(breaking)*
-** Renamed `maxConnectionPoolSize` to `maxConnections` (default 128), 
`idleConnectionTimeoutMillis` to `idleTimeout` (default 180s), and 
`resultIterationBatchSize` to `batchSize` (default 64; the connection-level 
default that fills a request's per-request `batchSize` when unset, mirroring 
how `bulkResults` shares one name across scopes); the old builder methods, 
accessors, and YAML keys are removed. *(breaking)*
-** Added `readTimeout` (default 0/off), a per-request idle-read timeout armed 
when a request is written and reset on each inbound response chunk via a new 
`ReadTimeoutHandler` inserted after the HTTP codec, so it never fires while a 
pooled connection is idle between requests. Settable as `readTimeout(Duration)` 
or `readTimeoutMillis(long)` (YAML key `connectionPool.readTimeoutMillis`).
-** Scoped `idleTimeout` to pooled connections only: an idle event that fires 
while a request is in flight is now ignored, so `idleTimeout` reaps only 
connections idle in the pool and no longer overlaps `readTimeout` on the 
in-flight window. A stalled in-flight response is bounded solely by 
`readTimeout`. Settable as `idleTimeout(Duration)` or `idleTimeoutMillis(long)` 
(YAML key `connectionPool.idleTimeoutMillis`). *(breaking)*
-** Renamed `RequestOptions.Builder.addG` to `traversalSource`; the old name is 
removed. *(breaking)*
-** Added `keepAliveTime` (default 30s/30000ms), the idle time before TCP 
keep-alive probes begin, enabling `ChannelOption.SO_KEEPALIVE` on the 
connection bootstrap and configuring the per-socket idle time via the JDK 
`TCP_KEEPIDLE` extended socket option where supported (JDK 11+ on Linux/macOS); 
it is a no-op fallback to the OS default idle time on platforms/JDKs where that 
option is unavailable. Set to `0` to disable. Settable as 
`keepAliveTime(Duration)` or `keepAliveTimeMillis(long)`  [...]
-** Added a `url(String)` builder method that configures the endpoint from a 
single URL, deriving SSL from the scheme (`https` enables, `http` disables), 
the contact point from the host, and the port and path when present; 
`addContactPoint(s)`/`port`/`path` remain for multi-host configurations.
-** Added `compression`, a `{NONE, DEFLATE}` enum defaulting to `DEFLATE` 
(compression on by default; set `NONE` to disable). `Accept-Encoding: deflate` 
is sent whenever `compression == DEFLATE`.
-** Added `maxResponseHeaderBytes` (default 8192) exposing the Netty 
`HttpClientCodec` max header size, previously hardcoded.
-** Added an `ssl(SslContext)` builder accepting a fully configured Netty 
`SslContext`.
-** Added a `proxy(ProxyOptions)` builder that inserts a Netty 
`HttpProxyHandler` into the pipeline before the SSL handler.
-** Removed the `maxResponseContentLength` connection option and its 
`HttpObjectAggregator` cap; responses are streamed and the new `readTimeout` is 
the partial mitigation.
-** Reconciled the `validationRequest` default: the builder default is now 
`g.inject(0)` to match the `Settings` default (it was previously `''`).
 * Removed `Transaction.open()` in favor of `begin()`, which is now the single 
transaction-start primitive across embedded and remote contexts.
 * Changed `begin()` and `close()` to be idempotent and calling it when a 
transaction is already in that state no longer throws.
 * Added `maxTransactionLifetime` setting to Gremlin Server, an absolute cap on 
the total age of an HTTP transaction that interrupts a running operation and 
rolls the transaction back when it fires (default 600000ms, set to `0` to 
disable).
 * Changed the Gremlin Server HTTP transaction idle timer to suspend while an 
operation is running (so a long-running operation is bounded by 
`evaluationTimeout` rather than the idle timeout) and to honor `0` as "disable 
idle reclamation"; the `idleTransactionTimeout` default is now 60000ms.
 * Added configurable CORS `allowedOrigins` setting to Gremlin Server; warns 
when wildcard origin is used alongside authentication.
-* Standardized `gremlin-go` connection options per the TinkerPop 4.x GLV 
proposal:
-** Moved `BasicAuth`/`SigV4Auth`/`SigV4AuthWithCredentials` out of package 
`gremlingo` into a new `auth` sub-package as 
`auth.Basic`/`auth.SigV4`/`auth.SigV4WithCredentials`. The flat `gremlingo` 
functions have been removed; use the `auth` sub-package.
-** Renamed `MaximumConcurrentConnections` to `MaxConnections` (default 128), 
`IdleConnectionTimeout` to `IdleTimeoutMillis` (default 180000), 
`KeepAliveInterval` to `KeepAliveTimeMillis` (default 30000), 
`ConnectionTimeout` to `ConnectTimeoutMillis` (default 5000), `TlsConfig` to 
`Ssl` (`*tls.Config`), and `RequestInterceptors` to `Interceptors`. Each 
timeout has a `time.Duration` companion 
(`IdleTimeout`/`KeepAliveTime`/`ConnectTimeout`); set only one form per option. 
*(breaking)*
-** Renamed `EnableCompression` to `Compression`, now a typed `Compression` 
const (`CompressionNone`/`CompressionDeflate`) defaulting to 
`CompressionDeflate` (compression on by default; set `CompressionNone` to 
disable); `Accept-Encoding: deflate` is sent by default and the manual 
per-chunk deflate decode path is retained. *(breaking)*
-** Added `BatchSize` (default 64), a connection-level default that fills the 
per-request `batchSize` when unset.
-** Added `MaxResponseHeaderBytes`, exposing 
`http.Transport.MaxResponseHeaderBytes`.
-** Added `Proxy` and set `http.Transport.Proxy` to `http.ProxyFromEnvironment` 
by default, fixing a regression where a custom transport silently dropped 
environment proxy configuration.
-** Added `ReadTimeoutMillis` (with a `time.Duration` companion `ReadTimeout`; 
set only one), an idle-read timeout reset on each read (via `SetReadDeadline`) 
that is re-armed across pooled-connection reuse and does not set 
`http.Client.Timeout`.
-** Added `BulkResults` (default false), a connection-level default for 
`bulkResults` applied to every request unless overridden per-request, aligning 
gremlin-go with the other GLVs which already expose a connection-level setting; 
the `DriverRemoteConnection` traversal path defaults to `true` regardless of 
this setting.
 * Fixed `ByteBuf` leak in `GraphBinaryMessageSerializerV4` when serialization 
throws an `IOException`.
 * Changed `Tree` to no longer extend `HashMap`; it is now a final class with a 
tree-shaped API (`childAt`, `hasChild`, `contains`, `findSubtree`, 
`getOrCreateChild`, `getNodesAtDepth`, `getLeafNodes`, `nodeCount`) and is no 
longer a `Map`.
 * Changed `count(local)` on a `Tree` to return the total node count 
(`Tree.nodeCount()`) instead of the root-entry count.
diff --git a/docs/src/reference/gremlin-variants.asciidoc 
b/docs/src/reference/gremlin-variants.asciidoc
index 941e2e83dd..33e9f990ff 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -1984,18 +1984,42 @@ can be passed in the constructor of a new `Client` or 
`DriverRemoteConnection` :
 |Key |Type |Description |Default
 |url |String |The resource uri. |None
 |options |Object |The connection options. |{}
-|options.traversalSource |String |The traversal source. |'g'
-|options.headers |Object |Additional HTTP header key/values included with each 
request. |undefined
-|options.interceptors |RequestInterceptor/RequestInterceptor[] |One or more 
functions that can modify the HTTP request before it is sent. |undefined
+|options.traversalSource |String |The name of the remote 
`GraphTraversalSource`. |'g'
+|options.maxConnections |Number |Caps the number of concurrent connections per 
origin on the default dispatcher. |128
+|options.readTimeoutMillis |Number |A per-read idle timeout in milliseconds 
(undici `bodyTimeout`). It resets per chunk, so it is safe for streaming. 
|runtime default
+|options.maxResponseHeaderBytes |Number |The maximum size of the response 
headers in bytes (undici `maxHeaderSize`). |runtime default
+|options.keepAliveTimeMillis |Number |Idle time in milliseconds before TCP 
keep-alive probes begin. Enables `SO_KEEPALIVE` on the socket. Set to `0` to 
disable. |30000
+|options.proxy |String |An HTTP proxy URI. When set, requests are routed 
through an undici `ProxyAgent`. |undefined
+|options.compression |String |Response compression codec, `'none'` or 
`'deflate'`. |'deflate'
+|options.batchSize |Number |Connection-level default that fills a request's 
`batchSize` when it is left unset. |64
+|options.bulkResults |Boolean |Connection-level default for `bulkResults`, 
applied to every request unless overridden per-request. The 
`DriverRemoteConnection` traversal path defaults to `true` regardless of this 
setting. |false
+|options.logger |Object/Function |A logger object (with 
`debug`/`info`/`warn`/`error` methods, e.g. `console`) or a `(level, message, 
...args)` callback. |none (disabled)
+|options.interceptors |RequestInterceptor/RequestInterceptor[] |One or more 
functions that can modify the HTTP request before it is sent, run in order. 
|undefined
 |options.auth |RequestInterceptor |An auth interceptor that is always appended 
to the end of the interceptor list so it runs last. |undefined
 |options.preciseNumbers |Boolean |When `true`, wraps deserialized numbers in 
typed wrappers that preserve the server's original type. |undefined
 |options.reader |GraphBinaryReader |The reader to use for deserializing 
responses. |GraphBinaryReader
-|options.writer |GraphBinaryWriter |The writer to use for serializing 
requests. |GraphBinaryWriter
 |options.enableUserAgentOnConnect |Boolean |Determines if a user agent header 
will be sent with requests. |true
-|options.agent |Agent |A custom `node:http` or `node:https` Agent for 
connection pooling or proxy configuration. |undefined
 |options.pdtRegistry |ProviderDefinedTypeRegistry |A registry for hydrating 
and dehydrating <<gremlin-javascript-pdt,Provider Defined Types>>. |undefined
 |=========================================================
 
+TLS is configured through the Node.js/undici runtime (for example the 
`NODE_EXTRA_CA_CERTS` or
+`NODE_TLS_REJECT_UNAUTHORIZED` environment variables), not through driver 
options. Custom headers are set via an
+interceptor rather than a `headers` option, for example `interceptors: (req) 
=> { req.headers['X-Custom'] = 'value'; }`.
+
+Note that no driver timeout bounds the *total* duration of a request once it 
is under way. `readTimeoutMillis` only bounds
+the gap between response chunks, so a response that keeps producing chunks 
will not time out no matter how long it
+runs overall, and there is no client-side "overall" request timeout. If you 
need an absolute deadline, impose it in
+your application around the call by racing the `submit` promise against a 
timeout:
+
+[source,javascript]
+----
+// bound the entire request to 30 seconds
+const results = await Promise.race([
+  client.submit('g.V().out().out()'),
+  new Promise((_, reject) => setTimeout(() => reject(new Error('request timed 
out')), 30000)),
+]);
+----
+
 [[gremlin-javascript-interceptors]]
 === RequestInterceptor
 
diff --git a/docs/src/upgrade/release-4.x.x.asciidoc 
b/docs/src/upgrade/release-4.x.x.asciidoc
index a6040deba5..4c2db11a7a 100644
--- a/docs/src/upgrade/release-4.x.x.asciidoc
+++ b/docs/src/upgrade/release-4.x.x.asciidoc
@@ -32,208 +32,80 @@ complete list of all the modifications that are part of 
this release.
 
 === Upgrading for Users
 
-==== Standardizing .NET Connection Options
-
-TinkerPop 4.x standardizes connection option names and defaults across the 
GLVs. In `gremlin-dotnet`, several
-`ConnectionSettings` properties and the `Auth` factory methods have been 
renamed for consistency. Because this is a
-major version, the old names have been removed rather than retained as 
aliases, and a number of new options have been
-added. The notes below describe the .NET changes. See <<glv-driver-changes, 
GLV Driver Changes>> for the
-equivalent changes in the other drivers.
-
-Renames (breaking). The following members have been renamed and the old names 
removed. Migrate to the new names:
-
-- `ConnectionTimeout` is now `ConnectTimeout`.
-- `MaxConnectionsPerServer` is now `MaxConnections`.
-- `IdleConnectionTimeout` is now `IdleTimeout`.
-- `KeepAliveInterval` is now `KeepAliveTime`.
-- `EnableCompression` is now `Compression`.
-- The `Auth.BasicAuth` and `Auth.SigV4Auth` factory methods are now 
`Auth.Basic` and `Auth.Sigv4`.
-
-Behavior changes. These change runtime behavior on upgrade, even if you do not 
change your configuration:
-
-- `ConnectTimeout` now defaults to 5s (lowered from 15s).
-- `KeepAliveTime` is now wired to a real TCP keep-alive socket option rather 
than the inert HTTP/2 ping timeout. It
-enables `SO_KEEPALIVE` and sets the per-socket idle time on Windows, Linux, 
and macOS; on other platforms keep-alive
-stays enabled at the OS default idle time.
-- `Compression` is now a `{None, Deflate}` enum defaulting to `Deflate` 
(compression on by default), so the driver
-sends `Accept-Encoding: deflate` by default. Set `Compression.None` to 
disable. The old `EnableCompression` `bool`
-has been removed.
-
-New options:
-
-- `Ssl` (an `SslClientAuthenticationOptions` for client certificates and 
custom CAs; `SkipCertificateValidation` is
-applied to an internal copy rather than mutating the supplied options).
-- `BatchSize` (default 64): a connection-level default that fills the 
per-request batch size when unset.
-- `MaxResponseHeaderBytes`: the maximum allowed size, in bytes, of the 
response headers.
-- `ReadTimeout`: a per-read idle timeout applied to each individual read of 
the response stream.
-- Each timeout is also settable in milliseconds via an `int` companion 
property (`ConnectTimeoutMillis`, `IdleTimeoutMillis`,
-  `ReadTimeoutMillis`, `KeepAliveTimeMillis`); the unsuffixed `TimeSpan` 
property is the idiomatic form.
-- `Proxy`: routes connections through an `IWebProxy`.
+==== Standardizing GLV Connection Options
+
+TinkerPop 4.x standardizes connection option names and defaults across all 
five Gremlin Language Variants (Java, Python,
+.NET, Go, and JavaScript). Each driver using its language-idiomatic casing 
(`camelCase`, `PascalCase`, or `snake_case`).
+Renames are breaking: the old option names have been removed (no deprecated 
aliases), so existing code must be updated.
+
+NOTE: Timeouts use a millisecond-suffixed canonical name 
(`connectTimeoutMillis`, `readTimeoutMillis`,
+`idleTimeoutMillis`, `keepAliveTimeMillis`, and the `_millis` form in Python). 
Java, Go, .NET, and Python also accept an
+idiomatic duration companion for the same setting (Java `Duration`, Go 
`time.Duration`, .NET `TimeSpan`, Python seconds);
+set only one form per option. JavaScript exposes only the millisecond form.
+
+===== Standardized options (cross-GLV)
+
+The table lists each standardized option by driver. Defaults are shown in 
parentheses; "n/a" means the driver does not
+expose that option.
+
+[width="100%",cols="2,2,2,2,2,2",options="header"]
+|=========================================================
+|Concept |Java (`Cluster.Builder`) |Python (kwarg) |.NET 
(`ConnectionSettings`) |Go (settings field) |JavaScript (option)
+|Max pooled connections |`maxConnections` (128) |`max_connections` (128) 
|`MaxConnections` (128) |`MaxConnections` (128) |`maxConnections` (128)
+|Connect timeout |`connectTimeoutMillis` (5000) |`connect_timeout_millis` 
(5000) |`ConnectTimeoutMillis` (5000) |`ConnectTimeoutMillis` (5000) |n/a
+|Idle-read timeout |`readTimeoutMillis` (0/off) |`read_timeout_millis` (off) 
|`ReadTimeoutMillis` (0/off) |`ReadTimeoutMillis` (0/off) |`readTimeoutMillis` 
(undici default)
+|Pool idle timeout |`idleTimeoutMillis` (180000) |`idle_timeout_millis` 
(180000) |`IdleTimeout` (180s) |`IdleTimeoutMillis` (180000) |n/a
+|TCP keep-alive idle |`keepAliveTimeMillis` (30000) |`keep_alive_time_millis` 
(30000) |`KeepAliveTime` (30s) |`KeepAliveTimeMillis` (30000) 
|`keepAliveTimeMillis` (30000)
+|Compression |`compression` (`DEFLATE`) |`compression` (`'deflate'`) 
|`Compression` (`Deflate`) |`Compression` (`CompressionDeflate`) |`compression` 
(`'deflate'`)
+|Connection-level batch size |`batchSize` (64) |`batch_size` (64) |`BatchSize` 
(64) |`BatchSize` (64) |`batchSize` (64)
+|Connection-level bulkResults |`bulkResults` (false) |`bulk_results` (false) 
|`BulkResults` (false) |`BulkResults` (false) |`bulkResults` (false)
+|Max response header bytes |`maxResponseHeaderBytes` (8192) |n/a 
|`MaxResponseHeaderBytes` (handler default) |`MaxResponseHeaderBytes` (net/http 
default) |`maxResponseHeaderBytes` (undici default)
+|TLS configuration |`ssl(SslContext)` + keystore builders |`ssl` 
(`SSLContext`) |`Ssl` (`SslClientAuthenticationOptions`) |`Ssl` (`*tls.Config`) 
|runtime (`NODE_EXTRA_CA_CERTS`, etc.)
+|HTTP proxy |`proxy(ProxyOptions)` |`proxy` |`Proxy` (`IWebProxy`) |`Proxy` 
(env default) |`proxy` (undici `ProxyAgent`)
+|Request interceptors |`interceptors` |`interceptors` |interceptor delegates 
|`Interceptors` |`interceptors`
+|=========================================================
+
+===== Behavior changes (all drivers)
+
+These change runtime behavior on upgrade even if you do not change your 
configuration:
+
+- *Compression defaults to on.* Every driver now defaults compression to 
`deflate` and sends `Accept-Encoding: deflate`.
+  Disable it with the compression option's "none" value (`Compression.NONE`, 
`'none'`, `Compression.None`,
+  `CompressionNone`).
+- *Connect timeout lowered to 5s* (from 15s, where applicable) and is now 
actually applied to transport establishment
+  (TCP connect plus TLS handshake), not the whole request.
+- *`readTimeout` is an idle-read timeout*, reset on each inbound response 
chunk, so it is streaming-safe and bounds only
+  the gap between chunks, not total response duration. It is off by default.
+- *`idleTimeout` reaps only pooled connections* that are idle between 
requests; it no longer bounds an in-flight
+  response (that is `readTimeout`'s job).
+
+===== Driver-specific notes
+
+- *Java* (`gremlin-driver`): renamed 
`maxConnectionPoolSize`->`maxConnections`, 
`connectionSetupTimeoutMillis`->`connectTimeoutMillis`,
+  `idleConnectionTimeoutMillis`->`idleTimeoutMillis`, 
`resultIterationBatchSize`->`batchSize`, and `RequestOptions` 
`addG`->`traversalSource`;
+  old names are removed. New: `readTimeoutMillis`, `keepAliveTimeMillis`, 
`maxResponseHeaderBytes`, `proxy(ProxyOptions)`,
+  `url(String)`, `ssl(SslContext)`. Removed `maxResponseContentLength`. 
`validationRequest` default reconciled to `g.inject(0)`.
+- *Python* (`gremlin-python`): renamed `pool_size`->`max_connections` (default 
8->128) and `ssl_options`->`ssl` (old names removed).
+  New: `connect_timeout_millis`, `read_timeout_millis`, `idle_timeout_millis`, 
`keep_alive_time_millis`, `compression`,
+  `batch_size`, `proxy`, `trust_env`. `auth.sigv4` gained an optional 
credentials provider. Removed `headers` (use interceptors)
+  and `max_content_length`. Requires `aiohttp>=3.11`.
+- *.NET* (`gremlin-dotnet`): renamed 
`MaxConnectionsPerServer`->`MaxConnections`, 
`ConnectionTimeout`->`ConnectTimeout`,
+  `IdleConnectionTimeout`->`IdleTimeout`, 
`KeepAliveInterval`->`KeepAliveTime`, `EnableCompression`->`Compression`, and
+  `Auth.BasicAuth`/`Auth.SigV4Auth`->`Auth.Basic`/`Auth.Sigv4`; old names 
removed. New: `ReadTimeout`, `MaxResponseHeaderBytes`,
+  `Proxy`, `Ssl`, `BulkResults`. `KeepAliveTime` now drives a real TCP 
keep-alive socket option instead of the inert HTTP/2 ping.
+- *Go* (`gremlin-go`): renamed 
`MaximumConcurrentConnections`->`MaxConnections`, 
`IdleConnectionTimeout`->`IdleTimeout`,
+  `KeepAliveInterval`->`KeepAliveTime`, `ConnectionTimeout`->`ConnectTimeout`, 
`TlsConfig`->`Ssl`, `RequestInterceptors`->`Interceptors`,
+  `EnableCompression`->`Compression`; old struct fields removed. Auth helpers 
moved from package `gremlingo`
+  (`BasicAuth`/`SigV4Auth`/`SigV4AuthWithCredentials`) into a new `auth` 
sub-package (`auth.Basic`/`auth.SigV4`/`auth.SigV4WithCredentials`).
+  New: `ReadTimeout(Millis)`, `MaxResponseHeaderBytes`, `Proxy` (defaults to 
`http.ProxyFromEnvironment`), `BatchSize`, `BulkResults`.
+- *JavaScript* (`gremlin-javascript`): adopted `undici` as a pinned dependency 
providing the default dispatcher built from the
+  options above. New: `readTimeoutMillis`, `keepAliveTimeMillis`, 
`maxResponseHeaderBytes`, `proxy`, `compression`, `batchSize`,
+  `bulkResults`, `logger`. Removed `headers` (use interceptors) and the 
`ca`/`cert`/`pfx`/`rejectUnauthorized`/`agent` options
+  (TLS is configured through the Node/undici runtime). undici is swapped out 
in browser bundles, where these connection-pool
+  options are managed by the browser.
 
 See: 
link:https://lists.apache.org/thread/yqtr2wnb1kq2pqqq4002cz511q5o0bkg[[DISCUSS] 
Standardizing GLV connection options in TinkerPop 4].
 
-==== Standardizing Go Connection Options
-
-TinkerPop 4.x standardizes connection option names and defaults across the 
GLVs. In `gremlin-go`, several
-`DriverRemoteConnectionSettings`/`ClientSettings` options introduced for the 
HTTP driver in 4.0.0-beta.2 have been
-renamed for consistency, the authentication helpers have moved into a 
dedicated `auth` sub-package, and a number of
-new options have been added. The notes below describe the Go changes. See 
<<glv-driver-changes, GLV Driver Changes>>
-for the equivalent changes in the other drivers.
-
-Renames (breaking). The following settings fields have been renamed. Because 
they are struct fields, they cannot be
-aliased, so existing code must be updated to the new names:
-
-- `MaximumConcurrentConnections` is now `MaxConnections` (default 128).
-- `IdleConnectionTimeout` is now `IdleTimeoutMillis` (default 180000), an 
`int` of milliseconds. The `time.Duration` companion `IdleTimeout` may be set 
instead (set only one).
-- `KeepAliveInterval` is now `KeepAliveTimeMillis` (default 30000), an `int` 
of milliseconds. The `time.Duration` companion `KeepAliveTime` may be set 
instead (set only one).
-- `ConnectionTimeout` is now `ConnectTimeoutMillis` (default 5000), an `int` 
of milliseconds. The `time.Duration` companion `ConnectTimeout` may be set 
instead (set only one).
-- `TlsConfig` (`*tls.Config`) is now `Ssl`.
-- `RequestInterceptors` is now `Interceptors`.
-- `EnableCompression` is now `Compression`.
-- The `BasicAuth`, `SigV4Auth`, and `SigV4AuthWithCredentials` functions have 
moved out of package `gremlingo` into a
-new `auth` sub-package 
(`github.com/apache/tinkerpop/gremlin-go/v4/driver/auth`) as `auth.Basic`, 
`auth.SigV4`, and
-`auth.SigV4WithCredentials`. The flat `gremlingo` functions have been removed; 
use the `auth` sub-package.
-
-Behavior changes. These change runtime behavior on upgrade, even if you do not 
change your configuration:
-
-- `Compression` is now a typed `Compression` const 
(`gremlingo.CompressionNone`/`gremlingo.CompressionDeflate`)
-defaulting to `CompressionDeflate` (compression on by default), so the driver 
sends `Accept-Encoding: deflate` by
-default. Set `gremlingo.CompressionNone` to disable it. The manual per-chunk 
deflate decode path is retained.
-- `http.Transport.Proxy` now defaults to `http.ProxyFromEnvironment`, so the 
standard `HTTP_PROXY`/`HTTPS_PROXY`/
-`NO_PROXY` environment variables are honored. Previously the custom transport 
left `Proxy` unset, silently dropping
-any environment proxy configuration.
-
-New options:
-
-- `ReadTimeoutMillis` (default 0, disabled), an `int` of milliseconds: a 
per-read idle timeout reset on each read of
-the response body and re-armed across pooled-connection reuse, so it never 
fires while a pooled connection is idle
-between requests. The `time.Duration` companion `ReadTimeout` may be set 
instead (set only one).
-- `MaxResponseHeaderBytes`: exposes `http.Transport.MaxResponseHeaderBytes` 
(native bytes).
-- `Proxy`: an explicit `func(*http.Request) (*url.URL, error)` proxy override 
for the transport.
-- `BatchSize` (default 64): a connection-level default that fills a request's 
`batchSize` when it is left
-unset.
-- `BulkResults` (default false): a connection-level default for `bulkResults` 
applied to every request unless
-overridden per-request. The `DriverRemoteConnection` traversal path defaults 
to `true` regardless of this setting.
-
-See: 
link:https://lists.apache.org/thread/yqtr2wnb1kq2pqqq4002cz511q5o0bkg[[DISCUSS] 
Standardizing GLV connection options in TinkerPop 4].
-
-==== Standardizing Python Connection Options
-
-TinkerPop 4.x standardizes connection option names and defaults across the 
GLVs. In `gremlin-python`, several
-connection options passed to `Client`/`DriverRemoteConnection` have been 
renamed for consistency. Because this is a
-major version, the old names have been removed rather than retained as 
aliases, and a number of new options have been
-added. The notes below describe the Python changes. See <<glv-driver-changes, 
GLV Driver Changes>> for the equivalent changes in
-the other drivers.
-
-Renames (breaking). The following options have been renamed and the old names 
removed. Migrate to the new names:
-
-- `pool_size` is now `max_connections` (default raised from 8 to 128). 
`max_connections` is also applied to the
-  aiohttp `TCPConnector` `limit`, so the transport layer reflects the option 
in addition to sizing the connection
-  pool.
-- `ssl_options` is now `ssl` (accepts an `ssl.SSLContext`).
-
-Behavior changes. These change runtime behavior on upgrade, even if you do not 
change your configuration:
-
-- `compression` is a new option that defaults to `'deflate'` (on), so the 
driver advertises `Accept-Encoding: deflate`
-  by default. Set `compression='none'` to disable it; when disabled, aiohttp's 
automatic `Accept-Encoding` injection is
-  suppressed so compression is not silently negotiated. Defaulting compression 
on is a deliberate deviation from the
-  proposal's agreed default-off, applied consistently across the GLVs by later 
agreement.
-- `connect_timeout` now defaults to 5s. Together with `read_timeout` it is 
wired into a single aiohttp `ClientTimeout`
-  via the socket-level `sock_connect`/`sock_read` knobs rather than a 
whole-request `total`, so long but legitimate
-  streaming responses are not aborted while a stalled server no longer hangs 
forever.
-- The minimum supported `aiohttp` has been raised to `3.11` (required for the 
`socket_factory` used by
-  `keep_alive_time`). *(breaking)*
-
-New options:
-
-- `connect_timeout` (default 5s): a new socket-connect timeout. It is combined 
with the existing `read_timeout`
-  (rewired into a single aiohttp `ClientTimeout` via 
`sock_connect`/`sock_read`, as described above).
-  Each timeout option's primary form is the millisecond-suffixed name 
(`connect_timeout_millis`, `idle_timeout_millis`,
-  `read_timeout_millis`, `keep_alive_time_millis`); the unsuffixed canonical 
name (`connect_timeout`, etc.) accepts the
-  idiomatic seconds value.
-- `idle_timeout` (default 180s): mapped to the aiohttp `TCPConnector` 
keep-alive timeout.
-- `keep_alive_time` (default 30s): enables TCP keep-alive probes via the 
connector socket factory
-  (`TCP_KEEPIDLE`/`TCP_KEEPALIVE`, guarded by platform availability).
-- `compression` (`'none'`/`'deflate'`, default `'deflate'`): the wire 
compression negotiated with the server.
-- `batch_size` (default 64): a connection-level default that fills the 
per-request `batchSize` when unset.
-- `proxy` and `trust_env`: explicit HTTP proxy and environment-trust options 
surfaced on the aiohttp `ClientSession`.
-- `auth.sigv4` gains a credentials-provider variant that accepts an optional 
credentials provider or callable, falling
-  back to the AWS environment variables.
-
-Removals (breaking):
-
-- The `max_content_length` kwarg has been removed (it was previously accepted 
but discarded).
-- The standalone `headers` kwarg has been removed from 
`Client`/`DriverRemoteConnection`; custom headers must now be
-  set via interceptors.
-
-See: 
link:https://lists.apache.org/thread/yqtr2wnb1kq2pqqq4002cz511q5o0bkg[[DISCUSS] 
Standardizing GLV connection options in TinkerPop 4].
-
-==== Standardizing Java Connection Options
-
-TinkerPop 4.x standardizes connection option names and defaults across the 
GLVs. In the Java reference driver
-(`gremlin-driver`), several `Cluster.Builder` options have been renamed for 
consistency. As this is a major version,
-the old names have been removed rather than retained as aliases, and a number 
of new options have been added. The notes
-below describe the Java changes. See <<glv-driver-changes, GLV Driver 
Changes>> for the equivalent changes in the
-other drivers.
-
-Renames (breaking). The following builder methods have been renamed and the 
old names removed; migrate to the new
-names:
-
-- `maxConnectionPoolSize` is now `maxConnections` (default 128).
-- `idleConnectionTimeoutMillis` is now `idleTimeout` (default 180000 ms), 
settable as `idleTimeout(Duration)` or `idleTimeoutMillis(long)` (YAML key 
`connectionPool.idleTimeoutMillis`).
-- `connectionSetupTimeoutMillis` is now `connectTimeout`, settable as 
`connectTimeout(Duration)` or `connectTimeoutMillis(int)` (YAML key 
`connectionPool.connectTimeoutMillis`).
-- `resultIterationBatchSize` is now `batchSize` (default 64); it sets the 
connection-level default that fills a request's per-request `batchSize` when 
unset (mirroring how `bulkResults` shares one name across connection and 
per-request scopes).
-- A new `ssl(SslContext)` builder accepts a fully configured Netty 
`SslContext`.
-- On `RequestOptions`, `addG` is now `traversalSource`.
-
-Behavior changes. These change runtime behavior on upgrade, even if you do not 
change your configuration:
-
-- `idleTimeout` now applies only to connections sitting idle in the pool 
between requests. Previously its underlying
-  Netty `IdleStateHandler` also watched the in-flight read window, so a 
stalled in-flight response could be closed
-  by `idleTimeout` even with `readTimeout` disabled. That in-flight coverage 
is now solely `readTimeout`'s job, so
-  if you relied on `idleTimeout` to bound a hung in-flight request, set 
`readTimeout` explicitly.
-- `connectTimeout` now defaults to 5s (lowered from 15s) and is now actually 
applied to the connection bootstrap.
-  Previously `connectionSetupTimeoutMillis` was validated but never enforced, 
so the connection setup was effectively
-  unbounded. A slow TCP or TLS setup now fails after 5s where it previously 
did not time out. If you rely on the old
-  behavior, set `connectTimeout` explicitly to a larger value.
-- `compression` now defaults to `Compression.DEFLATE` (on), so the driver 
sends `Accept-Encoding: deflate` by default.
-  This preserves the pre-4.x always-on Java behavior. Set 
`compression(Compression.NONE)` to disable it.
-- The `validationRequest` builder default has been reconciled to `g.inject(0)` 
(it was previously an empty string in
-  the builder, which did not match the config-file default).
-
-New options:
-
-- `readTimeout` (default 0, disabled): a streaming-safe idle-read timeout that 
fires only if no response chunk arrives
-  within the interval. It is armed per request and does not affect idle pooled 
connections. Settable as
-  `readTimeout(Duration)` or `readTimeoutMillis(long)` (YAML key 
`connectionPool.readTimeoutMillis`).
-- `keepAliveTime` (default 30000 ms): the idle time before TCP keep-alive 
probes begin. It enables `SO_KEEPALIVE` and
-  sets `TCP_KEEPIDLE` where supported (JDK 11+ on Linux and macOS). Set 0 to 
disable. Settable as
-  `keepAliveTime(Duration)` or `keepAliveTimeMillis(long)` (YAML key 
`connectionPool.keepAliveTimeMillis`).
-- `maxResponseHeaderBytes` (default 8192): the maximum response header size, 
which was previously hardcoded.
-- `proxy(ProxyOptions)`: routes connections through an HTTP proxy.
-- `url(String)`: configures the endpoint from a single URL (scheme, host, 
port, and path). The existing
-  `addContactPoint`/`addContactPoints`, `port`, and `path` options remain for 
multi-host configurations.
-
-The `maxResponseContentLength` setting has been removed. Responses are now 
streamed rather than aggregated
-and size-capped. The new idle `readTimeout` is the partial mitigation for 
unbounded or slow responses.
-
-Note that none of the driver timeouts bound the *total* time of a request: 
`readTimeout` only bounds the gap between
-response chunks, so a response that keeps producing chunks will not time out 
no matter how long it runs overall. If
-you need an overall deadline, impose it in your application around the 
asynchronous API, for example by wrapping a
-`CompletableFuture` with a timeout:
-
-[source,java]
-----
-// bound the entire request (submit plus full result iteration) to 30 seconds
-final List<Result> results = client.submitAsync("g.V().out().out()")
-        .thenCompose(ResultSet::all)
-        .orTimeout(30, TimeUnit.SECONDS)
-        .get();
-----
-
-See: 
link:https://lists.apache.org/thread/yqtr2wnb1kq2pqqq4002cz511q5o0bkg[[DISCUSS] 
Standardizing GLV connection options in TinkerPop 4
-].
-
 ==== Declarative Pattern Matching
 
 Gremlin has always offered both imperative and declarative styles to writing 
graph queries. While the imperative style
diff --git a/gremlin-examples/gremlin-javascript/connections.js 
b/gremlin-examples/gremlin-javascript/connections.js
index 23648fb675..fa33ad3073 100644
--- a/gremlin-examples/gremlin-javascript/connections.js
+++ b/gremlin-examples/gremlin-javascript/connections.js
@@ -20,7 +20,6 @@ under the License.
 const gremlin = require('gremlin');
 const traversal = gremlin.process.AnonymousTraversalSource.traversal;
 const DriverRemoteConnection = gremlin.driver.DriverRemoteConnection;
-const serializer = gremlin.structure.io.graphserializer;
 
 const serverUrl = 'ws://localhost:8182/gremlin';
 const vertexLabel = 'connection';
@@ -47,11 +46,13 @@ async function withRemote() {
 async function withConfigs() {
     // Connecting and customizing configurations
     const dc = new DriverRemoteConnection(serverUrl, {
-        mimeType: 'application/vnd.gremlin-v3.0+json',
-        reader: serializer,
-        writer: serializer,
-        rejectUnauthorized: false,
         traversalSource: 'g',
+        // Optionally tune the underlying connection pool and timeouts.
+        maxConnections: 64,
+        readTimeout: 30000,
+        // TLS is configured through the Node/undici runtime, not via driver
+        // options: e.g. set NODE_EXTRA_CA_CERTS to trust a custom CA, or
+        // NODE_TLS_REJECT_UNAUTHORIZED=0 to relax certificate verification.
     });
     const g = traversal().withRemote(dc);
 
diff --git a/gremlin-js/gremlin-javascript/lib/driver/client.ts 
b/gremlin-js/gremlin-javascript/lib/driver/client.ts
index fbb1ac4671..b13493b208 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/client.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/client.ts
@@ -126,8 +126,17 @@ export default class Client {
     if (requestOptions?.evaluationTimeout) {
       requestBuilder.addTimeoutMillis(requestOptions.evaluationTimeout);
     }
-    if (requestOptions?.bulkResults) {
+    // Per-request value wins (including an explicit `false`); otherwise apply 
the
+    // connection-level default only when it is true.
+    if (requestOptions?.bulkResults !== undefined) {
       requestBuilder.addBulkResults(requestOptions.bulkResults);
+    } else if (this._connection.bulkResults) {
+      requestBuilder.addBulkResults(true);
+    }
+    // Fill the per-request batchSize from the connection-level default when 
unset.
+    const batchSize = requestOptions?.batchSize ?? this._connection.batchSize;
+    if (batchSize !== undefined) {
+      requestBuilder.addBatchSize(batchSize);
     }
     if (requestOptions?.transactionId) {
       requestBuilder.addField('transactionId', requestOptions.transactionId);
diff --git a/gremlin-js/gremlin-javascript/lib/driver/connection.ts 
b/gremlin-js/gremlin-javascript/lib/driver/connection.ts
index 1be591ca9a..4a673d8b89 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/connection.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/connection.ts
@@ -23,7 +23,7 @@
 
 import { Buffer } from 'buffer';
 import { EventEmitter } from 'eventemitter3';
-import type { Agent } from 'node:http';
+import type { Dispatcher } from 'undici';
 import ioc, { createPreciseReader } from 
'../structure/io/binary/GraphBinary.js';
 import GraphBinaryReader from 
'../structure/io/binary/internals/GraphBinaryReader.js';
 import StreamReader from '../structure/io/binary/internals/StreamReader.js';
@@ -33,6 +33,8 @@ import {RequestMessage} from "./request-message.js";
 import { HttpRequest, RequestInterceptor } from './http-request.js';
 import ResponseError from './response-error.js';
 import { Traverser } from '../process/traversal.js';
+import { buildDispatcher, httpFetch } from './dispatcher.js';
+import { Logger, LoggerCallback, normalizeLogger } from './logger.js';
 
 const responseStatusCode = {
   success: 200,
@@ -40,24 +42,49 @@ const responseStatusCode = {
   partialContent: 206,
 };
 
+/**
+ * Selects the content encoding requested from, and decoded for, the server. 
`'deflate'` (the
+ * default) sends an `Accept-Encoding: deflate` header and decodes deflate 
responses; `'none'`
+ * turns compression off.
+ */
+export type Compression = 'none' | 'deflate';
+
 export type ConnectionOptions = {
-  ca?: string[];
-  cert?: string | string[] | Buffer;
-  pfx?: string | Buffer;
   preciseNumbers?: boolean;
   pdtRegistry?: any;
   reader?: any;
-  rejectUnauthorized?: boolean;
   traversalSource?: string;
-  headers?: Record<string, string | string[]>;
   enableUserAgentOnConnect?: boolean;
-  agent?: Agent;
+  /** Maximum number of concurrent connections per origin. Defaults to 128. */
+  maxConnections?: number;
+  /** Idle-read (body) timeout in milliseconds, applied to the default 
dispatcher. */
+  readTimeoutMillis?: number;
+  /** Maximum size of the response headers in bytes, applied to the default 
dispatcher. */
+  maxResponseHeaderBytes?: number;
+  /**
+   * Idle time in milliseconds before TCP keep-alive probes begin on a 
connection. Defaults to
+   * 30000 (30s) when unset. Set to `0` to disable keep-alive entirely.
+   */
+  keepAliveTimeMillis?: number;
+  /** HTTP proxy URI. When set, requests are routed through an undici 
`ProxyAgent`. */
+  proxy?: string;
+  /** Response compression codec. Defaults to `'deflate'` (on). */
+  compression?: Compression;
+  /** Connection-level default that fills a request's `batchSize` when it is 
left unset. Defaults to 64. */
+  batchSize?: number;
+  /** Connection-level default for `bulkResults`, applied to every request 
unless overridden per-request. Defaults to `false`. */
+  bulkResults?: boolean;
+  /** An optional logger (a logger object or a callback). Logging is disabled 
when unset. */
+  logger?: Logger;
   interceptors?: RequestInterceptor | RequestInterceptor[];
   /** An optional auth interceptor. As a convenience, this is always appended 
to the end of the
    *  interceptor list so it runs last, after any user interceptors have 
modified the request. */
   auth?: RequestInterceptor;
 };
 
+/** The default per-request batch size used when neither the request nor the 
connection sets one. */
+export const DEFAULT_BATCH_SIZE = 64;
+
 /**
  * Represents a single connection to a Gremlin Server.
  */
@@ -69,6 +96,11 @@ export default class Connection extends EventEmitter {
 
   private readonly _enableUserAgentOnConnect: boolean;
   private readonly _interceptors: RequestInterceptor[];
+  private readonly _dispatcher: Dispatcher | undefined;
+  private readonly _compression: Compression;
+  private readonly _batchSize: number;
+  private readonly _bulkResults: boolean;
+  private readonly _log: LoggerCallback;
 
   /**
    * Creates a new instance of {@link Connection}.
@@ -87,6 +119,29 @@ export default class Connection extends EventEmitter {
     }
     this.traversalSource = options.traversalSource || 'g';
     this._enableUserAgentOnConnect = options.enableUserAgentOnConnect !== 
false;
+    this._log = normalizeLogger(options.logger);
+
+    if (options.compression === undefined) {
+      this._compression = 'deflate';
+    } else if (options.compression === 'none' || options.compression === 
'deflate') {
+      this._compression = options.compression;
+    } else {
+      throw new TypeError(`compression must be 'none' or 'deflate'`);
+    }
+
+    this._batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
+    this._bulkResults = options.bulkResults ?? false;
+
+    // The driver builds and owns the undici dispatcher from the discrete 
options.
+    // TLS is configured through the Node/undici runtime (e.g. 
NODE_EXTRA_CA_CERTS),
+    // not through a driver option.
+    this._dispatcher = buildDispatcher({
+      maxConnections: options.maxConnections,
+      readTimeoutMillis: options.readTimeoutMillis,
+      maxResponseHeaderBytes: options.maxResponseHeaderBytes,
+      keepAliveTimeMillis: options.keepAliveTimeMillis,
+      proxy: options.proxy,
+    });
 
     const interceptors = options.interceptors;
     if (typeof interceptors === 'function') {
@@ -103,6 +158,23 @@ export default class Connection extends EventEmitter {
     if (options.auth) {
       this._interceptors.push(options.auth);
     }
+
+    this._log('debug', `Connection created for ${this.url}`);
+  }
+
+  /**
+   * The connection-level default batch size, used to fill a request's 
`batchSize` when unset.
+   */
+  get batchSize(): number {
+    return this._batchSize;
+  }
+
+  /**
+   * The connection-level default for `bulkResults`, applied to every request 
unless overridden
+   * per-request. Defaults to `false`.
+   */
+  get bulkResults(): boolean {
+    return this._bulkResults;
   }
 
   /**
@@ -178,7 +250,7 @@ export default class Connection extends EventEmitter {
     }
 
     if (!response.body) {
-      // 204 No Content — nothing to yield
+      // 204 No Content - nothing to yield
       return;
     }
 
@@ -190,7 +262,7 @@ export default class Connection extends EventEmitter {
       completed = true;
     } finally {
       if (!completed) {
-        // Consumer broke out early or an error occurred — abort to release 
the connection
+        // Consumer broke out early or an error occurred - abort to release 
the connection
         abortController.abort();
       }
     }
@@ -213,6 +285,10 @@ export default class Connection extends EventEmitter {
       'Accept': this._reader.mimeType,
     };
 
+    if (this._compression === 'deflate') {
+      headers['Accept-Encoding'] = 'deflate';
+    }
+
     if (this._enableUserAgentOnConnect) {
       const userAgent = await utils.getUserAgent();
       if (userAgent !== undefined) {
@@ -220,12 +296,6 @@ export default class Connection extends EventEmitter {
       }
     }
 
-    if (this.options.headers) {
-      Object.entries(this.options.headers).forEach(([key, value]) => {
-        headers[key] = Array.isArray(value) ? value.join(', ') : value;
-      });
-    }
-
     const httpRequest = new HttpRequest('POST', this.url, headers, request);
 
     // Promote transactionId to HTTP header before interceptors run.
@@ -249,12 +319,17 @@ export default class Connection extends EventEmitter {
     // Auto-serialize body to JSON after interceptors run (idempotent if 
already serialized)
     httpRequest.serializeBody();
 
-    return fetch(httpRequest.url, {
+    this._log('debug', `Sending ${httpRequest.method} request to 
${httpRequest.url}`);
+
+    return httpFetch.fetch(httpRequest.url, {
       method: httpRequest.method,
       headers: httpRequest.headers,
       body: httpRequest.body,
       signal,
-    });
+      // Node only: the undici dispatcher carries the connection-pool options. 
In the browser
+      // it is undefined and the field is omitted, letting the user agent 
manage the transport.
+      ...(this._dispatcher ? { dispatcher: this._dispatcher } : {}),
+    } as RequestInit);
   }
 
   async #handleResponse(response: Response) {
@@ -321,7 +396,8 @@ export default class Connection extends EventEmitter {
    * Closes the Connection.
    * @return {Promise}
    */
-  close() {
-    return Promise.resolve();
+  async close() {
+    this._log('debug', `Closing connection to ${this.url}`);
+    await this._dispatcher?.close();
   }
 }
diff --git a/gremlin-js/gremlin-javascript/lib/driver/dispatcher.browser.ts 
b/gremlin-js/gremlin-javascript/lib/driver/dispatcher.browser.ts
new file mode 100644
index 0000000000..8f80cbb014
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/driver/dispatcher.browser.ts
@@ -0,0 +1,79 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+/**
+ * Browser counterpart of {@link ./dispatcher.ts}. The Node build configures 
the underlying HTTP
+ * transport through an `undici` dispatcher, but `undici` is built on 
`node:net`/`node:tls` and
+ * cannot run in a browser. Bundlers pick this file up via the `"browser"` 
field in `package.json`,
+ * so a browser bundle never imports `undici`.
+ *
+ * The browser's HTTP stack manages the connection-pool options that the Node 
dispatcher carries,
+ * so {@link buildDispatcher} throws if any is set explicitly (rather than 
silently ignoring it)
+ * and otherwise returns `undefined`, leaving the connection to issue a plain 
`fetch`. It is the
+ * only export the connection imports through the swapped `./dispatcher.js` 
path.
+ */
+
+/** Transport options accepted by {@link buildDispatcher}; mirrors the Node 
dispatcher's options. */
+type DispatcherOptions = {
+  maxConnections?: number;
+  readTimeoutMillis?: number;
+  maxResponseHeaderBytes?: number;
+  keepAliveTimeMillis?: number;
+  proxy?: string;
+};
+
+/**
+ * Connection-pool / transport options that only the Node (`undici`) 
dispatcher can honor. The
+ * browser's HTTP stack owns these concerns and exposes no `fetch` API to 
configure them, so they
+ * are rejected here rather than silently ignored. `readTimeoutMillis` is 
intentionally not in this list:
+ * it has a browser analog (an `AbortSignal`-based timeout) and is handled by 
the connection.
+ */
+const NODE_ONLY_OPTIONS: (keyof DispatcherOptions)[] = [
+  'maxConnections',
+  'keepAliveTimeMillis',
+  'maxResponseHeaderBytes',
+  'proxy',
+];
+
+/**
+ * In the browser there is no `undici` dispatcher, so `fetch` is issued 
without one and the browser
+ * manages the transport. Any {@link NODE_ONLY_OPTIONS} set explicitly cannot 
be honored and throws,
+ * so a misconfiguration surfaces immediately instead of being silently 
dropped. With none set this
+ * returns `undefined` and the connection omits the `dispatcher` field.
+ */
+export function buildDispatcher(options: DispatcherOptions = {}): undefined {
+  const unsupported = NODE_ONLY_OPTIONS.filter((key) => options[key] !== 
undefined);
+  if (unsupported.length > 0) {
+    throw new Error(
+      `The following connection options are not supported in the browser and 
are managed by the ` +
+      `browser's HTTP stack: ${unsupported.join(', ')}. Remove them when 
running in a browser ` +
+      `(they are supported in Node).`,
+    );
+  }
+  return undefined;
+}
+
+/**
+ * Browser counterpart of the Node `httpFetch` holder. There is no `undici` in 
the browser, so
+ * requests use the global `fetch` and the user agent manages the transport. 
Same holder shape as
+ * the Node build.
+ */
+export const httpFetch: { fetch: typeof globalThis.fetch } = {
+  fetch: globalThis.fetch.bind(globalThis),
+};
diff --git a/gremlin-js/gremlin-javascript/lib/driver/dispatcher.ts 
b/gremlin-js/gremlin-javascript/lib/driver/dispatcher.ts
new file mode 100644
index 0000000000..7e323aff77
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/driver/dispatcher.ts
@@ -0,0 +1,124 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import { Agent, ProxyAgent, buildConnector, Dispatcher, fetch as undiciFetch } 
from 'undici';
+
+/** Default concurrent-connections-per-origin cap (Node's global fetch is 
uncapped). */
+export const DEFAULT_MAX_CONNECTIONS = 128;
+
+/** Canonical TinkerPop 4.x default TCP keep-alive idle time (30s), shared 
across the GLVs. */
+export const DEFAULT_KEEP_ALIVE_TIME = 30000;
+
+/** Options for the default undici {@link Dispatcher}; unset fields use undici 
defaults. */
+export type DispatcherOptions = {
+  /** Max concurrent connections per origin. Defaults to {@link 
DEFAULT_MAX_CONNECTIONS}. */
+  maxConnections?: number;
+  /** Idle-read (body) timeout in ms. Maps to undici `bodyTimeout`. */
+  readTimeoutMillis?: number;
+  /** Max response header size in bytes. Maps to undici `maxHeaderSize`. */
+  maxResponseHeaderBytes?: number;
+  /** Idle ms before TCP keep-alive probes begin. Defaults to 30s; `0` 
disables keep-alive. */
+  keepAliveTimeMillis?: number;
+  /** HTTP proxy URI. When set, a {@link ProxyAgent} is built instead of an 
{@link Agent}. */
+  proxy?: string;
+};
+
+/**
+ * Resolves the effective keep-alive idle delay: unset falls back to {@link 
DEFAULT_KEEP_ALIVE_TIME},
+ * a non-positive value disables keep-alive (returns `null`).
+ */
+export function resolveKeepAliveTime(keepAliveTime?: number): number | null {
+  const effective = keepAliveTime ?? DEFAULT_KEEP_ALIVE_TIME;
+  return effective > 0 ? effective : null;
+}
+
+/**
+ * Builds an undici connector that sets `SO_KEEPALIVE` with the given idle 
delay on each new socket.
+ * `baseConnector` is parameterised so the wiring can be unit-tested with a 
fake connector.
+ */
+export function buildKeepAliveConnector(
+  delay: number,
+  baseConnector: ReturnType<typeof buildConnector> = buildConnector({}),
+): ReturnType<typeof buildConnector> {
+  return (connectOptions: buildConnector.Options, callback: 
buildConnector.Callback) => {
+    baseConnector(connectOptions, (err, socket) => {
+      if (err) {
+        callback(err, null);
+        return;
+      }
+      if (socket) {
+        socket.setKeepAlive(true, delay);
+      }
+      callback(null, socket);
+    });
+  };
+}
+
+/**
+ * Maps the discrete driver options onto undici {@link Agent.Options} (the 
single source of truth
+ * for that mapping). Exported as a seam so the mapping can be asserted in 
unit tests.
+ */
+export function buildAgentOptions(options: DispatcherOptions = {}): 
Agent.Options {
+  const maxConnections = options.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
+
+  const agentOptions: Agent.Options = {
+    connections: maxConnections,
+  };
+
+  // Connect/idle timeouts are intentionally left to undici defaults (the GLV 
spec marks the JS
+  // connect/idle timeout as N/A), not exposed as driver options.
+  if (options.readTimeoutMillis !== undefined) {
+    agentOptions.bodyTimeout = options.readTimeoutMillis;
+  }
+  if (options.maxResponseHeaderBytes !== undefined) {
+    agentOptions.maxHeaderSize = options.maxResponseHeaderBytes;
+  }
+
+  const keepAliveDelay = resolveKeepAliveTime(options.keepAliveTimeMillis);
+  if (keepAliveDelay !== null) {
+    agentOptions.connect = buildKeepAliveConnector(keepAliveDelay);
+  }
+
+  return agentOptions;
+}
+
+/**
+ * Builds the default undici {@link Dispatcher}: a {@link ProxyAgent} when 
`proxy` is set, otherwise
+ * a plain {@link Agent}. (Agent and ProxyAgent share one dispatcher slot, so 
only one is built.)
+ */
+export function buildDispatcher(options: DispatcherOptions = {}): Dispatcher {
+  const agentOptions = buildAgentOptions(options);
+
+  if (options.proxy) {
+    return new ProxyAgent({ uri: options.proxy, ...agentOptions });
+  }
+
+  return new Agent(agentOptions);
+}
+
+/**
+ * HTTP `fetch` for the Node build. Uses undici's own `fetch` (not the global 
one) so `fetch` and the
+ * {@link buildDispatcher} dispatcher share one undici version, whatever 
undici the Node runtime
+ * bundles (e.g. Node 26 ships undici 8, incompatible with an undici 6 
dispatcher). A holder object,
+ * not a bare export, so tests can swap `fetch` (an ESM export can't be 
reassigned, an object
+ * property can). The cast aligns undici's `fetch` type with the global one.
+ */
+export const httpFetch: { fetch: typeof globalThis.fetch } = {
+  fetch: undiciFetch as unknown as typeof globalThis.fetch,
+};
diff --git a/gremlin-js/gremlin-javascript/lib/driver/logger.ts 
b/gremlin-js/gremlin-javascript/lib/driver/logger.ts
new file mode 100644
index 0000000000..7de8558ea4
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/lib/driver/logger.ts
@@ -0,0 +1,71 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+/** The set of log levels emitted by the driver, ordered from least to most 
severe. */
+export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
+
+/**
+ * A logger object with one method per {@link LogLevel}. This shape is 
compatible with the
+ * global `console` as well as common logging libraries.
+ */
+export interface LoggerObject {
+  debug(message: string, ...args: any[]): void;
+  info(message: string, ...args: any[]): void;
+  warn(message: string, ...args: any[]): void;
+  error(message: string, ...args: any[]): void;
+}
+
+/**
+ * A logger callback that receives the level alongside the message and any 
extra arguments.
+ */
+export type LoggerCallback = (level: LogLevel, message: string, ...args: 
any[]) => void;
+
+/**
+ * The `logger` connection option. It may be either a {@link LoggerObject} 
(e.g. the global
+ * `console`) or a {@link LoggerCallback}. Logging is disabled when no logger 
is provided.
+ */
+export type Logger = LoggerObject | LoggerCallback;
+
+/**
+ * Normalizes the user-supplied {@link Logger} option into a single callback. 
When no logger is
+ * provided, a no-op callback is returned so call sites do not need to 
null-check.
+ *
+ * @param {Logger} [logger] The user-supplied logger option.
+ * @returns {LoggerCallback} A callback that dispatches to the configured 
logger.
+ */
+export function normalizeLogger(logger?: Logger): LoggerCallback {
+  if (logger === undefined || logger === null) {
+    return () => {};
+  }
+
+  if (typeof logger === 'function') {
+    return logger;
+  }
+
+  if (typeof logger === 'object') {
+    return (level: LogLevel, message: string, ...args: any[]) => {
+      const fn = (logger as LoggerObject)[level];
+      if (typeof fn === 'function') {
+        fn.call(logger, message, ...args);
+      }
+    };
+  }
+
+  throw new TypeError('logger must be a function, a logger object, or 
undefined');
+}
diff --git a/gremlin-js/gremlin-javascript/lib/driver/request-message.ts 
b/gremlin-js/gremlin-javascript/lib/driver/request-message.ts
index c878309935..29dc55219c 100644
--- a/gremlin-js/gremlin-javascript/lib/driver/request-message.ts
+++ b/gremlin-js/gremlin-javascript/lib/driver/request-message.ts
@@ -27,6 +27,7 @@ const Tokens = {
   ARGS_MATERIALIZE_PROPERTIES: 'materializeProperties',
   TIMEOUT_MS: 'timeoutMs',
   BULK_RESULTS: 'bulkResults',
+  BATCH_SIZE: 'batchSize',
   MATERIALIZE_PROPERTIES_TOKENS: 'tokens',
   MATERIALIZE_PROPERTIES_ALL: 'all'
 };
@@ -42,6 +43,7 @@ export class RequestMessage {
   private g?: string;
   private materializeProperties?: string;
   private bulkResults?: boolean;
+  private batchSize?: number;
   private customFields: Map<string, any>;
 
   private constructor(
@@ -52,6 +54,7 @@ export class RequestMessage {
     g: string | undefined,
     materializeProperties: string | undefined,
     bulkResults: boolean | undefined,
+    batchSize: number | undefined,
     customFields: Map<string, any>
   ) {
     if (!gremlin) {
@@ -65,6 +68,7 @@ export class RequestMessage {
     this.g = g;
     this.materializeProperties = materializeProperties;
     this.bulkResults = bulkResults;
+    this.batchSize = batchSize;
     this.customFields = customFields;
   }
 
@@ -96,6 +100,10 @@ export class RequestMessage {
     return this.bulkResults;
   }
 
+  getBatchSize(): number | undefined {
+    return this.batchSize;
+  }
+
   getFields(): ReadonlyMap<string, any> {
     return this.customFields;
   }
@@ -129,6 +137,9 @@ export class RequestMessage {
     if (this.bulkResults !== undefined) {
       payload['bulkResults'] = this.bulkResults;
     }
+    if (this.batchSize !== undefined) {
+      payload['batchSize'] = this.batchSize;
+    }
 
     // Flatten custom/provider fields to the top level
     this.customFields.forEach((v, k) => {
@@ -155,6 +166,7 @@ export class Builder {
   public g?: string;
   public materializeProperties?: string;
   public bulkResults?: boolean;
+  public batchSize?: number;
   public additionalFields = new Map<string, any>();
 
   constructor(gremlin: string) {
@@ -230,6 +242,12 @@ export class Builder {
     return this;
   }
 
+  addBatchSize(batchSize: number): Builder {
+    if (batchSize <= 0) throw new Error('batchSize argument must be 
positive.');
+    this.batchSize = batchSize;
+    return this;
+  }
+
   addField(key: string, value: any): Builder {
     this.additionalFields.set(key, value);
     return this;
@@ -256,6 +274,7 @@ export class Builder {
       this.g,
       this.materializeProperties,
       this.bulkResults,
+      this.batchSize,
       this.additionalFields
     );
   }
diff --git a/gremlin-js/gremlin-javascript/package.json 
b/gremlin-js/gremlin-javascript/package.json
index 0d704e0cf2..fc86b56ad2 100644
--- a/gremlin-js/gremlin-javascript/package.json
+++ b/gremlin-js/gremlin-javascript/package.json
@@ -20,6 +20,10 @@
   "type": "module",
   "main": "./build/cjs/index.cjs",
   "types": "./build/esm/index.d.ts",
+  "browser": {
+    "./build/esm/driver/dispatcher.js": 
"./build/esm/driver/dispatcher.browser.js",
+    "./build/cjs/driver/dispatcher.cjs": 
"./build/cjs/driver/dispatcher.browser.cjs"
+  },
   "exports": {
     ".": {
       "import": "./build/esm/index.js",
@@ -44,7 +48,8 @@
   "dependencies": {
     "antlr4ng": "3.0.16",
     "buffer": "^6.0.3",
-    "eventemitter3": "^5.0.1"
+    "eventemitter3": "^5.0.1",
+    "undici": "6.27.0"
   },
   "optionalDependencies": {
     "@aws-sdk/credential-providers": "^3.967.0",
diff --git a/gremlin-js/gremlin-javascript/test/helper.js 
b/gremlin-js/gremlin-javascript/test/helper.js
index fa7f0d65b2..fab0452578 100644
--- a/gremlin-js/gremlin-javascript/test/helper.js
+++ b/gremlin-js/gremlin-javascript/test/helper.js
@@ -52,7 +52,6 @@ export function getClient(traversalSource) {
 export function getAuthenticatedClient(traversalSource, interceptors) {
   return new Client(serverAuthUrl, {
     traversalSource,
-    rejectUnauthorized: false,
     interceptors,
   });
 }
@@ -60,7 +59,6 @@ export function getAuthenticatedClient(traversalSource, 
interceptors) {
 export function getAuthenticatedConnection(traversalSource, interceptors) {
   return new DriverRemoteConnection(serverAuthUrl, {
     traversalSource,
-    rejectUnauthorized: false,
     interceptors,
   });
 }
diff --git a/gremlin-js/gremlin-javascript/test/unit/client-test.js 
b/gremlin-js/gremlin-javascript/test/unit/client-test.js
index 38342a3a9f..df39f1bceb 100644
--- a/gremlin-js/gremlin-javascript/test/unit/client-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/client-test.js
@@ -51,4 +51,100 @@ describe('Client', function () {
     customClient._connection = connectionMock;
     customClient.submit(query)
   });
+
+  it('should fill batchSize from the connection default when unset', function 
() {
+    const connectionMock = {
+      batchSize: 64,
+      submit: function (requestMessage) {
+        assert.strictEqual(requestMessage.getBatchSize(), 64);
+        return Promise.resolve();
+      }
+    };
+
+    const customClient = new Client('http://localhost:9321', 
{connectOnStartup: false});
+    customClient._connection = connectionMock;
+    customClient.submit(query);
+  });
+
+  it('should let a per-request batchSize override the connection default', 
function () {
+    const connectionMock = {
+      batchSize: 64,
+      submit: function (requestMessage) {
+        assert.strictEqual(requestMessage.getBatchSize(), 10);
+        return Promise.resolve();
+      }
+    };
+
+    const customClient = new Client('http://localhost:9321', 
{connectOnStartup: false});
+    customClient._connection = connectionMock;
+    customClient.submit(query, null, { batchSize: 10 });
+  });
+
+  it('should forward an explicit bulkResults:false', function () {
+    const connectionMock = {
+      submit: function (requestMessage) {
+        assert.strictEqual(requestMessage.getBulkResults(), false);
+        return Promise.resolve();
+      }
+    };
+
+    const customClient = new Client('http://localhost:9321', 
{connectOnStartup: false});
+    customClient._connection = connectionMock;
+    customClient.submit(query, null, { bulkResults: false });
+  });
+
+  it('should forward an explicit bulkResults:true', function () {
+    const connectionMock = {
+      submit: function (requestMessage) {
+        assert.strictEqual(requestMessage.getBulkResults(), true);
+        return Promise.resolve();
+      }
+    };
+
+    const customClient = new Client('http://localhost:9321', 
{connectOnStartup: false});
+    customClient._connection = connectionMock;
+    customClient.submit(query, null, { bulkResults: true });
+  });
+
+  it('should fill bulkResults from the connection-level option when unset 
per-request', function () {
+    const connectionMock = {
+      bulkResults: true,
+      submit: function (requestMessage) {
+        assert.strictEqual(requestMessage.getBulkResults(), true);
+        return Promise.resolve();
+      }
+    };
+
+    const customClient = new Client('http://localhost:9321', 
{connectOnStartup: false});
+    customClient._connection = connectionMock;
+    customClient.submit(query);
+  });
+
+  it('should let a per-request bulkResults:false override the connection-level 
true', function () {
+    const connectionMock = {
+      bulkResults: true,
+      submit: function (requestMessage) {
+        assert.strictEqual(requestMessage.getBulkResults(), false);
+        return Promise.resolve();
+      }
+    };
+
+    const customClient = new Client('http://localhost:9321', 
{connectOnStartup: false});
+    customClient._connection = connectionMock;
+    customClient.submit(query, null, { bulkResults: false });
+  });
+
+  it('should not set bulkResults when neither per-request nor connection-level 
is set', function () {
+    const connectionMock = {
+      bulkResults: undefined,
+      submit: function (requestMessage) {
+        assert.strictEqual(requestMessage.getBulkResults(), undefined);
+        return Promise.resolve();
+      }
+    };
+
+    const customClient = new Client('http://localhost:9321', 
{connectOnStartup: false});
+    customClient._connection = connectionMock;
+    customClient.submit(query);
+  });
 });
diff --git a/gremlin-js/gremlin-javascript/test/unit/connection-test.js 
b/gremlin-js/gremlin-javascript/test/unit/connection-test.js
index 2ef6cc477c..ffc2ea739e 100644
--- a/gremlin-js/gremlin-javascript/test/unit/connection-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/connection-test.js
@@ -20,19 +20,20 @@
 import assert from 'assert';
 import Connection from '../../lib/driver/connection.js';
 import { RequestMessage } from '../../lib/driver/request-message.js';
+import { httpFetch } from '../../lib/driver/dispatcher.js';
 
 // Connection-level unit tests that verify interceptor wiring and 
auto-serialization
-// by mocking the global fetch and capturing what the Connection sends.
+// by mocking the driver's fetch (httpFetch.fetch) and capturing what the 
Connection sends.
 describe('Connection (request pipeline)', function () {
   let originalFetch;
   let captured;
 
   beforeEach(function () {
     captured = null;
-    originalFetch = global.fetch;
+    originalFetch = httpFetch.fetch;
     // Return an error response so we don't need a valid GraphBinary body; the 
test only
     // cares about what was sent to fetch. The resulting ResponseError is 
swallowed per-test.
-    global.fetch = (url, init) => {
+    httpFetch.fetch = (url, init) => {
       captured = { url, init };
       return Promise.resolve({
         ok: false,
@@ -45,7 +46,7 @@ describe('Connection (request pipeline)', function () {
   });
 
   afterEach(function () {
-    global.fetch = originalFetch;
+    httpFetch.fetch = originalFetch;
   });
 
   function makeConnection(options = {}) {
@@ -147,4 +148,62 @@ describe('Connection (request pipeline)', function () {
 
     assert.deepStrictEqual(order, [1, 2, 3], 'auth interceptor should always 
run last');
   });
+
+  it('sends Accept-Encoding: deflate by default (compression on)', async 
function () {
+    const conn = makeConnection();
+    await submitAndIgnoreError(conn, 
RequestMessage.build('g.V()').addG('g').create());
+
+    assert.strictEqual(captured.init.headers['Accept-Encoding'], 'deflate');
+  });
+
+  it('does not send an Accept-Encoding header when compression is explicitly 
off', async function () {
+    const conn = makeConnection({ compression: 'none' });
+    await submitAndIgnoreError(conn, 
RequestMessage.build('g.V()').addG('g').create());
+
+    assert.strictEqual(captured.init.headers['Accept-Encoding'], undefined);
+  });
+
+  it('throws for an invalid compression value', function () {
+    assert.throws(
+      () => makeConnection({ compression: 'gzip' }),
+      /compression must be/);
+  });
+
+  it('passes the default dispatcher to fetch', async function () {
+    const conn = makeConnection();
+    await submitAndIgnoreError(conn, 
RequestMessage.build('g.V()').addG('g').create());
+
+    assert.ok(captured.init.dispatcher, 'fetch should receive a dispatcher');
+  });
+
+  it('closes its built dispatcher on close()', async function () {
+    const conn = makeConnection();
+    await submitAndIgnoreError(conn, 
RequestMessage.build('g.V()').addG('g').create());
+
+    const built = captured.init.dispatcher;
+    assert.ok(built, 'a dispatcher should have been built and passed to 
fetch');
+
+    await conn.close();
+    // A closed undici Agent reports itself as destroyed; closing it again is 
a no-op.
+    assert.strictEqual(built.closed === true || built.destroyed === true, true,
+      'the connection-built dispatcher should be closed on close()');
+  });
+
+  it('exposes the default batch size of 64', function () {
+    const conn = makeConnection();
+    assert.strictEqual(conn.batchSize, 64);
+  });
+
+  it('honors a custom batchSize', function () {
+    const conn = makeConnection({ batchSize: 250 });
+    assert.strictEqual(conn.batchSize, 250);
+  });
+
+  it('invokes a logger callback during the request lifecycle', async function 
() {
+    const lines = [];
+    const conn = makeConnection({ logger: (level, message) => 
lines.push([level, message]) });
+    await submitAndIgnoreError(conn, 
RequestMessage.build('g.V()').addG('g').create());
+
+    assert.ok(lines.some(([, m]) => /Sending POST request/.test(m)), 'should 
log the outgoing request');
+  });
 });
diff --git a/gremlin-js/gremlin-javascript/test/unit/dispatcher-browser-test.js 
b/gremlin-js/gremlin-javascript/test/unit/dispatcher-browser-test.js
new file mode 100644
index 0000000000..5cf15c958a
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/unit/dispatcher-browser-test.js
@@ -0,0 +1,59 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import assert from 'assert';
+import { buildDispatcher } from '../../lib/driver/dispatcher.browser.js';
+
+describe('dispatcher (browser)', function () {
+  it('returns undefined when no transport options are set', function () {
+    assert.strictEqual(buildDispatcher(), undefined);
+    assert.strictEqual(buildDispatcher({}), undefined);
+  });
+
+  it('returns undefined when only readTimeoutMillis is set (handled by the 
connection, not the dispatcher)', function () {
+    assert.strictEqual(buildDispatcher({ readTimeoutMillis: 5000 }), 
undefined);
+  });
+
+  for (const [key, value] of [
+    ['maxConnections', 10],
+    ['keepAliveTimeMillis', 15000],
+    ['maxResponseHeaderBytes', 8192],
+    ['proxy', 'http://proxy.local:8080'],
+  ]) {
+    it(`throws when ${key} is set explicitly`, function () {
+      assert.throws(() => buildDispatcher({ [key]: value }), (err) => {
+        assert.ok(err instanceof Error);
+        assert.ok(err.message.includes(key), `message should name ${key}`);
+        return true;
+      });
+    });
+  }
+
+  it('lists every unsupported option that was set', function () {
+    assert.throws(
+      () => buildDispatcher({ maxConnections: 10, proxy: 'http://p:1', 
readTimeoutMillis: 1000 }),
+      (err) => {
+        assert.ok(err.message.includes('maxConnections'));
+        assert.ok(err.message.includes('proxy'));
+        assert.ok(!err.message.includes('readTimeoutMillis'), 
'readTimeoutMillis is supported and must not be listed');
+        return true;
+      },
+    );
+  });
+});
diff --git a/gremlin-js/gremlin-javascript/test/unit/dispatcher-test.js 
b/gremlin-js/gremlin-javascript/test/unit/dispatcher-test.js
new file mode 100644
index 0000000000..8cfc4add0b
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/unit/dispatcher-test.js
@@ -0,0 +1,187 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import assert from 'assert';
+import { Agent, ProxyAgent } from 'undici';
+import {
+  buildDispatcher,
+  buildAgentOptions,
+  buildKeepAliveConnector,
+  resolveKeepAliveTime,
+  DEFAULT_MAX_CONNECTIONS,
+  DEFAULT_KEEP_ALIVE_TIME,
+} from '../../lib/driver/dispatcher.js';
+
+describe('dispatcher', function () {
+  it('builds a plain Agent by default', async function () {
+    const d = buildDispatcher();
+    assert.ok(d instanceof Agent, 'should be an undici Agent');
+    assert.ok(!(d instanceof ProxyAgent), 'should not be a ProxyAgent without 
a proxy');
+    await d.close();
+  });
+
+  it('defaults the connection cap to 128', function () {
+    assert.strictEqual(DEFAULT_MAX_CONNECTIONS, 128);
+  });
+
+  it('defaults the keep-alive idle time to 30000ms', function () {
+    assert.strictEqual(DEFAULT_KEEP_ALIVE_TIME, 30000);
+  });
+
+  it('builds a plain Agent when only timeouts and header size are set', async 
function () {
+    const d = buildDispatcher({ readTimeoutMillis: 1000, 
maxResponseHeaderBytes: 16384, maxConnections: 8 });
+    assert.ok(d instanceof Agent);
+    assert.ok(!(d instanceof ProxyAgent));
+    await d.close();
+  });
+
+  it('builds a ProxyAgent when a proxy is configured', async function () {
+    const d = buildDispatcher({ proxy: 'http://localhost:3128' });
+    assert.ok(d instanceof ProxyAgent, 'should be an undici ProxyAgent');
+    await d.close();
+  });
+
+  it('builds a single dispatcher with keep-alive wired through a custom 
connector', async function () {
+    const d = buildDispatcher({ keepAliveTimeMillis: 30000 });
+    assert.ok(d instanceof Agent);
+    await d.close();
+  });
+
+  it('builds a ProxyAgent with keep-alive and timeouts together', async 
function () {
+    const d = buildDispatcher({
+      proxy: 'http://localhost:3128',
+      keepAliveTimeMillis: 5000,
+      readTimeoutMillis: 2000,
+      maxResponseHeaderBytes: 8192,
+      maxConnections: 64,
+    });
+    assert.ok(d instanceof ProxyAgent);
+    await d.close();
+  });
+
+  describe('buildAgentOptions (undici option mapping)', function () {
+    it('maps readTimeoutMillis to the undici Agent bodyTimeout', function () {
+      const opts = buildAgentOptions({ readTimeoutMillis: 1234 });
+      assert.strictEqual(opts.bodyTimeout, 1234);
+    });
+
+    it('maps maxResponseHeaderBytes to the undici Agent maxHeaderSize', 
function () {
+      const opts = buildAgentOptions({ maxResponseHeaderBytes: 16384 });
+      assert.strictEqual(opts.maxHeaderSize, 16384);
+    });
+
+    it('maps maxConnections to the undici Agent connections', function () {
+      const opts = buildAgentOptions({ maxConnections: 8 });
+      assert.strictEqual(opts.connections, 8);
+    });
+
+    it('defaults connections to DEFAULT_MAX_CONNECTIONS when unset', function 
() {
+      const opts = buildAgentOptions();
+      assert.strictEqual(opts.connections, DEFAULT_MAX_CONNECTIONS);
+    });
+
+    it('omits bodyTimeout and maxHeaderSize when their options are unset', 
function () {
+      const opts = buildAgentOptions();
+      assert.strictEqual(opts.bodyTimeout, undefined);
+      assert.strictEqual(opts.maxHeaderSize, undefined);
+    });
+
+    it('maps both readTimeoutMillis and maxResponseHeaderBytes together', 
function () {
+      const opts = buildAgentOptions({ readTimeoutMillis: 2000, 
maxResponseHeaderBytes: 8192 });
+      assert.strictEqual(opts.bodyTimeout, 2000);
+      assert.strictEqual(opts.maxHeaderSize, 8192);
+    });
+
+    it('wires a custom connect connector for keep-alive by default', function 
() {
+      const opts = buildAgentOptions();
+      assert.strictEqual(typeof opts.connect, 'function', 'keep-alive should 
install a connect connector');
+    });
+
+    it('omits the connect connector when keep-alive is disabled 
(keepAliveTimeMillis 0)', function () {
+      const opts = buildAgentOptions({ keepAliveTimeMillis: 0 });
+      assert.strictEqual(opts.connect, undefined);
+    });
+  });
+
+  describe('resolveKeepAliveTime', function () {
+    it('falls back to the 30000ms default when unset', function () {
+      assert.strictEqual(resolveKeepAliveTime(undefined), 
DEFAULT_KEEP_ALIVE_TIME);
+      assert.strictEqual(resolveKeepAliveTime(), 30000);
+    });
+
+    it('honors a user-supplied positive override', function () {
+      assert.strictEqual(resolveKeepAliveTime(5000), 5000);
+    });
+
+    it('disables keep-alive (null) when set to 0 or a negative value', 
function () {
+      assert.strictEqual(resolveKeepAliveTime(0), null);
+      assert.strictEqual(resolveKeepAliveTime(-1), null);
+    });
+  });
+
+  describe('buildKeepAliveConnector', function () {
+    // Drives the connector with a fake base connector and socket so the 
applied
+    // keep-alive delay can be asserted without opening a real socket.
+    function fakeBaseConnector(socket, err) {
+      return (_connectOptions, callback) => callback(err ?? null, err ? null : 
socket);
+    }
+
+    function fakeSocket() {
+      const calls = [];
+      return {
+        calls,
+        setKeepAlive(enable, delay) {
+          calls.push({ enable, delay });
+        },
+      };
+    }
+
+    it('applies the default 30000ms delay to a freshly established socket', 
function (done) {
+      const socket = fakeSocket();
+      const delay = resolveKeepAliveTime(undefined);
+      const connector = buildKeepAliveConnector(delay, 
fakeBaseConnector(socket));
+      connector({}, (err, returned) => {
+        assert.ifError(err);
+        assert.strictEqual(returned, socket);
+        assert.deepStrictEqual(socket.calls, [{ enable: true, delay: 30000 }]);
+        done();
+      });
+    });
+
+    it('applies a user-supplied override delay', function (done) {
+      const socket = fakeSocket();
+      const connector = buildKeepAliveConnector(resolveKeepAliveTime(7500), 
fakeBaseConnector(socket));
+      connector({}, (err) => {
+        assert.ifError(err);
+        assert.deepStrictEqual(socket.calls, [{ enable: true, delay: 7500 }]);
+        done();
+      });
+    });
+
+    it('propagates connector errors without touching the socket', function 
(done) {
+      const boom = new Error('connect failed');
+      const connector = buildKeepAliveConnector(30000, fakeBaseConnector(null, 
boom));
+      connector({}, (err, returned) => {
+        assert.strictEqual(err, boom);
+        assert.strictEqual(returned, null);
+        done();
+      });
+    });
+  });
+});
diff --git a/gremlin-js/gremlin-javascript/test/unit/logger-test.js 
b/gremlin-js/gremlin-javascript/test/unit/logger-test.js
new file mode 100644
index 0000000000..53d3d3a664
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/unit/logger-test.js
@@ -0,0 +1,67 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import assert from 'assert';
+import { normalizeLogger } from '../../lib/driver/logger.js';
+
+describe('logger', function () {
+  it('returns a no-op when no logger is provided', function () {
+    const log = normalizeLogger();
+    // Should not throw and should not require a target.
+    assert.doesNotThrow(() => log('info', 'hello'));
+  });
+
+  it('returns a no-op for null', function () {
+    const log = normalizeLogger(null);
+    assert.doesNotThrow(() => log('warn', 'hello'));
+  });
+
+  it('passes through a callback logger with level, message, and args', 
function () {
+    const calls = [];
+    const log = normalizeLogger((level, message, ...args) => 
calls.push([level, message, args]));
+    log('debug', 'msg', 1, 2);
+    assert.deepStrictEqual(calls, [['debug', 'msg', [1, 2]]]);
+  });
+
+  it('dispatches to a logger object method matching the level', function () {
+    const calls = [];
+    const obj = {
+      debug: (m, ...a) => calls.push(['debug', m, a]),
+      info: (m, ...a) => calls.push(['info', m, a]),
+      warn: (m, ...a) => calls.push(['warn', m, a]),
+      error: (m, ...a) => calls.push(['error', m, a]),
+    };
+    const log = normalizeLogger(obj);
+    log('info', 'hi', 'x');
+    log('error', 'boom');
+    assert.deepStrictEqual(calls, [
+      ['info', 'hi', ['x']],
+      ['error', 'boom', []],
+    ]);
+  });
+
+  it('silently ignores levels missing on the logger object', function () {
+    const log = normalizeLogger({ error: () => { throw new Error('should not 
be called'); } });
+    assert.doesNotThrow(() => log('debug', 'hello'));
+  });
+
+  it('throws for an unsupported logger type', function () {
+    assert.throws(() => normalizeLogger(42), /logger must be/);
+  });
+});
diff --git a/gremlin-js/gremlin-mcp/src/gremlin/connection.ts 
b/gremlin-js/gremlin-mcp/src/gremlin/connection.ts
index 7e45d3101b..8742878da7 100644
--- a/gremlin-js/gremlin-mcp/src/gremlin/connection.ts
+++ b/gremlin-js/gremlin-mcp/src/gremlin/connection.ts
@@ -72,7 +72,7 @@ const createConnection = (
   authenticatorInput: Option.Option<{ username: string; password: string }>
 ): Effect.Effect<ConnectionState, GremlinConnectionError> =>
   Effect.gen(function* () {
-    const protocol = useSSL ? 'wss' : 'ws';
+    const protocol = useSSL ? 'https' : 'http';
     const url = `${protocol}://${host}:${port}/gremlin`;
 
     yield* Effect.logInfo('Creating Gremlin connection', {
@@ -82,21 +82,16 @@ const createConnection = (
       traversalSource,
     });
 
-    const headers = Option.match(authenticatorInput, {
+    const auth = Option.match(authenticatorInput, {
       onNone: () => undefined,
-      onSome: ({ username, password }) => {
-        const credentials = `${username}:${password}`;
-        return {
-          Authorization: `Basic 
${Buffer.from(credentials).toString('base64')}`,
-        };
-      },
+      onSome: ({ username, password }) => driver.auth.basic(username, 
password),
     });
 
     const connection = yield* Effect.try({
       try: () =>
         new DriverRemoteConnection(url, {
           traversalSource,
-          headers,
+          auth,
         }),
       catch: error => Errors.connection('Failed to create remote connection', 
{ error }),
     });
@@ -104,7 +99,7 @@ const createConnection = (
     const g = AnonymousTraversalSource.traversal().withRemote(connection);
     const client = new Client(url, {
       traversalSource,
-      headers,
+      auth,
     });
 
     // Verify the server is reachable before caching
diff --git a/gremlin-js/package-lock.json b/gremlin-js/package-lock.json
index fd04601a6b..81898d413f 100644
--- a/gremlin-js/package-lock.json
+++ b/gremlin-js/package-lock.json
@@ -18,7 +18,8 @@
       "dependencies": {
         "antlr4ng": "3.0.16",
         "buffer": "^6.0.3",
-        "eventemitter3": "^5.0.1"
+        "eventemitter3": "^5.0.1",
+        "undici": "6.27.0"
       },
       "devDependencies": {
         "@cucumber/cucumber": "~13.0.0",
@@ -71,6 +72,15 @@
         }
       }
     },
+    "gremlin-javascript/node_modules/undici": {
+      "version": "6.27.0",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz";,
+      "integrity": 
"sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.17"
+      }
+    },
     "gremlin-mcp": {
       "version": "4.0.0-alpha1",
       "hasInstallScript": true,

Reply via email to