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,