diff --git a/.claude/skills/review-pr/SKILL.md b/.claude/skills/review-pr/SKILL.md
new file mode 100644
index 0000000..7f5bb93
--- /dev/null
+++ b/.claude/skills/review-pr/SKILL.md
@@ -0,0 +1,857 @@
+---
+name: review-pr
+description: Review a GitHub pull request or local Git range against @questdb/nodejs-client TypeScript ILP/QWP client coding standards
+argument-hint: "[PR number or URL | --range=
Creates a new HttpTransport instance using Node.js HTTP modules.
-Sender configuration object containing connection details
-Protected ReadonlysecureProtected ReadonlyhostProtected ReadonlyportProtected ReadonlyusernameProtected ReadonlypasswordProtected ReadonlytokenProtected ReadonlytlsProtected ReadonlytlsProtected ReadonlyrequestProtected ReadonlyrequestProtected ReadonlyretryProtected ReadonlylogHTTP transport does not require explicit connection closure.
-Promise that resolves immediately
-Gets the default auto-flush row count for HTTP transport.
-Default number of rows that trigger auto-flush
-Sends data to QuestDB using HTTP POST.
-Buffer containing the data to send
-Internal parameter for tracking retry start time
-Internal parameter for tracking retry intervals
-Promise resolving to true if data was sent successfully
-The QuestDB client's API provides methods to connect to the database, ingest data, and close the connection.
-The client supports multiple transport protocols.
-Transport Options: -
-The client supports authentication.
-Authentication details can be passed to the Sender in its configuration options.
-The client supports Basic username/password and Bearer token authentication methods when used with HTTP protocol,
-and JWK token authentication when ingesting data via TCP.
-Please, note that authentication is enabled by default in QuestDB Enterprise only.
-Details on how to configure authentication in the open source version of
-QuestDB: https://questdb.io/docs/reference/api/ilp/authenticate
-
-The client also supports TLS encryption for both, HTTP and TCP transports to provide a secure connection.
-Please, note that the open source version of QuestDB does not support TLS, and requires an external reverse-proxy,
-such as Nginx to enable encryption.
-
-The client supports multiple protocol versions for data serialization. Protocol version 1 uses text-based -serialization, while version 2 uses binary encoding for doubles and supports array columns for improved -performance. The client can automatically negotiate the protocol version with the server when using HTTP/HTTPS -by setting the protocol_version to 'auto' (default behavior). -
--The client uses a buffer to store data. It automatically flushes the buffer by sending its content to the server. -Auto flushing can be disabled via configuration options to gain control over transactions. Initial and maximum -buffer sizes can also be set. -
-
-It is recommended that the Sender is created by using one of the static factory methods,
-Sender.fromConfig(configString, extraOptions) or Sender.fromEnv(extraOptions).
-If the Sender is created via its constructor, at least the SenderOptions configuration object should be
-initialized from a configuration string to make sure that the parameters are validated.
-Detailed description of the Sender's configuration options can be found in
-the SenderOptions documentation.
-
-Transport Configuration Examples: -
-HTTP Transport Implementation:
-By default, HTTP/HTTPS transport uses the high-performance Undici library for connection management and request handling.
-For compatibility or specific requirements, you can enable the standard HTTP transport using Node.js built-in modules
-by setting stdlib_http=on in the configuration string. The standard HTTP transport provides the same functionality
-but uses Node.js http/https modules instead of Undici.
-
-Extra options can be provided to the Sender in the extraOptions configuration object.
-A custom logging function and a custom HTTP(S) agent can be passed to the Sender in this object.
-The logger implementation provides the option to direct log messages to the same place where the host application's
-log is saved. The default logger writes to the console.
-The custom HTTP(S) agent option becomes handy if there is a need to modify the default options set for the
-HTTP(S) connections. A popular setting would be disabling persistent connections, in this case an agent can be
-passed to the Sender with keepAlive set to false.
-For example: Sender.fromConfig(`http::addr=host:port`, { agent: new undici.Agent({ connect: { keepAlive: false } })})
-If no custom agent is configured, the Sender will use its own agent which overrides some default values
-of undici.Agent. The Sender's own agent uses persistent connections with 1 minute idle timeout, pipelines requests default to 1.
-
Creates an instance of Sender.
-Sender configuration object.
-See SenderOptions documentation for detailed description of configuration options.
StaticfromCreates a Sender object by parsing the provided configuration string.
-Configuration string.
OptionalextraOptions: ExtraOptionsOptional extra configuration.
A Sender object initialized from the provided configuration string.
-StaticfromCreates a Sender object by parsing the configuration string set in the QDB_CLIENT_CONF environment variable.
-OptionalextraOptions: ExtraOptionsOptional extra configuration.
A Sender object initialized from the QDB_CLIENT_CONF environment variable.
-Resets the sender's buffer, data sitting in the buffer will be lost.
-In other words it clears the buffer, and sets the writing position to the beginning of the buffer.
Returns with a reference to this sender.
-Creates a TCP connection to the database.
-Resolves to true if the client is connected.
-Sends the content of the sender's buffer to the database and compacts the buffer. -If the last row is not finished it stays in the sender's buffer.
-Resolves to true when there was data in the buffer to send, and it was sent successfully.
-Closes the connection to the database.
-Data sitting in the Sender's buffer will be lost unless flush() is called before close().
Writes the table name into the buffer of the sender of the sender.
-Table name.
-Returns with a reference to this sender.
-Writes a symbol name and value into the buffer of the sender.
-Use it to insert into SYMBOL columns.
Symbol name.
-Symbol value, toString() is called to extract the actual symbol value from the parameter.
-Returns with a reference to this sender.
-Writes a string column with its value into the buffer of the sender.
-Use it to insert into VARCHAR and STRING columns.
Column name.
-Column value, accepts only string values.
-Returns with a reference to this sender.
-Writes a boolean column with its value into the buffer of the sender.
-Use it to insert into BOOLEAN columns.
Column name.
-Column value, accepts only boolean values.
-Returns with a reference to this sender.
-Writes a 64-bit floating point value into the buffer of the sender.
-Use it to insert into DOUBLE or FLOAT database columns.
Column name.
-Column value, accepts only number values.
-Returns with a reference to this sender.
-Writes an array column with its values into the buffer of the sender.
-Column name
-Array values to write (currently supports double arrays)
-Returns with a reference to this sender.
-Writes a 64-bit signed integer into the buffer of the sender.
-Use it to insert into LONG, INT, SHORT and BYTE columns.
Column name.
-Column value, accepts only number values.
-Returns with a reference to this sender.
-Writes a timestamp column and its value into the buffer of the sender.
-Use this method to insert data into TIMESTAMP or TIMESTAMP_NS columns.
Precision rules:
-'ns' (nanoseconds) are sent with full nanosecond precision.
-All other timestamps are sent with microsecond precision.The column name.
-The epoch timestamp. Must be an integer or a BigInt.
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. -Supported values:
-'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
-Writes a decimal value into the buffer using the text format.
-Use it to insert into DECIMAL database columns.
-Column name.
-Column value, accepts only number/string values.
-Returns with a reference to this buffer.
-Writes a decimal value into the buffer using the binary format.
-Use it to insert into DECIMAL database columns.
-Column name.
-The unscaled value of the decimal in two's -complement representation and big-endian byte order. -An empty array represents the NULL value.
-The scale of the decimal value.
-Returns with a reference to this buffer.
-Closes the row after writing the designated timestamp into the buffer of the sender.
-Precision rules:
-'ns' (nanoseconds) are sent with full nanosecond precision.
-All other timestamps are sent with microsecond precision.Designated epoch timestamp. Must be an integer or a BigInt.
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. -Supported values:
-'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
-Closes the row without writing designated timestamp into the buffer of the sender.
-Designated timestamp will be populated by the server on this record.
Buffer implementation for protocol version 1.
-Sends floating point numbers in their text form.
Creates a new SenderBufferV1 instance.
-Sender configuration object.
-See SenderOptions documentation for detailed description of configuration options.
Resets the buffer, data sitting in the buffer will be lost.
-In other words it clears the buffer, and sets the writing position to the beginning of the buffer.
Returns with a reference to this buffer.
-Returns a cropped buffer, or null if there is nothing to send.
-The returned buffer is backed by this buffer instance, meaning the view can change as the buffer is mutated.
-Used only in tests to assert the buffer's content.
Returns a cropped buffer ready to send to the server, or null if there is nothing to send.
-The returned buffer is a copy of this buffer.
-It also compacts the buffer.
Writes the table name into the buffer.
-Table name.
-Returns with a reference to this buffer.
-Writes a symbol name and value into the buffer.
-Use it to insert into SYMBOL columns.
Symbol name.
-Symbol value, toString() is called to extract the actual symbol value from the parameter.
-Returns with a reference to this buffer.
-Writes a string column with its value into the buffer.
-Use it to insert into VARCHAR and STRING columns.
Column name.
-Column value, accepts only string values.
-Returns with a reference to this buffer.
-Writes a boolean column with its value into the buffer.
-Use it to insert into BOOLEAN columns.
Column name.
-Column value, accepts only boolean values.
-Returns with a reference to this buffer.
-Writes a 64-bit signed integer into the buffer.
-Use it to insert into LONG, INT, SHORT and BYTE columns.
Column name.
-Column value, accepts only number values.
-Returns with a reference to this buffer.
-Writes a timestamp column and its value into the buffer.
-Use this method to insert data into TIMESTAMP or TIMESTAMP_NS columns.
Precision rules:
-'ns' (nanoseconds) are sent with full nanosecond precision.
-All other timestamps are sent with microsecond precision.The column name.
-The epoch timestamp. Must be an integer or a BigInt.
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. -Supported values:
-'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
-Closes the row after writing the designated timestamp into the buffer.
-Precision rules:
-'ns' (nanoseconds) are sent with full nanosecond precision.
-All other timestamps are sent with microsecond precision.Designated epoch timestamp. Must be an integer or a BigInt.
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. -Supported values:
-'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
-Closes the row without writing designated timestamp into the buffer.
-Designated timestamp will be populated by the server on this record.
Returns the current position of the buffer.
-New data will be written into the buffer starting from this position.
ProtectedcheckChecks if the buffer has sufficient capacity for additional data and resizes if needed.
-Array of strings to calculate the required capacity for
-Base number of bytes to add to the calculation
-Writes a decimal value into the buffer using its text format.
-Use it to insert into DECIMAL database columns.
-Column name.
-The decimal value to write.
-number or a string containing a valid decimal representation."123.45" or "-0.001").Returns with a reference to this buffer.
-Writes a decimal value into the buffer using its binary format.
-Use it to insert into DECIMAL database columns.
-Column name.
-The unscaled integer portion of the decimal value.
-bigint is provided, it will be converted automatically.Int8Array is provided, it must contain the two’s complement representation
-of the unscaled value in big-endian byte order.Int8Array represents a NULL value.The number of fractional digits (the scale) of the decimal value.
-Returns with a reference to this buffer.
-Writes a 64-bit floating point value into the buffer using v1 serialization (text format).
-Use it to insert into DOUBLE or FLOAT database columns.
Column name.
-Column value, accepts only number values.
-Returns with a reference to this sender.
-ProtectedwriteArray columns are not supported in protocol v1.
-Buffer implementation for protocol version 2.
-Sends floating point numbers in binary form, and provides support for arrays.
Creates a new SenderBufferV2 instance.
-Sender configuration object.
-See SenderOptions documentation for detailed description of configuration options.
Resets the buffer, data sitting in the buffer will be lost.
-In other words it clears the buffer, and sets the writing position to the beginning of the buffer.
Returns with a reference to this buffer.
-Returns a cropped buffer, or null if there is nothing to send.
-The returned buffer is backed by this buffer instance, meaning the view can change as the buffer is mutated.
-Used only in tests to assert the buffer's content.
Returns a cropped buffer ready to send to the server, or null if there is nothing to send.
-The returned buffer is a copy of this buffer.
-It also compacts the buffer.
Writes the table name into the buffer.
-Table name.
-Returns with a reference to this buffer.
-Writes a symbol name and value into the buffer.
-Use it to insert into SYMBOL columns.
Symbol name.
-Symbol value, toString() is called to extract the actual symbol value from the parameter.
-Returns with a reference to this buffer.
-Writes a string column with its value into the buffer.
-Use it to insert into VARCHAR and STRING columns.
Column name.
-Column value, accepts only string values.
-Returns with a reference to this buffer.
-Writes a boolean column with its value into the buffer.
-Use it to insert into BOOLEAN columns.
Column name.
-Column value, accepts only boolean values.
-Returns with a reference to this buffer.
-Writes a 64-bit signed integer into the buffer.
-Use it to insert into LONG, INT, SHORT and BYTE columns.
Column name.
-Column value, accepts only number values.
-Returns with a reference to this buffer.
-Writes a timestamp column and its value into the buffer.
-Use this method to insert data into TIMESTAMP or TIMESTAMP_NS columns.
Precision rules:
-'ns' (nanoseconds) are sent with full nanosecond precision.
-All other timestamps are sent with microsecond precision.The column name.
-The epoch timestamp. Must be an integer or a BigInt.
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. -Supported values:
-'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
-Closes the row after writing the designated timestamp into the buffer.
-Precision rules:
-'ns' (nanoseconds) are sent with full nanosecond precision.
-All other timestamps are sent with microsecond precision.Designated epoch timestamp. Must be an integer or a BigInt.
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. -Supported values:
-'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
-Closes the row without writing designated timestamp into the buffer.
-Designated timestamp will be populated by the server on this record.
Returns the current position of the buffer.
-New data will be written into the buffer starting from this position.
ProtectedcheckChecks if the buffer has sufficient capacity for additional data and resizes if needed.
-Array of strings to calculate the required capacity for
-Base number of bytes to add to the calculation
-Writes a decimal value into the buffer using its text format.
-Use it to insert into DECIMAL database columns.
-Column name.
-The decimal value to write.
-number or a string containing a valid decimal representation."123.45" or "-0.001").Returns with a reference to this buffer.
-Writes a decimal value into the buffer using its binary format.
-Use it to insert into DECIMAL database columns.
-Column name.
-The unscaled integer portion of the decimal value.
-bigint is provided, it will be converted automatically.Int8Array is provided, it must contain the two’s complement representation
-of the unscaled value in big-endian byte order.Int8Array represents a NULL value.The number of fractional digits (the scale) of the decimal value.
-Returns with a reference to this buffer.
-Writes a 64-bit floating point value into the buffer using v2 serialization (binary format).
-Use it to insert into DOUBLE or FLOAT database columns.
Column name.
-Column value, accepts only number values.
-Returns with a reference to this buffer.
-ProtectedwriteWrite an array column with its values into the buffer using v2 format.
-Column name
-Array values to write (currently supports double arrays)
-Returns with a reference to this buffer.
-Sender configuration options.
-
-Properties of the object are initialized through a configuration string.
-The configuration string has the following format: protocol::key=value;key=value...
-The keys are case-sensitive, the trailing semicolon is optional.
-The values are validated and an error is thrown if the format is invalid.
-
-Connection and protocol options
Creates a Sender options object by parsing the provided configuration string.
-Configuration string.
OptionalextraOptions: ExtraOptionsOptional extra configuration.
Optionalprotocol_OptionaladdrOptionalhostOptionalportOptionalusernameOptionalpasswordOptionaltokenOptionaltoken_Optionaltoken_Optionalauto_Optionalauto_Optionalauto_Optionalrequest_Optionalrequest_Optionalretry_Optionalinit_Optionalmax_Optionaltls_Optionaltls_Optionaltls_Optionaltls_Optionalmax_OptionallogOptionalagentOptionalstdlib_OptionalauthOptionaljwkStaticresolveResolves the protocol version, if it is set to 'auto'.
-If TCP transport is used, the protocol version will default to 1.
-In case of HTTP transport the /settings endpoint of the database is used to find the protocol versions
-supported by the server, and the highest will be selected.
-When calling the /settings endpoint the timeout and TLS options are used from the options object.
SenderOptions instance needs resolving protocol version
-StaticresolveStaticfromCreates a Sender options object by parsing the provided configuration string.
-Configuration string.
OptionalextraOptions: ExtraOptionsOptional extra configuration.
A Sender configuration object initialized from the provided configuration string.
-StaticfromCreates a Sender options object by parsing the configuration string set in the QDB_CLIENT_CONF environment variable.
-OptionalextraOptions: ExtraOptionsOptional extra configuration.
A Sender configuration object initialized from the QDB_CLIENT_CONF environment variable.
-TCP transport implementation.
-Supports both plain TCP or secure TLS-encrypted connections with configurable JWK token authentication.
Creates a new TcpTransport instance.
-Sender configuration object containing connection and authentication details
-HTTP transport implementation using the Undici library.
-Provides high-performance HTTP requests with connection pooling and retry logic.
-Supports both HTTP and HTTPS protocols with configurable authentication.
Creates a new UndiciTransport instance.
-Sender configuration object containing connection and retry settings
-Protected ReadonlysecureProtected ReadonlyhostProtected ReadonlyportProtected ReadonlyusernameProtected ReadonlypasswordProtected ReadonlytokenProtected ReadonlytlsProtected ReadonlytlsProtected ReadonlyrequestProtected ReadonlyrequestProtected ReadonlyretryProtected ReadonlylogBrowser-safe typed positional bind encoder.
+Setters must be called in ascending zero-based index order. SQL placeholders
+are one-based, so index 0 binds $1, index 1 binds $2, and so on.
Binds a DATE expressed as milliseconds since the Unix epoch.
+Binds a TIMESTAMP expressed as microseconds since the Unix epoch.
+Binds a TIMESTAMP_NS expressed as nanoseconds since the Unix epoch.
+An HTTP rejection while creating a browser qdb_session cookie.
Optional ReadonlycauseOptional ReadonlycloseReadonlykindReadonlyresponseOptional ReadonlyretryableOptional ReadonlyserverOptional ReadonlyserverOptionalstackOptional ReadonlystatusOptional ReadonlystatusOptional ReadonlytimeoutNode opening phase that exceeded its deadline.
+Optional ReadonlytryOptional ReadonlyurlA bounds-checked, runtime-neutral little-endian byte reader.
+A growable, runtime-neutral little-endian byte writer.
+Browser-safe facade owning bounded ingress and egress connection pools. +Borrowed handles are exclusive; separate query leases execute concurrently.
+Borrows one exclusive egress connection for one or more serial queries.
+Borrows an exclusive fluent sender; close() flushes and returns its slot.
+Rejects new borrows and closes idle resources. Borrowed query sessions are +cancelled and closed; borrowed senders retain ownership during a bounded +drain and own their teardown if they outlive it.
+Pre-connects the configured minimum sender and query pool sizes.
+The owning QWP client, or one of its returned lease handles, is closed.
+A requested durable-ACK capability was not confirmed by the server.
+Optional ReadonlycauseOptional ReadonlycloseReadonlykindOptional ReadonlyretryableOptional ReadonlyserverOptional ReadonlyserverOptionalstackOptional ReadonlystatusOptional ReadonlystatusOptional ReadonlytimeoutNode opening phase that exceeded its deadline.
+Optional ReadonlytryReadonlyurlOne QWP query/statement and its stream of materialized result batches.
+OptionalviewHandler: QwpResultBatchViewHandlerInternalWhether the consumer has retired while the wire still drains.
+InternalInternalStarts the deadline after QUERY_REQUEST reaches the transport.
+Waits for completion without changing the query lifecycle. A finite wait +returns false on expiry; the query remains active until it completes, is +cancelled explicitly, or its configured query deadline expires.
+InternalInternalPreserves batch/callback order before a wire query error.
+Whether the query has reached any terminal outcome.
+InternalCredit needed to discard a late batch while cancellation drains.
+InternalPublishes a batch after reserveMaterializedBatch().
+InternalQueues a decoded view after reserveViewBatch().
+InternalReleases a reservation when decoding fails.
+InternalReleases a reservation when zero-copy decoding fails.
+InternalWaits for one decoded materialized-batch slot.
+InternalWaits for one reusable zero-copy view slot.
+InternalInternalDiscards queued results and retires the consumer immediately.
+InternalWaits until all callback-scoped views have been released.
+Result iteration ended before the server completed the query.
+The server did not terminate a cancelled query within the drain deadline.
+A client-side query deadline expired and a QWP CANCEL was sent.
+OptionalrequestId: bigintBrowser-safe QWP egress session.
+The server currently executes one query at a time per connection, so this +session deliberately rejects overlapping query calls. A completed query's +materialized batches may still be consumed while the next query runs.
+ReadonlyreadyInitial SERVER_INFO; use serverInfo for the current post-failover snapshot.
+Effective codec and level echoed by the server on the active endpoint.
+Effective Zstd level, or zero for raw or unknown negotiation.
+Cached immutable SERVER_INFO for the currently bound endpoint. Reading it +never initiates a connection or failover walk. It is undefined before the +initial bind and refreshes after every successful reconnect.
+InternalCancels and drains an active operation before a pooled lease is returned. +False means the physical session is no longer safe to reuse.
+Executes a query through a bounded, reusable, zero-copy batch callback. +Callbacks run serially and are awaited before their batch is invalidated +and flow-control credit is replenished. The receive loop decodes ahead +into the remaining reusable slots, up to bufferPoolSize.
+InternalBest-effort cancellation followed by physical connection teardown for +facade shutdown. Unlike pooled lease return, this does not wait for the +server to finish draining the cancelled query.
+StaticconnectOptionalsignal: AbortSignalCancels a connection or SERVER_INFO handshake still in progress.
+OptionalcloseInfo: QwpConnectionCloseInfoEvery eligible QWP endpoint in one connection sweep failed.
+The ingress ACK watermark did not reach the requested frame in time.
+Connection-scoped ingress sequencer.
+One promise is registered before each WebSocket send, preventing a fast ACK +from racing its waiter. Calls are serialized to preserve the server's +zero-based wire sequence. Successful ACKs are cumulative, so an ACK for +sequence N resolves every outstanding send through N.
+Highest cumulative ACK watermark. When durable ACK was negotiated this +advances only after durability; otherwise it follows ordinary OK ACKs.
+Highest stable frame sequence published by this session/transport.
+Prompts the server to publish its latest durable-ingress watermarks. +Node transports use a WebSocket PING; browsers send the protocol-level +table-less durable-ACK poll frame. Browser completion means the control +frame was published; durable progress arrives independently because the +server may withhold its cumulative OK while a transaction remains open.
+Publishes one pre-encoded frame without allocating an ACK waiter. +Applications can observe later acceptance through progress callbacks.
+Encodes and publishes tables without waiting for their server ACK. With +Node store-and-forward this resolves only after every frame is durable in +the local journal; browser and non-persistent transports resolve after the +WebSocket accepts the frames.
+Publishes tables with the automatic connection-scoped symbol dictionary. +After a replay dictionary persistence error, retries use full inline +symbols and no longer depend on the failed sidecar.
+InternalRegisters runtime-specific cleanup owned by this session.
+Starts one pre-encoded frame with independent publication and ACKs.
+Sends tables using the session's connection-scoped symbol dictionary. +String symbol values are assigned stable IDs automatically. +If a replay dictionary append fails, that call rejects with +QwpReplayDictionaryPersistenceError; retrying uses full inline symbols.
+Delta-dictionary variant of sendTablesWithPublication().
+Starts an ingress batch and exposes local publication separately from its +server ACK. High-level senders use this boundary to retain retryable rows +until a persistent replay journal owns the complete logical batch.
+Waits independently for the cumulative frame ACK watermark. A negative +target is already satisfied, but still surfaces a latched session error.
+Waits until a durable ACK covers every table transaction in an OK ACK. +Durable tracking must have been enabled with durableAckKeepaliveMs.
+StaticconnectOptionalsignal: AbortSignalCancels a first connect that is still negotiating. The reconnect loop
+owns its own controller, but the initial attempt bypasses it -- it is
+either handed in as initialConnection or awaited directly below -- so
+without this a close() during the first connect left the socket and its
+deadline alive for the full connect/auth timeout.
OptionalcloseInfo: QwpConnectionCloseInfoACK-driven trimming did not free in-memory replay capacity in time.
+One frame can never fit in the configured in-memory replay budget.
+A bounded QWP pool could not provide a connection before its deadline.
+A pooled resource failed while a new slot was being connected.
+Raised when a QWP payload is malformed, truncated, or unsupported.
+One exclusively borrowed egress session from a QwpClient query pool.
+InternalReadonlyreadyInitial SERVER_INFO; use serverInfo for the current post-failover snapshot.
+Cached immutable SERVER_INFO for this lease's currently bound endpoint. +Reading it does not drive failover; a successful query replay refreshes it.
+A configured QWP reconnect policy exhausted its retry boundary.
+A replay store cannot preserve the dictionary required by delta frames.
+Optionalcause: unknownA replay dictionary sidecar rejected an append before its delta frame was +published. The reconnecting transport has permanently switched to full, +self-contained symbol encoding; retrying the logical batch is safe.
+A replayed ingress frame was rejected and remains in persistent storage.
+Optionalmessage: stringReadonlybatchReadonlycolumnsReadonlyrequestReadonlyrowReadonlytableStateful decoder for connection-scoped QWP result batches.
+Decodes into one slot from a reusable batch/column-view pool without +materializing a JavaScript value array. Reusing the same slot invalidates +its prior view; callers must not reuse a slot until its consumer releases +the preceding batch.
+InternalDrops frame-backed references after a failed slot decode.
+Batch-owned reusable view delivered by QwpEgressSession.queryViews(). +Access is invalid after the callback returns. materialize() creates an +independently owned QwpResultBatch when retention is required.
+Visits rows in index order with one re-pointed row view. The callback is +synchronous; copy values that must survive the current invocation.
+InternalInvalidates the view. Normally called automatically after queryViews().
+Returns the batch-owned reusable row view pinned to rowIndex. Every call +returns the same object re-pointed at the requested row.
+Reusable, zero-copy view over one QWP result column.
+The view and every byte slice returned from it are valid only while the +surrounding queryViews() callback is running. Copy data that must outlive +the callback.
+Fixed-width stride, zero for bit-packed BOOLEAN, or -1 when variable.
+Lazily materializes one cell; prefer typed/raw accessors on hot paths.
+Zero-copy encoded ARRAY row, including dimension header.
+Zero-copy BINARY bytes.
+Zero-copy UTF-8 bytes for a VARCHAR value.
+Reusable dense-index table; only the first rowCount entries are valid.
+Raw per-row NULL bitmap, without copying. Undefined means no NULLs.
+Concatenated VARCHAR/BINARY payload bytes, without copying.
+Reusable per-row SYMBOL IDs; NULL-row entries are unspecified.
+Raw packed non-null values. Fixed-width values use QWP little-endian +layout; booleans are bit-packed and variable-width columns contain their +uint32 offset table. SYMBOL returns undefined because IDs are varints.
+Reusable row-pinned facade over a QwpResultBatchView.
+The batch owns one instance and re-points it in place. It is valid only +while the surrounding queryViews() callback is running, and must not be +retained across forEachRow() iterations. Byte and array views returned by +its accessors remain zero-copy and have the same lifetime.
+Parent batch, primarily for column metadata.
+Zero-based row currently pinned by this reusable view.
+Zero-copy encoded ARRAY row, including its dimension header.
+Zero-copy BINARY bytes.
+Zero-copy UTF-8 bytes for a VARCHAR value.
+Re-points this flyweight at a row and returns the same instance.
+A connected endpoint advertised a role that does not satisfy target.
Optionalurl: string | URLOptionalserverZone: stringOptional ReadonlycauseOptional ReadonlycloseReadonlykindOptional ReadonlyretryableOptional ReadonlyserverOptional ReadonlyserverOptionalstackOptional ReadonlystatusOptional ReadonlystatusReadonlytargetOptional ReadonlytimeoutNode opening phase that exceeded its deadline.
+Optional ReadonlytryOptional ReadonlyurlA QWP send was rejected because its WebSocket closed.
+OptionalcloseInfo: QwpConnectionCloseInfoA failure while handing a QWP frame to the WebSocket transport.
+Optionalcause: unknownThe WebSocket did not drain a QWP frame before its send deadline.
+OptionalbufferedAmountBytes: numberBrowser-safe high-level QWP ingress API.
+Applications normally obtain this class through create/connectQwpNodeSender +or create/connectQwpBrowserSender, rather than constructing sessions and +QwpTableBuffer instances themselves.
+Highest cumulative ACK watermark, or -1n before acknowledgement.
+Highest stable frame sequence published by this sender.
+Adds a QuestDB DOUBLE[] value with between 1 and 32 dimensions.
+Discards the row in progress, including its table selection, so the next +row starts from table() again. Rows already completed stay staged.
+Commits rows previously sent by transactional auto-flush. This is an +ergonomic alias for flush(); pending local rows are included in the same +group-closing frame.
+Adds a QuestDB DATE column value in milliseconds since the epoch.
+-9_223_372_036_854_775_808n is QuestDB's DATE NULL sentinel: it is
+stored as NULL and cannot be stored as an ordinary value.
Publishes completed rows to the local ingress/replay boundary. This does +not wait for a server ACK unless awaitServerAck or awaitDurableAck is set.
+Publishes pending rows without waiting for their server ACK and returns +the highest frame sequence produced by this call, or -1n when empty. +Pass the result to waitForAcknowledged() when an explicit delivery +barrier is needed.
+Adds a QuestDB INT column value. -2_147_483_648 is QuestDB's INT NULL
+sentinel: it is stored as NULL and cannot be stored as an ordinary value.
Adds a protocol LONG[] column value with between 1 and 32 dimensions.
+Current QuestDB servers reject LONG-array ingestion with long arrays are not supported, only double arrays. This method remains available for
+Java-client and protocol parity.
Adds a QuestDB LONG column value. -9_223_372_036_854_775_808n is
+QuestDB's LONG NULL sentinel: it is stored as NULL and cannot be stored as
+an ordinary value.
InternalFlushes completed rows and resets borrower-local staging without closing +the physical session. Used by the pooled QWP client when a lease returns.
+Independently waits until the cumulative ACK watermark covers a frame.
+OptionaltimeoutMs: numberCompiles an immutable table schema into an atomic object-row writer. +The returned writer remains usable after this sender is reset.
+close() could not publish and acknowledge all committed ingress frames.
+Connection-scoped QWP symbol dictionary. IDs are dense from zero.
+Appends positionally without de-duplicating recovered entries.
+Rolls back entries added while preparing a frame that was not published.
+Mutable columnar staging area for one QWP ingress table.
+Returns null when the current row already contains this column. The first +value wins, matching the existing Sender API.
+Closes the current row and back-fills missing columns with nulls.
+Truncates every column back to the last completed row.
+Copies a completed half-open row range into an independent table buffer. +Compact column values and their null bitmaps are sliced together, so the +result can be encoded without materialising rows first.
+A reusable table-bound writer compiled from a QWP schema.
+InternalConstruct table writers with QwpSender.writer().
+Validates and atomically appends one complete object row.
+Appends a synchronous or asynchronous stream of complete object rows.
+Recovered delta frames depend on symbol IDs that neither the durable +dictionary prefix nor the surviving frames can reconstruct.
+Optionalcause: unknownA failure while establishing or validating a QWP WebSocket upgrade.
+Optional ReadonlycauseOptional ReadonlycloseReadonlykindOptional ReadonlyretryableOptional ReadonlyserverOptional ReadonlyserverOptionalstackOptional ReadonlystatusOptional ReadonlystatusOptional ReadonlytimeoutNode opening phase that exceeded its deadline.
+Optional ReadonlytryOptional ReadonlyurlA complete object row failed compiled-writer validation.
+HTTP transport implementation using Node.js built-in http/https modules.
+Supports both HTTP and HTTPS protocols with configurable authentication.
Creates a new HttpTransport instance using Node.js HTTP modules.
+Sender configuration object containing connection details
+Protected ReadonlyhostProtected ReadonlylogProtected ReadonlypasswordProtected ReadonlyportProtected ReadonlyrequestProtected ReadonlyrequestProtected ReadonlyretryProtected ReadonlysecureProtected ReadonlytlsProtected ReadonlytlsProtected ReadonlytokenProtected ReadonlyusernameHTTP transport does not require explicit connection closure.
+Promise that resolves immediately
+Gets the default auto-flush row count for HTTP transport.
+Default number of rows that trigger auto-flush
+Sends data to QuestDB using HTTP POST.
+Buffer containing the data to send
+Internal parameter for tracking retry start time
+Internal parameter for tracking retry intervals
+Promise resolving to true if data was sent successfully
+ReadonlybatchOptionalcauseReadonlymaxOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareBrowser-safe typed positional bind encoder.
+Setters must be called in ascending zero-based index order. SQL placeholders
+are one-based, so index 0 binds $1, index 1 binds $2, and so on.
Binds a DATE expressed as milliseconds since the Unix epoch.
+Binds a TIMESTAMP expressed as microseconds since the Unix epoch.
+Binds a TIMESTAMP_NS expressed as nanoseconds since the Unix epoch.
+A bounds-checked, runtime-neutral little-endian byte reader.
+A growable, runtime-neutral little-endian byte writer.
+Browser-safe facade owning bounded ingress and egress connection pools. +Borrowed handles are exclusive; separate query leases execute concurrently.
+Borrows one exclusive egress connection for one or more serial queries.
+Borrows an exclusive fluent sender; close() flushes and returns its slot.
+Rejects new borrows and closes idle resources. Borrowed query sessions are +cancelled and closed; borrowed senders retain ownership during a bounded +drain and own their teardown if they outlive it.
+Pre-connects the configured minimum sender and query pool sizes.
+The owning QWP client, or one of its returned lease handles, is closed.
+OptionalcauseOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareA requested durable-ACK capability was not confirmed by the server.
+Optional ReadonlycauseOptional ReadonlycloseReadonlykindOptional ReadonlyretryableOptional ReadonlyserverOptional ReadonlyserverOptionalstackOptional ReadonlystatusOptional ReadonlystatusOptional ReadonlytimeoutNode opening phase that exceeded its deadline.
+Optional ReadonlytryReadonlyurlStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+True for a 421 response from a read-only replica.
+True for a 421 response from a primary still completing catch-up.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareOne QWP query/statement and its stream of materialized result batches.
+OptionalviewHandler: QwpResultBatchViewHandlerInternalWhether the consumer has retired while the wire still drains.
+InternalInternalStarts the deadline after QUERY_REQUEST reaches the transport.
+Waits for completion without changing the query lifecycle. A finite wait +returns false on expiry; the query remains active until it completes, is +cancelled explicitly, or its configured query deadline expires.
+InternalInternalPreserves batch/callback order before a wire query error.
+Whether the query has reached any terminal outcome.
+InternalCredit needed to discard a late batch while cancellation drains.
+InternalPublishes a batch after reserveMaterializedBatch().
+InternalQueues a decoded view after reserveViewBatch().
+InternalReleases a reservation when decoding fails.
+InternalReleases a reservation when zero-copy decoding fails.
+InternalWaits for one decoded materialized-batch slot.
+InternalWaits for one reusable zero-copy view slot.
+InternalInternalDiscards queued results and retires the consumer immediately.
+InternalWaits until all callback-scoped views have been released.
+Result iteration ended before the server completed the query.
+OptionalcauseReadonlyrequestOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareThe server did not terminate a cancelled query within the drain deadline.
+OptionalcauseReadonlyrequestOptionalstackReadonlytimeoutStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareOptionalcauseReadonlyrequestOptionalstackReadonlystatusStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareA client-side query deadline expired and a QWP CANCEL was sent.
+OptionalcauseReadonlyrequestOptionalstackReadonlytimeoutStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareOptionalrequestId: bigintOptionalcauseOptional ReadonlyrequestOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareBrowser-safe QWP egress session.
+The server currently executes one query at a time per connection, so this +session deliberately rejects overlapping query calls. A completed query's +materialized batches may still be consumed while the next query runs.
+ReadonlyreadyInitial SERVER_INFO; use serverInfo for the current post-failover snapshot.
+Effective codec and level echoed by the server on the active endpoint.
+Effective Zstd level, or zero for raw or unknown negotiation.
+Cached immutable SERVER_INFO for the currently bound endpoint. Reading it +never initiates a connection or failover walk. It is undefined before the +initial bind and refreshes after every successful reconnect.
+InternalCancels and drains an active operation before a pooled lease is returned. +False means the physical session is no longer safe to reuse.
+Executes a query through a bounded, reusable, zero-copy batch callback. +Callbacks run serially and are awaited before their batch is invalidated +and flow-control credit is replenished. The receive loop decodes ahead +into the remaining reusable slots, up to bufferPoolSize.
+InternalBest-effort cancellation followed by physical connection teardown for +facade shutdown. Unlike pooled lease return, this does not wait for the +server to finish draining the cancelled query.
+StaticconnectOptionalsignal: AbortSignalCancels a connection or SERVER_INFO handshake still in progress.
+OptionalcloseInfo: QwpConnectionCloseInfoOptionalcauseOptional ReadonlycloseOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareEvery eligible QWP endpoint in one connection sweep failed.
+ReadonlyattemptsOptional ReadonlycauseOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareThe ingress ACK watermark did not reach the requested frame in time.
+ReadonlyacknowledgedOptionalcauseOptionalstackReadonlytargetReadonlytimeoutStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareOptionalcauseReadonlyresponseReadonlysenderOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareConnection-scoped ingress sequencer.
+One promise is registered before each WebSocket send, preventing a fast ACK +from racing its waiter. Calls are serialized to preserve the server's +zero-based wire sequence. Successful ACKs are cumulative, so an ACK for +sequence N resolves every outstanding send through N.
+Highest cumulative ACK watermark. When durable ACK was negotiated this +advances only after durability; otherwise it follows ordinary OK ACKs.
+Highest stable frame sequence published by this session/transport.
+Prompts the server to publish its latest durable-ingress watermarks. +Node transports use a WebSocket PING; browsers send the protocol-level +table-less durable-ACK poll frame. Browser completion means the control +frame was published; durable progress arrives independently because the +server may withhold its cumulative OK while a transaction remains open.
+Publishes one pre-encoded frame without allocating an ACK waiter. +Applications can observe later acceptance through progress callbacks.
+Encodes and publishes tables without waiting for their server ACK. With +Node store-and-forward this resolves only after every frame is durable in +the local journal; browser and non-persistent transports resolve after the +WebSocket accepts the frames.
+Publishes tables with the automatic connection-scoped symbol dictionary. +After a replay dictionary persistence error, retries use full inline +symbols and no longer depend on the failed sidecar.
+InternalRegisters runtime-specific cleanup owned by this session.
+Starts one pre-encoded frame with independent publication and ACKs.
+Sends tables using the session's connection-scoped symbol dictionary. +String symbol values are assigned stable IDs automatically. +If a replay dictionary append fails, that call rejects with +QwpReplayDictionaryPersistenceError; retrying uses full inline symbols.
+Delta-dictionary variant of sendTablesWithPublication().
+Starts an ingress batch and exposes local publication separately from its +server ACK. High-level senders use this boundary to retain retryable rows +until a persistent replay journal owns the complete logical batch.
+Waits independently for the cumulative frame ACK watermark. A negative +target is already satisfied, but still surfaces a latched session error.
+Waits until a durable ACK covers every table transaction in an OK ACK. +Durable tracking must have been enabled with durableAckKeepaliveMs.
+StaticconnectOptionalsignal: AbortSignalCancels a first connect that is still negotiating. The reconnect loop
+owns its own controller, but the initial attempt bypasses it -- it is
+either handed in as initialConnection or awaited directly below -- so
+without this a close() during the first connect left the socket and its
+deadline alive for the full connect/auth timeout.
OptionalcloseInfo: QwpConnectionCloseInfoOptionalcauseOptional ReadonlycloseOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareACK-driven trimming did not free in-memory replay capacity in time.
+OptionalcauseReadonlymaxReadonlyrequiredOptionalstackReadonlytimeoutReadonlyusedStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareOne frame can never fit in the configured in-memory replay budget.
+OptionalcauseReadonlymaxReadonlypayloadReadonlyrequiredOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareNode store-and-forward journal with configurable local durability.
+The active fixed-size segment and one hot spare remain open for positional
+writes. append fsyncs each frame, periodic batches barriers, and memory
+relies on OS writeback. An ACK persists its cursor before bounded background
+trimming. A crash between the server ACK and local deletion can cause
+at-least-once replay. An exclusive, lifetime lock prevents another process
+from recovering or mutating the same directory.
Persists new dense entries before a delta frame is made replayable.
+Opens and validates the journal without materializing every payload.
+Implementations that provide this must also provide readPayload.
Loads the durable, dense symbol prefix used by persisted delta frames.
+Reads one previously loaded durable payload on demand.
+Atomically replaces an unusable dictionary after surviving committed +frames prove that its complete ID space can be reconstructed.
+Bounded Node-only scanner and background drainer for replay slots left by +terminated producer processes. Each adopted slot uses its own connection.
+Node-only, fire-and-forget QWP v1 ingress session over IPv4 UDP.
+Each datagram is self-contained: it carries one table, an inline schema and +local symbol dictionaries. There are no ACKs, retries, transactions, +authentication, compression, or store-and-forward semantics.
+StaticconnectA bounded QWP pool could not provide a connection before its deadline.
+OptionalcauseReadonlyresourceOptionalstackReadonlytimeoutStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareA pooled resource failed while a new slot was being connected.
+ReadonlycauseReadonlyresourceOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareRaised when a QWP payload is malformed, truncated, or unsupported.
+OptionalcauseOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareOne exclusively borrowed egress session from a QwpClient query pool.
+InternalReadonlyreadyInitial SERVER_INFO; use serverInfo for the current post-failover snapshot.
+Cached immutable SERVER_INFO for this lease's currently bound endpoint. +Reading it does not drive failover; a successful query replay refreshes it.
+A configured QWP reconnect policy exhausted its retry boundary.
+ReadonlyattemptsReadonlycauseOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareA replay store cannot preserve the dictionary required by delta frames.
+Optionalcause: unknownOptional ReadonlycauseOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareA replay dictionary sidecar rejected an append before its delta frame was +published. The reconnecting transport has permanently switched to full, +self-contained symbol encoding; retrying the logical batch is safe.
+Optional ReadonlycauseOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareA replayed ingress frame was rejected and remains in persistent storage.
+Optionalmessage: stringOptionalcauseReadonlyframeOptionalstackReadonlystatusStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareOptional ReadonlycauseReadonlymaxReadonlyrequiredReadonlyretryableWhether reconnecting and replaying can plausibly clear this failure.
+Background maintenance and checkpoint faults are parked and cleared on the +next successful batch, so a briefly full, read-only or descriptor-starved +filesystem is retryable. Structural corruption and a slot lock taken over +by another process are verdicts on the journal itself and are not. The +ingress connection lives in the browser-safe layer and cannot reference +these classes, so it reads this flag structurally.
+OptionalstackReadonlytimeoutStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareOptionalcause: unknownOptional ReadonlycauseReadonlydirectoryReadonlyretryableWhether reconnecting and replaying can plausibly clear this failure.
+Background maintenance and checkpoint faults are parked and cleared on the +next successful batch, so a briefly full, read-only or descriptor-starved +filesystem is retryable. Structural corruption and a slot lock taken over +by another process are verdicts on the journal itself and are not. The +ingress connection lives in the browser-safe layer and cannot reference +these classes, so it reads this flag structurally.
+OptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareDurable journal bytes are structurally corrupt and cannot be replayed.
+Optionalcause: unknownOptional ReadonlycauseReadonlyretryableCorrupt bytes read the same way on every attempt.
+OptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareOptionalcause: unknownOptional ReadonlycauseReadonlyretryableWhether reconnecting and replaying can plausibly clear this failure.
+Background maintenance and checkpoint faults are parked and cleared on the +next successful batch, so a briefly full, read-only or descriptor-starved +filesystem is retryable. Structural corruption and a slot lock taken over +by another process are verdicts on the journal itself and are not. The +ingress connection lives in the browser-safe layer and cannot reference +these classes, so it reads this flag structurally.
+OptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareOptional ReadonlycauseReadonlymaxReadonlyrequiredReadonlyretryableWhether reconnecting and replaying can plausibly clear this failure.
+Background maintenance and checkpoint faults are parked and cleared on the +next successful batch, so a briefly full, read-only or descriptor-starved +filesystem is retryable. Structural corruption and a slot lock taken over +by another process are verdicts on the journal itself and are not. The +ingress connection lives in the browser-safe layer and cannot reference +these classes, so it reads this flag structurally.
+OptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareThe advisory lock guarding this journal was taken over by another process +while it was open, so this store may no longer write to it.
+A holder whose heartbeat lapses -- a long synchronous section, a paused +process, a stalled filesystem -- can have its slot reclaimed while it still +believes it holds it. Whatever this store does next must not be an append: +the new owner appends at offsets this store still believes are free, and +because a frame's sequence is derived from its position, an overwrite of the +same width leaves a journal that reopens as intact with the new owner's +frames gone. Failing the append is what keeps that loss impossible.
+Optional ReadonlycauseReadonlydirectoryReadonlyretryableRetrying is precisely what must not happen: the slot belongs to another +process now, so replaying out of it would race that owner's appends.
+OptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareOptionalholderPid: numberOptional ReadonlycauseReadonlydirectoryOptional ReadonlyholderReadonlyretryableWhether reconnecting and replaying can plausibly clear this failure.
+Background maintenance and checkpoint faults are parked and cleared on the +next successful batch, so a briefly full, read-only or descriptor-starved +filesystem is retryable. Structural corruption and a slot lock taken over +by another process are verdicts on the journal itself and are not. The +ingress connection lives in the browser-safe layer and cannot reference +these classes, so it reads this flag structurally.
+OptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareA terminal replay slot was preserved under a quarantine pathname.
+Optional ReadonlycauseReadonlydirectoryReadonlyquarantineReadonlyretryableWhether reconnecting and replaying can plausibly clear this failure.
+Background maintenance and checkpoint faults are parked and cleared on the +next successful batch, so a briefly full, read-only or descriptor-starved +filesystem is retryable. Structural corruption and a slot lock taken over +by another process are verdicts on the journal itself and are not. The +ingress connection lives in the browser-safe layer and cannot reference +these classes, so it reads this flag structurally.
+OptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareOptional ReadonlycauseReadonlymaxReadonlypayloadReadonlyretryableWhether reconnecting and replaying can plausibly clear this failure.
+Background maintenance and checkpoint faults are parked and cleared on the +next successful batch, so a briefly full, read-only or descriptor-starved +filesystem is retryable. Structural corruption and a slot lock taken over +by another process are verdicts on the journal itself and are not. The +ingress connection lives in the browser-safe layer and cannot reference +these classes, so it reads this flag structurally.
+OptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareReadonlybatchReadonlycolumnsReadonlyrequestReadonlyrowReadonlytableStateful decoder for connection-scoped QWP result batches.
+Decodes into one slot from a reusable batch/column-view pool without +materializing a JavaScript value array. Reusing the same slot invalidates +its prior view; callers must not reuse a slot until its consumer releases +the preceding batch.
+InternalDrops frame-backed references after a failed slot decode.
+Batch-owned reusable view delivered by QwpEgressSession.queryViews(). +Access is invalid after the callback returns. materialize() creates an +independently owned QwpResultBatch when retention is required.
+Visits rows in index order with one re-pointed row view. The callback is +synchronous; copy values that must survive the current invocation.
+InternalInvalidates the view. Normally called automatically after queryViews().
+Returns the batch-owned reusable row view pinned to rowIndex. Every call +returns the same object re-pointed at the requested row.
+Reusable, zero-copy view over one QWP result column.
+The view and every byte slice returned from it are valid only while the +surrounding queryViews() callback is running. Copy data that must outlive +the callback.
+Fixed-width stride, zero for bit-packed BOOLEAN, or -1 when variable.
+Lazily materializes one cell; prefer typed/raw accessors on hot paths.
+Zero-copy encoded ARRAY row, including dimension header.
+Zero-copy BINARY bytes.
+Zero-copy UTF-8 bytes for a VARCHAR value.
+Reusable dense-index table; only the first rowCount entries are valid.
+Raw per-row NULL bitmap, without copying. Undefined means no NULLs.
+Concatenated VARCHAR/BINARY payload bytes, without copying.
+Reusable per-row SYMBOL IDs; NULL-row entries are unspecified.
+Raw packed non-null values. Fixed-width values use QWP little-endian +layout; booleans are bit-packed and variable-width columns contain their +uint32 offset table. SYMBOL returns undefined because IDs are varints.
+Reusable row-pinned facade over a QwpResultBatchView.
+The batch owns one instance and re-points it in place. It is valid only +while the surrounding queryViews() callback is running, and must not be +retained across forEachRow() iterations. Byte and array views returned by +its accessors remain zero-copy and have the same lifetime.
+Parent batch, primarily for column metadata.
+Zero-based row currently pinned by this reusable view.
+Zero-copy encoded ARRAY row, including its dimension header.
+Zero-copy BINARY bytes.
+Zero-copy UTF-8 bytes for a VARCHAR value.
+Re-points this flyweight at a row and returns the same instance.
+A connected endpoint advertised a role that does not satisfy target.
Optionalurl: string | URLOptionalserverZone: stringOptional ReadonlycauseOptional ReadonlycloseReadonlykindOptional ReadonlyretryableOptional ReadonlyserverOptional ReadonlyserverOptionalstackOptional ReadonlystatusOptional ReadonlystatusReadonlytargetOptional ReadonlytimeoutNode opening phase that exceeded its deadline.
+Optional ReadonlytryOptional ReadonlyurlStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+True for a 421 response from a read-only replica.
+True for a 421 response from a primary still completing catch-up.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareA QWP send was rejected because its WebSocket closed.
+OptionalcloseInfo: QwpConnectionCloseInfoOptional ReadonlycauseOptional ReadonlycloseOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareA failure while handing a QWP frame to the WebSocket transport.
+Optionalcause: unknownOptional ReadonlycauseOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareThe WebSocket did not drain a QWP frame before its send deadline.
+OptionalbufferedAmountBytes: numberOptional ReadonlybufferedOptional ReadonlycauseOptionalstackReadonlytimeoutStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareBrowser-safe high-level QWP ingress API.
+Applications normally obtain this class through create/connectQwpNodeSender +or create/connectQwpBrowserSender, rather than constructing sessions and +QwpTableBuffer instances themselves.
+Highest cumulative ACK watermark, or -1n before acknowledgement.
+Highest stable frame sequence published by this sender.
+Adds a QuestDB DOUBLE[] value with between 1 and 32 dimensions.
+Discards the row in progress, including its table selection, so the next +row starts from table() again. Rows already completed stay staged.
+Commits rows previously sent by transactional auto-flush. This is an +ergonomic alias for flush(); pending local rows are included in the same +group-closing frame.
+Adds a QuestDB DATE column value in milliseconds since the epoch.
+-9_223_372_036_854_775_808n is QuestDB's DATE NULL sentinel: it is
+stored as NULL and cannot be stored as an ordinary value.
Publishes completed rows to the local ingress/replay boundary. This does +not wait for a server ACK unless awaitServerAck or awaitDurableAck is set.
+Publishes pending rows without waiting for their server ACK and returns +the highest frame sequence produced by this call, or -1n when empty. +Pass the result to waitForAcknowledged() when an explicit delivery +barrier is needed.
+Adds a QuestDB INT column value. -2_147_483_648 is QuestDB's INT NULL
+sentinel: it is stored as NULL and cannot be stored as an ordinary value.
Adds a protocol LONG[] column value with between 1 and 32 dimensions.
+Current QuestDB servers reject LONG-array ingestion with long arrays are not supported, only double arrays. This method remains available for
+Java-client and protocol parity.
Adds a QuestDB LONG column value. -9_223_372_036_854_775_808n is
+QuestDB's LONG NULL sentinel: it is stored as NULL and cannot be stored as
+an ordinary value.
InternalFlushes completed rows and resets borrower-local staging without closing +the physical session. Used by the pooled QWP client when a lease returns.
+Independently waits until the cumulative ACK watermark covers a frame.
+OptionaltimeoutMs: numberCompiles an immutable table schema into an atomic object-row writer. +The returned writer remains usable after this sender is reset.
+close() could not publish and acknowledge all committed ingress frames.
+ReadonlyacknowledgedOptionalcauseOptionalstackReadonlytargetReadonlytimeoutStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareConnection-scoped QWP symbol dictionary. IDs are dense from zero.
+Appends positionally without de-duplicating recovered entries.
+Rolls back entries added while preparing a frame that was not published.
+Mutable columnar staging area for one QWP ingress table.
+Returns null when the current row already contains this column. The first +value wins, matching the existing Sender API.
+Closes the current row and back-fills missing columns with nulls.
+Truncates every column back to the last completed row.
+Copies a completed half-open row range into an independent table buffer. +Compact column values and their null bitmaps are sliced together, so the +result can be encoded without materialising rows first.
+A reusable table-bound writer compiled from a QWP schema.
+InternalConstruct table writers with QwpSender.writer().
+Validates and atomically appends one complete object row.
+Appends a synchronous or asynchronous stream of complete object rows.
+A single encoded row cannot fit into the configured UDP datagram.
+OptionalcauseReadonlydatagramReadonlymaxReadonlyrowOptionalstackReadonlytableStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareRecovered delta frames depend on symbol IDs that neither the durable +dictionary prefix nor the surviving frames can reconstruct.
+Optionalcause: unknownOptional ReadonlycauseOptionalstackStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareA failure while establishing or validating a QWP WebSocket upgrade.
+Optional ReadonlycauseOptional ReadonlycloseReadonlykindOptional ReadonlyretryableOptional ReadonlyserverOptional ReadonlyserverOptionalstackOptional ReadonlystatusOptional ReadonlystatusOptional ReadonlytimeoutNode opening phase that exceeded its deadline.
+Optional ReadonlytryOptional ReadonlyurlStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+True for a 421 response from a read-only replica.
+True for a 421 response from a primary still completing catch-up.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareA failure while establishing or validating a QWP WebSocket upgrade.
+Optionalurl: string | URLOptional ReadonlycauseReadonlyclientOptional ReadonlycloseReadonlykindOptional ReadonlyretryableOptional ReadonlyserverReadonlyserverOptional ReadonlyserverOptionalstackOptional ReadonlystatusOptional ReadonlystatusOptional ReadonlytimeoutNode opening phase that exceeded its deadline.
+Optional ReadonlytryOptional ReadonlyurlStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+True for a 421 response from a read-only replica.
+True for a 421 response from a primary still completing catch-up.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareA complete object row failed compiled-writer validation.
+ReadonlycauseReadonlycolumnReadonlyrowOptionalstackReadonlytableStaticstackThe Error.stackTraceLimit property specifies the number of stack frames
+collected by a stack trace (whether generated by new Error().stack or
+Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
+will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will +not capture any frames.
+StaticcaptureCreates a .stack property on targetObject, which when accessed returns
+a string representing the location in the code at which
+Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`
+
+
+The first line of the trace will be prefixed with
+${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
+above constructorOpt, including constructorOpt, will be omitted from the
+generated stack trace.
The constructorOpt argument is useful for hiding implementation
+details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();
+
+
+OptionalconstructorOpt: FunctionStaticprepareThe QuestDB client's API provides methods to connect to the database, ingest data, and close the connection.
+The client supports multiple transport protocols.
+Transport Options: +
+The client supports authentication.
+Authentication details can be passed to the Sender in its configuration options.
+The client supports Basic username/password and Bearer token authentication methods when used with HTTP protocol,
+and JWK token authentication when ingesting data via TCP.
+Please, note that authentication is enabled by default in QuestDB Enterprise only.
+Details on how to configure authentication in the open source version of
+QuestDB: https://questdb.io/docs/reference/api/ilp/authenticate
+
+The client also supports TLS encryption for both, HTTP and TCP transports to provide a secure connection.
+Please, note that the open source version of QuestDB does not support TLS, and requires an external reverse-proxy,
+such as Nginx to enable encryption.
+
+The client supports multiple protocol versions for data serialization. Protocol version 1 uses text-based +serialization, while version 2 uses binary encoding for doubles and supports array columns for improved +performance. The client can automatically negotiate the protocol version with the server when using HTTP/HTTPS +by setting the protocol_version to 'auto' (default behavior). +
++The client uses a buffer to store data. It automatically flushes the buffer by sending its content to the server. +Auto flushing can be disabled via configuration options to gain control over transactions. Initial and maximum +buffer sizes can also be set. +
+
+It is recommended that the Sender is created by using one of the static factory methods,
+Sender.fromConfig(configString, extraOptions) or Sender.fromEnv(extraOptions).
+If the Sender is created via its constructor, at least the SenderOptions configuration object should be
+initialized from a configuration string to make sure that the parameters are validated.
+Detailed description of the Sender's configuration options can be found in
+the SenderOptions documentation.
+
+Transport Configuration Examples: +
+HTTP Transport Implementation:
+By default, HTTP/HTTPS transport uses the high-performance Undici library for connection management and request handling.
+For compatibility or specific requirements, you can enable the standard HTTP transport using Node.js built-in modules
+by setting stdlib_http=on in the configuration string. The standard HTTP transport provides the same functionality
+but uses Node.js http/https modules instead of Undici.
+
+Extra options can be provided to the Sender in the extraOptions configuration object.
+A custom logging function and a custom HTTP(S) agent can be passed to the Sender in this object.
+The logger implementation provides the option to direct log messages to the same place where the host application's
+log is saved. The default logger writes to the console.
+The custom HTTP(S) agent option becomes handy if there is a need to modify the default options set for the
+HTTP(S) connections. A popular setting would be disabling persistent connections, in this case an agent can be
+passed to the Sender with keepAlive set to false.
+For example: Sender.fromConfig(`http::addr=host:port`, { agent: new undici.Agent({ connect: { keepAlive: false } })})
+An undici.Agent applies only to the default HTTP(S) transport. QWP WS/WSS uses the ws package and requires
+a Node.js http.Agent/https.Agent; an incompatible top-level agent is ignored with a warning.
+If no custom agent is configured, the Sender will use its own agent which overrides some default values
+of undici.Agent. The Sender's own agent uses persistent connections with 1 minute idle timeout, pipelines requests default to 1.
+
Creates an instance of Sender.
+Sender configuration object.
+See SenderOptions documentation for detailed description of configuration options.
Highest cumulative QWP ACK watermark, or -1n when unavailable.
+Highest stable QWP frame sequence published, or -1n when unavailable.
+Writes an array column with its values into the buffer of the sender.
+Column name
+Array values to write (currently supports double arrays). A null or undefined value omits the column entirely when arrays are supported; protocol v1 rejects the call for every value.
+Returns with a reference to this sender.
+Closes the row after writing the designated timestamp. +On ILP, an invalid timestamp unit is rejected before closing begins and +leaves the row open so this method can be retried. If other validation or +encoding rejects the row before it is completed, the incomplete row and its +table selection are discarded; rows completed earlier remain staged. Start +the next row with table again. If this call triggers an auto-flush +that fails, ILP transports have already removed the entire staged batch from +the sender buffer. Applications that need to retry ILP rows must retain and +resubmit them. QWP retains successfully closed rows for its retry and replay +path.
+Precision rules:
+'ns' (nanoseconds) are sent with full nanosecond precision.
+All other timestamps are sent with microsecond precision.Designated epoch timestamp. Must be an integer or a BigInt.
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. +Supported values:
+'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsResolves after the row is closed and any triggered auto-flush completes.
+Closes the row without writing a designated timestamp. +Designated timestamp will be populated by the server on this record. +If validation or encoding rejects the row before it is completed, the +incomplete row and its table selection are discarded; rows completed +earlier remain staged. Start the next row with table again. If this +call triggers an auto-flush that fails, ILP transports have already removed +the entire staged batch from the sender buffer. Applications that need to +retry ILP rows must retain and resubmit them. QWP retains successfully +closed rows for its retry and replay path.
+Resolves after the row is closed and any triggered auto-flush completes.
+Writes a boolean column with its value into the buffer of the sender.
+Use it to insert into BOOLEAN columns.
Column name.
+Column value, accepts only boolean values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this sender.
+Closes the connection to the database. QWP publishes completed rows and +performs a bounded acknowledgement drain first. Other transports retain +their legacy behavior and require an explicit flush().
+Establishes the transport connection for TCP, TCPS, WS, WSS, and UDP. +HTTP and HTTPS connect per request and reject this call because no explicit +connection step is required.
+Resolves to true if the client is connected.
+Writes a decimal value into the buffer using the binary format.
+Use it to insert into DECIMAL database columns.
+Column name.
+The unscaled value of the decimal in two's +complement representation and big-endian byte order. +A null or undefined value omits the column entirely when decimals are +supported; ILP protocol v1/v2 reject the call for every value. +An empty array also represents NULL, but the two are not encoded alike: +on the ILP transports an empty array writes an explicit NULL decimal +field, while the QWP transports omit the column exactly as they do for +null. QuestDB records NULL either way for a column that already exists.
+The scale of the decimal value.
+Returns with a reference to this buffer.
+Writes a decimal value into the buffer using the text format.
+Use it to insert into DECIMAL database columns.
+Column name.
+Column value, accepts only number/string values. A null or undefined value omits the column entirely when decimals are supported; ILP protocol v1/v2 reject the call for every value.
+Returns with a reference to this buffer.
+Writes a 64-bit floating point value into the buffer of the sender.
+Use it to insert into DOUBLE or FLOAT database columns.
Column name.
+Column value, accepts only number values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this sender.
+Sends the content of the sender's buffer to the database and compacts the buffer. +If the last row is not finished it stays in the sender's buffer.
+Resolves to true when there was data in the buffer to send, and it was sent successfully.
+Flushes pending rows and returns the highest QWP frame sequence published +by this call. Non-QWP transports flush normally and return -1n because +they do not expose frame sequences.
+Writes a 64-bit signed integer into the buffer of the sender.
+Use it to insert into LONG, INT, SHORT and BYTE columns.
Column name.
+Column value, accepts only number values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this sender.
+Resets the sender's buffer, data sitting in the buffer will be lost.
+In other words it clears the buffer, and sets the writing position to the beginning of the buffer.
Returns with a reference to this sender.
+Writes a string column with its value into the buffer of the sender.
+Use it to insert into VARCHAR and STRING columns.
Column name.
+Column value, accepts only string values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this sender.
+Writes a symbol name and value into the buffer of the sender.
+Use it to insert into SYMBOL columns.
Symbol name.
+Symbol value, toString() is called to extract the actual symbol value from the parameter. A null or undefined value omits the symbol entirely (stored as NULL).
+Returns with a reference to this sender.
+Writes the table name into the buffer of the sender of the sender.
+Table name.
+Returns with a reference to this sender.
+Writes a timestamp column and its value into the buffer of the sender.
+Use this method to insert data into TIMESTAMP or TIMESTAMP_NS columns.
Precision rules:
+'ns' (nanoseconds) are sent with full nanosecond precision.
+All other timestamps are sent with microsecond precision.The column name.
+The epoch timestamp. Must be an integer or a BigInt. A null or undefined value omits the column entirely (stored as NULL).
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. +Supported values:
+'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
+Waits independently for a cumulative QWP ACK watermark.
+OptionaltimeoutMs: numberCompiles a table-bound object-row writer for QWP transports. +Legacy ILP transports continue to use the fluent row API.
+StaticfromCreates a Sender object by parsing the provided configuration string.
+Configuration string.
OptionalextraOptions: ExtraOptionsOptional extra configuration.
A Sender object initialized from the provided configuration string.
+StaticfromCreates a Sender object by parsing the configuration string set in the QDB_CLIENT_CONF environment variable.
+OptionalextraOptions: ExtraOptionsOptional extra configuration.
A Sender object initialized from the QDB_CLIENT_CONF environment variable.
+Buffer implementation for protocol version 1.
+Sends floating point numbers in their text form.
Creates a new SenderBufferV1 instance.
+Sender configuration object.
+See SenderOptions documentation for detailed description of configuration options.
Array columns are not supported in protocol v1.
+The capability check applies even when the value is null or undefined.
Column name.
+Array values.
+Returns with a reference to this buffer.
+Closes the row after writing the designated timestamp into the buffer.
+Precision rules:
+'ns' (nanoseconds) are sent with full nanosecond precision.
+All other timestamps are sent with microsecond precision.Designated epoch timestamp. Must be an integer or a BigInt.
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. +Supported values:
+'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
+Closes the row without writing designated timestamp into the buffer.
+Designated timestamp will be populated by the server on this record.
Writes a boolean column with its value into the buffer.
+Use it to insert into BOOLEAN columns.
Column name.
+Column value, accepts only boolean values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this buffer.
+ProtectedcheckChecks if the buffer has sufficient capacity for additional data and resizes if needed.
+Array of strings to calculate the required capacity for
+Base number of bytes to add to the calculation
+Returns the current position of the buffer.
+New data will be written into the buffer starting from this position.
Writes a decimal value into the buffer using its binary format.
+Use it to insert into DECIMAL database columns.
+Decimals are not supported by protocol v1/v2, so this base implementation +rejects the call even when the value is null or undefined. Protocol v3 +overrides this with a validating implementation.
+Column name.
+The unscaled +integer portion of the decimal value.
+The number of fractional digits (the scale) of the decimal value.
+Returns with a reference to this buffer.
+Writes a decimal value into the buffer using its text format.
+Use it to insert into DECIMAL database columns.
+Decimals are not supported by protocol v1/v2, so this base implementation +rejects the call even when the value is null or undefined. Protocol v3 +overrides this with a validating implementation.
+Column name.
+The decimal value to +write.
+Returns with a reference to this buffer.
+Writes a 64-bit floating point value into the buffer using v1 serialization (text format).
+Use it to insert into DOUBLE or FLOAT database columns.
Column name.
+Column value, accepts only number values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this sender.
+Writes a 64-bit signed integer into the buffer.
+Use it to insert into LONG, INT, SHORT and BYTE columns.
Column name.
+Column value, accepts only number values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this buffer.
+Resets the buffer, data sitting in the buffer will be lost.
+In other words it clears the buffer, and sets the writing position to the beginning of the buffer.
Returns with a reference to this buffer.
+Writes a string column with its value into the buffer.
+Use it to insert into VARCHAR and STRING columns.
Column name.
+Column value, accepts only string values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this buffer.
+Writes a symbol name and value into the buffer.
+Use it to insert into SYMBOL columns.
Symbol name.
+Symbol value, toString() is called to extract the actual symbol value from the parameter. A null or undefined value omits the symbol entirely (stored as NULL).
+Returns with a reference to this buffer.
+Writes the table name into the buffer.
+Table name.
+Returns with a reference to this buffer.
+Writes a timestamp column and its value into the buffer.
+Use this method to insert data into TIMESTAMP or TIMESTAMP_NS columns.
Precision rules:
+'ns' (nanoseconds) are sent with full nanosecond precision.
+All other timestamps are sent with microsecond precision.The column name.
+The epoch timestamp. Must be an integer or a BigInt. A null or undefined value omits the column entirely (stored as NULL).
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. +Supported values:
+'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
+Returns a cropped buffer ready to send to the server, or null if there is nothing to send.
+The returned buffer is a copy of this buffer.
+It also compacts the buffer.
Returns a cropped buffer, or null if there is nothing to send.
+The returned buffer is backed by this buffer instance, meaning the view can change as the buffer is mutated.
+Used only in tests to assert the buffer's content.
ProtectedwriteBuffer implementation for protocol version 2.
+Sends floating point numbers in binary form, and provides support for arrays.
Creates a new SenderBufferV2 instance.
+Sender configuration object.
+See SenderOptions documentation for detailed description of configuration options.
Write an array column with its values into the buffer using v2 format.
+Column name
+Array values to write (currently supports double arrays). A null or undefined value omits the column entirely, storing NULL.
+Returns with a reference to this buffer.
+Closes the row after writing the designated timestamp into the buffer.
+Precision rules:
+'ns' (nanoseconds) are sent with full nanosecond precision.
+All other timestamps are sent with microsecond precision.Designated epoch timestamp. Must be an integer or a BigInt.
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. +Supported values:
+'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
+Closes the row without writing designated timestamp into the buffer.
+Designated timestamp will be populated by the server on this record.
Writes a boolean column with its value into the buffer.
+Use it to insert into BOOLEAN columns.
Column name.
+Column value, accepts only boolean values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this buffer.
+ProtectedcheckChecks if the buffer has sufficient capacity for additional data and resizes if needed.
+Array of strings to calculate the required capacity for
+Base number of bytes to add to the calculation
+Returns the current position of the buffer.
+New data will be written into the buffer starting from this position.
Writes a decimal value into the buffer using its binary format.
+Use it to insert into DECIMAL database columns.
+Decimals are not supported by protocol v1/v2, so this base implementation +rejects the call even when the value is null or undefined. Protocol v3 +overrides this with a validating implementation.
+Column name.
+The unscaled +integer portion of the decimal value.
+The number of fractional digits (the scale) of the decimal value.
+Returns with a reference to this buffer.
+Writes a decimal value into the buffer using its text format.
+Use it to insert into DECIMAL database columns.
+Decimals are not supported by protocol v1/v2, so this base implementation +rejects the call even when the value is null or undefined. Protocol v3 +overrides this with a validating implementation.
+Column name.
+The decimal value to +write.
+Returns with a reference to this buffer.
+Writes a 64-bit floating point value into the buffer using v2 serialization (binary format).
+Use it to insert into DOUBLE or FLOAT database columns.
Column name.
+Column value, accepts only number values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this buffer.
+Writes a 64-bit signed integer into the buffer.
+Use it to insert into LONG, INT, SHORT and BYTE columns.
Column name.
+Column value, accepts only number values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this buffer.
+Resets the buffer, data sitting in the buffer will be lost.
+In other words it clears the buffer, and sets the writing position to the beginning of the buffer.
Returns with a reference to this buffer.
+Writes a string column with its value into the buffer.
+Use it to insert into VARCHAR and STRING columns.
Column name.
+Column value, accepts only string values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this buffer.
+Writes a symbol name and value into the buffer.
+Use it to insert into SYMBOL columns.
Symbol name.
+Symbol value, toString() is called to extract the actual symbol value from the parameter. A null or undefined value omits the symbol entirely (stored as NULL).
+Returns with a reference to this buffer.
+Writes the table name into the buffer.
+Table name.
+Returns with a reference to this buffer.
+Writes a timestamp column and its value into the buffer.
+Use this method to insert data into TIMESTAMP or TIMESTAMP_NS columns.
Precision rules:
+'ns' (nanoseconds) are sent with full nanosecond precision.
+All other timestamps are sent with microsecond precision.The column name.
+The epoch timestamp. Must be an integer or a BigInt. A null or undefined value omits the column entirely (stored as NULL).
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. +Supported values:
+'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
+Returns a cropped buffer ready to send to the server, or null if there is nothing to send.
+The returned buffer is a copy of this buffer.
+It also compacts the buffer.
Returns a cropped buffer, or null if there is nothing to send.
+The returned buffer is backed by this buffer instance, meaning the view can change as the buffer is mutated.
+Used only in tests to assert the buffer's content.
ProtectedwriteBuffer implementation for protocol version 3.
+Provides support for decimals.
+Creates a new SenderBufferV3 instance.
+Sender configuration object.
+See SenderOptions documentation for detailed description of configuration options.
+Write an array column with its values into the buffer using v2 format.
+Column name
+Array values to write (currently supports double arrays). A null or undefined value omits the column entirely, storing NULL.
+Returns with a reference to this buffer.
+Closes the row after writing the designated timestamp into the buffer.
+Precision rules:
+'ns' (nanoseconds) are sent with full nanosecond precision.
+All other timestamps are sent with microsecond precision.Designated epoch timestamp. Must be an integer or a BigInt.
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. +Supported values:
+'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
+Closes the row without writing designated timestamp into the buffer.
+Designated timestamp will be populated by the server on this record.
Writes a boolean column with its value into the buffer.
+Use it to insert into BOOLEAN columns.
Column name.
+Column value, accepts only boolean values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this buffer.
+ProtectedcheckChecks if the buffer has sufficient capacity for additional data and resizes if needed.
+Array of strings to calculate the required capacity for
+Base number of bytes to add to the calculation
+Returns the current position of the buffer.
+New data will be written into the buffer starting from this position.
Writes a decimal value into the buffer using its binary format.
+Use it to insert into DECIMAL database columns.
+Column name.
+The unscaled integer portion of the decimal value.
+bigint is provided, it will be converted automatically.Int8Array is provided, it must contain the two’s complement representation
+of the unscaled value in big-endian byte order.Int8Array represents a NULL value.The number of fractional digits (the scale) of the decimal value.
+Returns with a reference to this buffer.
+Writes a decimal value into the buffer using its text format.
+Use it to insert into DECIMAL database columns.
+Column name.
+The decimal value to write.
+number or a string containing a valid decimal representation."123.45" or "-0.001").Returns with a reference to this buffer.
+Writes a 64-bit floating point value into the buffer using v2 serialization (binary format).
+Use it to insert into DOUBLE or FLOAT database columns.
Column name.
+Column value, accepts only number values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this buffer.
+Writes a 64-bit signed integer into the buffer.
+Use it to insert into LONG, INT, SHORT and BYTE columns.
Column name.
+Column value, accepts only number values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this buffer.
+Resets the buffer, data sitting in the buffer will be lost.
+In other words it clears the buffer, and sets the writing position to the beginning of the buffer.
Returns with a reference to this buffer.
+Writes a string column with its value into the buffer.
+Use it to insert into VARCHAR and STRING columns.
Column name.
+Column value, accepts only string values. A null or undefined value omits the column entirely (stored as NULL).
+Returns with a reference to this buffer.
+Writes a symbol name and value into the buffer.
+Use it to insert into SYMBOL columns.
Symbol name.
+Symbol value, toString() is called to extract the actual symbol value from the parameter. A null or undefined value omits the symbol entirely (stored as NULL).
+Returns with a reference to this buffer.
+Writes the table name into the buffer.
+Table name.
+Returns with a reference to this buffer.
+Writes a timestamp column and its value into the buffer.
+Use this method to insert data into TIMESTAMP or TIMESTAMP_NS columns.
Precision rules:
+'ns' (nanoseconds) are sent with full nanosecond precision.
+All other timestamps are sent with microsecond precision.The column name.
+The epoch timestamp. Must be an integer or a BigInt. A null or undefined value omits the column entirely (stored as NULL).
Optionalunit: TimestampUnit = "us"The time unit of the timestamp. +Supported values:
+'ns' — nanoseconds (requires BigInt)'us' — microseconds (default)'ms' — millisecondsReturns with a reference to this buffer.
+Returns a cropped buffer ready to send to the server, or null if there is nothing to send.
+The returned buffer is a copy of this buffer.
+It also compacts the buffer.
Returns a cropped buffer, or null if there is nothing to send.
+The returned buffer is backed by this buffer instance, meaning the view can change as the buffer is mutated.
+Used only in tests to assert the buffer's content.
ProtectedwriteSender configuration options.
+
+Properties of the object are initialized through a configuration string.
+The configuration string has the following format: protocol::key=value;key=value...
+The keys are case-sensitive, the trailing semicolon is optional.
+The values are validated and an error is thrown if the format is invalid.
+
+Connection and protocol options
Creates a Sender options object by parsing the provided configuration string.
+Configuration string.
OptionalextraOptions: ExtraOptionsOptional extra configuration.
OptionaladdrOptionalagentOptionalauthOptionalauto_Optionalauto_Optionalauto_Optionalauto_OptionalhostOptionalinit_OptionaljwkOptionallogOptionalmax_Optionalmax_Optionalmax_Optionalmulticast_OptionalpasswordOptionalportOptionalprotocol_OptionalqwpOptionalrequest_Optionalrequest_Optionalretry_Optionalstdlib_Optionaltls_Optionaltls_Optionaltls_Optionaltls_OptionaltokenOptionaltoken_Optionaltoken_OptionalusernameStaticfromCreates a Sender options object by parsing the provided configuration string.
+Configuration string.
OptionalextraOptions: ExtraOptionsOptional extra configuration.
A Sender configuration object initialized from the provided configuration string.
+StaticfromCreates a Sender options object by parsing the configuration string set in the QDB_CLIENT_CONF environment variable.
+OptionalextraOptions: ExtraOptionsOptional extra configuration.
A Sender configuration object initialized from the QDB_CLIENT_CONF environment variable.
+StaticresolveResolves the protocol version, if it is set to 'auto'.
+If TCP transport is used, the protocol version will default to 1.
+In case of HTTP transport the /settings endpoint of the database is used to find the protocol versions
+supported by the server, and the highest will be selected.
+When calling the /settings endpoint the timeout and TLS options are used from the options object.
SenderOptions instance needs resolving protocol version
+StaticresolveTCP transport implementation.
+Supports both plain TCP or secure TLS-encrypted connections with configurable JWK token authentication.
Creates a new TcpTransport instance.
+Sender configuration object containing connection and authentication details
+HTTP transport implementation using the Undici library.
+Provides high-performance HTTP requests with connection pooling and retry logic.
+Supports both HTTP and HTTPS protocols with configurable authentication.
Creates a new UndiciTransport instance.
+Sender configuration object containing connection and retry settings
+Protected ReadonlyhostProtected ReadonlylogProtected ReadonlypasswordProtected ReadonlyportProtected ReadonlyrequestProtected ReadonlyrequestProtected ReadonlyretryProtected ReadonlysecureProtected ReadonlytlsProtected ReadonlytlsProtected ReadonlytokenProtected ReadonlyusernameDefines a QuestDB BINARY column. Inputs are copied on append.
+Defines a QuestDB BOOLEAN column.
+Authenticates over REST and asks QuestDB to issue the HttpOnly cookies a
+browser needs before opening QWP WebSockets. REST and OIDC tokens both use
+Bearer authentication. When serviceAccount is present the same request
+also creates Enterprise's qdbServiceAccount impersonation cookie.
Defines a signed 8-bit QuestDB BYTE column.
+Defines a QuestDB CHAR column. Inputs are one UTF-16 code unit.
+Creates and prewarms a combined browser QWP ingress/egress client.
+Opens a browser WebSocket and waits for the egress SERVER_INFO handshake.
+Optionalsignal: AbortSignalCancels an opening connection during pooled-client shutdown.
+Opens a browser WebSocket and starts an ingress ACK/NACK session.
+Optionalsignal: AbortSignalCancels a first connect still negotiating; see QwpIngressSession.connect.
+Opens a browser QWP connection and returns a fluent sender.
+Opens a QWP-capable browser WebSocket.
+Browsers cannot set Authorization or X-QWP-* upgrade headers. QuestDB accepts +browser upgrades when Origin and Host have the same authority, so serve the +app from the QuestDB origin or route QWP through a same-origin reverse proxy. +When authentication is enabled, pass sessionBootstrap or call +bootstrapQwpBrowserSession first so the browser can attach qdb_session.
+Creates a lazy browser QWP client with bounded sender and query pools.
+Creates a stateful browser endpoint walker suitable for session reconnects.
+Creates a browser-safe fluent QWP sender without opening the WebSocket yet. +Call connect(), or let the first flush connect lazily.
+OptionalquarantinedPath: stringOmitted when the bytes were abandoned rather than preserved on disk.
+OptionalfromFsn: bigintDefines a QuestDB DATE column. Inputs are milliseconds since the epoch.
+-9_223_372_036_854_775_808n is QuestDB's DATE NULL sentinel: it is stored
+as NULL and cannot be stored as an ordinary DATE value.
Defines a QuestDB DECIMAL128 column of fixed scale, up to 38.
+Defines a QuestDB DECIMAL256 column of fixed scale, up to 76.
+Defines a QuestDB DECIMAL64 column of fixed scale, up to 18.
+Parses the server's X-QWP-Content-Encoding response. Unknown values remain
+observable but do not claim that Zstd was negotiated; RESULT_BATCH flags
+remain authoritative for each individual batch.
Decodes one QWP-framed server-to-client egress message.
+Decodes an ingress ACK, durable ACK, or NACK WebSocket payload.
+Reads the connection-scoped dictionary prefix from a delta ingress frame.
+Browser-safe fallback for asynchronous ingress rejections and abandoned
+persistent data. Applications can replace it with onSenderError.
Defines the writer's required designated timestamp field.
+Defines a 64-bit QuestDB DOUBLE column. Alias of float64.
+Defines a QuestDB DOUBLE[] column with between 1 and 32 dimensions.
+Builds the Node upgrade header for an egress compression preference.
+Runs a setter callback and returns the exact QUERY_REQUEST bind section.
+Optionaldictionary: QwpSymbolDictionaryEncodes one QWP v1 ingress message.
+Encodes the unframed client-to-server QUERY_REQUEST payload.
+Defines a 32-bit QuestDB FLOAT column.
+Defines a 64-bit QuestDB DOUBLE column.
+Defines a QuestDB GEOHASH column of fixed precision.
+Precision in bits, 1 through 60. Base-32 text inputs
+carry five bits per character, so geohash(20) accepts four characters.
Defines a signed 32-bit QuestDB INT column.
+-2_147_483_648 is QuestDB's INT NULL sentinel: it is stored as NULL and
+cannot be stored as an ordinary INT value.
Defines a signed 64-bit QuestDB LONG column. Inputs must be bigint.
+-9_223_372_036_854_775_808n is QuestDB's LONG NULL sentinel: it is stored
+as NULL and cannot be stored as an ordinary LONG value.
Defines a QuestDB IPV4 column. 0.0.0.0 is the NULL sentinel.
Defines a signed 64-bit QuestDB LONG column. Alias of int64.
+Defines a QuestDB LONG256 column.
+Defines a protocol LONG[] column with between 1 and 32 dimensions.
+Current QuestDB servers reject LONG-array ingestion with long arrays are not supported, only double arrays. This descriptor remains available for
+Java-client and protocol parity.
Defines a signed 16-bit QuestDB SHORT column.
+Defines a string-valued QuestDB SYMBOL column.
+Defines a regular timestamp column with an explicit input unit.
+Defines a QuestDB UUID column.
+Defines a string-valued QuestDB VARCHAR column.
+Writes an unsigned LEB128 uint64.
+Converts a bigint into a two's complement big-endian byte array. +Produces the minimal-width representation that preserves the sign.
+The value to serialise
+Byte array in big-endian order
+Defines a QuestDB BINARY column. Inputs are copied on append.
+Defines a QuestDB BOOLEAN column.
+Defines a signed 8-bit QuestDB BYTE column.
+Defines a QuestDB CHAR column. Inputs are one UTF-16 code unit.
+Creates and prewarms a combined Node QWP ingress/egress client.
+Creates and prewarms a combined Node QWP ingress/egress client.
+OptionalextraOptions: QwpNodeClientConfigOptionsOpens a Node WebSocket and waits for the egress SERVER_INFO handshake.
+Optionalsignal: AbortSignalCancels an opening connection during pooled-client shutdown.
+Opens a Node WebSocket and starts an ingress ACK/NACK session.
+Optionalsignal: AbortSignalCancels a first connect still negotiating; see QwpIngressSession.connect.
+Opens a Node QWP connection and returns a fluent sender.
+Opens a Node IPv4 UDP socket for fire-and-forget QWP ingress.
+Opens a Node UDP socket and returns a fluent fire-and-forget QWP sender.
+Opens a Node QWP WebSocket with the upgrade headers required by QuestDB.
+Factory function to create a SenderBuffer instance based on the protocol version.
+Sender configuration object. +See SenderOptions documentation for detailed description of configuration options.
+A SenderBuffer instance appropriate for the specified protocol version
+OptionalquarantinedPath: stringOmitted when the bytes were abandoned rather than preserved on disk.
+Creates a lazy Node QWP client with bounded sender and query pools.
+Creates a lazy Node QWP client with bounded sender and query pools.
+OptionalextraOptions: QwpNodeClientConfigOptionsCreates a stateful Node endpoint walker suitable for session reconnects.
+Creates a fluent Node QWP sender without opening the WebSocket yet. +Call connect(), or let the first flush connect lazily.
+Creates a fluent Node QWP-over-UDP sender without opening its socket yet. +UDP has no authentication, server ACK, durable ACK, transaction, retry, or +store-and-forward semantics.
+OptionalfromFsn: bigintFactory function to create appropriate transport instance based on configuration.
+Sender configuration options including protocol and connection details
+Transport instance appropriate for the specified protocol
+Defines a QuestDB DATE column. Inputs are milliseconds since the epoch.
+-9_223_372_036_854_775_808n is QuestDB's DATE NULL sentinel: it is stored
+as NULL and cannot be stored as an ordinary DATE value.
Defines a QuestDB DECIMAL128 column of fixed scale, up to 38.
+Defines a QuestDB DECIMAL256 column of fixed scale, up to 76.
+Defines a QuestDB DECIMAL64 column of fixed scale, up to 18.
+Parses the server's X-QWP-Content-Encoding response. Unknown values remain
+observable but do not claim that Zstd was negotiated; RESULT_BATCH flags
+remain authoritative for each individual batch.
Decodes one QWP-framed server-to-client egress message.
+Decodes an ingress ACK, durable ACK, or NACK WebSocket payload.
+Reads the connection-scoped dictionary prefix from a delta ingress frame.
+Browser-safe fallback for asynchronous ingress rejections and abandoned
+persistent data. Applications can replace it with onSenderError.
Defines the writer's required designated timestamp field.
+Defines a 64-bit QuestDB DOUBLE column. Alias of float64.
+Defines a QuestDB DOUBLE[] column with between 1 and 32 dimensions.
+Builds the Node upgrade header for an egress compression preference.
+Runs a setter callback and returns the exact QUERY_REQUEST bind section.
+Optionaldictionary: QwpSymbolDictionaryEncodes one QWP v1 ingress message.
+Encodes the unframed client-to-server QUERY_REQUEST payload.
+Defines a 32-bit QuestDB FLOAT column.
+Defines a 64-bit QuestDB DOUBLE column.
+Defines a QuestDB GEOHASH column of fixed precision.
+Precision in bits, 1 through 60. Base-32 text inputs
+carry five bits per character, so geohash(20) accepts four characters.
Defines a signed 32-bit QuestDB INT column.
+-2_147_483_648 is QuestDB's INT NULL sentinel: it is stored as NULL and
+cannot be stored as an ordinary INT value.
Defines a signed 64-bit QuestDB LONG column. Inputs must be bigint.
+-9_223_372_036_854_775_808n is QuestDB's LONG NULL sentinel: it is stored
+as NULL and cannot be stored as an ordinary LONG value.
Defines a QuestDB IPV4 column. 0.0.0.0 is the NULL sentinel.
Defines a signed 64-bit QuestDB LONG column. Alias of int64.
+Defines a QuestDB LONG256 column.
+Defines a protocol LONG[] column with between 1 and 32 dimensions.
+Current QuestDB servers reject LONG-array ingestion with long arrays are not supported, only double arrays. This descriptor remains available for
+Java-client and protocol parity.
Resolves and validates one ws/wss configuration string for both QWP sides.
+Returns child replay slots containing unacknowledged records.
+The scan is deliberately read-only and does not inspect lock ownership. +Adoption obtains the replay store's exclusive lock, closing the race with a +live foreground producer or another drainer.
+OptionalexcludeSlot: (slotName: string) => booleanDefines a signed 16-bit QuestDB SHORT column.
+Defines a string-valued QuestDB SYMBOL column.
+Defines a regular timestamp column with an explicit input unit.
+Defines a QuestDB UUID column.
+Defines a string-valued QuestDB VARCHAR column.
+Writes an unsigned LEB128 uint64.
+Converts a bigint into a two's complement big-endian byte array. -Produces the minimal-width representation that preserves the sign.
-The value to serialise
-Byte array in big-endian order
-Factory function to create a SenderBuffer instance based on the protocol version.
-Sender configuration object. -See SenderOptions documentation for detailed description of configuration options.
-A SenderBuffer instance appropriate for the specified protocol version
-Factory function to create appropriate transport instance based on configuration.
-Sender configuration options including protocol and connection details
-Transport instance appropriate for the specified protocol
-# With npm
npm i -s @questdb/nodejs-client
# With yarn
yarn add @questdb/nodejs-client
# With pnpm
pnpm add @questdb/nodejs-client
+QuestDB JavaScript Client - v4.2.0 QuestDB JavaScript Client - v4.2.0
QuestDB JavaScript Client
This repository builds two runtime-specific npm packages from a shared private
+core: @questdb/nodejs-client for Node.js and @questdb/browser-client for
+browsers. The browser package exposes its complete API from its package root and
+does not include Node.js transports or dependencies.
+Installation
# With npm
npm i -s @questdb/nodejs-client
# With yarn
yarn add @questdb/nodejs-client
# With pnpm
pnpm add @questdb/nodejs-client
+
+
+For browser applications:
+npm install @questdb/browser-client
Compatibility table
@@ -28,25 +36,252 @@ Compatibility tablestdlib_http option to switch to the standard HTTP/HTTPS modules.
Configuration options
Detailed description of the client's configuration options can be found in
-the SenderOptions documentation.
+the SenderOptions documentation.
Examples
The examples below demonstrate how to use the client.
-For more details, please, check the Sender's documentation.
+For more details, see the Sender documentation.
Basic API usage
import { Sender } from "@questdb/nodejs-client";
async function run() {
// create a sender using HTTP protocol
const sender = await Sender.fromConfig("http::addr=127.0.0.1:9000");
// add rows to the buffer of the sender
await sender
.table("trades")
.symbol("symbol", "BTC-USD")
.symbol("side", "sell")
.floatColumn("price", 39269.98)
.floatColumn("amount", 0.011)
.at(Date.now(), "ms");
// flush the buffer of the sender, sending the data to QuestDB
// the buffer is cleared after the data is sent, and the sender is ready to accept new data
await sender.flush();
// close the connection after all rows ingested
// unflushed data will be lost
await sender.close();
}
run().then(console.log).catch(console.error);
-Authentication and secure connection
Username and password authentication with HTTP transport
import { Sender } from "@questdb/nodejs-client";
async function run() {
// authentication details
const USER = "admin";
const PWD = "quest";
// pass the authentication details to the sender
// for secure connection use 'https' protocol instead of 'http'
const sender = await Sender.fromConfig(
`http::addr=127.0.0.1:9000;username=${USER};password=${PWD}`
);
// add rows to the buffer of the sender
await sender
.table("trades")
.symbol("symbol", "ETH-USD")
.symbol("side", "sell")
.floatColumn("price", 2615.54)
.floatColumn("amount", 0.00044)
.at(Date.now(), "ms");
// flush the buffer of the sender, sending the data to QuestDB
await sender.flush();
// close the connection after all rows ingested
await sender.close();
}
run().catch(console.error);
+Null and undefined values
Passing null or undefined as a column or symbol value omits that column from
+the row, and QuestDB records the omission as NULL. This is the model the QuestDB
+clients share — the Java client puts it as "to mark the value NULL, omit the
+column from the row" — with the JavaScript client doing the omission for you, so a
+record with optional fields needs no branching:
+const trade: { side?: string; amount?: number } = { amount: 0.011 };
await sender
.table("trades")
.symbol("symbol", "BTC-USD")
.symbol("side", trade.side) // undefined -> column omitted -> NULL
.floatColumn("price", 39269.98)
.floatColumn("amount", trade.amount)
.at(Date.now(), "ms");
// wire: trades,symbol=BTC-USD price=39269.98,amount=0.011 <timestamp>
+
+
+The eight column methods on Sender follow this rule for both ILP
+(http/https/tcp/tcps) and QWP (ws/wss/udp) transports, subject to
+protocol support. The broader direct QwpSender API and compiled QWP writers
+follow the same omission rule for their additional column types. Capability
+checks still run for nullish values: ILP v1 always rejects arrayColumn, and ILP
+v1/v2 always reject the decimal column methods. The QWP-only
+QwpSender.long256Column method spreads one value over four arguments; it omits
+the column when all four words are nullish and rejects a partial set rather
+than treating it as NULL.
+Three consequences are worth knowing:
+
+- An omitted column is not created on a table that does not already have it. The
+omission carries no type, so schema-on-write has nothing to infer from.
+- A row in which every value is nullish behaves differently per protocol. ILP
+has no way to encode a row with no fields, so
at()/atNow() rejects it with
+"The row must have a symbol or column set before it is closed". QWP is
+columnar and can express it, so the row is sent with no columns — carrying
+only its designated timestamp.
+- A rejected
at()/atNow() on ILP discards the row it could not close,
+including its table name, and leaves rows already in the buffer alone. Catch
+the error and start the next row from table(); there is no need to reset()
+and nothing already buffered is lost. The exception is an invalid designated
+timestamp unit: it is rejected before closing begins, leaving the row open so
+at() can be retried with a valid unit. If an ILP auto-flush send fails, the
+completed batch has already been removed from the sender buffer; applications
+that need to retry must retain and resubmit those rows. QWP keeps successfully
+closed rows for its retry and replay path.
+
+Changed in this release. Earlier versions threw a type error for most
+nullish values, and protocol v2 encoded arrayColumn(name, null) as an explicit
+NULL array marker. Supported column methods now omit the column instead. If your
+code relied on the throw as a data-quality guard, validate before calling the
+sender.
+QWP ingress from Node.js or a browser
See the complete QWP guide for ingress and egress APIs, the combined
+pooled client, browser authentication, delivery semantics, migration guidance, and
+the public API policy.
+Node.js applications can select QWP through the regular Sender API:
+import { Sender } from "@questdb/nodejs-client";
const sender = await Sender.fromConfig("ws::addr=127.0.0.1:9000");
await sender.connect();
await sender
.table("trades")
.symbol("symbol", "ETH-USD")
.floatColumn("price", 2615.54)
.at(Date.now(), "ms");
await sender.flush();
await sender.close();
+
+
+For repeated object rows, compile the table schema once. The resulting writer
+validates each complete row before changing sender state and accepts both individual
+rows and synchronous or asynchronous iterables:
+import * as qwp from "@questdb/nodejs-client";
const trades = sender.writer("trades", {
symbol: qwp.symbol(),
side: qwp.symbol(),
price: qwp.double(),
quantity: qwp.long(),
timestamp: qwp.designatedTimestamp("ns"),
});
await trades.row({
symbol: "ETH-USD",
side: "sell",
price: 2615.54,
quantity: 42n,
timestamp: 1_723_000_000_000_000_000n,
});
await trades.rows(moreTrades);
+
+
+The schema vocabulary covers every QuestDB column type the fluent row API can write,
+including date(), char(), binary(), uuid(), long256(), ipv4(),
+geohash(precisionBits), decimal64/128/256(scale), doubleArray(), and
+longArray(). See QWP.md for the accepted value
+forms of each field.
+The regular Sender accepts the same unified QWP configuration vocabulary as
+the pooled Node client. Use comma-separated or repeated addr values for
+failover; standalone ingress validates but otherwise ignores egress- and
+pool-only keys.
+Node.js also supports fire-and-forget QWP-over-UDP through the same API:
+const sender = await Sender.fromConfig(
"udp::addr=239.1.2.3:9007;max_datagram_size=1400;multicast_ttl=1",
);
await sender.connect();
await sender
.table("trades")
.symbol("symbol", "ETH-USD")
.floatColumn("price", 2615.54)
.atNow();
await sender.close();
+
+
+UDP datagrams are self-contained and split at row boundaries. UDP has no
+authentication, acknowledgements, transactions, retry, or store-and-forward and is
+not available in browsers. See the QWP guide for the lower-level Node UDP API.
+QWP flush() resolves at the local publication boundary by default in both
+Node.js and browsers, matching the Java QWP sender. Set
+qwp.sender.awaitServerAck: true to wait for QuestDB's protocol ACK instead,
+or awaitDurableAck: true to wait through durable upload. When Node QWP is
+configured with qwp.webSocket.storeAndForward, the publication boundary is
+the local durable journal, so the sender can accept flushes while QuestDB is
+offline and a background drainer reconnects and sends them in order.
+Set initialConnectMode to "off" (the default), "sync", or "async" to
+choose fail-fast, bounded blocking, or background startup. Supplying reconnect
+budget settings without an explicit mode promotes initial startup to "sync",
+matching the Java client. The configuration-string
+equivalent is initial_connect_retry, used together with the store-and-forward
+options in extraOptions.qwp.
+Persistent frames are coalesced into fixed-size 4 MiB .sfa segments by default,
+using the shared Java/Rust/Python SFA envelope, manifest, ACK watermark, and symbol
+dictionary formats. The active segment and a pre-sized temporary hot spare keep open
+handles. A shared worker provisions spares, checkpoints files, and trims acknowledged
+segments. Recovery keeps only frame offsets in memory and reads payloads from disk as
+they are sent, so a large persisted backlog is not duplicated on the JavaScript heap.
+Set drainOrphans: true when sibling journal directories share a dedicated parent:
+the Node client scans and drains slots left by failed producer processes with bounded
+concurrency. Pooled QWP clients recover idle in-range and out-of-range sender-N
+slots automatically without raising senderPoolMin, including leftovers after
+senderPoolMax is reduced. Terminally bad slots are marked .failed for inspection
+and can be re-enabled with
+retryQwpNodeOrphanSlot(). This persistent mode is Node-only; browser senders
+use the in-memory replay boundary.
+Browser applications use the browser entry point, which has no Node.js
+dependencies. Cookies are supplied by the browser during a same-origin
+WebSocket upgrade. Browser and non-persistent Node ingress reconnect by default and
+retain unacknowledged frames in memory; set reconnect: false in the session options
+for a fixed connection. Only Node store-and-forward survives process failure.
+import { connectQwpBrowserSender } from "@questdb/browser-client";
const url = new URL("/write/v4", location.href);
url.protocol = location.protocol === "https:" ? "wss:" : "ws:";
const sender = await connectQwpBrowserSender({ url }, { autoFlush: false });
await sender.table("events").longColumn("value", 42n).atNow();
await sender.flush();
await sender.close();
+
+
+For batches larger than the automatic flush threshold, transactional mode
+keeps each auto-flushed frame in an open server-side transaction. An explicit
+flush() (or its commit() alias) publishes the group-closing frame. Set
+awaitServerAck: true, or wait on the sequence returned by
+flushAndGetSequence(), when the call must also observe the cumulative ACK.
+QuestDB guarantees this atomicity per table; a flush that contains multiple
+tables is not one cross-table transaction.
+const sender = await connectQwpBrowserSender(
{ url },
{
autoFlushRows: 10_000,
autoFlushBytes: 4 * 1024 * 1024,
transactional: true,
},
);
for (const event of events) {
await sender
.table("events")
.symbol("source", event.source)
.longColumn("value", event.value)
.at(event.timestamp, "ms");
}
await sender.commit();
await sender.close();
+
+
+QWP close() publishes completed rows and waits up to 5 seconds for their
+committed-frame ACK watermark. Configure closeFlushTimeoutMs (or
+close_flush_timeout_millis in a ws:: string); 0 publishes without waiting.
+An unfinished row is not completed implicitly.
+The server intentionally withholds ACKs for deferred frames until commit. The
+sender pipelines transactional auto-flushes without waiting for those ACKs,
+then publishes the group-closing frame at flush()/commit(). With
+awaitServerAck or awaitDurableAck, that call also waits for all covered
+ACKs; durable waiting starts only after the transaction commits. Closing
+without an explicit commit abandons the open transaction and logs a warning;
+QuestDB rolls it back when the WebSocket disconnects.
+Ingress sessions expose browser-safe progress/error callbacks and immutable
+metrics snapshots. Reconnect events remain on reconnect.onEvent, keeping
+connection topology separate from batch acceptance and durable progress.
+import {
QWP_INGRESS_PROGRESS_KIND,
createQwpBrowserSender,
} from "@questdb/browser-client";
const sender = createQwpBrowserSender(
{ url },
{ autoFlush: false },
{
reconnect: {
onEvent: (event) => console.info("QWP connection", event),
},
onProgress: (event) => {
if (event.kind === QWP_INGRESS_PROGRESS_KIND.ACKNOWLEDGED) {
console.info("accepted through", event.sequence);
}
},
onError: (event) => console.error("QWP ingress", event.error),
onSenderError: (error) =>
console.error(
"QWP rejection",
error.category,
error.appliedPolicy,
error.fromFsn,
error.toFsn,
),
},
);
await sender.connect();
const snapshot = sender.metrics;
console.info(
snapshot.totalRowsPublished,
snapshot.ingress?.totalFramesReplayed,
);
+
+
+Snapshots distinguish the client-session acceptance sequence from persistent
+replay watermarks. With durable ACKs, replayAcknowledgedFrameSequence
+advances only after the durable watermark covers a frame. Observer callbacks are
+dispatched asynchronously through bounded, drop-oldest inboxes, so they do not run
+inside ACK or reconnect protocol stacks. The metrics snapshot exposes delivered and
+dropped progress, connection, and error notification counters.
+connectionListenerInboxCapacity and errorInboxCapacity tune the Java-compatible
+64/256 defaults. onSenderError receives typed category/policy, wire status, message
+sequence, stable frame-sequence range, and quarantine context. If it is omitted,
+retriable rejections are logged at warn and terminal rejections or abandoned data at
+error; general asynchronous ingress failures are also logged when onError is
+omitted. Observer exceptions are contained, but CPU-bound callbacks should still move
+work to a Worker because browser and Node JavaScript share the event loop.
+When QuestDB authentication is enabled, establish the browser's HttpOnly
+qdb_session cookie over REST before opening a QWP WebSocket. A QuestDB REST
+token and an OIDC access token both use the bearer form. The application is
+responsible for obtaining an OIDC token from its identity provider; the client
+does not run an interactive OIDC authorization flow.
+import {
bootstrapQwpBrowserSession,
connectQwpBrowserSender,
} from "@questdb/browser-client";
await bootstrapQwpBrowserSession({
url: new URL("/exec", location.href),
authentication: { type: "bearer", token: oidcOrRestAccessToken },
// QuestDB Enterprise only; omit to use the authenticated principal.
serviceAccount: "market_data_writer",
});
const sender = await connectQwpBrowserSender({ url }, { autoFlush: false });
+
+
+The bootstrap can also be attached to the connection options. It then runs
+before each initial, reconnect, or failover WebSocket attempt:
+const sender = await connectQwpBrowserSender(
{
url,
sessionBootstrap: {
authentication: {
type: "basic",
username: "admin",
password: "quest",
},
},
},
{ autoFlush: false },
);
+
+
+The REST request uses credentials: "include". The default bootstrap URL is
+/exec beside /write/v4 or /read/v1; set sessionBootstrap.url explicitly
+when a reverse proxy exposes a different REST path. The REST and WebSocket
+routes should be served from the same browser origin (or configured with
+credentialed CORS), otherwise the browser may decline to store or send the
+HttpOnly cookies. JavaScript deliberately never reads qdb_session or the
+Enterprise qdbServiceAccount cookie.
+Browsers can request durable ingress acknowledgements without custom HTTP
+headers. The client offers a QWP WebSocket subprotocol and verifies that the
+server selected it before sending data. Browser keepalives use side-effect-free,
+table-less QWP poll frames because the WebSocket API does not expose
+protocol-level PING frames. A poll completes once published: durable progress
+arrives independently, and an open deferred transaction may intentionally
+prevent the server from sending a cumulative OK for that poll. Supplying
+durableAckKeepaliveMs requires durable negotiation (requestDurableAck: true,
+either explicit or implied by awaitDurableAck); manual polls and durable waits
+reject locally when the capability was not negotiated.
+const sender = await connectQwpBrowserSender(
{ url, requestDurableAck: true },
{ autoFlush: false, awaitDurableAck: true },
);
+
+
+Browser durable ACKs are an in-memory delivery confirmation only. Persistent
+store-and-forward remains available exclusively through the Node.js entry
+point. In-memory ingress replay is capped at 128 MiB and waits at most 30 seconds
+for ACK-driven trimming by default; tune memoryReplayMaxBytes and
+memoryReplayAppendDeadlineMs in the ingress session options when needed.
+Zstd-compressed QWP egress
Node.js egress clients can opt into compressed result batches during the
+WebSocket upgrade. Raw batches remain the default for compatibility.
+import { connectQwpNodeEgress } from "@questdb/nodejs-client";
const session = await connectQwpNodeEgress(
{
url: "ws://127.0.0.1:9000/read/v1",
compression: "zstd",
compressionLevel: 3,
},
{
queryTimeoutMs: 30_000,
},
);
try {
const query = await session.query("select * from trades", {
initialCredit: 1024 * 1024,
});
console.log("effective Zstd level", session.negotiatedZstdLevel);
for await (const batch of query) {
for (const row of batch.rows()) console.log(row);
}
await query.completion;
} finally {
await session.close();
}
+
+
+Zstd decoding and negotiation are also included in the browser entry point.
+Because browsers cannot set the X-QWP-Accept-Encoding upgrade header, the
+client sends the same preference through the WebSocket URL's
+qwp_accept_encoding parameter. No proxy-injected compression header is
+required. Older servers ignore the parameter and safely continue with raw
+batches.
+Level 1 is the lowest-CPU default and is usually the right starting point.
+Higher values trade server CPU for wire size; the client accepts levels 1–22,
+while the server may clamp the request or apply an operator-configured level.
+session.negotiatedCompression and session.negotiatedZstdLevel report what
+the active server actually selected and refresh after reconnection or failover.
+Both "zstd" and "auto" advertise Zstd followed by raw fallback, and the
+server still sends an individual batch raw when compression would make it
+larger.
+Matching the Java client, egress queries default initialCredit to zero, meaning
+unbounded server send-ahead. Set a positive session or per-query value to bound wire
+buffering—particularly in browsers. With positive credit, the client automatically
+replenishes the exact wire size of each result batch after consumption. Set
+autoCredit: false to manage credit explicitly through query.grantCredit().
+For allocation-sensitive consumers, session.queryViews(sql, onBatch) supplies
+bounded, reusable column views instead of materializing every value into JavaScript
+arrays. Typed accessors read fixed-width values directly from QWP bytes, and raw
+byte views are available for vectorized processing. The callback is awaited before
+credit is replenished, while the receive loop decodes ahead through the bounded
+reusable buffer pool. Views are invalid when their callback returns; copy a byte
+view with .slice() or call batch.materialize() inside the callback to retain
+data. Tune the default four-slot pool with the session's bufferPoolSize.
+queryTimeoutMs sets the session's default query deadline; a per-query
+timeoutMs overrides it, and zero disables the deadline. When a deadline
+expires, the client rejects iteration and query.completion with
+QwpEgressQueryTimeoutError, sends QWP CANCEL, and waits for the terminal
+server response before accepting another query on that connection. Breaking out
+of for await early cancels the query too. cancelDrainTimeoutMs bounds that
+wait (5 seconds by default); an unresponsive cancellation closes the connection
+with QwpEgressQueryCancelTimeoutError instead of wedging the session.
+To bound only the caller's wait without cancelling, use
+await query.awaitCompletion(timeoutMs). It returns false on timeout and leaves
+the query active, matching Java Completion.await(timeout, unit). The SERVER_INFO
+handshake timeout defaults to five seconds on both clients.
+Authentication and secure connection
Username and password authentication with HTTP transport
import { Sender } from "@questdb/nodejs-client";
async function run() {
// authentication details
const USER = "admin";
const PWD = "quest";
// pass the authentication details to the sender
// for secure connection use 'https' protocol instead of 'http'
const sender = await Sender.fromConfig(
`http::addr=127.0.0.1:9000;username=${USER};password=${PWD}`,
);
// add rows to the buffer of the sender
await sender
.table("trades")
.symbol("symbol", "ETH-USD")
.symbol("side", "sell")
.floatColumn("price", 2615.54)
.floatColumn("amount", 0.00044)
.at(Date.now(), "ms");
// flush the buffer of the sender, sending the data to QuestDB
await sender.flush();
// close the connection after all rows ingested
await sender.close();
}
run().catch(console.error);
-REST token authentication with HTTP transport
import { Sender } from "@questdb/nodejs-client";
async function run() {
// authentication details
const TOKEN = "Xyvd3er6GF87ysaHk";
// pass the authentication details to the sender
// for secure connection use 'https' protocol instead of 'http'
const sender = await Sender.fromConfig(
`http::addr=127.0.0.1:9000;token=${TOKEN}`
);
// add rows to the buffer of the sender
await sender
.table("trades")
.symbol("symbol", "ETH-USD")
.symbol("side", "sell")
.floatColumn("price", 2615.54)
.floatColumn("amount", 0.00044)
.at(Date.now(), "ms");
// flush the buffer of the sender, sending the data to QuestDB
await sender.flush();
// close the connection after all rows ingested
await sender.close();
}
run().catch(console.error);
+REST token authentication with HTTP transport
import { Sender } from "@questdb/nodejs-client";
async function run() {
// authentication details
const TOKEN = "Xyvd3er6GF87ysaHk";
// pass the authentication details to the sender
// for secure connection use 'https' protocol instead of 'http'
const sender = await Sender.fromConfig(
`http::addr=127.0.0.1:9000;token=${TOKEN}`,
);
// add rows to the buffer of the sender
await sender
.table("trades")
.symbol("symbol", "ETH-USD")
.symbol("side", "sell")
.floatColumn("price", 2615.54)
.floatColumn("amount", 0.00044)
.at(Date.now(), "ms");
// flush the buffer of the sender, sending the data to QuestDB
await sender.flush();
// close the connection after all rows ingested
await sender.close();
}
run().catch(console.error);
-JWK token authentication with TCP transport
import { Sender } from "@questdb/nodejs-client";
async function run() {
// authentication details
const CLIENT_ID = "admin";
const PRIVATE_KEY = "ZRxmCOQBpZoj2fZ-lEtqzVDkCre_ouF3ePpaQNDwoQk";
// pass the authentication details to the sender
const sender = await Sender.fromConfig(
`tcp::addr=127.0.0.1:9009;username=${CLIENT_ID};token=${PRIVATE_KEY}`
);
await sender.connect();
// add rows to the buffer of the sender
await sender
.table("trades")
.symbol("symbol", "BTC-USD")
.symbol("side", "sell")
.floatColumn("price", 39269.98)
.floatColumn("amount", 0.001)
.at(Date.now(), "ms");
// flush the buffer of the sender, sending the data to QuestDB
await sender.flush();
// close the connection after all rows ingested
await sender.close();
}
run().catch(console.error);
+JWK token authentication with TCP transport
import { Sender } from "@questdb/nodejs-client";
async function run() {
// authentication details
const CLIENT_ID = "admin";
const PRIVATE_KEY = "ZRxmCOQBpZoj2fZ-lEtqzVDkCre_ouF3ePpaQNDwoQk";
// pass the authentication details to the sender
const sender = await Sender.fromConfig(
`tcp::addr=127.0.0.1:9009;username=${CLIENT_ID};token=${PRIVATE_KEY}`,
);
await sender.connect();
// add rows to the buffer of the sender
await sender
.table("trades")
.symbol("symbol", "BTC-USD")
.symbol("side", "sell")
.floatColumn("price", 39269.98)
.floatColumn("amount", 0.001)
.at(Date.now(), "ms");
// flush the buffer of the sender, sending the data to QuestDB
await sender.flush();
// close the connection after all rows ingested
await sender.close();
}
run().catch(console.error);
-Array usage example
import { Sender } from "@questdb/nodejs-client";
async function run() {
// create a sender
const sender = await Sender.fromConfig('http::addr=localhost:9000');
// order book snapshots to ingest
const orderBooks = [
{
symbol: 'BTC-USD',
exchange: 'Coinbase',
timestamp: Date.now(),
bidPrices: [50100.25, 50100.20, 50100.15, 50100.10, 50100.05],
bidSizes: [0.5, 1.2, 2.1, 0.8, 3.5],
askPrices: [50100.30, 50100.35, 50100.40, 50100.45, 50100.50],
askSizes: [0.6, 1.5, 1.8, 2.2, 4.0]
},
{
symbol: 'ETH-USD',
exchange: 'Coinbase',
timestamp: Date.now(),
bidPrices: [2850.50, 2850.45, 2850.40, 2850.35, 2850.30],
bidSizes: [5.0, 8.2, 12.5, 6.8, 15.0],
askPrices: [2850.55, 2850.60, 2850.65, 2850.70, 2850.75],
askSizes: [4.5, 7.8, 10.2, 8.5, 20.0]
}
];
try {
// add rows to the buffer of the sender
for (const orderBook of orderBooks) {
await sender
.table('order_book_l2')
.symbol('symbol', orderBook.symbol)
.symbol('exchange', orderBook.exchange)
.arrayColumn('bid_prices', orderBook.bidPrices)
.arrayColumn('bid_sizes', orderBook.bidSizes)
.arrayColumn('ask_prices', orderBook.askPrices)
.arrayColumn('ask_sizes', orderBook.askSizes)
.at(orderBook.timestamp, 'ms');
}
// flush the buffer of the sender, sending the data to QuestDB
// the buffer is cleared after the data is sent, and the sender is ready to accept new data
await sender.flush();
} finally {
// close the connection after all rows ingested
await sender.close();
}
}
run().then(console.log).catch(console.error);
+Array usage example
import { Sender } from "@questdb/nodejs-client";
async function run() {
// create a sender
const sender = await Sender.fromConfig("http::addr=localhost:9000");
// order book snapshots to ingest
const orderBooks = [
{
symbol: "BTC-USD",
exchange: "Coinbase",
timestamp: Date.now(),
bidPrices: [50100.25, 50100.2, 50100.15, 50100.1, 50100.05],
bidSizes: [0.5, 1.2, 2.1, 0.8, 3.5],
askPrices: [50100.3, 50100.35, 50100.4, 50100.45, 50100.5],
askSizes: [0.6, 1.5, 1.8, 2.2, 4.0],
},
{
symbol: "ETH-USD",
exchange: "Coinbase",
timestamp: Date.now(),
bidPrices: [2850.5, 2850.45, 2850.4, 2850.35, 2850.3],
bidSizes: [5.0, 8.2, 12.5, 6.8, 15.0],
askPrices: [2850.55, 2850.6, 2850.65, 2850.7, 2850.75],
askSizes: [4.5, 7.8, 10.2, 8.5, 20.0],
},
];
try {
// add rows to the buffer of the sender
for (const orderBook of orderBooks) {
await sender
.table("order_book_l2")
.symbol("symbol", orderBook.symbol)
.symbol("exchange", orderBook.exchange)
.arrayColumn("bid_prices", orderBook.bidPrices)
.arrayColumn("bid_sizes", orderBook.bidSizes)
.arrayColumn("ask_prices", orderBook.askPrices)
.arrayColumn("ask_sizes", orderBook.askSizes)
.at(orderBook.timestamp, "ms");
}
// flush the buffer of the sender, sending the data to QuestDB
// the buffer is cleared after the data is sent, and the sender is ready to accept new data
await sender.flush();
} finally {
// close the connection after all rows ingested
await sender.close();
}
}
run().then(console.log).catch(console.error);
-Worker threads example
import { Sender } from "@questdb/nodejs-client";
import { Worker, isMainThread, parentPort, workerData } from "worker_threads";
// fake venue
// generates random prices and amounts for a ticker for max 5 seconds, then the feed closes
function* venue(ticker) {
let end = false;
setTimeout(() => {
end = true;
}, rndInt(5000));
while (!end) {
yield { ticker, price: Math.random(), amount: Math.random() };
}
}
// market data feed simulator
// uses the fake venue to deliver price and amount updates to the feed handler (onTick() callback)
async function subscribe(ticker, onTick) {
const feed = venue(workerData.ticker);
let tick;
while ((tick = feed.next().value)) {
await onTick(tick);
await sleep(rndInt(30));
}
}
async function run() {
if (isMainThread) {
const tickers = ["ETH-USD", "BTC-USD", "SOL-USD", "DOGE-USD"];
// main thread to start a worker thread for each ticker
for (let ticker of tickers) {
new Worker(__filename, { workerData: { ticker: ticker } })
.on("error", (err) => {
throw err;
})
.on("exit", () => {
console.log(`${ticker} thread exiting...`);
})
.on("message", (msg) => {
console.log(`Ingested ${msg.count} prices for ticker ${msg.ticker}`);
});
}
} else {
// it is important that each worker has a dedicated sender object
// threads cannot share the sender because they would write into the same buffer
const sender = await Sender.fromConfig("http::addr=127.0.0.1:9000");
// subscribe for the market data of the ticker assigned to the worker
// ingest each price update into the database using the sender
let count = 0;
await subscribe(workerData.ticker, async (tick) => {
await sender
.table("trades")
.symbol("symbol", tick.ticker)
.symbol("side", "sell")
.floatColumn("price", tick.price)
.floatColumn("amount", tick.amount)
.at(Date.now(), "ms");
await sender.flush();
count++;
});
// let the main thread know how many prices were ingested
parentPort.postMessage({ ticker: workerData.ticker, count });
// close the connection to the database
await sender.close();
}
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function rndInt(limit: number) {
return Math.floor(Math.random() * limit + 1);
}
run().then(console.log).catch(console.error);
+Worker threads example
import { Sender } from "@questdb/nodejs-client";
import { Worker, isMainThread, parentPort, workerData } from "worker_threads";
// fake venue
// generates random prices and amounts for a ticker for max 5 seconds, then the feed closes
function* venue(ticker) {
let end = false;
setTimeout(() => {
end = true;
}, rndInt(5000));
while (!end) {
yield { ticker, price: Math.random(), amount: Math.random() };
}
}
// market data feed simulator
// uses the fake venue to deliver price and amount updates to the feed handler (onTick() callback)
async function subscribe(ticker, onTick) {
const feed = venue(workerData.ticker);
let tick;
while ((tick = feed.next().value)) {
await onTick(tick);
await sleep(rndInt(30));
}
}
async function run() {
if (isMainThread) {
const tickers = ["ETH-USD", "BTC-USD", "SOL-USD", "DOGE-USD"];
// main thread to start a worker thread for each ticker
for (let ticker of tickers) {
new Worker(__filename, { workerData: { ticker: ticker } })
.on("error", (err) => {
throw err;
})
.on("exit", () => {
console.log(`${ticker} thread exiting...`);
})
.on("message", (msg) => {
console.log(`Ingested ${msg.count} prices for ticker ${msg.ticker}`);
});
}
} else {
// it is important that each worker has a dedicated sender object
// threads cannot share the sender because they would write into the same buffer
const sender = await Sender.fromConfig("http::addr=127.0.0.1:9000");
// subscribe for the market data of the ticker assigned to the worker
// ingest each price update into the database using the sender
let count = 0;
await subscribe(workerData.ticker, async (tick) => {
await sender
.table("trades")
.symbol("symbol", tick.ticker)
.symbol("side", "sell")
.floatColumn("price", tick.price)
.floatColumn("amount", tick.amount)
.at(Date.now(), "ms");
await sender.flush();
count++;
});
// let the main thread know how many prices were ingested
parentPort.postMessage({ ticker: workerData.ticker, count });
// close the connection to the database
await sender.close();
}
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function rndInt(limit: number) {
return Math.floor(Math.random() * limit + 1);
}
run().then(console.log).catch(console.error);
Decimal usage example
Since v9.2.0, QuestDB supports the DECIMAL data type.
@@ -62,4 +297,4 @@
CommunityCommunity Forum.
You can also sign up to our mailing list
to get notified of new releases.
-
+
diff --git a/docs/interfaces/SenderBuffer.html b/docs/interfaces/SenderBuffer.html
deleted file mode 100644
index 96981d7..0000000
--- a/docs/interfaces/SenderBuffer.html
+++ /dev/null
@@ -1,151 +0,0 @@
-SenderBuffer | QuestDB Node.js Client - v4.2.0 Interface SenderBuffer
Buffer used by the Sender for data serialization.
-Provides methods for writing different data types into the buffer.
- interface SenderBuffer {
reset(): SenderBuffer;
toBufferView(pos?: number): Buffer;
toBufferNew(pos?: number): Buffer<ArrayBufferLike>;
table(table: string): SenderBuffer;
symbol(name: string, value: unknown): SenderBuffer;
stringColumn(name: string, value: string): SenderBuffer;
booleanColumn(name: string, value: boolean): SenderBuffer;
floatColumn(name: string, value: number): SenderBuffer;
arrayColumn(name: string, value: unknown[]): SenderBuffer;
intColumn(name: string, value: number): SenderBuffer;
timestampColumn(
name: string,
value: number | bigint,
unit?: TimestampUnit,
): SenderBuffer;
decimalColumnText(name: string, value: string | number): SenderBuffer;
decimalColumn(
name: string,
unscaled: bigint | Int8Array<ArrayBufferLike>,
scale: number,
): SenderBuffer;
at(timestamp: number | bigint, unit?: TimestampUnit): void;
atNow(): void;
currentPosition(): number;
}Index
Methods
reset
Resets the buffer, data sitting in the buffer will be lost.
-In other words it clears the buffer, and sets the writing position to the beginning of the buffer.
-Returns SenderBuffer
Returns with a reference to this buffer.
-
to Buffer View
Returns a cropped buffer, or null if there is nothing to send.
-The returned buffer is backed by this buffer instance, meaning the view can change as the buffer is mutated.
-Used only in tests to assert the buffer's content.
-Parameters
Optionalpos: numberOptional position parameter
-
Returns Buffer
A view of the buffer
-
to Buffer New
Returns a cropped buffer ready to send to the server, or null if there is nothing to send.
-The returned buffer is a copy of this buffer.
-It also compacts the buffer.
-Parameters
Optionalpos: numberOptional position parameter
-
Returns Buffer<ArrayBufferLike>
A copy of the buffer ready to send, or null
-
table
Writes the table name into the buffer.
-Parameters
- table: string
Table name.
-
Returns SenderBuffer
Returns with a reference to this buffer.
-
symbol
Writes a symbol name and value into the buffer.
-Use it to insert into SYMBOL columns.
-Parameters
- name: string
Symbol name.
- - value: unknown
Symbol value, toString() is called to extract the actual symbol value from the parameter.
-
Returns SenderBuffer
Returns with a reference to this buffer.
-
string Column
Writes a string column with its value into the buffer.
-Use it to insert into VARCHAR and STRING columns.
-Parameters
- name: string
Column name.
- - value: string
Column value, accepts only string values.
-
Returns SenderBuffer
Returns with a reference to this buffer.
-
boolean Column
Writes a boolean column with its value into the buffer.
-Use it to insert into BOOLEAN columns.
-Parameters
- name: string
Column name.
- - value: boolean
Column value, accepts only boolean values.
-
Returns SenderBuffer
Returns with a reference to this buffer.
-
float Column
Writes a 64-bit floating point value into the buffer.
-Use it to insert into DOUBLE or FLOAT database columns.
-Parameters
- name: string
Column name.
- - value: number
Column value, accepts only number values.
-
Returns SenderBuffer
Returns with a reference to this buffer.
-
array Column
Writes an array column with its values into the buffer.
-Parameters
- name: string
Column name
- - value: unknown[]
Array values to write (currently supports double arrays)
-
Returns SenderBuffer
Returns with a reference to this buffer.
-
int Column
Writes a 64-bit signed integer into the buffer.
-Use it to insert into LONG, INT, SHORT and BYTE columns.
-Parameters
- name: string
Column name.
- - value: number
Column value, accepts only number values.
-
Returns SenderBuffer
Returns with a reference to this buffer.
-
timestamp Column
Writes a timestamp column and its value into the buffer.
-Use this method to insert data into TIMESTAMP or TIMESTAMP_NS columns.
-Precision rules:
-
-- Protocol v2 and higher:
-Timestamps passed with unit
'ns' (nanoseconds) are sent with full nanosecond precision.
-All other timestamps are sent with microsecond precision.
-- Protocol v1:
-Always uses microsecond precision, even if the timestamp is specified in nanoseconds.
-
-Parameters
- name: string
The column name.
- - value: number | bigint
The epoch timestamp. Must be an integer or a BigInt.
- Optionalunit: TimestampUnitThe time unit of the timestamp.
-Supported values:
-
-'ns' — nanoseconds (requires BigInt)
-'us' — microseconds (default)
-'ms' — milliseconds
-
-
Returns SenderBuffer
Returns with a reference to this buffer.
-
decimal Column Text
Writes a decimal value into the buffer using its text format.
-Use it to insert into DECIMAL database columns.
-Parameters
- name: string
Column name.
- - value: string | number
The decimal value to write.
-
-- Accepts either a
number or a string containing a valid decimal representation.
-- String values should follow standard decimal notation (e.g.,
"123.45" or "-0.001").
-
-
Returns SenderBuffer
Returns with a reference to this buffer.
-
decimal Column
- decimalColumn(
name: string,
unscaled: bigint | Int8Array<ArrayBufferLike>,
scale: number,
): SenderBufferWrites a decimal value into the buffer using its binary format.
-Use it to insert into DECIMAL database columns.
-Parameters
- name: string
Column name.
- - unscaled: bigint | Int8Array<ArrayBufferLike>
The unscaled integer portion of the decimal value.
-
-- If a
bigint is provided, it will be converted automatically.
-- If an
Int8Array is provided, it must contain the two’s complement representation
-of the unscaled value in big-endian byte order.
-- An empty
Int8Array represents a NULL value.
-
- - scale: number
The number of fractional digits (the scale) of the decimal value.
-
Returns SenderBuffer
Returns with a reference to this buffer.
-
at
Closes the row after writing the designated timestamp into the buffer.
-Precision rules:
-
-- Protocol v2 and higher:
-Timestamps passed with unit
'ns' (nanoseconds) are sent with full nanosecond precision.
-All other timestamps are sent with microsecond precision.
-- Protocol v1:
-Always uses microsecond precision, even if the timestamp is specified in nanoseconds.
-
-Parameters
- timestamp: number | bigint
Designated epoch timestamp. Must be an integer or a BigInt.
- Optionalunit: TimestampUnitThe time unit of the timestamp.
-Supported values:
-
-'ns' — nanoseconds (requires BigInt)
-'us' — microseconds (default)
-'ms' — milliseconds
-
-
Returns void
Returns with a reference to this buffer.
-
at Now
Closes the row without writing designated timestamp into the buffer.
-Designated timestamp will be populated by the server on this record.
-Returns void
current Position
Returns the current position of the buffer.
-New data will be written into the buffer starting from this position.
-Returns number
The current write position in the buffer
-
diff --git a/docs/interfaces/SenderTransport.html b/docs/interfaces/SenderTransport.html
deleted file mode 100644
index 86ad4c3..0000000
--- a/docs/interfaces/SenderTransport.html
+++ /dev/null
@@ -1,18 +0,0 @@
-SenderTransport | QuestDB Node.js Client - v4.2.0 Interface SenderTransport
Interface for QuestDB transport implementations.
-Defines the contract for different transport protocols (HTTP/HTTPS/TCP/TCPS).
- interface SenderTransport {
connect(): Promise<boolean>;
send(data: Buffer): Promise<boolean>;
close(): Promise<void>;
getDefaultAutoFlushRows(): number;
}Implemented by
Index
Methods
Methods
connect
Establishes a connection to the database server.
-Should not be called on HTTP transports.
-Returns Promise<boolean>
Promise resolving to true if connection is successful
-
send
Sends the data to the database server.
-Parameters
- data: Buffer
Buffer containing the data to send
-
Returns Promise<boolean>
Promise resolving to true if data was sent successfully
-
close
Closes the connection to the database server.
-Should not be called on HTTP transports.
-Returns Promise<void>
Promise that resolves when the connection is closed
-
get Default Auto Flush Rows
Gets the default number of rows that trigger auto-flush for this transport.
-Returns number
Default auto-flush row count
-
diff --git a/docs/interfaces/_questdb_browser-client.QwpArrayValue.html b/docs/interfaces/_questdb_browser-client.QwpArrayValue.html
new file mode 100644
index 0000000..0768eb8
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpArrayValue.html
@@ -0,0 +1,3 @@
+QwpArrayValue | QuestDB JavaScript Client - v4.2.0 Interface QwpArrayValue
Index
Properties
dimensions
+values
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpBinaryConnection.html b/docs/interfaces/_questdb_browser-client.QwpBinaryConnection.html
new file mode 100644
index 0000000..561383f
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpBinaryConnection.html
@@ -0,0 +1,29 @@
+QwpBinaryConnection | QuestDB JavaScript Client - v4.2.0 Interface QwpBinaryConnection
Normalized binary connection consumed by QWP sessions.
+Adapters buffer messages until the single async iterator consumes them, so
+unsolicited frames such as egress SERVER_INFO cannot race session startup.
+ interface QwpBinaryConnection {
closed: Promise<QwpConnectionCloseInfo>;
endpoint?: string | URL;
handshake: QwpHandshakeMetadata;
ingressDeltaSymbolDictionaryEnabled?: boolean;
ingressSymbolDictionary?: readonly string[];
managesIngressSenderErrors?: boolean;
messages: AsyncIterable<Uint8Array<ArrayBufferLike>>;
close(code?: number, reason?: string): Promise<void>;
deprioritizeEndpoint(): void;
getIngressFrameSequence(clientSequence: bigint): undefined | bigint;
getIngressMetrics(): QwpIngressTransportMetrics;
ping(): Promise<void>;
send(payload: Uint8Array): Promise<void>;
skipIngressClientSequence(): void;
}Properties
Readonlyclosed
Optional Readonlyendpoint
endpoint?: string | URLEndpoint backing this connection, when supplied by its adapter.
+Readonlyhandshake
Optional Readonly Internalingress Delta Symbol Dictionary Enabled
ingressDeltaSymbolDictionaryEnabled?: booleanFalse after replay dictionary persistence becomes unavailable.
+Optional Readonly Internalingress Symbol Dictionary
ingressSymbolDictionary?: readonly string[]Recovered ingress dictionary supplied by replay connections.
+Optional Readonly Internalmanages Ingress Sender Errors
managesIngressSenderErrors?: booleanTrue when the transport dispatches typed sender errors itself.
+Readonlymessages
messages: AsyncIterable<Uint8Array<ArrayBufferLike>>Methods
close
Parameters
Optionalcode: numberOptionalreason: string
Returns Promise<void>
Optionaldeprioritize Endpoint
InternalMarks this endpoint as temporarily unsuitable and asks a stateful
+connection factory to start its next sweep at another configured endpoint.
+Returns void
Optionalget Ingress Frame Sequence
InternalResolves a session sequence to its stable replay FSN.
+Parameters
- clientSequence: bigint
Returns undefined | bigint
Optionalget Ingress Metrics
InternalPhysical delivery metrics exposed by replaying transports.
+Returns QwpIngressTransportMetrics
Optionalping
Sends an RFC 6455 PING when the underlying runtime supports it.
+Returns Promise<void>
send
Parameters
- payload: Uint8Array
Returns Promise<void>
Optionalskip Ingress Client Sequence
InternalReserves a client sequence for a split-batch suffix suppressed
+before send(), keeping replay ACK translation aligned with the session.
+Returns void
diff --git a/docs/interfaces/_questdb_browser-client.QwpBrowserClusterOptions.html b/docs/interfaces/_questdb_browser-client.QwpBrowserClusterOptions.html
new file mode 100644
index 0000000..4bcd451
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpBrowserClusterOptions.html
@@ -0,0 +1,18 @@
+QwpBrowserClusterOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpBrowserClusterOptions
Shared browser transport and authentication for one QWP cluster.
+ interface QwpBrowserClusterOptions {
closeTimeoutMs?: number;
connectTimeoutMs?: number;
failoverUrls?: readonly (string | URL)[];
protocols?: string | string[];
sendTimeoutMs?: number;
sessionBootstrap?: QwpBrowserSessionBootstrapConfig;
url: string | URL;
webSocketFactory?: (
url: string | URL,
protocols?: string | string[],
) => QwpWebSocketLike;
}Hierarchy (View Summary)
- QwpWebSocketConnectOptions
- QwpBrowserClusterOptions
Index
Properties
Optionalclose Timeout Ms
closeTimeoutMs?: numberMaximum time allowed for a graceful WebSocket close. Defaults to 15s.
+Optionalconnect Timeout Ms
connectTimeoutMs?: numberNode TCP/TLS connection deadline, or the complete opening deadline in a
+browser. Defaults to 15s.
+Optionalfailover Urls
failoverUrls?: readonly (string | URL)[]Additional endpoints attempted in order when the preferred endpoint fails.
+Optionalprotocols
protocols?: string | string[]Optionalsend Timeout Ms
sendTimeoutMs?: numberMaximum time a send may remain queued by the WebSocket. Defaults to 15s.
+Optionalsession Bootstrap
Authenticates before every connection attempt. When url is omitted from
+this bootstrap, its REST endpoint follows the active cluster endpoint.
+url
url: string | URLOptionalweb Socket Factory
Shared test or framework hook; either side may override it.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpBrowserEgressOptions.html b/docs/interfaces/_questdb_browser-client.QwpBrowserEgressOptions.html
new file mode 100644
index 0000000..a1838e8
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpBrowserEgressOptions.html
@@ -0,0 +1,35 @@
+QwpBrowserEgressOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpBrowserEgressOptions
Browser WebSocket options plus protocol-level egress topology routing.
+ interface QwpBrowserEgressOptions {
closeTimeoutMs?: number;
compression?: QwpEgressCompression;
compressionLevel?: number;
connectTimeoutMs?: number;
failoverUrls?: readonly (string | URL)[];
ingressNegotiationTimeoutMs?: number;
maxBatchRows?: number;
protocols?: string | string[];
requestDurableAck?: boolean;
sendTimeoutMs?: number;
sessionBootstrap?: QwpBrowserSessionBootstrapConfig;
target?: QwpTarget;
url: string | URL;
webSocketFactory?: (
url: string | URL,
protocols?: string | string[],
) => QwpWebSocketLike;
zone?: string;
}Hierarchy (View Summary)
- QwpBrowserWebSocketOptions
- QwpEgressRoutingOptions
- QwpBrowserEgressOptions
Properties
Optionalclose Timeout Ms
closeTimeoutMs?: numberMaximum time allowed for a graceful WebSocket close. Defaults to 15s.
+Optionalcompression
Requests Zstd-compressed result batches through browser-visible URL
+negotiation. Defaults to raw for compatibility.
+Optionalcompression Level
compressionLevel?: numberZstd level hint. Must be between 1 and 22.
+Optionalconnect Timeout Ms
connectTimeoutMs?: numberNode TCP/TLS connection deadline, or the complete opening deadline in a
+browser. Defaults to 15s.
+Optionalfailover Urls
failoverUrls?: readonly (string | URL)[]Additional endpoints attempted in order when the preferred endpoint fails.
+Optionalingress Negotiation Timeout Ms
ingressNegotiationTimeoutMs?: numberTime allowed for the optional ingress SERVER_INFO message. Defaults to
+250ms; zero disables the initial wait while retaining late negotiation.
+Optionalmax Batch Rows
maxBatchRows?: numberRequests a server-side RESULT_BATCH row cap.
+Optionalprotocols
protocols?: string | string[]Optionalrequest Durable Ack
requestDurableAck?: booleanRequests durable ingress ACKs through browser-visible WebSocket
+subprotocol negotiation.
+Optionalsend Timeout Ms
sendTimeoutMs?: numberMaximum time a send may remain queued by the WebSocket. Defaults to 15s.
+Optionalsession Bootstrap
Authenticates over REST before every WebSocket connection attempt so the
+browser can attach QuestDB's HttpOnly session cookies to the upgrade.
+Optionaltarget
Selects any readable node, a primary/standalone node, or a replica.
+url
url: string | URLOptionalweb Socket Factory
Test or framework hook; defaults to the browser's global WebSocket.
+Optionalzone
zone?: stringOpaque, case-insensitive preferred zone; cross-zone fallback stays enabled.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpBrowserSessionBootstrapOptions.html b/docs/interfaces/_questdb_browser-client.QwpBrowserSessionBootstrapOptions.html
new file mode 100644
index 0000000..aa76ddb
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpBrowserSessionBootstrapOptions.html
@@ -0,0 +1,10 @@
+QwpBrowserSessionBootstrapOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpBrowserSessionBootstrapOptions
interface QwpBrowserSessionBootstrapOptions {
authentication: QwpBrowserSessionAuthentication;
fetch?: QwpBrowserFetch;
serviceAccount?: string;
signal?: AbortSignal;
url: string | URL;
}Index
Properties
Properties
authentication
Optionalfetch
Test or framework hook; defaults to the browser's global fetch.
+Optionalservice Account
serviceAccount?: stringOptional Enterprise service account to assume for subsequent QWP use.
+Optionalsignal
signal?: AbortSignalCancels only the REST bootstrap request.
+url
url: string | URLExact QuestDB /exec HTTP(S) URL used to create the session cookie.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpBrowserSessionBootstrapResult.html b/docs/interfaces/_questdb_browser-client.QwpBrowserSessionBootstrapResult.html
new file mode 100644
index 0000000..a2da15f
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpBrowserSessionBootstrapResult.html
@@ -0,0 +1,4 @@
+QwpBrowserSessionBootstrapResult | QuestDB JavaScript Client - v4.2.0 Interface QwpBrowserSessionBootstrapResult
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpBrowserSplitClientOptions.html b/docs/interfaces/_questdb_browser-client.QwpBrowserSplitClientOptions.html
new file mode 100644
index 0000000..95ae07c
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpBrowserSplitClientOptions.html
@@ -0,0 +1,9 @@
+QwpBrowserSplitClientOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpBrowserSplitClientOptions
Backwards-compatible form with completely independent connection trees.
+ interface QwpBrowserSplitClientOptions {
cluster?: undefined;
egress: QwpBrowserEgressOptions;
egressSession?: QwpEgressSessionOptions;
ingress: QwpBrowserWebSocketOptions;
ingressSession?: QwpIngressSessionOptions;
pool?: QwpClientPoolOptions;
sender?: QwpSenderOptions;
}Hierarchy
- QwpBrowserClientBaseOptions
- QwpBrowserSplitClientOptions
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpBrowserUnifiedClientOptions.html b/docs/interfaces/_questdb_browser-client.QwpBrowserUnifiedClientOptions.html
new file mode 100644
index 0000000..b7b04b9
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpBrowserUnifiedClientOptions.html
@@ -0,0 +1,10 @@
+QwpBrowserUnifiedClientOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpBrowserUnifiedClientOptions
Recommended combined-browser form. One endpoint list and authentication
+bootstrap are shared while side-specific protocol options remain explicit.
+ interface QwpBrowserUnifiedClientOptions {
cluster: QwpBrowserClusterOptions;
egress?: Partial<
Pick<
QwpBrowserEgressOptions,
| "target"
| "compressionLevel"
| "connectTimeoutMs"
| "sendTimeoutMs"
| "closeTimeoutMs"
| "compression"
| "maxBatchRows"
| "webSocketFactory"
| "protocols"
| "zone",
>,
>;
egressSession?: QwpEgressSessionOptions;
ingress?: Partial<
Pick<
QwpBrowserWebSocketOptions,
| "connectTimeoutMs"
| "sendTimeoutMs"
| "closeTimeoutMs"
| "requestDurableAck"
| "ingressNegotiationTimeoutMs"
| "webSocketFactory"
| "protocols",
>,
>;
ingressSession?: QwpIngressSessionOptions;
pool?: QwpClientPoolOptions;
sender?: QwpSenderOptions;
}Hierarchy
- QwpBrowserClientBaseOptions
- QwpBrowserUnifiedClientOptions
Index
Properties
Properties
cluster
Optionalegress
egress?: Partial<
Pick<
QwpBrowserEgressOptions,
| "target"
| "compressionLevel"
| "connectTimeoutMs"
| "sendTimeoutMs"
| "closeTimeoutMs"
| "compression"
| "maxBatchRows"
| "webSocketFactory"
| "protocols"
| "zone",
>,
>Optionalegress Session
Optionalingress
ingress?: Partial<
Pick<
QwpBrowserWebSocketOptions,
| "connectTimeoutMs"
| "sendTimeoutMs"
| "closeTimeoutMs"
| "requestDurableAck"
| "ingressNegotiationTimeoutMs"
| "webSocketFactory"
| "protocols",
>,
>Optionalingress Session
Optionalpool
Optionalsender
diff --git a/docs/interfaces/_questdb_browser-client.QwpBrowserWebSocketOptions.html b/docs/interfaces/_questdb_browser-client.QwpBrowserWebSocketOptions.html
new file mode 100644
index 0000000..c450cdb
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpBrowserWebSocketOptions.html
@@ -0,0 +1,23 @@
+QwpBrowserWebSocketOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpBrowserWebSocketOptions
interface QwpBrowserWebSocketOptions {
closeTimeoutMs?: number;
connectTimeoutMs?: number;
failoverUrls?: readonly (string | URL)[];
ingressNegotiationTimeoutMs?: number;
protocols?: string | string[];
requestDurableAck?: boolean;
sendTimeoutMs?: number;
sessionBootstrap?: QwpBrowserSessionBootstrapConfig;
url: string | URL;
webSocketFactory?: (
url: string | URL,
protocols?: string | string[],
) => QwpWebSocketLike;
}Hierarchy (View Summary)
- QwpWebSocketConnectOptions
- QwpBrowserWebSocketOptions
Properties
Optionalclose Timeout Ms
closeTimeoutMs?: numberMaximum time allowed for a graceful WebSocket close. Defaults to 15s.
+Optionalconnect Timeout Ms
connectTimeoutMs?: numberNode TCP/TLS connection deadline, or the complete opening deadline in a
+browser. Defaults to 15s.
+Optionalfailover Urls
failoverUrls?: readonly (string | URL)[]Additional endpoints attempted in order when the preferred endpoint fails.
+Optionalingress Negotiation Timeout Ms
ingressNegotiationTimeoutMs?: numberTime allowed for the optional ingress SERVER_INFO message. Defaults to
+250ms; zero disables the initial wait while retaining late negotiation.
+Optionalprotocols
protocols?: string | string[]Optionalrequest Durable Ack
requestDurableAck?: booleanRequests durable ingress ACKs through browser-visible WebSocket
+subprotocol negotiation.
+Optionalsend Timeout Ms
sendTimeoutMs?: numberMaximum time a send may remain queued by the WebSocket. Defaults to 15s.
+Optionalsession Bootstrap
Authenticates over REST before every WebSocket connection attempt so the
+browser can attach QuestDB's HttpOnly session cookies to the upgrade.
+url
url: string | URLOptionalweb Socket Factory
Test or framework hook; defaults to the browser's global WebSocket.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpCacheResetMessage.html b/docs/interfaces/_questdb_browser-client.QwpCacheResetMessage.html
new file mode 100644
index 0000000..bd72958
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpCacheResetMessage.html
@@ -0,0 +1,7 @@
+QwpCacheResetMessage | QuestDB JavaScript Client - v4.2.0 Interface QwpCacheResetMessage
interface QwpCacheResetMessage {
flags: number;
kind: "cache-reset";
payloadLength: number;
resetMask: number;
tableCount: number;
version: number;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpCacheResetMessage
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpClientFactories.html b/docs/interfaces/_questdb_browser-client.QwpClientFactories.html
new file mode 100644
index 0000000..79e9954
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpClientFactories.html
@@ -0,0 +1,9 @@
+QwpClientFactories | QuestDB JavaScript Client - v4.2.0 Interface QwpClientFactories
interface QwpClientFactories {
senderSlotReservation?: QwpPoolSlotReservation;
close(): void | Promise<void>;
createQuerySession(
slot: number,
signal?: AbortSignal,
): Promise<QwpEgressSession>;
createSender(slot: number, signal?: AbortSignal): Promise<QwpSender>;
start(): void | Promise<void>;
}Index
Properties
Methods
Properties
Optional Internalsender Slot Reservation
Coordinates stable persistent sender slots with recovery.
+Methods
Optionalclose
InternalStops runtime-specific background services during close.
+Returns void | Promise<void>
create Query Session
Parameters
- slot: number
Optionalsignal: AbortSignal
Returns Promise<QwpEgressSession>
create Sender
Parameters
- slot: number
Optionalsignal: AbortSignal
Returns Promise<QwpSender>
Optionalstart
InternalStarts runtime-specific background services on first use.
+Returns void | Promise<void>
diff --git a/docs/interfaces/_questdb_browser-client.QwpClientMetrics.html b/docs/interfaces/_questdb_browser-client.QwpClientMetrics.html
new file mode 100644
index 0000000..ce30be4
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpClientMetrics.html
@@ -0,0 +1,5 @@
+QwpClientMetrics | QuestDB JavaScript Client - v4.2.0 Interface QwpClientMetrics
interface QwpClientMetrics {
closed: boolean;
closing: boolean;
queries: QwpResourcePoolMetrics;
senders: QwpResourcePoolMetrics;
}
diff --git a/docs/interfaces/_questdb_browser-client.QwpClientPoolOptions.html b/docs/interfaces/_questdb_browser-client.QwpClientPoolOptions.html
new file mode 100644
index 0000000..5a05e4a
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpClientPoolOptions.html
@@ -0,0 +1,18 @@
+QwpClientPoolOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpClientPoolOptions
interface QwpClientPoolOptions {
acquireTimeoutMs?: number;
housekeepingIntervalMs?: number;
idleTimeoutMs?: number;
maxLifetimeMs?: number;
queryPoolMax?: number;
queryPoolMin?: number;
senderPoolMax?: number;
senderPoolMin?: number;
}Index
Properties
Optionalacquire Timeout Ms
acquireTimeoutMs?: numberMaximum wait for a returned pool slot and for leases during shutdown.
+The shutdown wait is capped at 5 seconds. Defaults to 5 seconds.
+Optionalhousekeeping Interval Ms
housekeepingIntervalMs?: numberIdle/lifetime sweep interval. Defaults to 5s and must be at least 100ms.
+Optionalidle Timeout Ms
idleTimeoutMs?: numberIdle time before an excess pooled connection is closed. Defaults to 60s; zero disables.
+Optionalmax Lifetime Ms
maxLifetimeMs?: numberMaximum pooled connection age before recycling it while idle. Defaults to 30m; zero disables.
+Optionalquery Pool Max
queryPoolMax?: numberMaximum concurrently borrowed query connections. Defaults to 4.
+Optionalquery Pool Min
queryPoolMin?: numberWarm egress connections created by connect(). Defaults to 1.
+Optionalsender Pool Max
senderPoolMax?: numberMaximum concurrently borrowed ingress senders. Defaults to 4.
+Optionalsender Pool Min
senderPoolMin?: numberWarm ingress connections created by connect(). Defaults to 1.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpColumnBuffer.html b/docs/interfaces/_questdb_browser-client.QwpColumnBuffer.html
new file mode 100644
index 0000000..11404b2
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpColumnBuffer.html
@@ -0,0 +1,11 @@
+QwpColumnBuffer | QuestDB JavaScript Client - v4.2.0 Interface QwpColumnBuffer
interface QwpColumnBuffer {
decimalScale?: number;
geohashPrecision?: number;
name: string;
nulls: boolean[];
size: number;
type: QwpColumnType;
values: unknown[];
}Index
Properties
Properties
Optionaldecimal Scale
decimalScale?: numberOptionalgeohash Precision
geohashPrecision?: numbername
name: stringnulls
nulls: boolean[]One entry per row; true means NULL.
+size
size: numberRows accounted for so far, including nulls.
+type
values
values: unknown[]Non-null values only; QWP compacts values around the null bitmap.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpConnectionCloseInfo.html b/docs/interfaces/_questdb_browser-client.QwpConnectionCloseInfo.html
new file mode 100644
index 0000000..eb24ef7
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpConnectionCloseInfo.html
@@ -0,0 +1,4 @@
+QwpConnectionCloseInfo | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_browser-client.QwpDecimalValue.html b/docs/interfaces/_questdb_browser-client.QwpDecimalValue.html
new file mode 100644
index 0000000..85edce6
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpDecimalValue.html
@@ -0,0 +1,3 @@
+QwpDecimalValue | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_browser-client.QwpEgressQueryOptions.html b/docs/interfaces/_questdb_browser-client.QwpEgressQueryOptions.html
new file mode 100644
index 0000000..59e1748
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpEgressQueryOptions.html
@@ -0,0 +1,17 @@
+QwpEgressQueryOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpEgressQueryOptions
interface QwpEgressQueryOptions {
autoCredit?: boolean;
bindCount?: number;
bindPayload?: Uint8Array<ArrayBufferLike>;
binds?: QwpBindSetter;
initialCredit?: number | bigint;
resetDictionary?: boolean;
timeoutMs?: number;
}Index
Properties
Properties
Optionalauto Credit
autoCredit?: booleanReplenishes positive initial credit by each RESULT_BATCH wire size after
+the async iterator advances past that batch. Defaults to true.
+Optionalbind Count
bindCount?: numberAdvanced escape hatch for an already encoded bind section.
+Optionalbind Payload
bindPayload?: Uint8Array<ArrayBufferLike>Advanced escape hatch for an already encoded bind section.
+Optionalbinds
Sets typed positional parameters; index 0 maps to SQL placeholder $1.
+Optionalinitial Credit
initialCredit?: number | bigintOverrides session send-ahead credit. Zero explicitly disables flow control.
+Optionalreset Dictionary
resetDictionary?: booleanAsk a capable server to reset its connection-scoped symbol dictionary.
+Silently omitted when the server lacks QUERY_FLAGS for rolling upgrades.
+Optionaltimeout Ms
timeoutMs?: numberPer-query deadline overriding the session default. Zero disables it.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpEgressReplayResetEvent.html b/docs/interfaces/_questdb_browser-client.QwpEgressReplayResetEvent.html
new file mode 100644
index 0000000..7f15d9c
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpEgressReplayResetEvent.html
@@ -0,0 +1,8 @@
+QwpEgressReplayResetEvent | QuestDB JavaScript Client - v4.2.0 Interface QwpEgressReplayResetEvent
interface QwpEgressReplayResetEvent {
cause?: unknown;
endpoint?: string | URL;
previousEndpoint?: string | URL;
requestId: bigint;
serverInfo: QwpServerInfoMessage;
}Index
Properties
Properties
Optional Readonlycause
cause?: unknownOptional Readonlyendpoint
endpoint?: string | URLOptional Readonlyprevious Endpoint
previousEndpoint?: string | URLReadonlyrequest Id
requestId: bigintClient request being re-executed on the replacement connection.
+Readonlyserver Info
Authoritative SERVER_INFO received from the replacement endpoint.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpEgressRoutingOptions.html b/docs/interfaces/_questdb_browser-client.QwpEgressRoutingOptions.html
new file mode 100644
index 0000000..2731aec
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpEgressRoutingOptions.html
@@ -0,0 +1,8 @@
+QwpEgressRoutingOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpEgressRoutingOptions
Endpoint routing preferences. Named for egress, where they landed first, but
+ingress ranks and validates its endpoints with the same machinery and honours
+the same two keys.
+Hierarchy (View Summary)
- QwpEgressRoutingOptions
diff --git a/docs/interfaces/_questdb_browser-client.QwpEgressSessionOptions.html b/docs/interfaces/_questdb_browser-client.QwpEgressSessionOptions.html
new file mode 100644
index 0000000..64e0231
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpEgressSessionOptions.html
@@ -0,0 +1,19 @@
+QwpEgressSessionOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpEgressSessionOptions
interface QwpEgressSessionOptions {
bufferPoolSize?: number;
cancelDrainTimeoutMs?: number;
initialCredit?: number | bigint;
onReplayReset?: (event: QwpEgressReplayResetEvent) => void | Promise<void>;
queryTimeoutMs?: number;
reconnect?: false | QwpReconnectOptions;
serverInfoTimeoutMs?: number;
}Index
Properties
Optionalbuffer Pool Size
bufferPoolSize?: numberMaximum decoded batches waiting for a consumer. Defaults to 4.
+Optionalcancel Drain Timeout Ms
cancelDrainTimeoutMs?: numberMaximum wait for a terminal response after CANCEL. Defaults to 5 seconds.
+Optionalinitial Credit
initialCredit?: number | bigintDefault per-query send-ahead credit. Defaults to zero (unbounded).
+Optionalon Replay Reset
Optional notification immediately before an active query is re-executed.
+Not-yet-consumed batches are discarded automatically; callers that retain
+an already-consumed prefix should discard it here. Omitting this callback
+leaves replay enabled and is appropriate for idempotent consumers.
+Optionalquery Timeout Ms
queryTimeoutMs?: numberDefault per-query deadline. Zero or undefined disables query deadlines.
+Optionalreconnect
Bounded failover policy. Failover and at-least-once active-query replay
+are enabled by default; set false to keep one fixed connection.
+Optionalserver Info Timeout Ms
serverInfoTimeoutMs?: numberSERVER_INFO handshake deadline. Defaults to 5 seconds.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpEgressViewQuery.html b/docs/interfaces/_questdb_browser-client.QwpEgressViewQuery.html
new file mode 100644
index 0000000..96912dd
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpEgressViewQuery.html
@@ -0,0 +1,9 @@
+QwpEgressViewQuery | QuestDB JavaScript Client - v4.2.0 Interface QwpEgressViewQuery
Control handle returned by queryViews().
+ interface QwpEgressViewQuery {
completion: Promise<QwpQueryCompletion>;
requestId: bigint;
awaitCompletion(timeoutMs: number): Promise<boolean>;
cancel(): Promise<void>;
grantCredit(additionalBytes: number | bigint): Promise<void>;
isDone(): boolean;
}Index
Properties
Methods
diff --git a/docs/interfaces/_questdb_browser-client.QwpEncodedBinds.html b/docs/interfaces/_questdb_browser-client.QwpEncodedBinds.html
new file mode 100644
index 0000000..58d00af
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpEncodedBinds.html
@@ -0,0 +1,3 @@
+QwpEncodedBinds | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_browser-client.QwpExecDoneMessage.html b/docs/interfaces/_questdb_browser-client.QwpExecDoneMessage.html
new file mode 100644
index 0000000..db0654a
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpExecDoneMessage.html
@@ -0,0 +1,9 @@
+QwpExecDoneMessage | QuestDB JavaScript Client - v4.2.0 Interface QwpExecDoneMessage
interface QwpExecDoneMessage {
flags: number;
kind: "exec-done";
operationType: number;
payloadLength: number;
requestId: bigint;
rowsAffected: bigint;
tableCount: number;
version: number;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpExecDoneMessage
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpFailoverAttempt.html b/docs/interfaces/_questdb_browser-client.QwpFailoverAttempt.html
new file mode 100644
index 0000000..238e257
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpFailoverAttempt.html
@@ -0,0 +1,3 @@
+QwpFailoverAttempt | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_browser-client.QwpFrame.html b/docs/interfaces/_questdb_browser-client.QwpFrame.html
new file mode 100644
index 0000000..6b92a56
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpFrame.html
@@ -0,0 +1,6 @@
+QwpFrame | QuestDB JavaScript Client - v4.2.0 Interface QwpFrame
interface QwpFrame {
flags: number;
payload: Uint8Array;
payloadLength: number;
tableCount: number;
version: number;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpFrame
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpFrameHeader.html b/docs/interfaces/_questdb_browser-client.QwpFrameHeader.html
new file mode 100644
index 0000000..72b1306
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpFrameHeader.html
@@ -0,0 +1,5 @@
+QwpFrameHeader | QuestDB JavaScript Client - v4.2.0 Interface QwpFrameHeader
interface QwpFrameHeader {
flags: number;
payloadLength: number;
tableCount: number;
version: number;
}Hierarchy (View Summary)
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpGeohashValue.html b/docs/interfaces/_questdb_browser-client.QwpGeohashValue.html
new file mode 100644
index 0000000..6b3eb08
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpGeohashValue.html
@@ -0,0 +1,3 @@
+QwpGeohashValue | QuestDB JavaScript Client - v4.2.0 Interface QwpGeohashValue
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpHandshakeMetadata.html b/docs/interfaces/_questdb_browser-client.QwpHandshakeMetadata.html
new file mode 100644
index 0000000..0ebc600
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpHandshakeMetadata.html
@@ -0,0 +1,16 @@
+QwpHandshakeMetadata | QuestDB JavaScript Client - v4.2.0 Interface QwpHandshakeMetadata
Metadata negotiated during the QWP WebSocket upgrade.
+ interface QwpHandshakeMetadata {
contentEncoding?: string;
durableAckEnabled?: boolean;
maxBatchSizeBytes?: number;
negotiatedCompression?: QwpNegotiatedEgressCompression;
qwpVersion: number;
serverRole?: string;
serverZone?: string;
}Index
Properties
Optional Readonlycontent Encoding
contentEncoding?: stringServer-selected egress content encoding, when advertised.
+Optional Readonlydurable Ack Enabled
durableAckEnabled?: booleanWhether the server confirmed durable-ACK support.
+Optional Readonlymax Batch Size Bytes
maxBatchSizeBytes?: numberServer's hard ingress WebSocket-payload cap, when advertised.
+Optional Readonlynegotiated Compression
Parsed effective egress codec and level selected by the server.
+Readonlyqwp Version
qwpVersion: numberQWP protocol version selected by the server.
+Optional Readonlyserver Role
serverRole?: stringServer role advertised on a successful upgrade, when available.
+Optional Readonlyserver Zone
serverZone?: stringServer zone advertised on a successful upgrade, when available.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressEncodeOptions.html b/docs/interfaces/_questdb_browser-client.QwpIngressEncodeOptions.html
new file mode 100644
index 0000000..58a6a29
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressEncodeOptions.html
@@ -0,0 +1,7 @@
+QwpIngressEncodeOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressEncodeOptions
interface QwpIngressEncodeOptions {
confirmedMaxSymbolId?: number;
deferCommit?: boolean;
dictionary?: QwpSymbolDictionary;
gorilla?: boolean;
}Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressErrorEvent.html b/docs/interfaces/_questdb_browser-client.QwpIngressErrorEvent.html
new file mode 100644
index 0000000..d32a7f0
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressErrorEvent.html
@@ -0,0 +1,8 @@
+QwpIngressErrorEvent | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressErrorEvent
interface QwpIngressErrorEvent {
error: Error;
metrics: QwpIngressMetrics;
response?: QwpIngressResponse;
senderError?: QwpSenderError;
terminal: boolean;
timestampMs: number;
}Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressMetrics.html b/docs/interfaces/_questdb_browser-client.QwpIngressMetrics.html
new file mode 100644
index 0000000..ca0bb98
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressMetrics.html
@@ -0,0 +1,42 @@
+QwpIngressMetrics | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressMetrics
Immutable point-in-time ingress telemetry, safe in browsers and Node.js.
+ interface QwpIngressMetrics {
acknowledgedSequence: bigint;
deliveredConnectionNotifications: number;
deliveredErrorNotifications: number;
deliveredProgressNotifications: number;
droppedConnectionNotifications: number;
droppedErrorNotifications: number;
droppedProgressNotifications: number;
lastError?: Error;
memoryReplayMaxBytes?: number;
memoryReplayUsedBytes?: number;
pendingDurableTables: number;
pendingReplayBytes: number;
pendingReplayFrames: number;
pendingResponseBytes: number;
pendingResponses: number;
publishedSequence: bigint;
replayAcknowledgedFrameSequence?: bigint;
replayPublishedFrameSequence?: bigint;
totalAcks: number;
totalBytesPublished: number;
totalBytesReplayed: number;
totalBytesSent: number;
totalDurableAcks: number;
totalErrors: number;
totalFailovers: number;
totalFramesPublished: number;
totalFramesReplayed: number;
totalFramesSent: number;
totalMemoryReplayAppendTimeouts: number;
totalMemoryReplayBackpressureStalls: number;
totalNacks: number;
totalReconnectAttempts: number;
totalReconnectErrors: number;
totalReconnectsSucceeded: number;
waitingMemoryReplayAppends: number;
}Index
Properties
acknowledged Sequence
+delivered Connection Notifications
+delivered Error Notifications
+delivered Progress Notifications
+dropped Connection Notifications
+dropped Error Notifications
+dropped Progress Notifications
+last Error?
+memory Replay Max Bytes?
+memory Replay Used Bytes?
+pending Durable Tables
+pending Replay Bytes
+pending Replay Frames
+pending Response Bytes
+pending Responses
+published Sequence
+replay Acknowledged Frame Sequence?
+replay Published Frame Sequence?
+total Acks
+total Bytes Published
+total Bytes Replayed
+total Bytes Sent
+total Durable Acks
+total Errors
+total Failovers
+total Frames Published
+total Frames Replayed
+total Frames Sent
+total Memory Replay Append Timeouts
+total Memory Replay Backpressure Stalls
+total Nacks
+total Reconnect Attempts
+total Reconnect Errors
+total Reconnects Succeeded
+waiting Memory Replay Appends
+Properties
Readonlyacknowledged Sequence
acknowledgedSequence: bigintHighest client-session sequence covered by a successful cumulative ACK.
+Readonlydelivered Connection Notifications
deliveredConnectionNotifications: numberReadonlydelivered Error Notifications
deliveredErrorNotifications: numberReadonlydelivered Progress Notifications
deliveredProgressNotifications: numberReadonlydropped Connection Notifications
droppedConnectionNotifications: numberReadonlydropped Error Notifications
droppedErrorNotifications: numberReadonlydropped Progress Notifications
droppedProgressNotifications: numberOptional Readonlylast Error
lastError?: ErrorOptional Readonlymemory Replay Max Bytes
memoryReplayMaxBytes?: numberOptional Readonlymemory Replay Used Bytes
memoryReplayUsedBytes?: numberReadonlypending Durable Tables
pendingDurableTables: numberReadonlypending Replay Bytes
pendingReplayBytes: numberReadonlypending Replay Frames
pendingReplayFrames: numberReadonlypending Response Bytes
pendingResponseBytes: numberReadonlypending Responses
pendingResponses: numberReadonlypublished Sequence
publishedSequence: bigintHighest client-session sequence allocated, or -1 before the first send.
+Optional Readonlyreplay Acknowledged Frame Sequence
replayAcknowledgedFrameSequence?: bigintTrim watermark; in durable-ACK mode it advances only after durability.
+Optional Readonlyreplay Published Frame Sequence
replayPublishedFrameSequence?: bigintStable store-and-forward watermark; absent without reconnect/replay.
+Readonlytotal Acks
totalAcks: numberReadonlytotal Bytes Published
totalBytesPublished: numberReadonlytotal Bytes Replayed
totalBytesReplayed: numberReadonlytotal Bytes Sent
totalBytesSent: numberReadonlytotal Durable Acks
totalDurableAcks: numberReadonlytotal Errors
totalErrors: numberReadonlytotal Failovers
totalFailovers: numberReadonlytotal Frames Published
totalFramesPublished: numberReadonlytotal Frames Replayed
totalFramesReplayed: numberReadonlytotal Frames Sent
totalFramesSent: numberPhysical sends; includes replay and dictionary catch-up when available.
+Readonlytotal Memory Replay Append Timeouts
totalMemoryReplayAppendTimeouts: numberReadonlytotal Memory Replay Backpressure Stalls
totalMemoryReplayBackpressureStalls: numberReadonlytotal Nacks
totalNacks: numberReadonlytotal Reconnect Attempts
totalReconnectAttempts: numberReadonlytotal Reconnect Errors
totalReconnectErrors: numberReadonlytotal Reconnects Succeeded
totalReconnectsSucceeded: numberReadonlywaiting Memory Replay Appends
waitingMemoryReplayAppends: number
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressProgressEvent.html b/docs/interfaces/_questdb_browser-client.QwpIngressProgressEvent.html
new file mode 100644
index 0000000..9baa2b9
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressProgressEvent.html
@@ -0,0 +1,6 @@
+QwpIngressProgressEvent | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressProgressEvent
interface QwpIngressProgressEvent {
kind: QwpIngressProgressKind;
metrics: QwpIngressMetrics;
response?: QwpIngressResponse;
sequence?: bigint;
timestampMs: number;
}Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressReplayRecord.html b/docs/interfaces/_questdb_browser-client.QwpIngressReplayRecord.html
new file mode 100644
index 0000000..3b9bf5f
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressReplayRecord.html
@@ -0,0 +1,3 @@
+QwpIngressReplayRecord | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressReplayRecord
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressReplayReference.html b/docs/interfaces/_questdb_browser-client.QwpIngressReplayReference.html
new file mode 100644
index 0000000..fbf1b86
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressReplayReference.html
@@ -0,0 +1,4 @@
+QwpIngressReplayReference | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressReplayReference
Lightweight durable-frame descriptor used by disk-backed replay stores.
+Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressReplayStore.html b/docs/interfaces/_questdb_browser-client.QwpIngressReplayStore.html
new file mode 100644
index 0000000..bda9907
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressReplayStore.html
@@ -0,0 +1,18 @@
+QwpIngressReplayStore | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressReplayStore
Browser-safe abstraction; Node supplies a persistent filesystem implementation.
+ interface QwpIngressReplayStore {
acknowledgeThrough(frameSequence: bigint): Promise<void>;
append(record: QwpIngressReplayRecord): Promise<void>;
appendSymbolDictionary(
startId: number,
entries: readonly string[],
): Promise<void>;
close(): Promise<void>;
load(): Promise<readonly QwpIngressReplayRecord[]>;
loadReferences(): Promise<readonly QwpIngressReplayReference[]>;
loadSymbolDictionary(): Promise<readonly string[]>;
readPayload(frameSequence: bigint): Promise<Uint8Array<ArrayBufferLike>>;
replaceSymbolDictionary(entries: readonly string[]): Promise<void>;
}Methods
acknowledge Through
Parameters
- frameSequence: bigint
Returns Promise<void>
append
Parameters
- record: QwpIngressReplayRecord
Returns Promise<void>
Optionalappend Symbol Dictionary
Persists new dense entries before a delta frame is made replayable.
+Parameters
- startId: number
- entries: readonly string[]
Returns Promise<void>
close
Returns Promise<void>
load
Returns Promise<readonly QwpIngressReplayRecord[]>
Optionalload References
Opens and validates the journal without materializing every payload.
+Implementations that provide this must also provide readPayload.
+Returns Promise<readonly QwpIngressReplayReference[]>
Optionalload Symbol Dictionary
Loads the durable, dense symbol prefix used by persisted delta frames.
+Returns Promise<readonly string[]>
Optionalread Payload
Reads one previously loaded durable payload on demand.
+Parameters
- frameSequence: bigint
Returns Promise<Uint8Array<ArrayBufferLike>>
Optionalreplace Symbol Dictionary
Atomically replaces an unusable dictionary after surviving committed
+frames prove that its complete ID space can be reconstructed.
+Parameters
- entries: readonly string[]
Returns Promise<void>
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressResponse.html b/docs/interfaces/_questdb_browser-client.QwpIngressResponse.html
new file mode 100644
index 0000000..d6e340c
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressResponse.html
@@ -0,0 +1,5 @@
+QwpIngressResponse | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressResponse
interface QwpIngressResponse {
errorMessage?: string;
sequence: null | bigint;
status: number;
tables: QwpIngressTableResult[];
}Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressSendResult.html b/docs/interfaces/_questdb_browser-client.QwpIngressSendResult.html
new file mode 100644
index 0000000..6cf6192
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressSendResult.html
@@ -0,0 +1,11 @@
+QwpIngressSendResult | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressSendResult
One ingress operation with independent local-publication and server-ACK
+completion. Publication resolves after every physical frame belonging to
+the logical batch has been accepted by the connection. For persistent Node
+transports that means the frames are durable in the replay journal.
+ interface QwpIngressSendResult {
acknowledgement: Promise<QwpIngressResponse>;
publication: Promise<void>;
sequence: bigint;
}Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressSessionOptions.html b/docs/interfaces/_questdb_browser-client.QwpIngressSessionOptions.html
new file mode 100644
index 0000000..2bdd0fd
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressSessionOptions.html
@@ -0,0 +1,56 @@
+QwpIngressSessionOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressSessionOptions
interface QwpIngressSessionOptions {
ackTimeoutMs?: number;
backgroundStoreAndForward?: boolean;
catchUpCapGapMinEscalationWindowMs?: number;
connectionListenerInboxCapacity?: number;
durableAckKeepaliveMs?: number;
errorInboxCapacity?: number;
initialConnectMode?: QwpInitialConnectMode;
maxBatchSizeBytes?: number;
memoryReplayAppendDeadlineMs?: number;
memoryReplayMaxBytes?: number;
onDurableAck?: (response: QwpIngressResponse) => void;
onError?: (event: QwpIngressErrorEvent) => void;
onProgress?: (event: QwpIngressProgressEvent) => void;
onResponse?: (response: QwpIngressResponse) => void;
onSenderError?: (error: QwpSenderError) => void;
orphanDurableAckMismatchMaxDurationMs?: number;
orphanStoreAndForward?: boolean;
reconnect?: false | QwpReconnectOptions;
replayStore?: QwpIngressReplayStore;
}Index
Properties
ack Timeout Ms?
+background Store And Forward?
+catch Up Cap Gap Min Escalation Window Ms?
+connection Listener Inbox Capacity?
+durable Ack Keepalive Ms?
+error Inbox Capacity?
+initial Connect Mode?
+max Batch Size Bytes?
+memory Replay Append Deadline Ms?
+memory Replay Max Bytes?
+on Durable Ack?
+on Error?
+on Progress?
+on Response?
+on Sender Error?
+orphan Durable Ack Mismatch Max Duration Ms?
+orphan Store And Forward?
+reconnect?
+replay Store?
+Properties
Optionalack Timeout Ms
ackTimeoutMs?: numberOptional Internalbackground Store And Forward
backgroundStoreAndForward?: booleanStarts memory or persistent replay without waiting for a server.
+Optional Internalcatch Up Cap Gap Min Escalation Window Ms
catchUpCapGapMinEscalationWindowMs?: numberMinimum cap-gap dwell before an orphan can be quarantined.
+Optionalconnection Listener Inbox Capacity
connectionListenerInboxCapacity?: numberBounded reconnect-listener inbox. Oldest pending events are dropped when
+full. Defaults to 64, matching the Java client.
+Optionaldurable Ack Keepalive Ms
durableAckKeepaliveMs?: numberEnables durable-ACK tracking. While committed table transactions await
+durable upload, Node transports send WebSocket PING frames and browser
+transports send table-less QWP poll frames. Zero keeps tracking enabled
+but disables automatic polling. Factory-created browser sessions require
+requestDurableAck=true when this option is supplied.
+Optionalerror Inbox Capacity
errorInboxCapacity?: numberBounded typed/legacy error inbox. Oldest pending errors are dropped when
+full. Defaults to 256, matching the Java client.
+Optional Internalinitial Connect Mode
Initial connection policy supplied by the Node adapter.
+Optionalmax Batch Size Bytes
maxBatchSizeBytes?: numberOptional local ingress frame cap. Browsers cannot read WebSocket upgrade
+headers, so browser applications should set this to the server's configured
+QWP cap. When the server also advertises a cap, the smaller value wins.
+Table batches are split at row boundaries automatically; an individual row
+that cannot fit is rejected with QwpBatchTooLargeError before it is sent.
+Optionalmemory Replay Append Deadline Ms
memoryReplayAppendDeadlineMs?: numberMaximum time a memory replay append waits for ACK-driven trimming after
+reaching memoryReplayMaxBytes. Defaults to 30 seconds.
+Optionalmemory Replay Max Bytes
memoryReplayMaxBytes?: numberHard cap for the built-in memory-only replay queue, including estimated
+per-frame bookkeeping. Defaults to 128 MiB. This applies in browsers and
+non-persistent Node sessions; custom replay stores enforce their own cap.
+Optionalon Durable Ack
Optionalon Error
Server rejections, deadlines, and terminal session failures.
+Optionalon Progress
Monotonic send/accept/durability notifications. Callback errors are ignored.
+Optionalon Response
Optionalon Sender Error
Java-parity typed server-rejection and data-loss notifications. When
+omitted, the default handler logs retriable errors at warn and terminal
+errors or abandoned data at error.
+Optional Internalorphan Durable Ack Mismatch Max Duration Ms
orphanDurableAckMismatchMaxDurationMs?: numberConsecutive durable-ACK gap budget retained for orphan SF.
+Optional Internalorphan Store And Forward
orphanStoreAndForward?: booleanOrphan sessions may quarantine persistent catch-up cap gaps.
+Optionalreconnect
Bounded reconnection and at-least-once replay policy. Reconnection is
+enabled by default for factory-created sessions; set false to keep one
+fixed connection. Browser and non-persistent Node replay is memory-only.
+An ACK lost during disconnect can cause a frame to be replayed after the
+server accepted it; configure server-side deduplication when duplicates
+are not acceptable.
+Optional Internalreplay Store
Node adapter hook for persistent store-and-forward.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressSymbolDictionaryDelta.html b/docs/interfaces/_questdb_browser-client.QwpIngressSymbolDictionaryDelta.html
new file mode 100644
index 0000000..91065e4
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressSymbolDictionaryDelta.html
@@ -0,0 +1,3 @@
+QwpIngressSymbolDictionaryDelta | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressTableResult.html b/docs/interfaces/_questdb_browser-client.QwpIngressTableResult.html
new file mode 100644
index 0000000..3d6f24e
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressTableResult.html
@@ -0,0 +1,3 @@
+QwpIngressTableResult | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressTableResult
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpIngressTransportMetrics.html b/docs/interfaces/_questdb_browser-client.QwpIngressTransportMetrics.html
new file mode 100644
index 0000000..cbfdfd7
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpIngressTransportMetrics.html
@@ -0,0 +1,29 @@
+QwpIngressTransportMetrics | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressTransportMetrics
Physical ingress delivery counters maintained by reconnecting transports.
+ interface QwpIngressTransportMetrics {
acknowledgedFrameSequence: bigint;
deliveredConnectionNotifications?: number;
deliveredErrorNotifications?: number;
droppedConnectionNotifications?: number;
droppedErrorNotifications?: number;
memoryReplayMaxBytes?: number;
memoryReplayUsedBytes?: number;
pendingReplayBytes: number;
pendingReplayFrames: number;
publishedFrameSequence: bigint;
totalBytesReplayed: number;
totalBytesSent: number;
totalFailovers: number;
totalFramesReplayed: number;
totalFramesSent: number;
totalMemoryReplayAppendTimeouts: number;
totalMemoryReplayBackpressureStalls: number;
totalReconnectAttempts: number;
totalReconnectErrors: number;
totalReconnectsSucceeded: number;
totalServerNacks: number;
waitingMemoryReplayAppends: number;
}Index
Properties
acknowledged Frame Sequence
+delivered Connection Notifications?
+delivered Error Notifications?
+dropped Connection Notifications?
+dropped Error Notifications?
+memory Replay Max Bytes?
+memory Replay Used Bytes?
+pending Replay Bytes
+pending Replay Frames
+published Frame Sequence
+total Bytes Replayed
+total Bytes Sent
+total Failovers
+total Frames Replayed
+total Frames Sent
+total Memory Replay Append Timeouts
+total Memory Replay Backpressure Stalls
+total Reconnect Attempts
+total Reconnect Errors
+total Reconnects Succeeded
+total Server Nacks
+waiting Memory Replay Appends
+Properties
Readonlyacknowledged Frame Sequence
acknowledgedFrameSequence: bigintHighest replay-frame sequence removed from store-and-forward.
+Optional Readonlydelivered Connection Notifications
deliveredConnectionNotifications?: numberOptional Readonlydelivered Error Notifications
deliveredErrorNotifications?: numberOptional Readonlydropped Connection Notifications
droppedConnectionNotifications?: numberOptional Readonlydropped Error Notifications
droppedErrorNotifications?: numberOptional Readonlymemory Replay Max Bytes
memoryReplayMaxBytes?: numberConfigured cap for the built-in memory replay store.
+Optional Readonlymemory Replay Used Bytes
memoryReplayUsedBytes?: numberEstimated payload and record-bookkeeping bytes charged to that cap.
+Readonlypending Replay Bytes
pendingReplayBytes: numberReadonlypending Replay Frames
pendingReplayFrames: numberReadonlypublished Frame Sequence
publishedFrameSequence: bigintHighest stable replay-frame sequence handed to the transport.
+Readonlytotal Bytes Replayed
totalBytesReplayed: numberReadonlytotal Bytes Sent
totalBytesSent: numberReadonlytotal Failovers
totalFailovers: numberReadonlytotal Frames Replayed
totalFramesReplayed: numberReadonlytotal Frames Sent
totalFramesSent: numberPhysical WebSocket sends, including replay and dictionary catch-up.
+Readonlytotal Memory Replay Append Timeouts
totalMemoryReplayAppendTimeouts: numberReadonlytotal Memory Replay Backpressure Stalls
totalMemoryReplayBackpressureStalls: numberReadonlytotal Reconnect Attempts
totalReconnectAttempts: numberReadonlytotal Reconnect Errors
totalReconnectErrors: numberReadonlytotal Reconnects Succeeded
totalReconnectsSucceeded: numberReadonlytotal Server Nacks
totalServerNacks: numberReadonlywaiting Memory Replay Appends
waitingMemoryReplayAppends: number
diff --git a/docs/interfaces/_questdb_browser-client.QwpLong256Value.html b/docs/interfaces/_questdb_browser-client.QwpLong256Value.html
new file mode 100644
index 0000000..ad486b6
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpLong256Value.html
@@ -0,0 +1,3 @@
+QwpLong256Value | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_browser-client.QwpPoolSlotReservation.html b/docs/interfaces/_questdb_browser-client.QwpPoolSlotReservation.html
new file mode 100644
index 0000000..36bef4c
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpPoolSlotReservation.html
@@ -0,0 +1,5 @@
+QwpPoolSlotReservation | QuestDB JavaScript Client - v4.2.0 Interface QwpPoolSlotReservationInternal
Cross-owner reservation for stable pooled sender slot indexes.
+ interface QwpPoolSlotReservation {
onAvailable(listener: () => void): () => void;
release(slot: number): void;
tryReserve(slot: number): boolean;
}Index
Methods
diff --git a/docs/interfaces/_questdb_browser-client.QwpQueryErrorMessage.html b/docs/interfaces/_questdb_browser-client.QwpQueryErrorMessage.html
new file mode 100644
index 0000000..28fc249
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpQueryErrorMessage.html
@@ -0,0 +1,9 @@
+QwpQueryErrorMessage | QuestDB JavaScript Client - v4.2.0 Interface QwpQueryErrorMessage
interface QwpQueryErrorMessage {
flags: number;
kind: "query-error";
message: string;
payloadLength: number;
requestId: bigint;
status: number;
tableCount: number;
version: number;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpQueryErrorMessage
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpQueryRequest.html b/docs/interfaces/_questdb_browser-client.QwpQueryRequest.html
new file mode 100644
index 0000000..ed7f7be
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpQueryRequest.html
@@ -0,0 +1,13 @@
+QwpQueryRequest | QuestDB JavaScript Client - v4.2.0 Interface QwpQueryRequest
interface QwpQueryRequest {
bindCount?: number;
bindPayload?: Uint8Array<ArrayBufferLike>;
binds?: QwpBindSetter;
initialCredit?: number | bigint;
queryFlags?: number | bigint;
requestId: number | bigint;
sql: string;
}Index
Properties
Properties
Optionalbind Count
bindCount?: numberAdvanced escape hatch for an already encoded bind section.
+Optionalbind Payload
bindPayload?: Uint8Array<ArrayBufferLike>Advanced escape hatch for an already encoded bind section.
+Optionalbinds
Browser-safe typed positional binds.
+Optionalinitial Credit
initialCredit?: number | bigintZero means unbounded.
+Optionalquery Flags
queryFlags?: number | bigintAppend only after SERVER_INFO advertises QUERY_FLAGS.
+request Id
requestId: number | bigintsql
sql: string
diff --git a/docs/interfaces/_questdb_browser-client.QwpReconnectEvent.html b/docs/interfaces/_questdb_browser-client.QwpReconnectEvent.html
new file mode 100644
index 0000000..2b46bdc
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpReconnectEvent.html
@@ -0,0 +1,10 @@
+QwpReconnectEvent | QuestDB JavaScript Client - v4.2.0 Interface QwpReconnectEvent
interface QwpReconnectEvent {
attempt: number;
cause?: unknown;
endpoint?: string | URL;
episodeMs?: number;
kind: QwpReconnectEventKind;
previousEndpoint?: string | URL;
timestampMs: number;
}Index
Properties
Properties
Readonlyattempt
attempt: numberOne-based reconnect sweep number; zero for lifecycle-only events.
+Optional Readonlycause
cause?: unknownOptional Readonlyendpoint
endpoint?: string | URLOptional Readonlyepisode Ms
episodeMs?: numberElapsed time in the current consecutive capability-gap episode.
+Readonlykind
Optional Readonlyprevious Endpoint
previousEndpoint?: string | URLReadonlytimestamp Ms
timestampMs: number
diff --git a/docs/interfaces/_questdb_browser-client.QwpReconnectOptions.html b/docs/interfaces/_questdb_browser-client.QwpReconnectOptions.html
new file mode 100644
index 0000000..04a4863
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpReconnectOptions.html
@@ -0,0 +1,17 @@
+QwpReconnectOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpReconnectOptions
interface QwpReconnectOptions {
initialBackoffMs?: number;
maxAttempts?: number;
maxBackoffMs?: number;
maxDurationMs?: number;
maxFrameRejections?: number;
onEvent?: (event: QwpReconnectEvent) => void;
poisonMinEscalationWindowMs?: number;
}Index
Properties
Optionalinitial Backoff Ms
initialBackoffMs?: numberFull-jitter ceiling before the first failed sweep is retried. Defaults to 100ms.
+Optionalmax Attempts
maxAttempts?: numberMaximum connection sweeps per outage. Defaults to 3; zero is unlimited.
+Optionalmax Backoff Ms
maxBackoffMs?: numberFull-jitter exponential-backoff ceiling. Defaults to 5s.
+Optionalmax Duration Ms
maxDurationMs?: numberTotal reconnect deadline. Defaults to 30s; zero disables the deadline.
+Optionalmax Frame Rejections
maxFrameRejections?: numberConsecutive retriable rejections of one ingress frame before it is treated
+as poison and retained for inspection. Defaults to 4.
+Optionalon Event
Optionalpoison Min Escalation Window Ms
poisonMinEscalationWindowMs?: numberMinimum time the same ingress frame must remain suspect before repeated
+rejections or non-orderly closes become terminal. Defaults to 5s; zero
+escalates as soon as maxFrameRejections is reached.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpResourcePoolMetrics.html b/docs/interfaces/_questdb_browser-client.QwpResourcePoolMetrics.html
new file mode 100644
index 0000000..58c65e9
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpResourcePoolMetrics.html
@@ -0,0 +1,8 @@
+QwpResourcePoolMetrics | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_browser-client.QwpResultArrayValue.html b/docs/interfaces/_questdb_browser-client.QwpResultArrayValue.html
new file mode 100644
index 0000000..8d6dcc0
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpResultArrayValue.html
@@ -0,0 +1,3 @@
+QwpResultArrayValue | QuestDB JavaScript Client - v4.2.0 Interface QwpResultArrayValue
interface QwpResultArrayValue {
dimensions: readonly number[];
values: readonly number[] | readonly bigint[];
}Index
Properties
dimensions
+values
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpResultBatchMessage.html b/docs/interfaces/_questdb_browser-client.QwpResultBatchMessage.html
new file mode 100644
index 0000000..9cc1f0d
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpResultBatchMessage.html
@@ -0,0 +1,11 @@
+QwpResultBatchMessage | QuestDB JavaScript Client - v4.2.0 Interface QwpResultBatchMessage
interface QwpResultBatchMessage {
batchSequence: bigint;
body: Uint8Array;
flags: number;
kind: "result-batch";
payloadLength: number;
requestId: bigint;
tableCount: number;
version: number;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpResultBatchMessage
Index
Properties
Properties
batch Sequence
batchSequence: bigintbody
body: Uint8ArrayRaw or Zstd-compressed delta dictionary and columnar table block; decoded
+by the batch decoder according to the frame flags.
+flags
flags: numberkind
kind: "result-batch"payload Length
payloadLength: numberrequest Id
requestId: biginttable Count
tableCount: numberversion
version: number
diff --git a/docs/interfaces/_questdb_browser-client.QwpResultColumn.html b/docs/interfaces/_questdb_browser-client.QwpResultColumn.html
new file mode 100644
index 0000000..c05f025
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpResultColumn.html
@@ -0,0 +1,6 @@
+QwpResultColumn | QuestDB JavaScript Client - v4.2.0 Interface QwpResultColumn
interface QwpResultColumn {
name: string;
precisionBits?: number;
scale?: number;
type: QwpColumnType;
values: readonly QwpResultValue[];
}Hierarchy (View Summary)
- QwpResultColumnSchema
- QwpResultColumn
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpResultColumnSchema.html b/docs/interfaces/_questdb_browser-client.QwpResultColumnSchema.html
new file mode 100644
index 0000000..4e8698a
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpResultColumnSchema.html
@@ -0,0 +1,3 @@
+QwpResultColumnSchema | QuestDB JavaScript Client - v4.2.0 Interface QwpResultColumnSchema
Hierarchy (View Summary)
- QwpResultColumnSchema
diff --git a/docs/interfaces/_questdb_browser-client.QwpResultEndMessage.html b/docs/interfaces/_questdb_browser-client.QwpResultEndMessage.html
new file mode 100644
index 0000000..ae0e630
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpResultEndMessage.html
@@ -0,0 +1,9 @@
+QwpResultEndMessage | QuestDB JavaScript Client - v4.2.0 Interface QwpResultEndMessage
interface QwpResultEndMessage {
finalSequence: bigint;
flags: number;
kind: "result-end";
payloadLength: number;
requestId: bigint;
tableCount: number;
totalRows: bigint;
version: number;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpResultEndMessage
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpSenderEncodeOptions.html b/docs/interfaces/_questdb_browser-client.QwpSenderEncodeOptions.html
new file mode 100644
index 0000000..bddc67c
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpSenderEncodeOptions.html
@@ -0,0 +1,4 @@
+QwpSenderEncodeOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpSenderEncodeOptions
Hierarchy
- Pick<QwpIngressEncodeOptions, "gorilla">
- QwpSenderEncodeOptions
Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpSenderError.html b/docs/interfaces/_questdb_browser-client.QwpSenderError.html
new file mode 100644
index 0000000..b848885
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpSenderError.html
@@ -0,0 +1,14 @@
+QwpSenderError | QuestDB JavaScript Client - v4.2.0 Interface QwpSenderError
Immutable Java-parity context for an ingress rejection or data loss.
+ interface QwpSenderError {
appliedPolicy: QwpSenderErrorPolicy;
category: QwpSenderErrorCategory;
detectedAtMs: number;
fromFsn?: bigint;
messageSequence?: bigint;
quarantinedPath?: string;
serverMessage?: string;
serverStatusByte?: number;
tableName?: string;
toFsn?: bigint;
}Index
Properties
Properties
Readonlyapplied Policy
Readonlycategory
Readonlydetected At Ms
detectedAtMs: numberOptional Readonlyfrom Fsn
fromFsn?: bigintInclusive stable store-and-forward frame-sequence range.
+Optional Readonlymessage Sequence
messageSequence?: bigintOptional Readonlyquarantined Path
quarantinedPath?: stringPreserved on-disk bytes for a data-loss/quarantine notification.
+Optional Readonlyserver Message
serverMessage?: stringOptional Readonlyserver Status Byte
serverStatusByte?: numberOptional Readonlytable Name
tableName?: stringOptional Readonlyto Fsn
toFsn?: bigint
diff --git a/docs/interfaces/_questdb_browser-client.QwpSenderErrorResponseContext.html b/docs/interfaces/_questdb_browser-client.QwpSenderErrorResponseContext.html
new file mode 100644
index 0000000..fd235f6
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpSenderErrorResponseContext.html
@@ -0,0 +1,7 @@
+QwpSenderErrorResponseContext | QuestDB JavaScript Client - v4.2.0 Interface QwpSenderErrorResponseContext
interface QwpSenderErrorResponseContext {
appliedPolicy?: QwpSenderErrorPolicy;
detectedAtMs?: number;
fromFsn?: bigint;
messageSequence?: bigint;
tableName?: string;
toFsn?: bigint;
}Index
Properties
diff --git a/docs/interfaces/_questdb_browser-client.QwpSenderMetrics.html b/docs/interfaces/_questdb_browser-client.QwpSenderMetrics.html
new file mode 100644
index 0000000..1d40427
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpSenderMetrics.html
@@ -0,0 +1,18 @@
+QwpSenderMetrics | QuestDB JavaScript Client - v4.2.0 Interface QwpSenderMetrics
Immutable high-level sender counters plus the active ingress snapshot.
+ interface QwpSenderMetrics {
autoFlushBytes: number;
closed: boolean;
closing: boolean;
connected: boolean;
deferredRows: number;
effectiveAutoFlushBytes: number;
ingress?: QwpIngressMetrics;
pendingBytes: number;
pendingRows: number;
totalFlushes: number;
totalFlushFailures: number;
totalRowsPublished: number;
totalRowsStaged: number;
totalTransactionsCommitted: number;
}Properties
Readonlyauto Flush Bytes
autoFlushBytes: numberReadonlyclosed
closed: booleanReadonlyclosing
closing: booleanReadonlyconnected
connected: booleanReadonlydeferred Rows
deferredRows: numberReadonlyeffective Auto Flush Bytes
effectiveAutoFlushBytes: numberOptional Readonlyingress
Readonlypending Bytes
pendingBytes: numberEstimated raw column-buffer bytes currently staged.
+Readonlypending Rows
pendingRows: numberReadonlytotal Flushes
totalFlushes: numberReadonlytotal Flush Failures
totalFlushFailures: numberReadonlytotal Rows Published
totalRowsPublished: numberRows whose encoded frames have entered the ingress session.
+Readonlytotal Rows Staged
totalRowsStaged: numberReadonlytotal Transactions Committed
totalTransactionsCommitted: number
diff --git a/docs/interfaces/_questdb_browser-client.QwpSenderOptions.html b/docs/interfaces/_questdb_browser-client.QwpSenderOptions.html
new file mode 100644
index 0000000..f89e9a1
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpSenderOptions.html
@@ -0,0 +1,31 @@
+QwpSenderOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpSenderOptions
Options for the browser-safe, fluent QWP sender.
+ interface QwpSenderOptions {
autoFlush?: boolean;
autoFlushBytes?: number;
autoFlushIntervalMs?: number;
autoFlushRows?: number;
awaitDurableAck?: boolean;
awaitServerAck?: boolean;
closeFlushTimeoutMs?: number;
durableAckTimeoutMs?: number;
encode?: QwpSenderEncodeOptions;
log?: QwpSenderLogger;
maxNameLength?: number;
transactional?: boolean;
}Properties
Optionalauto Flush
autoFlush?: booleanOptionalauto Flush Bytes
autoFlushBytes?: numberSoft threshold for estimated buffered column bytes. Zero disables the byte
+trigger. Defaults to zero and is clamped below a connected server's batch
+cap; exact encoded frames remain subject to the protocol batch limit.
+Optionalauto Flush Interval Ms
autoFlushIntervalMs?: numberOptionalauto Flush Rows
autoFlushRows?: numberOptionalawait Durable Ack
awaitDurableAck?: booleanWait for durable upload after every successful ingress ACK. When true,
+this implies awaitServerAck unless awaitServerAck is explicitly false.
+Optionalawait Server Ack
awaitServerAck?: booleanWait for the server's protocol ACK before flush()/commit() resolves.
+Defaults to false, matching the Java QWP sender's local-publication
+boundary. Set this to true for an acknowledgement barrier, or use
+flushAndGetSequence() followed by waitForAcknowledged().
+Optionalclose Flush Timeout Ms
closeFlushTimeoutMs?: numberMaximum time close() spends publishing queued rows and waiting for the
+server ACK watermark. Zero or a negative value skips the drain. Defaults
+to 5 seconds.
+Optionaldurable Ack Timeout Ms
durableAckTimeoutMs?: numberOptionalencode
QWP frame encoding options supported by the high-level sender.
+Optionallog
Optionalmax Name Length
maxNameLength?: numberMaximum UTF-8 byte length of table and column names. Defaults to 127.
+Optionaltransactional
transactional?: booleanKeep auto-flushed rows in an open server-side transaction. An explicit
+flush()/commit() closes the transaction. QWP transactions are atomic per
+table, rather than across every table in a multi-table flush.
+
diff --git a/docs/interfaces/_questdb_browser-client.QwpSenderSession.html b/docs/interfaces/_questdb_browser-client.QwpSenderSession.html
new file mode 100644
index 0000000..c38b35a
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpSenderSession.html
@@ -0,0 +1,15 @@
+QwpSenderSession | QuestDB JavaScript Client - v4.2.0 Interface QwpSenderSession
The subset of QwpIngressSession used by QwpSender.
+ interface QwpSenderSession {
acknowledgedFrameSequence?: bigint;
maxBatchSizeBytes?: number;
metrics?: QwpIngressMetrics;
publishedFrameSequence?: bigint;
close(code?: number, reason?: string): Promise<void>;
publishTables(
tables: readonly QwpTableBuffer[],
options?: QwpIngressEncodeOptions,
): Promise<void>;
publishTablesDelta(
tables: readonly QwpTableBuffer[],
options?: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">,
): Promise<void>;
sendTables(
tables: readonly QwpTableBuffer[],
options?: QwpIngressEncodeOptions,
): Promise<QwpIngressResponse>;
sendTablesDelta(
tables: readonly QwpTableBuffer[],
options?: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">,
): Promise<QwpIngressResponse>;
sendTablesDeltaWithPublication(
tables: readonly QwpTableBuffer[],
options?: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">,
): QwpIngressSendResult;
sendTablesWithPublication(
tables: readonly QwpTableBuffer[],
options?: QwpIngressEncodeOptions,
): QwpIngressSendResult;
waitForAcknowledged(
targetSequence: bigint,
timeoutMs?: number,
): Promise<void>;
waitForDurable(
response: QwpIngressResponse,
timeoutMs?: number,
): Promise<void>;
}Properties
Optional Readonlyacknowledged Frame Sequence
acknowledgedFrameSequence?: bigintOptional Readonlymax Batch Size Bytes
maxBatchSizeBytes?: numberOptional Readonlymetrics
Optional Readonlypublished Frame Sequence
publishedFrameSequence?: bigintMethods
close
Parameters
Optionalcode: numberOptionalreason: string
Returns Promise<void>
Optionalpublish Tables
- publishTables(
tables: readonly QwpTableBuffer[],
options?: QwpIngressEncodeOptions,
): Promise<void>Parameters
- tables: readonly QwpTableBuffer[]
Optionaloptions: QwpIngressEncodeOptions
Returns Promise<void>
Optionalpublish Tables Delta
- publishTablesDelta(
tables: readonly QwpTableBuffer[],
options?: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">,
): Promise<void>Parameters
- tables: readonly QwpTableBuffer[]
Optionaloptions: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">
Returns Promise<void>
send Tables
- sendTables(
tables: readonly QwpTableBuffer[],
options?: QwpIngressEncodeOptions,
): Promise<QwpIngressResponse>Parameters
- tables: readonly QwpTableBuffer[]
Optionaloptions: QwpIngressEncodeOptions
Returns Promise<QwpIngressResponse>
Optionalsend Tables Delta
- sendTablesDelta(
tables: readonly QwpTableBuffer[],
options?: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">,
): Promise<QwpIngressResponse>Parameters
- tables: readonly QwpTableBuffer[]
Optionaloptions: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">
Returns Promise<QwpIngressResponse>
Optionalsend Tables Delta With Publication
- sendTablesDeltaWithPublication(
tables: readonly QwpTableBuffer[],
options?: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">,
): QwpIngressSendResultParameters
- tables: readonly QwpTableBuffer[]
Optionaloptions: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">
Returns QwpIngressSendResult
Optionalsend Tables With Publication
- sendTablesWithPublication(
tables: readonly QwpTableBuffer[],
options?: QwpIngressEncodeOptions,
): QwpIngressSendResultParameters
- tables: readonly QwpTableBuffer[]
Optionaloptions: QwpIngressEncodeOptions
Returns QwpIngressSendResult
Optionalwait For Acknowledged
Parameters
- targetSequence: bigint
OptionaltimeoutMs: number
Returns Promise<void>
wait For Durable
Parameters
- response: QwpIngressResponse
OptionaltimeoutMs: number
Returns Promise<void>
diff --git a/docs/interfaces/_questdb_browser-client.QwpServerInfoMessage.html b/docs/interfaces/_questdb_browser-client.QwpServerInfoMessage.html
new file mode 100644
index 0000000..946ea70
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpServerInfoMessage.html
@@ -0,0 +1,16 @@
+QwpServerInfoMessage | QuestDB JavaScript Client - v4.2.0 Interface QwpServerInfoMessage
Immutable endpoint metadata from the most recent successful egress bind.
+ interface QwpServerInfoMessage {
capabilities: number;
clusterId: string;
compressionCodec: null | number;
compressionLevel: null | number;
epoch: bigint;
flags: number;
kind: "server-info";
nodeId: string;
payloadLength: number;
role: number;
serverWallNanoseconds: bigint;
tableCount: number;
version: number;
zoneId: null | string;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpServerInfoMessage
Index
Properties
Properties
capabilities
capabilities: numbercluster Id
clusterId: stringcompression Codec
compressionCodec: null | numbercompression Level
compressionLevel: null | numberepoch
epoch: bigintflags
flags: numberkind
kind: "server-info"node Id
nodeId: stringpayload Length
payloadLength: numberrole
role: numberserver Wall Nanoseconds
serverWallNanoseconds: biginttable Count
tableCount: numberversion
version: numberzone Id
zoneId: null | string
diff --git a/docs/interfaces/_questdb_browser-client.QwpSymbolValue.html b/docs/interfaces/_questdb_browser-client.QwpSymbolValue.html
new file mode 100644
index 0000000..90159d0
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpSymbolValue.html
@@ -0,0 +1,3 @@
+QwpSymbolValue | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_browser-client.QwpUpgradeErrorDetails.html b/docs/interfaces/_questdb_browser-client.QwpUpgradeErrorDetails.html
new file mode 100644
index 0000000..fff96fb
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpUpgradeErrorDetails.html
@@ -0,0 +1,14 @@
+QwpUpgradeErrorDetails | QuestDB JavaScript Client - v4.2.0 Interface QwpUpgradeErrorDetails
interface QwpUpgradeErrorDetails {
cause?: unknown;
closeCode?: number;
kind: QwpUpgradeErrorKind;
retryable?: boolean;
serverRole?: string;
serverZone?: string;
statusCode?: number;
statusMessage?: string;
timeoutPhase?: QwpUpgradeTimeoutPhase;
tryNextEndpoint?: boolean;
url?: string | URL;
}Index
Properties
Properties
Optionalcause
cause?: unknownOptionalclose Code
closeCode?: numberkind
Optionalretryable
retryable?: booleanWhether a later retry against the configured endpoint set may recover.
+Optionalserver Role
serverRole?: stringOptionalserver Zone
serverZone?: stringOptionalstatus Code
statusCode?: numberOptionalstatus Message
statusMessage?: stringOptionaltimeout Phase
Optionaltry Next Endpoint
tryNextEndpoint?: booleanWhether failover code should try another endpoint before surfacing this.
+Optionalurl
url?: string | URL
diff --git a/docs/interfaces/_questdb_browser-client.QwpUuidValue.html b/docs/interfaces/_questdb_browser-client.QwpUuidValue.html
new file mode 100644
index 0000000..56d8e59
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpUuidValue.html
@@ -0,0 +1,3 @@
+QwpUuidValue | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_browser-client.QwpWebSocketConnectOptions.html b/docs/interfaces/_questdb_browser-client.QwpWebSocketConnectOptions.html
new file mode 100644
index 0000000..594a181
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpWebSocketConnectOptions.html
@@ -0,0 +1,12 @@
+QwpWebSocketConnectOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpWebSocketConnectOptions
interface QwpWebSocketConnectOptions {
closeTimeoutMs?: number;
connectTimeoutMs?: number;
failoverUrls?: readonly (string | URL)[];
protocols?: string | string[];
sendTimeoutMs?: number;
url: string | URL;
}Hierarchy (View Summary)
- QwpWebSocketConnectOptions
Index
Properties
Properties
Optionalclose Timeout Ms
closeTimeoutMs?: numberMaximum time allowed for a graceful WebSocket close. Defaults to 15s.
+Optionalconnect Timeout Ms
connectTimeoutMs?: numberNode TCP/TLS connection deadline, or the complete opening deadline in a
+browser. Defaults to 15s.
+Optionalfailover Urls
failoverUrls?: readonly (string | URL)[]Additional endpoints attempted in order when the preferred endpoint fails.
+Optionalprotocols
protocols?: string | string[]Optionalsend Timeout Ms
sendTimeoutMs?: numberMaximum time a send may remain queued by the WebSocket. Defaults to 15s.
+url
url: string | URL
diff --git a/docs/interfaces/_questdb_browser-client.QwpWebSocketLike.html b/docs/interfaces/_questdb_browser-client.QwpWebSocketLike.html
new file mode 100644
index 0000000..f21c07b
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpWebSocketLike.html
@@ -0,0 +1,18 @@
+QwpWebSocketLike | QuestDB JavaScript Client - v4.2.0 Interface QwpWebSocketLike
interface QwpWebSocketLike {
binaryType: string;
bufferedAmount?: number;
protocol?: string;
readyState: number;
addEventListener(
type: "open",
listener: (event: unknown) => void,
options?: { once?: boolean },
): void;
addEventListener(
type: "message",
listener: (event: QwpWebSocketMessageEvent) => void,
): void;
addEventListener(
type: "error",
listener: (event: unknown) => void,
options?: { once?: boolean },
): void;
addEventListener(
type: "close",
listener: (event: QwpWebSocketCloseEvent) => void,
options?: { once?: boolean },
): void;
close(code?: number, reason?: string): void;
ping(): void;
removeEventListener(type: "open", listener: (event: unknown) => void): void;
removeEventListener(
type: "message",
listener: (event: QwpWebSocketMessageEvent) => void,
): void;
removeEventListener(
type: "error",
listener: (event: unknown) => void,
): void;
removeEventListener(
type: "close",
listener: (event: QwpWebSocketCloseEvent) => void,
): void;
send(data: Uint8Array): void;
sendWithCallback(data: Uint8Array, callback: (error?: Error) => void): void;
terminate(): void;
}Index
Properties
Methods
Properties
binary Type
binaryType: stringOptional Readonlybuffered Amount
bufferedAmount?: numberNumber of application bytes queued by WHATWG-compatible WebSockets.
+Optional Readonlyprotocol
protocol?: stringWebSocket subprotocol selected by the server, or an empty string.
+Readonlyready State
readyState: numberMethods
add Event Listener
- addEventListener(
type: "open",
listener: (event: unknown) => void,
options?: { once?: boolean },
): voidParameters
- type: "open"
- listener: (event: unknown) => void
Optionaloptions: { once?: boolean }
Returns void
Parameters
- type: "message"
- listener: (event: QwpWebSocketMessageEvent) => void
Returns void
- addEventListener(
type: "error",
listener: (event: unknown) => void,
options?: { once?: boolean },
): voidParameters
- type: "error"
- listener: (event: unknown) => void
Optionaloptions: { once?: boolean }
Returns void
- addEventListener(
type: "close",
listener: (event: QwpWebSocketCloseEvent) => void,
options?: { once?: boolean },
): voidParameters
- type: "close"
- listener: (event: QwpWebSocketCloseEvent) => void
Optionaloptions: { once?: boolean }
Returns void
close
Parameters
Optionalcode: numberOptionalreason: string
Returns void
Optionalping
Node WebSocket implementations may expose control-frame PING.
+Returns void
Optionalremove Event Listener
Optional cleanup hook implemented by browser WebSocket and Node ws.
+Parameters
- type: "open"
- listener: (event: unknown) => void
Returns void
Parameters
- type: "message"
- listener: (event: QwpWebSocketMessageEvent) => void
Returns void
Parameters
- type: "error"
- listener: (event: unknown) => void
Returns void
Parameters
- type: "close"
- listener: (event: QwpWebSocketCloseEvent) => void
Returns void
send
Parameters
- data: Uint8Array
Returns void
Optionalsend With Callback
Node adapter hook for the ws.send(data, callback) completion signal.
+Parameters
- data: Uint8Array
- callback: (error?: Error) => void
Returns void
Optionalterminate
Node WebSocket implementations may support immediate termination.
+Returns void
diff --git a/docs/interfaces/_questdb_browser-client.QwpWriterColumn.html b/docs/interfaces/_questdb_browser-client.QwpWriterColumn.html
new file mode 100644
index 0000000..d3acdec
--- /dev/null
+++ b/docs/interfaces/_questdb_browser-client.QwpWriterColumn.html
@@ -0,0 +1,19 @@
+QwpWriterColumn | QuestDB JavaScript Client - v4.2.0 Interface QwpWriterColumn<T, DesignatedTimestamp>
A reusable, immutable column definition for a compiled QWP table writer.
+ interface QwpWriterColumn<T, DesignatedTimestamp extends boolean = false> {
__qwpWriterInput?: T;
designatedTimestamp: DesignatedTimestamp;
kind: QwpWriterColumnKind;
precisionBits?: number;
scale?: number;
unit?: QwpTimestampUnit;
}Type Parameters
- T
- DesignatedTimestamp extends boolean = false
Index
Properties
Properties
Optional Readonly Internal__ qwp Writer Input
Carries the input type without adding a runtime value. Never
+assigned, and deliberately a plain property rather than a unique symbol:
+each emitted bundle would declare its own symbol, making the key nominally
+distinct per entry point. A column built by './qwp' would then satisfy
+another bundle's QwpWriterColumn without ever matching its phantom key, so
+QwpWriterColumnInput would infer unknown and every row field would
+silently accept anything. A shared property name resolves structurally
+across bundles, which is what keeps row typing alive for consumers of the
+published package.
+Readonlydesignated Timestamp
Readonlykind
Optional Readonlyprecision Bits
precisionBits?: numberGEOHASH precision in bits, fixed for the whole column.
+Optional Readonlyscale
scale?: numberDECIMAL scale, fixed for the whole column.
+Optional Readonlyunit
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpArrayValue.html b/docs/interfaces/_questdb_nodejs-client.QwpArrayValue.html
new file mode 100644
index 0000000..3a0e068
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpArrayValue.html
@@ -0,0 +1,3 @@
+QwpArrayValue | QuestDB JavaScript Client - v4.2.0 Interface QwpArrayValue
Index
Properties
dimensions
+values
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpBinaryConnection.html b/docs/interfaces/_questdb_nodejs-client.QwpBinaryConnection.html
new file mode 100644
index 0000000..aeb5278
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpBinaryConnection.html
@@ -0,0 +1,29 @@
+QwpBinaryConnection | QuestDB JavaScript Client - v4.2.0 Interface QwpBinaryConnection
Normalized binary connection consumed by QWP sessions.
+Adapters buffer messages until the single async iterator consumes them, so
+unsolicited frames such as egress SERVER_INFO cannot race session startup.
+ interface QwpBinaryConnection {
closed: Promise<QwpConnectionCloseInfo>;
endpoint?: string | URL;
handshake: QwpHandshakeMetadata;
ingressDeltaSymbolDictionaryEnabled?: boolean;
ingressSymbolDictionary?: readonly string[];
managesIngressSenderErrors?: boolean;
messages: AsyncIterable<Uint8Array<ArrayBufferLike>>;
close(code?: number, reason?: string): Promise<void>;
deprioritizeEndpoint(): void;
getIngressFrameSequence(clientSequence: bigint): bigint;
getIngressMetrics(): QwpIngressTransportMetrics;
ping(): Promise<void>;
send(payload: Uint8Array): Promise<void>;
skipIngressClientSequence(): void;
}Properties
Readonlyclosed
Optional Readonlyendpoint
endpoint?: string | URLEndpoint backing this connection, when supplied by its adapter.
+Readonlyhandshake
Optional Readonly Internalingress Delta Symbol Dictionary Enabled
ingressDeltaSymbolDictionaryEnabled?: booleanFalse after replay dictionary persistence becomes unavailable.
+Optional Readonly Internalingress Symbol Dictionary
ingressSymbolDictionary?: readonly string[]Recovered ingress dictionary supplied by replay connections.
+Optional Readonly Internalmanages Ingress Sender Errors
managesIngressSenderErrors?: booleanTrue when the transport dispatches typed sender errors itself.
+Readonlymessages
messages: AsyncIterable<Uint8Array<ArrayBufferLike>>Methods
close
Parameters
Optionalcode: numberOptionalreason: string
Returns Promise<void>
Optionaldeprioritize Endpoint
InternalMarks this endpoint as temporarily unsuitable and asks a stateful
+connection factory to start its next sweep at another configured endpoint.
+Returns void
Optionalget Ingress Frame Sequence
InternalResolves a session sequence to its stable replay FSN.
+Parameters
- clientSequence: bigint
Returns bigint
Optionalget Ingress Metrics
InternalPhysical delivery metrics exposed by replaying transports.
+Returns QwpIngressTransportMetrics
Optionalping
Sends an RFC 6455 PING when the underlying runtime supports it.
+Returns Promise<void>
send
Parameters
- payload: Uint8Array
Returns Promise<void>
Optionalskip Ingress Client Sequence
InternalReserves a client sequence for a split-batch suffix suppressed
+before send(), keeping replay ACK translation aligned with the session.
+Returns void
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpCacheResetMessage.html b/docs/interfaces/_questdb_nodejs-client.QwpCacheResetMessage.html
new file mode 100644
index 0000000..2867d86
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpCacheResetMessage.html
@@ -0,0 +1,7 @@
+QwpCacheResetMessage | QuestDB JavaScript Client - v4.2.0 Interface QwpCacheResetMessage
interface QwpCacheResetMessage {
flags: number;
kind: "cache-reset";
payloadLength: number;
resetMask: number;
tableCount: number;
version: number;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpCacheResetMessage
Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpClientFactories.html b/docs/interfaces/_questdb_nodejs-client.QwpClientFactories.html
new file mode 100644
index 0000000..0b84fbc
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpClientFactories.html
@@ -0,0 +1,9 @@
+QwpClientFactories | QuestDB JavaScript Client - v4.2.0 Interface QwpClientFactories
interface QwpClientFactories {
senderSlotReservation?: QwpPoolSlotReservation;
close(): void | Promise<void>;
createQuerySession(
slot: number,
signal?: AbortSignal,
): Promise<QwpEgressSession>;
createSender(slot: number, signal?: AbortSignal): Promise<QwpSender>;
start(): void | Promise<void>;
}Index
Properties
Methods
Properties
Optional Internalsender Slot Reservation
Coordinates stable persistent sender slots with recovery.
+Methods
Optionalclose
InternalStops runtime-specific background services during close.
+Returns void | Promise<void>
create Query Session
Parameters
- slot: number
Optionalsignal: AbortSignal
Returns Promise<QwpEgressSession>
create Sender
Parameters
- slot: number
Optionalsignal: AbortSignal
Returns Promise<QwpSender>
Optionalstart
InternalStarts runtime-specific background services on first use.
+Returns void | Promise<void>
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpClientMetrics.html b/docs/interfaces/_questdb_nodejs-client.QwpClientMetrics.html
new file mode 100644
index 0000000..21cb28e
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpClientMetrics.html
@@ -0,0 +1,5 @@
+QwpClientMetrics | QuestDB JavaScript Client - v4.2.0 Interface QwpClientMetrics
interface QwpClientMetrics {
closed: boolean;
closing: boolean;
queries: QwpResourcePoolMetrics;
senders: QwpResourcePoolMetrics;
}
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpClientPoolOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpClientPoolOptions.html
new file mode 100644
index 0000000..a893d06
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpClientPoolOptions.html
@@ -0,0 +1,18 @@
+QwpClientPoolOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpClientPoolOptions
interface QwpClientPoolOptions {
acquireTimeoutMs?: number;
housekeepingIntervalMs?: number;
idleTimeoutMs?: number;
maxLifetimeMs?: number;
queryPoolMax?: number;
queryPoolMin?: number;
senderPoolMax?: number;
senderPoolMin?: number;
}Index
Properties
Optionalacquire Timeout Ms
acquireTimeoutMs?: numberMaximum wait for a returned pool slot and for leases during shutdown.
+The shutdown wait is capped at 5 seconds. Defaults to 5 seconds.
+Optionalhousekeeping Interval Ms
housekeepingIntervalMs?: numberIdle/lifetime sweep interval. Defaults to 5s and must be at least 100ms.
+Optionalidle Timeout Ms
idleTimeoutMs?: numberIdle time before an excess pooled connection is closed. Defaults to 60s; zero disables.
+Optionalmax Lifetime Ms
maxLifetimeMs?: numberMaximum pooled connection age before recycling it while idle. Defaults to 30m; zero disables.
+Optionalquery Pool Max
queryPoolMax?: numberMaximum concurrently borrowed query connections. Defaults to 4.
+Optionalquery Pool Min
queryPoolMin?: numberWarm egress connections created by connect(). Defaults to 1.
+Optionalsender Pool Max
senderPoolMax?: numberMaximum concurrently borrowed ingress senders. Defaults to 4.
+Optionalsender Pool Min
senderPoolMin?: numberWarm ingress connections created by connect(). Defaults to 1.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpColumnBuffer.html b/docs/interfaces/_questdb_nodejs-client.QwpColumnBuffer.html
new file mode 100644
index 0000000..ccfa818
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpColumnBuffer.html
@@ -0,0 +1,11 @@
+QwpColumnBuffer | QuestDB JavaScript Client - v4.2.0 Interface QwpColumnBuffer
interface QwpColumnBuffer {
decimalScale?: number;
geohashPrecision?: number;
name: string;
nulls: boolean[];
size: number;
type: QwpColumnType;
values: unknown[];
}Index
Properties
Properties
Optionaldecimal Scale
decimalScale?: numberOptionalgeohash Precision
geohashPrecision?: numbername
name: stringnulls
nulls: boolean[]One entry per row; true means NULL.
+size
size: numberRows accounted for so far, including nulls.
+type
values
values: unknown[]Non-null values only; QWP compacts values around the null bitmap.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpConnectionCloseInfo.html b/docs/interfaces/_questdb_nodejs-client.QwpConnectionCloseInfo.html
new file mode 100644
index 0000000..6210aa2
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpConnectionCloseInfo.html
@@ -0,0 +1,4 @@
+QwpConnectionCloseInfo | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpDecimalValue.html b/docs/interfaces/_questdb_nodejs-client.QwpDecimalValue.html
new file mode 100644
index 0000000..8f998be
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpDecimalValue.html
@@ -0,0 +1,3 @@
+QwpDecimalValue | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpEgressQueryOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpEgressQueryOptions.html
new file mode 100644
index 0000000..b0ac8c5
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpEgressQueryOptions.html
@@ -0,0 +1,17 @@
+QwpEgressQueryOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpEgressQueryOptions
interface QwpEgressQueryOptions {
autoCredit?: boolean;
bindCount?: number;
bindPayload?: Uint8Array;
binds?: QwpBindSetter;
initialCredit?: number | bigint;
resetDictionary?: boolean;
timeoutMs?: number;
}Index
Properties
Properties
Optionalauto Credit
autoCredit?: booleanReplenishes positive initial credit by each RESULT_BATCH wire size after
+the async iterator advances past that batch. Defaults to true.
+Optionalbind Count
bindCount?: numberAdvanced escape hatch for an already encoded bind section.
+Optionalbind Payload
bindPayload?: Uint8ArrayAdvanced escape hatch for an already encoded bind section.
+Optionalbinds
Sets typed positional parameters; index 0 maps to SQL placeholder $1.
+Optionalinitial Credit
initialCredit?: number | bigintOverrides session send-ahead credit. Zero explicitly disables flow control.
+Optionalreset Dictionary
resetDictionary?: booleanAsk a capable server to reset its connection-scoped symbol dictionary.
+Silently omitted when the server lacks QUERY_FLAGS for rolling upgrades.
+Optionaltimeout Ms
timeoutMs?: numberPer-query deadline overriding the session default. Zero disables it.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpEgressReplayResetEvent.html b/docs/interfaces/_questdb_nodejs-client.QwpEgressReplayResetEvent.html
new file mode 100644
index 0000000..45d2963
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpEgressReplayResetEvent.html
@@ -0,0 +1,8 @@
+QwpEgressReplayResetEvent | QuestDB JavaScript Client - v4.2.0 Interface QwpEgressReplayResetEvent
interface QwpEgressReplayResetEvent {
cause?: unknown;
endpoint?: string | URL;
previousEndpoint?: string | URL;
requestId: bigint;
serverInfo: QwpServerInfoMessage;
}Index
Properties
Properties
Optional Readonlycause
cause?: unknownOptional Readonlyendpoint
endpoint?: string | URLOptional Readonlyprevious Endpoint
previousEndpoint?: string | URLReadonlyrequest Id
requestId: bigintClient request being re-executed on the replacement connection.
+Readonlyserver Info
Authoritative SERVER_INFO received from the replacement endpoint.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpEgressRoutingOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpEgressRoutingOptions.html
new file mode 100644
index 0000000..d2ce187
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpEgressRoutingOptions.html
@@ -0,0 +1,8 @@
+QwpEgressRoutingOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpEgressRoutingOptions
Endpoint routing preferences. Named for egress, where they landed first, but
+ingress ranks and validates its endpoints with the same machinery and honours
+the same two keys.
+Hierarchy (View Summary)
- QwpEgressRoutingOptions
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpEgressSessionOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpEgressSessionOptions.html
new file mode 100644
index 0000000..10f3a24
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpEgressSessionOptions.html
@@ -0,0 +1,19 @@
+QwpEgressSessionOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpEgressSessionOptions
interface QwpEgressSessionOptions {
bufferPoolSize?: number;
cancelDrainTimeoutMs?: number;
initialCredit?: number | bigint;
onReplayReset?: (event: QwpEgressReplayResetEvent) => void | Promise<void>;
queryTimeoutMs?: number;
reconnect?: false | QwpReconnectOptions;
serverInfoTimeoutMs?: number;
}Index
Properties
Optionalbuffer Pool Size
bufferPoolSize?: numberMaximum decoded batches waiting for a consumer. Defaults to 4.
+Optionalcancel Drain Timeout Ms
cancelDrainTimeoutMs?: numberMaximum wait for a terminal response after CANCEL. Defaults to 5 seconds.
+Optionalinitial Credit
initialCredit?: number | bigintDefault per-query send-ahead credit. Defaults to zero (unbounded).
+Optionalon Replay Reset
Optional notification immediately before an active query is re-executed.
+Not-yet-consumed batches are discarded automatically; callers that retain
+an already-consumed prefix should discard it here. Omitting this callback
+leaves replay enabled and is appropriate for idempotent consumers.
+Optionalquery Timeout Ms
queryTimeoutMs?: numberDefault per-query deadline. Zero or undefined disables query deadlines.
+Optionalreconnect
Bounded failover policy. Failover and at-least-once active-query replay
+are enabled by default; set false to keep one fixed connection.
+Optionalserver Info Timeout Ms
serverInfoTimeoutMs?: numberSERVER_INFO handshake deadline. Defaults to 5 seconds.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpEgressViewQuery.html b/docs/interfaces/_questdb_nodejs-client.QwpEgressViewQuery.html
new file mode 100644
index 0000000..7cb2fbe
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpEgressViewQuery.html
@@ -0,0 +1,9 @@
+QwpEgressViewQuery | QuestDB JavaScript Client - v4.2.0 Interface QwpEgressViewQuery
Control handle returned by queryViews().
+ interface QwpEgressViewQuery {
completion: Promise<QwpQueryCompletion>;
requestId: bigint;
awaitCompletion(timeoutMs: number): Promise<boolean>;
cancel(): Promise<void>;
grantCredit(additionalBytes: number | bigint): Promise<void>;
isDone(): boolean;
}Index
Properties
Methods
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpEncodedBinds.html b/docs/interfaces/_questdb_nodejs-client.QwpEncodedBinds.html
new file mode 100644
index 0000000..de8088c
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpEncodedBinds.html
@@ -0,0 +1,3 @@
+QwpEncodedBinds | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpExecDoneMessage.html b/docs/interfaces/_questdb_nodejs-client.QwpExecDoneMessage.html
new file mode 100644
index 0000000..6cdd534
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpExecDoneMessage.html
@@ -0,0 +1,9 @@
+QwpExecDoneMessage | QuestDB JavaScript Client - v4.2.0 Interface QwpExecDoneMessage
interface QwpExecDoneMessage {
flags: number;
kind: "exec-done";
operationType: number;
payloadLength: number;
requestId: bigint;
rowsAffected: bigint;
tableCount: number;
version: number;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpExecDoneMessage
Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpFailoverAttempt.html b/docs/interfaces/_questdb_nodejs-client.QwpFailoverAttempt.html
new file mode 100644
index 0000000..851dce2
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpFailoverAttempt.html
@@ -0,0 +1,3 @@
+QwpFailoverAttempt | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpFrame.html b/docs/interfaces/_questdb_nodejs-client.QwpFrame.html
new file mode 100644
index 0000000..75d4eee
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpFrame.html
@@ -0,0 +1,6 @@
+QwpFrame | QuestDB JavaScript Client - v4.2.0 Interface QwpFrame
interface QwpFrame {
flags: number;
payload: Uint8Array;
payloadLength: number;
tableCount: number;
version: number;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpFrame
Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpFrameHeader.html b/docs/interfaces/_questdb_nodejs-client.QwpFrameHeader.html
new file mode 100644
index 0000000..1066225
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpFrameHeader.html
@@ -0,0 +1,5 @@
+QwpFrameHeader | QuestDB JavaScript Client - v4.2.0 Interface QwpFrameHeader
interface QwpFrameHeader {
flags: number;
payloadLength: number;
tableCount: number;
version: number;
}Hierarchy (View Summary)
Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpGeohashValue.html b/docs/interfaces/_questdb_nodejs-client.QwpGeohashValue.html
new file mode 100644
index 0000000..4137306
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpGeohashValue.html
@@ -0,0 +1,3 @@
+QwpGeohashValue | QuestDB JavaScript Client - v4.2.0 Interface QwpGeohashValue
Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpHandshakeMetadata.html b/docs/interfaces/_questdb_nodejs-client.QwpHandshakeMetadata.html
new file mode 100644
index 0000000..a018d36
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpHandshakeMetadata.html
@@ -0,0 +1,16 @@
+QwpHandshakeMetadata | QuestDB JavaScript Client - v4.2.0 Interface QwpHandshakeMetadata
Metadata negotiated during the QWP WebSocket upgrade.
+ interface QwpHandshakeMetadata {
contentEncoding?: string;
durableAckEnabled?: boolean;
maxBatchSizeBytes?: number;
negotiatedCompression?: QwpNegotiatedEgressCompression;
qwpVersion: number;
serverRole?: string;
serverZone?: string;
}Index
Properties
Optional Readonlycontent Encoding
contentEncoding?: stringServer-selected egress content encoding, when advertised.
+Optional Readonlydurable Ack Enabled
durableAckEnabled?: booleanWhether the server confirmed durable-ACK support.
+Optional Readonlymax Batch Size Bytes
maxBatchSizeBytes?: numberServer's hard ingress WebSocket-payload cap, when advertised.
+Optional Readonlynegotiated Compression
Parsed effective egress codec and level selected by the server.
+Readonlyqwp Version
qwpVersion: numberQWP protocol version selected by the server.
+Optional Readonlyserver Role
serverRole?: stringServer role advertised on a successful upgrade, when available.
+Optional Readonlyserver Zone
serverZone?: stringServer zone advertised on a successful upgrade, when available.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressEncodeOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressEncodeOptions.html
new file mode 100644
index 0000000..111e440
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressEncodeOptions.html
@@ -0,0 +1,7 @@
+QwpIngressEncodeOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressEncodeOptions
interface QwpIngressEncodeOptions {
confirmedMaxSymbolId?: number;
deferCommit?: boolean;
dictionary?: QwpSymbolDictionary;
gorilla?: boolean;
}Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressErrorEvent.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressErrorEvent.html
new file mode 100644
index 0000000..ce123d7
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressErrorEvent.html
@@ -0,0 +1,8 @@
+QwpIngressErrorEvent | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressErrorEvent
interface QwpIngressErrorEvent {
error: Error;
metrics: QwpIngressMetrics;
response?: QwpIngressResponse;
senderError?: QwpSenderError;
terminal: boolean;
timestampMs: number;
}Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressMetrics.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressMetrics.html
new file mode 100644
index 0000000..78ff936
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressMetrics.html
@@ -0,0 +1,42 @@
+QwpIngressMetrics | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressMetrics
Immutable point-in-time ingress telemetry, safe in browsers and Node.js.
+ interface QwpIngressMetrics {
acknowledgedSequence: bigint;
deliveredConnectionNotifications: number;
deliveredErrorNotifications: number;
deliveredProgressNotifications: number;
droppedConnectionNotifications: number;
droppedErrorNotifications: number;
droppedProgressNotifications: number;
lastError?: Error;
memoryReplayMaxBytes?: number;
memoryReplayUsedBytes?: number;
pendingDurableTables: number;
pendingReplayBytes: number;
pendingReplayFrames: number;
pendingResponseBytes: number;
pendingResponses: number;
publishedSequence: bigint;
replayAcknowledgedFrameSequence?: bigint;
replayPublishedFrameSequence?: bigint;
totalAcks: number;
totalBytesPublished: number;
totalBytesReplayed: number;
totalBytesSent: number;
totalDurableAcks: number;
totalErrors: number;
totalFailovers: number;
totalFramesPublished: number;
totalFramesReplayed: number;
totalFramesSent: number;
totalMemoryReplayAppendTimeouts: number;
totalMemoryReplayBackpressureStalls: number;
totalNacks: number;
totalReconnectAttempts: number;
totalReconnectErrors: number;
totalReconnectsSucceeded: number;
waitingMemoryReplayAppends: number;
}Index
Properties
acknowledged Sequence
+delivered Connection Notifications
+delivered Error Notifications
+delivered Progress Notifications
+dropped Connection Notifications
+dropped Error Notifications
+dropped Progress Notifications
+last Error?
+memory Replay Max Bytes?
+memory Replay Used Bytes?
+pending Durable Tables
+pending Replay Bytes
+pending Replay Frames
+pending Response Bytes
+pending Responses
+published Sequence
+replay Acknowledged Frame Sequence?
+replay Published Frame Sequence?
+total Acks
+total Bytes Published
+total Bytes Replayed
+total Bytes Sent
+total Durable Acks
+total Errors
+total Failovers
+total Frames Published
+total Frames Replayed
+total Frames Sent
+total Memory Replay Append Timeouts
+total Memory Replay Backpressure Stalls
+total Nacks
+total Reconnect Attempts
+total Reconnect Errors
+total Reconnects Succeeded
+waiting Memory Replay Appends
+Properties
Readonlyacknowledged Sequence
acknowledgedSequence: bigintHighest client-session sequence covered by a successful cumulative ACK.
+Readonlydelivered Connection Notifications
deliveredConnectionNotifications: numberReadonlydelivered Error Notifications
deliveredErrorNotifications: numberReadonlydelivered Progress Notifications
deliveredProgressNotifications: numberReadonlydropped Connection Notifications
droppedConnectionNotifications: numberReadonlydropped Error Notifications
droppedErrorNotifications: numberReadonlydropped Progress Notifications
droppedProgressNotifications: numberOptional Readonlylast Error
lastError?: ErrorOptional Readonlymemory Replay Max Bytes
memoryReplayMaxBytes?: numberOptional Readonlymemory Replay Used Bytes
memoryReplayUsedBytes?: numberReadonlypending Durable Tables
pendingDurableTables: numberReadonlypending Replay Bytes
pendingReplayBytes: numberReadonlypending Replay Frames
pendingReplayFrames: numberReadonlypending Response Bytes
pendingResponseBytes: numberReadonlypending Responses
pendingResponses: numberReadonlypublished Sequence
publishedSequence: bigintHighest client-session sequence allocated, or -1 before the first send.
+Optional Readonlyreplay Acknowledged Frame Sequence
replayAcknowledgedFrameSequence?: bigintTrim watermark; in durable-ACK mode it advances only after durability.
+Optional Readonlyreplay Published Frame Sequence
replayPublishedFrameSequence?: bigintStable store-and-forward watermark; absent without reconnect/replay.
+Readonlytotal Acks
totalAcks: numberReadonlytotal Bytes Published
totalBytesPublished: numberReadonlytotal Bytes Replayed
totalBytesReplayed: numberReadonlytotal Bytes Sent
totalBytesSent: numberReadonlytotal Durable Acks
totalDurableAcks: numberReadonlytotal Errors
totalErrors: numberReadonlytotal Failovers
totalFailovers: numberReadonlytotal Frames Published
totalFramesPublished: numberReadonlytotal Frames Replayed
totalFramesReplayed: numberReadonlytotal Frames Sent
totalFramesSent: numberPhysical sends; includes replay and dictionary catch-up when available.
+Readonlytotal Memory Replay Append Timeouts
totalMemoryReplayAppendTimeouts: numberReadonlytotal Memory Replay Backpressure Stalls
totalMemoryReplayBackpressureStalls: numberReadonlytotal Nacks
totalNacks: numberReadonlytotal Reconnect Attempts
totalReconnectAttempts: numberReadonlytotal Reconnect Errors
totalReconnectErrors: numberReadonlytotal Reconnects Succeeded
totalReconnectsSucceeded: numberReadonlywaiting Memory Replay Appends
waitingMemoryReplayAppends: number
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressProgressEvent.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressProgressEvent.html
new file mode 100644
index 0000000..faf2f0a
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressProgressEvent.html
@@ -0,0 +1,6 @@
+QwpIngressProgressEvent | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressProgressEvent
interface QwpIngressProgressEvent {
kind: QwpIngressProgressKind;
metrics: QwpIngressMetrics;
response?: QwpIngressResponse;
sequence?: bigint;
timestampMs: number;
}Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressReplayRecord.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressReplayRecord.html
new file mode 100644
index 0000000..7971faf
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressReplayRecord.html
@@ -0,0 +1,3 @@
+QwpIngressReplayRecord | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressReplayRecord
Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressReplayReference.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressReplayReference.html
new file mode 100644
index 0000000..30ed278
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressReplayReference.html
@@ -0,0 +1,4 @@
+QwpIngressReplayReference | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressReplayReference
Lightweight durable-frame descriptor used by disk-backed replay stores.
+Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressReplayStore.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressReplayStore.html
new file mode 100644
index 0000000..5f8e7d6
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressReplayStore.html
@@ -0,0 +1,18 @@
+QwpIngressReplayStore | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressReplayStore
Browser-safe abstraction; Node supplies a persistent filesystem implementation.
+ interface QwpIngressReplayStore {
acknowledgeThrough(frameSequence: bigint): Promise<void>;
append(record: QwpIngressReplayRecord): Promise<void>;
appendSymbolDictionary(
startId: number,
entries: readonly string[],
): Promise<void>;
close(): Promise<void>;
load(): Promise<readonly QwpIngressReplayRecord[]>;
loadReferences(): Promise<readonly QwpIngressReplayReference[]>;
loadSymbolDictionary(): Promise<readonly string[]>;
readPayload(frameSequence: bigint): Promise<Uint8Array<ArrayBufferLike>>;
replaceSymbolDictionary(entries: readonly string[]): Promise<void>;
}Implemented by
Methods
acknowledge Through
Parameters
- frameSequence: bigint
Returns Promise<void>
append
Parameters
- record: QwpIngressReplayRecord
Returns Promise<void>
Optionalappend Symbol Dictionary
Persists new dense entries before a delta frame is made replayable.
+Parameters
- startId: number
- entries: readonly string[]
Returns Promise<void>
close
Returns Promise<void>
load
Returns Promise<readonly QwpIngressReplayRecord[]>
Optionalload References
Opens and validates the journal without materializing every payload.
+Implementations that provide this must also provide readPayload.
+Returns Promise<readonly QwpIngressReplayReference[]>
Optionalload Symbol Dictionary
Loads the durable, dense symbol prefix used by persisted delta frames.
+Returns Promise<readonly string[]>
Optionalread Payload
Reads one previously loaded durable payload on demand.
+Parameters
- frameSequence: bigint
Returns Promise<Uint8Array<ArrayBufferLike>>
Optionalreplace Symbol Dictionary
Atomically replaces an unusable dictionary after surviving committed
+frames prove that its complete ID space can be reconstructed.
+Parameters
- entries: readonly string[]
Returns Promise<void>
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressResponse.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressResponse.html
new file mode 100644
index 0000000..08f7381
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressResponse.html
@@ -0,0 +1,5 @@
+QwpIngressResponse | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressResponse
interface QwpIngressResponse {
errorMessage?: string;
sequence: bigint;
status: number;
tables: QwpIngressTableResult[];
}Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressSendResult.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressSendResult.html
new file mode 100644
index 0000000..e0b6c9b
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressSendResult.html
@@ -0,0 +1,11 @@
+QwpIngressSendResult | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressSendResult
One ingress operation with independent local-publication and server-ACK
+completion. Publication resolves after every physical frame belonging to
+the logical batch has been accepted by the connection. For persistent Node
+transports that means the frames are durable in the replay journal.
+ interface QwpIngressSendResult {
acknowledgement: Promise<QwpIngressResponse>;
publication: Promise<void>;
sequence: bigint;
}Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressSessionOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressSessionOptions.html
new file mode 100644
index 0000000..46a2596
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressSessionOptions.html
@@ -0,0 +1,56 @@
+QwpIngressSessionOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressSessionOptions
interface QwpIngressSessionOptions {
ackTimeoutMs?: number;
backgroundStoreAndForward?: boolean;
catchUpCapGapMinEscalationWindowMs?: number;
connectionListenerInboxCapacity?: number;
durableAckKeepaliveMs?: number;
errorInboxCapacity?: number;
initialConnectMode?: QwpInitialConnectMode;
maxBatchSizeBytes?: number;
memoryReplayAppendDeadlineMs?: number;
memoryReplayMaxBytes?: number;
onDurableAck?: (response: QwpIngressResponse) => void;
onError?: (event: QwpIngressErrorEvent) => void;
onProgress?: (event: QwpIngressProgressEvent) => void;
onResponse?: (response: QwpIngressResponse) => void;
onSenderError?: (error: QwpSenderError) => void;
orphanDurableAckMismatchMaxDurationMs?: number;
orphanStoreAndForward?: boolean;
reconnect?: false | QwpReconnectOptions;
replayStore?: QwpIngressReplayStore;
}Index
Properties
ack Timeout Ms?
+background Store And Forward?
+catch Up Cap Gap Min Escalation Window Ms?
+connection Listener Inbox Capacity?
+durable Ack Keepalive Ms?
+error Inbox Capacity?
+initial Connect Mode?
+max Batch Size Bytes?
+memory Replay Append Deadline Ms?
+memory Replay Max Bytes?
+on Durable Ack?
+on Error?
+on Progress?
+on Response?
+on Sender Error?
+orphan Durable Ack Mismatch Max Duration Ms?
+orphan Store And Forward?
+reconnect?
+replay Store?
+Properties
Optionalack Timeout Ms
ackTimeoutMs?: numberOptional Internalbackground Store And Forward
backgroundStoreAndForward?: booleanStarts memory or persistent replay without waiting for a server.
+Optional Internalcatch Up Cap Gap Min Escalation Window Ms
catchUpCapGapMinEscalationWindowMs?: numberMinimum cap-gap dwell before an orphan can be quarantined.
+Optionalconnection Listener Inbox Capacity
connectionListenerInboxCapacity?: numberBounded reconnect-listener inbox. Oldest pending events are dropped when
+full. Defaults to 64, matching the Java client.
+Optionaldurable Ack Keepalive Ms
durableAckKeepaliveMs?: numberEnables durable-ACK tracking. While committed table transactions await
+durable upload, Node transports send WebSocket PING frames and browser
+transports send table-less QWP poll frames. Zero keeps tracking enabled
+but disables automatic polling. Factory-created browser sessions require
+requestDurableAck=true when this option is supplied.
+Optionalerror Inbox Capacity
errorInboxCapacity?: numberBounded typed/legacy error inbox. Oldest pending errors are dropped when
+full. Defaults to 256, matching the Java client.
+Optional Internalinitial Connect Mode
Initial connection policy supplied by the Node adapter.
+Optionalmax Batch Size Bytes
maxBatchSizeBytes?: numberOptional local ingress frame cap. Browsers cannot read WebSocket upgrade
+headers, so browser applications should set this to the server's configured
+QWP cap. When the server also advertises a cap, the smaller value wins.
+Table batches are split at row boundaries automatically; an individual row
+that cannot fit is rejected with QwpBatchTooLargeError before it is sent.
+Optionalmemory Replay Append Deadline Ms
memoryReplayAppendDeadlineMs?: numberMaximum time a memory replay append waits for ACK-driven trimming after
+reaching memoryReplayMaxBytes. Defaults to 30 seconds.
+Optionalmemory Replay Max Bytes
memoryReplayMaxBytes?: numberHard cap for the built-in memory-only replay queue, including estimated
+per-frame bookkeeping. Defaults to 128 MiB. This applies in browsers and
+non-persistent Node sessions; custom replay stores enforce their own cap.
+Optionalon Durable Ack
Optionalon Error
Server rejections, deadlines, and terminal session failures.
+Optionalon Progress
Monotonic send/accept/durability notifications. Callback errors are ignored.
+Optionalon Response
Optionalon Sender Error
Java-parity typed server-rejection and data-loss notifications. When
+omitted, the default handler logs retriable errors at warn and terminal
+errors or abandoned data at error.
+Optional Internalorphan Durable Ack Mismatch Max Duration Ms
orphanDurableAckMismatchMaxDurationMs?: numberConsecutive durable-ACK gap budget retained for orphan SF.
+Optional Internalorphan Store And Forward
orphanStoreAndForward?: booleanOrphan sessions may quarantine persistent catch-up cap gaps.
+Optionalreconnect
Bounded reconnection and at-least-once replay policy. Reconnection is
+enabled by default for factory-created sessions; set false to keep one
+fixed connection. Browser and non-persistent Node replay is memory-only.
+An ACK lost during disconnect can cause a frame to be replayed after the
+server accepted it; configure server-side deduplication when duplicates
+are not acceptable.
+Optional Internalreplay Store
Node adapter hook for persistent store-and-forward.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressSymbolDictionaryDelta.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressSymbolDictionaryDelta.html
new file mode 100644
index 0000000..022e4aa
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressSymbolDictionaryDelta.html
@@ -0,0 +1,3 @@
+QwpIngressSymbolDictionaryDelta | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressTableResult.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressTableResult.html
new file mode 100644
index 0000000..6eb9ee0
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressTableResult.html
@@ -0,0 +1,3 @@
+QwpIngressTableResult | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressTableResult
Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpIngressTransportMetrics.html b/docs/interfaces/_questdb_nodejs-client.QwpIngressTransportMetrics.html
new file mode 100644
index 0000000..2bf6fb0
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpIngressTransportMetrics.html
@@ -0,0 +1,29 @@
+QwpIngressTransportMetrics | QuestDB JavaScript Client - v4.2.0 Interface QwpIngressTransportMetrics
Physical ingress delivery counters maintained by reconnecting transports.
+ interface QwpIngressTransportMetrics {
acknowledgedFrameSequence: bigint;
deliveredConnectionNotifications?: number;
deliveredErrorNotifications?: number;
droppedConnectionNotifications?: number;
droppedErrorNotifications?: number;
memoryReplayMaxBytes?: number;
memoryReplayUsedBytes?: number;
pendingReplayBytes: number;
pendingReplayFrames: number;
publishedFrameSequence: bigint;
totalBytesReplayed: number;
totalBytesSent: number;
totalFailovers: number;
totalFramesReplayed: number;
totalFramesSent: number;
totalMemoryReplayAppendTimeouts: number;
totalMemoryReplayBackpressureStalls: number;
totalReconnectAttempts: number;
totalReconnectErrors: number;
totalReconnectsSucceeded: number;
totalServerNacks: number;
waitingMemoryReplayAppends: number;
}Index
Properties
acknowledged Frame Sequence
+delivered Connection Notifications?
+delivered Error Notifications?
+dropped Connection Notifications?
+dropped Error Notifications?
+memory Replay Max Bytes?
+memory Replay Used Bytes?
+pending Replay Bytes
+pending Replay Frames
+published Frame Sequence
+total Bytes Replayed
+total Bytes Sent
+total Failovers
+total Frames Replayed
+total Frames Sent
+total Memory Replay Append Timeouts
+total Memory Replay Backpressure Stalls
+total Reconnect Attempts
+total Reconnect Errors
+total Reconnects Succeeded
+total Server Nacks
+waiting Memory Replay Appends
+Properties
Readonlyacknowledged Frame Sequence
acknowledgedFrameSequence: bigintHighest replay-frame sequence removed from store-and-forward.
+Optional Readonlydelivered Connection Notifications
deliveredConnectionNotifications?: numberOptional Readonlydelivered Error Notifications
deliveredErrorNotifications?: numberOptional Readonlydropped Connection Notifications
droppedConnectionNotifications?: numberOptional Readonlydropped Error Notifications
droppedErrorNotifications?: numberOptional Readonlymemory Replay Max Bytes
memoryReplayMaxBytes?: numberConfigured cap for the built-in memory replay store.
+Optional Readonlymemory Replay Used Bytes
memoryReplayUsedBytes?: numberEstimated payload and record-bookkeeping bytes charged to that cap.
+Readonlypending Replay Bytes
pendingReplayBytes: numberReadonlypending Replay Frames
pendingReplayFrames: numberReadonlypublished Frame Sequence
publishedFrameSequence: bigintHighest stable replay-frame sequence handed to the transport.
+Readonlytotal Bytes Replayed
totalBytesReplayed: numberReadonlytotal Bytes Sent
totalBytesSent: numberReadonlytotal Failovers
totalFailovers: numberReadonlytotal Frames Replayed
totalFramesReplayed: numberReadonlytotal Frames Sent
totalFramesSent: numberPhysical WebSocket sends, including replay and dictionary catch-up.
+Readonlytotal Memory Replay Append Timeouts
totalMemoryReplayAppendTimeouts: numberReadonlytotal Memory Replay Backpressure Stalls
totalMemoryReplayBackpressureStalls: numberReadonlytotal Reconnect Attempts
totalReconnectAttempts: numberReadonlytotal Reconnect Errors
totalReconnectErrors: numberReadonlytotal Reconnects Succeeded
totalReconnectsSucceeded: numberReadonlytotal Server Nacks
totalServerNacks: numberReadonlywaiting Memory Replay Appends
waitingMemoryReplayAppends: number
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpLong256Value.html b/docs/interfaces/_questdb_nodejs-client.QwpLong256Value.html
new file mode 100644
index 0000000..b29f140
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpLong256Value.html
@@ -0,0 +1,3 @@
+QwpLong256Value | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeClientConfigOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeClientConfigOptions.html
new file mode 100644
index 0000000..990e50b
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeClientConfigOptions.html
@@ -0,0 +1,13 @@
+QwpNodeClientConfigOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeClientConfigOptions
Programmatic hooks layered over a unified ws/wss cluster string. Values in
+this object take precedence after the complete string has been validated.
+ interface QwpNodeClientConfigOptions {
egress?: Partial<
Pick<
QwpNodeEgressOptions,
"target"
| "zone"
| "compression"
| "compressionLevel"
| "maxBatchRows",
>,
>;
egressSession?: QwpEgressSessionOptions;
ingressSession?: QwpIngressSessionOptions;
pool?: QwpClientPoolOptions;
sender?: QwpSenderOptions;
storeAndForward?: QwpNodeStoreAndForwardOptions;
webSocket?: Partial<Omit<QwpNodeWebSocketOptions, "url" | "failoverUrls">>;
}Index
Properties
Properties
Optionalegress
egress?: Partial<
Pick<
QwpNodeEgressOptions,
"target"
| "zone"
| "compression"
| "compressionLevel"
| "maxBatchRows",
>,
>Egress-only routing and compression overrides.
+Optionalegress Session
Optionalingress Session
Optionalpool
Optionalsender
Optionalstore And Forward
Optional persistent ingress configuration; may supply/override sf_dir.
+Optionalweb Socket
Shared transport overrides applied to both ingress and egress.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeClientOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeClientOptions.html
new file mode 100644
index 0000000..9c9bcb1
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeClientOptions.html
@@ -0,0 +1,13 @@
+QwpNodeClientOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeClientOptions
Node configuration for a combined pooled QWP ingress/egress client.
+ interface QwpNodeClientOptions {
egress: QwpNodeEgressOptions;
egressSession?: QwpEgressSessionOptions;
ingress: QwpNodeIngressOptions;
ingressSession?: QwpIngressSessionOptions;
lazyConnect?: boolean;
pool?: QwpClientPoolOptions;
sender?: QwpSenderOptions;
}Index
Properties
Properties
egress
Optionalegress Session
ingress
Optionalingress Session
Optionallazy Connect
lazyConnect?: booleanCoordinates a non-blocking startup: ingress connects in the background,
+using memory replay when store-and-forward is absent, and the egress pool
+remains cold until the first query. Conflicts with a positive queryPoolMin
+or a non-async initialConnectMode.
+Optionalpool
Optionalsender
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeEgressOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeEgressOptions.html
new file mode 100644
index 0000000..6c46300
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeEgressOptions.html
@@ -0,0 +1,39 @@
+QwpNodeEgressOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeEgressOptions
Endpoint routing preferences. Named for egress, where they landed first, but
+ingress ranks and validates its endpoints with the same machinery and honours
+the same two keys.
+ interface QwpNodeEgressOptions {
agent?: Agent;
authorization?: string;
authTimeoutMs?: number;
clientId?: string;
closeTimeoutMs?: number;
compression?: QwpEgressCompression;
compressionLevel?: number;
connectTimeoutMs?: number;
failoverUrls?: readonly (string | URL)[];
headers?: Record<string, string>;
maxBatchRows?: number;
maxVersion?: number;
protocols?: string | string[];
requestDurableAck?: boolean;
sendTimeoutMs?: number;
target?: QwpTarget;
url: string | URL;
webSocketFactory?: (
url: string | URL,
options: {
agent?: Agent;
headers: Record<string, string>;
onConnected: () => void;
onUpgrade: (headers: IncomingHttpHeaders) => void;
onUpgradeRejected: (rejection: QwpNodeUpgradeRejection) => void;
protocols?: string | string[];
},
) => QwpWebSocketLike;
zone?: string;
}Hierarchy (View Summary)
- QwpNodeWebSocketOptions
- QwpEgressRoutingOptions
- QwpNodeEgressOptions
Properties
Optionalagent
agent?: AgentOptional HTTP(S) agent used for the WebSocket upgrade.
+Optionalauthorization
authorization?: stringOptionalauth Timeout Ms
authTimeoutMs?: numberTime allowed after TCP/TLS connection for HTTP authentication and the
+WebSocket upgrade. Defaults to 15s.
+Optionalclient Id
clientId?: stringOptionalclose Timeout Ms
closeTimeoutMs?: numberMaximum time allowed for a graceful WebSocket close. Defaults to 15s.
+Optionalcompression
Requests Zstd-compressed result batches. The default is raw, which
+preserves compatibility with servers that predate QWP compression.
+auto currently advertises the same ordered preference as zstd.
+Optionalcompression Level
compressionLevel?: numberZstd level hint sent to the server. Must be between 1 and 22.
+Optionalconnect Timeout Ms
connectTimeoutMs?: numberNode TCP/TLS connection deadline, or the complete opening deadline in a
+browser. Defaults to 15s.
+Optionalfailover Urls
failoverUrls?: readonly (string | URL)[]Additional endpoints attempted in order when the preferred endpoint fails.
+Optionalheaders
headers?: Record<string, string>Optionalmax Batch Rows
maxBatchRows?: numberRequests a server-side RESULT_BATCH row cap.
+Optionalmax Version
maxVersion?: numberOptionalprotocols
protocols?: string | string[]Optionalrequest Durable Ack
requestDurableAck?: booleanOptionalsend Timeout Ms
sendTimeoutMs?: numberMaximum time a send may remain queued by the WebSocket. Defaults to 15s.
+Optionaltarget
Selects any readable node, a primary/standalone node, or a replica.
+url
url: string | URLOptionalweb Socket Factory
webSocketFactory?: (
url: string | URL,
options: {
agent?: Agent;
headers: Record<string, string>;
onConnected: () => void;
onUpgrade: (headers: IncomingHttpHeaders) => void;
onUpgradeRejected: (rejection: QwpNodeUpgradeRejection) => void;
protocols?: string | string[];
},
) => QwpWebSocketLikeTest hook; defaults to the Node-only ws implementation.
+Optionalzone
zone?: stringOpaque, case-insensitive preferred zone; cross-zone fallback stays enabled.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeFileReplayStoreMetrics.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeFileReplayStoreMetrics.html
new file mode 100644
index 0000000..eb3d267
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeFileReplayStoreMetrics.html
@@ -0,0 +1,14 @@
+QwpNodeFileReplayStoreMetrics | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeFileReplayStoreMetrics
interface QwpNodeFileReplayStoreMetrics {
backpressurePolicy: QwpSfBackpressurePolicy;
checkpointPending: boolean;
dirtyRecords: number;
durability: QwpSfDurability;
lastCheckpointError?: QwpReplayStoreCheckpointError;
pendingRecords: number;
pendingSegments: number;
totalAppendTimeouts: number;
totalBackpressureStalls: number;
totalBytes: number;
totalCheckpointFailures: number;
totalCheckpoints: number;
waitingAppends: number;
}Properties
Readonlybackpressure Policy
Readonlycheckpoint Pending
checkpointPending: booleanReadonlydirty Records
dirtyRecords: numberReadonlydurability
Optional Readonlylast Checkpoint Error
Readonlypending Records
pendingRecords: numberReadonlypending Segments
pendingSegments: numberReadonlytotal Append Timeouts
totalAppendTimeouts: numberReadonlytotal Backpressure Stalls
totalBackpressureStalls: numberReadonlytotal Bytes
totalBytes: numberReadonlytotal Checkpoint Failures
totalCheckpointFailures: numberReadonlytotal Checkpoints
totalCheckpoints: numberReadonlywaiting Appends
waitingAppends: number
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeFileReplayStoreOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeFileReplayStoreOptions.html
new file mode 100644
index 0000000..eeac681
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeFileReplayStoreOptions.html
@@ -0,0 +1,27 @@
+QwpNodeFileReplayStoreOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeFileReplayStoreOptions
interface QwpNodeFileReplayStoreOptions {
appendDeadlineMs?: number;
backpressurePolicy?: QwpSfBackpressurePolicy;
checkpointIntervalMs?: number;
directory: string;
durability?: QwpSfDurability;
maxBytes?: number;
maxSegmentBytes?: number;
onRecoveryDataLoss?: (report: QwpNodeReplayDataLossReport) => void;
}Hierarchy (View Summary)
- QwpNodeFileReplayStoreOptions
Index
Properties
Optionalappend Deadline Ms
appendDeadlineMs?: numberPer-append capacity or retryable store-fault deadline. Defaults to 30 seconds.
+Optionalbackpressure Policy
Behavior when maxBytes is exhausted. error fails immediately; wait
+pauses the append until ACK trimming frees space or its deadline expires.
+Defaults to error for backwards compatibility.
+Optionalcheckpoint Interval Ms
checkpointIntervalMs?: numberPeriodic durability checkpoint cadence. Defaults to 5 seconds.
+directory
directory: stringExclusive directory used by one ingress session.
+Optionaldurability
Local persistence barrier. append preserves the existing fsync-per-frame
+behavior, periodic checkpoints dirty files in the background, and
+memory relies on OS page-cache writeback. Defaults to append.
+Optionalmax Bytes
maxBytes?: numberTarget maximum journal size including fixed segment reservations and
+symbol metadata. Defaults to 1 GiB. The current symbol dictionary may
+exceed this target so it cannot consume the journal's live frame budget
+before a drained close retires that dictionary generation.
+Optionalmax Segment Bytes
maxSegmentBytes?: numberMaximum QWP frame payload and target segment data size. Each fixed segment
+reserves this value plus one record header and its 24-byte SFA header,
+so a maximum-sized frame still fits. Defaults to 4 MiB.
+Optionalon Recovery Data Loss
Reports journal bytes abandoned during recovery. Defaults to logging at
+error level; recovery still succeeds, so this must never be silent.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeIngressOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeIngressOptions.html
new file mode 100644
index 0000000..e0bffdf
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeIngressOptions.html
@@ -0,0 +1,37 @@
+QwpNodeIngressOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeIngressOptions
Endpoint routing preferences. Named for egress, where they landed first, but
+ingress ranks and validates its endpoints with the same machinery and honours
+the same two keys.
+ interface QwpNodeIngressOptions {
agent?: Agent;
authorization?: string;
authTimeoutMs?: number;
clientId?: string;
closeTimeoutMs?: number;
connectTimeoutMs?: number;
failoverUrls?: readonly (string | URL)[];
headers?: Record<string, string>;
maxVersion?: number;
protocols?: string | string[];
requestDurableAck?: boolean;
senderId?: string;
sendTimeoutMs?: number;
storeAndForward?: QwpNodeStoreAndForwardOptions;
target?: QwpTarget;
url: string | URL;
webSocketFactory?: (
url: string | URL,
options: {
agent?: Agent;
headers: Record<string, string>;
onConnected: () => void;
onUpgrade: (headers: IncomingHttpHeaders) => void;
onUpgradeRejected: (rejection: QwpNodeUpgradeRejection) => void;
protocols?: string | string[];
},
) => QwpWebSocketLike;
zone?: string;
}Hierarchy (View Summary)
- QwpNodeWebSocketOptions
- QwpEgressRoutingOptions
- QwpNodeIngressOptions
Properties
Optionalagent
agent?: AgentOptional HTTP(S) agent used for the WebSocket upgrade.
+Optionalauthorization
authorization?: stringOptionalauth Timeout Ms
authTimeoutMs?: numberTime allowed after TCP/TLS connection for HTTP authentication and the
+WebSocket upgrade. Defaults to 15s.
+Optionalclient Id
clientId?: stringOptionalclose Timeout Ms
closeTimeoutMs?: numberMaximum time allowed for a graceful WebSocket close. Defaults to 15s.
+Optionalconnect Timeout Ms
connectTimeoutMs?: numberNode TCP/TLS connection deadline, or the complete opening deadline in a
+browser. Defaults to 15s.
+Optionalfailover Urls
failoverUrls?: readonly (string | URL)[]Additional endpoints attempted in order when the preferred endpoint fails.
+Optionalheaders
headers?: Record<string, string>Optionalmax Version
maxVersion?: numberOptionalprotocols
protocols?: string | string[]Optionalrequest Durable Ack
requestDurableAck?: booleanOptionalsender Id
senderId?: stringSlot name below storeAndForward.directory. Unified configurations default
+to default; pooled clients derive <senderId>-<slot> names.
+Optionalsend Timeout Ms
sendTimeoutMs?: numberMaximum time a send may remain queued by the WebSocket. Defaults to 15s.
+Optionalstore And Forward
Upgrades the default in-memory ingress replay to persistent Node
+store-and-forward. Use a directory owned exclusively by this session.
+Optionaltarget
Selects any readable node, a primary/standalone node, or a replica.
+url
url: string | URLOptionalweb Socket Factory
webSocketFactory?: (
url: string | URL,
options: {
agent?: Agent;
headers: Record<string, string>;
onConnected: () => void;
onUpgrade: (headers: IncomingHttpHeaders) => void;
onUpgradeRejected: (rejection: QwpNodeUpgradeRejection) => void;
protocols?: string | string[];
},
) => QwpWebSocketLikeTest hook; defaults to the Node-only ws implementation.
+Optionalzone
zone?: stringOpaque, case-insensitive preferred zone; cross-zone fallback stays enabled.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeOrphanDrainEvent.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeOrphanDrainEvent.html
new file mode 100644
index 0000000..582a1d7
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeOrphanDrainEvent.html
@@ -0,0 +1,12 @@
+QwpNodeOrphanDrainEvent | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeOrphanDrainEvent
interface QwpNodeOrphanDrainEvent {
attempt?: number;
directory?: string;
episodeMs?: number;
error?: Error;
kind: QwpNodeOrphanDrainEventKind;
metrics: QwpNodeOrphanDrainerMetrics;
senderError?: QwpSenderError;
timestampMs: number;
}Index
Properties
Properties
Optional Readonlyattempt
attempt?: numberOne-based attempt in the current capability/topology episode.
+Optional Readonlydirectory
directory?: stringOptional Readonlyepisode Ms
episodeMs?: numberElapsed time in the current consecutive capability-gap episode.
+Optional Readonlyerror
error?: ErrorReadonlykind
Readonlymetrics
Optional Readonlysender Error
Present when a failed slot has been abandoned behind its sentinel.
+Readonlytimestamp Ms
timestampMs: number
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeOrphanDrainSession.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeOrphanDrainSession.html
new file mode 100644
index 0000000..fb3c235
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeOrphanDrainSession.html
@@ -0,0 +1,7 @@
+QwpNodeOrphanDrainSession | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeOrphanDrainSession
Minimal session surface used by the Node orphan drainer.
+ interface QwpNodeOrphanDrainSession {
closed: Promise<QwpConnectionCloseInfo>;
metrics: Pick<
QwpIngressTransportMetrics,
"pendingReplayFrames"
| "pendingReplayBytes",
> & { lastError?: Error };
close(code?: number, reason?: string): Promise<void>;
pollDurableAck(): Promise<void>;
}Index
Properties
Methods
Properties
Readonlyclosed
Readonlymetrics
metrics: Pick<
QwpIngressTransportMetrics,
"pendingReplayFrames"
| "pendingReplayBytes",
> & { lastError?: Error }
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeOrphanDrainerMetrics.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeOrphanDrainerMetrics.html
new file mode 100644
index 0000000..a9d3833
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeOrphanDrainerMetrics.html
@@ -0,0 +1,17 @@
+QwpNodeOrphanDrainerMetrics | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeOrphanDrainerMetrics
interface QwpNodeOrphanDrainerMetrics {
active: number;
closed: boolean;
closing: boolean;
deliveredErrorNotifications: number;
deliveredNotifications: number;
discovered: number;
drained: number;
droppedErrorNotifications: number;
droppedNotifications: number;
failed: number;
locked: number;
queued: number;
retrying: number;
scanFailures: number;
scans: number;
}Index
Properties
Readonlyactive
active: numberReadonlyclosed
closed: booleanReadonlyclosing
closing: booleanReadonlydelivered Error Notifications
deliveredErrorNotifications: numberReadonlydelivered Notifications
deliveredNotifications: numberReadonlydiscovered
discovered: numberReadonlydrained
drained: numberReadonlydropped Error Notifications
droppedErrorNotifications: numberReadonlydropped Notifications
droppedNotifications: numberReadonlyfailed
failed: numberReadonlylocked
locked: numberReadonlyqueued
queued: numberReadonlyretrying
retrying: numberAttempts that failed transiently and left the slot in place.
+Readonlyscan Failures
scanFailures: numberReadonlyscans
scans: number
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeOrphanDrainerOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeOrphanDrainerOptions.html
new file mode 100644
index 0000000..ee828f9
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeOrphanDrainerOptions.html
@@ -0,0 +1,25 @@
+QwpNodeOrphanDrainerOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeOrphanDrainerOptions
interface QwpNodeOrphanDrainerOptions {
durableAckPollIntervalMs?: number;
errorInboxCapacity?: number;
eventInboxCapacity?: number;
excludeSlot?: (slotName: string) => boolean;
maxConcurrent?: number;
onEvent?: (event: QwpNodeOrphanDrainEvent) => void;
onSenderError?: (error: QwpSenderError) => void;
releaseSlot?: (directory: string) => void;
rootDirectory: string;
scanIntervalMs?: number;
tryReserveSlot?: (directory: string) => boolean;
createSession(
directory: string,
onReconnectEvent?: (event: QwpReconnectEvent) => void,
): Promise<QwpNodeOrphanDrainSession>;
}Index
Properties
Optionaldurable Ack Poll Interval Ms
durableAckPollIntervalMs?: numberDurable-ACK prompt cadence for adopted sessions. Zero disables it.
+Optionalerror Inbox Capacity
errorInboxCapacity?: numberBounded data-loss inbox. Defaults to 256.
+Optionalevent Inbox Capacity
eventInboxCapacity?: numberBounded lifecycle-event inbox. Defaults to 64.
+Optionalexclude Slot
excludeSlot?: (slotName: string) => booleanSlot names owned by the foreground producer/pool and never adoptable.
+Optionalmax Concurrent
maxConcurrent?: numberMaximum slots drained concurrently. Defaults to 4.
+Optionalon Event
Optionalon Sender Error
Java-parity data-loss notification for an abandoned orphan slot.
+Optionalrelease Slot
releaseSlot?: (directory: string) => voidReleases a reservation previously granted by tryReserveSlot.
+root Directory
rootDirectory: stringDirectory whose child directories are independent replay slots.
+Optionalscan Interval Ms
scanIntervalMs?: numberPeriodic rescan cadence; zero disables the timer. Explicit scanNow()
+requests remain available. Defaults to 30s.
+Optionaltry Reserve Slot
tryReserveSlot?: (directory: string) => booleanAtomically reserves a candidate against a foreground pool owner.
+Methods
create Session
- createSession(
directory: string,
onReconnectEvent?: (event: QwpReconnectEvent) => void,
): Promise<QwpNodeOrphanDrainSession>Creates one independent replay session for an adopted slot.
+Parameters
- directory: string
OptionalonReconnectEvent: (event: QwpReconnectEvent) => void
Returns Promise<QwpNodeOrphanDrainSession>
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeReplayDataLossReport.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeReplayDataLossReport.html
new file mode 100644
index 0000000..f9ce148
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeReplayDataLossReport.html
@@ -0,0 +1,10 @@
+QwpNodeReplayDataLossReport | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeReplayDataLossReport
Frames discarded while recovering a damaged journal. Emitted instead of
+failing recovery when the damage sits in the active segment, matching the
+Java client, which zeroes an active torn tail by policy and reports the
+residue through a WARN plus MmapSegment.tornTailBytes().
+ interface QwpNodeReplayDataLossReport {
directory: string;
discardedBytes: number;
reason: string;
segmentFile: string;
}Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeReplayRecoveryEvent.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeReplayRecoveryEvent.html
new file mode 100644
index 0000000..9ecdd0c
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeReplayRecoveryEvent.html
@@ -0,0 +1,7 @@
+QwpNodeReplayRecoveryEvent | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeReplayRecoveryEvent
Notification that an unreplayable foreground slot was preserved aside.
+ interface QwpNodeReplayRecoveryEvent {
directory: string;
error: QwpReplayStoreQuarantinedError;
quarantineDirectory: string;
senderError: QwpSenderError;
timestampMs: number;
}Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeStoreAndForwardOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeStoreAndForwardOptions.html
new file mode 100644
index 0000000..9023d19
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeStoreAndForwardOptions.html
@@ -0,0 +1,50 @@
+QwpNodeStoreAndForwardOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeStoreAndForwardOptions
Node store-and-forward controls layered on the crash-safe replay journal.
+ interface QwpNodeStoreAndForwardOptions {
appendDeadlineMs?: number;
backpressurePolicy?: QwpSfBackpressurePolicy;
catchUpCapGapMinEscalationWindowMs?: number;
checkpointIntervalMs?: number;
directory: string;
drainOrphans?: boolean;
durability?: QwpSfDurability;
initialConnectMode?: QwpInitialConnectMode;
maxBackgroundDrainers?: number;
maxBytes?: number;
maxSegmentBytes?: number;
onOrphanDrainEvent?: (event: QwpNodeOrphanDrainEvent) => void;
onRecoveryDataLoss?: (report: QwpNodeReplayDataLossReport) => void;
onRecoveryQuarantine?: (event: QwpNodeReplayRecoveryEvent) => void;
orphanScanIntervalMs?: number;
}Hierarchy (View Summary)
- QwpNodeFileReplayStoreOptions
- QwpNodeStoreAndForwardOptions
Index
Properties
append Deadline Ms?
+backpressure Policy?
+catch Up Cap Gap Min Escalation Window Ms?
+checkpoint Interval Ms?
+directory
+drain Orphans?
+durability?
+initial Connect Mode?
+max Background Drainers?
+max Bytes?
+max Segment Bytes?
+on Orphan Drain Event?
+on Recovery Data Loss?
+on Recovery Quarantine?
+orphan Scan Interval Ms?
+Properties
Optionalappend Deadline Ms
appendDeadlineMs?: numberPer-append capacity or retryable store-fault deadline. Defaults to 30 seconds.
+Optionalbackpressure Policy
Behavior when maxBytes is exhausted. error fails immediately; wait
+pauses the append until ACK trimming frees space or its deadline expires.
+Defaults to error for backwards compatibility.
+Optionalcatch Up Cap Gap Min Escalation Window Ms
catchUpCapGapMinEscalationWindowMs?: numberMinimum time an orphan slot's symbol catch-up cap gap must persist before
+it is quarantined. The gap must also be observed 16 times. Defaults to
+five minutes; zero uses the observation threshold alone.
+Optionalcheckpoint Interval Ms
checkpointIntervalMs?: numberPeriodic durability checkpoint cadence. Defaults to 5 seconds.
+directory
directory: stringExclusive directory used by one ingress session.
+Optionaldrain Orphans
drainOrphans?: booleanAdopts sibling replay slots left by terminated producers. Standalone
+senders default this to false; pooled clients always recover their own
+idle in-range and out-of-range sender-N slots.
+Optionaldurability
Local persistence barrier. append preserves the existing fsync-per-frame
+behavior, periodic checkpoints dirty files in the background, and
+memory relies on OS page-cache writeback. Defaults to append.
+Optionalinitial Connect Mode
Initial server connection policy. Defaults to off; an explicitly tuned
+reconnect policy promotes it to sync, matching the Java client.
+Optionalmax Background Drainers
maxBackgroundDrainers?: numberMaximum sibling slots drained concurrently. Defaults to 4.
+Optionalmax Bytes
maxBytes?: numberTarget maximum journal size including fixed segment reservations and
+symbol metadata. Defaults to 1 GiB. The current symbol dictionary may
+exceed this target so it cannot consume the journal's live frame budget
+before a drained close retires that dictionary generation.
+Optionalmax Segment Bytes
maxSegmentBytes?: numberMaximum QWP frame payload and target segment data size. Each fixed segment
+reserves this value plus one record header and its 24-byte SFA header,
+so a maximum-sized frame still fits. Defaults to 4 MiB.
+Optionalon Orphan Drain Event
Receives isolated scanner, drainer, durable-ACK capability-gap, and
+primary-unavailable lifecycle notifications.
+Optionalon Recovery Data Loss
Reports journal bytes abandoned during recovery. Defaults to logging at
+error level; recovery still succeeds, so this must never be silent.
+Optionalon Recovery Quarantine
Receives a data-loss notification when corrupt foreground replay bytes are
+preserved under an .unreplayable-N pathname and a fresh slot is opened.
+Optionalorphan Scan Interval Ms
orphanScanIntervalMs?: numberPeriodic rescan cadence; zero disables the timer. Pooled ownership
+changes can still trigger a scan. Defaults to 30 seconds.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeUdpMetrics.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeUdpMetrics.html
new file mode 100644
index 0000000..9e43fa9
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeUdpMetrics.html
@@ -0,0 +1,6 @@
+QwpNodeUdpMetrics | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeUdpMetrics
interface QwpNodeUdpMetrics {
closed: boolean;
publishedDatagramSequence: bigint;
totalBytesSent: number;
totalDatagramsSent: number;
totalSendErrors: number;
}Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeUdpOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeUdpOptions.html
new file mode 100644
index 0000000..0f45aac
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeUdpOptions.html
@@ -0,0 +1,15 @@
+QwpNodeUdpOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeUdpOptions
interface QwpNodeUdpOptions {
host: string;
maxDatagramSize?: number;
multicastInterface?: string;
multicastTtl?: number;
onError?: (error: Error) => void;
port?: number;
socketFactory?: () => QwpNodeUdpSocketLike;
}Index
Properties
Properties
host
host: stringDestination hostname or IPv4 address.
+Optionalmax Datagram Size
maxDatagramSize?: numberMaximum encoded datagram size. Defaults to 1400 bytes.
+Optionalmulticast Interface
multicastInterface?: stringOptional local IPv4 interface used for multicast traffic.
+Optionalmulticast Ttl
multicastTtl?: numberIPv4 multicast TTL from 0 through 255. Defaults to 0.
+Optionalon Error
onError?: (error: Error) => voidReceives isolated local socket errors; UDP has no server acknowledgement.
+Optionalport
port?: numberDestination port. Defaults to the Java QWP UDP port, 9007.
+Optional Internalsocket Factory
Test hook.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeUdpSocketLike.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeUdpSocketLike.html
new file mode 100644
index 0000000..e388a21
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeUdpSocketLike.html
@@ -0,0 +1,8 @@
+QwpNodeUdpSocketLike | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeUdpSocketLike
Minimal injectable UDP socket surface used by the Node QWP sender.
+ interface QwpNodeUdpSocketLike {
bind(port: number, address: string, callback: () => void): void;
close(callback: () => void): void;
on(event: "error", listener: (error: Error) => void): unknown;
send(
message: Uint8Array,
port: number,
address: string,
callback: (error: Error, bytes: number) => void,
): void;
setMulticastInterface(multicastInterface: string): void;
setMulticastTTL(ttl: number): number;
}Index
Methods
Methods
bind
Parameters
- port: number
- address: string
- callback: () => void
Returns void
close
Parameters
- callback: () => void
Returns void
on
Parameters
- event: "error"
- listener: (error: Error) => void
Returns unknown
send
set Multicast Interface
Parameters
- multicastInterface: string
Returns void
set Multicast TTL
Parameters
- ttl: number
Returns number
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeUpgradeRejection.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeUpgradeRejection.html
new file mode 100644
index 0000000..2329153
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeUpgradeRejection.html
@@ -0,0 +1,4 @@
+QwpNodeUpgradeRejection | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeUpgradeRejection
interface QwpNodeUpgradeRejection {
headers: IncomingHttpHeaders;
statusCode: number;
statusMessage?: string;
}Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpNodeWebSocketOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpNodeWebSocketOptions.html
new file mode 100644
index 0000000..520c89b
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpNodeWebSocketOptions.html
@@ -0,0 +1,24 @@
+QwpNodeWebSocketOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpNodeWebSocketOptions
interface QwpNodeWebSocketOptions {
agent?: Agent;
authorization?: string;
authTimeoutMs?: number;
clientId?: string;
closeTimeoutMs?: number;
connectTimeoutMs?: number;
failoverUrls?: readonly (string | URL)[];
headers?: Record<string, string>;
maxVersion?: number;
protocols?: string | string[];
requestDurableAck?: boolean;
sendTimeoutMs?: number;
url: string | URL;
webSocketFactory?: (
url: string | URL,
options: {
agent?: Agent;
headers: Record<string, string>;
onConnected: () => void;
onUpgrade: (headers: IncomingHttpHeaders) => void;
onUpgradeRejected: (rejection: QwpNodeUpgradeRejection) => void;
protocols?: string | string[];
},
) => QwpWebSocketLike;
}Hierarchy (View Summary)
- QwpWebSocketConnectOptions
- QwpNodeWebSocketOptions
Index
Properties
Optionalagent
agent?: AgentOptional HTTP(S) agent used for the WebSocket upgrade.
+Optionalauthorization
authorization?: stringOptionalauth Timeout Ms
authTimeoutMs?: numberTime allowed after TCP/TLS connection for HTTP authentication and the
+WebSocket upgrade. Defaults to 15s.
+Optionalclient Id
clientId?: stringOptionalclose Timeout Ms
closeTimeoutMs?: numberMaximum time allowed for a graceful WebSocket close. Defaults to 15s.
+Optionalconnect Timeout Ms
connectTimeoutMs?: numberNode TCP/TLS connection deadline, or the complete opening deadline in a
+browser. Defaults to 15s.
+Optionalfailover Urls
failoverUrls?: readonly (string | URL)[]Additional endpoints attempted in order when the preferred endpoint fails.
+Optionalheaders
headers?: Record<string, string>Optionalmax Version
maxVersion?: numberOptionalprotocols
protocols?: string | string[]Optionalrequest Durable Ack
requestDurableAck?: booleanOptionalsend Timeout Ms
sendTimeoutMs?: numberMaximum time a send may remain queued by the WebSocket. Defaults to 15s.
+url
url: string | URLOptionalweb Socket Factory
webSocketFactory?: (
url: string | URL,
options: {
agent?: Agent;
headers: Record<string, string>;
onConnected: () => void;
onUpgrade: (headers: IncomingHttpHeaders) => void;
onUpgradeRejected: (rejection: QwpNodeUpgradeRejection) => void;
protocols?: string | string[];
},
) => QwpWebSocketLikeTest hook; defaults to the Node-only ws implementation.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpPoolSlotReservation.html b/docs/interfaces/_questdb_nodejs-client.QwpPoolSlotReservation.html
new file mode 100644
index 0000000..42d37f2
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpPoolSlotReservation.html
@@ -0,0 +1,5 @@
+QwpPoolSlotReservation | QuestDB JavaScript Client - v4.2.0 Interface QwpPoolSlotReservationInternal
Cross-owner reservation for stable pooled sender slot indexes.
+ interface QwpPoolSlotReservation {
onAvailable(listener: () => void): () => void;
release(slot: number): void;
tryReserve(slot: number): boolean;
}Index
Methods
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpQueryErrorMessage.html b/docs/interfaces/_questdb_nodejs-client.QwpQueryErrorMessage.html
new file mode 100644
index 0000000..0768e9f
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpQueryErrorMessage.html
@@ -0,0 +1,9 @@
+QwpQueryErrorMessage | QuestDB JavaScript Client - v4.2.0 Interface QwpQueryErrorMessage
interface QwpQueryErrorMessage {
flags: number;
kind: "query-error";
message: string;
payloadLength: number;
requestId: bigint;
status: number;
tableCount: number;
version: number;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpQueryErrorMessage
Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpQueryRequest.html b/docs/interfaces/_questdb_nodejs-client.QwpQueryRequest.html
new file mode 100644
index 0000000..464ee45
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpQueryRequest.html
@@ -0,0 +1,13 @@
+QwpQueryRequest | QuestDB JavaScript Client - v4.2.0 Interface QwpQueryRequest
interface QwpQueryRequest {
bindCount?: number;
bindPayload?: Uint8Array;
binds?: QwpBindSetter;
initialCredit?: number | bigint;
queryFlags?: number | bigint;
requestId: number | bigint;
sql: string;
}Index
Properties
Properties
Optionalbind Count
bindCount?: numberAdvanced escape hatch for an already encoded bind section.
+Optionalbind Payload
bindPayload?: Uint8ArrayAdvanced escape hatch for an already encoded bind section.
+Optionalbinds
Browser-safe typed positional binds.
+Optionalinitial Credit
initialCredit?: number | bigintZero means unbounded.
+Optionalquery Flags
queryFlags?: number | bigintAppend only after SERVER_INFO advertises QUERY_FLAGS.
+request Id
requestId: number | bigintsql
sql: string
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpReconnectEvent.html b/docs/interfaces/_questdb_nodejs-client.QwpReconnectEvent.html
new file mode 100644
index 0000000..d85cbec
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpReconnectEvent.html
@@ -0,0 +1,10 @@
+QwpReconnectEvent | QuestDB JavaScript Client - v4.2.0 Interface QwpReconnectEvent
interface QwpReconnectEvent {
attempt: number;
cause?: unknown;
endpoint?: string | URL;
episodeMs?: number;
kind: QwpReconnectEventKind;
previousEndpoint?: string | URL;
timestampMs: number;
}Index
Properties
Properties
Readonlyattempt
attempt: numberOne-based reconnect sweep number; zero for lifecycle-only events.
+Optional Readonlycause
cause?: unknownOptional Readonlyendpoint
endpoint?: string | URLOptional Readonlyepisode Ms
episodeMs?: numberElapsed time in the current consecutive capability-gap episode.
+Readonlykind
Optional Readonlyprevious Endpoint
previousEndpoint?: string | URLReadonlytimestamp Ms
timestampMs: number
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpReconnectOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpReconnectOptions.html
new file mode 100644
index 0000000..4bf0221
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpReconnectOptions.html
@@ -0,0 +1,17 @@
+QwpReconnectOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpReconnectOptions
interface QwpReconnectOptions {
initialBackoffMs?: number;
maxAttempts?: number;
maxBackoffMs?: number;
maxDurationMs?: number;
maxFrameRejections?: number;
onEvent?: (event: QwpReconnectEvent) => void;
poisonMinEscalationWindowMs?: number;
}Index
Properties
Optionalinitial Backoff Ms
initialBackoffMs?: numberFull-jitter ceiling before the first failed sweep is retried. Defaults to 100ms.
+Optionalmax Attempts
maxAttempts?: numberMaximum connection sweeps per outage. Defaults to 3; zero is unlimited.
+Optionalmax Backoff Ms
maxBackoffMs?: numberFull-jitter exponential-backoff ceiling. Defaults to 5s.
+Optionalmax Duration Ms
maxDurationMs?: numberTotal reconnect deadline. Defaults to 30s; zero disables the deadline.
+Optionalmax Frame Rejections
maxFrameRejections?: numberConsecutive retriable rejections of one ingress frame before it is treated
+as poison and retained for inspection. Defaults to 4.
+Optionalon Event
Optionalpoison Min Escalation Window Ms
poisonMinEscalationWindowMs?: numberMinimum time the same ingress frame must remain suspect before repeated
+rejections or non-orderly closes become terminal. Defaults to 5s; zero
+escalates as soon as maxFrameRejections is reached.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpResourcePoolMetrics.html b/docs/interfaces/_questdb_nodejs-client.QwpResourcePoolMetrics.html
new file mode 100644
index 0000000..d759a79
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpResourcePoolMetrics.html
@@ -0,0 +1,8 @@
+QwpResourcePoolMetrics | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpResultArrayValue.html b/docs/interfaces/_questdb_nodejs-client.QwpResultArrayValue.html
new file mode 100644
index 0000000..0c9daf3
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpResultArrayValue.html
@@ -0,0 +1,3 @@
+QwpResultArrayValue | QuestDB JavaScript Client - v4.2.0 Interface QwpResultArrayValue
interface QwpResultArrayValue {
dimensions: readonly number[];
values: readonly number[] | readonly bigint[];
}Index
Properties
dimensions
+values
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpResultBatchMessage.html b/docs/interfaces/_questdb_nodejs-client.QwpResultBatchMessage.html
new file mode 100644
index 0000000..180cfcf
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpResultBatchMessage.html
@@ -0,0 +1,11 @@
+QwpResultBatchMessage | QuestDB JavaScript Client - v4.2.0 Interface QwpResultBatchMessage
interface QwpResultBatchMessage {
batchSequence: bigint;
body: Uint8Array;
flags: number;
kind: "result-batch";
payloadLength: number;
requestId: bigint;
tableCount: number;
version: number;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpResultBatchMessage
Index
Properties
Properties
batch Sequence
batchSequence: bigintbody
body: Uint8ArrayRaw or Zstd-compressed delta dictionary and columnar table block; decoded
+by the batch decoder according to the frame flags.
+flags
flags: numberkind
kind: "result-batch"payload Length
payloadLength: numberrequest Id
requestId: biginttable Count
tableCount: numberversion
version: number
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpResultColumn.html b/docs/interfaces/_questdb_nodejs-client.QwpResultColumn.html
new file mode 100644
index 0000000..beb6668
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpResultColumn.html
@@ -0,0 +1,6 @@
+QwpResultColumn | QuestDB JavaScript Client - v4.2.0 Interface QwpResultColumn
interface QwpResultColumn {
name: string;
precisionBits?: number;
scale?: number;
type: QwpColumnType;
values: readonly QwpResultValue[];
}Hierarchy (View Summary)
- QwpResultColumnSchema
- QwpResultColumn
Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpResultColumnSchema.html b/docs/interfaces/_questdb_nodejs-client.QwpResultColumnSchema.html
new file mode 100644
index 0000000..4353115
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpResultColumnSchema.html
@@ -0,0 +1,3 @@
+QwpResultColumnSchema | QuestDB JavaScript Client - v4.2.0 Interface QwpResultColumnSchema
Hierarchy (View Summary)
- QwpResultColumnSchema
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpResultEndMessage.html b/docs/interfaces/_questdb_nodejs-client.QwpResultEndMessage.html
new file mode 100644
index 0000000..26b686f
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpResultEndMessage.html
@@ -0,0 +1,9 @@
+QwpResultEndMessage | QuestDB JavaScript Client - v4.2.0 Interface QwpResultEndMessage
interface QwpResultEndMessage {
finalSequence: bigint;
flags: number;
kind: "result-end";
payloadLength: number;
requestId: bigint;
tableCount: number;
totalRows: bigint;
version: number;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpResultEndMessage
Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpSenderEncodeOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpSenderEncodeOptions.html
new file mode 100644
index 0000000..40c2b3c
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpSenderEncodeOptions.html
@@ -0,0 +1,4 @@
+QwpSenderEncodeOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpSenderEncodeOptions
Hierarchy
- Pick<QwpIngressEncodeOptions, "gorilla">
- QwpSenderEncodeOptions
Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpSenderError.html b/docs/interfaces/_questdb_nodejs-client.QwpSenderError.html
new file mode 100644
index 0000000..eab6dd7
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpSenderError.html
@@ -0,0 +1,14 @@
+QwpSenderError | QuestDB JavaScript Client - v4.2.0 Interface QwpSenderError
Immutable Java-parity context for an ingress rejection or data loss.
+ interface QwpSenderError {
appliedPolicy: QwpSenderErrorPolicy;
category: QwpSenderErrorCategory;
detectedAtMs: number;
fromFsn?: bigint;
messageSequence?: bigint;
quarantinedPath?: string;
serverMessage?: string;
serverStatusByte?: number;
tableName?: string;
toFsn?: bigint;
}Index
Properties
Properties
Readonlyapplied Policy
Readonlycategory
Readonlydetected At Ms
detectedAtMs: numberOptional Readonlyfrom Fsn
fromFsn?: bigintInclusive stable store-and-forward frame-sequence range.
+Optional Readonlymessage Sequence
messageSequence?: bigintOptional Readonlyquarantined Path
quarantinedPath?: stringPreserved on-disk bytes for a data-loss/quarantine notification.
+Optional Readonlyserver Message
serverMessage?: stringOptional Readonlyserver Status Byte
serverStatusByte?: numberOptional Readonlytable Name
tableName?: stringOptional Readonlyto Fsn
toFsn?: bigint
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpSenderErrorResponseContext.html b/docs/interfaces/_questdb_nodejs-client.QwpSenderErrorResponseContext.html
new file mode 100644
index 0000000..0a8344b
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpSenderErrorResponseContext.html
@@ -0,0 +1,7 @@
+QwpSenderErrorResponseContext | QuestDB JavaScript Client - v4.2.0 Interface QwpSenderErrorResponseContext
interface QwpSenderErrorResponseContext {
appliedPolicy?: QwpSenderErrorPolicy;
detectedAtMs?: number;
fromFsn?: bigint;
messageSequence?: bigint;
tableName?: string;
toFsn?: bigint;
}Index
Properties
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpSenderMetrics.html b/docs/interfaces/_questdb_nodejs-client.QwpSenderMetrics.html
new file mode 100644
index 0000000..972af04
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpSenderMetrics.html
@@ -0,0 +1,18 @@
+QwpSenderMetrics | QuestDB JavaScript Client - v4.2.0 Interface QwpSenderMetrics
Immutable high-level sender counters plus the active ingress snapshot.
+ interface QwpSenderMetrics {
autoFlushBytes: number;
closed: boolean;
closing: boolean;
connected: boolean;
deferredRows: number;
effectiveAutoFlushBytes: number;
ingress?: QwpIngressMetrics;
pendingBytes: number;
pendingRows: number;
totalFlushes: number;
totalFlushFailures: number;
totalRowsPublished: number;
totalRowsStaged: number;
totalTransactionsCommitted: number;
}Properties
Readonlyauto Flush Bytes
autoFlushBytes: numberReadonlyclosed
closed: booleanReadonlyclosing
closing: booleanReadonlyconnected
connected: booleanReadonlydeferred Rows
deferredRows: numberReadonlyeffective Auto Flush Bytes
effectiveAutoFlushBytes: numberOptional Readonlyingress
Readonlypending Bytes
pendingBytes: numberEstimated raw column-buffer bytes currently staged.
+Readonlypending Rows
pendingRows: numberReadonlytotal Flushes
totalFlushes: numberReadonlytotal Flush Failures
totalFlushFailures: numberReadonlytotal Rows Published
totalRowsPublished: numberRows whose encoded frames have entered the ingress session.
+Readonlytotal Rows Staged
totalRowsStaged: numberReadonlytotal Transactions Committed
totalTransactionsCommitted: number
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpSenderOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpSenderOptions.html
new file mode 100644
index 0000000..cb32b1e
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpSenderOptions.html
@@ -0,0 +1,31 @@
+QwpSenderOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpSenderOptions
Options for the browser-safe, fluent QWP sender.
+ interface QwpSenderOptions {
autoFlush?: boolean;
autoFlushBytes?: number;
autoFlushIntervalMs?: number;
autoFlushRows?: number;
awaitDurableAck?: boolean;
awaitServerAck?: boolean;
closeFlushTimeoutMs?: number;
durableAckTimeoutMs?: number;
encode?: QwpSenderEncodeOptions;
log?: QwpSenderLogger;
maxNameLength?: number;
transactional?: boolean;
}Properties
Optionalauto Flush
autoFlush?: booleanOptionalauto Flush Bytes
autoFlushBytes?: numberSoft threshold for estimated buffered column bytes. Zero disables the byte
+trigger. Defaults to zero and is clamped below a connected server's batch
+cap; exact encoded frames remain subject to the protocol batch limit.
+Optionalauto Flush Interval Ms
autoFlushIntervalMs?: numberOptionalauto Flush Rows
autoFlushRows?: numberOptionalawait Durable Ack
awaitDurableAck?: booleanWait for durable upload after every successful ingress ACK. When true,
+this implies awaitServerAck unless awaitServerAck is explicitly false.
+Optionalawait Server Ack
awaitServerAck?: booleanWait for the server's protocol ACK before flush()/commit() resolves.
+Defaults to false, matching the Java QWP sender's local-publication
+boundary. Set this to true for an acknowledgement barrier, or use
+flushAndGetSequence() followed by waitForAcknowledged().
+Optionalclose Flush Timeout Ms
closeFlushTimeoutMs?: numberMaximum time close() spends publishing queued rows and waiting for the
+server ACK watermark. Zero or a negative value skips the drain. Defaults
+to 5 seconds.
+Optionaldurable Ack Timeout Ms
durableAckTimeoutMs?: numberOptionalencode
QWP frame encoding options supported by the high-level sender.
+Optionallog
Optionalmax Name Length
maxNameLength?: numberMaximum UTF-8 byte length of table and column names. Defaults to 127.
+Optionaltransactional
transactional?: booleanKeep auto-flushed rows in an open server-side transaction. An explicit
+flush()/commit() closes the transaction. QWP transactions are atomic per
+table, rather than across every table in a multi-table flush.
+
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpSenderSession.html b/docs/interfaces/_questdb_nodejs-client.QwpSenderSession.html
new file mode 100644
index 0000000..2707ba8
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpSenderSession.html
@@ -0,0 +1,15 @@
+QwpSenderSession | QuestDB JavaScript Client - v4.2.0 Interface QwpSenderSession
The subset of QwpIngressSession used by QwpSender.
+ interface QwpSenderSession {
acknowledgedFrameSequence?: bigint;
maxBatchSizeBytes?: number;
metrics?: QwpIngressMetrics;
publishedFrameSequence?: bigint;
close(code?: number, reason?: string): Promise<void>;
publishTables(
tables: readonly QwpTableBuffer[],
options?: QwpIngressEncodeOptions,
): Promise<void>;
publishTablesDelta(
tables: readonly QwpTableBuffer[],
options?: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">,
): Promise<void>;
sendTables(
tables: readonly QwpTableBuffer[],
options?: QwpIngressEncodeOptions,
): Promise<QwpIngressResponse>;
sendTablesDelta(
tables: readonly QwpTableBuffer[],
options?: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">,
): Promise<QwpIngressResponse>;
sendTablesDeltaWithPublication(
tables: readonly QwpTableBuffer[],
options?: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">,
): QwpIngressSendResult;
sendTablesWithPublication(
tables: readonly QwpTableBuffer[],
options?: QwpIngressEncodeOptions,
): QwpIngressSendResult;
waitForAcknowledged(
targetSequence: bigint,
timeoutMs?: number,
): Promise<void>;
waitForDurable(
response: QwpIngressResponse,
timeoutMs?: number,
): Promise<void>;
}Implemented by
Properties
Optional Readonlyacknowledged Frame Sequence
acknowledgedFrameSequence?: bigintOptional Readonlymax Batch Size Bytes
maxBatchSizeBytes?: numberOptional Readonlymetrics
Optional Readonlypublished Frame Sequence
publishedFrameSequence?: bigintMethods
close
Parameters
Optionalcode: numberOptionalreason: string
Returns Promise<void>
Optionalpublish Tables
- publishTables(
tables: readonly QwpTableBuffer[],
options?: QwpIngressEncodeOptions,
): Promise<void>Parameters
- tables: readonly QwpTableBuffer[]
Optionaloptions: QwpIngressEncodeOptions
Returns Promise<void>
Optionalpublish Tables Delta
- publishTablesDelta(
tables: readonly QwpTableBuffer[],
options?: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">,
): Promise<void>Parameters
- tables: readonly QwpTableBuffer[]
Optionaloptions: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">
Returns Promise<void>
send Tables
- sendTables(
tables: readonly QwpTableBuffer[],
options?: QwpIngressEncodeOptions,
): Promise<QwpIngressResponse>Parameters
- tables: readonly QwpTableBuffer[]
Optionaloptions: QwpIngressEncodeOptions
Returns Promise<QwpIngressResponse>
Optionalsend Tables Delta
- sendTablesDelta(
tables: readonly QwpTableBuffer[],
options?: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">,
): Promise<QwpIngressResponse>Parameters
- tables: readonly QwpTableBuffer[]
Optionaloptions: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">
Returns Promise<QwpIngressResponse>
Optionalsend Tables Delta With Publication
- sendTablesDeltaWithPublication(
tables: readonly QwpTableBuffer[],
options?: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">,
): QwpIngressSendResultParameters
- tables: readonly QwpTableBuffer[]
Optionaloptions: Pick<QwpIngressEncodeOptions, "gorilla" | "deferCommit">
Returns QwpIngressSendResult
Optionalsend Tables With Publication
- sendTablesWithPublication(
tables: readonly QwpTableBuffer[],
options?: QwpIngressEncodeOptions,
): QwpIngressSendResultParameters
- tables: readonly QwpTableBuffer[]
Optionaloptions: QwpIngressEncodeOptions
Returns QwpIngressSendResult
Optionalwait For Acknowledged
Parameters
- targetSequence: bigint
OptionaltimeoutMs: number
Returns Promise<void>
wait For Durable
Parameters
- response: QwpIngressResponse
OptionaltimeoutMs: number
Returns Promise<void>
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpServerInfoMessage.html b/docs/interfaces/_questdb_nodejs-client.QwpServerInfoMessage.html
new file mode 100644
index 0000000..6bcb3ed
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpServerInfoMessage.html
@@ -0,0 +1,16 @@
+QwpServerInfoMessage | QuestDB JavaScript Client - v4.2.0 Interface QwpServerInfoMessage
Immutable endpoint metadata from the most recent successful egress bind.
+ interface QwpServerInfoMessage {
capabilities: number;
clusterId: string;
compressionCodec: number;
compressionLevel: number;
epoch: bigint;
flags: number;
kind: "server-info";
nodeId: string;
payloadLength: number;
role: number;
serverWallNanoseconds: bigint;
tableCount: number;
version: number;
zoneId: string;
}Hierarchy (View Summary)
- QwpFrameHeader
- QwpServerInfoMessage
Index
Properties
Properties
capabilities
capabilities: numbercluster Id
clusterId: stringcompression Codec
compressionCodec: numbercompression Level
compressionLevel: numberepoch
epoch: bigintflags
flags: numberkind
kind: "server-info"node Id
nodeId: stringpayload Length
payloadLength: numberrole
role: numberserver Wall Nanoseconds
serverWallNanoseconds: biginttable Count
tableCount: numberversion
version: numberzone Id
zoneId: string
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpSymbolValue.html b/docs/interfaces/_questdb_nodejs-client.QwpSymbolValue.html
new file mode 100644
index 0000000..986fb49
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpSymbolValue.html
@@ -0,0 +1,3 @@
+QwpSymbolValue | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpUpgradeErrorDetails.html b/docs/interfaces/_questdb_nodejs-client.QwpUpgradeErrorDetails.html
new file mode 100644
index 0000000..d807b6b
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpUpgradeErrorDetails.html
@@ -0,0 +1,14 @@
+QwpUpgradeErrorDetails | QuestDB JavaScript Client - v4.2.0 Interface QwpUpgradeErrorDetails
interface QwpUpgradeErrorDetails {
cause?: unknown;
closeCode?: number;
kind: QwpUpgradeErrorKind;
retryable?: boolean;
serverRole?: string;
serverZone?: string;
statusCode?: number;
statusMessage?: string;
timeoutPhase?: QwpUpgradeTimeoutPhase;
tryNextEndpoint?: boolean;
url?: string | URL;
}Index
Properties
Properties
Optionalcause
cause?: unknownOptionalclose Code
closeCode?: numberkind
Optionalretryable
retryable?: booleanWhether a later retry against the configured endpoint set may recover.
+Optionalserver Role
serverRole?: stringOptionalserver Zone
serverZone?: stringOptionalstatus Code
statusCode?: numberOptionalstatus Message
statusMessage?: stringOptionaltimeout Phase
Optionaltry Next Endpoint
tryNextEndpoint?: booleanWhether failover code should try another endpoint before surfacing this.
+Optionalurl
url?: string | URL
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpUuidValue.html b/docs/interfaces/_questdb_nodejs-client.QwpUuidValue.html
new file mode 100644
index 0000000..b8fd562
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpUuidValue.html
@@ -0,0 +1,3 @@
+QwpUuidValue | QuestDB JavaScript Client - v4.2.0
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpWebSocketConnectOptions.html b/docs/interfaces/_questdb_nodejs-client.QwpWebSocketConnectOptions.html
new file mode 100644
index 0000000..15826d4
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpWebSocketConnectOptions.html
@@ -0,0 +1,12 @@
+QwpWebSocketConnectOptions | QuestDB JavaScript Client - v4.2.0 Interface QwpWebSocketConnectOptions
interface QwpWebSocketConnectOptions {
closeTimeoutMs?: number;
connectTimeoutMs?: number;
failoverUrls?: readonly (string | URL)[];
protocols?: string | string[];
sendTimeoutMs?: number;
url: string | URL;
}Hierarchy (View Summary)
- QwpWebSocketConnectOptions
Index
Properties
Properties
Optionalclose Timeout Ms
closeTimeoutMs?: numberMaximum time allowed for a graceful WebSocket close. Defaults to 15s.
+Optionalconnect Timeout Ms
connectTimeoutMs?: numberNode TCP/TLS connection deadline, or the complete opening deadline in a
+browser. Defaults to 15s.
+Optionalfailover Urls
failoverUrls?: readonly (string | URL)[]Additional endpoints attempted in order when the preferred endpoint fails.
+Optionalprotocols
protocols?: string | string[]Optionalsend Timeout Ms
sendTimeoutMs?: numberMaximum time a send may remain queued by the WebSocket. Defaults to 15s.
+url
url: string | URL
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpWebSocketLike.html b/docs/interfaces/_questdb_nodejs-client.QwpWebSocketLike.html
new file mode 100644
index 0000000..e15a6a6
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpWebSocketLike.html
@@ -0,0 +1,18 @@
+QwpWebSocketLike | QuestDB JavaScript Client - v4.2.0 Interface QwpWebSocketLike
interface QwpWebSocketLike {
binaryType: string;
bufferedAmount?: number;
protocol?: string;
readyState: number;
addEventListener(
type: "open",
listener: (event: unknown) => void,
options?: { once?: boolean },
): void;
addEventListener(
type: "message",
listener: (event: QwpWebSocketMessageEvent) => void,
): void;
addEventListener(
type: "error",
listener: (event: unknown) => void,
options?: { once?: boolean },
): void;
addEventListener(
type: "close",
listener: (event: QwpWebSocketCloseEvent) => void,
options?: { once?: boolean },
): void;
close(code?: number, reason?: string): void;
ping(): void;
removeEventListener(type: "open", listener: (event: unknown) => void): void;
removeEventListener(
type: "message",
listener: (event: QwpWebSocketMessageEvent) => void,
): void;
removeEventListener(
type: "error",
listener: (event: unknown) => void,
): void;
removeEventListener(
type: "close",
listener: (event: QwpWebSocketCloseEvent) => void,
): void;
send(data: Uint8Array): void;
sendWithCallback(data: Uint8Array, callback: (error?: Error) => void): void;
terminate(): void;
}Index
Properties
Methods
Properties
binary Type
binaryType: stringOptional Readonlybuffered Amount
bufferedAmount?: numberNumber of application bytes queued by WHATWG-compatible WebSockets.
+Optional Readonlyprotocol
protocol?: stringWebSocket subprotocol selected by the server, or an empty string.
+Readonlyready State
readyState: numberMethods
add Event Listener
- addEventListener(
type: "open",
listener: (event: unknown) => void,
options?: { once?: boolean },
): voidParameters
- type: "open"
- listener: (event: unknown) => void
Optionaloptions: { once?: boolean }
Returns void
Parameters
- type: "message"
- listener: (event: QwpWebSocketMessageEvent) => void
Returns void
- addEventListener(
type: "error",
listener: (event: unknown) => void,
options?: { once?: boolean },
): voidParameters
- type: "error"
- listener: (event: unknown) => void
Optionaloptions: { once?: boolean }
Returns void
- addEventListener(
type: "close",
listener: (event: QwpWebSocketCloseEvent) => void,
options?: { once?: boolean },
): voidParameters
- type: "close"
- listener: (event: QwpWebSocketCloseEvent) => void
Optionaloptions: { once?: boolean }
Returns void
close
Parameters
Optionalcode: numberOptionalreason: string
Returns void
Optionalping
Node WebSocket implementations may expose control-frame PING.
+Returns void
Optionalremove Event Listener
Optional cleanup hook implemented by browser WebSocket and Node ws.
+Parameters
- type: "open"
- listener: (event: unknown) => void
Returns void
Parameters
- type: "message"
- listener: (event: QwpWebSocketMessageEvent) => void
Returns void
Parameters
- type: "error"
- listener: (event: unknown) => void
Returns void
Parameters
- type: "close"
- listener: (event: QwpWebSocketCloseEvent) => void
Returns void
send
Parameters
- data: Uint8Array
Returns void
Optionalsend With Callback
Node adapter hook for the ws.send(data, callback) completion signal.
+Parameters
- data: Uint8Array
- callback: (error?: Error) => void
Returns void
Optionalterminate
Node WebSocket implementations may support immediate termination.
+Returns void
diff --git a/docs/interfaces/_questdb_nodejs-client.QwpWriterColumn.html b/docs/interfaces/_questdb_nodejs-client.QwpWriterColumn.html
new file mode 100644
index 0000000..b1ff0e7
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.QwpWriterColumn.html
@@ -0,0 +1,19 @@
+QwpWriterColumn | QuestDB JavaScript Client - v4.2.0 Interface QwpWriterColumn<T, DesignatedTimestamp>
A reusable, immutable column definition for a compiled QWP table writer.
+ interface QwpWriterColumn<T, DesignatedTimestamp extends boolean = false> {
__qwpWriterInput?: T;
designatedTimestamp: DesignatedTimestamp;
kind: QwpWriterColumnKind;
precisionBits?: number;
scale?: number;
unit?: QwpTimestampUnit;
}Type Parameters
- T
- DesignatedTimestamp extends boolean = false
Index
Properties
Properties
Optional Readonly Internal__ qwp Writer Input
Carries the input type without adding a runtime value. Never
+assigned, and deliberately a plain property rather than a unique symbol:
+each emitted bundle would declare its own symbol, making the key nominally
+distinct per entry point. A column built by './qwp' would then satisfy
+another bundle's QwpWriterColumn without ever matching its phantom key, so
+QwpWriterColumnInput would infer unknown and every row field would
+silently accept anything. A shared property name resolves structurally
+across bundles, which is what keeps row typing alive for consumers of the
+published package.
+Readonlydesignated Timestamp
Readonlykind
Optional Readonlyprecision Bits
precisionBits?: numberGEOHASH precision in bits, fixed for the whole column.
+Optional Readonlyscale
scale?: numberDECIMAL scale, fixed for the whole column.
+Optional Readonlyunit
diff --git a/docs/interfaces/_questdb_nodejs-client.SenderBuffer.html b/docs/interfaces/_questdb_nodejs-client.SenderBuffer.html
new file mode 100644
index 0000000..092cf0b
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.SenderBuffer.html
@@ -0,0 +1,159 @@
+SenderBuffer | QuestDB JavaScript Client - v4.2.0 Interface SenderBuffer
Buffer used by the Sender for data serialization.
+Provides methods for writing different data types into the buffer.
+ interface SenderBuffer {
arrayColumn(name: string, value: unknown[]): SenderBuffer;
at(timestamp: number | bigint, unit?: TimestampUnit): void;
atNow(): void;
booleanColumn(name: string, value: boolean): SenderBuffer;
currentPosition(): number;
decimalColumn(
name: string,
unscaled: bigint | Int8Array<ArrayBufferLike>,
scale: number,
): SenderBuffer;
decimalColumnText(name: string, value: string | number): SenderBuffer;
floatColumn(name: string, value: number): SenderBuffer;
intColumn(name: string, value: number): SenderBuffer;
reset(): SenderBuffer;
stringColumn(name: string, value: string): SenderBuffer;
symbol(name: string, value: unknown): SenderBuffer;
table(table: string): SenderBuffer;
timestampColumn(
name: string,
value: number | bigint,
unit?: TimestampUnit,
): SenderBuffer;
toBufferNew(pos?: number): Buffer<ArrayBufferLike>;
toBufferView(pos?: number): Buffer;
}Index
Methods
array Column
Writes an array column with its values into the buffer.
+Parameters
- name: string
Column name
+ - value: unknown[]
Array values to write (currently supports double arrays). A null or undefined value omits the column entirely when arrays are supported; protocol v1 rejects the call for every value.
+
Returns SenderBuffer
Returns with a reference to this buffer.
+
at
Closes the row after writing the designated timestamp into the buffer.
+Precision rules:
+
+- Protocol v2 and higher:
+Timestamps passed with unit
'ns' (nanoseconds) are sent with full nanosecond precision.
+All other timestamps are sent with microsecond precision.
+- Protocol v1:
+Always uses microsecond precision, even if the timestamp is specified in nanoseconds.
+
+Parameters
- timestamp: number | bigint
Designated epoch timestamp. Must be an integer or a BigInt.
+ Optionalunit: TimestampUnitThe time unit of the timestamp.
+Supported values:
+
+'ns' — nanoseconds (requires BigInt)
+'us' — microseconds (default)
+'ms' — milliseconds
+
+
Returns void
Returns with a reference to this buffer.
+
at Now
Closes the row without writing designated timestamp into the buffer.
+Designated timestamp will be populated by the server on this record.
+Returns void
boolean Column
Writes a boolean column with its value into the buffer.
+Use it to insert into BOOLEAN columns.
+Parameters
- name: string
Column name.
+ - value: boolean
Column value, accepts only boolean values. A null or undefined value omits the column entirely (stored as NULL).
+
Returns SenderBuffer
Returns with a reference to this buffer.
+
current Position
Returns the current position of the buffer.
+New data will be written into the buffer starting from this position.
+Returns number
The current write position in the buffer
+
decimal Column
- decimalColumn(
name: string,
unscaled: bigint | Int8Array<ArrayBufferLike>,
scale: number,
): SenderBufferWrites a decimal value into the buffer using its binary format.
+Use it to insert into DECIMAL database columns.
+Parameters
- name: string
Column name.
+ - unscaled: bigint | Int8Array<ArrayBufferLike>
The unscaled integer portion of the decimal value.
+
+- If a
bigint is provided, it will be converted automatically.
+- If an
Int8Array is provided, it must contain the two’s complement representation
+of the unscaled value in big-endian byte order.
+- An empty
Int8Array represents a NULL value.
+- A null or undefined value omits the column entirely (stored as NULL)
+when decimals are supported; protocol v1/v2 reject the call for every value.
+
+ - scale: number
The number of fractional digits (the scale) of the decimal value.
+
Returns SenderBuffer
Returns with a reference to this buffer.
+
decimal Column Text
Writes a decimal value into the buffer using its text format.
+Use it to insert into DECIMAL database columns.
+Parameters
- name: string
Column name.
+ - value: string | number
The decimal value to write.
+
+- Accepts either a
number or a string containing a valid decimal representation.
+- String values should follow standard decimal notation (e.g.,
"123.45" or "-0.001").
+- A null or undefined value omits the column entirely (stored as NULL)
+when decimals are supported; protocol v1/v2 reject the call for every value.
+
+
Returns SenderBuffer
Returns with a reference to this buffer.
+
float Column
Writes a 64-bit floating point value into the buffer.
+Use it to insert into DOUBLE or FLOAT database columns.
+Parameters
- name: string
Column name.
+ - value: number
Column value, accepts only number values. A null or undefined value omits the column entirely (stored as NULL).
+
Returns SenderBuffer
Returns with a reference to this buffer.
+
int Column
Writes a 64-bit signed integer into the buffer.
+Use it to insert into LONG, INT, SHORT and BYTE columns.
+Parameters
- name: string
Column name.
+ - value: number
Column value, accepts only number values. A null or undefined value omits the column entirely (stored as NULL).
+
Returns SenderBuffer
Returns with a reference to this buffer.
+
reset
Resets the buffer, data sitting in the buffer will be lost.
+In other words it clears the buffer, and sets the writing position to the beginning of the buffer.
+Returns SenderBuffer
Returns with a reference to this buffer.
+
string Column
Writes a string column with its value into the buffer.
+Use it to insert into VARCHAR and STRING columns.
+Parameters
- name: string
Column name.
+ - value: string
Column value, accepts only string values. A null or undefined value omits the column entirely (stored as NULL).
+
Returns SenderBuffer
Returns with a reference to this buffer.
+
symbol
Writes a symbol name and value into the buffer.
+Use it to insert into SYMBOL columns.
+Parameters
- name: string
Symbol name.
+ - value: unknown
Symbol value, toString() is called to extract the actual symbol value from the parameter. A null or undefined value omits the symbol entirely (stored as NULL).
+
Returns SenderBuffer
Returns with a reference to this buffer.
+
table
Writes the table name into the buffer.
+Parameters
- table: string
Table name.
+
Returns SenderBuffer
Returns with a reference to this buffer.
+
timestamp Column
Writes a timestamp column and its value into the buffer.
+Use this method to insert data into TIMESTAMP or TIMESTAMP_NS columns.
+Precision rules:
+
+- Protocol v2 and higher:
+Timestamps passed with unit
'ns' (nanoseconds) are sent with full nanosecond precision.
+All other timestamps are sent with microsecond precision.
+- Protocol v1:
+Always uses microsecond precision, even if the timestamp is specified in nanoseconds.
+
+Parameters
- name: string
The column name.
+ - value: number | bigint
The epoch timestamp. Must be an integer or a BigInt. A null or undefined value omits the column entirely (stored as NULL).
+ Optionalunit: TimestampUnitThe time unit of the timestamp.
+Supported values:
+
+'ns' — nanoseconds (requires BigInt)
+'us' — microseconds (default)
+'ms' — milliseconds
+
+
Returns SenderBuffer
Returns with a reference to this buffer.
+
to Buffer New
Returns a cropped buffer ready to send to the server, or null if there is nothing to send.
+The returned buffer is a copy of this buffer.
+It also compacts the buffer.
+Parameters
Optionalpos: numberOptional position parameter
+
Returns Buffer<ArrayBufferLike>
A copy of the buffer ready to send, or null
+
to Buffer View
Returns a cropped buffer, or null if there is nothing to send.
+The returned buffer is backed by this buffer instance, meaning the view can change as the buffer is mutated.
+Used only in tests to assert the buffer's content.
+Parameters
Optionalpos: numberOptional position parameter
+
Returns Buffer
A view of the buffer
+
diff --git a/docs/interfaces/_questdb_nodejs-client.SenderTransport.html b/docs/interfaces/_questdb_nodejs-client.SenderTransport.html
new file mode 100644
index 0000000..a16b840
--- /dev/null
+++ b/docs/interfaces/_questdb_nodejs-client.SenderTransport.html
@@ -0,0 +1,18 @@
+SenderTransport | QuestDB JavaScript Client - v4.2.0 Interface SenderTransport
Interface for QuestDB transport implementations.
+Defines the contract for different transport protocols (HTTP/HTTPS/TCP/TCPS).
+ interface SenderTransport {
close(): Promise<void>;
connect(): Promise<boolean>;
getDefaultAutoFlushRows(): number;
send(data: Buffer): Promise<boolean>;
}Implemented by
Index
Methods
Methods
close
Closes the connection to the database server.
+Should not be called on HTTP transports.
+Returns Promise<void>
Promise that resolves when the connection is closed
+
connect
Establishes a connection to the database server.
+Should not be called on HTTP transports.
+Returns Promise<boolean>
Promise resolving to true if connection is successful
+
get Default Auto Flush Rows
Gets the default number of rows that trigger auto-flush for this transport.
+Returns number
Default auto-flush row count
+
send
Sends the data to the database server.
+Parameters
- data: Buffer
Buffer containing the data to send
+
Returns Promise<boolean>
Promise resolving to true if data was sent successfully
+
diff --git a/docs/media/QWP.md b/docs/media/QWP.md
new file mode 100644
index 0000000..413fe11
--- /dev/null
+++ b/docs/media/QWP.md
@@ -0,0 +1,1531 @@
+# QuestDB Wire Protocol (QWP)
+
+This guide covers QWP ingress and egress from Node.js and browser applications.
+It describes the supported public entry points, delivery semantics, authentication,
+failure handling, and migration from the existing Node.js sender and the low-level
+QWP API.
+
+QWP support is currently a preview. The documented exports are the compatibility
+baseline for the first QWP release, but may still change before that release. Once
+released, changes to this documented surface follow the package's semantic-versioning
+policy. Imports from internal source paths are never supported.
+
+## Choose an entry point
+
+| Package | Runtime | Use it for |
+| ------------------------- | ------- | --------------------------------------------------------------------------------------------------- |
+| `@questdb/nodejs-client` | Node.js | Existing `Sender`, QWP codecs, WebSocket/UDP ingress, egress, TLS, and persistent store-and-forward |
+| `@questdb/browser-client` | Browser | Browser-safe QWP ingress, egress, authentication bootstrap, sessions, and codecs |
+
+Each distribution exposes its complete API directly from its package root.
+
+Browser applications should install and import `@questdb/browser-client`. Its
+published module graph has no Node.js imports, Node.js typings, Node engine
+requirement, `undici`, or `ws`. Node-only transports and persistence remain in
+`@questdb/nodejs-client`.
+
+QWP uses `/write/v4` for ingress and `/read/v1` for egress. A server must expose
+these WebSocket routes; optional features are enabled only when negotiation confirms
+that the server supports them.
+
+## Ingress
+
+### Node.js through the existing `Sender`
+
+Changing `http::` or `tcp::` to `ws::` selects QWP while preserving the familiar
+fluent row API:
+
+```typescript
+import { Sender } from "@questdb/nodejs-client";
+
+const sender = await Sender.fromConfig(
+ "wss::addr=questdb.example:9000;token=REST_OR_OIDC_TOKEN;auto_flush=off",
+);
+await sender.connect();
+
+try {
+ await sender
+ .table("trades")
+ .symbol("symbol", "ETH-USD")
+ .floatColumn("price", 2_615.54)
+ .timestampColumn("received_at", Date.now(), "ms")
+ .at(Date.now(), "ms");
+ await sender.flush();
+} finally {
+ await sender.close();
+}
+```
+
+`username` plus `password` selects HTTP Basic authentication for the WebSocket
+upgrade. `token` selects Bearer authentication. Use `wss::` in production.
+
+`Sender.fromConfig()` uses the same Java-compatible `ws::`/`wss::` vocabulary
+as `connectQwpNodeClient()`. Comma-separated or repeated `addr` values configure
+ordered failover endpoints, and ingress, egress, pool, and reserved policy keys
+are validated from one schema. The standalone sender applies ingress-owned keys;
+keys owned only by egress or the pooled facade are accepted as intentional no-ops.
+
+## Configuration-string keys
+
+Every `ws::`/`wss::` connect string is parsed by one schema, shared with the
+other QuestDB clients, whichever entry point builds the client —
+`Sender.fromConfig()`, `SenderOptions.fromConfig()`, `connectQwpNodeClient()`,
+or `connectQwpNodeQuery()`. An unrecognised key is rejected with
+`unknown configuration key: `; a legacy ILP key adds a hint pointing at
+where it applies instead.
+
+Keys are grouped by the component that applies them. A client applies the keys
+its own side owns and accepts the rest as intentional no-ops, so one connect
+string can configure a sender, a query client, or the pooled facade. Every key
+also has a programmatic equivalent on the corresponding options object; the
+connect string is the portable spelling.
+
+The complete connect string is parsed and validated before typed overrides are
+applied. When both forms set the same option, the typed value wins. In
+particular, `Sender.fromConfig()` applies `qwp.webSocket.failoverUrls`,
+`target`, `zone`, and `senderId` after URL parsing. The primary ingress URL
+continues to come from `addr`, because the typed object intentionally omits
+`url`.
+
+### Connection
+
+| Key | Value | Default | Meaning |
+| -------------------- | ------------------ | --------- | ---------------------------------------------------------------------------------------- |
+| `addr` | `host[:port]` | port 9000 | Endpoint. Repeat the key, or comma-separate, for ordered failover. |
+| `username`, `user` | string | — | HTTP Basic user for the WebSocket upgrade. |
+| `password`, `pass` | string | — | HTTP Basic password. |
+| `token` | string | — | Bearer token; alternative to Basic. |
+| `tls_verify` | `on`, `unsafe_off` | on | Certificate verification. `unsafe_off` disables it. |
+| `tls_roots` | path | — | PEM file containing trusted private-CA certificates. PKCS#12 is not supported. |
+| `tls_roots_password` | string | — | Unsupported by Node; convert PKCS#12 roots to PEM and omit this key. |
+| `auth_timeout_ms` | integer ms | `15000` | Deadline for the upgrade and authentication exchange. |
+| `connect_timeout` | integer ms | `15000` | Deadline for the TCP/TLS transport, and for the upgrade unless `auth_timeout_ms` is set. |
+
+### Ingress
+
+| Key | Value | Default | Meaning |
+| ----------------------------------------------- | ---------------- | --------- | ------------------------------------------------------------------------ |
+| `auto_flush` | `on`, `off` | on | Master switch for all auto-flush triggers. |
+| `auto_flush_rows` | integer | `1000` | Flush after this many staged rows. |
+| `auto_flush_bytes` | integer or `off` | off | Flush once staged rows reach this estimated size. |
+| `auto_flush_interval` | integer ms | `100` | Flush when this long has passed. Checked as rows are added. |
+| `close_flush_timeout_millis` | integer ms | `5000` | Bound on `close()`'s ACK drain. `0` or negative is a fast close. |
+| `transaction` | `on`, `off` | off | Group each flush into a per-table transaction. |
+| `request_durable_ack` | `on`, `off` | off | Require durable ACKs; fails if the server cannot confirm them. |
+| `durable_ack_keepalive_interval_millis` | integer ms | — | Poll interval for durable-ACK progress. |
+| `max_name_len` | integer | `127` | Maximum table and column name length, in UTF-8 bytes. |
+| `sender_id` | string | `default` | Identifies this producer to the server and in the journal. |
+| `max_frame_rejections` | integer | `4` | Consecutive suspect outcomes for one frame before terminal escalation. |
+| `poison_min_escalation_window_millis` | integer ms | `5000` | Minimum dwell before a poison frame may escalate. |
+| `catch_up_cap_gap_min_escalation_window_millis` | integer ms | `300000` | Minimum dwell before an orphan symbol-dictionary cap gap is quarantined. |
+| `connection_listener_inbox_capacity` | integer | — | Bound on the connection-event inbox before events are dropped. |
+| `error_inbox_capacity` | integer | — | Bound on the `onSenderError` inbox before events are dropped. |
+
+### Reconnect and failover
+
+| Key | Value | Default | Meaning |
+| ---------------------------------- | --------------------------- | ------- | -------------------------------------------------------------------------------------- |
+| `reconnect_initial_backoff_millis` | integer ms | — | First reconnect delay; grows exponentially with jitter. |
+| `reconnect_max_backoff_millis` | integer ms | — | Ceiling for one reconnect delay. |
+| `reconnect_max_duration_millis` | integer ms | — | Budget for a reconnect episode. This is the QWP replacement for ILP's `retry_timeout`. |
+| `failover` | `on`, `off` | — | Enables endpoint failover for egress. |
+| `failover_max_attempts` | integer ≥ 1 | — | Failover attempts before giving up. |
+| `failover_backoff_initial_ms` | integer ms | — | First failover delay. |
+| `failover_backoff_max_ms` | integer ms | — | Ceiling for one failover delay. |
+| `failover_max_duration_ms` | integer ms | — | Budget for a failover episode. |
+| `target` | `any`, `primary`, `replica` | — | Server role this client will accept, on both ingress and egress. |
+| `zone` | string | — | Preferred topology zone when ranking endpoints, on both ingress and egress. |
+
+### Store-and-forward (Node only)
+
+Setting `sf_dir` turns on the persistent journal; the rest tune it. A default
+shown as a dash is applied downstream of the connect string, by the sender or
+session that consumes it.
+
+| Key | Value | Default | Meaning |
+| --------------------------- | ------------------------------ | ------------- | ----------------------------------------------------------------- |
+| `sf_dir` | path | — | Journal directory. Enables store-and-forward. |
+| `sf_durability` | `memory`, `periodic`, `append` | `memory` | Local durability barrier after each vectored append. |
+| `sf_max_total_bytes` | integer bytes | `10737418240` | Journal ceiling. Reaching it is the one error a producer sees. |
+| `sf_max_segment_bytes` | integer bytes | `4194304` | Size of one segment file. |
+| `sf_sync_interval_millis` | integer ms | — | Checkpoint interval when `sf_durability=periodic`. |
+| `sf_append_deadline_millis` | integer ms | `30000` | How long an append waits for space or a retryable journal fault. |
+| `initial_connect_retry` | `off`, `sync`, `async` | `off` | Startup policy when the server is unreachable. Requires `sf_dir`. |
+| `drain_orphans` | `on`, `off` | off | Adopt and drain journals left by crashed producers. |
+| `max_background_drainers` | integer | — | Concurrent orphan drainers. |
+
+### Egress
+
+| Key | Value | Default | Meaning |
+| ------------------- | --------------------- | ---------- | -------------------------------------------------- |
+| `max_batch_rows` | integer, 1..1048576 | — | Rows the server puts in one result batch. |
+| `initial_credit` | integer ≥ 0 | — | Starting flow-control credit for a query. |
+| `buffer_pool_size` | integer ≥ 1 | — | Reusable result buffers held per session. |
+| `compression` | `raw`, `zstd`, `auto` | negotiated | Result compression to negotiate. |
+| `compression_level` | integer, 1..22 | — | zstd level requested from the server. |
+| `client_id` | string | — | Identifies this client in server-side diagnostics. |
+
+### Pool
+
+Applied by the pooled facade; a standalone sender or query client ignores them.
+
+| Key | Value | Default | Meaning |
+| ------------------------- | ----------- | ------- | --------------------------------------------- |
+| `sender_pool_min` | integer | — | Senders kept warm. |
+| `sender_pool_max` | integer | — | Sender ceiling. |
+| `query_pool_min` | integer | — | Query sessions kept warm. |
+| `query_pool_max` | integer | — | Query-session ceiling. |
+| `acquire_timeout_ms` | integer ms | — | How long `acquire()` waits for a free entry. |
+| `query_close_timeout_ms` | integer ms | — | Bound on closing a borrowed query session. |
+| `idle_timeout_ms` | integer ms | — | Idle time before a pooled entry is reaped. |
+| `max_lifetime_ms` | integer ms | — | Absolute lifetime of a pooled entry. |
+| `housekeeper_interval_ms` | integer ms | — | How often the pool reaps aged entries. |
+| `lazy_connect` | `on`, `off` | off | Start without blocking on a first connection. |
+
+### Reserved
+
+`on_write_error`, `on_server_error`, `on_internal_error`, `on_parse_error`,
+`on_schema_error` and `on_security_error` are part of the shared vocabulary and
+are accepted, but this client does not yet apply them: server-error policy comes
+from `qwpDefaultSenderErrorPolicy` and the `onSenderError` stream. They are
+listed so a connect string written for another QuestDB client is not rejected.
+
+### Node.js fire-and-forget UDP
+
+`udp::` selects Node-only QWP v1 over IPv4 UDP while retaining the fluent row API:
+
+```typescript
+import { Sender } from "@questdb/nodejs-client";
+
+const sender = await Sender.fromConfig(
+ "udp::addr=239.1.2.3:9007;max_datagram_size=1400;multicast_ttl=1",
+);
+await sender.connect();
+await sender
+ .table("trades")
+ .symbol("symbol", "ETH-USD")
+ .floatColumn("price", 2615.54)
+ .atNow();
+await sender.close();
+```
+
+The default port is 9007, the maximum datagram size (`max_datagram_size`) is 1400
+bytes, and the multicast TTL (`multicast_ttl`) is zero. Each datagram is
+self-contained, contains exactly one table, and uses an inline schema plus
+table-local symbol dictionaries. Batches are split at row boundaries;
+`QwpUdpDatagramTooLargeError` is raised before transmission when one row cannot
+fit. `connectQwpNodeUdpSender()` and `connectQwpNodeUdp()` expose the same
+transport from `@questdb/nodejs-client`.
+
+UDP provides no authentication, TLS, server or durable ACK, transactions,
+reconnection, compression, or store-and-forward. Local socket errors are delivered
+to `QwpNodeUdpOptions.onError`; like the Java sender, they are observational and do
+not retry rows that may already have been handed to the network. UDP is unavailable
+from the browser entry point.
+
+Advanced QWP options are accepted in the second argument:
+
+```typescript
+const sender = await Sender.fromConfig(
+ "wss::addr=questdb.example:9000;token=REST_OR_OIDC_TOKEN;initial_connect_retry=async",
+ {
+ qwp: {
+ webSocket: {
+ requestDurableAck: true,
+ connectTimeoutMs: 5_000,
+ authTimeoutMs: 15_000,
+ failoverUrls: ["wss://questdb-dr.example:9000/write/v4"],
+ target: "any",
+ zone: "eu-west-1a",
+ senderId: "producer-a",
+ storeAndForward: {
+ directory: "/var/lib/my-service/qwp-replay/producer-a",
+ maxBytes: 512 * 1024 * 1024,
+ durability: "periodic",
+ checkpointIntervalMs: 5_000,
+ backpressurePolicy: "wait",
+ appendDeadlineMs: 30_000,
+ catchUpCapGapMinEscalationWindowMs: 300_000,
+ drainOrphans: true,
+ maxBackgroundDrainers: 4,
+ },
+ },
+ sender: {
+ awaitDurableAck: true,
+ autoFlushRows: 10_000,
+ autoFlushBytes: 4 * 1024 * 1024,
+ },
+ session: {
+ reconnect: {
+ maxAttempts: 0,
+ maxDurationMs: 0,
+ },
+ },
+ },
+ },
+);
+```
+
+Node bounds connection establishment in two phases. `connectTimeoutMs` covers
+DNS plus the TCP/TLS connection; after that succeeds, `authTimeoutMs` independently
+covers the authenticated HTTP request and WebSocket upgrade. Both default to 15
+seconds, so one endpoint attempt can take up to their sum. A timeout is reported as
+`QwpUpgradeError` with `timeoutPhase` set to `"connect"` or `"authentication"`.
+Browsers cannot observe the transport boundary, so their `connectTimeoutMs` continues
+to cover the complete WebSocket opening lifecycle and they do not expose
+`authTimeoutMs`.
+
+Give each active sender its own store-and-forward directory. The Node.js journal
+persists frames and their symbol dictionary before sending. Set
+`initialConnectMode: "async"` when a persistent sender must start while every
+endpoint is offline. Unless
+`awaitServerAck: true` or `awaitDurableAck: true` is selected, `flush()` resolves once
+the complete logical flush reaches the configured local journal boundary; a background
+drainer then sends it in order. The default `"append"` boundary is locally durable,
+while `"periodic"` and `"memory"` trade that immediate guarantee for throughput.
+Applications can therefore keep publishing during an outage until the configured
+`maxBytes` applies backpressure. A failed journal publication leaves the high-level
+rows staged so the caller can retry.
+
+`initialConnectMode` selects persistent startup behavior: `"off"` (the default)
+makes one
+fail-fast attempt, `"sync"` retries on the caller within the configured reconnect
+budget, and `"async"` returns immediately while
+the background replay loop connects. `Sender.fromConfig()` also accepts
+`initial_connect_retry=off|sync|async` when `qwp.webSocket.storeAndForward` is
+supplied. Initial authentication, upgrade, and capability failures remain terminal.
+When no mode is explicit, configuring any reconnect duration/backoff key promotes
+the initial connection to `"sync"`, so that budget also governs startup.
+After a foreground persistent sender has connected successfully at least once, the
+same failures are retried indefinitely so credential rotation and rolling capability
+changes cannot strand its journal. The configured reconnect attempt/duration budget
+therefore bounds `"sync"` startup and non-persistent reconnects, not steady-state
+foreground store-and-forward recovery.
+
+The connect-string key
+`catch_up_cap_gap_min_escalation_window_millis` is the equivalent of
+`catchUpCapGapMinEscalationWindowMs`.
+
+`durability` controls the local persistence barrier:
+
+- `"append"` (the default) issues a data-only durability barrier after every vectored
+ positional frame write; manifest and directory metadata retain full barriers;
+ hot-spare creation and activation are durable before publication resolves.
+- `"periodic"` checkpoints segment files, symbol metadata, and directory changes in the
+ background. The default interval is 5 seconds, and `close()` performs a final
+ checkpoint. A power failure can lose the most recent checkpoint window.
+- `"memory"` relies on operating-system writeback. It survives an orderly close and
+ normally a process failure, but it makes no power-loss durability promise.
+
+`backpressurePolicy: "error"` preserves the existing immediate
+`QwpReplayStoreFullError` behavior. Set it to `"wait"` to pause publication until an
+ACK advances the checksummed cursor, then a bounded background trimmer deletes fully
+drained segments.
+`appendDeadlineMs` bounds each such pause and retries of transient journal faults
+such as a briefly read-only, full, or descriptor-starved filesystem (30 seconds
+by default). Expiry raises `QwpReplayStoreAppendTimeoutError`. Waiting appenders
+do not hold the journal mutation queue, so ACK cleanup and checkpoint recovery
+can continue. Corruption and loss of the journal lock remain immediate failures.
+Direct users of `QwpNodeFileReplayStore` can inspect `metrics` for pending records
+and segments, checkpoint work, checkpoint failures, active waiters, stalls, and
+timeouts.
+
+The persisted symbol dictionary is monotonic for one open journal generation and
+cannot be reclaimed by an ACK alone. It counts toward the `maxBytes` target together
+with each complete fixed-segment reservation, including the hot spare. The journal
+preserves up to 32 MiB (or the configured target when smaller) for live frame segments
+if dictionary growth uses all remaining headroom. Dictionary persistence itself is
+never rejected by the target, so actual disk usage can exceed it by the current
+dictionary overshoot and at most one liveness segment. Frame growth beyond that
+allowance remains backpressured until background ACK trimming frees complete segments.
+A partly acknowledged segment remains charged to the disk budget until its last live
+record is acknowledged.
+Once every frame is acknowledged,
+`close()` removes the dictionary under the journal lock; the next clean start uses a
+fresh symbol-ID space. A partially drained close retains the dictionary required by
+the surviving frames.
+
+The journal takes an exclusive lock when it is loaded and holds it until the sender
+or session closes. A second live Node.js process using the same directory fails with
+`QwpReplayStoreLockedError` before recovery or cleanup can mutate journal contents,
+unless the first has stopped heartbeating long enough to be reclaimed — in which case
+it is the first that stops writing, as described under the heartbeat below.
+Ownership is held by a `.lock.owner` directory created next to the slot: `mkdir` is
+the only exclusive-by-construction filesystem operation available on every supported
+platform without a native addon, so exactly one process can create it. The holder PID
+is recorded in `.lock.pid` for diagnostics, and the stable `.lock` file is created and
+left in place so a slot keeps the on-disk shape a Java client expects. Short-lived
+locks under the shared parent directory's `.slot-locks` child serialize orphan
+adoption with close/rename/recreate quarantine transitions.
+
+**A Node.js client and a Java client must not use one persistence directory at the
+same time.** The Java client locks `.lock` with `flock` on Unix and `LockFileEx` on
+Windows. The Node.js client does not participate in those kernel locks, so the two
+runtimes will not see each other's lock and can both open the same slot, corrupting
+the journal. The persistence format itself remains cross-client: a directory written
+by one runtime can be handed to the other once the first has closed it. Only
+concurrent access is unsupported, and only between runtimes — two Node.js processes
+still exclude each other correctly.
+
+A kernel lock disappears the instant its holder dies; a directory does not. The holder
+therefore refreshes the owner directory's mtime every 5 seconds, and a contender
+reclaims a slot whose mtime has not advanced for 15 seconds. A contender also reclaims
+immediately when the owner record names a process that no longer exists on the same
+host, which is the common case after a crash. A stale owner directory is renamed aside
+before removal, so two contenders racing to reclaim one slot cannot both win it. Each
+acquisition also writes a token into the owner record and checks it before removing
+anything, so a release can never take away a directory that has since been handed to
+somebody else.
+
+If a holder is paused long enough for its heartbeat to lapse — `SIGSTOP`, a suspended
+VM, a stalled filesystem, or any synchronous section that blocks the event loop for
+more than 15 seconds — its lock can be reclaimed while it still believes it holds it.
+Such a holder stops writing: once it can no longer vouch for its own lock, every
+append, checkpoint and acknowledgement on that journal fails with
+`QwpReplayStoreLockLostError`, and the sender falls back to whatever its durability
+policy does when the journal is unavailable. This is deliberately conservative — the
+holder fails as soon as a contender _could_ have taken the slot, not only once one
+demonstrably has — because the alternative is writing at offsets the new owner now
+owns. A frame's sequence is derived from its position in the segment, so a same-width
+overwrite would otherwise reopen as a complete journal with the new owner's
+acknowledged frames missing and nothing reported.
+
+New journals use the cross-client SFA persistence layout. Fixed-size
+`sf-.sfa` files have the Java/Rust 24-byte `SF01` header and
+`[crc32c, payloadLength, payload]` frame envelope. `sf-manifest.bin` and
+`.ack-watermark` use the shared dual-slot checksummed metadata layout, while
+`.symbol-dict` uses the shared chunked `SYD1` representation. TypeScript tests load
+Java-produced segment and dictionary fixtures and compare TypeScript output with the
+same normalized bytes.
+
+Each segment reserves `maxSegmentBytes` of target payload data (4 MiB by default)
+plus one frame header so a maximum-sized frame fits. The active segment and one
+pre-sized temporary hot spare keep open file handles; rotation activates the spare.
+A process-wide, unreferenced worker provisions replacements, checkpoints dirty paths,
+and performs ACK-driven unlink and directory barriers. ACK trimming advances the
+durable manifest head before handing removal to that worker and runs in bounded
+background batches. Frame append uses a vectored header-plus-payload write, avoiding
+an additional payload-sized journal buffer.
+
+A background provisioning, checkpoint or trim failure is parked on the store and
+raised from the next journal call, then cleared by the next successful batch. Because
+such a fault is transient — a briefly full, read-only or descriptor-starved volume —
+reaching one while applying a server acknowledgement reconnects and replays rather
+than ending the sender: a filesystem hiccup must not cost a running producer. Failures
+that are verdicts on the journal itself carry `retryable: false` and stay terminal;
+today those are `QwpReplayStoreCorruptionError` and `QwpReplayStoreLockLostError`. The
+store persists its acknowledgement cursor before it mutates anything, so a fault at
+that moment leaves exactly the state a crash at that moment would leave, and replay
+resumes from the persisted watermark.
+
+Recovery validates segment CRCs with a reusable 64 KiB scanner and indexes only frame
+sequence, file offset, and payload length. The reconnect loop reads one payload from
+its retained segment handle when it is ready to send it; it does not materialize the
+complete persisted backlog. Fresh background store-and-forward frames likewise drop
+their resident payload after journal publication and are read back on demand. Memory
+therefore scales with the active encoding/send window rather than total disk backlog.
+
+Recovery also handles the canonical creation crash window in which a valid SFA
+segment becomes durable before its manifest.
+
+On startup, a dictionary sidecar truncated at a complete-block boundary is rebuilt
+from the ordered symbol deltas embedded in surviving committed frames and healed
+before replay. A corrupt or stale dictionary sidecar is replaced when those committed
+frames independently reconstruct a complete dense dictionary from ID zero. If the
+frame journal is structurally corrupt, or the surviving deltas contain a dictionary
+gap or conflict that cannot be reconstructed, the foreground slot is renamed to
+`.unreplayable-N`, marked with `.failed`, and preserved for inspection. The
+sender then starts once with a clean slot at the configured path.
+`onRecoveryQuarantine` receives the original and quarantine paths plus the terminal
+cause and a typed `senderError`. The shared `onSenderError` callback receives the same
+`data-loss` / `abandoned` verdict and its `quarantinedPath`. This build-time recovery
+notification is synchronous because no connected sender dispatcher exists yet;
+callback failures cannot interrupt recovery. Quarantined paths are never adopted by
+the orphan scanner. Operational filesystem errors are not quarantined and still fail
+startup, so a temporary permissions or disk problem cannot be mistaken for data
+corruption.
+
+For a standalone sender, `drainOrphans: true` scans sibling directories beneath the
+configured journal directory's parent, excludes the sender's own directory, and
+adopts record-bearing slots left by failed producers. Adoption is lock-protected and
+uses an independent QWP connection per slot, bounded by `maxBackgroundDrainers` (4 by
+default). The scanner runs immediately and then every 30 seconds; set
+`orphanScanIntervalMs: 0` for a startup-only scan. Terminal recovery failures create
+`.failed` in the slot so a corrupt or permanently rejected head cannot cause a hot
+retry loop. After inspection or repair, call `retryQwpNodeOrphanSlot(slotDirectory)`
+to make it eligible again. `onOrphanDrainEvent` reports discovery, drain, lock
+contention, quarantine, scanner failures, durable-ACK capability gaps, and transient
+all-replica windows through a bounded asynchronous inbox. An abandoned slot also
+reports a typed `data-loss` sender error. Callback exceptions cannot interrupt
+recovery.
+
+Blocking (`off` or `sync`) foreground startup fails immediately if every usable
+endpoint lacks durable-ACK support. Asynchronous foreground startup and steady-state
+store-and-forward reconnects retain their records and retry through rolling upgrades.
+An orphan slot retries a consecutive durable-ACK capability-gap episode until either
+16 connection sweeps or the configured reconnect `maxDurationMs` is reached, then it
+is quarantined behind `.failed` (`maxDurationMs: 0` disables only the time half of
+the budget). A transport outage or an all-replica window resets both halves of this
+orphan budget; neither transient condition can itself quarantine persisted data. The
+`durable-ack-unavailable`,
+`durable-ack-persistent-failure`, and `primary-unavailable` orphan events expose the
+distinction to operators.
+
+A foreground sender retries a symbol-dictionary catch-up entry that is too large for
+the current target forever because a larger-cap node may return. An orphan drainer
+quarantines that slot only after 16 consecutive incompatible-cap observations and a
+minimum five-minute dwell. Tune the dwell with
+`catchUpCapGapMinEscalationWindowMs`; an unrelated transport or upgrade failure resets
+the episode so outage time cannot accidentally satisfy it.
+
+Keep sibling adoption off unless the parent is a dedicated store-and-forward group:
+every record-bearing child directory that is not the foreground slot is considered
+eligible. Browser senders never scan or persist local slots.
+
+An offline sender cannot inspect the server-advertised batch cap before its first
+publication. Set `qwp.session.maxBatchSizeBytes` to a value no greater than the
+smallest target node's cap when offline startup is required.
+
+Set `awaitServerAck: true` when a particular flush must observe QuestDB's protocol ACK
+before returning. `awaitDurableAck: true` implies server-ACK waiting and additionally
+waits for replicated/durable progress. Browser senders use the in-memory replay
+publication boundary by default and do not offer persistent disk publication.
+
+A crash after the server accepts a frame but before local acknowledgement cleanup can
+replay that frame, so delivery is at least once. Applications that require exactly-once
+effects should use their own stable event key or another idempotency strategy. Closing
+a persistent sender stops its drainer but preserves published, unacknowledged frames for
+the next sender using that directory.
+
+### Direct high-level API
+
+Use `QwpSender` directly when QWP-only column types or detailed session controls are
+needed:
+
+```typescript
+import * as qwp from "@questdb/nodejs-client";
+import { connectQwpNodeSender } from "@questdb/nodejs-client";
+
+const sender = await connectQwpNodeSender(
+ {
+ url: "wss://questdb.example:9000/write/v4",
+ authorization: `Bearer ${token}`,
+ },
+ {
+ autoFlushRows: 5_000,
+ autoFlushBytes: 4 * 1024 * 1024,
+ autoFlushIntervalMs: 1_000,
+ encode: { symbolDictionary: "delta", gorilla: true },
+ },
+);
+
+try {
+ await sender
+ .table("telemetry")
+ .symbol("device", "sensor-7")
+ .longColumn("sequence", 42n)
+ .uuidColumn("event_id", "9f1c96b2-54b8-4d85-bb24-e82c6f1ac120")
+ .at(1_775_000_000_000, "ms");
+ await sender.flush();
+} finally {
+ await sender.close();
+}
+```
+
+A row in progress is the columns staged so far plus the table selected by
+`table()`. When a setter or `at()` rejects a value, the sender discards both, so a
+half-built row can never reach QuestDB and the next row starts from `table()` again:
+
+```typescript
+for (const reading of readings) {
+ try {
+ await sender
+ .table("telemetry")
+ .symbol("device", reading.device)
+ .floatColumn("value", reading.value)
+ .at(reading.timestamp, "ms");
+ } catch (error) {
+ // Only this row is gone. Rows staged earlier stay pending.
+ log.warn(error);
+ }
+}
+await sender.flush();
+```
+
+Setters called after a failure raise `table name must be set before adding columns`
+rather than quietly joining a fresh row. `cancelRow()` discards a row in progress the
+same way without an error, and `reset()` remains the heavier option that also drops
+every row staged since the last flush.
+
+### Compiled object-row writers
+
+For repeated rows with one table schema, compile a table-bound writer instead of
+sharing the fluent row-builder state:
+
+```typescript
+const trades = sender.writer("trades", {
+ symbol: qwp.symbol(),
+ side: qwp.symbol(),
+ price: qwp.double(),
+ quantity: qwp.long(),
+ timestamp: qwp.designatedTimestamp("ns"),
+});
+
+await trades.row({
+ symbol: "ETH-USD",
+ side: "sell",
+ price: 2615.54,
+ quantity: 42n,
+ timestamp: 1_723_000_000_000_000_000n,
+});
+
+await trades.rows([
+ {
+ symbol: "BTC-USD",
+ side: "buy",
+ price: 39_269.98,
+ quantity: 7n,
+ timestamp: 1_723_000_001_000_000_000n,
+ },
+]);
+```
+
+`rows()` accepts `Iterable` and `AsyncIterable` sources and applies the sender's
+normal auto-flush, batch-cap, backpressure, transaction, symbol-dictionary, and ACK
+settings. The schema is validated once.
+
+The schema vocabulary covers every column type the fluent row API can write:
+
+| Field | QuestDB type | Accepted row values |
+| --------------------------- | -------------------- | -------------------------------------------------------------------------------------- |
+| `symbol()` | SYMBOL | `string` |
+| `varchar()` | VARCHAR | `string` |
+| `char()` | CHAR | `string` of one UTF-16 code unit |
+| `bool()` | BOOLEAN | `boolean` |
+| `byte()` | BYTE | `number` |
+| `short()` | SHORT | `number` |
+| `int32()` | INT | `number`; `-2_147_483_648` is the NULL sentinel |
+| `int64()`, `long()` | LONG | `bigint`; `-9_223_372_036_854_775_808n` is the NULL sentinel |
+| `float32()` | FLOAT | `number` |
+| `float64()`, `double()` | DOUBLE | `number` |
+| `timestamp(unit)` | TIMESTAMP | `number` or `bigint`; `"ns"` requires `bigint` |
+| `designatedTimestamp(unit)` | designated TIMESTAMP | as above, required in every row |
+| `date()` | DATE | epoch milliseconds; `-9_223_372_036_854_775_808n` is the NULL sentinel |
+| `binary()` | BINARY | `Uint8Array`, copied on append |
+| `uuid()` | UUID | canonical UUID text, 16 canonical big-endian bytes, or `{ low, high }` |
+| `long256()` | LONG256 | unsigned 256-bit `bigint`, `0x` hex text, four little-endian words, or `{ words }` |
+| `ipv4()` | IPV4 | dotted-quad text or signed/unsigned packed address; `0.0.0.0` is the NULL sentinel |
+| `geohash(precisionBits)` | GEOHASH | raw bits, base-32 text of `precisionBits / 5` characters, or `{ bits, precisionBits }` |
+| `decimal64(scale)` | DECIMAL64 | unscaled `bigint`, decimal text, `number`, or `{ unscaled, scale }` |
+| `decimal128(scale)` | DECIMAL128 | as above, scale up to 38 |
+| `decimal256(scale)` | DECIMAL256 | as above, scale up to 76 |
+| `doubleArray()` | DOUBLE[] | uniform nested arrays or `{ dimensions, values }`; 1 to 32 dimensions |
+| `longArray()` | LONG[] | encodes the protocol type, but current QuestDB servers reject ingestion |
+
+LONG, LONG256, and nanosecond timestamp inputs are `bigint` so they cannot silently
+lose precision. The record forms are exactly what the egress result views hand back,
+so a query result value can be written straight into a row without conversion.
+
+Current QuestDB servers accept only DOUBLE arrays for ingestion. `longArrayColumn()`
+and `longArray()` remain available for Java-client and protocol parity and encode the
+QWP LONG_ARRAY type, but flushing one is rejected by the server with `long arrays are
+not supported, only double arrays`. Decoding LONG_ARRAY values in query results remains
+supported. QWP arrays may have between 1 and 32 dimensions; the client rejects a larger
+rank before encoding a frame.
+
+QuestDB reserves the minimum signed value as NULL for INT, LONG, and DATE. Both the
+fluent setters (`int32Column()`, `longColumn()`, and `dateColumn()`) and the compiled
+writer fields above follow the Java QWP client and server convention: passing
+`-2_147_483_648` to INT, or `-9_223_372_036_854_775_808n` to LONG or DATE, stores
+NULL. These sentinel values cannot be stored as ordinary numeric values. Passing
+`null` or `undefined`, or omitting a compiled-writer field, also writes NULL.
+
+Widths are spelled out deliberately. The fluent row API predates these names and its
+`floatColumn()` and `intColumn()` are 64-bit despite reading as 32-bit, with
+`float32Column()` and `int32Column()` as the narrow forms. Compiled writers avoid the
+ambiguity: `float32()`/`float64()` and `int32()`/`int64()` mean exactly what they say.
+
+Geohash precision and decimal scale belong to the column, not the value, so they are
+fixed when the schema is compiled and validated against the sender's staged schema on
+every append. Decimal text and `{ unscaled, scale }` values are rescaled to the
+column's scale when that is exact, and rejected when it would round: at
+`decimal64(2)`, `"1.50"` stages as `150n` and `"1.005"` raises `QwpWriterRowError`.
+Base-32 geohash text carries five bits per character, so `geohash(20)` accepts
+`"u33d"` and rejects `"u33"`.
+
+Regular fields may be absent, `null`, or `undefined`, which writes a NULL. A schema
+may contain at most one designated timestamp and, when present, that field is required
+in every row. Unknown object keys and type mismatches raise `QwpWriterRowError`; bulk
+errors include the zero-based row index. A failing row is never partly staged. Rows
+successfully completed before a later iterable row fails remain available to flush.
+
+Compiled writers are also available through the regular Node `Sender` when it uses a
+QWP transport. Calling `writer()` for an HTTP or TCP ILP sender raises an error. A
+writer obtained from a pooled sender lease cannot be used after the lease is closed.
+
+Like the Java QWP sender, `flush()` and `commit()` resolve after the complete
+logical flush reaches the local ingress/replay publication boundary. They do
+not wait for a server ACK by default. Set `awaitServerAck: true` for an
+implicit ACK barrier, or use the explicit sequence API below.
+
+For producer-controlled acknowledgement barriers, publish first and wait for the
+cumulative ACK watermark separately:
+
+```typescript
+await sender
+ .table("telemetry")
+ .symbol("device", "sensor-7")
+ .longColumn("sequence", 43n)
+ .atNow();
+
+const sequence = await sender.flushAndGetSequence();
+await sender.waitForAcknowledged(sequence, 5_000);
+```
+
+`flushAndGetSequence()` always resolves at the publication boundary, independently
+of `awaitServerAck`, and returns the highest stable frame sequence published by that
+call. It returns `-1n` when there was nothing to publish. `publishedSequence` and
+`acknowledgedSequence` expose the current immutable watermarks. ACK waits are
+cumulative, so one later acknowledgement resolves all covered waits and callers may
+wait for different sequences concurrently. When durable ACK was negotiated, the
+acknowledged watermark advances only after QuestDB reports durable progress;
+otherwise it follows ordinary protocol OK responses. A deadline failure raises
+`QwpIngressAckTimeoutError` without closing an otherwise healthy session.
+
+Rows are staged until an auto-flush boundary or an explicit `flush()`. A `null` or
+`undefined` column value omits that column from the row. `atNow()` asks QuestDB to
+assign the designated timestamp; `at(value, unit)` sends an explicit `ns`, `us`, or
+`ms` timestamp. `close()` publishes completed rows and waits for the committed-frame
+ACK watermark for up to `closeFlushTimeoutMs` (5 seconds by default, matching the
+Java client). Set it to `0` or a negative value for a fast close, which publishes
+without the ACK drain; publication itself stays bounded, so `close()` always
+returns. An unfinished row is still discarded with a warning.
+The configuration-string equivalent is `close_flush_timeout_millis`.
+
+`autoFlushBytes` is a soft threshold over estimated raw column-buffer storage and is
+disabled by default (`0`). It combines with `autoFlushRows` and
+`autoFlushIntervalMs`: reaching any enabled threshold flushes after the completed row,
+so one row of overshoot is possible. Once connected, an enabled byte threshold is
+clamped to 90% of the server-advertised batch cap. Schema and symbol-dictionary
+overhead make this an estimate; exact encoded-size enforcement and automatic frame
+splitting remain the ingress session's responsibility. `sender.metrics.pendingBytes`
+and `sender.metrics.effectiveAutoFlushBytes` expose the live estimate and applied
+threshold. Configuration strings use `auto_flush_bytes=N`; `off` is equivalent to
+zero.
+
+The sender automatically maintains connection-scoped symbol IDs, emits dictionary
+deltas, tracks acknowledgements, and splits multi-row batches at the smaller of the
+client cap and the server-advertised cap. One row that cannot fit is rejected with
+`QwpBatchTooLargeError` before it is sent.
+
+Low-level Node sessions expose `publishFrame()`, `publishTables()`, and
+`publishTablesDelta()` for local-publication semantics. Their `send*()` counterparts
+continue to return the server ACK. Use the publication methods only with persistent
+store-and-forward when local durability is the intended completion boundary.
+`sendFrameWithPublication()`, `sendTablesWithPublication()`, and
+`sendTablesDeltaWithPublication()` expose both boundaries from one operation: await
+`publication` before releasing retryable source rows, then await `acknowledgement`
+when server acceptance is also required. If a split logical batch cannot be fully
+journaled, its unattempted suffix is suppressed and the operation's publication
+promise rejects.
+
+### Browser ingress
+
+Browser applications must use the browser entry point and a same-origin WebSocket
+route (directly or through a reverse proxy):
+
+```typescript
+import { connectQwpBrowserSender } from "@questdb/browser-client";
+
+const url = new URL("/write/v4", location.href);
+url.protocol = location.protocol === "https:" ? "wss:" : "ws:";
+
+const sender = await connectQwpBrowserSender({ url }, { autoFlush: false });
+
+try {
+ await sender.table("page_events").symbol("kind", "view").atNow();
+ await sender.flush();
+} finally {
+ await sender.close();
+}
+```
+
+The browser WebSocket API cannot set `Authorization` or arbitrary `X-QWP-*`
+upgrade headers. When authentication is enabled, create QuestDB's HttpOnly session
+cookies over REST before opening the WebSocket:
+
+```typescript
+import {
+ bootstrapQwpBrowserSession,
+ connectQwpBrowserSender,
+} from "@questdb/browser-client";
+
+await bootstrapQwpBrowserSession({
+ url: new URL("/exec", location.href),
+ authentication: { type: "bearer", token: oidcOrRestAccessToken },
+ // QuestDB Enterprise only; omit this to use the logged-in principal.
+ serviceAccount: "market_data_writer",
+});
+
+const sender = await connectQwpBrowserSender({ url });
+```
+
+Basic authentication is also accepted as `{ type: "basic", username, password }`.
+The application obtains OIDC tokens from its identity provider; this package does
+not run an interactive OIDC flow. The bootstrap request uses
+`credentials: "include"`. REST and WebSocket endpoints therefore need the same
+browser origin, or correctly configured credentialed CORS and cookie attributes.
+JavaScript never reads `qdb_session` or the Enterprise `qdbServiceAccount` cookie.
+
+Set `sessionBootstrap` on the WebSocket options to repeat bootstrap before every
+initial, reconnect, and failover attempt:
+
+```typescript
+const sender = await connectQwpBrowserSender({
+ url,
+ sessionBootstrap: {
+ authentication: { type: "bearer", token: oidcOrRestAccessToken },
+ serviceAccount: "market_data_writer",
+ },
+});
+```
+
+### Transactions and durable acknowledgement
+
+Transactional auto-flush keeps automatically emitted frames in an open server-side
+transaction. `commit()` (an alias for `flush()`) publishes the group-closing frame.
+The example also waits for its cumulative durable acknowledgement because it enables
+`awaitDurableAck`:
+
+```typescript
+const sender = await connectQwpBrowserSender(
+ { url, requestDurableAck: true },
+ {
+ transactional: true,
+ autoFlushRows: 10_000,
+ awaitDurableAck: true,
+ durableAckTimeoutMs: 30_000,
+ },
+);
+
+for (const event of events) {
+ await sender
+ .table("events")
+ .symbol("source", event.source)
+ .longColumn("value", event.value)
+ .at(event.timestamp, "ms");
+}
+await sender.commit();
+```
+
+Transactions are atomic per table, not across all tables in one flush. Closing a
+sender publishes locally staged transactional rows but does not implicitly commit;
+QuestDB rolls the open server transaction back. The sender logs a warning in this case.
+
+In browsers, durable ACK capability is negotiated with a WebSocket subprotocol;
+Node.js uses upgrade headers. Setting `awaitDurableAck` automatically requests the
+capability unless `requestDurableAck` was set explicitly. The connection fails with
+`QwpDurableAckUnavailableError` when the server does not confirm it. Browser durable
+tracking is in memory only. Persistent store-and-forward is intentionally Node-only.
+
+Browser ingress adds `qwp_browser_handshake=v1` to the WebSocket URL. Compatible
+servers send a small `SERVER_INFO` message immediately after the upgrade, and the
+sender uses its exact ingress payload cap for automatic splitting. Older servers
+ignore the query parameter; after a bounded 250 ms negotiation window the client
+continues in unknown-cap mode. Set `ingressNegotiationTimeoutMs` to tune that window,
+or keep using `maxBatchSizeBytes` as a local compatibility limit.
+
+### Reconnect, failover, and roles
+
+The preferred URL and `failoverUrls` form one endpoint set. Endpoints are ranked by
+observed health (`healthy`, unknown, transient rejection, transport error, topology
+rejection) and then by zone affinity; configuration order breaks ties. Health outranks
+zone, so a known healthy cross-zone node is preferred to an untried local node. Every
+connection sweep can still try every endpoint, allowing role and health changes to
+recover. A non-orderly close demotes the selected endpoint before the next sweep.
+Each standalone Node sender/drainer family, and each pooled orphan scanner, shares one
+live health ledger among its walkers while keeping independent sweep cursors, so
+concurrent drainers cannot consume one another's endpoint attempts. After a foreground
+round is exhausted, stale classifications are reset while learned zone tiers persist;
+the most recent successful same-zone endpoint remains sticky. Background orphan
+drainers publish health observations but never reset foreground classifications.
+Ingress reconnect is enabled by default for factory-created browser and Node sessions.
+Unacknowledged frames are retained in memory and replayed at least once after a
+transport failure. The built-in memory replay queue is capped at 128 MiB. When the
+cap is full, publication waits for ACK-driven trimming for at most 30 seconds, then
+rejects with `QwpMemoryReplayAppendTimeoutError`; a single frame that can never fit
+is rejected immediately with `QwpMemoryReplayFrameTooLargeError`. Set
+`memoryReplayMaxBytes` and `memoryReplayAppendDeadlineMs` on ingress session options
+to tune these bounds. The accounting includes a fixed per-frame allowance so many
+small frames cannot bypass the byte cap.
+
+The default memory policy uses full-jitter backoff from 100 ms to 5 seconds and a
+five-minute per-outage deadline; the initial connection remains fail-fast. Set
+`reconnect: false` for one fixed connection. Supplying a `reconnect` object tunes the
+bounds, emits lifecycle events through `onEvent`, and retains the earlier opt-in
+behavior of retrying initial connection establishment.
+
+QuestDB stops processing a connection's later frames after any ingress NACK so a
+cumulative ACK cannot advance across the rejected sequence. Reconnecting sessions
+recycle that connection and replay from their last ACK. A fixed `reconnect: false`
+session instead becomes terminal and closes immediately after reporting the NACK;
+create a new session before sending more rows.
+
+Each retry delay is selected between zero and the current exponential ceiling,
+preventing clients disconnected together from retrying in lockstep. Configured attempt
+and duration bounds apply to browser/memory reconnect and Node `"sync"` startup. A
+Node foreground store-and-forward replay loop remains unbounded after startup. Without
+`storeAndForward`, both Node and browser ingress replay only for the lifetime of the
+process or page; configuring a Node directory makes the same replay crash-safe.
+
+Ingress also detects a replay head that is repeatedly NACKed or followed by a
+non-orderly WebSocket close. `maxFrameRejections` defaults to 4 consecutive strikes,
+and `poisonMinEscalationWindowMs` defaults to 5 seconds. Both conditions must be met
+before escalation. Normal (1000), going-away (1001), service-restart (1012), and
+try-again-later (1013) closes, `NOT_WRITABLE`, retriable symbol-dictionary catch-up
+rejections, and intervening connection-establishment failures reset the strike
+episode. Abnormal closes (1006), internal-error closes (1011), and transport errors
+without close information may count when an unacknowledged replay head exists.
+Escalation is terminal for that producer; store-and-forward retains and quarantines
+the affected rows for explicit `retryQwpNodeOrphanSlot()` recovery rather than
+silently discarding them.
+
+Node.js sees the rejected upgrade status and `X-QuestDB-Role`, so a read-only replica
+or catching-up primary can be classified and skipped. Browsers deliberately expose
+an opaque upgrade error because their WebSocket API hides the HTTP response. Avoid
+placing ingress replica endpoints in a browser endpoint list unless the proxy routes
+writers to a primary.
+
+### Observability
+
+Use immutable metrics snapshots for polling and callbacks for event-driven telemetry:
+
+```typescript
+import {
+ QWP_INGRESS_PROGRESS_KIND,
+ createQwpNodeSender,
+} from "@questdb/nodejs-client";
+
+const sender = createQwpNodeSender(
+ { url: "ws://localhost:9000/write/v4" },
+ {},
+ {
+ reconnect: {
+ onEvent: (event) => console.info("QWP connection", event),
+ },
+ onProgress: (event) => {
+ if (event.kind === QWP_INGRESS_PROGRESS_KIND.ACKNOWLEDGED) {
+ console.info("accepted through", event.sequence);
+ }
+ },
+ onError: (event) => console.error("QWP ingress", event.error),
+ onSenderError: (error) => {
+ console.error(
+ "QWP rejection",
+ error.category,
+ error.appliedPolicy,
+ error.fromFsn,
+ error.toFsn,
+ );
+ },
+ },
+);
+
+await sender.connect();
+console.info(sender.metrics);
+```
+
+Callbacks are placed on bounded asynchronous inboxes and never invoked inside ACK,
+reconnect, or orphan-recovery protocol stacks. Connection events default to 64 retained
+entries and errors to 256; `connectionListenerInboxCapacity` and
+`errorInboxCapacity` (or their snake-case unified-string keys) tune those bounds.
+Overflow drops the oldest pending entry and retains the newest state. Inspect
+`droppedProgressNotifications`, `droppedConnectionNotifications`, and
+`droppedErrorNotifications` in the immutable ingress metrics; non-zero values mean an
+observer is not keeping up. Callback failures are contained. Callbacks still execute on
+the JavaScript event loop, so CPU-bound synchronous work should be moved to an
+application worker.
+
+`onSenderError` is the Java-parity rejection stream. Its immutable payload includes
+`category`, applied policy, raw server status/message, wire message sequence, inclusive
+stable `[fromFsn, toFsn]` correlation range, optional single-table attribution, and
+`quarantinedPath` for abandoned persistent data. The legacy `onError` callback remains
+available for timeouts and general session failures; classified NACK events also expose
+the same payload as `event.senderError`. When `onSenderError` is omitted, QWP logs
+retriable rejections at `warn` and terminal rejections or abandoned data at `error`.
+General asynchronous session failures are likewise logged when `onError` is omitted,
+so a background store-and-forward failure is never silent by default. Reconnect and
+orphan-drain fallbacks use the same bounded asynchronous error inbox; direct session
+fallback logging adds no callback or close-time dependency. Both paths work in browsers
+and Node.js.
+
+## Egress
+
+QWP egress streams typed result batches. One connection executes one active query at
+a time.
+
+```typescript
+import { connectQwpNodeEgress } from "@questdb/nodejs-client";
+
+const session = await connectQwpNodeEgress(
+ {
+ url: "wss://questdb.example:9000/read/v1",
+ failoverUrls: [
+ "wss://questdb-replica-2.example:9000/read/v1",
+ "wss://questdb-primary.example:9000/read/v1",
+ ],
+ target: "replica",
+ zone: "eu-west-1a",
+ authorization: `Bearer ${token}`,
+ compression: "zstd",
+ compressionLevel: 3,
+ maxBatchRows: 4096,
+ },
+ { queryTimeoutMs: 30_000, bufferPoolSize: 4 },
+);
+
+try {
+ const query = await session.query(
+ "select timestamp, symbol, price from trades where symbol = $1",
+ {
+ binds: (binds) => binds.setVarchar(0, "ETH-USD"),
+ initialCredit: 1024 * 1024,
+ },
+ );
+
+ for await (const batch of query) {
+ console.info(batch.columns);
+ for (const row of batch.rows()) console.info(row);
+ }
+
+ const completion = await query.completion;
+ console.info(completion);
+} finally {
+ await session.close();
+}
+```
+
+### Bounded reusable result views
+
+`query()` keeps its convenient materialized batches. For hot paths, `queryViews()`
+avoids allocating a JavaScript value array for every column and delivers one
+reusable batch view through an awaited callback:
+
+```typescript
+const query = await session.queryViews(
+ "select timestamp, symbol, price from trades",
+ async (batch) => {
+ const timestamp = batch.column(0);
+ const symbol = batch.column(1);
+ const price = batch.column(2);
+
+ // Fixed-width values are read directly from the QWP little-endian bytes.
+ for (let row = 0; row < batch.rowCount; row++) {
+ if (!price.isNull(row)) {
+ consume(
+ timestamp.getLong(row),
+ symbol.getSymbol(row),
+ price.getDouble(row),
+ );
+ }
+ }
+
+ // Raw views are available for vectorized consumers.
+ consumePackedDoubles(price.valuesBytes()!);
+ },
+ { initialCredit: 256 * 1024 },
+);
+await query.completion;
+```
+
+For conventional row-major processing, the same batch also owns one reusable
+`QwpResultRowView`:
+
+```typescript
+batch.forEachRow((row) => {
+ if (!row.isNull(2)) {
+ consume(row.getLong(0), row.getSymbol(1), row.getDouble(2));
+ }
+});
+
+// Direct indexed access uses the same flyweight.
+const first = batch.row(0);
+consume(first.rowIndex, first.getString(1));
+```
+
+`forEachRow()` is synchronous, visits rows in index order, propagates callback
+exceptions, and re-points the same row object on every iteration. Do not retain
+the row object or any zero-copy value returned from it; copy the value inside the
+current invocation when it must survive. Calling `batch.row(index)` also returns
+that shared object, re-pointed to the requested row.
+
+The batch, its column objects, and every `Uint8Array`/`Int32Array` returned by a
+column or row are valid only until the callback settles. The decoder reuses those
+objects and its NULL-index, symbol-ID, array-offset, and Gorilla-timestamp scratch
+storage for later batches. Copy an individual byte view with `.slice()`, or call
+`batch.materialize()` inside the callback, when data must be retained.
+
+Raw fixed-width, NULL, VARCHAR/BINARY, and array data views point into the current
+decoded frame; Zstd results point into that batch's decompressed buffer. Accessors
+such as `getString()` and `get()` decode or construct only the requested cell. The
+callback is awaited before automatic credit is replenished, so the configured
+credit window bounds server read-ahead while application work is in progress.
+
+`target` accepts `any` (the default), `primary`, or `replica`. Primary routing also
+accepts standalone servers and a primary completing catch-up, matching the Java
+client. Both keys apply to ingress and egress alike. `zone` is an opaque, case-insensitive preference for `any` and `replica`;
+cross-zone endpoints remain eligible. It is ignored for `primary`, which must be
+followed across zones. The client validates the authoritative role and zone from the
+first QWP `SERVER_INFO` frame before accepting an endpoint, so the same guarantees
+work in browsers even though browser WebSocket APIs hide upgrade response headers.
+
+Bind indexes are zero-based in the client: index `0` is SQL placeholder `$1`.
+`QwpBindValues` supports booleans, integer and floating-point values, dates,
+microsecond and nanosecond timestamps, strings, UUIDs, LONG256, geohashes,
+decimals, and typed nulls. Set values in ascending index order. `bindPayload` and
+`bindCount` remain advanced escape hatches for pre-encoded data.
+
+Set per-query `resetDictionary: true` to ask the server to reset its
+connection-scoped egress symbol dictionary before execution. The client sends the
+flag only when `SERVER_INFO` advertises `QUERY_FLAGS`; older servers receive the
+same flag-free request as the default path, so this option remains safe during a
+rolling upgrade.
+
+Matching Java, the high-level client defaults `initialCredit` to zero, allowing
+unbounded server send-ahead. Set a positive session-level or per-query value to bound
+wire buffering, particularly in browsers. With positive credit, the exact wire size
+of each consumed batch is replenished automatically. Set `autoCredit: false` and call
+`query.grantCredit()` for manual control.
+
+Both materialized `query()` results and zero-copy `queryViews()` use a client-side
+decoded-batch pool with four slots by default. Set the session-level
+`bufferPoolSize` to tune this bound. Materialized decoding pauses when all slots are
+queued until iteration requests another batch. For `queryViews()`, callbacks remain
+serial and callback-scoped, while the receive loop continues decoding into the other
+reusable slots; a slow callback stalls decoding only after the pool fills. This bound
+is independent of QWP credit, so `initialCredit: 0` no longer permits an unbounded
+queue of decoded batches. Protocol credit remains the stronger end-to-end bound,
+particularly in browsers where the WebSocket implementation may buffer raw frames
+before JavaScript reads them.
+
+A session `queryTimeoutMs` supplies the default deadline; per-query `timeoutMs`
+overrides it, and zero disables it. Expiry rejects iteration and `completion` with
+`QwpEgressQueryTimeoutError`, sends QWP `CANCEL`, and drains the terminal response
+before the connection accepts another query. Breaking out of `for await` early also
+discards buffered batches, restores their flow-control credit, sends `CANCEL`, and
+rejects `completion` with `QwpEgressQueryAbandonedError`. Call `query.cancel()` for
+explicit cancellation.
+
+`await query.awaitCompletion(timeoutMs)` bounds only the caller's wait and returns
+`false` without cancelling when the timeout expires, matching Java
+`Completion.await(timeout, unit)`. `query.isDone()` reports terminal state. Use the
+query deadline options only when timeout should actively cancel the server query.
+The initial and reconnect `SERVER_INFO` timeout defaults to five seconds, matching
+Java, and remains configurable through `serverInfoTimeoutMs`.
+
+Cancellation draining is bounded by `cancelDrainTimeoutMs` (5 seconds by default).
+Late batches are decoded and credited while the terminal response is pending. If the
+server does not terminate the query within the bound, the client fails with
+`QwpEgressQueryCancelTimeoutError` and closes the unusable connection instead of
+leaving the session permanently occupied.
+
+Node.js and browsers can request Zstd with `compression: "zstd"` or `"auto"` and a
+level from 1 through 22. Raw remains the compatibility default. Node uses
+`X-QWP-Accept-Encoding`; browsers send the same preference in the URL's
+`qwp_accept_encoding` parameter. Compatible servers report the effective codec and
+operator-forced level in the existing egress `SERVER_INFO` message. Check
+`session.negotiatedCompression` after the handshake. Older servers ignore the query
+parameter and safely remain raw. The decoder handles raw and Zstd batches in both
+runtimes.
+
+Set transport-level `maxBatchRows` from 1 through 1,048,576 to ask QuestDB for
+smaller `RESULT_BATCH` messages. The server clamps the request to its hard cap. Node
+sends `X-QWP-Max-Batch-Rows`; browsers use the `qwp_max_batch_rows` URL parameter,
+which requires a server that supports browser QWP negotiation. Older servers ignore
+the browser parameter and keep their configured batch size.
+
+A single `RESULT_BATCH` may declare at most `QWP_MAX_CELLS_PER_BATCH` cells --
+32Mi, its rows multiplied by its columns. The row and column caps bound each
+dimension on its own, and a compressed body detaches the grid they describe from
+the bytes on the wire: an all-NULL column is one bit per cell before Zstd, so
+without this bound a few kilobytes of RLE-compressed bitmap declares a result no
+heap can hold. The bound is checked before any column is read, and 32Mi cells sits
+far above any plausible result -- the widest supported table at 16k rows, or a full
+1,048,576-row batch at 32 columns. Lower `maxBatchRows` for genuinely wide tables.
+
+Opening a connection runs under two deadlines. `connect_timeout` covers the TCP/TLS
+transport, and `auth_timeout_ms` takes over for the WebSocket upgrade and the
+authentication exchange as soon as the transport connects; both default to 15
+seconds. Setting only `connect_timeout` bounds both phases with that value, so an
+endpoint that accepts TCP and never answers the upgrade -- a stalled proxy or load
+balancer -- fails inside the budget you asked for rather than 15 seconds later. Set
+`auth_timeout_ms` as well when the upgrade legitimately needs longer than the
+transport.
+
+Egress failover is enabled by default in Node.js and browsers. A transport failure or
+invalid protocol response closes and deprioritizes that endpoint, reconnects, resets
+connection-scoped decoding state, and re-executes the active query. The default policy
+uses eight connection sweeps, full-jitter backoff starting at 50 ms and capped at one
+second, and a 30-second outage deadline. `QUERY_ERROR` remains a query result and does
+not trigger failover.
+
+`session.ready` resolves once with the initial `SERVER_INFO`. Read
+`session.serverInfo` for the immutable snapshot from the currently bound endpoint:
+role, zone, cluster and node IDs, epoch, capabilities, server clock, and negotiated
+compression. Reading the property is non-perturbing and never initiates a failover
+walk. If an endpoint dies, it continues to report the previous snapshot until the
+transport successfully rebinds, then refreshes to the new endpoint.
+
+Re-execution is at least once: a statement may have completed before its response was
+lost, and a consumer may already have observed a prefix of SELECT rows. Queued but
+unconsumed batches are discarded automatically. Configure `onReplayReset` when the
+application must clear an accumulated prefix before batches restart at sequence zero;
+the callback is an optional notification, not an opt-in. Set `reconnect: false` to use
+one fixed connection and surface failures without replay. Supplying a `reconnect`
+object tunes the failover bounds and also retains the earlier opt-in behavior of
+retrying initial connection establishment.
+
+Browser egress uses the same session API:
+
+```typescript
+import { connectQwpBrowserEgress } from "@questdb/browser-client";
+
+const readUrl = new URL("/read/v1", location.href);
+readUrl.protocol = location.protocol === "https:" ? "wss:" : "ws:";
+
+const session = await connectQwpBrowserEgress({
+ url: readUrl,
+ failoverUrls: ["wss://replica-2.example/read/v1"],
+ target: "replica",
+ zone: "eu-west-1a",
+ compression: "zstd",
+ compressionLevel: 3,
+ sessionBootstrap: {
+ authentication: { type: "bearer", token: oidcOrRestAccessToken },
+ },
+});
+```
+
+## Combined pooled client
+
+Use `QwpClient` when one long-lived application component needs both ingestion
+and concurrent queries. The Node and browser entry points provide configured
+factories; each borrowed handle exclusively owns one pooled WebSocket until its
+`close()` returns it:
+
+For Node, the recommended common-case API accepts one Java-style
+`ws::`/`wss::` cluster string. Every `addr` entry is shared by ingress and
+egress; the facade derives `/write/v4` and `/read/v1`, applies the same
+authentication and TLS configuration to both sides, and validates ingress,
+egress, and pool settings before opening a socket:
+
+```typescript
+import { connectQwpNodeClient } from "@questdb/nodejs-client";
+
+const db = await connectQwpNodeClient(
+ "wss::" +
+ "addr=node-a.example:9000,node-b.example:9000;" +
+ `token=${token};` +
+ "target=replica;zone=eu-west-1a;" +
+ "sender_pool_max=2;query_pool_max=8;",
+);
+```
+
+Repeated `addr=` keys also accumulate endpoints. Programmatic overrides for
+callbacks, custom agents, store-and-forward, sender/session settings, and pool
+sizes may be passed as the second argument. The whole string is still validated
+before overrides are applied, matching the Java builder's fail-fast behavior.
+
+A custom `wss://` agent is the WebSocket upgrade's sole TLS channel, so it
+carries its own certificate verification and cannot be combined with
+`tls_verify`, `tls_roots`, or `tls_roots_password` — that combination is
+rejected rather than silently dropping either. Configure verification on the
+agent instead, and pass an `https.Agent` for `wss` (a plain `http.Agent` is for
+`ws`).
+
+The Node client accepts `tls_roots` only as valid PEM-encoded CA certificates.
+Password-protected PKCS#12 trust stores and `tls_roots_password` are rejected:
+Node's `pfx` option represents client private-key/certificate identity, not
+additional trusted roots. Export the CA certificates to PEM and omit the
+password key.
+
+Set `lazy_connect=on` to tolerate an unavailable cluster during startup. In the
+JavaScript client, ingress uses memory replay by default, or persistent replay when
+`sf_dir` is present, with `initial_connect_retry=async`; egress uses
+`query_pool_min=0` and connects on the first query. Explicit
+`initial_connect_retry=off|sync` or a positive `query_pool_min` conflicts with
+`lazy_connect` and is rejected before the client is created:
+
+```typescript
+const db = await connectQwpNodeClient(
+ "wss::addr=node-a.example,node-b.example;" + "lazy_connect=on;",
+);
+```
+
+For unified strings with `sf_dir`, Java-compatible defaults apply: memory
+durability, a 10 GiB total journal cap, 4 MiB frame/segment batches, a 30-second
+capacity wait, a 60-second close drain, and fail-fast initial connection. Set
+`sender_id` to name the disk slot base; pooled senders use `-`.
+Without `sf_dir`, `sf_max_total_bytes` and `sf_append_deadline_millis` tune the
+built-in memory replay queue instead.
+The parser also supports `max_name_len` and the Java listener/error inbox
+capacity keys. Those capacities actively bound asynchronous connection and
+typed-error delivery and are reflected in ingress drop counters.
+
+The object form remains available for cases where constructing the two sides
+separately is useful:
+
+```typescript
+import { connectQwpNodeClient } from "@questdb/nodejs-client";
+
+const db = await connectQwpNodeClient({
+ ingress: {
+ url: "wss://questdb.example:9000/write/v4",
+ authorization: `Bearer ${token}`,
+ },
+ egress: {
+ url: "wss://questdb.example:9000/read/v1",
+ authorization: `Bearer ${token}`,
+ target: "replica",
+ zone: "eu-west-1a",
+ },
+ pool: {
+ senderPoolMin: 1,
+ senderPoolMax: 2,
+ queryPoolMin: 1,
+ queryPoolMax: 8,
+ acquireTimeoutMs: 5_000,
+ idleTimeoutMs: 60_000,
+ maxLifetimeMs: 30 * 60_000,
+ housekeepingIntervalMs: 5_000,
+ },
+});
+
+try {
+ const sender = await db.borrowSender();
+ try {
+ await sender.table("trades").symbol("symbol", "ETH-USD").atNow();
+ } finally {
+ // Flushes completed rows and returns the sender; the socket stays pooled.
+ await sender.close();
+ }
+
+ const [prices, volumes] = await Promise.all([
+ db.borrowQuery(),
+ db.borrowQuery(),
+ ]);
+ try {
+ // These use independent egress WebSockets and may execute concurrently.
+ const drain = async (lease, sql) => {
+ const query = await lease.query(sql);
+ for await (const batch of query) consume(batch);
+ await query.completion;
+ };
+ await Promise.all([
+ drain(prices, "select * from latest_prices"),
+ drain(volumes, "select * from hourly_volumes"),
+ ]);
+ } finally {
+ await Promise.all([prices.close(), volumes.close()]);
+ }
+} finally {
+ await db.close();
+}
+```
+
+Browser applications can likewise describe the cluster, REST/OIDC
+authentication bootstrap, and failover order once. A cluster URL may be an
+origin, a reverse-proxy base path, or an existing `/write/v4` or `/read/v1`
+endpoint; the facade derives both protocol routes while preserving query
+parameters. Omit `sessionBootstrap.url` to derive the matching `/exec` route
+for every failover endpoint:
+
+```typescript
+import { connectQwpBrowserClient } from "@questdb/browser-client";
+
+const db = await connectQwpBrowserClient({
+ cluster: {
+ url: "wss://node-a.example/qdb",
+ failoverUrls: ["wss://node-b.example/qdb"],
+ sessionBootstrap: {
+ authentication: { type: "bearer", token: oidcOrRestAccessToken },
+ serviceAccount: "analytics",
+ },
+ },
+ ingress: { requestDurableAck: true },
+ egress: {
+ target: "replica",
+ zone: "eu-west-1a",
+ compression: "zstd",
+ },
+ pool: { senderPoolMax: 2, queryPoolMax: 8 },
+});
+```
+
+`url`, `failoverUrls`, and `sessionBootstrap` belong to `cluster` in this
+unified form and are rejected if repeated under `ingress` or `egress`.
+Side-specific timeouts, WebSocket factories, durable-ACK settings, routing, and
+compression remain available as explicit overrides. The original split object
+form with complete `ingress` and `egress` trees remains supported for advanced
+cases that intentionally connect the two sides differently.
+
+`connectQwpNodeClient()` and `connectQwpBrowserClient()` prewarm each configured
+pool minimum. Their `createQwp*Client()` counterparts are lazy. Pools grow to
+their maximum under concurrent borrows and apply one FIFO acquisition deadline;
+exhaustion raises `QwpPoolAcquireTimeoutError`. Query handles are single-flight,
+but separate borrowed handles run concurrently. Returning a handle with an active
+query sends `CANCEL` and waits for the session's bounded cancellation drain; a
+connection that cannot drain is closed instead of being handed to another borrower.
+Each query lease exposes the same refreshed snapshot as `lease.serverInfo`; accessing
+it after returning the lease raises `QwpClientClosedError` rather than exposing a
+pooled connection now owned by another borrower.
+The shared housekeeper closes excess connections after `idleTimeoutMs` and recycles
+connections older than `maxLifetimeMs` once they are idle, while always retaining
+each configured pool minimum. Set either timeout to zero to disable that policy;
+`housekeepingIntervalMs` controls how quickly an expired idle connection is noticed.
+Prefer returning application-owned leases before calling `QwpClient.close()`.
+If shutdown races a borrower, it rejects queued borrowers, closes idle connections,
+and cancels active queries before closing every borrowed query connection. A query
+lease that is never returned therefore cannot retain a WebSocket after client
+shutdown; subsequent operations on it fail as closed. Borrowed senders remain under
+their producer's ownership: shutdown waits up to `acquireTimeoutMs` (capped at five
+seconds) for them to return and never closes a sender underneath its borrower. A
+sender returned during or after shutdown is closed instead of re-entering the pool,
+while a sender that outlives the bounded wait owns its eventual teardown.
+
+Pooled sender `close()` flushes completed rows, discards an unfinished row with a
+warning, and resets staging before reuse. With Node store-and-forward enabled, the
+configured directory is treated as a pool root and each stable sender slot owns a
+`sender-N` child directory, avoiding journal lock conflicts. The configured
+`senderPoolMin` remains authoritative. A client-level recovery scanner reserves and
+drains inactive canonical slots independently of foreground pool connections, both
+inside the current range and outside it after `senderPoolMax` is reduced. Foreground
+creation and recovery share an atomic slot coordinator, so neither can acquire a
+managed journal while the other owns it. This managed-slot recovery is automatic;
+`drainOrphans: true` additionally adopts noncanonical sibling slots beneath the pool
+root.
+
+## Error handling and cleanup
+
+The public error classes preserve enough context for policy decisions:
+
+| Error | Meaning |
+| ---------------------------------- | ----------------------------------------------------------------------------------------------------------- |
+| `QwpUpgradeError` | Classified authentication, role, version, capability, timeout, transport, or browser-opaque upgrade failure |
+| `QwpRoleMismatchError` | A connected endpoint's advertised role does not satisfy the requested egress target |
+| `QwpPoolAcquireTimeoutError` | Every pooled connection is leased beyond the configured acquisition deadline |
+| `QwpPoolResourceError` | Creating a new pooled sender or query connection failed |
+| `QwpClientClosedError` | The pooled client or an individual returned lease is already closed |
+| `QwpDurableAckUnavailableError` | Durable acknowledgement was required but not negotiated |
+| `QwpSendTimeoutError` | A send did not drain before its deadline; delivery is unknown |
+| `QwpSenderCloseTimeoutError` | Sender shutdown could not publish and ACK-drain all committed ingress frames within its deadline |
+| `QwpIngressNackError` | QuestDB rejected an ingress frame |
+| `QwpIngressAckTimeoutError` | The cumulative ingress ACK watermark did not reach the requested sequence before its deadline |
+| `QwpBatchTooLargeError` | One encoded row cannot fit the effective ingress cap |
+| `QwpReconnectExhaustedError` | The configured reconnect boundary was reached |
+| `QwpReplayRejectedError` | A replayed frame was rejected and retained for inspection |
+| `QwpReplayStoreFullError` | The Node.js replay journal reached its configured size |
+| `QwpReplayStoreAppendTimeoutError` | The Node.js replay journal did not regain capacity before the configured append deadline |
+| `QwpReplayStoreCheckpointError` | A periodic Node.js replay-journal checkpoint failed; operations fail closed until a retry succeeds |
+| `QwpReplayStoreLockedError` | Another process owns the configured Node.js replay directory |
+| `QwpEgressQueryError` | QuestDB returned a terminal query error |
+| `QwpEgressQueryAbandonedError` | Result iteration ended before the server completed the query |
+| `QwpEgressQueryTimeoutError` | The client deadline expired and cancellation began |
+| `QwpEgressQueryCancelTimeoutError` | A cancelled query did not produce a terminal server response before the drain deadline |
+| `QwpEgressReplayRequiredError` | Deprecated compatibility type from the former explicit replay opt-in |
+
+Always close senders and sessions in `finally`. Sender publication plus ACK draining is
+bounded by `closeFlushTimeoutMs`; the subsequent WebSocket closing handshake is bounded
+by `closeTimeoutMs`. In Node, `connectTimeoutMs` and `authTimeoutMs` independently
+bound transport connection and authenticated upgrade. `sendTimeoutMs`, acknowledgement
+timeouts, and query deadlines cover later lifecycle phases; configure each according
+to the deployment rather than using one very large catch-all value.
+
+## Migration guide
+
+### Existing Node.js `Sender`
+
+For the common fluent API, migration is primarily a transport change:
+
+```diff
+- const sender = await Sender.fromConfig("http::addr=localhost:9000");
++ const sender = await Sender.fromConfig("ws::addr=localhost:9000");
+```
+
+Review these behavioral differences before rollout:
+
+- QWP `flush()` uses the Java-compatible local-publication boundary by default in
+ browsers and Node.js. Set `awaitServerAck` for a protocol ACK barrier, or
+ `awaitDurableAck` to wait through durable upload. With Node persistent
+ store-and-forward, local publication means durable journal append.
+- QWP symbol dictionaries are connection-scoped and automatic.
+- Table and column identifiers are rejected locally using the Java client's rules;
+ column identity is case-insensitive and preserves the spelling first declared.
+- Large batches are split to the negotiated WebSocket payload cap.
+- QWP transactional auto-flush is per table and must be explicitly committed.
+- Browser and Node QWP ingress reconnect by default with in-memory, at-least-once
+ replay. That queue has a 128 MiB cap and a bounded 30-second capacity wait by
+ default. Configure Node store-and-forward when replay must survive process failure.
+- HTTP/TCP-only keys do not carry over to `ws::`; use the unified QWP connect-string
+ vocabulary. Programmatic callbacks, custom agents, and other non-string hooks remain
+ available under `extraOptions.qwp`.
+- Auto-flush defaults differ from the ILP transports, matching the Java client's
+ separate WebSocket defaults: `auto_flush_rows` is `1000` where `http::` uses
+ `75000` and `tcp::` uses `600`, and `auto_flush_interval` is `100` ms where both
+ use `1000` ms. A workload migrated on the one-line change above therefore sends
+ smaller batches far more often; set both keys explicitly to keep its previous
+ batching.
+
+Roll out `ws::` per sender instance so the existing protocols can remain in service
+during migration.
+
+### Low-level QWP ingress
+
+Code that manually creates `QwpTableBuffer` and calls
+`QwpIngressSession.sendTables()` can normally move to `connectQwpNodeSender()` or
+`connectQwpBrowserSender()`. Keep low-level sessions only when an application needs
+to produce encoded table buffers itself. The high-level sender owns batching, symbol
+deltas, ACK tracking, auto-flush, transactions, and durable waits.
+
+### Java client concepts
+
+The TypeScript high-level sender follows the Java client's core model—fluent rows,
+automatic batching, connection-scoped symbol dictionaries, negotiated caps, durable
+acknowledgement, and persistent replay—but uses runtime-specific connection factories:
+
+| Java client concept | TypeScript API |
+| ---------------------------- | ------------------------------------------------------------- |
+| Sender/builder configuration | `Sender.fromConfig()` in Node.js, or `connectQwp*Sender()` |
+| Fluent table row | `table()`, typed column methods, `at()` / `atNow()` |
+| Local publish/commit | `flush()` / `commit()` |
+| Explicit ACK barrier | `flushAndGetSequence()` plus `waitForAcknowledged()` |
+| Durable delivery | `requestDurableAck` plus `awaitDurableAck` |
+| Store-and-forward | Node `storeAndForward`; intentionally unavailable in browsers |
+| Fire-and-forget UDP ingress | Node `udp::` or `connectQwpNodeUdpSender()` |
+| Query parameters | `session.query(sql, { binds })` |
+| Materialized result batches | `for await (const batch of query)` |
+| Reusable result views | `queryViews()` with column views or `forEachRow()` row views |
+| Egress row/buffer bounds | `maxBatchRows` and session `bufferPoolSize` |
+
+Unlike Java's dedicated dispatcher threads, TypeScript callback inboxes schedule work on
+later JavaScript event-loop turns. This keeps user callbacks out of protocol call stacks,
+but CPU-bound callback code still blocks the runtime and belongs in a Worker or
+`worker_threads` task.
+
+## Development benchmarks
+
+The repository includes diagnostic QWP benchmarks for ingress encoding, fluent sender
+construction, symbol dictionaries, egress materialization and reusable views, Zstd,
+store-and-forward persistence/recovery, and live completion-boundary latency. See
+[`benchmarks/README.md`](benchmarks/README.md) for commands and result interpretation.
+They are intentionally not CI performance gates.
+
+## Public API policy
+
+Only the four package entry points listed at the top are public. In particular,
+paths containing `internal`, `qwp-node`, or `src` are implementation details even if
+a bundler can resolve them. The compatibility contract checks the documented
+high-level constructors, session classes, errors, constants, and option signatures
+from the shared, browser, and Node entry points. Additional low-level codec exports
+from `qwp` are intended for advanced integrations; prefer high-level APIs when no
+custom encoder or transport is required.
diff --git a/docs/modules.html b/docs/modules.html
index 741f1bc..6d5120f 100644
--- a/docs/modules.html
+++ b/docs/modules.html
@@ -1,2 +1 @@
-QuestDB Node.js Client - v4.2.0 QuestDB Node.js Client - v4.2.0
A Node.js client for QuestDB.
-Interfaces
Type Aliases
+QuestDB JavaScript Client - v4.2.0 QuestDB JavaScript Client - v4.2.0
diff --git a/docs/modules/_questdb_browser-client.html b/docs/modules/_questdb_browser-client.html
new file mode 100644
index 0000000..4aa088f
--- /dev/null
+++ b/docs/modules/_questdb_browser-client.html
@@ -0,0 +1,109 @@
+@questdb/browser-client | QuestDB JavaScript Client - v4.2.0 Module @questdb/browser-client
Browser WebSocket adapter and browser-safe QWP protocol/session APIs.
+QuestDB JavaScript Client for browsers
The official browser-only QuestDB client. It provides QWP ingestion, streaming
+queries, failover, typed row writers, and browser session authentication without
+Node.js modules or polyfills.
+The complete browser API is exported from @questdb/browser-client. There are
+no additional public import paths.
+Features
+- QWP ingestion through the browser's native WebSocket API
+- Streaming queries with typed bind variables and result batches
+- Automatic batching, reconnect, failover, and acknowledgement tracking
+- Transactional ingestion and durable acknowledgement negotiation
+- REST, OIDC, and Basic authentication through HttpOnly session cookies
+- ESM, CommonJS, and bundled TypeScript declarations
+- No Node.js built-ins, Node.js typings,
ws, or undici
+
+Requirements
+- A modern browser with
WebSocket, fetch, URL, TextEncoder, and
+TextDecoder
+- QuestDB QWP routes exposed at
/write/v4 and /read/v1
+- The
/exec REST route when authentication bootstrap is needed
+
+This package does not contain the Node.js ILP transports; use
+@questdb/nodejs-client for server-side Node.js programs.
+Installation
npm install @questdb/browser-client
+
+
+yarn add @questdb/browser-client
+
+
+pnpm add @questdb/browser-client
+
+
+The package works with browser bundlers such as Vite, Rollup, webpack, and
+esbuild. Import only from the package root:
+import { connectQwpBrowserSender } from "@questdb/browser-client";
+
+
+Quick start: ingest from a browser
Serve QuestDB's QWP route from the application's origin, either directly or
+through a reverse proxy. The browser will then apply the page's normal cookie,
+origin, and TLS rules to the WebSocket connection.
+import { connectQwpBrowserSender } from "@questdb/browser-client";
const writeUrl = new URL("/write/v4", window.location.href);
writeUrl.protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const sender = await connectQwpBrowserSender(
{ url: writeUrl },
{ autoFlush: false },
);
try {
await sender
.table("page_events")
.symbol("kind", "view")
.stringColumn("path", window.location.pathname)
.timestampColumn("recorded_at", Date.now(), "ms")
.atNow();
await sender.flush();
} finally {
await sender.close();
}
+
+
+Use wss: whenever the page is served over HTTPS. Browsers block insecure
+WebSockets from secure pages.
+Batch and commit rows
Transactional mode keeps automatically emitted frames in one open server-side
+transaction. commit() publishes the final frame. Transactions are atomic per
+table, not across every table in one flush.
+import { connectQwpBrowserSender } from "@questdb/browser-client";
const sender = await connectQwpBrowserSender(
{ url: writeUrl, requestDurableAck: true },
{
transactional: true,
autoFlushRows: 10_000,
awaitDurableAck: true,
durableAckTimeoutMs: 30_000,
},
);
try {
for (const event of [
{ source: "checkout", value: 1n, timestamp: Date.now() },
{ source: "search", value: 3n, timestamp: Date.now() },
]) {
await sender
.table("events")
.symbol("source", event.source)
.longColumn("value", event.value)
.at(event.timestamp, "ms");
}
await sender.commit();
} finally {
await sender.close();
}
+
+
+Browser replay is held in memory and survives reconnects only while the page is
+alive. Persistent store-and-forward is intentionally available only from the
+Node.js package.
+Type-safe object rows
Compile a table schema once when application data already has an object shape.
+TypeScript checks every row against the schema.
+import {
connectQwpBrowserSender,
designatedTimestamp,
double,
symbol,
} from "@questdb/browser-client";
const sender = await connectQwpBrowserSender({ url: writeUrl });
try {
const measurements = sender.writer("measurements", {
device: symbol(),
temperature: double(),
timestamp: designatedTimestamp("ms"),
});
await measurements.rows([
{ device: "sensor-1", temperature: 21.4, timestamp: Date.now() },
{ device: "sensor-2", temperature: 22.1, timestamp: Date.now() },
]);
await sender.flush();
} finally {
await sender.close();
}
+
+
+The schema vocabulary also covers QuestDB integers, decimals, UUIDs, IPv4
+addresses, geohashes, binary values, and arrays.
+Authentication
Browser JavaScript cannot add an Authorization header to a WebSocket upgrade.
+Authenticate over REST first so QuestDB can set an HttpOnly session cookie. The
+browser then sends that cookie during the QWP WebSocket upgrade.
+import {
bootstrapQwpBrowserSession,
connectQwpBrowserSender,
} from "@questdb/browser-client";
await bootstrapQwpBrowserSession({
url: new URL("/exec", window.location.href),
authentication: {
type: "bearer",
token: oidcOrRestAccessToken,
},
// QuestDB Enterprise only; omit to use the authenticated principal.
serviceAccount: "market_data_writer",
});
const sender = await connectQwpBrowserSender({ url: writeUrl });
+
+
+Basic authentication is also supported:
+const sender = await connectQwpBrowserSender({
url: writeUrl,
sessionBootstrap: {
authentication: {
type: "basic",
username: "admin",
password: "quest",
},
},
});
+
+
+Putting sessionBootstrap on the connection options repeats authentication
+before initial connection, reconnect, and failover attempts. The package does
+not run an interactive OIDC flow; the application obtains access tokens from
+its identity provider.
+The bootstrap request uses credentials. Prefer serving /exec, /write/v4,
+and /read/v1 from the application's origin. Cross-origin deployments require
+credentialed CORS and cookie attributes that permit the browser to store and
+send the session cookie. JavaScript never reads the HttpOnly cookie.
+Stream query results
QWP egress streams typed result batches. A session runs one active query at a
+time and automatically reconnects and walks configured failover URLs.
+import { connectQwpBrowserEgress } from "@questdb/browser-client";
const readUrl = new URL("/read/v1", window.location.href);
readUrl.protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const session = await connectQwpBrowserEgress(
{
url: readUrl,
compression: "zstd",
sessionBootstrap: {
authentication: { type: "bearer", token: oidcOrRestAccessToken },
},
},
{ queryTimeoutMs: 30_000 },
);
try {
const query = await session.query(
"select timestamp, device, temperature " +
"from measurements where device = $1",
{
// Bind index 0 corresponds to SQL placeholder $1.
binds: (binds) => binds.setVarchar(0, "sensor-1"),
// A positive credit window bounds server read-ahead.
initialCredit: 1024 * 1024,
},
);
for await (const batch of query) {
for (const row of batch.rows()) {
console.log(row);
}
}
await query.completion;
} finally {
await session.close();
}
+
+
+Use queryViews() for reusable zero-copy result views in allocation-sensitive
+applications. Copy any view that must outlive its batch callback.
+Combined ingestion and query client
connectQwpBrowserClient() creates bounded sender and query pools for an
+application component that needs concurrent ingestion and queries:
+import { connectQwpBrowserClient } from "@questdb/browser-client";
const clusterUrl = new URL("/", window.location.href);
clusterUrl.protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const db = await connectQwpBrowserClient({
cluster: {
url: clusterUrl,
sessionBootstrap: {
authentication: { type: "bearer", token: oidcOrRestAccessToken },
},
},
ingress: { requestDurableAck: true },
egress: { target: "replica", compression: "zstd" },
pool: { senderPoolMax: 2, queryPoolMax: 4 },
});
try {
const sender = await db.borrowSender();
try {
await sender.table("events").symbol("kind", "view").atNow();
} finally {
// Flushes completed rows and returns the sender to the pool.
await sender.close();
}
const query = await db.borrowQuery();
try {
const result = await query.query("select count() from events");
for await (const batch of result) console.log([...batch.rows()]);
await result.completion;
} finally {
await query.close();
}
} finally {
await db.close();
}
+
+
+Error handling and shutdown
+- Always close senders, query sessions, borrowed pool handles, and pooled
+clients in
finally blocks.
+- Await asynchronous row completion methods such as
at(), atNow(), and
+writer row()/rows() calls.
+- An unfinished row is never completed implicitly during
flush() or close().
+- Do not share one sender between unrelated concurrent producers.
+- Re-executed queries are at least once after failover; clear already consumed
+results in an
onReplayReset callback when duplicate prefixes matter.
+
+More documentation
+Classes
- QwpBatchTooLargeError
- QwpBindValues
- QwpBrowserSessionBootstrapError
- QwpByteReader
- QwpByteWriter
- QwpClient
- QwpClientClosedError
- QwpEgressQuery
- QwpEgressQueryAbandonedError
- QwpEgressQueryCancelTimeoutError
- QwpEgressQueryError
- QwpEgressQueryTimeoutError
- QwpEgressReplayRequiredError
- QwpEgressSession
- QwpEgressSessionClosedError
- QwpFailoverError
- QwpIngressAckTimeoutError
- QwpIngressNackError
- QwpIngressSession
- QwpIngressSessionClosedError
- QwpMemoryReplayAppendTimeoutError
- QwpMemoryReplayFrameTooLargeError
- QwpPoolAcquireTimeoutError
- QwpPoolResourceError
- QwpProtocolError
- QwpQueryLease
- QwpReconnectExhaustedError
- QwpReplayDictionaryError
- QwpReplayDictionaryPersistenceError
- QwpReplayRejectedError
- QwpResultBatch
- QwpResultBatchDecoder
- QwpResultBatchView
- QwpResultColumnView
- QwpResultRowView
- QwpRoleMismatchError
- QwpSendClosedError
- QwpSender
- QwpSenderCloseTimeoutError
- QwpSendError
- QwpSendTimeoutError
- QwpSymbolDictionary
- QwpTableBuffer
- QwpTableWriter
- QwpUnrecoverableReplayDictionaryError
- QwpUpgradeError
- QwpWriterRowError
Interfaces
- QwpArrayValue
- QwpBinaryConnection
- QwpBrowserClusterOptions
- QwpBrowserEgressOptions
- QwpBrowserSessionBootstrapOptions
- QwpBrowserSessionBootstrapResult
- QwpBrowserSplitClientOptions
- QwpBrowserUnifiedClientOptions
- QwpBrowserWebSocketOptions
- QwpCacheResetMessage
- QwpClientFactories
- QwpClientMetrics
- QwpClientPoolOptions
- QwpColumnBuffer
- QwpConnectionCloseInfo
- QwpDecimalValue
- QwpEgressQueryOptions
- QwpEgressReplayResetEvent
- QwpEgressRoutingOptions
- QwpEgressSessionOptions
- QwpEgressViewQuery
- QwpEncodedBinds
- QwpExecDoneMessage
- QwpFailoverAttempt
- QwpFrame
- QwpFrameHeader
- QwpGeohashValue
- QwpHandshakeMetadata
- QwpIngressEncodeOptions
- QwpIngressErrorEvent
- QwpIngressMetrics
- QwpIngressProgressEvent
- QwpIngressReplayRecord
- QwpIngressReplayReference
- QwpIngressReplayStore
- QwpIngressResponse
- QwpIngressSendResult
- QwpIngressSessionOptions
- QwpIngressSymbolDictionaryDelta
- QwpIngressTableResult
- QwpIngressTransportMetrics
- QwpLong256Value
- QwpPoolSlotReservation
- QwpQueryErrorMessage
- QwpQueryRequest
- QwpReconnectEvent
- QwpReconnectOptions
- QwpResourcePoolMetrics
- QwpResultArrayValue
- QwpResultBatchMessage
- QwpResultColumn
- QwpResultColumnSchema
- QwpResultEndMessage
- QwpSenderEncodeOptions
- QwpSenderError
- QwpSenderErrorResponseContext
- QwpSenderMetrics
- QwpSenderOptions
- QwpSenderSession
- QwpServerInfoMessage
- QwpSymbolValue
- QwpUpgradeErrorDetails
- QwpUuidValue
- QwpWebSocketConnectOptions
- QwpWebSocketLike
- QwpWriterColumn
Type Aliases
- QwpBindSetter
- QwpBindType
- QwpBrowserClientEgressOptions
- QwpBrowserClientIngressOptions
- QwpBrowserClientOptions
- QwpBrowserFetch
- QwpBrowserSessionAuthentication
- QwpBrowserSessionBootstrapConfig
- QwpColumnType
- QwpConnectionFactory
- QwpDecimalInput
- QwpDoubleArrayInput
- QwpEgressCompression
- QwpEgressMessage
- QwpGeohashInput
- QwpIngressProgressKind
- QwpInitialConnectMode
- QwpInt64
- QwpIpv4Input
- QwpLong256Input
- QwpLong256Words
- QwpLongArrayInput
- QwpNegotiatedEgressCompression
- QwpNestedLongArray
- QwpNestedNumberArray
- QwpQueryCompletion
- QwpReconnectEventKind
- QwpResultBatchViewHandler
- QwpResultRowViewCallback
- QwpResultValue
- QwpSenderErrorCategory
- QwpSenderErrorPolicy
- QwpSenderLogger
- QwpSenderSessionFactory
- QwpTarget
- QwpTimestampUnit
- QwpUpgradeErrorKind
- QwpUpgradeTimeoutPhase
- QwpUuidInput
- QwpWriterColumnKind
- QwpWriterRow
- QwpWriterSchema
Variables
- QWP_COLUMN_TYPE
- QWP_COMPRESSION_CODEC
- QWP_DECIMAL_MAX_SCALE
- QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZE
- QWP_DEFAULT_EGRESS_INITIAL_CREDIT
- QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS
- QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL
- QWP_EGRESS_CAPABILITY
- QWP_EGRESS_MESSAGE
- QWP_EGRESS_PATH
- QWP_ENCODING_GORILLA
- QWP_ENCODING_UNCOMPRESSED
- QWP_FLAG_DEFER_COMMIT
- QWP_FLAG_DELTA_SYMBOL_DICTIONARY
- QWP_FLAG_DURABLE_ACK_POLL
- QWP_FLAG_GORILLA
- QWP_FLAG_ZSTD
- QWP_HEADER_SIZE
- QWP_INGRESS_PATH
- QWP_INGRESS_PROGRESS_KIND
- QWP_INITIAL_CONNECT_MODE
- QWP_MAGIC
- QWP_MAX_ARRAY_DIMENSION_LENGTH
- QWP_MAX_ARRAY_DIMENSIONS
- QWP_MAX_BATCH_ROWS_UPPER_BOUND
- QWP_MAX_CELLS_PER_BATCH
- QWP_MAX_COLUMN_NAME_LENGTH
- QWP_MAX_COLUMNS_PER_TABLE
- QWP_MAX_ERROR_MESSAGE_LENGTH
- QWP_MAX_IDENTIFIER_BYTES
- QWP_MAX_ROWS_PER_TABLE
- QWP_MAX_SYMBOL_DICTIONARY_SIZE
- QWP_MAX_TABLE_NAME_LENGTH
- QWP_MAX_ZSTD_DECOMPRESSED_SIZE
- QWP_QUERY_FLAG_RESET_DICTIONARY
- QWP_RECONNECT_EVENT_KIND
- QWP_RESET_MASK_DICTIONARY
- QWP_SENDER_ERROR_CATEGORY
- QWP_SENDER_ERROR_POLICY
- QWP_SERVER_ROLE
- QWP_STATUS
- QWP_TARGET
- QWP_UPGRADE_ERROR_KIND
- QWP_UPGRADE_TIMEOUT_PHASE
- QWP_VERSION
- QWP_ZSTD_MAX_COMPRESSION_LEVEL
- QWP_ZSTD_MIN_COMPRESSION_LEVEL
Functions
- addQwpDurableAckWebSocketProtocol
- binary
- bool
- bootstrapQwpBrowserSession
- byte
- char
- concatBytes
- connectQwpBrowserClient
- connectQwpBrowserEgress
- connectQwpBrowserIngress
- connectQwpBrowserSender
- connectQwpBrowserWebSocket
- createQwpBrowserClient
- createQwpBrowserConnectionFactory
- createQwpBrowserSender
- createQwpDataLossSenderError
- createQwpProtocolViolationSenderError
- createQwpSenderError
- date
- decimal128
- decimal256
- decimal64
- decodeQwpContentEncoding
- decodeQwpEgressMessage
- decodeQwpFrame
- decodeQwpIngressResponse
- decodeQwpIngressServerInfo
- decodeQwpIngressSymbolDictionaryDelta
- decodeQwpVarint
- decodeUtf8
- decompressQwpZstdFrame
- defaultQwpSenderErrorHandler
- designatedTimestamp
- double
- doubleArray
- encodeQwpAcceptEncoding
- encodeQwpBinds
- encodeQwpCancel
- encodeQwpCredit
- encodeQwpDurableAckPollFrame
- encodeQwpFrame
- encodeQwpGorilla
- encodeQwpIngressCommitFrame
- encodeQwpIngressFrame
- encodeQwpIngressSymbolDictionaryFrame
- encodeQwpQueryRequest
- encodeQwpVarint
- encodeUtf8
- flattenQwpArray
- float32
- float64
- geohash
- int32
- int64
- ipv4
- isQwpDurableAckWebSocketProtocol
- long
- long256
- longArray
- qwpDefaultSenderErrorPolicy
- qwpGorillaSize
- qwpSenderErrorCategory
- qwpVarintSize
- readQwpVarint
- readQwpVarintNumber
- short
- symbol
- timestamp
- utf8Length
- uuid
- varchar
- writeQwpFrameHeader
- writeQwpVarint
diff --git a/docs/modules/_questdb_nodejs-client.html b/docs/modules/_questdb_nodejs-client.html
new file mode 100644
index 0000000..12e9b90
--- /dev/null
+++ b/docs/modules/_questdb_nodejs-client.html
@@ -0,0 +1,136 @@
+@questdb/nodejs-client | QuestDB JavaScript Client - v4.2.0 Module @questdb/nodejs-client
The QuestDB JavaScript client.
+This entry point targets Node.js. Use @questdb/browser-client for the browser build.
+QuestDB JavaScript Client for Node.js
The official QuestDB client for Node.js and TypeScript. Use it to ingest rows
+with the InfluxDB Line Protocol (ILP), ingest and query with the QuestDB Wire
+Protocol (QWP), and keep publishing through outages with Node-only persistent
+store-and-forward.
+The complete Node.js API is exported from @questdb/nodejs-client. There are no
+additional public import paths.
+Features
+- ILP ingestion over HTTP, HTTPS, TCP, and TLS-encrypted TCP
+- QWP ingestion over WebSocket, secure WebSocket, and UDP
+- Streaming QWP queries with typed bind variables and result batches
+- Automatic batching, failover, reconnect, and acknowledgement tracking
+- Persistent QWP store-and-forward for process and server outages
+- ESM, CommonJS, and bundled TypeScript declarations
+
+Requirements
+- Node.js 20 or newer
+- A running QuestDB instance
+- QWP endpoints
/write/v4 and /read/v1 for QWP ingestion and queries
+
+Installation
npm install @questdb/nodejs-client
+
+
+yarn add @questdb/nodejs-client
+
+
+pnpm add @questdb/nodejs-client
+
+
+Quick start: ILP over HTTP
Sender buffers rows locally. Add as many complete rows as needed, then call
+flush() to send the batch.
+import { Sender } from "@questdb/nodejs-client";
const sender = await Sender.fromConfig("http::addr=localhost:9000");
try {
await sender
.table("trades")
.symbol("symbol", "ETH-USD")
.symbol("side", "buy")
.floatColumn("price", 2_615.54)
.floatColumn("amount", 0.25)
.at(Date.now(), "ms");
await sender.flush();
} finally {
await sender.close();
}
+
+
+HTTP and HTTPS connect for each request. TCP, TCPS, WS, WSS, and UDP transports
+have an explicit connection, so call await sender.connect() before writing.
+Choosing a transport
+
+
+Configuration prefix
+Protocol
+Typical use
+
+
+
+
+http::, https::
+ILP
+Recommended general-purpose ingestion
+
+
+tcp::, tcps::
+ILP
+Long-lived ILP connection
+
+
+ws::, wss::
+QWP
+Acknowledged ingestion, failover, and store-and-forward
+
+
+udp::
+QWP
+Fire-and-forget datagrams on trusted networks
+
+
+
+Use encrypted transports and certificate verification outside trusted local
+development environments.
+Batch multiple rows
Avoid flushing after every row when the application can send a larger batch.
+The sender also supports automatic flushing through its configuration options.
+import { Sender } from "@questdb/nodejs-client";
const sender = await Sender.fromConfig("http::addr=localhost:9000");
try {
for (const trade of [
{ symbol: "ETH-USD", price: 2_615.54, amount: 0.25 },
{ symbol: "BTC-USD", price: 59_750.1, amount: 0.01 },
]) {
await sender
.table("trades")
.symbol("symbol", trade.symbol)
.floatColumn("price", trade.price)
.floatColumn("amount", trade.amount)
.atNow();
}
await sender.flush();
} finally {
await sender.close();
}
+
+
+Passing null or undefined to a supported symbol or column method omits that
+column from the row, which records a SQL NULL in QuestDB.
+Authentication and TLS
Configuration strings use the form
+protocol::key=value;key=value. HTTP Basic authentication uses username and
+password; REST and OIDC access tokens use token.
+import { Sender } from "@questdb/nodejs-client";
const sender = await Sender.fromConfig(
`https::addr=questdb.example:9000;token=${process.env.QUESTDB_TOKEN};tls_verify=on`,
);
try {
await sender.table("service_health").booleanColumn("healthy", true).atNow();
await sender.flush();
} finally {
await sender.close();
}
+
+
+The same configuration can be provided through QDB_CLIENT_CONF:
+import { Sender } from "@questdb/nodejs-client";
// QDB_CLIENT_CONF=http::addr=localhost:9000
const sender = await Sender.fromEnv();
+
+
+QWP ingestion
Changing the configuration prefix to ws:: or wss:: selects QWP while
+keeping the familiar Sender row API.
+import { Sender } from "@questdb/nodejs-client";
const sender = await Sender.fromConfig(
`wss::addr=questdb.example:9000;token=${process.env.QUESTDB_TOKEN};auto_flush=off`,
);
await sender.connect();
try {
await sender
.table("trades")
.symbol("symbol", "ETH-USD")
.floatColumn("price", 2_615.54)
.timestampColumn("received_at", Date.now(), "ms")
.atNow();
await sender.flush();
} finally {
await sender.close();
}
+
+
+QWP senders support server acknowledgements, durable acknowledgements,
+transactions, reconnect, failover, compiled row writers, and metrics. See the
+QWP guide
+for the delivery semantics of each option.
+Type-safe object rows
For repeated object-shaped rows, compile a table schema once. TypeScript then
+checks each row against that schema.
+import {
Sender,
designatedTimestamp,
double,
long,
symbol,
} from "@questdb/nodejs-client";
const sender = await Sender.fromConfig("ws::addr=localhost:9000");
await sender.connect();
try {
const trades = sender.writer("trades", {
symbol: symbol(),
side: symbol(),
price: double(),
quantity: long(),
timestamp: designatedTimestamp("ns"),
});
await trades.rows([
{
symbol: "ETH-USD",
side: "buy",
price: 2_615.54,
quantity: 42n,
timestamp: 1_723_000_000_000_000_000n,
},
{
symbol: "BTC-USD",
side: "sell",
price: 59_750.1,
quantity: 1n,
timestamp: 1_723_000_001_000_000_000n,
},
]);
await sender.flush();
} finally {
await sender.close();
}
+
+
+Compiled writers are available with QWP transports only.
+QWP queries
QWP egress streams typed result batches. One egress session executes one active
+query at a time.
+import { connectQwpNodeEgress } from "@questdb/nodejs-client";
const session = await connectQwpNodeEgress(
{
url: "wss://questdb.example:9000/read/v1",
authorization: `Bearer ${process.env.QUESTDB_TOKEN}`,
compression: "zstd",
},
{ queryTimeoutMs: 30_000 },
);
try {
const query = await session.query(
"select timestamp, symbol, price from trades where symbol = $1",
{
// Bind index 0 corresponds to SQL placeholder $1.
binds: (binds) => binds.setVarchar(0, "ETH-USD"),
initialCredit: 1024 * 1024,
},
);
for await (const batch of query) {
for (const row of batch.rows()) {
console.log(row);
}
}
await query.completion;
} finally {
await session.close();
}
+
+
+Use queryViews() instead of query() for reusable, allocation-conscious
+column and row views.
+Persistent store-and-forward
Node.js can journal QWP frames to disk before sending them. The producer can
+continue accepting rows during a QuestDB outage and replay them in order after
+reconnection.
+import { Sender } from "@questdb/nodejs-client";
const sender = await Sender.fromConfig(
"wss::" +
"addr=questdb-a.example:9000,questdb-b.example:9000;" +
"sf_dir=/var/lib/my-service/questdb-replay;" +
"initial_connect_retry=async;",
);
await sender.connect();
+
+
+Give every active producer its own journal directory. Durability,
+backpressure, capacity, orphan recovery, and shutdown behavior are covered in
+the store-and-forward section of the QWP guide.
+Error handling and shutdown
+- Always call
close() in a finally block.
+- Call
flush() before closing an ILP sender; otherwise buffered rows are lost.
+- A QWP sender publishes completed rows during close, but an unfinished row is
+never completed implicitly.
+- Do not write concurrently through one
Sender. Give each worker or producer
+its own sender.
+- Treat authentication and protocol errors as configuration failures rather
+than retrying the same request indefinitely.
+
+More documentation
+Classes
- HttpTransport
- QwpBatchTooLargeError
- QwpBindValues
- QwpByteReader
- QwpByteWriter
- QwpClient
- QwpClientClosedError
- QwpEgressQuery
- QwpEgressQueryAbandonedError
- QwpEgressQueryCancelTimeoutError
- QwpEgressQueryError
- QwpEgressQueryTimeoutError
- QwpEgressReplayRequiredError
- QwpEgressSession
- QwpEgressSessionClosedError
- QwpFailoverError
- QwpIngressAckTimeoutError
- QwpIngressNackError
- QwpIngressSession
- QwpIngressSessionClosedError
- QwpMemoryReplayAppendTimeoutError
- QwpMemoryReplayFrameTooLargeError
- QwpNodeFileReplayStore
- QwpNodeOrphanDrainer
- QwpNodeUdpSession
- QwpPoolAcquireTimeoutError
- QwpPoolResourceError
- QwpProtocolError
- QwpQueryLease
- QwpReconnectExhaustedError
- QwpReplayDictionaryError
- QwpReplayDictionaryPersistenceError
- QwpReplayRejectedError
- QwpReplayStoreAppendTimeoutError
- QwpReplayStoreCheckpointError
- QwpReplayStoreCorruptionError
- QwpReplayStoreError
- QwpReplayStoreFullError
- QwpReplayStoreLockedError
- QwpReplayStoreLockLostError
- QwpReplayStoreQuarantinedError
- QwpReplayStoreSegmentTooLargeError
- QwpResultBatch
- QwpResultBatchDecoder
- QwpResultBatchView
- QwpResultColumnView
- QwpResultRowView
- QwpRoleMismatchError
- QwpSendClosedError
- QwpSender
- QwpSenderCloseTimeoutError
- QwpSendError
- QwpSendTimeoutError
- QwpSymbolDictionary
- QwpTableBuffer
- QwpTableWriter
- QwpUdpDatagramTooLargeError
- QwpUnrecoverableReplayDictionaryError
- QwpUpgradeError
- QwpVersionMismatchError
- QwpWriterRowError
- Sender
- SenderBufferV1
- SenderBufferV2
- SenderBufferV3
- SenderOptions
- TcpTransport
- UndiciTransport
Interfaces
- QwpArrayValue
- QwpBinaryConnection
- QwpCacheResetMessage
- QwpClientFactories
- QwpClientMetrics
- QwpClientPoolOptions
- QwpColumnBuffer
- QwpConnectionCloseInfo
- QwpDecimalValue
- QwpEgressQueryOptions
- QwpEgressReplayResetEvent
- QwpEgressRoutingOptions
- QwpEgressSessionOptions
- QwpEgressViewQuery
- QwpEncodedBinds
- QwpExecDoneMessage
- QwpFailoverAttempt
- QwpFrame
- QwpFrameHeader
- QwpGeohashValue
- QwpHandshakeMetadata
- QwpIngressEncodeOptions
- QwpIngressErrorEvent
- QwpIngressMetrics
- QwpIngressProgressEvent
- QwpIngressReplayRecord
- QwpIngressReplayReference
- QwpIngressReplayStore
- QwpIngressResponse
- QwpIngressSendResult
- QwpIngressSessionOptions
- QwpIngressSymbolDictionaryDelta
- QwpIngressTableResult
- QwpIngressTransportMetrics
- QwpLong256Value
- QwpNodeClientConfigOptions
- QwpNodeClientOptions
- QwpNodeEgressOptions
- QwpNodeFileReplayStoreMetrics
- QwpNodeFileReplayStoreOptions
- QwpNodeIngressOptions
- QwpNodeOrphanDrainerMetrics
- QwpNodeOrphanDrainerOptions
- QwpNodeOrphanDrainEvent
- QwpNodeOrphanDrainSession
- QwpNodeReplayDataLossReport
- QwpNodeReplayRecoveryEvent
- QwpNodeStoreAndForwardOptions
- QwpNodeUdpMetrics
- QwpNodeUdpOptions
- QwpNodeUdpSocketLike
- QwpNodeUpgradeRejection
- QwpNodeWebSocketOptions
- QwpPoolSlotReservation
- QwpQueryErrorMessage
- QwpQueryRequest
- QwpReconnectEvent
- QwpReconnectOptions
- QwpResourcePoolMetrics
- QwpResultArrayValue
- QwpResultBatchMessage
- QwpResultColumn
- QwpResultColumnSchema
- QwpResultEndMessage
- QwpSenderEncodeOptions
- QwpSenderError
- QwpSenderErrorResponseContext
- QwpSenderMetrics
- QwpSenderOptions
- QwpSenderSession
- QwpServerInfoMessage
- QwpSymbolValue
- QwpUpgradeErrorDetails
- QwpUuidValue
- QwpWebSocketConnectOptions
- QwpWebSocketLike
- QwpWriterColumn
- SenderBuffer
- SenderTransport
Type Aliases
- ExtraOptions
- Logger
- QwpBindSetter
- QwpBindType
- QwpColumnType
- QwpConnectionFactory
- QwpDecimalInput
- QwpDoubleArrayInput
- QwpEgressCompression
- QwpEgressMessage
- QwpExtraOptions
- QwpGeohashInput
- QwpIngressProgressKind
- QwpInitialConnectMode
- QwpInt64
- QwpIpv4Input
- QwpLong256Input
- QwpLong256Words
- QwpLongArrayInput
- QwpNegotiatedEgressCompression
- QwpNestedLongArray
- QwpNestedNumberArray
- QwpNodeOrphanDrainEventKind
- QwpQueryCompletion
- QwpReconnectEventKind
- QwpResultBatchViewHandler
- QwpResultRowViewCallback
- QwpResultValue
- QwpSenderErrorCategory
- QwpSenderErrorPolicy
- QwpSenderLogger
- QwpSenderSessionFactory
- QwpSfBackpressurePolicy
- QwpSfDurability
- QwpTarget
- QwpTimestampUnit
- QwpUpgradeErrorKind
- QwpUpgradeTimeoutPhase
- QwpUuidInput
- QwpWriterColumnKind
- QwpWriterRow
- QwpWriterSchema
- TimestampUnit
Variables
- QWP_COLUMN_TYPE
- QWP_COMPRESSION_CODEC
- QWP_DECIMAL_MAX_SCALE
- QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZE
- QWP_DEFAULT_EGRESS_INITIAL_CREDIT
- QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS
- QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL
- QWP_EGRESS_CAPABILITY
- QWP_EGRESS_MESSAGE
- QWP_EGRESS_PATH
- QWP_ENCODING_GORILLA
- QWP_ENCODING_UNCOMPRESSED
- QWP_FLAG_DEFER_COMMIT
- QWP_FLAG_DELTA_SYMBOL_DICTIONARY
- QWP_FLAG_DURABLE_ACK_POLL
- QWP_FLAG_GORILLA
- QWP_FLAG_ZSTD
- QWP_HEADER_SIZE
- QWP_INGRESS_PATH
- QWP_INGRESS_PROGRESS_KIND
- QWP_INITIAL_CONNECT_MODE
- QWP_MAGIC
- QWP_MAX_ARRAY_DIMENSION_LENGTH
- QWP_MAX_ARRAY_DIMENSIONS
- QWP_MAX_BATCH_ROWS_UPPER_BOUND
- QWP_MAX_CELLS_PER_BATCH
- QWP_MAX_COLUMN_NAME_LENGTH
- QWP_MAX_COLUMNS_PER_TABLE
- QWP_MAX_ERROR_MESSAGE_LENGTH
- QWP_MAX_IDENTIFIER_BYTES
- QWP_MAX_ROWS_PER_TABLE
- QWP_MAX_SYMBOL_DICTIONARY_SIZE
- QWP_MAX_TABLE_NAME_LENGTH
- QWP_MAX_ZSTD_DECOMPRESSED_SIZE
- QWP_ORPHAN_DRAIN_EVENT_KIND
- QWP_ORPHAN_FAILED_SENTINEL
- QWP_QUERY_FLAG_RESET_DICTIONARY
- QWP_RECONNECT_EVENT_KIND
- QWP_RESET_MASK_DICTIONARY
- QWP_SENDER_ERROR_CATEGORY
- QWP_SENDER_ERROR_POLICY
- QWP_SERVER_ROLE
- QWP_SF_BACKPRESSURE_POLICY
- QWP_SF_DURABILITY
- QWP_STATUS
- QWP_TARGET
- QWP_UPGRADE_ERROR_KIND
- QWP_UPGRADE_TIMEOUT_PHASE
- QWP_VERSION
- QWP_ZSTD_MAX_COMPRESSION_LEVEL
- QWP_ZSTD_MIN_COMPRESSION_LEVEL
Functions
- addQwpDurableAckWebSocketProtocol
- bigintToTwosComplementBytes
- binary
- bool
- byte
- char
- concatBytes
- connectQwpNodeClient
- connectQwpNodeEgress
- connectQwpNodeIngress
- connectQwpNodeSender
- connectQwpNodeUdp
- connectQwpNodeUdpSender
- connectQwpNodeWebSocket
- createBuffer
- createQwpDataLossSenderError
- createQwpNodeClient
- createQwpNodeConnectionFactory
- createQwpNodeSender
- createQwpNodeUdpSender
- createQwpProtocolViolationSenderError
- createQwpSenderError
- createTransport
- date
- decimal128
- decimal256
- decimal64
- decodeQwpContentEncoding
- decodeQwpEgressMessage
- decodeQwpFrame
- decodeQwpIngressResponse
- decodeQwpIngressServerInfo
- decodeQwpIngressSymbolDictionaryDelta
- decodeQwpVarint
- decodeUtf8
- decompressQwpZstdFrame
- defaultQwpSenderErrorHandler
- designatedTimestamp
- double
- doubleArray
- encodeQwpAcceptEncoding
- encodeQwpBinds
- encodeQwpCancel
- encodeQwpCredit
- encodeQwpDurableAckPollFrame
- encodeQwpFrame
- encodeQwpGorilla
- encodeQwpIngressCommitFrame
- encodeQwpIngressFrame
- encodeQwpIngressSymbolDictionaryFrame
- encodeQwpQueryRequest
- encodeQwpVarint
- encodeUtf8
- flattenQwpArray
- float32
- float64
- geohash
- int32
- int64
- ipv4
- isQwpDurableAckWebSocketProtocol
- long
- long256
- longArray
- parseQwpNodeClientConfig
- qwpDefaultSenderErrorPolicy
- qwpGorillaSize
- qwpSenderErrorCategory
- qwpVarintSize
- readQwpVarint
- readQwpVarintNumber
- retryQwpNodeOrphanSlot
- scanQwpNodeOrphanSlots
- short
- symbol
- timestamp
- utf8Length
- uuid
- varchar
- writeQwpFrameHeader
- writeQwpVarint
diff --git a/docs/types/ExtraOptions.html b/docs/types/ExtraOptions.html
deleted file mode 100644
index ce1cb2b..0000000
--- a/docs/types/ExtraOptions.html
+++ /dev/null
@@ -1,3 +0,0 @@
-ExtraOptions | QuestDB Node.js Client - v4.2.0
diff --git a/docs/types/Logger.html b/docs/types/Logger.html
deleted file mode 100644
index fd702cb..0000000
--- a/docs/types/Logger.html
+++ /dev/null
@@ -1,4 +0,0 @@
-Logger | QuestDB Node.js Client - v4.2.0 Type Alias Logger
Logger: (
level: "error" | "warn" | "info" | "debug",
message: string | Error,
) => voidLogger function type definition.
-Type declaration
- (level: "error" | "warn" | "info" | "debug", message: string | Error): void
Parameters
- level: "error" | "warn" | "info" | "debug"
The log level for the message
- - message: string | Error
The message to log, either a string or Error object
-
Returns void
diff --git a/docs/types/TimestampUnit.html b/docs/types/TimestampUnit.html
deleted file mode 100644
index c79c21d..0000000
--- a/docs/types/TimestampUnit.html
+++ /dev/null
@@ -1,2 +0,0 @@
-TimestampUnit | QuestDB Node.js Client - v4.2.0 Type Alias TimestampUnit
TimestampUnit: "ns" | "us" | "ms"Supported timestamp units for QuestDB operations.
-
diff --git a/docs/types/_questdb_browser-client.QwpBindSetter.html b/docs/types/_questdb_browser-client.QwpBindSetter.html
new file mode 100644
index 0000000..39845ed
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpBindSetter.html
@@ -0,0 +1 @@
+QwpBindSetter | QuestDB JavaScript Client - v4.2.0 Type Alias QwpBindSetter
Type declaration
- (binds: QwpBindValues): void
Parameters
- binds: QwpBindValues
Returns void
diff --git a/docs/types/_questdb_browser-client.QwpBindType.html b/docs/types/_questdb_browser-client.QwpBindType.html
new file mode 100644
index 0000000..83389fe
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpBindType.html
@@ -0,0 +1,2 @@
+QwpBindType | QuestDB JavaScript Client - v4.2.0 Type Alias QwpBindType
QwpBindType:
| typeof BOOLEAN
| typeof BYTE
| typeof SHORT
| typeof INT
| typeof LONG
| typeof FLOAT
| typeof DOUBLE
| typeof TIMESTAMP
| typeof DATE
| typeof UUID
| typeof LONG256
| typeof GEOHASH
| typeof VARCHAR
| typeof TIMESTAMP_NANOS
| typeof DECIMAL64
| typeof DECIMAL128
| typeof DECIMAL256
| typeof CHARPhase-1 scalar bind types exposed by the Java reference client.
+
diff --git a/docs/types/_questdb_browser-client.QwpBrowserClientEgressOptions.html b/docs/types/_questdb_browser-client.QwpBrowserClientEgressOptions.html
new file mode 100644
index 0000000..701f53f
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpBrowserClientEgressOptions.html
@@ -0,0 +1,2 @@
+QwpBrowserClientEgressOptions | QuestDB JavaScript Client - v4.2.0 Type Alias QwpBrowserClientEgressOptions
QwpBrowserClientEgressOptions: Partial<
Pick<
QwpBrowserEgressOptions,
| "protocols"
| "connectTimeoutMs"
| "sendTimeoutMs"
| "closeTimeoutMs"
| "webSocketFactory"
| "target"
| "zone"
| "compression"
| "compressionLevel"
| "maxBatchRows",
>,
>Egress-only overrides for a unified browser cluster.
+
diff --git a/docs/types/_questdb_browser-client.QwpBrowserClientIngressOptions.html b/docs/types/_questdb_browser-client.QwpBrowserClientIngressOptions.html
new file mode 100644
index 0000000..990650b
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpBrowserClientIngressOptions.html
@@ -0,0 +1,2 @@
+QwpBrowserClientIngressOptions | QuestDB JavaScript Client - v4.2.0 Type Alias QwpBrowserClientIngressOptions
QwpBrowserClientIngressOptions: Partial<
Pick<
QwpBrowserWebSocketOptions,
| "protocols"
| "connectTimeoutMs"
| "sendTimeoutMs"
| "closeTimeoutMs"
| "requestDurableAck"
| "ingressNegotiationTimeoutMs"
| "webSocketFactory",
>,
>Ingress-only overrides for a unified browser cluster.
+
diff --git a/docs/types/_questdb_browser-client.QwpBrowserClientOptions.html b/docs/types/_questdb_browser-client.QwpBrowserClientOptions.html
new file mode 100644
index 0000000..9afafd7
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpBrowserClientOptions.html
@@ -0,0 +1,2 @@
+QwpBrowserClientOptions | QuestDB JavaScript Client - v4.2.0 Type Alias QwpBrowserClientOptions
Browser configuration for a combined pooled QWP ingress/egress client.
+
diff --git a/docs/types/_questdb_browser-client.QwpBrowserFetch.html b/docs/types/_questdb_browser-client.QwpBrowserFetch.html
new file mode 100644
index 0000000..2a7107b
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpBrowserFetch.html
@@ -0,0 +1 @@
+QwpBrowserFetch | QuestDB JavaScript Client - v4.2.0 Type Alias QwpBrowserFetch
QwpBrowserFetch: (input: string | URL, init?: RequestInit) => Promise<Response>Type declaration
- (input: string | URL, init?: RequestInit): Promise<Response>
Parameters
- input: string | URL
Optionalinit: RequestInit
Returns Promise<Response>
diff --git a/docs/types/_questdb_browser-client.QwpBrowserSessionAuthentication.html b/docs/types/_questdb_browser-client.QwpBrowserSessionAuthentication.html
new file mode 100644
index 0000000..422fc85
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpBrowserSessionAuthentication.html
@@ -0,0 +1,3 @@
+QwpBrowserSessionAuthentication | QuestDB JavaScript Client - v4.2.0 Type Alias QwpBrowserSessionAuthentication
QwpBrowserSessionAuthentication:
| { password: string; type: "basic"; username: string }
| { token: string; type: "bearer" }Type declaration
- { password: string; type: "basic"; username: string }
password: string
type: "basic"
HTTP Basic authentication.
+username: string
- { token: string; type: "bearer" }
token: string
type: "bearer"
QuestDB REST token or OIDC access token.
+
diff --git a/docs/types/_questdb_browser-client.QwpBrowserSessionBootstrapConfig.html b/docs/types/_questdb_browser-client.QwpBrowserSessionBootstrapConfig.html
new file mode 100644
index 0000000..1f5011e
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpBrowserSessionBootstrapConfig.html
@@ -0,0 +1,2 @@
+QwpBrowserSessionBootstrapConfig | QuestDB JavaScript Client - v4.2.0 Type Alias QwpBrowserSessionBootstrapConfig
QwpBrowserSessionBootstrapConfig: Omit<QwpBrowserSessionBootstrapOptions, "url"> & {
url?: string | URL;
}Type declaration
Optionalurl?: string | URL
Defaults to /exec on the current QWP endpoint's HTTP origin.
+
diff --git a/docs/types/_questdb_browser-client.QwpColumnType.html b/docs/types/_questdb_browser-client.QwpColumnType.html
new file mode 100644
index 0000000..b492e97
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpColumnType.html
@@ -0,0 +1 @@
+QwpColumnType | QuestDB JavaScript Client - v4.2.0 Type Alias QwpColumnType
diff --git a/docs/types/_questdb_browser-client.QwpConnectionFactory.html b/docs/types/_questdb_browser-client.QwpConnectionFactory.html
new file mode 100644
index 0000000..372f995
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpConnectionFactory.html
@@ -0,0 +1,5 @@
+QwpConnectionFactory | QuestDB JavaScript Client - v4.2.0 Type Alias QwpConnectionFactory
Opens one connection. The optional signal is aborted when the owning session
+closes, so a factory that is still negotiating can tear its socket down
+instead of leaving it alive until its own deadline expires. Factories that
+ignore the parameter remain assignable.
+Type declaration
- (signal?: AbortSignal): Promise<QwpBinaryConnection>
Parameters
Optionalsignal: AbortSignal
Returns Promise<QwpBinaryConnection>
diff --git a/docs/types/_questdb_browser-client.QwpDecimalInput.html b/docs/types/_questdb_browser-client.QwpDecimalInput.html
new file mode 100644
index 0000000..ca4a6b2
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpDecimalInput.html
@@ -0,0 +1,3 @@
+QwpDecimalInput | QuestDB JavaScript Client - v4.2.0 Type Alias QwpDecimalInput
QwpDecimalInput: bigint | number | string | { scale: number; unscaled: bigint }DECIMAL input: the unscaled bigint at the column's scale, decimal text (or
+a number) that is exactly representable at that scale, or the egress record.
+
diff --git a/docs/types/_questdb_browser-client.QwpDoubleArrayInput.html b/docs/types/_questdb_browser-client.QwpDoubleArrayInput.html
new file mode 100644
index 0000000..79ebd7e
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpDoubleArrayInput.html
@@ -0,0 +1,2 @@
+QwpDoubleArrayInput | QuestDB JavaScript Client - v4.2.0 Type Alias QwpDoubleArrayInput
QwpDoubleArrayInput:
| QwpNestedNumberArray
| { dimensions: readonly number[]; values: readonly number[] }DOUBLE array input: nested arrays or a flat shape-and-values record.
+
diff --git a/docs/types/_questdb_browser-client.QwpEgressCompression.html b/docs/types/_questdb_browser-client.QwpEgressCompression.html
new file mode 100644
index 0000000..ac32e2b
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpEgressCompression.html
@@ -0,0 +1 @@
+QwpEgressCompression | QuestDB JavaScript Client - v4.2.0 Type Alias QwpEgressCompression
QwpEgressCompression: "raw" | "zstd" | "auto"
diff --git a/docs/types/_questdb_browser-client.QwpEgressMessage.html b/docs/types/_questdb_browser-client.QwpEgressMessage.html
new file mode 100644
index 0000000..0ef16b4
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpEgressMessage.html
@@ -0,0 +1 @@
+QwpEgressMessage | QuestDB JavaScript Client - v4.2.0 Type Alias QwpEgressMessage
QwpEgressMessage:
| QwpServerInfoMessage
| QwpResultBatchMessage
| QwpResultEndMessage
| QwpQueryErrorMessage
| QwpExecDoneMessage
| QwpCacheResetMessage
diff --git a/docs/types/_questdb_browser-client.QwpGeohashInput.html b/docs/types/_questdb_browser-client.QwpGeohashInput.html
new file mode 100644
index 0000000..4b66240
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpGeohashInput.html
@@ -0,0 +1,3 @@
+QwpGeohashInput | QuestDB JavaScript Client - v4.2.0 Type Alias QwpGeohashInput
QwpGeohashInput:
| bigint
| number
| string
| { bits: bigint; precisionBits: number }GEOHASH input: the raw bits, base-32 geohash text whose length matches the
+column precision, or the egress bit record.
+
diff --git a/docs/types/_questdb_browser-client.QwpIngressProgressKind.html b/docs/types/_questdb_browser-client.QwpIngressProgressKind.html
new file mode 100644
index 0000000..82a6bed
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpIngressProgressKind.html
@@ -0,0 +1 @@
+QwpIngressProgressKind | QuestDB JavaScript Client - v4.2.0 Type Alias QwpIngressProgressKind
diff --git a/docs/types/_questdb_browser-client.QwpInitialConnectMode.html b/docs/types/_questdb_browser-client.QwpInitialConnectMode.html
new file mode 100644
index 0000000..b73f852
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpInitialConnectMode.html
@@ -0,0 +1 @@
+QwpInitialConnectMode | QuestDB JavaScript Client - v4.2.0 Type Alias QwpInitialConnectMode
diff --git a/docs/types/_questdb_browser-client.QwpInt64.html b/docs/types/_questdb_browser-client.QwpInt64.html
new file mode 100644
index 0000000..a708a62
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpInt64.html
@@ -0,0 +1 @@
+QwpInt64 | QuestDB JavaScript Client - v4.2.0 Type Alias QwpInt64
QwpInt64: number | bigint
diff --git a/docs/types/_questdb_browser-client.QwpIpv4Input.html b/docs/types/_questdb_browser-client.QwpIpv4Input.html
new file mode 100644
index 0000000..c34bc28
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpIpv4Input.html
@@ -0,0 +1,2 @@
+QwpIpv4Input | QuestDB JavaScript Client - v4.2.0 Type Alias QwpIpv4Input
QwpIpv4Input: string | numberIPV4 input: dotted-quad text or a signed/unsigned packed 32-bit address.
+
diff --git a/docs/types/_questdb_browser-client.QwpLong256Input.html b/docs/types/_questdb_browser-client.QwpLong256Input.html
new file mode 100644
index 0000000..2ee9527
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpLong256Input.html
@@ -0,0 +1,3 @@
+QwpLong256Input | QuestDB JavaScript Client - v4.2.0 Type Alias QwpLong256Input
LONG256 input: an unsigned 256-bit bigint, a 0x-prefixed hex string of
+up to 64 digits, four little-endian words, or the egress word record.
+
diff --git a/docs/types/_questdb_browser-client.QwpLong256Words.html b/docs/types/_questdb_browser-client.QwpLong256Words.html
new file mode 100644
index 0000000..e9f4dc7
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpLong256Words.html
@@ -0,0 +1,2 @@
+QwpLong256Words | QuestDB JavaScript Client - v4.2.0 Type Alias QwpLong256Words
QwpLong256Words: readonly [bigint, bigint, bigint, bigint]LONG256 little-endian words; word 0 is least significant.
+
diff --git a/docs/types/_questdb_browser-client.QwpLongArrayInput.html b/docs/types/_questdb_browser-client.QwpLongArrayInput.html
new file mode 100644
index 0000000..6ebcfbd
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpLongArrayInput.html
@@ -0,0 +1,2 @@
+QwpLongArrayInput | QuestDB JavaScript Client - v4.2.0 Type Alias QwpLongArrayInput
QwpLongArrayInput:
| QwpNestedLongArray
| { dimensions: readonly number[]; values: readonly (number | bigint)[] }LONG array input: nested arrays or a flat shape-and-values record.
+
diff --git a/docs/types/_questdb_browser-client.QwpNegotiatedEgressCompression.html b/docs/types/_questdb_browser-client.QwpNegotiatedEgressCompression.html
new file mode 100644
index 0000000..7f6523f
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpNegotiatedEgressCompression.html
@@ -0,0 +1 @@
+QwpNegotiatedEgressCompression | QuestDB JavaScript Client - v4.2.0 Type Alias QwpNegotiatedEgressCompression
QwpNegotiatedEgressCompression:
| { codec: "raw"; level: 0 }
| { codec: "zstd"; level: number }
| { codec: "unknown"; contentEncoding: string; level: 0 }
diff --git a/docs/types/_questdb_browser-client.QwpNestedLongArray.html b/docs/types/_questdb_browser-client.QwpNestedLongArray.html
new file mode 100644
index 0000000..347181f
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpNestedLongArray.html
@@ -0,0 +1,2 @@
+QwpNestedLongArray | QuestDB JavaScript Client - v4.2.0 Type Alias QwpNestedLongArray
Nested LONG array of uniform shape.
+
diff --git a/docs/types/_questdb_browser-client.QwpNestedNumberArray.html b/docs/types/_questdb_browser-client.QwpNestedNumberArray.html
new file mode 100644
index 0000000..9d9067e
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpNestedNumberArray.html
@@ -0,0 +1,2 @@
+QwpNestedNumberArray | QuestDB JavaScript Client - v4.2.0 Type Alias QwpNestedNumberArray
Nested DOUBLE array of uniform shape.
+
diff --git a/docs/types/_questdb_browser-client.QwpQueryCompletion.html b/docs/types/_questdb_browser-client.QwpQueryCompletion.html
new file mode 100644
index 0000000..6c3fb0e
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpQueryCompletion.html
@@ -0,0 +1 @@
+QwpQueryCompletion | QuestDB JavaScript Client - v4.2.0 Type Alias QwpQueryCompletion
diff --git a/docs/types/_questdb_browser-client.QwpReconnectEventKind.html b/docs/types/_questdb_browser-client.QwpReconnectEventKind.html
new file mode 100644
index 0000000..1215ba9
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpReconnectEventKind.html
@@ -0,0 +1 @@
+QwpReconnectEventKind | QuestDB JavaScript Client - v4.2.0 Type Alias QwpReconnectEventKind
diff --git a/docs/types/_questdb_browser-client.QwpResultBatchViewHandler.html b/docs/types/_questdb_browser-client.QwpResultBatchViewHandler.html
new file mode 100644
index 0000000..289e431
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpResultBatchViewHandler.html
@@ -0,0 +1,3 @@
+QwpResultBatchViewHandler | QuestDB JavaScript Client - v4.2.0 Type Alias QwpResultBatchViewHandler
QwpResultBatchViewHandler: (
batch: QwpResultBatchView,
query: QwpEgressViewQuery,
) => void | Promise<void>Runs while one reusable batch view is valid. Do not retain the batch,
+columns, or raw byte slices after the callback settles.
+Type declaration
- (batch: QwpResultBatchView, query: QwpEgressViewQuery): void | Promise<void>
Parameters
- batch: QwpResultBatchView
- query: QwpEgressViewQuery
Returns void | Promise<void>
diff --git a/docs/types/_questdb_browser-client.QwpResultRowViewCallback.html b/docs/types/_questdb_browser-client.QwpResultRowViewCallback.html
new file mode 100644
index 0000000..1c20f3b
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpResultRowViewCallback.html
@@ -0,0 +1,2 @@
+QwpResultRowViewCallback | QuestDB JavaScript Client - v4.2.0 Type Alias QwpResultRowViewCallback
Callback invoked by QwpResultBatchView.forEachRow().
+Type declaration
- (row: QwpResultRowView): void
Parameters
- row: QwpResultRowView
Returns void
diff --git a/docs/types/_questdb_browser-client.QwpResultValue.html b/docs/types/_questdb_browser-client.QwpResultValue.html
new file mode 100644
index 0000000..9032cf4
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpResultValue.html
@@ -0,0 +1 @@
+QwpResultValue | QuestDB JavaScript Client - v4.2.0 Type Alias QwpResultValue
QwpResultValue:
| boolean
| number
| bigint
| string
| Uint8Array
| QwpDecimalValue
| QwpUuidValue
| QwpLong256Value
| QwpGeohashValue
| QwpResultArrayValue
| null
diff --git a/docs/types/_questdb_browser-client.QwpSenderErrorCategory.html b/docs/types/_questdb_browser-client.QwpSenderErrorCategory.html
new file mode 100644
index 0000000..8719050
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpSenderErrorCategory.html
@@ -0,0 +1 @@
+QwpSenderErrorCategory | QuestDB JavaScript Client - v4.2.0 Type Alias QwpSenderErrorCategory
diff --git a/docs/types/_questdb_browser-client.QwpSenderErrorPolicy.html b/docs/types/_questdb_browser-client.QwpSenderErrorPolicy.html
new file mode 100644
index 0000000..5cf6204
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpSenderErrorPolicy.html
@@ -0,0 +1 @@
+QwpSenderErrorPolicy | QuestDB JavaScript Client - v4.2.0 Type Alias QwpSenderErrorPolicy
diff --git a/docs/types/_questdb_browser-client.QwpSenderLogger.html b/docs/types/_questdb_browser-client.QwpSenderLogger.html
new file mode 100644
index 0000000..7b75334
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpSenderLogger.html
@@ -0,0 +1 @@
+QwpSenderLogger | QuestDB JavaScript Client - v4.2.0 Type Alias QwpSenderLogger
QwpSenderLogger: (
level: "error" | "warn" | "info" | "debug",
message: string | Error,
) => voidType declaration
- (level: "error" | "warn" | "info" | "debug", message: string | Error): void
Parameters
- level: "error" | "warn" | "info" | "debug"
- message: string | Error
Returns void
diff --git a/docs/types/_questdb_browser-client.QwpSenderSessionFactory.html b/docs/types/_questdb_browser-client.QwpSenderSessionFactory.html
new file mode 100644
index 0000000..07d6fe7
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpSenderSessionFactory.html
@@ -0,0 +1,5 @@
+QwpSenderSessionFactory | QuestDB JavaScript Client - v4.2.0 Type Alias QwpSenderSessionFactory
Opens the sender's session. The signal is aborted by close(), so a connect
+still negotiating can be torn down instead of outliving the sender by up to
+its connect/auth deadline. Factories that ignore the parameter remain
+assignable, matching QwpConnectionFactory.
+Type declaration
- (signal?: AbortSignal): Promise<QwpSenderSession>
Parameters
Optionalsignal: AbortSignal
Returns Promise<QwpSenderSession>
diff --git a/docs/types/_questdb_browser-client.QwpTarget.html b/docs/types/_questdb_browser-client.QwpTarget.html
new file mode 100644
index 0000000..2dd7265
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpTarget.html
@@ -0,0 +1,2 @@
+QwpTarget | QuestDB JavaScript Client - v4.2.0 Type Alias QwpTarget
Server role accepted by an egress connection. Defaults to any.
+
diff --git a/docs/types/_questdb_browser-client.QwpTimestampUnit.html b/docs/types/_questdb_browser-client.QwpTimestampUnit.html
new file mode 100644
index 0000000..792ce1d
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpTimestampUnit.html
@@ -0,0 +1 @@
+QwpTimestampUnit | QuestDB JavaScript Client - v4.2.0 Type Alias QwpTimestampUnit
QwpTimestampUnit: "ns" | "us" | "ms"
diff --git a/docs/types/_questdb_browser-client.QwpUpgradeErrorKind.html b/docs/types/_questdb_browser-client.QwpUpgradeErrorKind.html
new file mode 100644
index 0000000..8158e08
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpUpgradeErrorKind.html
@@ -0,0 +1 @@
+QwpUpgradeErrorKind | QuestDB JavaScript Client - v4.2.0 Type Alias QwpUpgradeErrorKind
diff --git a/docs/types/_questdb_browser-client.QwpUpgradeTimeoutPhase.html b/docs/types/_questdb_browser-client.QwpUpgradeTimeoutPhase.html
new file mode 100644
index 0000000..acd680b
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpUpgradeTimeoutPhase.html
@@ -0,0 +1,2 @@
+QwpUpgradeTimeoutPhase | QuestDB JavaScript Client - v4.2.0 Type Alias QwpUpgradeTimeoutPhase
Opening phase whose Node QWP deadline expired.
+
diff --git a/docs/types/_questdb_browser-client.QwpUuidInput.html b/docs/types/_questdb_browser-client.QwpUuidInput.html
new file mode 100644
index 0000000..777ecfd
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpUuidInput.html
@@ -0,0 +1,4 @@
+QwpUuidInput | QuestDB JavaScript Client - v4.2.0 Type Alias QwpUuidInput
QwpUuidInput: string | Uint8Array | { high: bigint; low: bigint }UUID input: canonical text, 16 canonical (RFC 4122) big-endian bytes, or the
+egress limb pair. All three forms describe the same UUID; the byte form is
+what uuid.parse() and java.util.UUID produce, not pre-encoded wire bytes.
+
diff --git a/docs/types/_questdb_browser-client.QwpWriterColumnKind.html b/docs/types/_questdb_browser-client.QwpWriterColumnKind.html
new file mode 100644
index 0000000..d5c3693
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpWriterColumnKind.html
@@ -0,0 +1 @@
+QwpWriterColumnKind | QuestDB JavaScript Client - v4.2.0 Type Alias QwpWriterColumnKind
QwpWriterColumnKind:
| "symbol"
| "varchar"
| "bool"
| "byte"
| "short"
| "int32"
| "int64"
| "float32"
| "float64"
| "timestamp"
| "date"
| "char"
| "binary"
| "uuid"
| "long256"
| "ipv4"
| "geohash"
| "decimal64"
| "decimal128"
| "decimal256"
| "doubleArray"
| "longArray"
diff --git a/docs/types/_questdb_browser-client.QwpWriterRow.html b/docs/types/_questdb_browser-client.QwpWriterRow.html
new file mode 100644
index 0000000..bc85e00
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpWriterRow.html
@@ -0,0 +1,2 @@
+QwpWriterRow | QuestDB JavaScript Client - v4.2.0 Type Alias QwpWriterRow<Schema>
QwpWriterRow: {
[Key in QwpDesignatedTimestampKey<Schema>]-?: QwpWriterColumnInput<
Schema[Key],
>
} & {
[Key in QwpRegularColumnKey<Schema>]?:
| QwpWriterColumnInput<Schema[Key]>
| null
}The object accepted by a table writer compiled from Schema.
+Type Parameters
- Schema extends QwpWriterSchema
diff --git a/docs/types/_questdb_browser-client.QwpWriterSchema.html b/docs/types/_questdb_browser-client.QwpWriterSchema.html
new file mode 100644
index 0000000..bcbd2a1
--- /dev/null
+++ b/docs/types/_questdb_browser-client.QwpWriterSchema.html
@@ -0,0 +1 @@
+QwpWriterSchema | QuestDB JavaScript Client - v4.2.0 Type Alias QwpWriterSchema
diff --git a/docs/types/_questdb_nodejs-client.ExtraOptions.html b/docs/types/_questdb_nodejs-client.ExtraOptions.html
new file mode 100644
index 0000000..6c1ac35
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.ExtraOptions.html
@@ -0,0 +1,6 @@
+ExtraOptions | QuestDB JavaScript Client - v4.2.0 Type Alias ExtraOptions
type ExtraOptions = {
agent?: Agent | http.Agent | https.Agent;
log?: Logger;
qwp?: QwpExtraOptions;
}
diff --git a/docs/types/_questdb_nodejs-client.Logger.html b/docs/types/_questdb_nodejs-client.Logger.html
new file mode 100644
index 0000000..bd21e85
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.Logger.html
@@ -0,0 +1,4 @@
+Logger | QuestDB JavaScript Client - v4.2.0 Type Alias Logger
Logger: (
level: "error" | "warn" | "info" | "debug",
message: string | Error,
) => voidLogger function type definition.
+Type declaration
- (level: "error" | "warn" | "info" | "debug", message: string | Error): void
Parameters
- level: "error" | "warn" | "info" | "debug"
The log level for the message
+ - message: string | Error
The message to log, either a string or Error object
+
Returns void
diff --git a/docs/types/_questdb_nodejs-client.QwpBindSetter.html b/docs/types/_questdb_nodejs-client.QwpBindSetter.html
new file mode 100644
index 0000000..c47142e
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpBindSetter.html
@@ -0,0 +1 @@
+QwpBindSetter | QuestDB JavaScript Client - v4.2.0 Type Alias QwpBindSetter
Type declaration
- (binds: QwpBindValues): void
Parameters
- binds: QwpBindValues
Returns void
diff --git a/docs/types/_questdb_nodejs-client.QwpBindType.html b/docs/types/_questdb_nodejs-client.QwpBindType.html
new file mode 100644
index 0000000..e420a35
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpBindType.html
@@ -0,0 +1,2 @@
+QwpBindType | QuestDB JavaScript Client - v4.2.0 Type Alias QwpBindType
QwpBindType:
| typeof BOOLEAN
| typeof BYTE
| typeof SHORT
| typeof INT
| typeof LONG
| typeof FLOAT
| typeof DOUBLE
| typeof TIMESTAMP
| typeof DATE
| typeof UUID
| typeof LONG256
| typeof GEOHASH
| typeof VARCHAR
| typeof TIMESTAMP_NANOS
| typeof DECIMAL64
| typeof DECIMAL128
| typeof DECIMAL256
| typeof CHARPhase-1 scalar bind types exposed by the Java reference client.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpColumnType.html b/docs/types/_questdb_nodejs-client.QwpColumnType.html
new file mode 100644
index 0000000..f173eda
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpColumnType.html
@@ -0,0 +1 @@
+QwpColumnType | QuestDB JavaScript Client - v4.2.0 Type Alias QwpColumnType
diff --git a/docs/types/_questdb_nodejs-client.QwpConnectionFactory.html b/docs/types/_questdb_nodejs-client.QwpConnectionFactory.html
new file mode 100644
index 0000000..3d83bbb
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpConnectionFactory.html
@@ -0,0 +1,5 @@
+QwpConnectionFactory | QuestDB JavaScript Client - v4.2.0 Type Alias QwpConnectionFactory
Opens one connection. The optional signal is aborted when the owning session
+closes, so a factory that is still negotiating can tear its socket down
+instead of leaving it alive until its own deadline expires. Factories that
+ignore the parameter remain assignable.
+Type declaration
- (signal?: AbortSignal): Promise<QwpBinaryConnection>
Parameters
Optionalsignal: AbortSignal
Returns Promise<QwpBinaryConnection>
diff --git a/docs/types/_questdb_nodejs-client.QwpDecimalInput.html b/docs/types/_questdb_nodejs-client.QwpDecimalInput.html
new file mode 100644
index 0000000..6bb2b5f
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpDecimalInput.html
@@ -0,0 +1,3 @@
+QwpDecimalInput | QuestDB JavaScript Client - v4.2.0 Type Alias QwpDecimalInput
QwpDecimalInput: bigint | number | string | { scale: number; unscaled: bigint }DECIMAL input: the unscaled bigint at the column's scale, decimal text (or
+a number) that is exactly representable at that scale, or the egress record.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpDoubleArrayInput.html b/docs/types/_questdb_nodejs-client.QwpDoubleArrayInput.html
new file mode 100644
index 0000000..a8b88e0
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpDoubleArrayInput.html
@@ -0,0 +1,2 @@
+QwpDoubleArrayInput | QuestDB JavaScript Client - v4.2.0 Type Alias QwpDoubleArrayInput
QwpDoubleArrayInput:
| QwpNestedNumberArray
| { dimensions: readonly number[]; values: readonly number[] }DOUBLE array input: nested arrays or a flat shape-and-values record.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpEgressCompression.html b/docs/types/_questdb_nodejs-client.QwpEgressCompression.html
new file mode 100644
index 0000000..7bc063c
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpEgressCompression.html
@@ -0,0 +1 @@
+QwpEgressCompression | QuestDB JavaScript Client - v4.2.0 Type Alias QwpEgressCompression
QwpEgressCompression: "raw" | "zstd" | "auto"
diff --git a/docs/types/_questdb_nodejs-client.QwpEgressMessage.html b/docs/types/_questdb_nodejs-client.QwpEgressMessage.html
new file mode 100644
index 0000000..45c3ea0
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpEgressMessage.html
@@ -0,0 +1 @@
+QwpEgressMessage | QuestDB JavaScript Client - v4.2.0 Type Alias QwpEgressMessage
QwpEgressMessage:
| QwpServerInfoMessage
| QwpResultBatchMessage
| QwpResultEndMessage
| QwpQueryErrorMessage
| QwpExecDoneMessage
| QwpCacheResetMessage
diff --git a/docs/types/_questdb_nodejs-client.QwpExtraOptions.html b/docs/types/_questdb_nodejs-client.QwpExtraOptions.html
new file mode 100644
index 0000000..ff0fe2a
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpExtraOptions.html
@@ -0,0 +1,11 @@
+QwpExtraOptions | QuestDB JavaScript Client - v4.2.0 Type Alias QwpExtraOptions
type QwpExtraOptions = {
sender?: QwpSenderOptions;
session?: QwpIngressSessionOptions;
udp?: Omit<QwpNodeUdpOptions, "host" | "port">;
webSocket?: Omit<QwpNodeIngressOptions, "url">;
}Index
Properties
Properties
Optionalsender
High-level buffering and auto-flush options.
+Optionalsession
Ingress ACK, durable-ACK, and reconnect options.
+Optionaludp
Node-only QWP-over-UDP socket overrides.
+Optionalweb Socket
Node ingress overrides. Values are applied after the connect string has
+been fully parsed and validated; typed values win when both forms set the
+same option.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpGeohashInput.html b/docs/types/_questdb_nodejs-client.QwpGeohashInput.html
new file mode 100644
index 0000000..93c701d
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpGeohashInput.html
@@ -0,0 +1,3 @@
+QwpGeohashInput | QuestDB JavaScript Client - v4.2.0 Type Alias QwpGeohashInput
QwpGeohashInput:
| bigint
| number
| string
| { bits: bigint; precisionBits: number }GEOHASH input: the raw bits, base-32 geohash text whose length matches the
+column precision, or the egress bit record.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpIngressProgressKind.html b/docs/types/_questdb_nodejs-client.QwpIngressProgressKind.html
new file mode 100644
index 0000000..40b466a
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpIngressProgressKind.html
@@ -0,0 +1 @@
+QwpIngressProgressKind | QuestDB JavaScript Client - v4.2.0 Type Alias QwpIngressProgressKind
diff --git a/docs/types/_questdb_nodejs-client.QwpInitialConnectMode.html b/docs/types/_questdb_nodejs-client.QwpInitialConnectMode.html
new file mode 100644
index 0000000..d88b35b
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpInitialConnectMode.html
@@ -0,0 +1 @@
+QwpInitialConnectMode | QuestDB JavaScript Client - v4.2.0 Type Alias QwpInitialConnectMode
diff --git a/docs/types/_questdb_nodejs-client.QwpInt64.html b/docs/types/_questdb_nodejs-client.QwpInt64.html
new file mode 100644
index 0000000..cb1738d
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpInt64.html
@@ -0,0 +1 @@
+QwpInt64 | QuestDB JavaScript Client - v4.2.0 Type Alias QwpInt64
QwpInt64: number | bigint
diff --git a/docs/types/_questdb_nodejs-client.QwpIpv4Input.html b/docs/types/_questdb_nodejs-client.QwpIpv4Input.html
new file mode 100644
index 0000000..ff6d189
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpIpv4Input.html
@@ -0,0 +1,2 @@
+QwpIpv4Input | QuestDB JavaScript Client - v4.2.0 Type Alias QwpIpv4Input
QwpIpv4Input: string | numberIPV4 input: dotted-quad text or a signed/unsigned packed 32-bit address.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpLong256Input.html b/docs/types/_questdb_nodejs-client.QwpLong256Input.html
new file mode 100644
index 0000000..9f30d46
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpLong256Input.html
@@ -0,0 +1,3 @@
+QwpLong256Input | QuestDB JavaScript Client - v4.2.0 Type Alias QwpLong256Input
LONG256 input: an unsigned 256-bit bigint, a 0x-prefixed hex string of
+up to 64 digits, four little-endian words, or the egress word record.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpLong256Words.html b/docs/types/_questdb_nodejs-client.QwpLong256Words.html
new file mode 100644
index 0000000..4132858
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpLong256Words.html
@@ -0,0 +1,2 @@
+QwpLong256Words | QuestDB JavaScript Client - v4.2.0 Type Alias QwpLong256Words
QwpLong256Words: readonly [bigint, bigint, bigint, bigint]LONG256 little-endian words; word 0 is least significant.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpLongArrayInput.html b/docs/types/_questdb_nodejs-client.QwpLongArrayInput.html
new file mode 100644
index 0000000..7452c24
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpLongArrayInput.html
@@ -0,0 +1,2 @@
+QwpLongArrayInput | QuestDB JavaScript Client - v4.2.0 Type Alias QwpLongArrayInput
QwpLongArrayInput:
| QwpNestedLongArray
| { dimensions: readonly number[]; values: readonly (number | bigint)[] }LONG array input: nested arrays or a flat shape-and-values record.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpNegotiatedEgressCompression.html b/docs/types/_questdb_nodejs-client.QwpNegotiatedEgressCompression.html
new file mode 100644
index 0000000..f1a25cd
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpNegotiatedEgressCompression.html
@@ -0,0 +1 @@
+QwpNegotiatedEgressCompression | QuestDB JavaScript Client - v4.2.0 Type Alias QwpNegotiatedEgressCompression
QwpNegotiatedEgressCompression:
| { codec: "raw"; level: 0 }
| { codec: "zstd"; level: number }
| { codec: "unknown"; contentEncoding: string; level: 0 }
diff --git a/docs/types/_questdb_nodejs-client.QwpNestedLongArray.html b/docs/types/_questdb_nodejs-client.QwpNestedLongArray.html
new file mode 100644
index 0000000..cdea330
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpNestedLongArray.html
@@ -0,0 +1,2 @@
+QwpNestedLongArray | QuestDB JavaScript Client - v4.2.0 Type Alias QwpNestedLongArray
Nested LONG array of uniform shape.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpNestedNumberArray.html b/docs/types/_questdb_nodejs-client.QwpNestedNumberArray.html
new file mode 100644
index 0000000..d7dfb75
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpNestedNumberArray.html
@@ -0,0 +1,2 @@
+QwpNestedNumberArray | QuestDB JavaScript Client - v4.2.0 Type Alias QwpNestedNumberArray
Nested DOUBLE array of uniform shape.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpNodeOrphanDrainEventKind.html b/docs/types/_questdb_nodejs-client.QwpNodeOrphanDrainEventKind.html
new file mode 100644
index 0000000..ed1b51a
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpNodeOrphanDrainEventKind.html
@@ -0,0 +1 @@
+QwpNodeOrphanDrainEventKind | QuestDB JavaScript Client - v4.2.0 Type Alias QwpNodeOrphanDrainEventKind
QwpNodeOrphanDrainEventKind: typeof QWP_ORPHAN_DRAIN_EVENT_KIND[keyof typeof QWP_ORPHAN_DRAIN_EVENT_KIND]
diff --git a/docs/types/_questdb_nodejs-client.QwpQueryCompletion.html b/docs/types/_questdb_nodejs-client.QwpQueryCompletion.html
new file mode 100644
index 0000000..e963919
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpQueryCompletion.html
@@ -0,0 +1 @@
+QwpQueryCompletion | QuestDB JavaScript Client - v4.2.0 Type Alias QwpQueryCompletion
diff --git a/docs/types/_questdb_nodejs-client.QwpReconnectEventKind.html b/docs/types/_questdb_nodejs-client.QwpReconnectEventKind.html
new file mode 100644
index 0000000..f0f9bcd
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpReconnectEventKind.html
@@ -0,0 +1 @@
+QwpReconnectEventKind | QuestDB JavaScript Client - v4.2.0 Type Alias QwpReconnectEventKind
diff --git a/docs/types/_questdb_nodejs-client.QwpResultBatchViewHandler.html b/docs/types/_questdb_nodejs-client.QwpResultBatchViewHandler.html
new file mode 100644
index 0000000..0dea2d1
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpResultBatchViewHandler.html
@@ -0,0 +1,3 @@
+QwpResultBatchViewHandler | QuestDB JavaScript Client - v4.2.0 Type Alias QwpResultBatchViewHandler
QwpResultBatchViewHandler: (
batch: QwpResultBatchView,
query: QwpEgressViewQuery,
) => void | Promise<void>Runs while one reusable batch view is valid. Do not retain the batch,
+columns, or raw byte slices after the callback settles.
+Type declaration
- (batch: QwpResultBatchView, query: QwpEgressViewQuery): void | Promise<void>
Parameters
- batch: QwpResultBatchView
- query: QwpEgressViewQuery
Returns void | Promise<void>
diff --git a/docs/types/_questdb_nodejs-client.QwpResultRowViewCallback.html b/docs/types/_questdb_nodejs-client.QwpResultRowViewCallback.html
new file mode 100644
index 0000000..536e1d8
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpResultRowViewCallback.html
@@ -0,0 +1,2 @@
+QwpResultRowViewCallback | QuestDB JavaScript Client - v4.2.0 Type Alias QwpResultRowViewCallback
Callback invoked by QwpResultBatchView.forEachRow().
+Type declaration
- (row: QwpResultRowView): void
Parameters
- row: QwpResultRowView
Returns void
diff --git a/docs/types/_questdb_nodejs-client.QwpResultValue.html b/docs/types/_questdb_nodejs-client.QwpResultValue.html
new file mode 100644
index 0000000..6fd57e8
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpResultValue.html
@@ -0,0 +1 @@
+QwpResultValue | QuestDB JavaScript Client - v4.2.0 Type Alias QwpResultValue
QwpResultValue:
| boolean
| number
| bigint
| string
| Uint8Array
| QwpDecimalValue
| QwpUuidValue
| QwpLong256Value
| QwpGeohashValue
| QwpResultArrayValue
| null
diff --git a/docs/types/_questdb_nodejs-client.QwpSenderErrorCategory.html b/docs/types/_questdb_nodejs-client.QwpSenderErrorCategory.html
new file mode 100644
index 0000000..006618b
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpSenderErrorCategory.html
@@ -0,0 +1 @@
+QwpSenderErrorCategory | QuestDB JavaScript Client - v4.2.0 Type Alias QwpSenderErrorCategory
diff --git a/docs/types/_questdb_nodejs-client.QwpSenderErrorPolicy.html b/docs/types/_questdb_nodejs-client.QwpSenderErrorPolicy.html
new file mode 100644
index 0000000..5399dc4
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpSenderErrorPolicy.html
@@ -0,0 +1 @@
+QwpSenderErrorPolicy | QuestDB JavaScript Client - v4.2.0 Type Alias QwpSenderErrorPolicy
diff --git a/docs/types/_questdb_nodejs-client.QwpSenderLogger.html b/docs/types/_questdb_nodejs-client.QwpSenderLogger.html
new file mode 100644
index 0000000..df340a3
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpSenderLogger.html
@@ -0,0 +1 @@
+QwpSenderLogger | QuestDB JavaScript Client - v4.2.0 Type Alias QwpSenderLogger
QwpSenderLogger: (
level: "error" | "warn" | "info" | "debug",
message: string | Error,
) => voidType declaration
- (level: "error" | "warn" | "info" | "debug", message: string | Error): void
Parameters
- level: "error" | "warn" | "info" | "debug"
- message: string | Error
Returns void
diff --git a/docs/types/_questdb_nodejs-client.QwpSenderSessionFactory.html b/docs/types/_questdb_nodejs-client.QwpSenderSessionFactory.html
new file mode 100644
index 0000000..1df3f38
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpSenderSessionFactory.html
@@ -0,0 +1,5 @@
+QwpSenderSessionFactory | QuestDB JavaScript Client - v4.2.0 Type Alias QwpSenderSessionFactory
Opens the sender's session. The signal is aborted by close(), so a connect
+still negotiating can be torn down instead of outliving the sender by up to
+its connect/auth deadline. Factories that ignore the parameter remain
+assignable, matching QwpConnectionFactory.
+Type declaration
- (signal?: AbortSignal): Promise<QwpSenderSession>
Parameters
Optionalsignal: AbortSignal
Returns Promise<QwpSenderSession>
diff --git a/docs/types/_questdb_nodejs-client.QwpSfBackpressurePolicy.html b/docs/types/_questdb_nodejs-client.QwpSfBackpressurePolicy.html
new file mode 100644
index 0000000..86900db
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpSfBackpressurePolicy.html
@@ -0,0 +1 @@
+QwpSfBackpressurePolicy | QuestDB JavaScript Client - v4.2.0 Type Alias QwpSfBackpressurePolicy
diff --git a/docs/types/_questdb_nodejs-client.QwpSfDurability.html b/docs/types/_questdb_nodejs-client.QwpSfDurability.html
new file mode 100644
index 0000000..4e25840
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpSfDurability.html
@@ -0,0 +1 @@
+QwpSfDurability | QuestDB JavaScript Client - v4.2.0 Type Alias QwpSfDurability
diff --git a/docs/types/_questdb_nodejs-client.QwpTarget.html b/docs/types/_questdb_nodejs-client.QwpTarget.html
new file mode 100644
index 0000000..9c2981a
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpTarget.html
@@ -0,0 +1,2 @@
+QwpTarget | QuestDB JavaScript Client - v4.2.0 Type Alias QwpTarget
Server role accepted by an egress connection. Defaults to any.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpTimestampUnit.html b/docs/types/_questdb_nodejs-client.QwpTimestampUnit.html
new file mode 100644
index 0000000..984a91e
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpTimestampUnit.html
@@ -0,0 +1 @@
+QwpTimestampUnit | QuestDB JavaScript Client - v4.2.0 Type Alias QwpTimestampUnit
QwpTimestampUnit: "ns" | "us" | "ms"
diff --git a/docs/types/_questdb_nodejs-client.QwpUpgradeErrorKind.html b/docs/types/_questdb_nodejs-client.QwpUpgradeErrorKind.html
new file mode 100644
index 0000000..8eb4d33
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpUpgradeErrorKind.html
@@ -0,0 +1 @@
+QwpUpgradeErrorKind | QuestDB JavaScript Client - v4.2.0 Type Alias QwpUpgradeErrorKind
diff --git a/docs/types/_questdb_nodejs-client.QwpUpgradeTimeoutPhase.html b/docs/types/_questdb_nodejs-client.QwpUpgradeTimeoutPhase.html
new file mode 100644
index 0000000..b7cfe84
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpUpgradeTimeoutPhase.html
@@ -0,0 +1,2 @@
+QwpUpgradeTimeoutPhase | QuestDB JavaScript Client - v4.2.0 Type Alias QwpUpgradeTimeoutPhase
Opening phase whose Node QWP deadline expired.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpUuidInput.html b/docs/types/_questdb_nodejs-client.QwpUuidInput.html
new file mode 100644
index 0000000..d3997a9
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpUuidInput.html
@@ -0,0 +1,4 @@
+QwpUuidInput | QuestDB JavaScript Client - v4.2.0 Type Alias QwpUuidInput
QwpUuidInput: string | Uint8Array | { high: bigint; low: bigint }UUID input: canonical text, 16 canonical (RFC 4122) big-endian bytes, or the
+egress limb pair. All three forms describe the same UUID; the byte form is
+what uuid.parse() and java.util.UUID produce, not pre-encoded wire bytes.
+
diff --git a/docs/types/_questdb_nodejs-client.QwpWriterColumnKind.html b/docs/types/_questdb_nodejs-client.QwpWriterColumnKind.html
new file mode 100644
index 0000000..c59847b
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpWriterColumnKind.html
@@ -0,0 +1 @@
+QwpWriterColumnKind | QuestDB JavaScript Client - v4.2.0 Type Alias QwpWriterColumnKind
QwpWriterColumnKind:
| "symbol"
| "varchar"
| "bool"
| "byte"
| "short"
| "int32"
| "int64"
| "float32"
| "float64"
| "timestamp"
| "date"
| "char"
| "binary"
| "uuid"
| "long256"
| "ipv4"
| "geohash"
| "decimal64"
| "decimal128"
| "decimal256"
| "doubleArray"
| "longArray"
diff --git a/docs/types/_questdb_nodejs-client.QwpWriterRow.html b/docs/types/_questdb_nodejs-client.QwpWriterRow.html
new file mode 100644
index 0000000..c08701a
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpWriterRow.html
@@ -0,0 +1,2 @@
+QwpWriterRow | QuestDB JavaScript Client - v4.2.0 Type Alias QwpWriterRow<Schema>
QwpWriterRow: {
[Key in QwpDesignatedTimestampKey<Schema>]-?: QwpWriterColumnInput<
Schema[Key],
>
} & {
[Key in QwpRegularColumnKey<Schema>]?:
| QwpWriterColumnInput<Schema[Key]>
| null
}The object accepted by a table writer compiled from Schema.
+Type Parameters
- Schema extends QwpWriterSchema
diff --git a/docs/types/_questdb_nodejs-client.QwpWriterSchema.html b/docs/types/_questdb_nodejs-client.QwpWriterSchema.html
new file mode 100644
index 0000000..912c770
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.QwpWriterSchema.html
@@ -0,0 +1 @@
+QwpWriterSchema | QuestDB JavaScript Client - v4.2.0 Type Alias QwpWriterSchema
diff --git a/docs/types/_questdb_nodejs-client.TimestampUnit.html b/docs/types/_questdb_nodejs-client.TimestampUnit.html
new file mode 100644
index 0000000..9321730
--- /dev/null
+++ b/docs/types/_questdb_nodejs-client.TimestampUnit.html
@@ -0,0 +1,2 @@
+TimestampUnit | QuestDB JavaScript Client - v4.2.0 Type Alias TimestampUnit
TimestampUnit: "ns" | "us" | "ms"Supported timestamp units for QuestDB operations.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_COLUMN_TYPE.html b/docs/variables/_questdb_browser-client.QWP_COLUMN_TYPE.html
new file mode 100644
index 0000000..a0cae32
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_COLUMN_TYPE.html
@@ -0,0 +1 @@
+QWP_COLUMN_TYPE | QuestDB JavaScript Client - v4.2.0 Variable QWP_COLUMN_TYPEConst
QWP_COLUMN_TYPE: {
BINARY: 23;
BOOLEAN: 1;
BYTE: 2;
CHAR: 22;
DATE: 11;
DECIMAL128: 20;
DECIMAL256: 21;
DECIMAL64: 19;
DOUBLE: 7;
DOUBLE_ARRAY: 17;
FLOAT: 6;
GEOHASH: 14;
INT: 4;
IPV4: 24;
LONG: 5;
LONG_ARRAY: 18;
LONG256: 13;
SHORT: 3;
SYMBOL: 9;
TIMESTAMP: 10;
TIMESTAMP_NANOS: 16;
UUID: 12;
VARCHAR: 15;
} = ...Type declaration
ReadonlyBINARY: 23
ReadonlyBOOLEAN: 1
ReadonlyBYTE: 2
ReadonlyCHAR: 22
ReadonlyDATE: 11
ReadonlyDECIMAL128: 20
ReadonlyDECIMAL256: 21
ReadonlyDECIMAL64: 19
ReadonlyDOUBLE: 7
ReadonlyDOUBLE_ARRAY: 17
ReadonlyFLOAT: 6
ReadonlyGEOHASH: 14
ReadonlyINT: 4
ReadonlyIPV4: 24
ReadonlyLONG: 5
ReadonlyLONG_ARRAY: 18
ReadonlyLONG256: 13
ReadonlySHORT: 3
ReadonlySYMBOL: 9
ReadonlyTIMESTAMP: 10
ReadonlyTIMESTAMP_NANOS: 16
ReadonlyUUID: 12
ReadonlyVARCHAR: 15
diff --git a/docs/variables/_questdb_browser-client.QWP_COMPRESSION_CODEC.html b/docs/variables/_questdb_browser-client.QWP_COMPRESSION_CODEC.html
new file mode 100644
index 0000000..9f58c9a
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_COMPRESSION_CODEC.html
@@ -0,0 +1 @@
+QWP_COMPRESSION_CODEC | QuestDB JavaScript Client - v4.2.0 Variable QWP_COMPRESSION_CODECConst
QWP_COMPRESSION_CODEC: { RAW: 0; ZSTD: 1 } = ...Type declaration
ReadonlyRAW: 0
ReadonlyZSTD: 1
diff --git a/docs/variables/_questdb_browser-client.QWP_DECIMAL_MAX_SCALE.html b/docs/variables/_questdb_browser-client.QWP_DECIMAL_MAX_SCALE.html
new file mode 100644
index 0000000..f00138c
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_DECIMAL_MAX_SCALE.html
@@ -0,0 +1,2 @@
+QWP_DECIMAL_MAX_SCALE | QuestDB JavaScript Client - v4.2.0 Variable QWP_DECIMAL_MAX_SCALEConst
QWP_DECIMAL_MAX_SCALE: { decimal128: 38; decimal256: 76; decimal64: 18 } = ...Maximum DECIMAL scale of each fixed-width decimal column type.
+Type declaration
Readonlydecimal128: 38
Readonlydecimal256: 76
Readonlydecimal64: 18
diff --git a/docs/variables/_questdb_browser-client.QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZE.html b/docs/variables/_questdb_browser-client.QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZE.html
new file mode 100644
index 0000000..a4169b5
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZE.html
@@ -0,0 +1,2 @@
+QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZE | QuestDB JavaScript Client - v4.2.0 Variable QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZEConst
QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZE: 4Default decoded result-buffer pool depth, matching the Java client.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_DEFAULT_EGRESS_INITIAL_CREDIT.html b/docs/variables/_questdb_browser-client.QWP_DEFAULT_EGRESS_INITIAL_CREDIT.html
new file mode 100644
index 0000000..ae7ed55
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_DEFAULT_EGRESS_INITIAL_CREDIT.html
@@ -0,0 +1,2 @@
+QWP_DEFAULT_EGRESS_INITIAL_CREDIT | QuestDB JavaScript Client - v4.2.0 Variable QWP_DEFAULT_EGRESS_INITIAL_CREDITConst
QWP_DEFAULT_EGRESS_INITIAL_CREDIT: 0Default send-ahead credit used by Java and TypeScript: zero is unbounded.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS.html b/docs/variables/_questdb_browser-client.QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS.html
new file mode 100644
index 0000000..c7c41a9
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS.html
@@ -0,0 +1,2 @@
+QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS | QuestDB JavaScript Client - v4.2.0 Variable QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MSConst
QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS: 5000 = 5_000Default wait for the initial or reconnected SERVER_INFO frame.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL.html b/docs/variables/_questdb_browser-client.QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL.html
new file mode 100644
index 0000000..761f495
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL.html
@@ -0,0 +1,3 @@
+QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL | QuestDB JavaScript Client - v4.2.0 Variable QWP_DURABLE_ACK_WEBSOCKET_PROTOCOLConst
QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL: "questdb.qwp.durable-ack.v1"Browser-visible WebSocket subprotocol used to request and confirm durable
+ingress acknowledgements. Browsers cannot set or inspect X-QWP-* headers.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_EGRESS_CAPABILITY.html b/docs/variables/_questdb_browser-client.QWP_EGRESS_CAPABILITY.html
new file mode 100644
index 0000000..415fe29
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_EGRESS_CAPABILITY.html
@@ -0,0 +1 @@
+QWP_EGRESS_CAPABILITY | QuestDB JavaScript Client - v4.2.0 Variable QWP_EGRESS_CAPABILITYConst
QWP_EGRESS_CAPABILITY: { COMPRESSION: 4; QUERY_FLAGS: 2; ZONE: 1 } = ...Type declaration
ReadonlyCOMPRESSION: 4
ReadonlyQUERY_FLAGS: 2
ReadonlyZONE: 1
diff --git a/docs/variables/_questdb_browser-client.QWP_EGRESS_MESSAGE.html b/docs/variables/_questdb_browser-client.QWP_EGRESS_MESSAGE.html
new file mode 100644
index 0000000..26cc165
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_EGRESS_MESSAGE.html
@@ -0,0 +1 @@
+QWP_EGRESS_MESSAGE | QuestDB JavaScript Client - v4.2.0 Variable QWP_EGRESS_MESSAGEConst
QWP_EGRESS_MESSAGE: {
CACHE_RESET: 23;
CANCEL: 20;
CREDIT: 21;
EXEC_DONE: 22;
QUERY_ERROR: 19;
QUERY_REQUEST: 16;
RESULT_BATCH: 17;
RESULT_END: 18;
SERVER_INFO: 24;
} = ...Type declaration
ReadonlyCACHE_RESET: 23
ReadonlyCANCEL: 20
ReadonlyCREDIT: 21
ReadonlyEXEC_DONE: 22
ReadonlyQUERY_ERROR: 19
ReadonlyQUERY_REQUEST: 16
ReadonlyRESULT_BATCH: 17
ReadonlyRESULT_END: 18
ReadonlySERVER_INFO: 24
diff --git a/docs/variables/_questdb_browser-client.QWP_EGRESS_PATH.html b/docs/variables/_questdb_browser-client.QWP_EGRESS_PATH.html
new file mode 100644
index 0000000..0575b49
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_EGRESS_PATH.html
@@ -0,0 +1 @@
+QWP_EGRESS_PATH | QuestDB JavaScript Client - v4.2.0 Variable QWP_EGRESS_PATHConst
QWP_EGRESS_PATH: "/read/v1"
diff --git a/docs/variables/_questdb_browser-client.QWP_ENCODING_GORILLA.html b/docs/variables/_questdb_browser-client.QWP_ENCODING_GORILLA.html
new file mode 100644
index 0000000..fdc2689
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_ENCODING_GORILLA.html
@@ -0,0 +1 @@
+QWP_ENCODING_GORILLA | QuestDB JavaScript Client - v4.2.0 Variable QWP_ENCODING_GORILLAConst
QWP_ENCODING_GORILLA: 1 = 0x01
diff --git a/docs/variables/_questdb_browser-client.QWP_ENCODING_UNCOMPRESSED.html b/docs/variables/_questdb_browser-client.QWP_ENCODING_UNCOMPRESSED.html
new file mode 100644
index 0000000..7ef3f21
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_ENCODING_UNCOMPRESSED.html
@@ -0,0 +1 @@
+QWP_ENCODING_UNCOMPRESSED | QuestDB JavaScript Client - v4.2.0 Variable QWP_ENCODING_UNCOMPRESSEDConst
QWP_ENCODING_UNCOMPRESSED: 0 = 0x00
diff --git a/docs/variables/_questdb_browser-client.QWP_FLAG_DEFER_COMMIT.html b/docs/variables/_questdb_browser-client.QWP_FLAG_DEFER_COMMIT.html
new file mode 100644
index 0000000..2836d2e
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_FLAG_DEFER_COMMIT.html
@@ -0,0 +1 @@
+QWP_FLAG_DEFER_COMMIT | QuestDB JavaScript Client - v4.2.0 Variable QWP_FLAG_DEFER_COMMITConst
QWP_FLAG_DEFER_COMMIT: 1 = 0x01
diff --git a/docs/variables/_questdb_browser-client.QWP_FLAG_DELTA_SYMBOL_DICTIONARY.html b/docs/variables/_questdb_browser-client.QWP_FLAG_DELTA_SYMBOL_DICTIONARY.html
new file mode 100644
index 0000000..67bebae
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_FLAG_DELTA_SYMBOL_DICTIONARY.html
@@ -0,0 +1 @@
+QWP_FLAG_DELTA_SYMBOL_DICTIONARY | QuestDB JavaScript Client - v4.2.0 Variable QWP_FLAG_DELTA_SYMBOL_DICTIONARYConst
QWP_FLAG_DELTA_SYMBOL_DICTIONARY: 8 = 0x08
diff --git a/docs/variables/_questdb_browser-client.QWP_FLAG_DURABLE_ACK_POLL.html b/docs/variables/_questdb_browser-client.QWP_FLAG_DURABLE_ACK_POLL.html
new file mode 100644
index 0000000..27edeb7
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_FLAG_DURABLE_ACK_POLL.html
@@ -0,0 +1,2 @@
+QWP_FLAG_DURABLE_ACK_POLL | QuestDB JavaScript Client - v4.2.0 Variable QWP_FLAG_DURABLE_ACK_POLLConst
QWP_FLAG_DURABLE_ACK_POLL: 2 = 0x02Table-less ingress control frame that polls negotiated durable-ACK progress.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_FLAG_GORILLA.html b/docs/variables/_questdb_browser-client.QWP_FLAG_GORILLA.html
new file mode 100644
index 0000000..51c4aa5
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_FLAG_GORILLA.html
@@ -0,0 +1 @@
+QWP_FLAG_GORILLA | QuestDB JavaScript Client - v4.2.0 Variable QWP_FLAG_GORILLAConst
QWP_FLAG_GORILLA: 4 = 0x04
diff --git a/docs/variables/_questdb_browser-client.QWP_FLAG_ZSTD.html b/docs/variables/_questdb_browser-client.QWP_FLAG_ZSTD.html
new file mode 100644
index 0000000..f93040c
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_FLAG_ZSTD.html
@@ -0,0 +1 @@
+QWP_FLAG_ZSTD | QuestDB JavaScript Client - v4.2.0 Variable QWP_FLAG_ZSTDConst
QWP_FLAG_ZSTD: 16 = 0x10
diff --git a/docs/variables/_questdb_browser-client.QWP_HEADER_SIZE.html b/docs/variables/_questdb_browser-client.QWP_HEADER_SIZE.html
new file mode 100644
index 0000000..2179ab6
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_HEADER_SIZE.html
@@ -0,0 +1 @@
+QWP_HEADER_SIZE | QuestDB JavaScript Client - v4.2.0 Variable QWP_HEADER_SIZEConst
QWP_HEADER_SIZE: 12
diff --git a/docs/variables/_questdb_browser-client.QWP_INGRESS_PATH.html b/docs/variables/_questdb_browser-client.QWP_INGRESS_PATH.html
new file mode 100644
index 0000000..fd42a3a
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_INGRESS_PATH.html
@@ -0,0 +1 @@
+QWP_INGRESS_PATH | QuestDB JavaScript Client - v4.2.0 Variable QWP_INGRESS_PATHConst
QWP_INGRESS_PATH: "/write/v4"
diff --git a/docs/variables/_questdb_browser-client.QWP_INGRESS_PROGRESS_KIND.html b/docs/variables/_questdb_browser-client.QWP_INGRESS_PROGRESS_KIND.html
new file mode 100644
index 0000000..f556099
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_INGRESS_PROGRESS_KIND.html
@@ -0,0 +1 @@
+QWP_INGRESS_PROGRESS_KIND | QuestDB JavaScript Client - v4.2.0 Variable QWP_INGRESS_PROGRESS_KINDConst
QWP_INGRESS_PROGRESS_KIND: {
ACKNOWLEDGED: "acknowledged";
DURABLE_ACKNOWLEDGED: "durable-acknowledged";
PUBLISHED: "published";
} = ...Type declaration
ReadonlyACKNOWLEDGED: "acknowledged"
ReadonlyDURABLE_ACKNOWLEDGED: "durable-acknowledged"
ReadonlyPUBLISHED: "published"
diff --git a/docs/variables/_questdb_browser-client.QWP_INITIAL_CONNECT_MODE.html b/docs/variables/_questdb_browser-client.QWP_INITIAL_CONNECT_MODE.html
new file mode 100644
index 0000000..c3bab54
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_INITIAL_CONNECT_MODE.html
@@ -0,0 +1,7 @@
+QWP_INITIAL_CONNECT_MODE | QuestDB JavaScript Client - v4.2.0 Variable QWP_INITIAL_CONNECT_MODEConst
QWP_INITIAL_CONNECT_MODE: { ASYNC: "async"; OFF: "off"; SYNC: "sync" } = ...Initial connection policy for an ingress reconnect session. Public browser
+and memory-only helpers resolve their default internally; Node persistent
+store-and-forward exposes all three modes.
+Type declaration
ReadonlyASYNC: "async"
Return immediately and connect on the background replay loop.
+ReadonlyOFF: "off"
Try once on the caller and fail immediately.
+ReadonlySYNC: "sync"
Retry on the caller within the configured reconnect budget.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_MAGIC.html b/docs/variables/_questdb_browser-client.QWP_MAGIC.html
new file mode 100644
index 0000000..6d9e27b
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAGIC.html
@@ -0,0 +1,2 @@
+QWP_MAGIC | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAGICConst
QWP_MAGIC: 827348817 = 0x31505751ASCII QWP1, represented as its little-endian uint32 value.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_MAX_ARRAY_DIMENSIONS.html b/docs/variables/_questdb_browser-client.QWP_MAX_ARRAY_DIMENSIONS.html
new file mode 100644
index 0000000..2987a81
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAX_ARRAY_DIMENSIONS.html
@@ -0,0 +1,2 @@
+QWP_MAX_ARRAY_DIMENSIONS | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_ARRAY_DIMENSIONSConst
QWP_MAX_ARRAY_DIMENSIONS: 32Maximum array rank accepted by QuestDB's QWP ingress decoder.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_MAX_ARRAY_DIMENSION_LENGTH.html b/docs/variables/_questdb_browser-client.QWP_MAX_ARRAY_DIMENSION_LENGTH.html
new file mode 100644
index 0000000..1ddc669
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAX_ARRAY_DIMENSION_LENGTH.html
@@ -0,0 +1,2 @@
+QWP_MAX_ARRAY_DIMENSION_LENGTH | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_ARRAY_DIMENSION_LENGTHConst
QWP_MAX_ARRAY_DIMENSION_LENGTH: 2147483647 = 2_147_483_647Maximum signed int32 array-axis length accepted by QWP ingress.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_MAX_BATCH_ROWS_UPPER_BOUND.html b/docs/variables/_questdb_browser-client.QWP_MAX_BATCH_ROWS_UPPER_BOUND.html
new file mode 100644
index 0000000..7937161
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAX_BATCH_ROWS_UPPER_BOUND.html
@@ -0,0 +1,2 @@
+QWP_MAX_BATCH_ROWS_UPPER_BOUND | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_BATCH_ROWS_UPPER_BOUNDConst
QWP_MAX_BATCH_ROWS_UPPER_BOUND: 1048576 = 1_048_576Largest client-requested egress RESULT_BATCH row cap.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_MAX_CELLS_PER_BATCH.html b/docs/variables/_questdb_browser-client.QWP_MAX_CELLS_PER_BATCH.html
new file mode 100644
index 0000000..75450b2
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAX_CELLS_PER_BATCH.html
@@ -0,0 +1,13 @@
+QWP_MAX_CELLS_PER_BATCH | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_CELLS_PER_BATCHConst
QWP_MAX_CELLS_PER_BATCH: 33554432 = 33_554_432Largest rowCount * columnCount a single RESULT_BATCH may declare.
+The row and column caps above bound each dimension on its own, and their
+product does not have to be reachable: 1,048,576 rows of 2,048 columns is
+2.1 billion cells. Decoding materializes two rowCount-length arrays per
+column, measured at 16 bytes per cell, so the product is what decides how
+much memory a response can cost. It is also the dimension a compressed body
+detaches from the wire: an all-NULL column is one bit per cell before zstd,
+so without this bound a few kilobytes of RLE-compressed bitmap declares a
+grid no heap can hold.
+32Mi cells is roughly 512 MB decoded. That is far above any plausible
+result -- the widest supported table at 16k rows, or a full 1,048,576-row
+batch at 32 columns -- and far below what the caps alone would permit.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_MAX_COLUMNS_PER_TABLE.html b/docs/variables/_questdb_browser-client.QWP_MAX_COLUMNS_PER_TABLE.html
new file mode 100644
index 0000000..2b93c41
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAX_COLUMNS_PER_TABLE.html
@@ -0,0 +1 @@
+QWP_MAX_COLUMNS_PER_TABLE | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_COLUMNS_PER_TABLEConst
QWP_MAX_COLUMNS_PER_TABLE: 2048
diff --git a/docs/variables/_questdb_browser-client.QWP_MAX_COLUMN_NAME_LENGTH.html b/docs/variables/_questdb_browser-client.QWP_MAX_COLUMN_NAME_LENGTH.html
new file mode 100644
index 0000000..98d1d51
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAX_COLUMN_NAME_LENGTH.html
@@ -0,0 +1,2 @@
+QWP_MAX_COLUMN_NAME_LENGTH | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_COLUMN_NAME_LENGTHConst
QWP_MAX_COLUMN_NAME_LENGTH: 127Default QWP ingress identifier limits, in UTF-8 wire bytes.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_MAX_ERROR_MESSAGE_LENGTH.html b/docs/variables/_questdb_browser-client.QWP_MAX_ERROR_MESSAGE_LENGTH.html
new file mode 100644
index 0000000..dd8c9d2
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAX_ERROR_MESSAGE_LENGTH.html
@@ -0,0 +1 @@
+QWP_MAX_ERROR_MESSAGE_LENGTH | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_ERROR_MESSAGE_LENGTHConst
QWP_MAX_ERROR_MESSAGE_LENGTH: 1024
diff --git a/docs/variables/_questdb_browser-client.QWP_MAX_IDENTIFIER_BYTES.html b/docs/variables/_questdb_browser-client.QWP_MAX_IDENTIFIER_BYTES.html
new file mode 100644
index 0000000..36ccdba
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAX_IDENTIFIER_BYTES.html
@@ -0,0 +1,6 @@
+QWP_MAX_IDENTIFIER_BYTES | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_IDENTIFIER_BYTESConst
QWP_MAX_IDENTIFIER_BYTES: number = ...Defensive byte bound for identifiers decoded from query results.
+Existing tables may have names created through APIs that apply Java's
+127-UTF-16-code-unit metadata limit. One code unit takes at most three UTF-8
+bytes, so query decoding accepts that larger representation even though QWP
+ingress enforces its 127-byte protocol limit.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_MAX_ROWS_PER_TABLE.html b/docs/variables/_questdb_browser-client.QWP_MAX_ROWS_PER_TABLE.html
new file mode 100644
index 0000000..674ecab
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAX_ROWS_PER_TABLE.html
@@ -0,0 +1 @@
+QWP_MAX_ROWS_PER_TABLE | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_ROWS_PER_TABLEConst
QWP_MAX_ROWS_PER_TABLE: 1000000 = 1_000_000
diff --git a/docs/variables/_questdb_browser-client.QWP_MAX_SYMBOL_DICTIONARY_SIZE.html b/docs/variables/_questdb_browser-client.QWP_MAX_SYMBOL_DICTIONARY_SIZE.html
new file mode 100644
index 0000000..3386ba5
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAX_SYMBOL_DICTIONARY_SIZE.html
@@ -0,0 +1 @@
+QWP_MAX_SYMBOL_DICTIONARY_SIZE | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_SYMBOL_DICTIONARY_SIZEConst
QWP_MAX_SYMBOL_DICTIONARY_SIZE: 1000000 = 1_000_000
diff --git a/docs/variables/_questdb_browser-client.QWP_MAX_TABLE_NAME_LENGTH.html b/docs/variables/_questdb_browser-client.QWP_MAX_TABLE_NAME_LENGTH.html
new file mode 100644
index 0000000..f57ced0
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAX_TABLE_NAME_LENGTH.html
@@ -0,0 +1 @@
+QWP_MAX_TABLE_NAME_LENGTH | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_TABLE_NAME_LENGTHConst
QWP_MAX_TABLE_NAME_LENGTH: 127
diff --git a/docs/variables/_questdb_browser-client.QWP_MAX_ZSTD_DECOMPRESSED_SIZE.html b/docs/variables/_questdb_browser-client.QWP_MAX_ZSTD_DECOMPRESSED_SIZE.html
new file mode 100644
index 0000000..1e5daa5
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_MAX_ZSTD_DECOMPRESSED_SIZE.html
@@ -0,0 +1,2 @@
+QWP_MAX_ZSTD_DECOMPRESSED_SIZE | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_ZSTD_DECOMPRESSED_SIZEConst
QWP_MAX_ZSTD_DECOMPRESSED_SIZE: number = ...Matches the Java client's per-connection decompression safety cap.
+
diff --git a/docs/variables/_questdb_browser-client.QWP_QUERY_FLAG_RESET_DICTIONARY.html b/docs/variables/_questdb_browser-client.QWP_QUERY_FLAG_RESET_DICTIONARY.html
new file mode 100644
index 0000000..3aa1500
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_QUERY_FLAG_RESET_DICTIONARY.html
@@ -0,0 +1 @@
+QWP_QUERY_FLAG_RESET_DICTIONARY | QuestDB JavaScript Client - v4.2.0 Variable QWP_QUERY_FLAG_RESET_DICTIONARYConst
QWP_QUERY_FLAG_RESET_DICTIONARY: 1 = 0x01
diff --git a/docs/variables/_questdb_browser-client.QWP_RECONNECT_EVENT_KIND.html b/docs/variables/_questdb_browser-client.QWP_RECONNECT_EVENT_KIND.html
new file mode 100644
index 0000000..105883c
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_RECONNECT_EVENT_KIND.html
@@ -0,0 +1,4 @@
+QWP_RECONNECT_EVENT_KIND | QuestDB JavaScript Client - v4.2.0 Variable QWP_RECONNECT_EVENT_KINDConst
QWP_RECONNECT_EVENT_KIND: {
ATTEMPT_FAILED: "attempt-failed";
CONNECTED: "connected";
DURABLE_ACK_PERSISTENT_FAILURE: "durable-ack-persistent-failure";
DURABLE_ACK_UNAVAILABLE: "durable-ack-unavailable";
FAILED_OVER: "failed-over";
PRIMARY_UNAVAILABLE: "primary-unavailable";
RECONNECTED: "reconnected";
RECONNECTING: "reconnecting";
} = ...Type declaration
ReadonlyATTEMPT_FAILED: "attempt-failed"
ReadonlyCONNECTED: "connected"
ReadonlyDURABLE_ACK_PERSISTENT_FAILURE: "durable-ack-persistent-failure"
An orphan exhausted its consecutive durable-ACK mismatch budget.
+ReadonlyDURABLE_ACK_UNAVAILABLE: "durable-ack-unavailable"
An unbounded SF loop is waiting for durable-ACK-capable endpoints.
+ReadonlyFAILED_OVER: "failed-over"
ReadonlyPRIMARY_UNAVAILABLE: "primary-unavailable"
Every reachable ingress endpoint is temporarily unable to be primary.
+ReadonlyRECONNECTED: "reconnected"
ReadonlyRECONNECTING: "reconnecting"
diff --git a/docs/variables/_questdb_browser-client.QWP_RESET_MASK_DICTIONARY.html b/docs/variables/_questdb_browser-client.QWP_RESET_MASK_DICTIONARY.html
new file mode 100644
index 0000000..ff948d7
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_RESET_MASK_DICTIONARY.html
@@ -0,0 +1 @@
+QWP_RESET_MASK_DICTIONARY | QuestDB JavaScript Client - v4.2.0 Variable QWP_RESET_MASK_DICTIONARYConst
QWP_RESET_MASK_DICTIONARY: 1 = 0x01
diff --git a/docs/variables/_questdb_browser-client.QWP_SENDER_ERROR_CATEGORY.html b/docs/variables/_questdb_browser-client.QWP_SENDER_ERROR_CATEGORY.html
new file mode 100644
index 0000000..de4a2e1
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_SENDER_ERROR_CATEGORY.html
@@ -0,0 +1 @@
+QWP_SENDER_ERROR_CATEGORY | QuestDB JavaScript Client - v4.2.0 Variable QWP_SENDER_ERROR_CATEGORYConst
QWP_SENDER_ERROR_CATEGORY: {
DATA_LOSS: "data-loss";
DICTIONARY_GAP: "dictionary-gap";
INTERNAL_ERROR: "internal-error";
NOT_WRITABLE: "not-writable";
PARSE_ERROR: "parse-error";
PROTOCOL_VIOLATION: "protocol-violation";
SCHEMA_MISMATCH: "schema-mismatch";
SECURITY_ERROR: "security-error";
UNKNOWN: "unknown";
WRITE_ERROR: "write-error";
} = ...Type declaration
ReadonlyDATA_LOSS: "data-loss"
ReadonlyDICTIONARY_GAP: "dictionary-gap"
ReadonlyINTERNAL_ERROR: "internal-error"
ReadonlyNOT_WRITABLE: "not-writable"
ReadonlyPARSE_ERROR: "parse-error"
ReadonlyPROTOCOL_VIOLATION: "protocol-violation"
ReadonlySCHEMA_MISMATCH: "schema-mismatch"
ReadonlySECURITY_ERROR: "security-error"
ReadonlyUNKNOWN: "unknown"
ReadonlyWRITE_ERROR: "write-error"
diff --git a/docs/variables/_questdb_browser-client.QWP_SENDER_ERROR_POLICY.html b/docs/variables/_questdb_browser-client.QWP_SENDER_ERROR_POLICY.html
new file mode 100644
index 0000000..2e7aa88
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_SENDER_ERROR_POLICY.html
@@ -0,0 +1 @@
+QWP_SENDER_ERROR_POLICY | QuestDB JavaScript Client - v4.2.0 Variable QWP_SENDER_ERROR_POLICYConst
QWP_SENDER_ERROR_POLICY: {
ABANDONED: "abandoned";
RETRIABLE: "retriable";
RETRIABLE_OTHER: "retriable-other";
TERMINAL: "terminal";
} = ...Type declaration
ReadonlyABANDONED: "abandoned"
ReadonlyRETRIABLE: "retriable"
ReadonlyRETRIABLE_OTHER: "retriable-other"
ReadonlyTERMINAL: "terminal"
diff --git a/docs/variables/_questdb_browser-client.QWP_SERVER_ROLE.html b/docs/variables/_questdb_browser-client.QWP_SERVER_ROLE.html
new file mode 100644
index 0000000..46c21bf
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_SERVER_ROLE.html
@@ -0,0 +1 @@
+QWP_SERVER_ROLE | QuestDB JavaScript Client - v4.2.0 Variable QWP_SERVER_ROLEConst
QWP_SERVER_ROLE: { PRIMARY: 1; PRIMARY_CATCHUP: 3; REPLICA: 2; STANDALONE: 0 } = ...Type declaration
ReadonlyPRIMARY: 1
ReadonlyPRIMARY_CATCHUP: 3
ReadonlyREPLICA: 2
ReadonlySTANDALONE: 0
diff --git a/docs/variables/_questdb_browser-client.QWP_STATUS.html b/docs/variables/_questdb_browser-client.QWP_STATUS.html
new file mode 100644
index 0000000..e590ff7
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_STATUS.html
@@ -0,0 +1 @@
+QWP_STATUS | QuestDB JavaScript Client - v4.2.0 Variable QWP_STATUSConst
QWP_STATUS: {
CANCELLED: 10;
DICTIONARY_GAP: 13;
DURABLE_ACK: 2;
INTERNAL_ERROR: 6;
LIMIT_EXCEEDED: 11;
NOT_WRITABLE: 12;
OK: 0;
PARSE_ERROR: 5;
SCHEMA_MISMATCH: 3;
SECURITY_ERROR: 8;
SERVER_INFO: 1;
WRITE_ERROR: 9;
} = ...Type declaration
ReadonlyCANCELLED: 10
ReadonlyDICTIONARY_GAP: 13
ReadonlyDURABLE_ACK: 2
ReadonlyINTERNAL_ERROR: 6
ReadonlyLIMIT_EXCEEDED: 11
ReadonlyNOT_WRITABLE: 12
ReadonlyOK: 0
ReadonlyPARSE_ERROR: 5
ReadonlySCHEMA_MISMATCH: 3
ReadonlySECURITY_ERROR: 8
ReadonlySERVER_INFO: 1
ReadonlyWRITE_ERROR: 9
diff --git a/docs/variables/_questdb_browser-client.QWP_TARGET.html b/docs/variables/_questdb_browser-client.QWP_TARGET.html
new file mode 100644
index 0000000..49bbef9
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_TARGET.html
@@ -0,0 +1 @@
+QWP_TARGET | QuestDB JavaScript Client - v4.2.0 Variable QWP_TARGETConst
QWP_TARGET: { ANY: "any"; PRIMARY: "primary"; REPLICA: "replica" } = ...Type declaration
ReadonlyANY: "any"
ReadonlyPRIMARY: "primary"
ReadonlyREPLICA: "replica"
diff --git a/docs/variables/_questdb_browser-client.QWP_UPGRADE_ERROR_KIND.html b/docs/variables/_questdb_browser-client.QWP_UPGRADE_ERROR_KIND.html
new file mode 100644
index 0000000..ce04a48
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_UPGRADE_ERROR_KIND.html
@@ -0,0 +1,2 @@
+QWP_UPGRADE_ERROR_KIND | QuestDB JavaScript Client - v4.2.0 Variable QWP_UPGRADE_ERROR_KINDConst
QWP_UPGRADE_ERROR_KIND: {
AUTHENTICATION: "authentication";
CAPABILITY_MISMATCH: "capability-mismatch";
HTTP_REJECTED: "http-rejected";
OPAQUE: "opaque";
ROLE_REJECTED: "role-rejected";
TIMEOUT: "timeout";
TRANSPORT: "transport";
VERSION_MISMATCH: "version-mismatch";
} = ...Type declaration
ReadonlyAUTHENTICATION: "authentication"
ReadonlyCAPABILITY_MISMATCH: "capability-mismatch"
ReadonlyHTTP_REJECTED: "http-rejected"
ReadonlyOPAQUE: "opaque"
Browser WebSocket APIs do not expose the rejected HTTP upgrade.
+ReadonlyROLE_REJECTED: "role-rejected"
ReadonlyTIMEOUT: "timeout"
ReadonlyTRANSPORT: "transport"
ReadonlyVERSION_MISMATCH: "version-mismatch"
diff --git a/docs/variables/_questdb_browser-client.QWP_UPGRADE_TIMEOUT_PHASE.html b/docs/variables/_questdb_browser-client.QWP_UPGRADE_TIMEOUT_PHASE.html
new file mode 100644
index 0000000..eacaa72
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_UPGRADE_TIMEOUT_PHASE.html
@@ -0,0 +1 @@
+QWP_UPGRADE_TIMEOUT_PHASE | QuestDB JavaScript Client - v4.2.0 Variable QWP_UPGRADE_TIMEOUT_PHASEConst
QWP_UPGRADE_TIMEOUT_PHASE: {
AUTHENTICATION: "authentication";
CONNECT: "connect";
} = ...Type declaration
ReadonlyAUTHENTICATION: "authentication"
ReadonlyCONNECT: "connect"
diff --git a/docs/variables/_questdb_browser-client.QWP_VERSION.html b/docs/variables/_questdb_browser-client.QWP_VERSION.html
new file mode 100644
index 0000000..d50e948
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_VERSION.html
@@ -0,0 +1 @@
+QWP_VERSION | QuestDB JavaScript Client - v4.2.0 Variable QWP_VERSIONConst
QWP_VERSION: 1
diff --git a/docs/variables/_questdb_browser-client.QWP_ZSTD_MAX_COMPRESSION_LEVEL.html b/docs/variables/_questdb_browser-client.QWP_ZSTD_MAX_COMPRESSION_LEVEL.html
new file mode 100644
index 0000000..51a4e97
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_ZSTD_MAX_COMPRESSION_LEVEL.html
@@ -0,0 +1 @@
+QWP_ZSTD_MAX_COMPRESSION_LEVEL | QuestDB JavaScript Client - v4.2.0 Variable QWP_ZSTD_MAX_COMPRESSION_LEVELConst
QWP_ZSTD_MAX_COMPRESSION_LEVEL: 22
diff --git a/docs/variables/_questdb_browser-client.QWP_ZSTD_MIN_COMPRESSION_LEVEL.html b/docs/variables/_questdb_browser-client.QWP_ZSTD_MIN_COMPRESSION_LEVEL.html
new file mode 100644
index 0000000..debbba4
--- /dev/null
+++ b/docs/variables/_questdb_browser-client.QWP_ZSTD_MIN_COMPRESSION_LEVEL.html
@@ -0,0 +1 @@
+QWP_ZSTD_MIN_COMPRESSION_LEVEL | QuestDB JavaScript Client - v4.2.0 Variable QWP_ZSTD_MIN_COMPRESSION_LEVELConst
QWP_ZSTD_MIN_COMPRESSION_LEVEL: 1
diff --git a/docs/variables/_questdb_nodejs-client.QWP_COLUMN_TYPE.html b/docs/variables/_questdb_nodejs-client.QWP_COLUMN_TYPE.html
new file mode 100644
index 0000000..81b18ff
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_COLUMN_TYPE.html
@@ -0,0 +1 @@
+QWP_COLUMN_TYPE | QuestDB JavaScript Client - v4.2.0 Variable QWP_COLUMN_TYPEConst
QWP_COLUMN_TYPE: {
BINARY: 23;
BOOLEAN: 1;
BYTE: 2;
CHAR: 22;
DATE: 11;
DECIMAL128: 20;
DECIMAL256: 21;
DECIMAL64: 19;
DOUBLE: 7;
DOUBLE_ARRAY: 17;
FLOAT: 6;
GEOHASH: 14;
INT: 4;
IPV4: 24;
LONG: 5;
LONG_ARRAY: 18;
LONG256: 13;
SHORT: 3;
SYMBOL: 9;
TIMESTAMP: 10;
TIMESTAMP_NANOS: 16;
UUID: 12;
VARCHAR: 15;
} = ...Type declaration
ReadonlyBINARY: 23
ReadonlyBOOLEAN: 1
ReadonlyBYTE: 2
ReadonlyCHAR: 22
ReadonlyDATE: 11
ReadonlyDECIMAL128: 20
ReadonlyDECIMAL256: 21
ReadonlyDECIMAL64: 19
ReadonlyDOUBLE: 7
ReadonlyDOUBLE_ARRAY: 17
ReadonlyFLOAT: 6
ReadonlyGEOHASH: 14
ReadonlyINT: 4
ReadonlyIPV4: 24
ReadonlyLONG: 5
ReadonlyLONG_ARRAY: 18
ReadonlyLONG256: 13
ReadonlySHORT: 3
ReadonlySYMBOL: 9
ReadonlyTIMESTAMP: 10
ReadonlyTIMESTAMP_NANOS: 16
ReadonlyUUID: 12
ReadonlyVARCHAR: 15
diff --git a/docs/variables/_questdb_nodejs-client.QWP_COMPRESSION_CODEC.html b/docs/variables/_questdb_nodejs-client.QWP_COMPRESSION_CODEC.html
new file mode 100644
index 0000000..ed4fc3e
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_COMPRESSION_CODEC.html
@@ -0,0 +1 @@
+QWP_COMPRESSION_CODEC | QuestDB JavaScript Client - v4.2.0 Variable QWP_COMPRESSION_CODECConst
QWP_COMPRESSION_CODEC: { RAW: 0; ZSTD: 1 } = ...Type declaration
ReadonlyRAW: 0
ReadonlyZSTD: 1
diff --git a/docs/variables/_questdb_nodejs-client.QWP_DECIMAL_MAX_SCALE.html b/docs/variables/_questdb_nodejs-client.QWP_DECIMAL_MAX_SCALE.html
new file mode 100644
index 0000000..f85a47c
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_DECIMAL_MAX_SCALE.html
@@ -0,0 +1,2 @@
+QWP_DECIMAL_MAX_SCALE | QuestDB JavaScript Client - v4.2.0 Variable QWP_DECIMAL_MAX_SCALEConst
QWP_DECIMAL_MAX_SCALE: { decimal128: 38; decimal256: 76; decimal64: 18 } = ...Maximum DECIMAL scale of each fixed-width decimal column type.
+Type declaration
Readonlydecimal128: 38
Readonlydecimal256: 76
Readonlydecimal64: 18
diff --git a/docs/variables/_questdb_nodejs-client.QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZE.html b/docs/variables/_questdb_nodejs-client.QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZE.html
new file mode 100644
index 0000000..16bf23e
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZE.html
@@ -0,0 +1,2 @@
+QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZE | QuestDB JavaScript Client - v4.2.0 Variable QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZEConst
QWP_DEFAULT_EGRESS_BUFFER_POOL_SIZE: 4Default decoded result-buffer pool depth, matching the Java client.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_DEFAULT_EGRESS_INITIAL_CREDIT.html b/docs/variables/_questdb_nodejs-client.QWP_DEFAULT_EGRESS_INITIAL_CREDIT.html
new file mode 100644
index 0000000..fa2d6ed
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_DEFAULT_EGRESS_INITIAL_CREDIT.html
@@ -0,0 +1,2 @@
+QWP_DEFAULT_EGRESS_INITIAL_CREDIT | QuestDB JavaScript Client - v4.2.0 Variable QWP_DEFAULT_EGRESS_INITIAL_CREDITConst
QWP_DEFAULT_EGRESS_INITIAL_CREDIT: 0Default send-ahead credit used by Java and TypeScript: zero is unbounded.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS.html b/docs/variables/_questdb_nodejs-client.QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS.html
new file mode 100644
index 0000000..9d0eba7
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS.html
@@ -0,0 +1,2 @@
+QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS | QuestDB JavaScript Client - v4.2.0 Variable QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MSConst
QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS: 5000 = 5_000Default wait for the initial or reconnected SERVER_INFO frame.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL.html b/docs/variables/_questdb_nodejs-client.QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL.html
new file mode 100644
index 0000000..784d78d
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL.html
@@ -0,0 +1,3 @@
+QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL | QuestDB JavaScript Client - v4.2.0 Variable QWP_DURABLE_ACK_WEBSOCKET_PROTOCOLConst
QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL: "questdb.qwp.durable-ack.v1"Browser-visible WebSocket subprotocol used to request and confirm durable
+ingress acknowledgements. Browsers cannot set or inspect X-QWP-* headers.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_EGRESS_CAPABILITY.html b/docs/variables/_questdb_nodejs-client.QWP_EGRESS_CAPABILITY.html
new file mode 100644
index 0000000..c39f835
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_EGRESS_CAPABILITY.html
@@ -0,0 +1 @@
+QWP_EGRESS_CAPABILITY | QuestDB JavaScript Client - v4.2.0 Variable QWP_EGRESS_CAPABILITYConst
QWP_EGRESS_CAPABILITY: { COMPRESSION: 4; QUERY_FLAGS: 2; ZONE: 1 } = ...Type declaration
ReadonlyCOMPRESSION: 4
ReadonlyQUERY_FLAGS: 2
ReadonlyZONE: 1
diff --git a/docs/variables/_questdb_nodejs-client.QWP_EGRESS_MESSAGE.html b/docs/variables/_questdb_nodejs-client.QWP_EGRESS_MESSAGE.html
new file mode 100644
index 0000000..99a48c1
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_EGRESS_MESSAGE.html
@@ -0,0 +1 @@
+QWP_EGRESS_MESSAGE | QuestDB JavaScript Client - v4.2.0 Variable QWP_EGRESS_MESSAGEConst
QWP_EGRESS_MESSAGE: {
CACHE_RESET: 23;
CANCEL: 20;
CREDIT: 21;
EXEC_DONE: 22;
QUERY_ERROR: 19;
QUERY_REQUEST: 16;
RESULT_BATCH: 17;
RESULT_END: 18;
SERVER_INFO: 24;
} = ...Type declaration
ReadonlyCACHE_RESET: 23
ReadonlyCANCEL: 20
ReadonlyCREDIT: 21
ReadonlyEXEC_DONE: 22
ReadonlyQUERY_ERROR: 19
ReadonlyQUERY_REQUEST: 16
ReadonlyRESULT_BATCH: 17
ReadonlyRESULT_END: 18
ReadonlySERVER_INFO: 24
diff --git a/docs/variables/_questdb_nodejs-client.QWP_EGRESS_PATH.html b/docs/variables/_questdb_nodejs-client.QWP_EGRESS_PATH.html
new file mode 100644
index 0000000..64d051e
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_EGRESS_PATH.html
@@ -0,0 +1 @@
+QWP_EGRESS_PATH | QuestDB JavaScript Client - v4.2.0 Variable QWP_EGRESS_PATHConst
QWP_EGRESS_PATH: "/read/v1"
diff --git a/docs/variables/_questdb_nodejs-client.QWP_ENCODING_GORILLA.html b/docs/variables/_questdb_nodejs-client.QWP_ENCODING_GORILLA.html
new file mode 100644
index 0000000..2338aa1
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_ENCODING_GORILLA.html
@@ -0,0 +1 @@
+QWP_ENCODING_GORILLA | QuestDB JavaScript Client - v4.2.0 Variable QWP_ENCODING_GORILLAConst
QWP_ENCODING_GORILLA: 1 = 0x01
diff --git a/docs/variables/_questdb_nodejs-client.QWP_ENCODING_UNCOMPRESSED.html b/docs/variables/_questdb_nodejs-client.QWP_ENCODING_UNCOMPRESSED.html
new file mode 100644
index 0000000..ef8e866
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_ENCODING_UNCOMPRESSED.html
@@ -0,0 +1 @@
+QWP_ENCODING_UNCOMPRESSED | QuestDB JavaScript Client - v4.2.0 Variable QWP_ENCODING_UNCOMPRESSEDConst
QWP_ENCODING_UNCOMPRESSED: 0 = 0x00
diff --git a/docs/variables/_questdb_nodejs-client.QWP_FLAG_DEFER_COMMIT.html b/docs/variables/_questdb_nodejs-client.QWP_FLAG_DEFER_COMMIT.html
new file mode 100644
index 0000000..628fe5a
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_FLAG_DEFER_COMMIT.html
@@ -0,0 +1 @@
+QWP_FLAG_DEFER_COMMIT | QuestDB JavaScript Client - v4.2.0 Variable QWP_FLAG_DEFER_COMMITConst
QWP_FLAG_DEFER_COMMIT: 1 = 0x01
diff --git a/docs/variables/_questdb_nodejs-client.QWP_FLAG_DELTA_SYMBOL_DICTIONARY.html b/docs/variables/_questdb_nodejs-client.QWP_FLAG_DELTA_SYMBOL_DICTIONARY.html
new file mode 100644
index 0000000..7598fb4
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_FLAG_DELTA_SYMBOL_DICTIONARY.html
@@ -0,0 +1 @@
+QWP_FLAG_DELTA_SYMBOL_DICTIONARY | QuestDB JavaScript Client - v4.2.0 Variable QWP_FLAG_DELTA_SYMBOL_DICTIONARYConst
QWP_FLAG_DELTA_SYMBOL_DICTIONARY: 8 = 0x08
diff --git a/docs/variables/_questdb_nodejs-client.QWP_FLAG_DURABLE_ACK_POLL.html b/docs/variables/_questdb_nodejs-client.QWP_FLAG_DURABLE_ACK_POLL.html
new file mode 100644
index 0000000..388bda7
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_FLAG_DURABLE_ACK_POLL.html
@@ -0,0 +1,2 @@
+QWP_FLAG_DURABLE_ACK_POLL | QuestDB JavaScript Client - v4.2.0 Variable QWP_FLAG_DURABLE_ACK_POLLConst
QWP_FLAG_DURABLE_ACK_POLL: 2 = 0x02Table-less ingress control frame that polls negotiated durable-ACK progress.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_FLAG_GORILLA.html b/docs/variables/_questdb_nodejs-client.QWP_FLAG_GORILLA.html
new file mode 100644
index 0000000..8e4794c
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_FLAG_GORILLA.html
@@ -0,0 +1 @@
+QWP_FLAG_GORILLA | QuestDB JavaScript Client - v4.2.0 Variable QWP_FLAG_GORILLAConst
QWP_FLAG_GORILLA: 4 = 0x04
diff --git a/docs/variables/_questdb_nodejs-client.QWP_FLAG_ZSTD.html b/docs/variables/_questdb_nodejs-client.QWP_FLAG_ZSTD.html
new file mode 100644
index 0000000..c824852
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_FLAG_ZSTD.html
@@ -0,0 +1 @@
+QWP_FLAG_ZSTD | QuestDB JavaScript Client - v4.2.0 Variable QWP_FLAG_ZSTDConst
QWP_FLAG_ZSTD: 16 = 0x10
diff --git a/docs/variables/_questdb_nodejs-client.QWP_HEADER_SIZE.html b/docs/variables/_questdb_nodejs-client.QWP_HEADER_SIZE.html
new file mode 100644
index 0000000..e4d90ef
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_HEADER_SIZE.html
@@ -0,0 +1 @@
+QWP_HEADER_SIZE | QuestDB JavaScript Client - v4.2.0 Variable QWP_HEADER_SIZEConst
QWP_HEADER_SIZE: 12
diff --git a/docs/variables/_questdb_nodejs-client.QWP_INGRESS_PATH.html b/docs/variables/_questdb_nodejs-client.QWP_INGRESS_PATH.html
new file mode 100644
index 0000000..59c8f04
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_INGRESS_PATH.html
@@ -0,0 +1 @@
+QWP_INGRESS_PATH | QuestDB JavaScript Client - v4.2.0 Variable QWP_INGRESS_PATHConst
QWP_INGRESS_PATH: "/write/v4"
diff --git a/docs/variables/_questdb_nodejs-client.QWP_INGRESS_PROGRESS_KIND.html b/docs/variables/_questdb_nodejs-client.QWP_INGRESS_PROGRESS_KIND.html
new file mode 100644
index 0000000..d178da3
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_INGRESS_PROGRESS_KIND.html
@@ -0,0 +1 @@
+QWP_INGRESS_PROGRESS_KIND | QuestDB JavaScript Client - v4.2.0 Variable QWP_INGRESS_PROGRESS_KINDConst
QWP_INGRESS_PROGRESS_KIND: {
ACKNOWLEDGED: "acknowledged";
DURABLE_ACKNOWLEDGED: "durable-acknowledged";
PUBLISHED: "published";
} = ...Type declaration
ReadonlyACKNOWLEDGED: "acknowledged"
ReadonlyDURABLE_ACKNOWLEDGED: "durable-acknowledged"
ReadonlyPUBLISHED: "published"
diff --git a/docs/variables/_questdb_nodejs-client.QWP_INITIAL_CONNECT_MODE.html b/docs/variables/_questdb_nodejs-client.QWP_INITIAL_CONNECT_MODE.html
new file mode 100644
index 0000000..8d21653
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_INITIAL_CONNECT_MODE.html
@@ -0,0 +1,7 @@
+QWP_INITIAL_CONNECT_MODE | QuestDB JavaScript Client - v4.2.0 Variable QWP_INITIAL_CONNECT_MODEConst
QWP_INITIAL_CONNECT_MODE: { ASYNC: "async"; OFF: "off"; SYNC: "sync" } = ...Initial connection policy for an ingress reconnect session. Public browser
+and memory-only helpers resolve their default internally; Node persistent
+store-and-forward exposes all three modes.
+Type declaration
ReadonlyASYNC: "async"
Return immediately and connect on the background replay loop.
+ReadonlyOFF: "off"
Try once on the caller and fail immediately.
+ReadonlySYNC: "sync"
Retry on the caller within the configured reconnect budget.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAGIC.html b/docs/variables/_questdb_nodejs-client.QWP_MAGIC.html
new file mode 100644
index 0000000..c8c9ff6
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAGIC.html
@@ -0,0 +1,2 @@
+QWP_MAGIC | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAGICConst
QWP_MAGIC: 827348817 = 0x31505751ASCII QWP1, represented as its little-endian uint32 value.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAX_ARRAY_DIMENSIONS.html b/docs/variables/_questdb_nodejs-client.QWP_MAX_ARRAY_DIMENSIONS.html
new file mode 100644
index 0000000..2f08df0
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAX_ARRAY_DIMENSIONS.html
@@ -0,0 +1,2 @@
+QWP_MAX_ARRAY_DIMENSIONS | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_ARRAY_DIMENSIONSConst
QWP_MAX_ARRAY_DIMENSIONS: 32Maximum array rank accepted by QuestDB's QWP ingress decoder.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAX_ARRAY_DIMENSION_LENGTH.html b/docs/variables/_questdb_nodejs-client.QWP_MAX_ARRAY_DIMENSION_LENGTH.html
new file mode 100644
index 0000000..4845d27
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAX_ARRAY_DIMENSION_LENGTH.html
@@ -0,0 +1,2 @@
+QWP_MAX_ARRAY_DIMENSION_LENGTH | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_ARRAY_DIMENSION_LENGTHConst
QWP_MAX_ARRAY_DIMENSION_LENGTH: 2147483647 = 2_147_483_647Maximum signed int32 array-axis length accepted by QWP ingress.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAX_BATCH_ROWS_UPPER_BOUND.html b/docs/variables/_questdb_nodejs-client.QWP_MAX_BATCH_ROWS_UPPER_BOUND.html
new file mode 100644
index 0000000..282184d
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAX_BATCH_ROWS_UPPER_BOUND.html
@@ -0,0 +1,2 @@
+QWP_MAX_BATCH_ROWS_UPPER_BOUND | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_BATCH_ROWS_UPPER_BOUNDConst
QWP_MAX_BATCH_ROWS_UPPER_BOUND: 1048576 = 1_048_576Largest client-requested egress RESULT_BATCH row cap.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAX_CELLS_PER_BATCH.html b/docs/variables/_questdb_nodejs-client.QWP_MAX_CELLS_PER_BATCH.html
new file mode 100644
index 0000000..1897215
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAX_CELLS_PER_BATCH.html
@@ -0,0 +1,13 @@
+QWP_MAX_CELLS_PER_BATCH | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_CELLS_PER_BATCHConst
QWP_MAX_CELLS_PER_BATCH: 33554432 = 33_554_432Largest rowCount * columnCount a single RESULT_BATCH may declare.
+The row and column caps above bound each dimension on its own, and their
+product does not have to be reachable: 1,048,576 rows of 2,048 columns is
+2.1 billion cells. Decoding materializes two rowCount-length arrays per
+column, measured at 16 bytes per cell, so the product is what decides how
+much memory a response can cost. It is also the dimension a compressed body
+detaches from the wire: an all-NULL column is one bit per cell before zstd,
+so without this bound a few kilobytes of RLE-compressed bitmap declares a
+grid no heap can hold.
+32Mi cells is roughly 512 MB decoded. That is far above any plausible
+result -- the widest supported table at 16k rows, or a full 1,048,576-row
+batch at 32 columns -- and far below what the caps alone would permit.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAX_COLUMNS_PER_TABLE.html b/docs/variables/_questdb_nodejs-client.QWP_MAX_COLUMNS_PER_TABLE.html
new file mode 100644
index 0000000..b95a8c7
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAX_COLUMNS_PER_TABLE.html
@@ -0,0 +1 @@
+QWP_MAX_COLUMNS_PER_TABLE | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_COLUMNS_PER_TABLEConst
QWP_MAX_COLUMNS_PER_TABLE: 2048
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAX_COLUMN_NAME_LENGTH.html b/docs/variables/_questdb_nodejs-client.QWP_MAX_COLUMN_NAME_LENGTH.html
new file mode 100644
index 0000000..43c3599
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAX_COLUMN_NAME_LENGTH.html
@@ -0,0 +1,2 @@
+QWP_MAX_COLUMN_NAME_LENGTH | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_COLUMN_NAME_LENGTHConst
QWP_MAX_COLUMN_NAME_LENGTH: 127Default QWP ingress identifier limits, in UTF-8 wire bytes.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAX_ERROR_MESSAGE_LENGTH.html b/docs/variables/_questdb_nodejs-client.QWP_MAX_ERROR_MESSAGE_LENGTH.html
new file mode 100644
index 0000000..734901c
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAX_ERROR_MESSAGE_LENGTH.html
@@ -0,0 +1 @@
+QWP_MAX_ERROR_MESSAGE_LENGTH | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_ERROR_MESSAGE_LENGTHConst
QWP_MAX_ERROR_MESSAGE_LENGTH: 1024
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAX_IDENTIFIER_BYTES.html b/docs/variables/_questdb_nodejs-client.QWP_MAX_IDENTIFIER_BYTES.html
new file mode 100644
index 0000000..1900a5e
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAX_IDENTIFIER_BYTES.html
@@ -0,0 +1,6 @@
+QWP_MAX_IDENTIFIER_BYTES | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_IDENTIFIER_BYTESConst
QWP_MAX_IDENTIFIER_BYTES: number = ...Defensive byte bound for identifiers decoded from query results.
+Existing tables may have names created through APIs that apply Java's
+127-UTF-16-code-unit metadata limit. One code unit takes at most three UTF-8
+bytes, so query decoding accepts that larger representation even though QWP
+ingress enforces its 127-byte protocol limit.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAX_ROWS_PER_TABLE.html b/docs/variables/_questdb_nodejs-client.QWP_MAX_ROWS_PER_TABLE.html
new file mode 100644
index 0000000..9a74ed9
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAX_ROWS_PER_TABLE.html
@@ -0,0 +1 @@
+QWP_MAX_ROWS_PER_TABLE | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_ROWS_PER_TABLEConst
QWP_MAX_ROWS_PER_TABLE: 1000000 = 1_000_000
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAX_SYMBOL_DICTIONARY_SIZE.html b/docs/variables/_questdb_nodejs-client.QWP_MAX_SYMBOL_DICTIONARY_SIZE.html
new file mode 100644
index 0000000..26435f5
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAX_SYMBOL_DICTIONARY_SIZE.html
@@ -0,0 +1 @@
+QWP_MAX_SYMBOL_DICTIONARY_SIZE | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_SYMBOL_DICTIONARY_SIZEConst
QWP_MAX_SYMBOL_DICTIONARY_SIZE: 1000000 = 1_000_000
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAX_TABLE_NAME_LENGTH.html b/docs/variables/_questdb_nodejs-client.QWP_MAX_TABLE_NAME_LENGTH.html
new file mode 100644
index 0000000..5d9cb0a
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAX_TABLE_NAME_LENGTH.html
@@ -0,0 +1 @@
+QWP_MAX_TABLE_NAME_LENGTH | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_TABLE_NAME_LENGTHConst
QWP_MAX_TABLE_NAME_LENGTH: 127
diff --git a/docs/variables/_questdb_nodejs-client.QWP_MAX_ZSTD_DECOMPRESSED_SIZE.html b/docs/variables/_questdb_nodejs-client.QWP_MAX_ZSTD_DECOMPRESSED_SIZE.html
new file mode 100644
index 0000000..5898dfd
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_MAX_ZSTD_DECOMPRESSED_SIZE.html
@@ -0,0 +1,2 @@
+QWP_MAX_ZSTD_DECOMPRESSED_SIZE | QuestDB JavaScript Client - v4.2.0 Variable QWP_MAX_ZSTD_DECOMPRESSED_SIZEConst
QWP_MAX_ZSTD_DECOMPRESSED_SIZE: number = ...Matches the Java client's per-connection decompression safety cap.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_ORPHAN_DRAIN_EVENT_KIND.html b/docs/variables/_questdb_nodejs-client.QWP_ORPHAN_DRAIN_EVENT_KIND.html
new file mode 100644
index 0000000..781dd66
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_ORPHAN_DRAIN_EVENT_KIND.html
@@ -0,0 +1,2 @@
+QWP_ORPHAN_DRAIN_EVENT_KIND | QuestDB JavaScript Client - v4.2.0 Variable QWP_ORPHAN_DRAIN_EVENT_KINDConst
QWP_ORPHAN_DRAIN_EVENT_KIND: {
DISCOVERED: "discovered";
DRAINED: "drained";
DURABLE_ACK_PERSISTENT_FAILURE: "durable-ack-persistent-failure";
DURABLE_ACK_UNAVAILABLE: "durable-ack-unavailable";
FAILED: "failed";
LOCKED: "locked";
PRIMARY_UNAVAILABLE: "primary-unavailable";
RETRYING: "retrying";
SCAN_FAILED: "scan-failed";
STARTED: "started";
} = ...Type declaration
ReadonlyDISCOVERED: "discovered"
ReadonlyDRAINED: "drained"
ReadonlyDURABLE_ACK_PERSISTENT_FAILURE: "durable-ack-persistent-failure"
ReadonlyDURABLE_ACK_UNAVAILABLE: "durable-ack-unavailable"
ReadonlyFAILED: "failed"
ReadonlyLOCKED: "locked"
ReadonlyPRIMARY_UNAVAILABLE: "primary-unavailable"
ReadonlyRETRYING: "retrying"
The attempt failed transiently; the slot is left for a later scan.
+ReadonlySCAN_FAILED: "scan-failed"
ReadonlySTARTED: "started"
diff --git a/docs/variables/_questdb_nodejs-client.QWP_ORPHAN_FAILED_SENTINEL.html b/docs/variables/_questdb_nodejs-client.QWP_ORPHAN_FAILED_SENTINEL.html
new file mode 100644
index 0000000..912c028
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_ORPHAN_FAILED_SENTINEL.html
@@ -0,0 +1,2 @@
+QWP_ORPHAN_FAILED_SENTINEL | QuestDB JavaScript Client - v4.2.0 Variable QWP_ORPHAN_FAILED_SENTINELConst
QWP_ORPHAN_FAILED_SENTINEL: ".failed"Java-compatible marker that excludes a failed slot from automatic drain.
+
diff --git a/docs/variables/_questdb_nodejs-client.QWP_QUERY_FLAG_RESET_DICTIONARY.html b/docs/variables/_questdb_nodejs-client.QWP_QUERY_FLAG_RESET_DICTIONARY.html
new file mode 100644
index 0000000..021580b
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_QUERY_FLAG_RESET_DICTIONARY.html
@@ -0,0 +1 @@
+QWP_QUERY_FLAG_RESET_DICTIONARY | QuestDB JavaScript Client - v4.2.0 Variable QWP_QUERY_FLAG_RESET_DICTIONARYConst
QWP_QUERY_FLAG_RESET_DICTIONARY: 1 = 0x01
diff --git a/docs/variables/_questdb_nodejs-client.QWP_RECONNECT_EVENT_KIND.html b/docs/variables/_questdb_nodejs-client.QWP_RECONNECT_EVENT_KIND.html
new file mode 100644
index 0000000..deedbc6
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_RECONNECT_EVENT_KIND.html
@@ -0,0 +1,4 @@
+QWP_RECONNECT_EVENT_KIND | QuestDB JavaScript Client - v4.2.0 Variable QWP_RECONNECT_EVENT_KINDConst
QWP_RECONNECT_EVENT_KIND: {
ATTEMPT_FAILED: "attempt-failed";
CONNECTED: "connected";
DURABLE_ACK_PERSISTENT_FAILURE: "durable-ack-persistent-failure";
DURABLE_ACK_UNAVAILABLE: "durable-ack-unavailable";
FAILED_OVER: "failed-over";
PRIMARY_UNAVAILABLE: "primary-unavailable";
RECONNECTED: "reconnected";
RECONNECTING: "reconnecting";
} = ...Type declaration
ReadonlyATTEMPT_FAILED: "attempt-failed"
ReadonlyCONNECTED: "connected"
ReadonlyDURABLE_ACK_PERSISTENT_FAILURE: "durable-ack-persistent-failure"
An orphan exhausted its consecutive durable-ACK mismatch budget.
+ReadonlyDURABLE_ACK_UNAVAILABLE: "durable-ack-unavailable"
An unbounded SF loop is waiting for durable-ACK-capable endpoints.
+ReadonlyFAILED_OVER: "failed-over"
ReadonlyPRIMARY_UNAVAILABLE: "primary-unavailable"
Every reachable ingress endpoint is temporarily unable to be primary.
+ReadonlyRECONNECTED: "reconnected"
ReadonlyRECONNECTING: "reconnecting"
diff --git a/docs/variables/_questdb_nodejs-client.QWP_RESET_MASK_DICTIONARY.html b/docs/variables/_questdb_nodejs-client.QWP_RESET_MASK_DICTIONARY.html
new file mode 100644
index 0000000..742c91a
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_RESET_MASK_DICTIONARY.html
@@ -0,0 +1 @@
+QWP_RESET_MASK_DICTIONARY | QuestDB JavaScript Client - v4.2.0 Variable QWP_RESET_MASK_DICTIONARYConst
QWP_RESET_MASK_DICTIONARY: 1 = 0x01
diff --git a/docs/variables/_questdb_nodejs-client.QWP_SENDER_ERROR_CATEGORY.html b/docs/variables/_questdb_nodejs-client.QWP_SENDER_ERROR_CATEGORY.html
new file mode 100644
index 0000000..36a4e1d
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_SENDER_ERROR_CATEGORY.html
@@ -0,0 +1 @@
+QWP_SENDER_ERROR_CATEGORY | QuestDB JavaScript Client - v4.2.0 Variable QWP_SENDER_ERROR_CATEGORYConst
QWP_SENDER_ERROR_CATEGORY: {
DATA_LOSS: "data-loss";
DICTIONARY_GAP: "dictionary-gap";
INTERNAL_ERROR: "internal-error";
NOT_WRITABLE: "not-writable";
PARSE_ERROR: "parse-error";
PROTOCOL_VIOLATION: "protocol-violation";
SCHEMA_MISMATCH: "schema-mismatch";
SECURITY_ERROR: "security-error";
UNKNOWN: "unknown";
WRITE_ERROR: "write-error";
} = ...Type declaration
ReadonlyDATA_LOSS: "data-loss"
ReadonlyDICTIONARY_GAP: "dictionary-gap"
ReadonlyINTERNAL_ERROR: "internal-error"
ReadonlyNOT_WRITABLE: "not-writable"
ReadonlyPARSE_ERROR: "parse-error"
ReadonlyPROTOCOL_VIOLATION: "protocol-violation"
ReadonlySCHEMA_MISMATCH: "schema-mismatch"
ReadonlySECURITY_ERROR: "security-error"
ReadonlyUNKNOWN: "unknown"
ReadonlyWRITE_ERROR: "write-error"
diff --git a/docs/variables/_questdb_nodejs-client.QWP_SENDER_ERROR_POLICY.html b/docs/variables/_questdb_nodejs-client.QWP_SENDER_ERROR_POLICY.html
new file mode 100644
index 0000000..5aced61
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_SENDER_ERROR_POLICY.html
@@ -0,0 +1 @@
+QWP_SENDER_ERROR_POLICY | QuestDB JavaScript Client - v4.2.0 Variable QWP_SENDER_ERROR_POLICYConst
QWP_SENDER_ERROR_POLICY: {
ABANDONED: "abandoned";
RETRIABLE: "retriable";
RETRIABLE_OTHER: "retriable-other";
TERMINAL: "terminal";
} = ...Type declaration
ReadonlyABANDONED: "abandoned"
ReadonlyRETRIABLE: "retriable"
ReadonlyRETRIABLE_OTHER: "retriable-other"
ReadonlyTERMINAL: "terminal"
diff --git a/docs/variables/_questdb_nodejs-client.QWP_SERVER_ROLE.html b/docs/variables/_questdb_nodejs-client.QWP_SERVER_ROLE.html
new file mode 100644
index 0000000..d69040c
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_SERVER_ROLE.html
@@ -0,0 +1 @@
+QWP_SERVER_ROLE | QuestDB JavaScript Client - v4.2.0 Variable QWP_SERVER_ROLEConst
QWP_SERVER_ROLE: { PRIMARY: 1; PRIMARY_CATCHUP: 3; REPLICA: 2; STANDALONE: 0 } = ...Type declaration
ReadonlyPRIMARY: 1
ReadonlyPRIMARY_CATCHUP: 3
ReadonlyREPLICA: 2
ReadonlySTANDALONE: 0
diff --git a/docs/variables/_questdb_nodejs-client.QWP_SF_BACKPRESSURE_POLICY.html b/docs/variables/_questdb_nodejs-client.QWP_SF_BACKPRESSURE_POLICY.html
new file mode 100644
index 0000000..21265b8
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_SF_BACKPRESSURE_POLICY.html
@@ -0,0 +1 @@
+QWP_SF_BACKPRESSURE_POLICY | QuestDB JavaScript Client - v4.2.0 Variable QWP_SF_BACKPRESSURE_POLICYConst
QWP_SF_BACKPRESSURE_POLICY: { ERROR: "error"; WAIT: "wait" } = ...Type declaration
ReadonlyERROR: "error"
ReadonlyWAIT: "wait"
diff --git a/docs/variables/_questdb_nodejs-client.QWP_SF_DURABILITY.html b/docs/variables/_questdb_nodejs-client.QWP_SF_DURABILITY.html
new file mode 100644
index 0000000..c991f64
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_SF_DURABILITY.html
@@ -0,0 +1 @@
+QWP_SF_DURABILITY | QuestDB JavaScript Client - v4.2.0 Variable QWP_SF_DURABILITYConst
QWP_SF_DURABILITY: { APPEND: "append"; MEMORY: "memory"; PERIODIC: "periodic" } = ...Type declaration
ReadonlyAPPEND: "append"
ReadonlyMEMORY: "memory"
ReadonlyPERIODIC: "periodic"
diff --git a/docs/variables/_questdb_nodejs-client.QWP_STATUS.html b/docs/variables/_questdb_nodejs-client.QWP_STATUS.html
new file mode 100644
index 0000000..aa00259
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_STATUS.html
@@ -0,0 +1 @@
+QWP_STATUS | QuestDB JavaScript Client - v4.2.0 Variable QWP_STATUSConst
QWP_STATUS: {
CANCELLED: 10;
DICTIONARY_GAP: 13;
DURABLE_ACK: 2;
INTERNAL_ERROR: 6;
LIMIT_EXCEEDED: 11;
NOT_WRITABLE: 12;
OK: 0;
PARSE_ERROR: 5;
SCHEMA_MISMATCH: 3;
SECURITY_ERROR: 8;
SERVER_INFO: 1;
WRITE_ERROR: 9;
} = ...Type declaration
ReadonlyCANCELLED: 10
ReadonlyDICTIONARY_GAP: 13
ReadonlyDURABLE_ACK: 2
ReadonlyINTERNAL_ERROR: 6
ReadonlyLIMIT_EXCEEDED: 11
ReadonlyNOT_WRITABLE: 12
ReadonlyOK: 0
ReadonlyPARSE_ERROR: 5
ReadonlySCHEMA_MISMATCH: 3
ReadonlySECURITY_ERROR: 8
ReadonlySERVER_INFO: 1
ReadonlyWRITE_ERROR: 9
diff --git a/docs/variables/_questdb_nodejs-client.QWP_TARGET.html b/docs/variables/_questdb_nodejs-client.QWP_TARGET.html
new file mode 100644
index 0000000..d24f2ee
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_TARGET.html
@@ -0,0 +1 @@
+QWP_TARGET | QuestDB JavaScript Client - v4.2.0 Variable QWP_TARGETConst
QWP_TARGET: { ANY: "any"; PRIMARY: "primary"; REPLICA: "replica" } = ...Type declaration
ReadonlyANY: "any"
ReadonlyPRIMARY: "primary"
ReadonlyREPLICA: "replica"
diff --git a/docs/variables/_questdb_nodejs-client.QWP_UPGRADE_ERROR_KIND.html b/docs/variables/_questdb_nodejs-client.QWP_UPGRADE_ERROR_KIND.html
new file mode 100644
index 0000000..97c329c
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_UPGRADE_ERROR_KIND.html
@@ -0,0 +1,2 @@
+QWP_UPGRADE_ERROR_KIND | QuestDB JavaScript Client - v4.2.0 Variable QWP_UPGRADE_ERROR_KINDConst
QWP_UPGRADE_ERROR_KIND: {
AUTHENTICATION: "authentication";
CAPABILITY_MISMATCH: "capability-mismatch";
HTTP_REJECTED: "http-rejected";
OPAQUE: "opaque";
ROLE_REJECTED: "role-rejected";
TIMEOUT: "timeout";
TRANSPORT: "transport";
VERSION_MISMATCH: "version-mismatch";
} = ...Type declaration
ReadonlyAUTHENTICATION: "authentication"
ReadonlyCAPABILITY_MISMATCH: "capability-mismatch"
ReadonlyHTTP_REJECTED: "http-rejected"
ReadonlyOPAQUE: "opaque"
Browser WebSocket APIs do not expose the rejected HTTP upgrade.
+ReadonlyROLE_REJECTED: "role-rejected"
ReadonlyTIMEOUT: "timeout"
ReadonlyTRANSPORT: "transport"
ReadonlyVERSION_MISMATCH: "version-mismatch"
diff --git a/docs/variables/_questdb_nodejs-client.QWP_UPGRADE_TIMEOUT_PHASE.html b/docs/variables/_questdb_nodejs-client.QWP_UPGRADE_TIMEOUT_PHASE.html
new file mode 100644
index 0000000..c8626e2
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_UPGRADE_TIMEOUT_PHASE.html
@@ -0,0 +1 @@
+QWP_UPGRADE_TIMEOUT_PHASE | QuestDB JavaScript Client - v4.2.0 Variable QWP_UPGRADE_TIMEOUT_PHASEConst
QWP_UPGRADE_TIMEOUT_PHASE: {
AUTHENTICATION: "authentication";
CONNECT: "connect";
} = ...Type declaration
ReadonlyAUTHENTICATION: "authentication"
ReadonlyCONNECT: "connect"
diff --git a/docs/variables/_questdb_nodejs-client.QWP_VERSION.html b/docs/variables/_questdb_nodejs-client.QWP_VERSION.html
new file mode 100644
index 0000000..cc5ec8b
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_VERSION.html
@@ -0,0 +1 @@
+QWP_VERSION | QuestDB JavaScript Client - v4.2.0 Variable QWP_VERSIONConst
QWP_VERSION: 1
diff --git a/docs/variables/_questdb_nodejs-client.QWP_ZSTD_MAX_COMPRESSION_LEVEL.html b/docs/variables/_questdb_nodejs-client.QWP_ZSTD_MAX_COMPRESSION_LEVEL.html
new file mode 100644
index 0000000..aabd1bb
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_ZSTD_MAX_COMPRESSION_LEVEL.html
@@ -0,0 +1 @@
+QWP_ZSTD_MAX_COMPRESSION_LEVEL | QuestDB JavaScript Client - v4.2.0 Variable QWP_ZSTD_MAX_COMPRESSION_LEVELConst
QWP_ZSTD_MAX_COMPRESSION_LEVEL: 22
diff --git a/docs/variables/_questdb_nodejs-client.QWP_ZSTD_MIN_COMPRESSION_LEVEL.html b/docs/variables/_questdb_nodejs-client.QWP_ZSTD_MIN_COMPRESSION_LEVEL.html
new file mode 100644
index 0000000..2a54c37
--- /dev/null
+++ b/docs/variables/_questdb_nodejs-client.QWP_ZSTD_MIN_COMPRESSION_LEVEL.html
@@ -0,0 +1 @@
+QWP_ZSTD_MIN_COMPRESSION_LEVEL | QuestDB JavaScript Client - v4.2.0 Variable QWP_ZSTD_MIN_COMPRESSION_LEVELConst
QWP_ZSTD_MIN_COMPRESSION_LEVEL: 1
diff --git a/examples.manifest.yaml b/examples.manifest.yaml
index 428a4dd..bfdcaa9 100644
--- a/examples.manifest.yaml
+++ b/examples.manifest.yaml
@@ -2,12 +2,12 @@
lang: javascript
path: examples/basic.js
header: |-
- NodeJS client library [repo](https://github.com/questdb/nodejs-questdb-client).
+ JavaScript client library [repo](https://github.com/questdb/nodejs-questdb-client).
- name: ilp-auth
lang: javascript
path: examples/auth.js
header: |-
- NodeJS client library [repo](https://github.com/questdb/nodejs-questdb-client).
+ JavaScript client library [repo](https://github.com/questdb/nodejs-questdb-client).
auth:
kid: testapp
d: 9b9x5WhJywDEuo1KGQWSPNxtX-6X6R2BRCKhYMMY6n8
@@ -20,7 +20,7 @@
lang: javascript
path: examples/auth_tls.js
header: |-
- NodeJS client library [repo](https://github.com/questdb/nodejs-questdb-client).
+ JavaScript client library [repo](https://github.com/questdb/nodejs-questdb-client).
auth:
kid: testapp
d: 9b9x5WhJywDEuo1KGQWSPNxtX-6X6R2BRCKhYMMY6n8
@@ -33,5 +33,5 @@
lang: javascript
path: examples/basic.js
header: |-
- NodeJS client library [repo](https://github.com/questdb/nodejs-questdb-client).
+ JavaScript client library [repo](https://github.com/questdb/nodejs-questdb-client).
conf: http::addr=localhost:9000
diff --git a/examples/qwp-basic.ts b/examples/qwp-basic.ts
new file mode 100644
index 0000000..c0ba737
--- /dev/null
+++ b/examples/qwp-basic.ts
@@ -0,0 +1,19 @@
+import { Sender } from "@questdb/nodejs-client";
+
+async function main(): Promise {
+ const sender = await Sender.fromConfig("ws::addr=localhost:9000");
+ await sender.connect();
+ try {
+ await sender
+ .table("trades")
+ .symbol("symbol", "ETH-USD")
+ .floatColumn("price", 2_615.54)
+ .floatColumn("amount", 0.00044)
+ .at(Date.now(), "ms");
+ await sender.flush();
+ } finally {
+ await sender.close();
+ }
+}
+
+void main();
diff --git a/examples/qwp-browser.ts b/examples/qwp-browser.ts
new file mode 100644
index 0000000..30982c9
--- /dev/null
+++ b/examples/qwp-browser.ts
@@ -0,0 +1,15 @@
+import { connectQwpBrowserSender } from "@questdb/browser-client";
+
+async function main(): Promise {
+ const url = new URL("/write/v4", location.href);
+ url.protocol = location.protocol === "https:" ? "wss:" : "ws:";
+ const sender = await connectQwpBrowserSender({ url }, { autoFlush: false });
+ try {
+ await sender.table("events").longColumn("value", 42n).atNow();
+ await sender.flush();
+ } finally {
+ await sender.close();
+ }
+}
+
+void main();
diff --git a/package.json b/package.json
index 2290156..0d31261 100644
--- a/package.json
+++ b/package.json
@@ -1,58 +1,46 @@
{
- "name": "@questdb/nodejs-client",
+ "name": "questdb-javascript-client-workspace",
"version": "4.2.0",
- "description": "QuestDB Node.js Client",
+ "private": true,
+ "description": "QuestDB JavaScript Client workspace",
+ "packageManager": "pnpm@10.12.4",
"scripts": {
"test": "vitest",
- "build": "bunchee",
- "eslint": "eslint src/**",
+ "test:qwp-browser": "pnpm build && vitest run --config vitest.qwp-browser.config.ts",
+ "test:dist": "pnpm build && vitest run --config vitest.dist.config.ts",
+ "typecheck:dist": "pnpm build && tsc --noEmit -p tsconfig.dist-types.json && tsc --noEmit -p tsconfig.dist-types.cjs.json",
+ "build": "pnpm --filter @questdb/nodejs-client build && pnpm --filter @questdb/browser-client build",
+ "eslint": "eslint packages/*/src",
"typecheck": "tsc --noEmit",
- "format": "prettier --write '{src,test}/**/*.{ts,js,json}'",
- "docs": "typedoc --out docs src/index.ts",
+ "typecheck:qwp-browser": "tsc --noEmit -p packages/browser-client/tsconfig.json",
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
+ "bench": "vitest bench --run benchmarks",
+ "bench:e2e": "vitest run --config vitest.bench-e2e.config.ts",
+ "typecheck:bench": "tsc --noEmit -p tsconfig.bench.json",
+ "lint:bench": "eslint 'benchmarks/**/*.ts' vitest.bench-e2e.config.ts",
+ "format:bench": "prettier --write 'benchmarks/**/*.{ts,md}' tsconfig.bench.json vitest.bench-e2e.config.ts",
+ "format": "prettier --write '{packages,test}/**/*.{ts,js,json}'",
+ "check:packages": "node scripts/check-build-artifacts.mjs",
+ "docs": "typedoc",
"preview:docs": "serve docs"
},
- "files": [
- "dist/cjs",
- "dist/es"
- ],
- "main": "dist/cjs/index.js",
- "module": "dist/es/index.mjs",
- "types": "dist/cjs/index.d.ts",
- "exports": {
- "import": {
- "types": "./dist/es/index.d.mts",
- "default": "./dist/es/index.mjs"
- },
- "require": {
- "types": "./dist/cjs/index.d.ts",
- "default": "./dist/cjs/index.js"
- }
- },
- "repository": {
- "type": "git",
- "url": "git+ssh://git@github.com/questdb/nodejs-questdb-client.git"
- },
- "keywords": [
- "QuestDB"
- ],
- "author": "QuestDB",
- "license": "Apache-2.0",
- "homepage": "https://questdb.github.io/nodejs-questdb-client",
"devDependencies": {
"@eslint/js": "^9.16.0",
"@microsoft/tsdoc": "^0.15.1",
"@types/node": "^22.15.17",
+ "@types/ws": "^8.18.1",
"bunchee": "^6.5.1",
"eslint": "^9.26.0",
+ "playwright": "^1.62.1",
"prettier": "^3.5.3",
+ "rollup": "^4.40.2",
"serve": "^14.2.4",
"testcontainers": "^10.25.0",
"typedoc": "^0.28.9",
"typescript": "^5.7.2",
"typescript-eslint": "^8.32.0",
- "vitest": "^3.1.3"
- },
- "dependencies": {
- "undici": "^7.8.0"
+ "undici": "^7.8.0",
+ "vitest": "^3.1.3",
+ "ws": "^8.21.3"
}
}
diff --git a/packages/browser-client/README.md b/packages/browser-client/README.md
new file mode 100644
index 0000000..27e4f02
--- /dev/null
+++ b/packages/browser-client/README.md
@@ -0,0 +1,323 @@
+# QuestDB JavaScript Client for browsers
+
+The official browser-only QuestDB client. It provides QWP ingestion, streaming
+queries, failover, typed row writers, and browser session authentication without
+Node.js modules or polyfills.
+
+The complete browser API is exported from `@questdb/browser-client`. There are
+no additional public import paths.
+
+## Features
+
+- QWP ingestion through the browser's native WebSocket API
+- Streaming queries with typed bind variables and result batches
+- Automatic batching, reconnect, failover, and acknowledgement tracking
+- Transactional ingestion and durable acknowledgement negotiation
+- REST, OIDC, and Basic authentication through HttpOnly session cookies
+- ESM, CommonJS, and bundled TypeScript declarations
+- No Node.js built-ins, Node.js typings, `ws`, or `undici`
+
+## Requirements
+
+- A modern browser with `WebSocket`, `fetch`, `URL`, `TextEncoder`, and
+ `TextDecoder`
+- QuestDB QWP routes exposed at `/write/v4` and `/read/v1`
+- The `/exec` REST route when authentication bootstrap is needed
+
+This package does not contain the Node.js ILP transports; use
+`@questdb/nodejs-client` for server-side Node.js programs.
+
+## Installation
+
+```shell
+npm install @questdb/browser-client
+```
+
+```shell
+yarn add @questdb/browser-client
+```
+
+```shell
+pnpm add @questdb/browser-client
+```
+
+The package works with browser bundlers such as Vite, Rollup, webpack, and
+esbuild. Import only from the package root:
+
+```typescript
+import { connectQwpBrowserSender } from "@questdb/browser-client";
+```
+
+## Quick start: ingest from a browser
+
+Serve QuestDB's QWP route from the application's origin, either directly or
+through a reverse proxy. The browser will then apply the page's normal cookie,
+origin, and TLS rules to the WebSocket connection.
+
+```typescript
+import { connectQwpBrowserSender } from "@questdb/browser-client";
+
+const writeUrl = new URL("/write/v4", window.location.href);
+writeUrl.protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+
+const sender = await connectQwpBrowserSender(
+ { url: writeUrl },
+ { autoFlush: false },
+);
+
+try {
+ await sender
+ .table("page_events")
+ .symbol("kind", "view")
+ .stringColumn("path", window.location.pathname)
+ .timestampColumn("recorded_at", Date.now(), "ms")
+ .atNow();
+
+ await sender.flush();
+} finally {
+ await sender.close();
+}
+```
+
+Use `wss:` whenever the page is served over HTTPS. Browsers block insecure
+WebSockets from secure pages.
+
+## Batch and commit rows
+
+Transactional mode keeps automatically emitted frames in one open server-side
+transaction. `commit()` publishes the final frame. Transactions are atomic per
+table, not across every table in one flush.
+
+```typescript
+import { connectQwpBrowserSender } from "@questdb/browser-client";
+
+const sender = await connectQwpBrowserSender(
+ { url: writeUrl, requestDurableAck: true },
+ {
+ transactional: true,
+ autoFlushRows: 10_000,
+ awaitDurableAck: true,
+ durableAckTimeoutMs: 30_000,
+ },
+);
+
+try {
+ for (const event of [
+ { source: "checkout", value: 1n, timestamp: Date.now() },
+ { source: "search", value: 3n, timestamp: Date.now() },
+ ]) {
+ await sender
+ .table("events")
+ .symbol("source", event.source)
+ .longColumn("value", event.value)
+ .at(event.timestamp, "ms");
+ }
+
+ await sender.commit();
+} finally {
+ await sender.close();
+}
+```
+
+Browser replay is held in memory and survives reconnects only while the page is
+alive. Persistent store-and-forward is intentionally available only from the
+Node.js package.
+
+## Type-safe object rows
+
+Compile a table schema once when application data already has an object shape.
+TypeScript checks every row against the schema.
+
+```typescript
+import {
+ connectQwpBrowserSender,
+ designatedTimestamp,
+ double,
+ symbol,
+} from "@questdb/browser-client";
+
+const sender = await connectQwpBrowserSender({ url: writeUrl });
+
+try {
+ const measurements = sender.writer("measurements", {
+ device: symbol(),
+ temperature: double(),
+ timestamp: designatedTimestamp("ms"),
+ });
+
+ await measurements.rows([
+ { device: "sensor-1", temperature: 21.4, timestamp: Date.now() },
+ { device: "sensor-2", temperature: 22.1, timestamp: Date.now() },
+ ]);
+
+ await sender.flush();
+} finally {
+ await sender.close();
+}
+```
+
+The schema vocabulary also covers QuestDB integers, decimals, UUIDs, IPv4
+addresses, geohashes, binary values, and arrays.
+
+## Authentication
+
+Browser JavaScript cannot add an `Authorization` header to a WebSocket upgrade.
+Authenticate over REST first so QuestDB can set an HttpOnly session cookie. The
+browser then sends that cookie during the QWP WebSocket upgrade.
+
+```typescript
+import {
+ bootstrapQwpBrowserSession,
+ connectQwpBrowserSender,
+} from "@questdb/browser-client";
+
+await bootstrapQwpBrowserSession({
+ url: new URL("/exec", window.location.href),
+ authentication: {
+ type: "bearer",
+ token: oidcOrRestAccessToken,
+ },
+ // QuestDB Enterprise only; omit to use the authenticated principal.
+ serviceAccount: "market_data_writer",
+});
+
+const sender = await connectQwpBrowserSender({ url: writeUrl });
+```
+
+Basic authentication is also supported:
+
+```typescript
+const sender = await connectQwpBrowserSender({
+ url: writeUrl,
+ sessionBootstrap: {
+ authentication: {
+ type: "basic",
+ username: "admin",
+ password: "quest",
+ },
+ },
+});
+```
+
+Putting `sessionBootstrap` on the connection options repeats authentication
+before initial connection, reconnect, and failover attempts. The package does
+not run an interactive OIDC flow; the application obtains access tokens from
+its identity provider.
+
+The bootstrap request uses credentials. Prefer serving `/exec`, `/write/v4`,
+and `/read/v1` from the application's origin. Cross-origin deployments require
+credentialed CORS and cookie attributes that permit the browser to store and
+send the session cookie. JavaScript never reads the HttpOnly cookie.
+
+## Stream query results
+
+QWP egress streams typed result batches. A session runs one active query at a
+time and automatically reconnects and walks configured failover URLs.
+
+```typescript
+import { connectQwpBrowserEgress } from "@questdb/browser-client";
+
+const readUrl = new URL("/read/v1", window.location.href);
+readUrl.protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+
+const session = await connectQwpBrowserEgress(
+ {
+ url: readUrl,
+ compression: "zstd",
+ sessionBootstrap: {
+ authentication: { type: "bearer", token: oidcOrRestAccessToken },
+ },
+ },
+ { queryTimeoutMs: 30_000 },
+);
+
+try {
+ const query = await session.query(
+ "select timestamp, device, temperature " +
+ "from measurements where device = $1",
+ {
+ // Bind index 0 corresponds to SQL placeholder $1.
+ binds: (binds) => binds.setVarchar(0, "sensor-1"),
+ // A positive credit window bounds server read-ahead.
+ initialCredit: 1024 * 1024,
+ },
+ );
+
+ for await (const batch of query) {
+ for (const row of batch.rows()) {
+ console.log(row);
+ }
+ }
+
+ await query.completion;
+} finally {
+ await session.close();
+}
+```
+
+Use `queryViews()` for reusable zero-copy result views in allocation-sensitive
+applications. Copy any view that must outlive its batch callback.
+
+## Combined ingestion and query client
+
+`connectQwpBrowserClient()` creates bounded sender and query pools for an
+application component that needs concurrent ingestion and queries:
+
+```typescript
+import { connectQwpBrowserClient } from "@questdb/browser-client";
+
+const clusterUrl = new URL("/", window.location.href);
+clusterUrl.protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+
+const db = await connectQwpBrowserClient({
+ cluster: {
+ url: clusterUrl,
+ sessionBootstrap: {
+ authentication: { type: "bearer", token: oidcOrRestAccessToken },
+ },
+ },
+ ingress: { requestDurableAck: true },
+ egress: { target: "replica", compression: "zstd" },
+ pool: { senderPoolMax: 2, queryPoolMax: 4 },
+});
+
+try {
+ const sender = await db.borrowSender();
+ try {
+ await sender.table("events").symbol("kind", "view").atNow();
+ } finally {
+ // Flushes completed rows and returns the sender to the pool.
+ await sender.close();
+ }
+
+ const query = await db.borrowQuery();
+ try {
+ const result = await query.query("select count() from events");
+ for await (const batch of result) console.log([...batch.rows()]);
+ await result.completion;
+ } finally {
+ await query.close();
+ }
+} finally {
+ await db.close();
+}
+```
+
+## Error handling and shutdown
+
+- Always close senders, query sessions, borrowed pool handles, and pooled
+ clients in `finally` blocks.
+- Await asynchronous row completion methods such as `at()`, `atNow()`, and
+ writer `row()`/`rows()` calls.
+- An unfinished row is never completed implicitly during `flush()` or `close()`.
+- Do not share one sender between unrelated concurrent producers.
+- Re-executed queries are at least once after failover; clear already consumed
+ results in an `onReplayReset` callback when duplicate prefixes matter.
+
+## More documentation
+
+- [Complete repository README](https://github.com/questdb/nodejs-questdb-client#readme)
+- [QWP guide](https://github.com/questdb/nodejs-questdb-client/blob/main/QWP.md)
+- [Browser API reference](https://questdb.github.io/nodejs-questdb-client/modules/_questdb_browser-client.html)
+- [QuestDB documentation](https://questdb.com/docs/)
+- [QuestDB Community Forum](https://community.questdb.com/)
diff --git a/packages/browser-client/THIRD_PARTY_NOTICES.md b/packages/browser-client/THIRD_PARTY_NOTICES.md
new file mode 100644
index 0000000..9d10e92
--- /dev/null
+++ b/packages/browser-client/THIRD_PARTY_NOTICES.md
@@ -0,0 +1,23 @@
+# Third-party notices
+
+This product bundles `fzstd` 0.1.1, which is available under the MIT License:
+
+Copyright (c) 2020 Arjun Barrett
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/browser-client/package.json b/packages/browser-client/package.json
new file mode 100644
index 0000000..04444a6
--- /dev/null
+++ b/packages/browser-client/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@questdb/browser-client",
+ "version": "4.2.0",
+ "description": "QuestDB JavaScript Client for browsers",
+ "scripts": {
+ "build": "node ../../scripts/clean-package-dist.mjs browser-client && bunchee"
+ },
+ "files": [
+ "dist",
+ "README.md",
+ "THIRD_PARTY_NOTICES.md"
+ ],
+ "main": "dist/cjs/index.js",
+ "module": "dist/es/index.mjs",
+ "browser": "dist/es/index.mjs",
+ "types": "dist/cjs/index.d.ts",
+ "exports": {
+ ".": {
+ "browser": {
+ "types": "./dist/es/index.d.mts",
+ "default": "./dist/es/index.mjs"
+ },
+ "import": {
+ "types": "./dist/es/index.d.mts",
+ "default": "./dist/es/index.mjs"
+ },
+ "require": {
+ "types": "./dist/cjs/index.d.ts",
+ "default": "./dist/cjs/index.js"
+ }
+ }
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/questdb/nodejs-questdb-client.git",
+ "directory": "packages/browser-client"
+ },
+ "homepage": "https://questdb.github.io/nodejs-questdb-client",
+ "keywords": [
+ "QuestDB",
+ "browser",
+ "JavaScript",
+ "TypeScript"
+ ],
+ "author": "QuestDB",
+ "license": "Apache-2.0",
+ "dependencies": {}
+}
diff --git a/packages/browser-client/src/index.ts b/packages/browser-client/src/index.ts
new file mode 100644
index 0000000..4294135
--- /dev/null
+++ b/packages/browser-client/src/index.ts
@@ -0,0 +1,988 @@
+/**
+ * Browser WebSocket adapter and browser-safe QWP protocol/session APIs.
+ * @packageDocumentation
+ */
+export * from "../../client-core/src/qwp";
+
+import {
+ openQwpWebSocket,
+ QwpWebSocketLike,
+ validateQwpWebSocketTimeouts,
+} from "../../client-core/src/_qwp/_internal/websocket-connection";
+import { createQwpFailoverConnectionFactory } from "../../client-core/src/_qwp/_internal/failover";
+import { createQwpEgressFailoverConnectionFactory } from "../../client-core/src/_qwp/_internal/egress-routing";
+import { validateQwpMaxBatchRows } from "../../client-core/src/_qwp/_internal/egress-limits";
+import {
+ addQwpDurableAckWebSocketProtocol,
+ decodeQwpIngressServerInfo,
+ encodeQwpAcceptEncoding,
+ isQwpDurableAckWebSocketProtocol,
+ QwpEgressCompression,
+ QWP_VERSION,
+} from "../../client-core/src/_qwp/_core";
+import {
+ QwpBinaryConnection,
+ QwpConnectionFactory,
+ QwpDurableAckUnavailableError,
+ QwpEgressRoutingOptions,
+ QwpSendClosedError,
+ QWP_UPGRADE_ERROR_KIND,
+ QwpUpgradeError,
+ QwpWebSocketConnectOptions,
+} from "../../client-core/src/_qwp/transport";
+import {
+ QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS,
+ QwpEgressSession,
+ QwpEgressSessionOptions,
+} from "../../client-core/src/_qwp/egress-session";
+import {
+ QwpIngressSession,
+ QwpIngressSessionOptions,
+} from "../../client-core/src/_qwp/ingress-session";
+import { QwpSender, QwpSenderOptions } from "../../client-core/src/_qwp/sender";
+import {
+ QwpClient,
+ QwpClientPoolOptions,
+} from "../../client-core/src/_qwp/client";
+
+export type { QwpWebSocketLike } from "../../client-core/src/_qwp/_internal/websocket-connection";
+
+export type QwpBrowserSessionAuthentication =
+ | {
+ /** HTTP Basic authentication. */
+ type: "basic";
+ username: string;
+ password: string;
+ }
+ | {
+ /** QuestDB REST token or OIDC access token. */
+ type: "bearer";
+ token: string;
+ };
+
+export type QwpBrowserFetch = (
+ input: string | URL,
+ init?: RequestInit,
+) => Promise;
+
+export interface QwpBrowserSessionBootstrapOptions {
+ /** Exact QuestDB `/exec` HTTP(S) URL used to create the session cookie. */
+ url: string | URL;
+ authentication: QwpBrowserSessionAuthentication;
+ /** Optional Enterprise service account to assume for subsequent QWP use. */
+ serviceAccount?: string;
+ /** Cancels only the REST bootstrap request. */
+ signal?: AbortSignal;
+ /** Test or framework hook; defaults to the browser's global fetch. */
+ fetch?: QwpBrowserFetch;
+}
+
+export interface QwpBrowserSessionBootstrapResult {
+ readonly url: string;
+ readonly status: number;
+ readonly serviceAccount?: string;
+}
+
+export type QwpBrowserSessionBootstrapConfig = Omit<
+ QwpBrowserSessionBootstrapOptions,
+ "url"
+> & {
+ /** Defaults to `/exec` on the current QWP endpoint's HTTP origin. */
+ url?: string | URL;
+};
+
+/** An HTTP rejection while creating a browser `qdb_session` cookie. */
+export class QwpBrowserSessionBootstrapError extends QwpUpgradeError {
+ constructor(
+ readonly responseBody: string,
+ url: string | URL,
+ statusCode: number,
+ statusMessage: string,
+ ) {
+ const authenticationFailure = statusCode === 401 || statusCode === 403;
+ const suffix = statusMessage ? ` ${statusMessage}` : "";
+ const detail = responseBody ? `: ${responseBody}` : "";
+ super(
+ `QWP browser session bootstrap rejected with HTTP ${statusCode}${suffix}${detail}`,
+ {
+ kind: authenticationFailure
+ ? QWP_UPGRADE_ERROR_KIND.AUTHENTICATION
+ : QWP_UPGRADE_ERROR_KIND.HTTP_REJECTED,
+ retryable:
+ !authenticationFailure && (statusCode === 429 || statusCode >= 500),
+ tryNextEndpoint: !authenticationFailure,
+ url,
+ statusCode,
+ statusMessage,
+ },
+ );
+ this.name = "QwpBrowserSessionBootstrapError";
+ }
+}
+
+function validateAuthentication(
+ authentication: QwpBrowserSessionAuthentication,
+): void {
+ if (authentication.type === "basic") {
+ if (!authentication.username) {
+ throw new TypeError("browser session username cannot be empty");
+ }
+ if (authentication.username.includes(":")) {
+ throw new TypeError("browser session username cannot contain ':'");
+ }
+ if (/\r|\n/.test(authentication.username + authentication.password)) {
+ throw new TypeError(
+ "browser session credentials cannot contain CR or LF",
+ );
+ }
+ return;
+ }
+ if (authentication.type === "bearer") {
+ if (!authentication.token) {
+ throw new TypeError("browser session bearer token cannot be empty");
+ }
+ if (/\r|\n/.test(authentication.token)) {
+ throw new TypeError(
+ "browser session bearer token cannot contain CR or LF",
+ );
+ }
+ return;
+ }
+ throw new TypeError(
+ `unsupported browser session authentication type '${String((authentication as { type?: unknown }).type)}'`,
+ );
+}
+
+function encodeBase64Utf8(value: string): string {
+ const alphabet =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ const bytes = new TextEncoder().encode(value);
+ let result = "";
+ for (let index = 0; index < bytes.length; index += 3) {
+ const first = bytes[index];
+ const second = bytes[index + 1];
+ const third = bytes[index + 2];
+ result += alphabet[first >>> 2];
+ result += alphabet[((first & 0x03) << 4) | ((second ?? 0) >>> 4)];
+ result +=
+ second === undefined
+ ? "="
+ : alphabet[((second & 0x0f) << 2) | ((third ?? 0) >>> 6)];
+ result += third === undefined ? "=" : alphabet[third & 0x3f];
+ }
+ return result;
+}
+
+function authorizationHeader(
+ authentication: QwpBrowserSessionAuthentication,
+): string {
+ validateAuthentication(authentication);
+ return authentication.type === "basic"
+ ? `Basic ${encodeBase64Utf8(`${authentication.username}:${authentication.password}`)}`
+ : `Bearer ${authentication.token}`;
+}
+
+function resolveHttpUrl(value: string | URL): URL {
+ const base = globalThis.location?.href;
+ const url = value instanceof URL ? new URL(value) : new URL(value, base);
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
+ throw new TypeError(
+ `browser session bootstrap URL must use HTTP or HTTPS: ${url}`,
+ );
+ }
+ return url;
+}
+
+function serviceAccountSql(serviceAccount: string | undefined): string {
+ if (serviceAccount === undefined) return "select 1";
+ if (!serviceAccount.trim()) {
+ throw new TypeError("browser session serviceAccount cannot be empty");
+ }
+ return `assume service account '${serviceAccount.replace(/'/g, "''")}'`;
+}
+
+function defaultBootstrapUrl(endpoint: string | URL): URL {
+ const base = globalThis.location?.href;
+ const url =
+ endpoint instanceof URL ? new URL(endpoint) : new URL(endpoint, base);
+ if (url.protocol === "ws:") url.protocol = "http:";
+ else if (url.protocol === "wss:") url.protocol = "https:";
+ else {
+ throw new TypeError(`QWP browser URL must use WS or WSS: ${url}`);
+ }
+ const suffix = /\/(?:write\/v4|read\/v1)\/?$/;
+ url.pathname = suffix.test(url.pathname)
+ ? url.pathname.replace(suffix, "/exec")
+ : "/exec";
+ url.search = "";
+ url.hash = "";
+ return url;
+}
+
+/**
+ * Authenticates over REST and asks QuestDB to issue the HttpOnly cookies a
+ * browser needs before opening QWP WebSockets. REST and OIDC tokens both use
+ * Bearer authentication. When `serviceAccount` is present the same request
+ * also creates Enterprise's `qdbServiceAccount` impersonation cookie.
+ */
+export async function bootstrapQwpBrowserSession(
+ options: QwpBrowserSessionBootstrapOptions,
+): Promise {
+ const requestUrl = resolveHttpUrl(options.url);
+ requestUrl.searchParams.set(
+ "query",
+ serviceAccountSql(options.serviceAccount),
+ );
+ requestUrl.searchParams.set("session", "true");
+ requestUrl.hash = "";
+ const fetcher = options.fetch ?? globalThis.fetch;
+ if (!fetcher) {
+ throw new Error("fetch is not available in this browser runtime");
+ }
+ const response = await fetcher(requestUrl, {
+ method: "GET",
+ credentials: "include",
+ headers: {
+ Accept: "application/json",
+ Authorization: authorizationHeader(options.authentication),
+ "Cache-Control": "no-store",
+ },
+ signal: options.signal,
+ });
+ let responseBody = "";
+ try {
+ responseBody = await response.text();
+ } catch (error) {
+ if (response.ok) {
+ return {
+ url: requestUrl.toString(),
+ status: response.status,
+ serviceAccount: options.serviceAccount,
+ };
+ }
+ responseBody = error instanceof Error ? error.message : String(error);
+ }
+ if (!response.ok) {
+ throw new QwpBrowserSessionBootstrapError(
+ responseBody.slice(0, 1_024),
+ requestUrl,
+ response.status,
+ response.statusText,
+ );
+ }
+ return {
+ url: requestUrl.toString(),
+ status: response.status,
+ serviceAccount: options.serviceAccount,
+ };
+}
+
+export interface QwpBrowserWebSocketOptions extends QwpWebSocketConnectOptions {
+ /**
+ * Requests durable ingress ACKs through browser-visible WebSocket
+ * subprotocol negotiation.
+ */
+ requestDurableAck?: boolean;
+ /**
+ * Time allowed for the optional ingress SERVER_INFO message. Defaults to
+ * 250ms; zero disables the initial wait while retaining late negotiation.
+ */
+ ingressNegotiationTimeoutMs?: number;
+ /**
+ * Authenticates over REST before every WebSocket connection attempt so the
+ * browser can attach QuestDB's HttpOnly session cookies to the upgrade.
+ */
+ sessionBootstrap?: QwpBrowserSessionBootstrapConfig;
+ /** Test or framework hook; defaults to the browser's global WebSocket. */
+ webSocketFactory?: (
+ url: string | URL,
+ protocols?: string | string[],
+ ) => QwpWebSocketLike;
+}
+
+/** Browser WebSocket options plus protocol-level egress topology routing. */
+export interface QwpBrowserEgressOptions
+ extends QwpBrowserWebSocketOptions,
+ QwpEgressRoutingOptions {
+ /**
+ * Requests Zstd-compressed result batches through browser-visible URL
+ * negotiation. Defaults to raw for compatibility.
+ */
+ compression?: QwpEgressCompression;
+ /** Zstd level hint. Must be between 1 and 22. */
+ compressionLevel?: number;
+ /** Requests a server-side RESULT_BATCH row cap. */
+ maxBatchRows?: number;
+}
+
+/** Shared browser transport and authentication for one QWP cluster. */
+export interface QwpBrowserClusterOptions extends QwpWebSocketConnectOptions {
+ /**
+ * Authenticates before every connection attempt. When `url` is omitted from
+ * this bootstrap, its REST endpoint follows the active cluster endpoint.
+ */
+ sessionBootstrap?: QwpBrowserSessionBootstrapConfig;
+ /** Shared test or framework hook; either side may override it. */
+ webSocketFactory?: (
+ url: string | URL,
+ protocols?: string | string[],
+ ) => QwpWebSocketLike;
+}
+
+/** Ingress-only overrides for a unified browser cluster. */
+export type QwpBrowserClientIngressOptions = Partial<
+ Pick<
+ QwpBrowserWebSocketOptions,
+ | "protocols"
+ | "connectTimeoutMs"
+ | "sendTimeoutMs"
+ | "closeTimeoutMs"
+ | "requestDurableAck"
+ | "ingressNegotiationTimeoutMs"
+ | "webSocketFactory"
+ >
+>;
+
+/** Egress-only overrides for a unified browser cluster. */
+export type QwpBrowserClientEgressOptions = Partial<
+ Pick<
+ QwpBrowserEgressOptions,
+ | "protocols"
+ | "connectTimeoutMs"
+ | "sendTimeoutMs"
+ | "closeTimeoutMs"
+ | "webSocketFactory"
+ | "target"
+ | "zone"
+ | "compression"
+ | "compressionLevel"
+ | "maxBatchRows"
+ >
+>;
+
+interface QwpBrowserClientBaseOptions {
+ sender?: QwpSenderOptions;
+ ingressSession?: QwpIngressSessionOptions;
+ egressSession?: QwpEgressSessionOptions;
+ pool?: QwpClientPoolOptions;
+}
+
+/**
+ * Recommended combined-browser form. One endpoint list and authentication
+ * bootstrap are shared while side-specific protocol options remain explicit.
+ */
+export interface QwpBrowserUnifiedClientOptions
+ extends QwpBrowserClientBaseOptions {
+ cluster: QwpBrowserClusterOptions;
+ ingress?: QwpBrowserClientIngressOptions;
+ egress?: QwpBrowserClientEgressOptions;
+}
+
+/** Backwards-compatible form with completely independent connection trees. */
+export interface QwpBrowserSplitClientOptions
+ extends QwpBrowserClientBaseOptions {
+ cluster?: never;
+ ingress: QwpBrowserWebSocketOptions;
+ egress: QwpBrowserEgressOptions;
+}
+
+/** Browser configuration for a combined pooled QWP ingress/egress client. */
+export type QwpBrowserClientOptions =
+ | QwpBrowserUnifiedClientOptions
+ | QwpBrowserSplitClientOptions;
+
+interface QwpResolvedBrowserClientOptions extends QwpBrowserClientBaseOptions {
+ ingress: QwpBrowserWebSocketOptions;
+ egress: QwpBrowserEgressOptions;
+}
+
+const DEFAULT_BROWSER_CONNECT_TIMEOUT_MS = 15_000;
+
+function composeBrowserAbortSignals(
+ signals: readonly (AbortSignal | undefined)[],
+): { signal: AbortSignal; dispose: () => void } {
+ const controller = new AbortController();
+ const listeners: { signal: AbortSignal; listener: () => void }[] = [];
+ for (const signal of signals) {
+ if (!signal) continue;
+ if (signal.aborted) {
+ controller.abort();
+ break;
+ }
+ const listener = (): void => controller.abort();
+ signal.addEventListener("abort", listener, { once: true });
+ listeners.push({ signal, listener });
+ }
+ return {
+ signal: controller.signal,
+ dispose: () => {
+ for (const entry of listeners) {
+ entry.signal.removeEventListener("abort", entry.listener);
+ }
+ },
+ };
+}
+
+/**
+ * Opens a QWP-capable browser WebSocket.
+ *
+ * Browsers cannot set Authorization or X-QWP-* upgrade headers. QuestDB accepts
+ * browser upgrades when Origin and Host have the same authority, so serve the
+ * app from the QuestDB origin or route QWP through a same-origin reverse proxy.
+ * When authentication is enabled, pass sessionBootstrap or call
+ * bootstrapQwpBrowserSession first so the browser can attach qdb_session.
+ */
+export function connectQwpBrowserWebSocket(
+ options: QwpBrowserWebSocketOptions,
+): Promise {
+ return createQwpFailoverConnectionFactory(
+ options.url,
+ options.failoverUrls,
+ (endpoint, signal) =>
+ connectQwpBrowserRawEndpoint(options, endpoint, signal),
+ )();
+}
+
+/** Creates a stateful browser endpoint walker suitable for session reconnects. */
+export function createQwpBrowserConnectionFactory(
+ options: QwpBrowserWebSocketOptions,
+): QwpConnectionFactory {
+ return createQwpFailoverConnectionFactory(
+ options.url,
+ options.failoverUrls,
+ (endpoint, signal) =>
+ connectQwpBrowserIngressEndpoint(options, endpoint, signal),
+ );
+}
+
+async function connectQwpBrowserEndpoint(
+ options: QwpBrowserWebSocketOptions,
+ endpoint: string | URL,
+ requestEndpoint: string | URL,
+ protocols: string | string[] | undefined,
+ signal: AbortSignal | undefined,
+ completeHandshake: (
+ selectedProtocol: string | undefined,
+ ) => QwpBinaryConnection["handshake"],
+ finishOpening: (
+ connection: QwpBinaryConnection,
+ ) => Promise = async (connection) => connection,
+): Promise {
+ validateQwpWebSocketTimeouts(options);
+ const connectTimeoutMs =
+ options.connectTimeoutMs ?? DEFAULT_BROWSER_CONNECT_TIMEOUT_MS;
+ const openingAbort = new AbortController();
+ let openedConnection: QwpBinaryConnection | undefined;
+ let deadlineTimer: ReturnType | undefined;
+ let rejectBoundary!: (error: Error) => void;
+ let boundarySettled = false;
+ const failBoundary = (error: Error, reason: string): void => {
+ if (boundarySettled) return;
+ boundarySettled = true;
+ rejectBoundary(error);
+ openingAbort.abort();
+ void openedConnection?.close(1000, reason).catch(() => undefined);
+ };
+ const boundary = new Promise((_resolve, reject) => {
+ rejectBoundary = reject;
+ });
+ const abortOpening = (): void => {
+ failBoundary(
+ new QwpSendClosedError(),
+ "QWP connection closed while connecting",
+ );
+ };
+ if (signal?.aborted) abortOpening();
+ else signal?.addEventListener("abort", abortOpening, { once: true });
+ if (!boundarySettled) {
+ deadlineTimer = setTimeout(() => {
+ failBoundary(
+ new QwpUpgradeError(
+ `QWP WebSocket connection timed out after ${connectTimeoutMs}ms`,
+ {
+ kind: QWP_UPGRADE_ERROR_KIND.TIMEOUT,
+ retryable: true,
+ tryNextEndpoint: true,
+ url: endpoint,
+ },
+ ),
+ "QWP connection timeout",
+ );
+ }, connectTimeoutMs);
+ }
+
+ const opening = (async (): Promise => {
+ if (options.sessionBootstrap) {
+ const bootstrapAbort = composeBrowserAbortSignals([
+ openingAbort.signal,
+ options.sessionBootstrap.signal,
+ ]);
+ try {
+ await bootstrapQwpBrowserSession({
+ ...options.sessionBootstrap,
+ url: options.sessionBootstrap.url ?? defaultBootstrapUrl(endpoint),
+ signal: bootstrapAbort.signal,
+ });
+ } finally {
+ bootstrapAbort.dispose();
+ }
+ }
+ if (openingAbort.signal.aborted) throw new QwpSendClosedError();
+ const factory =
+ options.webSocketFactory ??
+ ((url: string | URL, protocols?: string | string[]) => {
+ const WebSocketConstructor = (
+ globalThis as unknown as {
+ WebSocket?: new (
+ url: string | URL,
+ protocols?: string | string[],
+ ) => QwpWebSocketLike;
+ }
+ ).WebSocket;
+ if (!WebSocketConstructor) {
+ throw new Error("WebSocket is not available in this browser runtime");
+ }
+ return new WebSocketConstructor(url, protocols);
+ });
+ const socket = factory(requestEndpoint, protocols);
+ openedConnection = await openQwpWebSocket(socket, {
+ signal: openingAbort.signal,
+ url: endpoint,
+ connectTimeoutMs,
+ sendTimeoutMs: options.sendTimeoutMs,
+ closeTimeoutMs: options.closeTimeoutMs,
+ completeHandshake: () => completeHandshake(socket.protocol),
+ opaqueErrors: true,
+ });
+ return finishOpening(openedConnection);
+ })();
+
+ try {
+ const connection = await Promise.race([opening, boundary]);
+ boundarySettled = true;
+ return connection;
+ } finally {
+ boundarySettled = true;
+ if (deadlineTimer) clearTimeout(deadlineTimer);
+ signal?.removeEventListener("abort", abortOpening);
+ }
+}
+
+function browserNegotiationUrl(
+ endpoint: string | URL,
+ name: string,
+ value: string,
+): URL {
+ const url =
+ endpoint instanceof URL
+ ? new URL(endpoint)
+ : new URL(endpoint, globalThis.location?.href);
+ url.searchParams.set(name, value);
+ return url;
+}
+
+function connectQwpBrowserRawEndpoint(
+ options: QwpBrowserWebSocketOptions,
+ endpoint: string | URL,
+ signal?: AbortSignal,
+): Promise {
+ const protocols = options.requestDurableAck
+ ? addQwpDurableAckWebSocketProtocol(options.protocols)
+ : options.protocols;
+ return connectQwpBrowserEndpoint(
+ options,
+ endpoint,
+ endpoint,
+ protocols,
+ signal,
+ (selectedProtocol) => {
+ const durableAckEnabled =
+ isQwpDurableAckWebSocketProtocol(selectedProtocol);
+ if (options.requestDurableAck && !durableAckEnabled) {
+ throw new QwpDurableAckUnavailableError(endpoint);
+ }
+ return durableAckEnabled
+ ? { qwpVersion: QWP_VERSION, durableAckEnabled: true }
+ : { qwpVersion: QWP_VERSION };
+ },
+ );
+}
+
+async function applyQwpBrowserIngressHandshake(
+ connection: QwpBinaryConnection,
+ timeoutMs: number,
+): Promise {
+ const iterator = connection.messages[Symbol.asyncIterator]();
+ const pendingFirst = iterator.next();
+ const timeout = Symbol("QWP browser ingress negotiation timeout");
+ let timer: ReturnType | undefined;
+ const outcome =
+ timeoutMs === 0
+ ? timeout
+ : await Promise.race([
+ pendingFirst,
+ new Promise((resolve) => {
+ timer = setTimeout(resolve, timeoutMs, timeout);
+ }),
+ ]);
+ if (timer !== undefined) clearTimeout(timer);
+
+ const handshake: {
+ qwpVersion: number;
+ maxBatchSizeBytes?: number;
+ contentEncoding?: string;
+ negotiatedCompression?: QwpBinaryConnection["handshake"]["negotiatedCompression"];
+ durableAckEnabled?: boolean;
+ serverRole?: string;
+ serverZone?: string;
+ } = { ...connection.handshake };
+ let firstResult: IteratorResult | undefined;
+ let pendingResult: Promise> | undefined;
+ if (outcome === timeout) {
+ pendingResult = pendingFirst;
+ } else if (!outcome.done) {
+ const maxBatchSizeBytes = decodeQwpIngressServerInfo(outcome.value);
+ if (maxBatchSizeBytes === undefined) firstResult = outcome;
+ else handshake.maxBatchSizeBytes = maxBatchSizeBytes;
+ }
+
+ const messages: AsyncIterable = {
+ async *[Symbol.asyncIterator]() {
+ let result =
+ firstResult ??
+ (pendingResult === undefined
+ ? await iterator.next()
+ : await pendingResult);
+ while (!result.done) {
+ const maxBatchSizeBytes = decodeQwpIngressServerInfo(result.value);
+ if (maxBatchSizeBytes === undefined) yield result.value;
+ else handshake.maxBatchSizeBytes = maxBatchSizeBytes;
+ result = await iterator.next();
+ }
+ },
+ };
+
+ return {
+ messages,
+ handshake,
+ closed: connection.closed,
+ endpoint: connection.endpoint,
+ ingressSymbolDictionary: connection.ingressSymbolDictionary,
+ ingressDeltaSymbolDictionaryEnabled:
+ connection.ingressDeltaSymbolDictionaryEnabled,
+ getIngressMetrics: connection.getIngressMetrics
+ ? () => connection.getIngressMetrics!()
+ : undefined,
+ send: (payload) => connection.send(payload),
+ ping: connection.ping ? () => connection.ping!() : undefined,
+ close: (code, reason) => connection.close(code, reason),
+ };
+}
+
+async function connectQwpBrowserIngressEndpoint(
+ options: QwpBrowserWebSocketOptions,
+ endpoint: string | URL,
+ signal?: AbortSignal,
+): Promise {
+ const timeoutMs = options.ingressNegotiationTimeoutMs ?? 250;
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
+ throw new RangeError(
+ "ingressNegotiationTimeoutMs must be a non-negative finite number",
+ );
+ }
+ return connectQwpBrowserEndpoint(
+ options,
+ endpoint,
+ browserNegotiationUrl(endpoint, "qwp_browser_handshake", "v1"),
+ options.requestDurableAck
+ ? addQwpDurableAckWebSocketProtocol(options.protocols)
+ : options.protocols,
+ signal,
+ (selectedProtocol) => {
+ const durableAckEnabled =
+ isQwpDurableAckWebSocketProtocol(selectedProtocol);
+ if (options.requestDurableAck && !durableAckEnabled) {
+ throw new QwpDurableAckUnavailableError(endpoint);
+ }
+ return durableAckEnabled
+ ? { qwpVersion: QWP_VERSION, durableAckEnabled: true }
+ : { qwpVersion: QWP_VERSION };
+ },
+ async (connection) => {
+ try {
+ return await applyQwpBrowserIngressHandshake(connection, timeoutMs);
+ } catch (error) {
+ await connection
+ .close(1002, "invalid QWP ingress SERVER_INFO")
+ .catch(() => undefined);
+ throw error;
+ }
+ },
+ );
+}
+
+function connectQwpBrowserEgressEndpoint(
+ options: QwpBrowserEgressOptions,
+ endpoint: string | URL,
+ signal?: AbortSignal,
+): Promise {
+ const compression = options.compression ?? "raw";
+ const acceptEncoding = encodeQwpAcceptEncoding(
+ compression,
+ options.compressionLevel ?? 1,
+ );
+ const maxBatchRows = validateQwpMaxBatchRows(options.maxBatchRows);
+ let requestEndpoint: string | URL = endpoint;
+ if (acceptEncoding !== undefined) {
+ requestEndpoint = browserNegotiationUrl(
+ requestEndpoint,
+ "qwp_accept_encoding",
+ acceptEncoding,
+ );
+ }
+ if (maxBatchRows !== undefined) {
+ requestEndpoint = browserNegotiationUrl(
+ requestEndpoint,
+ "qwp_max_batch_rows",
+ String(maxBatchRows),
+ );
+ }
+ return connectQwpBrowserEndpoint(
+ options,
+ endpoint,
+ requestEndpoint,
+ options.protocols,
+ signal,
+ () => ({
+ qwpVersion: QWP_VERSION,
+ negotiatedCompression: { codec: "raw", level: 0 },
+ }),
+ );
+}
+
+/** Opens a browser WebSocket and starts an ingress ACK/NACK session. */
+export async function connectQwpBrowserIngress(
+ options: QwpBrowserWebSocketOptions,
+ sessionOptions: QwpIngressSessionOptions = {},
+ /** Cancels a first connect still negotiating; see QwpIngressSession.connect. */
+ signal?: AbortSignal,
+): Promise {
+ if (
+ sessionOptions.durableAckKeepaliveMs !== undefined &&
+ options.requestDurableAck !== true
+ ) {
+ throw new RangeError(
+ "durableAckKeepaliveMs requires requestDurableAck=true for browser ingress",
+ );
+ }
+ const effectiveSessionOptions: QwpIngressSessionOptions = {
+ ...sessionOptions,
+ durableAckKeepaliveMs: options.requestDurableAck
+ ? (sessionOptions.durableAckKeepaliveMs ?? 200)
+ : sessionOptions.durableAckKeepaliveMs,
+ };
+ return QwpIngressSession.connect(
+ createQwpBrowserConnectionFactory(options),
+ effectiveSessionOptions,
+ signal,
+ );
+}
+
+/**
+ * Creates a browser-safe fluent QWP sender without opening the WebSocket yet.
+ * Call connect(), or let the first flush connect lazily.
+ */
+export function createQwpBrowserSender(
+ options: QwpBrowserWebSocketOptions,
+ senderOptions: QwpSenderOptions = {},
+ sessionOptions: QwpIngressSessionOptions = {},
+): QwpSender {
+ return new QwpSender(
+ (signal) =>
+ connectQwpBrowserIngress(
+ {
+ ...options,
+ requestDurableAck:
+ options.requestDurableAck ?? senderOptions.awaitDurableAck,
+ },
+ sessionOptions,
+ signal,
+ ),
+ senderOptions,
+ );
+}
+
+/** Opens a browser QWP connection and returns a fluent sender. */
+export async function connectQwpBrowserSender(
+ options: QwpBrowserWebSocketOptions,
+ senderOptions: QwpSenderOptions = {},
+ sessionOptions: QwpIngressSessionOptions = {},
+): Promise {
+ const sender = createQwpBrowserSender(options, senderOptions, sessionOptions);
+ await sender.connect();
+ return sender;
+}
+
+/** Opens a browser WebSocket and waits for the egress SERVER_INFO handshake. */
+export async function connectQwpBrowserEgress(
+ options: QwpBrowserEgressOptions,
+ sessionOptions: QwpEgressSessionOptions = {},
+ /** Cancels an opening connection during pooled-client shutdown. */
+ signal?: AbortSignal,
+): Promise {
+ return QwpEgressSession.connect(
+ createQwpEgressFailoverConnectionFactory(
+ options.url,
+ options.failoverUrls,
+ (endpoint, signal) =>
+ connectQwpBrowserEgressEndpoint(options, endpoint, signal),
+ { target: options.target, zone: options.zone },
+ sessionOptions.serverInfoTimeoutMs ??
+ QWP_DEFAULT_EGRESS_SERVER_INFO_TIMEOUT_MS,
+ ),
+ sessionOptions,
+ signal,
+ );
+}
+
+const CLUSTER_OWNED_BROWSER_OPTION_NAMES = [
+ "url",
+ "failoverUrls",
+ "sessionBootstrap",
+] as const;
+
+function assertNoBrowserClusterOptionConflicts(
+ side: "ingress" | "egress",
+ options: object | undefined,
+): void {
+ if (!options) return;
+ for (const name of CLUSTER_OWNED_BROWSER_OPTION_NAMES) {
+ if (Object.prototype.hasOwnProperty.call(options, name)) {
+ throw new TypeError(
+ `conflicting browser client configuration: ${side}.${name} must be configured once under cluster.${name}`,
+ );
+ }
+ }
+}
+
+function browserClusterEndpoint(
+ endpoint: string | URL,
+ route: "write/v4" | "read/v1",
+): URL {
+ const url =
+ endpoint instanceof URL
+ ? new URL(endpoint)
+ : new URL(endpoint, globalThis.location?.href);
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") {
+ throw new TypeError(`QWP browser cluster URL must use WS or WSS: ${url}`);
+ }
+ if (url.hash) {
+ throw new TypeError(
+ `QWP browser cluster URL cannot contain a fragment: ${url}`,
+ );
+ }
+ const qwpRoute = /\/(?:write\/v4|read\/v1)\/?$/;
+ if (qwpRoute.test(url.pathname)) {
+ url.pathname = url.pathname.replace(qwpRoute, `/${route}`);
+ } else {
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/${route}`;
+ }
+ return url;
+}
+
+function resolveQwpBrowserClientOptions(
+ options: QwpBrowserClientOptions,
+): QwpResolvedBrowserClientOptions {
+ if ("cluster" in options && options.cluster !== undefined) {
+ assertNoBrowserClusterOptionConflicts("ingress", options.ingress);
+ assertNoBrowserClusterOptionConflicts("egress", options.egress);
+ const { url, failoverUrls, ...shared } = options.cluster;
+ const ingress: QwpBrowserWebSocketOptions = {
+ ...shared,
+ ...options.ingress,
+ url: browserClusterEndpoint(url, "write/v4"),
+ failoverUrls: failoverUrls?.map((endpoint) =>
+ browserClusterEndpoint(endpoint, "write/v4"),
+ ),
+ };
+ const egress: QwpBrowserEgressOptions = {
+ ...shared,
+ ...options.egress,
+ url: browserClusterEndpoint(url, "read/v1"),
+ failoverUrls: failoverUrls?.map((endpoint) =>
+ browserClusterEndpoint(endpoint, "read/v1"),
+ ),
+ };
+ return {
+ ingress,
+ egress,
+ sender: options.sender,
+ ingressSession: options.ingressSession,
+ egressSession: options.egressSession,
+ pool: options.pool,
+ };
+ }
+ if (!options.ingress || !options.egress) {
+ throw new TypeError(
+ "browser client configuration requires either cluster or both ingress and egress",
+ );
+ }
+ const split = options as QwpBrowserSplitClientOptions;
+ return {
+ ingress: split.ingress,
+ egress: split.egress,
+ sender: split.sender,
+ ingressSession: split.ingressSession,
+ egressSession: split.egressSession,
+ pool: split.pool,
+ };
+}
+
+/** Creates a lazy browser QWP client with bounded sender and query pools. */
+export function createQwpBrowserClient(
+ options: QwpBrowserClientOptions,
+): QwpClient {
+ const resolved = resolveQwpBrowserClientOptions(options);
+ return new QwpClient(
+ {
+ createSender: async (_slot, signal) => {
+ const sender = createQwpBrowserSender(
+ resolved.ingress,
+ resolved.sender,
+ resolved.ingressSession,
+ );
+ const abortOpening = (): void => {
+ void sender.close().catch(() => undefined);
+ };
+ try {
+ if (signal?.aborted) throw new QwpSendClosedError();
+ signal?.addEventListener("abort", abortOpening, { once: true });
+ await sender.connect();
+ if (signal?.aborted) throw new QwpSendClosedError();
+ return sender;
+ } catch (error) {
+ await sender.close().catch(() => undefined);
+ throw error;
+ } finally {
+ signal?.removeEventListener("abort", abortOpening);
+ }
+ },
+ createQuerySession: (_slot, signal) =>
+ connectQwpBrowserEgress(
+ resolved.egress,
+ resolved.egressSession,
+ signal,
+ ),
+ },
+ resolved.pool,
+ );
+}
+
+/** Creates and prewarms a combined browser QWP ingress/egress client. */
+export async function connectQwpBrowserClient(
+ options: QwpBrowserClientOptions,
+): Promise {
+ const client = createQwpBrowserClient(options);
+ await client.connect();
+ return client;
+}
diff --git a/packages/browser-client/tsconfig.json b/packages/browser-client/tsconfig.json
new file mode 100644
index 0000000..ed3f5f3
--- /dev/null
+++ b/packages/browser-client/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../../tsconfig.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "moduleResolution": "bundler",
+ "module": "ESNext",
+ "target": "ES2020",
+ "lib": ["ES2020", "DOM"],
+ "types": [],
+ "strict": true
+ }
+}
diff --git a/packages/browser-client/typedoc.json b/packages/browser-client/typedoc.json
new file mode 100644
index 0000000..4e1e6a6
--- /dev/null
+++ b/packages/browser-client/typedoc.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://typedoc.org/schema.json",
+ "entryPoints": ["./src/index.ts"],
+ "tsconfig": "./tsconfig.json"
+}
diff --git a/packages/client-core/package.json b/packages/client-core/package.json
new file mode 100644
index 0000000..7a97a58
--- /dev/null
+++ b/packages/client-core/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@questdb/client-core",
+ "version": "4.2.0",
+ "private": true,
+ "description": "Private shared implementation for the QuestDB JavaScript clients",
+ "dependencies": {
+ "fzstd": "0.1.1"
+ }
+}
diff --git a/packages/client-core/src/_qwp/_core/binds.ts b/packages/client-core/src/_qwp/_core/binds.ts
new file mode 100644
index 0000000..fbaeb7b
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/binds.ts
@@ -0,0 +1,482 @@
+import { encodeUtf8, QwpByteWriter } from "./bytes";
+import { QWP_COLUMN_TYPE, QWP_MAX_COLUMNS_PER_TABLE } from "./constants";
+import { writeQwpVarint } from "./varint";
+
+const INT64_MIN = -(1n << 63n);
+const INT64_MAX = (1n << 63n) - 1n;
+const UINT64_MAX = (1n << 64n) - 1n;
+const DECIMAL64_MAX_SCALE = 18;
+const DECIMAL128_MAX_SCALE = 38;
+const DECIMAL256_MAX_SCALE = 76;
+const GEOHASH_MIN_BITS = 1;
+const GEOHASH_MAX_BITS = 60;
+const NULL_FLAG = 0x01;
+const NULL_BITMAP = 0x01;
+const NON_NULL_FLAG = 0x00;
+
+export type QwpInt64 = number | bigint;
+
+/** Phase-1 scalar bind types exposed by the Java reference client. */
+export type QwpBindType =
+ | typeof QWP_COLUMN_TYPE.BOOLEAN
+ | typeof QWP_COLUMN_TYPE.BYTE
+ | typeof QWP_COLUMN_TYPE.SHORT
+ | typeof QWP_COLUMN_TYPE.INT
+ | typeof QWP_COLUMN_TYPE.LONG
+ | typeof QWP_COLUMN_TYPE.FLOAT
+ | typeof QWP_COLUMN_TYPE.DOUBLE
+ | typeof QWP_COLUMN_TYPE.TIMESTAMP
+ | typeof QWP_COLUMN_TYPE.DATE
+ | typeof QWP_COLUMN_TYPE.UUID
+ | typeof QWP_COLUMN_TYPE.LONG256
+ | typeof QWP_COLUMN_TYPE.GEOHASH
+ | typeof QWP_COLUMN_TYPE.VARCHAR
+ | typeof QWP_COLUMN_TYPE.TIMESTAMP_NANOS
+ | typeof QWP_COLUMN_TYPE.DECIMAL64
+ | typeof QWP_COLUMN_TYPE.DECIMAL128
+ | typeof QWP_COLUMN_TYPE.DECIMAL256
+ | typeof QWP_COLUMN_TYPE.CHAR;
+
+export type QwpBindSetter = (binds: QwpBindValues) => void;
+
+export interface QwpEncodedBinds {
+ count: number;
+ payload: Uint8Array;
+}
+
+function checkedIndex(value: number): number {
+ if (!Number.isSafeInteger(value) || value < 0) {
+ throw new RangeError("bind index must be a non-negative safe integer");
+ }
+ return value;
+}
+
+function checkedInteger(
+ value: number,
+ minimum: number,
+ maximum: number,
+ label: string,
+): number {
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
+ throw new RangeError(
+ `${label} must be an integer between ${minimum} and ${maximum}`,
+ );
+ }
+ return value;
+}
+
+function checkedInt64(value: QwpInt64, label: string): bigint {
+ let integer: bigint;
+ if (typeof value === "number") {
+ if (!Number.isSafeInteger(value)) {
+ throw new RangeError(`${label} must be a safe integer or bigint`);
+ }
+ integer = BigInt(value);
+ } else if (typeof value === "bigint") {
+ integer = value;
+ } else {
+ throw new TypeError(`${label} must be a safe integer or bigint`);
+ }
+ if (integer < INT64_MIN || integer > INT64_MAX) {
+ throw new RangeError(`${label} must fit in int64`);
+ }
+ return integer;
+}
+
+function checkedUint64Bits(value: QwpInt64, label: string): bigint {
+ let integer: bigint;
+ if (typeof value === "number") {
+ if (!Number.isSafeInteger(value)) {
+ throw new RangeError(`${label} must be a safe integer or bigint`);
+ }
+ integer = BigInt(value);
+ } else if (typeof value === "bigint") {
+ integer = value;
+ } else {
+ throw new TypeError(`${label} must be a safe integer or bigint`);
+ }
+ if (integer < INT64_MIN || integer > UINT64_MAX) {
+ throw new RangeError(`${label} must fit in 64 bits`);
+ }
+ return BigInt.asUintN(64, integer);
+}
+
+function checkedScale(value: number, maximum: number, label: string): number {
+ return checkedInteger(value, 0, maximum, `${label} scale`);
+}
+
+/**
+ * Browser-safe typed positional bind encoder.
+ *
+ * Setters must be called in ascending zero-based index order. SQL placeholders
+ * are one-based, so index 0 binds `$1`, index 1 binds `$2`, and so on.
+ */
+export class QwpBindValues {
+ private writer = new QwpByteWriter();
+ private expectedIndex = 0;
+
+ get count(): number {
+ return this.expectedIndex;
+ }
+
+ reset(): this {
+ this.writer = new QwpByteWriter();
+ this.expectedIndex = 0;
+ return this;
+ }
+
+ setBoolean(index: number, value: boolean): this {
+ if (typeof value !== "boolean") {
+ throw new TypeError("BOOLEAN bind value must be a boolean");
+ }
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.BOOLEAN, false);
+ this.writer.writeUint8(value ? 1 : 0);
+ return this;
+ }
+
+ setByte(index: number, value: number): this {
+ const checked = checkedInteger(value, -0x80, 0x7f, "BYTE bind");
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.BYTE, false);
+ this.writer.writeInt8(checked);
+ return this;
+ }
+
+ setShort(index: number, value: number): this {
+ const checked = checkedInteger(value, -0x8000, 0x7fff, "SHORT bind");
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.SHORT, false);
+ this.writer.writeInt16(checked);
+ return this;
+ }
+
+ setChar(index: number, value: string): this {
+ if (typeof value !== "string" || value.length !== 1) {
+ throw new TypeError("CHAR bind value must be one UTF-16 code unit");
+ }
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.CHAR, false);
+ this.writer.writeUint16(value.charCodeAt(0));
+ return this;
+ }
+
+ setInt(index: number, value: number): this {
+ const checked = checkedInteger(value, -0x80000000, 0x7fffffff, "INT bind");
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.INT, false);
+ this.writer.writeInt32(checked);
+ return this;
+ }
+
+ setLong(index: number, value: QwpInt64): this {
+ const checked = checkedInt64(value, "LONG bind");
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.LONG, false);
+ this.writer.writeBigInt64(checked);
+ return this;
+ }
+
+ setFloat(index: number, value: number): this {
+ if (typeof value !== "number") {
+ throw new TypeError("FLOAT bind value must be a number");
+ }
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.FLOAT, false);
+ this.writer.writeFloat32(value);
+ return this;
+ }
+
+ setDouble(index: number, value: number): this {
+ if (typeof value !== "number") {
+ throw new TypeError("DOUBLE bind value must be a number");
+ }
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.DOUBLE, false);
+ this.writer.writeFloat64(value);
+ return this;
+ }
+
+ /** Binds a DATE expressed as milliseconds since the Unix epoch. */
+ setDate(index: number, millisecondsSinceEpoch: QwpInt64): this {
+ const checked = checkedInt64(millisecondsSinceEpoch, "DATE bind");
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.DATE, false);
+ this.writer.writeBigInt64(checked);
+ return this;
+ }
+
+ /** Binds a TIMESTAMP expressed as microseconds since the Unix epoch. */
+ setTimestampMicros(index: number, microsecondsSinceEpoch: QwpInt64): this {
+ const checked = checkedInt64(microsecondsSinceEpoch, "TIMESTAMP bind");
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.TIMESTAMP, false);
+ this.writer.writeBigInt64(checked);
+ return this;
+ }
+
+ /** Binds a TIMESTAMP_NS expressed as nanoseconds since the Unix epoch. */
+ setTimestampNanos(index: number, nanosecondsSinceEpoch: QwpInt64): this {
+ const checked = checkedInt64(nanosecondsSinceEpoch, "TIMESTAMP_NANOS bind");
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.TIMESTAMP_NANOS, false);
+ this.writer.writeBigInt64(checked);
+ return this;
+ }
+
+ setVarchar(index: number, value: string | null): this {
+ if (value === null) return this.setNull(index, QWP_COLUMN_TYPE.VARCHAR);
+ if (typeof value !== "string") {
+ throw new TypeError("VARCHAR bind value must be a string or null");
+ }
+ const utf8 = encodeUtf8(value);
+ if (utf8.length > 0x7fffffff) {
+ throw new RangeError("VARCHAR bind exceeds the int32 wire length limit");
+ }
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.VARCHAR, false);
+ this.writer.writeUint32(0).writeUint32(utf8.length).writeBytes(utf8);
+ return this;
+ }
+
+ setUuid(index: number, value: string | null): this;
+ setUuid(index: number, low: QwpInt64, high: QwpInt64): this;
+ setUuid(
+ index: number,
+ valueOrLow: string | null | QwpInt64,
+ high?: QwpInt64,
+ ): this {
+ if (valueOrLow === null) return this.setNull(index, QWP_COLUMN_TYPE.UUID);
+ let lowBits: bigint;
+ let highBits: bigint;
+ if (typeof valueOrLow === "string") {
+ const match =
+ /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i.exec(
+ valueOrLow,
+ );
+ if (!match) {
+ throw new TypeError("UUID bind value must use canonical UUID syntax");
+ }
+ const hex = match.slice(1).join("");
+ highBits = BigInt(`0x${hex.slice(0, 16)}`);
+ lowBits = BigInt(`0x${hex.slice(16)}`);
+ } else {
+ if (high === undefined) {
+ throw new TypeError("UUID limb form requires both low and high limbs");
+ }
+ lowBits = checkedUint64Bits(valueOrLow, "UUID low limb");
+ highBits = checkedUint64Bits(high, "UUID high limb");
+ }
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.UUID, false);
+ this.writer.writeBigUint64(lowBits).writeBigUint64(highBits);
+ return this;
+ }
+
+ setLong256(
+ index: number,
+ word0: QwpInt64,
+ word1: QwpInt64,
+ word2: QwpInt64,
+ word3: QwpInt64,
+ ): this {
+ const words = [word0, word1, word2, word3].map((word, wordIndex) =>
+ checkedInt64(word, `LONG256 word ${wordIndex}`),
+ );
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.LONG256, false);
+ for (const word of words) this.writer.writeBigInt64(word);
+ return this;
+ }
+
+ setGeohash(index: number, precisionBits: number, value: QwpInt64): this {
+ const precision = checkedInteger(
+ precisionBits,
+ GEOHASH_MIN_BITS,
+ GEOHASH_MAX_BITS,
+ "GEOHASH precision",
+ );
+ const mask = (1n << BigInt(precision)) - 1n;
+ let bits = checkedInt64(value, "GEOHASH bind") & mask;
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.GEOHASH, false);
+ writeQwpVarint(this.writer, precision);
+ const byteCount = Math.ceil(precision / 8);
+ for (let byteIndex = 0; byteIndex < byteCount; byteIndex++) {
+ this.writer.writeUint8(Number(bits & 0xffn));
+ bits >>= 8n;
+ }
+ return this;
+ }
+
+ setDecimal64(index: number, scale: number, unscaled: QwpInt64): this {
+ const checked = checkedScale(scale, DECIMAL64_MAX_SCALE, "DECIMAL64");
+ const value = checkedInt64(unscaled, "DECIMAL64 unscaled value");
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.DECIMAL64, false);
+ this.writer.writeUint8(checked).writeBigInt64(value);
+ return this;
+ }
+
+ setDecimal128(
+ index: number,
+ scale: number,
+ low: QwpInt64,
+ high: QwpInt64,
+ ): this {
+ const checked = checkedScale(scale, DECIMAL128_MAX_SCALE, "DECIMAL128");
+ const lowBits = checkedInt64(low, "DECIMAL128 low limb");
+ const highBits = checkedInt64(high, "DECIMAL128 high limb");
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.DECIMAL128, false);
+ this.writer
+ .writeUint8(checked)
+ .writeBigInt64(lowBits)
+ .writeBigInt64(highBits);
+ return this;
+ }
+
+ setDecimal256(
+ index: number,
+ scale: number,
+ lowLow: QwpInt64,
+ lowHigh: QwpInt64,
+ highLow: QwpInt64,
+ highHigh: QwpInt64,
+ ): this {
+ const checked = checkedScale(scale, DECIMAL256_MAX_SCALE, "DECIMAL256");
+ const limbs = [lowLow, lowHigh, highLow, highHigh].map((limb, limbIndex) =>
+ checkedInt64(limb, `DECIMAL256 limb ${limbIndex}`),
+ );
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.DECIMAL256, false);
+ this.writer.writeUint8(checked);
+ for (const limb of limbs) this.writer.writeBigInt64(limb);
+ return this;
+ }
+
+ setNull(index: number, type: QwpBindType): this {
+ this.assertBindType(type);
+ switch (type) {
+ case QWP_COLUMN_TYPE.DECIMAL64:
+ return this.setNullDecimal64(index, 0);
+ case QWP_COLUMN_TYPE.DECIMAL128:
+ return this.setNullDecimal128(index, 0);
+ case QWP_COLUMN_TYPE.DECIMAL256:
+ return this.setNullDecimal256(index, 0);
+ case QWP_COLUMN_TYPE.GEOHASH:
+ return this.setNullGeohash(index, GEOHASH_MIN_BITS);
+ default:
+ this.advance(index);
+ this.writeHeader(type, true);
+ return this;
+ }
+ }
+
+ setNullDecimal64(index: number, scale: number): this {
+ const checked = checkedScale(scale, DECIMAL64_MAX_SCALE, "DECIMAL64");
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.DECIMAL64, true);
+ this.writer.writeUint8(checked);
+ return this;
+ }
+
+ setNullDecimal128(index: number, scale: number): this {
+ const checked = checkedScale(scale, DECIMAL128_MAX_SCALE, "DECIMAL128");
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.DECIMAL128, true);
+ this.writer.writeUint8(checked);
+ return this;
+ }
+
+ setNullDecimal256(index: number, scale: number): this {
+ const checked = checkedScale(scale, DECIMAL256_MAX_SCALE, "DECIMAL256");
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.DECIMAL256, true);
+ this.writer.writeUint8(checked);
+ return this;
+ }
+
+ setNullGeohash(index: number, precisionBits: number): this {
+ const precision = checkedInteger(
+ precisionBits,
+ GEOHASH_MIN_BITS,
+ GEOHASH_MAX_BITS,
+ "GEOHASH precision",
+ );
+ this.advance(index);
+ this.writeHeader(QWP_COLUMN_TYPE.GEOHASH, true);
+ writeQwpVarint(this.writer, precision);
+ return this;
+ }
+
+ toUint8Array(): Uint8Array {
+ return this.writer.toUint8Array();
+ }
+
+ private advance(index: number): void {
+ const checked = checkedIndex(index);
+ if (checked !== this.expectedIndex) {
+ throw new Error(
+ `bind index out of order: expected ${this.expectedIndex}, got ${checked}`,
+ );
+ }
+ if (this.expectedIndex >= QWP_MAX_COLUMNS_PER_TABLE) {
+ throw new RangeError(
+ `too many binds: exceeds ${QWP_MAX_COLUMNS_PER_TABLE}`,
+ );
+ }
+ this.expectedIndex++;
+ }
+
+ private assertBindType(type: number): asserts type is QwpBindType {
+ switch (type) {
+ case QWP_COLUMN_TYPE.BOOLEAN:
+ case QWP_COLUMN_TYPE.BYTE:
+ case QWP_COLUMN_TYPE.SHORT:
+ case QWP_COLUMN_TYPE.CHAR:
+ case QWP_COLUMN_TYPE.INT:
+ case QWP_COLUMN_TYPE.LONG:
+ case QWP_COLUMN_TYPE.FLOAT:
+ case QWP_COLUMN_TYPE.DOUBLE:
+ case QWP_COLUMN_TYPE.DATE:
+ case QWP_COLUMN_TYPE.TIMESTAMP:
+ case QWP_COLUMN_TYPE.TIMESTAMP_NANOS:
+ case QWP_COLUMN_TYPE.UUID:
+ case QWP_COLUMN_TYPE.LONG256:
+ case QWP_COLUMN_TYPE.GEOHASH:
+ case QWP_COLUMN_TYPE.VARCHAR:
+ case QWP_COLUMN_TYPE.DECIMAL64:
+ case QWP_COLUMN_TYPE.DECIMAL128:
+ case QWP_COLUMN_TYPE.DECIMAL256:
+ return;
+ default:
+ throw new RangeError(
+ `unsupported QWP bind type 0x${type.toString(16)}`,
+ );
+ }
+ }
+
+ private writeHeader(type: QwpBindType, isNull: boolean): void {
+ this.writer.writeUint8(type).writeUint8(isNull ? NULL_FLAG : NON_NULL_FLAG);
+ if (isNull) this.writer.writeUint8(NULL_BITMAP);
+ }
+}
+
+/** Runs a setter callback and returns the exact QUERY_REQUEST bind section. */
+export function encodeQwpBinds(setter: QwpBindSetter): QwpEncodedBinds {
+ if (typeof setter !== "function") {
+ throw new TypeError("binds must be a function");
+ }
+ const values = new QwpBindValues();
+ const result = setter(values) as unknown;
+ if (
+ result !== null &&
+ (typeof result === "object" || typeof result === "function") &&
+ "then" in result &&
+ typeof result.then === "function"
+ ) {
+ throw new TypeError("binds callback must be synchronous");
+ }
+ return { count: values.count, payload: values.toUint8Array() };
+}
diff --git a/packages/client-core/src/_qwp/_core/bytes.ts b/packages/client-core/src/_qwp/_core/bytes.ts
new file mode 100644
index 0000000..b18eebc
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/bytes.ts
@@ -0,0 +1,343 @@
+import { QwpProtocolError } from "./errors";
+
+const UTF8_ENCODER = new TextEncoder();
+const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
+
+export function encodeUtf8(value: string): Uint8Array {
+ return UTF8_ENCODER.encode(value);
+}
+
+// Node's Buffer.byteLength counts UTF-8 bytes natively, ~10x faster than
+// encoding into a Uint8Array only to read .length and discard it (measured
+// 22.7 ns vs 221 ns; the encoder UTF-8-encodes every VARCHAR cell twice --
+// once to size, once to write). Reached through globalThis so the browser
+// build, which has no Node types, still compiles and falls back to the
+// allocation-free scan below. Both count exactly what encodeUtf8() writes,
+// including the 3-byte replacement for an unpaired surrogate, so measured sizes
+// never disagree with the bytes emitted.
+const nodeByteLength = (
+ globalThis as {
+ Buffer?: { byteLength(value: string, encoding: "utf8"): number };
+ }
+).Buffer?.byteLength;
+
+export function utf8Length(value: string): number {
+ if (nodeByteLength) return nodeByteLength(value, "utf8");
+ let bytes = 0;
+ for (let index = 0; index < value.length; index++) {
+ const code = value.charCodeAt(index);
+ if (code < 0x80) {
+ bytes += 1;
+ } else if (code < 0x800) {
+ bytes += 2;
+ } else if (code >= 0xd800 && code <= 0xdbff) {
+ // A high surrogate paired with a low surrogate is one 4-byte code point;
+ // an unpaired one becomes the 3-byte replacement character.
+ const next = value.charCodeAt(index + 1);
+ if (next >= 0xdc00 && next <= 0xdfff) {
+ bytes += 4;
+ index++;
+ } else {
+ bytes += 3;
+ }
+ } else {
+ bytes += 3;
+ }
+ }
+ return bytes;
+}
+
+export function decodeUtf8(value: Uint8Array): string {
+ try {
+ return UTF8_DECODER.decode(value);
+ } catch (error) {
+ throw new QwpProtocolError(
+ `invalid UTF-8 payload: ${(error as Error).message}`,
+ );
+ }
+}
+
+export function concatBytes(parts: readonly Uint8Array[]): Uint8Array {
+ let length = 0;
+ for (const part of parts) length += part.length;
+ const result = new Uint8Array(length);
+ let offset = 0;
+ for (const part of parts) {
+ result.set(part, offset);
+ offset += part.length;
+ }
+ return result;
+}
+
+function checkedLength(value: number, label: string): number {
+ if (!Number.isSafeInteger(value) || value < 0) {
+ throw new RangeError(`${label} must be a non-negative safe integer`);
+ }
+ return value;
+}
+
+/** A growable, runtime-neutral little-endian byte writer. */
+export class QwpByteWriter {
+ private bytes: Uint8Array;
+ private view: DataView;
+ private cursor = 0;
+
+ constructor(initialCapacity = 128) {
+ checkedLength(initialCapacity, "initialCapacity");
+ this.bytes = new Uint8Array(Math.max(initialCapacity, 1));
+ this.view = new DataView(this.bytes.buffer);
+ }
+
+ get length(): number {
+ return this.cursor;
+ }
+
+ private ensure(additional: number): void {
+ checkedLength(additional, "additional byte count");
+ const required = this.cursor + additional;
+ if (required <= this.bytes.length) return;
+ let capacity = this.bytes.length;
+ while (capacity < required) capacity = Math.max(capacity * 2, required);
+ const next = new Uint8Array(capacity);
+ next.set(this.bytes.subarray(0, this.cursor));
+ this.bytes = next;
+ this.view = new DataView(next.buffer);
+ }
+
+ writeUint8(value: number): this {
+ this.ensure(1);
+ this.view.setUint8(this.cursor, value);
+ this.cursor++;
+ return this;
+ }
+
+ writeInt8(value: number): this {
+ this.ensure(1);
+ this.view.setInt8(this.cursor, value);
+ this.cursor++;
+ return this;
+ }
+
+ writeUint16(value: number): this {
+ this.ensure(2);
+ this.view.setUint16(this.cursor, value, true);
+ this.cursor += 2;
+ return this;
+ }
+
+ writeInt16(value: number): this {
+ this.ensure(2);
+ this.view.setInt16(this.cursor, value, true);
+ this.cursor += 2;
+ return this;
+ }
+
+ writeUint32(value: number): this {
+ this.ensure(4);
+ this.view.setUint32(this.cursor, value, true);
+ this.cursor += 4;
+ return this;
+ }
+
+ writeInt32(value: number): this {
+ this.ensure(4);
+ this.view.setInt32(this.cursor, value, true);
+ this.cursor += 4;
+ return this;
+ }
+
+ writeBigUint64(value: bigint): this {
+ this.ensure(8);
+ this.view.setBigUint64(this.cursor, BigInt.asUintN(64, value), true);
+ this.cursor += 8;
+ return this;
+ }
+
+ writeBigInt64(value: bigint): this {
+ this.ensure(8);
+ this.view.setBigInt64(this.cursor, BigInt.asIntN(64, value), true);
+ this.cursor += 8;
+ return this;
+ }
+
+ writeFloat32(value: number): this {
+ this.ensure(4);
+ this.view.setFloat32(this.cursor, value, true);
+ this.cursor += 4;
+ return this;
+ }
+
+ writeFloat64(value: number): this {
+ this.ensure(8);
+ this.view.setFloat64(this.cursor, value, true);
+ this.cursor += 8;
+ return this;
+ }
+
+ writeBytes(value: Uint8Array): this {
+ this.ensure(value.length);
+ this.bytes.set(value, this.cursor);
+ this.cursor += value.length;
+ return this;
+ }
+
+ writeUtf8(value: string): this {
+ return this.writeBytes(encodeUtf8(value));
+ }
+
+ writeZeroes(count: number): this {
+ this.ensure(count);
+ this.bytes.fill(0, this.cursor, this.cursor + count);
+ this.cursor += count;
+ return this;
+ }
+
+ patchUint8(offset: number, value: number): void {
+ if (offset < 0 || offset >= this.cursor) {
+ throw new RangeError(`patch offset ${offset} is outside written bytes`);
+ }
+ this.view.setUint8(offset, value);
+ }
+
+ patchUint16(offset: number, value: number): void {
+ if (offset < 0 || offset + 2 > this.cursor) {
+ throw new RangeError(`patch offset ${offset} is outside written bytes`);
+ }
+ this.view.setUint16(offset, value, true);
+ }
+
+ patchUint32(offset: number, value: number): void {
+ if (offset < 0 || offset + 4 > this.cursor) {
+ throw new RangeError(`patch offset ${offset} is outside written bytes`);
+ }
+ this.view.setUint32(offset, value, true);
+ }
+
+ toUint8Array(): Uint8Array {
+ return this.bytes.slice(0, this.cursor);
+ }
+}
+
+/** A bounds-checked, runtime-neutral little-endian byte reader. */
+export class QwpByteReader {
+ private readonly view: DataView;
+ private cursor: number;
+ private readonly end: number;
+
+ constructor(
+ readonly bytes: Uint8Array,
+ offset = 0,
+ length = bytes.length - offset,
+ ) {
+ checkedLength(offset, "offset");
+ checkedLength(length, "length");
+ if (offset + length > bytes.length) {
+ throw new QwpProtocolError("reader range exceeds payload length");
+ }
+ this.cursor = offset;
+ this.end = offset + length;
+ this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
+ }
+
+ get position(): number {
+ return this.cursor;
+ }
+
+ get remaining(): number {
+ return this.end - this.cursor;
+ }
+
+ private ensureAvailable(length: number, label: string): void {
+ if (length < 0 || this.cursor + length > this.end) {
+ throw new QwpProtocolError(
+ `truncated QWP payload while reading ${label}`,
+ );
+ }
+ }
+
+ readUint8(label = "uint8"): number {
+ this.ensureAvailable(1, label);
+ return this.view.getUint8(this.cursor++);
+ }
+
+ readInt8(label = "int8"): number {
+ this.ensureAvailable(1, label);
+ return this.view.getInt8(this.cursor++);
+ }
+
+ readUint16(label = "uint16"): number {
+ this.ensureAvailable(2, label);
+ const value = this.view.getUint16(this.cursor, true);
+ this.cursor += 2;
+ return value;
+ }
+
+ readInt16(label = "int16"): number {
+ this.ensureAvailable(2, label);
+ const value = this.view.getInt16(this.cursor, true);
+ this.cursor += 2;
+ return value;
+ }
+
+ readUint32(label = "uint32"): number {
+ this.ensureAvailable(4, label);
+ const value = this.view.getUint32(this.cursor, true);
+ this.cursor += 4;
+ return value;
+ }
+
+ readInt32(label = "int32"): number {
+ this.ensureAvailable(4, label);
+ const value = this.view.getInt32(this.cursor, true);
+ this.cursor += 4;
+ return value;
+ }
+
+ readBigUint64(label = "uint64"): bigint {
+ this.ensureAvailable(8, label);
+ const value = this.view.getBigUint64(this.cursor, true);
+ this.cursor += 8;
+ return value;
+ }
+
+ readBigInt64(label = "int64"): bigint {
+ this.ensureAvailable(8, label);
+ const value = this.view.getBigInt64(this.cursor, true);
+ this.cursor += 8;
+ return value;
+ }
+
+ readFloat32(label = "float32"): number {
+ this.ensureAvailable(4, label);
+ const value = this.view.getFloat32(this.cursor, true);
+ this.cursor += 4;
+ return value;
+ }
+
+ readFloat64(label = "float64"): number {
+ this.ensureAvailable(8, label);
+ const value = this.view.getFloat64(this.cursor, true);
+ this.cursor += 8;
+ return value;
+ }
+
+ readBytes(length: number, label = "bytes"): Uint8Array {
+ checkedLength(length, "byte length");
+ this.ensureAvailable(length, label);
+ const value = this.bytes.subarray(this.cursor, this.cursor + length);
+ this.cursor += length;
+ return value;
+ }
+
+ readUtf8(length: number, label = "UTF-8 string"): string {
+ return decodeUtf8(this.readBytes(length, label));
+ }
+
+ expectEnd(label = "QWP payload"): void {
+ if (this.remaining !== 0) {
+ throw new QwpProtocolError(
+ `${label} has ${this.remaining} unexpected trailing byte(s)`,
+ );
+ }
+ }
+}
diff --git a/packages/client-core/src/_qwp/_core/compression.ts b/packages/client-core/src/_qwp/_core/compression.ts
new file mode 100644
index 0000000..892fca5
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/compression.ts
@@ -0,0 +1,63 @@
+export const QWP_ZSTD_MIN_COMPRESSION_LEVEL = 1;
+export const QWP_ZSTD_MAX_COMPRESSION_LEVEL = 22;
+
+export type QwpEgressCompression = "raw" | "zstd" | "auto";
+
+export type QwpNegotiatedEgressCompression =
+ | {
+ readonly codec: "raw";
+ readonly level: 0;
+ }
+ | {
+ readonly codec: "zstd";
+ readonly level: number;
+ }
+ | {
+ readonly codec: "unknown";
+ readonly level: 0;
+ readonly contentEncoding: string;
+ };
+
+/** Builds the Node upgrade header for an egress compression preference. */
+export function encodeQwpAcceptEncoding(
+ preference: QwpEgressCompression,
+ level = QWP_ZSTD_MIN_COMPRESSION_LEVEL,
+): string | undefined {
+ if (preference !== "raw" && preference !== "zstd" && preference !== "auto") {
+ throw new RangeError("compression must be one of raw, zstd, or auto");
+ }
+ if (
+ !Number.isSafeInteger(level) ||
+ level < QWP_ZSTD_MIN_COMPRESSION_LEVEL ||
+ level > QWP_ZSTD_MAX_COMPRESSION_LEVEL
+ ) {
+ throw new RangeError(
+ `compressionLevel must be an integer between ${QWP_ZSTD_MIN_COMPRESSION_LEVEL} and ${QWP_ZSTD_MAX_COMPRESSION_LEVEL}`,
+ );
+ }
+ return preference === "raw" ? undefined : `zstd;level=${level},raw`;
+}
+
+/**
+ * Parses the server's `X-QWP-Content-Encoding` response. Unknown values remain
+ * observable but do not claim that Zstd was negotiated; RESULT_BATCH flags
+ * remain authoritative for each individual batch.
+ */
+export function decodeQwpContentEncoding(
+ value: string | undefined,
+): QwpNegotiatedEgressCompression {
+ const contentEncoding = value?.trim();
+ if (!contentEncoding) return { codec: "raw", level: 0 };
+ if (/^(?:raw|identity)$/i.test(contentEncoding)) {
+ return { codec: "raw", level: 0 };
+ }
+
+ const match = /^zstd\s*;\s*level\s*=\s*(\d+)$/i.exec(contentEncoding);
+ if (match) {
+ const level = Number(match[1]);
+ if (Number.isSafeInteger(level) && level > 0) {
+ return { codec: "zstd", level };
+ }
+ }
+ return { codec: "unknown", level: 0, contentEncoding };
+}
diff --git a/packages/client-core/src/_qwp/_core/constants.ts b/packages/client-core/src/_qwp/_core/constants.ts
new file mode 100644
index 0000000..083a2f7
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/constants.ts
@@ -0,0 +1,134 @@
+/** ASCII `QWP1`, represented as its little-endian uint32 value. */
+export const QWP_MAGIC = 0x31505751;
+export const QWP_VERSION = 1;
+export const QWP_HEADER_SIZE = 12;
+
+export const QWP_FLAG_DEFER_COMMIT = 0x01;
+/** Table-less ingress control frame that polls negotiated durable-ACK progress. */
+export const QWP_FLAG_DURABLE_ACK_POLL = 0x02;
+export const QWP_FLAG_GORILLA = 0x04;
+export const QWP_FLAG_DELTA_SYMBOL_DICTIONARY = 0x08;
+export const QWP_FLAG_ZSTD = 0x10;
+
+export const QWP_COLUMN_TYPE = {
+ BOOLEAN: 0x01,
+ BYTE: 0x02,
+ SHORT: 0x03,
+ INT: 0x04,
+ LONG: 0x05,
+ FLOAT: 0x06,
+ DOUBLE: 0x07,
+ SYMBOL: 0x09,
+ TIMESTAMP: 0x0a,
+ DATE: 0x0b,
+ UUID: 0x0c,
+ LONG256: 0x0d,
+ GEOHASH: 0x0e,
+ VARCHAR: 0x0f,
+ TIMESTAMP_NANOS: 0x10,
+ DOUBLE_ARRAY: 0x11,
+ LONG_ARRAY: 0x12,
+ DECIMAL64: 0x13,
+ DECIMAL128: 0x14,
+ DECIMAL256: 0x15,
+ CHAR: 0x16,
+ BINARY: 0x17,
+ IPV4: 0x18,
+} as const;
+
+export type QwpColumnType =
+ (typeof QWP_COLUMN_TYPE)[keyof typeof QWP_COLUMN_TYPE];
+
+export const QWP_ENCODING_UNCOMPRESSED = 0x00;
+export const QWP_ENCODING_GORILLA = 0x01;
+
+export const QWP_STATUS = {
+ OK: 0x00,
+ SERVER_INFO: 0x01,
+ DURABLE_ACK: 0x02,
+ SCHEMA_MISMATCH: 0x03,
+ PARSE_ERROR: 0x05,
+ INTERNAL_ERROR: 0x06,
+ SECURITY_ERROR: 0x08,
+ WRITE_ERROR: 0x09,
+ CANCELLED: 0x0a,
+ LIMIT_EXCEEDED: 0x0b,
+ NOT_WRITABLE: 0x0c,
+ DICTIONARY_GAP: 0x0d,
+} as const;
+
+export const QWP_EGRESS_MESSAGE = {
+ QUERY_REQUEST: 0x10,
+ RESULT_BATCH: 0x11,
+ RESULT_END: 0x12,
+ QUERY_ERROR: 0x13,
+ CANCEL: 0x14,
+ CREDIT: 0x15,
+ EXEC_DONE: 0x16,
+ CACHE_RESET: 0x17,
+ SERVER_INFO: 0x18,
+} as const;
+
+export const QWP_EGRESS_CAPABILITY = {
+ ZONE: 0x00000001,
+ QUERY_FLAGS: 0x00000002,
+ COMPRESSION: 0x00000004,
+} as const;
+
+export const QWP_COMPRESSION_CODEC = {
+ RAW: 0,
+ ZSTD: 1,
+} as const;
+
+export const QWP_QUERY_FLAG_RESET_DICTIONARY = 0x01;
+export const QWP_RESET_MASK_DICTIONARY = 0x01;
+
+export const QWP_SERVER_ROLE = {
+ STANDALONE: 0,
+ PRIMARY: 1,
+ REPLICA: 2,
+ PRIMARY_CATCHUP: 3,
+} as const;
+
+export const QWP_MAX_COLUMNS_PER_TABLE = 2048;
+/** Maximum array rank accepted by QuestDB's QWP ingress decoder. */
+export const QWP_MAX_ARRAY_DIMENSIONS = 32;
+/** Maximum signed int32 array-axis length accepted by QWP ingress. */
+export const QWP_MAX_ARRAY_DIMENSION_LENGTH = 2_147_483_647;
+/** Default QWP ingress identifier limits, in UTF-8 wire bytes. */
+export const QWP_MAX_COLUMN_NAME_LENGTH = 127;
+export const QWP_MAX_TABLE_NAME_LENGTH = 127;
+/**
+ * Defensive byte bound for identifiers decoded from query results.
+ *
+ * Existing tables may have names created through APIs that apply Java's
+ * 127-UTF-16-code-unit metadata limit. One code unit takes at most three UTF-8
+ * bytes, so query decoding accepts that larger representation even though QWP
+ * ingress enforces its 127-byte protocol limit.
+ */
+export const QWP_MAX_IDENTIFIER_BYTES = QWP_MAX_TABLE_NAME_LENGTH * 3;
+export const QWP_MAX_ROWS_PER_TABLE = 1_000_000;
+export const QWP_MAX_SYMBOL_DICTIONARY_SIZE = 1_000_000;
+export const QWP_MAX_ERROR_MESSAGE_LENGTH = 1024;
+/** Largest client-requested egress RESULT_BATCH row cap. */
+export const QWP_MAX_BATCH_ROWS_UPPER_BOUND = 1_048_576;
+/**
+ * Largest `rowCount * columnCount` a single RESULT_BATCH may declare.
+ *
+ * The row and column caps above bound each dimension on its own, and their
+ * product does not have to be reachable: 1,048,576 rows of 2,048 columns is
+ * 2.1 billion cells. Decoding materializes two `rowCount`-length arrays per
+ * column, measured at 16 bytes per cell, so the product is what decides how
+ * much memory a response can cost. It is also the dimension a compressed body
+ * detaches from the wire: an all-NULL column is one bit per cell before zstd,
+ * so without this bound a few kilobytes of RLE-compressed bitmap declares a
+ * grid no heap can hold.
+ *
+ * 32Mi cells is roughly 512 MB decoded. That is far above any plausible
+ * result -- the widest supported table at 16k rows, or a full 1,048,576-row
+ * batch at 32 columns -- and far below what the caps alone would permit.
+ */
+export const QWP_MAX_CELLS_PER_BATCH = 33_554_432;
+
+export const QWP_INGRESS_PATH = "/write/v4";
+export const QWP_EGRESS_PATH = "/read/v1";
diff --git a/packages/client-core/src/_qwp/_core/durable-ack.ts b/packages/client-core/src/_qwp/_core/durable-ack.ts
new file mode 100644
index 0000000..0aba5f8
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/durable-ack.ts
@@ -0,0 +1,27 @@
+/**
+ * Browser-visible WebSocket subprotocol used to request and confirm durable
+ * ingress acknowledgements. Browsers cannot set or inspect X-QWP-* headers.
+ */
+export const QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL = "questdb.qwp.durable-ack.v1";
+
+/** Adds the durable-ACK capability token without mutating user options. */
+export function addQwpDurableAckWebSocketProtocol(
+ protocols: string | readonly string[] | undefined,
+): string | string[] {
+ if (protocols === undefined) return QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL;
+ if (typeof protocols === "string") {
+ return protocols === QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL
+ ? protocols
+ : [protocols, QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL];
+ }
+ return protocols.includes(QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL)
+ ? [...protocols]
+ : [...protocols, QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL];
+}
+
+/** True when the server selected the browser durable-ACK subprotocol. */
+export function isQwpDurableAckWebSocketProtocol(
+ protocol: string | undefined,
+): boolean {
+ return protocol === QWP_DURABLE_ACK_WEBSOCKET_PROTOCOL;
+}
diff --git a/packages/client-core/src/_qwp/_core/egress.ts b/packages/client-core/src/_qwp/_core/egress.ts
new file mode 100644
index 0000000..a214c8a
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/egress.ts
@@ -0,0 +1,277 @@
+import { encodeUtf8, QwpByteReader, QwpByteWriter } from "./bytes";
+import { encodeQwpBinds, QwpBindSetter } from "./binds";
+import {
+ QWP_EGRESS_CAPABILITY,
+ QWP_EGRESS_MESSAGE,
+ QWP_MAX_COLUMNS_PER_TABLE,
+} from "./constants";
+import { decodeQwpFrame, QwpFrameHeader } from "./frame";
+import { QwpProtocolError } from "./errors";
+import { readQwpVarint, writeQwpVarint } from "./varint";
+
+export interface QwpQueryRequest {
+ requestId: number | bigint;
+ sql: string;
+ /** Zero means unbounded. */
+ initialCredit?: number | bigint;
+ /** Browser-safe typed positional binds. */
+ binds?: QwpBindSetter;
+ /** Advanced escape hatch for an already encoded bind section. */
+ bindCount?: number;
+ /** Advanced escape hatch for an already encoded bind section. */
+ bindPayload?: Uint8Array;
+ /** Append only after SERVER_INFO advertises QUERY_FLAGS. */
+ queryFlags?: number | bigint;
+}
+
+/** Immutable endpoint metadata from the most recent successful egress bind. */
+export interface QwpServerInfoMessage extends QwpFrameHeader {
+ kind: "server-info";
+ role: number;
+ epoch: bigint;
+ capabilities: number;
+ serverWallNanoseconds: bigint;
+ clusterId: string;
+ nodeId: string;
+ zoneId: string | null;
+ compressionCodec: number | null;
+ compressionLevel: number | null;
+}
+
+export interface QwpResultBatchMessage extends QwpFrameHeader {
+ kind: "result-batch";
+ requestId: bigint;
+ batchSequence: bigint;
+ /**
+ * Raw or Zstd-compressed delta dictionary and columnar table block; decoded
+ * by the batch decoder according to the frame flags.
+ */
+ body: Uint8Array;
+}
+
+export interface QwpResultEndMessage extends QwpFrameHeader {
+ kind: "result-end";
+ requestId: bigint;
+ finalSequence: bigint;
+ totalRows: bigint;
+}
+
+export interface QwpQueryErrorMessage extends QwpFrameHeader {
+ kind: "query-error";
+ requestId: bigint;
+ status: number;
+ message: string;
+}
+
+export interface QwpExecDoneMessage extends QwpFrameHeader {
+ kind: "exec-done";
+ requestId: bigint;
+ operationType: number;
+ rowsAffected: bigint;
+}
+
+export interface QwpCacheResetMessage extends QwpFrameHeader {
+ kind: "cache-reset";
+ resetMask: number;
+}
+
+export type QwpEgressMessage =
+ | QwpServerInfoMessage
+ | QwpResultBatchMessage
+ | QwpResultEndMessage
+ | QwpQueryErrorMessage
+ | QwpExecDoneMessage
+ | QwpCacheResetMessage;
+
+function requestId(value: number | bigint): bigint {
+ if (typeof value === "number") {
+ if (!Number.isSafeInteger(value) || value < 0) {
+ throw new RangeError("requestId must be a non-negative safe integer");
+ }
+ return BigInt(value);
+ }
+ if (value < 0n || value > 0xffffffffffffffffn) {
+ throw new RangeError("requestId must fit in uint64");
+ }
+ return value;
+}
+
+/** Encodes the unframed client-to-server QUERY_REQUEST payload. */
+export function encodeQwpQueryRequest(request: QwpQueryRequest): Uint8Array {
+ if (
+ request.binds !== undefined &&
+ (request.bindCount !== undefined || request.bindPayload !== undefined)
+ ) {
+ throw new Error(
+ "typed binds cannot be mixed with raw bindCount/bindPayload",
+ );
+ }
+ const encodedBinds = request.binds
+ ? encodeQwpBinds(request.binds)
+ : undefined;
+ const bindCount = encodedBinds?.count ?? request.bindCount ?? 0;
+ if (
+ !Number.isSafeInteger(bindCount) ||
+ bindCount < 0 ||
+ bindCount > QWP_MAX_COLUMNS_PER_TABLE
+ ) {
+ throw new RangeError(
+ `bindCount must be an integer between 0 and ${QWP_MAX_COLUMNS_PER_TABLE}`,
+ );
+ }
+ const bindPayload =
+ encodedBinds?.payload ?? request.bindPayload ?? new Uint8Array();
+ if (bindCount === 0 && bindPayload.length !== 0) {
+ throw new Error("bindPayload requires a non-zero bindCount");
+ }
+
+ const sql = encodeUtf8(request.sql);
+ const writer = new QwpByteWriter(32 + sql.length + bindPayload.length);
+ writer.writeUint8(QWP_EGRESS_MESSAGE.QUERY_REQUEST);
+ writer.writeBigUint64(requestId(request.requestId));
+ writeQwpVarint(writer, sql.length);
+ writer.writeBytes(sql);
+ writeQwpVarint(writer, request.initialCredit ?? 0);
+ writeQwpVarint(writer, bindCount);
+ writer.writeBytes(bindPayload);
+ if ((request.queryFlags ?? 0) !== 0) {
+ writeQwpVarint(writer, request.queryFlags!);
+ }
+ return writer.toUint8Array();
+}
+
+/** Encodes the unframed client-to-server CANCEL payload. */
+export function encodeQwpCancel(request: number | bigint): Uint8Array {
+ const writer = new QwpByteWriter(9);
+ writer.writeUint8(QWP_EGRESS_MESSAGE.CANCEL);
+ writer.writeBigUint64(requestId(request));
+ return writer.toUint8Array();
+}
+
+/** Encodes the unframed client-to-server CREDIT payload. */
+export function encodeQwpCredit(
+ request: number | bigint,
+ additionalBytes: number | bigint,
+): Uint8Array {
+ const writer = new QwpByteWriter(19);
+ writer.writeUint8(QWP_EGRESS_MESSAGE.CREDIT);
+ writer.writeBigUint64(requestId(request));
+ writeQwpVarint(writer, additionalBytes);
+ return writer.toUint8Array();
+}
+
+function readUint16Utf8(reader: QwpByteReader, label: string): string {
+ const length = reader.readUint16(`${label} length`);
+ return reader.readUtf8(length, label);
+}
+
+/** Decodes one QWP-framed server-to-client egress message. */
+export function decodeQwpEgressMessage(bytes: Uint8Array): QwpEgressMessage {
+ const frame = decodeQwpFrame(bytes);
+ const reader = new QwpByteReader(frame.payload);
+ const messageKind = reader.readUint8("egress message kind");
+ const header: QwpFrameHeader = {
+ version: frame.version,
+ flags: frame.flags,
+ tableCount: frame.tableCount,
+ payloadLength: frame.payloadLength,
+ };
+
+ switch (messageKind) {
+ case QWP_EGRESS_MESSAGE.SERVER_INFO: {
+ const role = reader.readUint8("server role");
+ const epoch = reader.readBigUint64("server epoch");
+ const capabilities = reader.readUint32("server capabilities");
+ const serverWallNanoseconds = reader.readBigInt64("server wall clock");
+ const clusterId = readUint16Utf8(reader, "cluster ID");
+ const nodeId = readUint16Utf8(reader, "node ID");
+ const zoneId =
+ (capabilities & QWP_EGRESS_CAPABILITY.ZONE) !== 0
+ ? readUint16Utf8(reader, "zone ID")
+ : null;
+ const compressionCodec =
+ (capabilities & QWP_EGRESS_CAPABILITY.COMPRESSION) !== 0
+ ? reader.readUint8("egress compression codec")
+ : null;
+ const compressionLevel =
+ compressionCodec !== null
+ ? reader.readUint8("egress compression level")
+ : null;
+ reader.expectEnd("SERVER_INFO");
+ return Object.freeze({
+ ...header,
+ kind: "server-info",
+ role,
+ epoch,
+ capabilities,
+ serverWallNanoseconds,
+ clusterId,
+ nodeId,
+ zoneId,
+ compressionCodec,
+ compressionLevel,
+ });
+ }
+ case QWP_EGRESS_MESSAGE.RESULT_BATCH: {
+ const requestId = reader.readBigUint64("result request ID");
+ const batchSequence = readQwpVarint(reader);
+ const body = reader.readBytes(reader.remaining, "result batch body");
+ return {
+ ...header,
+ kind: "result-batch",
+ requestId,
+ batchSequence,
+ body,
+ };
+ }
+ case QWP_EGRESS_MESSAGE.RESULT_END: {
+ const requestId = reader.readBigUint64("result request ID");
+ const finalSequence = readQwpVarint(reader);
+ const totalRows = readQwpVarint(reader);
+ reader.expectEnd("RESULT_END");
+ return {
+ ...header,
+ kind: "result-end",
+ requestId,
+ finalSequence,
+ totalRows,
+ };
+ }
+ case QWP_EGRESS_MESSAGE.QUERY_ERROR: {
+ const requestId = reader.readBigUint64("query error request ID");
+ const status = reader.readUint8("query error status");
+ const length = reader.readUint16("query error message length");
+ const message = reader.readUtf8(length, "query error message");
+ reader.expectEnd("QUERY_ERROR");
+ return {
+ ...header,
+ kind: "query-error",
+ requestId,
+ status,
+ message,
+ };
+ }
+ case QWP_EGRESS_MESSAGE.EXEC_DONE: {
+ const requestId = reader.readBigUint64("exec request ID");
+ const operationType = reader.readUint8("operation type");
+ const rowsAffected = readQwpVarint(reader);
+ reader.expectEnd("EXEC_DONE");
+ return {
+ ...header,
+ kind: "exec-done",
+ requestId,
+ operationType,
+ rowsAffected,
+ };
+ }
+ case QWP_EGRESS_MESSAGE.CACHE_RESET: {
+ const resetMask = reader.readUint8("cache reset mask");
+ reader.expectEnd("CACHE_RESET");
+ return { ...header, kind: "cache-reset", resetMask };
+ }
+ default:
+ throw new QwpProtocolError(
+ `unsupported QWP egress message kind 0x${messageKind.toString(16)}`,
+ );
+ }
+}
diff --git a/packages/client-core/src/_qwp/_core/errors.ts b/packages/client-core/src/_qwp/_core/errors.ts
new file mode 100644
index 0000000..58bd697
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/errors.ts
@@ -0,0 +1,7 @@
+/** Raised when a QWP payload is malformed, truncated, or unsupported. */
+export class QwpProtocolError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "QwpProtocolError";
+ }
+}
diff --git a/packages/client-core/src/_qwp/_core/frame.ts b/packages/client-core/src/_qwp/_core/frame.ts
new file mode 100644
index 0000000..b00a5b5
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/frame.ts
@@ -0,0 +1,72 @@
+import { QwpByteReader, QwpByteWriter } from "./bytes";
+import { QWP_HEADER_SIZE, QWP_MAGIC, QWP_VERSION } from "./constants";
+import { QwpProtocolError } from "./errors";
+
+export interface QwpFrameHeader {
+ version: number;
+ flags: number;
+ tableCount: number;
+ payloadLength: number;
+}
+
+export interface QwpFrame extends QwpFrameHeader {
+ payload: Uint8Array;
+}
+
+export function writeQwpFrameHeader(
+ writer: QwpByteWriter,
+ header: Omit & { version?: number },
+): void {
+ writer.writeUint32(QWP_MAGIC);
+ writer.writeUint8(header.version ?? QWP_VERSION);
+ writer.writeUint8(header.flags);
+ writer.writeUint16(header.tableCount);
+ writer.writeUint32(header.payloadLength);
+}
+
+export function encodeQwpFrame(
+ payload: Uint8Array,
+ flags = 0,
+ tableCount = 0,
+): Uint8Array {
+ const writer = new QwpByteWriter(QWP_HEADER_SIZE + payload.length);
+ writeQwpFrameHeader(writer, {
+ flags,
+ tableCount,
+ payloadLength: payload.length,
+ });
+ writer.writeBytes(payload);
+ return writer.toUint8Array();
+}
+
+export function decodeQwpFrame(bytes: Uint8Array): QwpFrame {
+ if (bytes.length < QWP_HEADER_SIZE) {
+ throw new QwpProtocolError("QWP frame is shorter than its 12-byte header");
+ }
+ const reader = new QwpByteReader(bytes);
+ const magic = reader.readUint32("QWP magic");
+ if (magic !== QWP_MAGIC) {
+ throw new QwpProtocolError(
+ `invalid QWP magic 0x${magic.toString(16).padStart(8, "0")}`,
+ );
+ }
+ const version = reader.readUint8("QWP version");
+ if (version !== QWP_VERSION) {
+ throw new QwpProtocolError(`unsupported QWP version ${version}`);
+ }
+ const flags = reader.readUint8("QWP flags");
+ const tableCount = reader.readUint16("QWP table count");
+ const payloadLength = reader.readUint32("QWP payload length");
+ if (payloadLength !== reader.remaining) {
+ throw new QwpProtocolError(
+ `QWP payload length mismatch [declared=${payloadLength}, actual=${reader.remaining}]`,
+ );
+ }
+ return {
+ version,
+ flags,
+ tableCount,
+ payloadLength,
+ payload: reader.readBytes(payloadLength, "QWP payload"),
+ };
+}
diff --git a/packages/client-core/src/_qwp/_core/gorilla.ts b/packages/client-core/src/_qwp/_core/gorilla.ts
new file mode 100644
index 0000000..7e8d138
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/gorilla.ts
@@ -0,0 +1,101 @@
+import { QwpByteWriter } from "./bytes";
+
+const INT32_MIN = -2147483648n;
+const INT32_MAX = 2147483647n;
+
+class QwpBitWriter {
+ private readonly bytes: Uint8Array;
+ private byteIndex = 0;
+ private bitIndex = 0;
+
+ constructor(capacity: number) {
+ this.bytes = new Uint8Array(capacity);
+ }
+
+ writeBits(value: number, count: number): void {
+ for (let index = 0; index < count; index++) {
+ if ((value >>> index) & 1) {
+ this.bytes[this.byteIndex] |= 1 << this.bitIndex;
+ }
+ this.bitIndex++;
+ if (this.bitIndex === 8) {
+ this.bitIndex = 0;
+ this.byteIndex++;
+ }
+ }
+ }
+
+ finish(): Uint8Array {
+ const length = this.byteIndex + (this.bitIndex > 0 ? 1 : 0);
+ return this.bytes.slice(0, length);
+ }
+}
+
+function encodedDeltaBits(deltaOfDelta: bigint): number {
+ if (deltaOfDelta === 0n) return 1;
+ if (deltaOfDelta >= -64n && deltaOfDelta <= 63n) return 9;
+ if (deltaOfDelta >= -256n && deltaOfDelta <= 255n) return 12;
+ if (deltaOfDelta >= -2048n && deltaOfDelta <= 2047n) return 16;
+ return 36;
+}
+
+/** Encoded byte count, or -1 when a delta-of-delta leaves int32 range. */
+export function qwpGorillaSize(timestamps: readonly bigint[]): number {
+ if (timestamps.length === 0) return 0;
+ if (timestamps.length === 1) return 8;
+ if (timestamps.length === 2) return 16;
+ let previousTimestamp = timestamps[1];
+ let previousDelta = timestamps[1] - timestamps[0];
+ let bits = 0;
+ for (let index = 2; index < timestamps.length; index++) {
+ const delta = timestamps[index] - previousTimestamp;
+ const deltaOfDelta = delta - previousDelta;
+ if (deltaOfDelta < INT32_MIN || deltaOfDelta > INT32_MAX) return -1;
+ bits += encodedDeltaBits(deltaOfDelta);
+ previousDelta = delta;
+ previousTimestamp = timestamps[index];
+ }
+ return 16 + Math.ceil(bits / 8);
+}
+
+/** Encodes timestamps with the QWP LSB-first Gorilla variant. */
+export function encodeQwpGorilla(timestamps: readonly bigint[]): Uint8Array {
+ const size = qwpGorillaSize(timestamps);
+ if (size < 0) {
+ throw new Error("Gorilla delta-of-delta is outside the int32 range");
+ }
+ const writer = new QwpByteWriter(Math.max(size, 1));
+ if (timestamps.length === 0) return writer.toUint8Array();
+ writer.writeBigInt64(timestamps[0]);
+ if (timestamps.length === 1) return writer.toUint8Array();
+ writer.writeBigInt64(timestamps[1]);
+ if (timestamps.length === 2) return writer.toUint8Array();
+
+ const bits = new QwpBitWriter(size - 16);
+ let previousTimestamp = timestamps[1];
+ let previousDelta = timestamps[1] - timestamps[0];
+ for (let index = 2; index < timestamps.length; index++) {
+ const delta = timestamps[index] - previousTimestamp;
+ const deltaOfDelta = delta - previousDelta;
+ // Prefixes are bit-reversed because QWP packs bits least-significant first.
+ if (deltaOfDelta === 0n) {
+ bits.writeBits(0, 1);
+ } else if (deltaOfDelta >= -64n && deltaOfDelta <= 63n) {
+ bits.writeBits(0b01, 2);
+ bits.writeBits(Number(deltaOfDelta & 0x7fn), 7);
+ } else if (deltaOfDelta >= -256n && deltaOfDelta <= 255n) {
+ bits.writeBits(0b011, 3);
+ bits.writeBits(Number(deltaOfDelta & 0x1ffn), 9);
+ } else if (deltaOfDelta >= -2048n && deltaOfDelta <= 2047n) {
+ bits.writeBits(0b0111, 4);
+ bits.writeBits(Number(deltaOfDelta & 0xfffn), 12);
+ } else {
+ bits.writeBits(0b1111, 4);
+ bits.writeBits(Number(deltaOfDelta & 0xffffffffn), 32);
+ }
+ previousDelta = delta;
+ previousTimestamp = timestamps[index];
+ }
+ writer.writeBytes(bits.finish());
+ return writer.toUint8Array();
+}
diff --git a/packages/client-core/src/_qwp/_core/identifiers.ts b/packages/client-core/src/_qwp/_core/identifiers.ts
new file mode 100644
index 0000000..017592e
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/identifiers.ts
@@ -0,0 +1,102 @@
+import { utf8Length } from "./bytes";
+
+function isIllegalCommonIdentifierCharacter(
+ character: string,
+ codeUnit: number,
+): boolean {
+ if (codeUnit <= 0x0f || codeUnit === 0x7f || codeUnit === 0xfeff) {
+ return true;
+ }
+ switch (character) {
+ case "?":
+ case ",":
+ case "'":
+ case '"':
+ case "\\":
+ case "/":
+ case ":":
+ case ")":
+ case "(":
+ case "+":
+ case "*":
+ case "%":
+ case "~":
+ return true;
+ default:
+ return false;
+ }
+}
+
+/** @internal Applies Java TableUtils rules and the QWP UTF-8 byte limit. */
+export function validateQwpTableName(
+ name: string,
+ maxNameLength: number,
+): void {
+ if (name.length === 0) throw new Error("table name cannot be empty");
+ if (utf8Length(name) > maxNameLength) {
+ throw new Error(`table name too long [maxLength=${maxNameLength}]`);
+ }
+ if (name.charAt(0) === " " || name.charAt(name.length - 1) === " ") {
+ throw new Error(`table name contains illegal characters: ${name}`);
+ }
+ for (let index = 0; index < name.length; index++) {
+ const character = name.charAt(index);
+ if (
+ (character === "." &&
+ (index === 0 ||
+ index === name.length - 1 ||
+ name.charAt(index - 1) === ".")) ||
+ isIllegalCommonIdentifierCharacter(character, name.charCodeAt(index))
+ ) {
+ throw new Error(`table name contains illegal characters: ${name}`);
+ }
+ }
+}
+
+/** @internal Applies Java TableUtils rules and the QWP UTF-8 byte limit. */
+export function validateQwpColumnName(
+ name: string,
+ maxNameLength: number,
+): void {
+ if (name.length === 0) throw new Error("column name cannot be empty");
+ if (utf8Length(name) > maxNameLength) {
+ throw new Error(`column name too long [maxLength=${maxNameLength}]`);
+ }
+ for (let index = 0; index < name.length; index++) {
+ const character = name.charAt(index);
+ if (
+ character === "." ||
+ character === "-" ||
+ isIllegalCommonIdentifierCharacter(character, name.charCodeAt(index))
+ ) {
+ throw new Error(`column name contains illegal characters: ${name}`);
+ }
+ }
+}
+
+/**
+ * @internal Java's LowerCaseCharSequenceIntHashMap lowercases each UTF-16 code
+ * unit independently. Taking the first code unit avoids JavaScript's one
+ * expanding lowercase mapping (U+0130) and gives the same simple mapping.
+ */
+export function qwpColumnNameKey(name: string): string {
+ // Fast path: a name of only lower-case-stable code units -- ASCII other than
+ // A-Z -- already equals its key, so it is returned without rebuilding. The
+ // first upper-case ASCII letter or non-ASCII code unit (which may lower-case
+ // or expand) drops to the per-code-unit mapping below, resuming from the
+ // stable prefix. This runs once per cell on the ingest path, so the common
+ // all-lower-case name skips the character-by-character rebuild entirely.
+ let index = 0;
+ for (; index < name.length; index++) {
+ const code = name.charCodeAt(index);
+ if (code >= 0x80 || (code >= 0x41 && code <= 0x5a)) break;
+ }
+ if (index === name.length) return name;
+
+ let key = name.slice(0, index);
+ for (; index < name.length; index++) {
+ const character = name.charAt(index);
+ key += character.toLowerCase().charAt(0);
+ }
+ return key;
+}
diff --git a/packages/client-core/src/_qwp/_core/index.ts b/packages/client-core/src/_qwp/_core/index.ts
new file mode 100644
index 0000000..e7c3db3
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/index.ts
@@ -0,0 +1,15 @@
+export * from "./bytes";
+export * from "./binds";
+export * from "./compression";
+export * from "./constants";
+export * from "./durable-ack";
+export * from "./egress";
+export * from "./errors";
+export * from "./frame";
+export * from "./gorilla";
+export * from "./ingress";
+export * from "./result-batch";
+export * from "./symbol-dictionary";
+export * from "./table";
+export * from "./varint";
+export * from "./zstd";
diff --git a/packages/client-core/src/_qwp/_core/ingress.ts b/packages/client-core/src/_qwp/_core/ingress.ts
new file mode 100644
index 0000000..145dc4a
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/ingress.ts
@@ -0,0 +1,846 @@
+import { encodeUtf8, QwpByteReader, QwpByteWriter, utf8Length } from "./bytes";
+import {
+ QWP_COLUMN_TYPE,
+ QWP_ENCODING_GORILLA,
+ QWP_ENCODING_UNCOMPRESSED,
+ QWP_FLAG_DEFER_COMMIT,
+ QWP_FLAG_DELTA_SYMBOL_DICTIONARY,
+ QWP_FLAG_DURABLE_ACK_POLL,
+ QWP_FLAG_GORILLA,
+ QWP_HEADER_SIZE,
+ QWP_MAX_ARRAY_DIMENSION_LENGTH,
+ QWP_MAX_ARRAY_DIMENSIONS,
+ QWP_MAX_ERROR_MESSAGE_LENGTH,
+ QWP_MAX_ROWS_PER_TABLE,
+ QWP_MAX_SYMBOL_DICTIONARY_SIZE,
+ QWP_STATUS,
+ QwpColumnType,
+} from "./constants";
+import { decodeQwpFrame, writeQwpFrameHeader } from "./frame";
+import { QwpProtocolError } from "./errors";
+import { encodeQwpGorilla, qwpGorillaSize } from "./gorilla";
+import { QwpSymbolDictionary } from "./symbol-dictionary";
+import {
+ QwpArrayValue,
+ QwpColumnBuffer,
+ QwpSymbolValue,
+ QwpTableBuffer,
+} from "./table";
+import { qwpVarintSize, readQwpVarintNumber, writeQwpVarint } from "./varint";
+
+export interface QwpIngressEncodeOptions {
+ gorilla?: boolean;
+ /** Present means connection-scoped delta dictionary mode. */
+ dictionary?: QwpSymbolDictionary;
+ /** Highest global symbol ID already published on this logical connection. */
+ confirmedMaxSymbolId?: number;
+ deferCommit?: boolean;
+}
+
+/**
+ * Per-column work that both encoder passes need, computed once.
+ *
+ * Sizing a column and writing it derive the same three things, and deriving
+ * them twice is not free: the Gorilla path rebuilt the bigint array and ran
+ * the bit-packing size computation in each pass, then encodeQwpGorilla() ran
+ * it a third time before encoding. Measured on the repository's own 10k-row
+ * benchmark workloads that cost 1.8x (trades) to 2.3x (sparse) of total frame
+ * encode time, for byte-identical output.
+ */
+interface ColumnPlan {
+ nullCount: number;
+ /** Gorilla bytes, or null when the column is written as raw int64s. */
+ gorilla?: Uint8Array | null;
+ /** Inline dictionary, built only when delta symbols are off. */
+ inline?: InlineSymbolDictionary;
+}
+
+interface ColumnEncodeOptions {
+ gorilla: boolean;
+ deltaSymbols: boolean;
+ dictionary?: QwpSymbolDictionary;
+ /**
+ * Scoped to a single encodeQwpIngressFrame() call, so a column mutated
+ * between calls can never be sized from a stale plan.
+ */
+ plans: Map;
+}
+
+function columnPlan(
+ column: QwpColumnBuffer,
+ options: ColumnEncodeOptions,
+): ColumnPlan {
+ let plan = options.plans.get(column);
+ if (!plan) {
+ plan = { nullCount: nullCount(column) };
+ options.plans.set(column, plan);
+ }
+ return plan;
+}
+
+/** Gorilla bytes for a timestamp column, or null when it stays uncompressed. */
+function plannedGorilla(
+ column: QwpColumnBuffer,
+ options: ColumnEncodeOptions,
+): Uint8Array | null {
+ const plan = columnPlan(column, options);
+ if (plan.gorilla === undefined) {
+ const timestamps = column.values.map((value) => BigInt(value as bigint));
+ plan.gorilla =
+ timestamps.length > 2 && qwpGorillaSize(timestamps) > 0
+ ? encodeQwpGorilla(timestamps)
+ : null;
+ }
+ return plan.gorilla;
+}
+
+function plannedInlineSymbols(
+ column: QwpColumnBuffer,
+ options: ColumnEncodeOptions,
+): InlineSymbolDictionary {
+ const plan = columnPlan(column, options);
+ plan.inline ??= inlineSymbolDictionary(column.values);
+ return plan.inline;
+}
+
+export interface QwpIngressTableResult {
+ name: string;
+ sequenceTransaction: bigint;
+}
+
+export interface QwpIngressResponse {
+ status: number;
+ sequence: bigint | null;
+ tables: QwpIngressTableResult[];
+ errorMessage?: string;
+}
+
+/** Decodes the browser-requested ingress SERVER_INFO payload when present. */
+export function decodeQwpIngressServerInfo(
+ payload: Uint8Array,
+): number | undefined {
+ if (payload[0] !== QWP_STATUS.SERVER_INFO) return undefined;
+ if (payload.byteLength !== 5) {
+ throw new QwpProtocolError("invalid QWP ingress SERVER_INFO length");
+ }
+ const maxBatchSizeBytes = new DataView(
+ payload.buffer,
+ payload.byteOffset,
+ payload.byteLength,
+ ).getUint32(1, true);
+ if (maxBatchSizeBytes === 0) {
+ throw new QwpProtocolError("invalid QWP ingress SERVER_INFO batch cap");
+ }
+ return maxBatchSizeBytes;
+}
+
+function symbolText(value: unknown): string {
+ if (typeof value === "string") return value;
+ // A bare dictionary ID carries no text, and this encoder builds its inline
+ // dictionary out of the texts, so there is nothing to resolve it against.
+ // Reading `.text` off a number yields undefined, which TextEncoder happily
+ // encodes as zero bytes -- every symbol in the frame would collapse into one
+ // empty-string entry and be acknowledged as if it were correct. Say so
+ // instead. symbolId() accepts the numeric form because the delta encoder is
+ // given the dictionary that gives it meaning.
+ if (typeof value === "number") {
+ throw new Error(
+ `QWP symbol ID ${value} needs a symbol dictionary; pass one to encode a delta frame, or supply the symbol as a string or {id, text}`,
+ );
+ }
+ const text = (value as QwpSymbolValue)?.text;
+ if (typeof text !== "string") {
+ throw new Error(
+ "QWP symbol value must be a string or a {id, text} pair, received " +
+ (value === null ? "null" : typeof value),
+ );
+ }
+ return text;
+}
+
+function symbolId(value: unknown, dictionary: QwpSymbolDictionary): number {
+ if (typeof value === "string") return dictionary.getOrAdd(value);
+ const id = typeof value === "number" ? value : (value as QwpSymbolValue).id;
+ if (!Number.isSafeInteger(id) || id < 0 || id >= dictionary.size) {
+ throw new Error(`QWP symbol ID is outside the dictionary: ${id}`);
+ }
+ if (typeof value !== "number") {
+ const symbol = value as QwpSymbolValue;
+ if (dictionary.valueAt(id) !== symbol.text) {
+ throw new Error(
+ `QWP symbol value does not match dictionary ID ${id}: '${symbol.text}'`,
+ );
+ }
+ }
+ return id;
+}
+
+interface InlineSymbolDictionary {
+ /** Distinct symbol texts in first-seen order, matching Set iteration. */
+ readonly entries: readonly string[];
+ /** The dictionary index of each row's value, in row order. */
+ readonly rowIds: readonly number[];
+}
+
+// A non-delta ("full") symbol column carries its own inline dictionary.
+// Resolving each row against it with Array.prototype.indexOf is O(rows x
+// distinct) -- measured quadratic, 67x slower than delta mode at 32k rows. A
+// Map keyed by text makes each lookup O(1), the same fix
+// QwpSymbolDictionary.getOrAdd already applies in delta mode. symbolText() runs
+// once per value here, so measureColumn and writeColumn no longer resolve each
+// value twice.
+function inlineSymbolDictionary(
+ values: readonly unknown[],
+): InlineSymbolDictionary {
+ const entries: string[] = [];
+ const indexByText = new Map();
+ const rowIds = new Array(values.length);
+ for (let row = 0; row < values.length; row++) {
+ const text = symbolText(values[row]);
+ let id = indexByText.get(text);
+ if (id === undefined) {
+ id = entries.length;
+ indexByText.set(text, id);
+ entries.push(text);
+ }
+ rowIds[row] = id;
+ }
+ return { entries, rowIds };
+}
+
+function nullCount(column: QwpColumnBuffer): number {
+ let count = 0;
+ for (const value of column.nulls) if (value) count++;
+ return count;
+}
+
+function fixedWidth(type: QwpColumnType): number | undefined {
+ switch (type) {
+ case QWP_COLUMN_TYPE.BYTE:
+ return 1;
+ case QWP_COLUMN_TYPE.SHORT:
+ case QWP_COLUMN_TYPE.CHAR:
+ return 2;
+ case QWP_COLUMN_TYPE.INT:
+ case QWP_COLUMN_TYPE.FLOAT:
+ case QWP_COLUMN_TYPE.IPV4:
+ return 4;
+ case QWP_COLUMN_TYPE.LONG:
+ case QWP_COLUMN_TYPE.DOUBLE:
+ case QWP_COLUMN_TYPE.DATE:
+ return 8;
+ case QWP_COLUMN_TYPE.UUID:
+ return 16;
+ case QWP_COLUMN_TYPE.LONG256:
+ return 32;
+ default:
+ return undefined;
+ }
+}
+
+function qwpStringSize(value: string): number {
+ const length = utf8Length(value);
+ return qwpVarintSize(length) + length;
+}
+
+function writeQwpString(writer: QwpByteWriter, value: string): void {
+ const bytes = encodeUtf8(value);
+ writeQwpVarint(writer, bytes.length);
+ writer.writeBytes(bytes);
+}
+
+function binaryValue(value: unknown, width?: number): Uint8Array {
+ if (!(value instanceof Uint8Array)) {
+ throw new Error("QWP binary values must be Uint8Array instances");
+ }
+ if (width !== undefined && value.length !== width) {
+ throw new Error(
+ `QWP binary value has length ${value.length}; expected ${width}`,
+ );
+ }
+ return value;
+}
+
+function columnPayloadSize(
+ column: QwpColumnBuffer,
+ rowCount: number,
+ options: ColumnEncodeOptions,
+): number {
+ let size = 1;
+ if (columnPlan(column, options).nullCount > 0) {
+ size += Math.ceil(rowCount / 8);
+ }
+ const valueCount = column.values.length;
+
+ if (column.type === QWP_COLUMN_TYPE.BOOLEAN) {
+ return size + Math.ceil(valueCount / 8);
+ }
+
+ // DATE is deliberately absent here. The protocol is asymmetric for it: on
+ // ingress the server parses DATE as a plain fixed-width int64
+ // (QwpTableBlockCursor dispatches TYPE_DATE to QwpFixedWidthColumnCursor,
+ // alongside LONG and UUID), while on egress it emits DATE through
+ // emitTimestampSlice with a per-column encoding byte. The result decoder in
+ // this package matches the egress side, so the two directions genuinely
+ // differ. Adding DATE to this branch makes every ingress frame carrying a
+ // DATE column misparse server-side.
+ if (
+ column.type === QWP_COLUMN_TYPE.TIMESTAMP ||
+ column.type === QWP_COLUMN_TYPE.TIMESTAMP_NANOS
+ ) {
+ if (!options.gorilla) return size + valueCount * 8;
+ const gorilla = plannedGorilla(column, options);
+ return size + 1 + (gorilla ? gorilla.byteLength : valueCount * 8);
+ }
+
+ const width = fixedWidth(column.type);
+ if (width !== undefined) return size + valueCount * width;
+
+ if (column.type === QWP_COLUMN_TYPE.SYMBOL) {
+ if (options.deltaSymbols) {
+ for (const value of column.values) {
+ size += qwpVarintSize(symbolId(value, options.dictionary!));
+ }
+ return size;
+ }
+ const { entries, rowIds } = plannedInlineSymbols(column, options);
+ size += qwpVarintSize(entries.length);
+ for (const entry of entries) size += qwpStringSize(entry);
+ for (const id of rowIds) size += qwpVarintSize(id);
+ return size;
+ }
+
+ if (
+ column.type === QWP_COLUMN_TYPE.VARCHAR ||
+ column.type === QWP_COLUMN_TYPE.BINARY
+ ) {
+ let dataLength = 0;
+ for (const value of column.values) {
+ dataLength +=
+ column.type === QWP_COLUMN_TYPE.VARCHAR
+ ? utf8Length(value as string)
+ : binaryValue(value).length;
+ }
+ return size + (valueCount + 1) * 4 + dataLength;
+ }
+
+ if (
+ column.type === QWP_COLUMN_TYPE.DOUBLE_ARRAY ||
+ column.type === QWP_COLUMN_TYPE.LONG_ARRAY
+ ) {
+ for (const value of column.values) {
+ const array = value as QwpArrayValue;
+ if (
+ array.dimensions.length === 0 ||
+ array.dimensions.length > QWP_MAX_ARRAY_DIMENSIONS
+ ) {
+ throw new RangeError(
+ `QWP array must have between 1 and ${QWP_MAX_ARRAY_DIMENSIONS} dimensions`,
+ );
+ }
+ for (const [index, dimension] of array.dimensions.entries()) {
+ if (
+ !Number.isSafeInteger(dimension) ||
+ dimension < 0 ||
+ dimension > QWP_MAX_ARRAY_DIMENSION_LENGTH
+ ) {
+ throw new RangeError(
+ `array dimension ${index} must be between 0 and ${QWP_MAX_ARRAY_DIMENSION_LENGTH}`,
+ );
+ }
+ }
+ size += 1 + array.dimensions.length * 4 + array.values.length * 8;
+ }
+ return size;
+ }
+
+ if (column.type === QWP_COLUMN_TYPE.GEOHASH) {
+ const precision = column.geohashPrecision ?? 1;
+ return (
+ size + qwpVarintSize(precision) + valueCount * Math.ceil(precision / 8)
+ );
+ }
+
+ if (column.type === QWP_COLUMN_TYPE.DECIMAL64) {
+ return size + 1 + valueCount * 8;
+ }
+ if (column.type === QWP_COLUMN_TYPE.DECIMAL128) {
+ return size + 1 + valueCount * 16;
+ }
+ if (column.type === QWP_COLUMN_TYPE.DECIMAL256) {
+ return size + 1 + valueCount * 32;
+ }
+
+ throw new Error(`unsupported QWP column type 0x${column.type.toString(16)}`);
+}
+
+function writeNullHeader(
+ writer: QwpByteWriter,
+ column: QwpColumnBuffer,
+ rowCount: number,
+ options: ColumnEncodeOptions,
+): void {
+ if (columnPlan(column, options).nullCount === 0) {
+ writer.writeUint8(0);
+ return;
+ }
+ writer.writeUint8(1);
+ const bitmap = new Uint8Array(Math.ceil(rowCount / 8));
+ for (let row = 0; row < rowCount; row++) {
+ if (column.nulls[row]) bitmap[row >>> 3] |= 1 << (row & 7);
+ }
+ writer.writeBytes(bitmap);
+}
+
+function writeSignedLittleEndian(
+ writer: QwpByteWriter,
+ value: bigint,
+ width: number,
+): void {
+ let remaining = BigInt.asIntN(width * 8, value);
+ for (let index = 0; index < width; index++) {
+ writer.writeUint8(Number(remaining & 0xffn));
+ remaining >>= 8n;
+ }
+}
+
+function writeColumn(
+ writer: QwpByteWriter,
+ column: QwpColumnBuffer,
+ rowCount: number,
+ options: ColumnEncodeOptions,
+): void {
+ writeNullHeader(writer, column, rowCount, options);
+
+ switch (column.type) {
+ case QWP_COLUMN_TYPE.BOOLEAN: {
+ const bitmap = new Uint8Array(Math.ceil(column.values.length / 8));
+ column.values.forEach((value, index) => {
+ if (value) bitmap[index >>> 3] |= 1 << (index & 7);
+ });
+ writer.writeBytes(bitmap);
+ return;
+ }
+ case QWP_COLUMN_TYPE.BYTE:
+ for (const value of column.values) writer.writeInt8(Number(value));
+ return;
+ case QWP_COLUMN_TYPE.SHORT:
+ for (const value of column.values) writer.writeInt16(Number(value));
+ return;
+ case QWP_COLUMN_TYPE.CHAR:
+ for (const value of column.values) {
+ const text = value as string;
+ if (text.length !== 1) {
+ throw new Error("QWP CHAR values must contain one UTF-16 code unit");
+ }
+ writer.writeUint16(text.charCodeAt(0));
+ }
+ return;
+ case QWP_COLUMN_TYPE.INT:
+ for (const value of column.values) writer.writeInt32(Number(value));
+ return;
+ case QWP_COLUMN_TYPE.IPV4:
+ for (const value of column.values)
+ writer.writeUint32(Number(value) >>> 0);
+ return;
+ case QWP_COLUMN_TYPE.FLOAT:
+ for (const value of column.values) writer.writeFloat32(Number(value));
+ return;
+ // DATE joins LONG here: raw int64s, no per-column encoding byte.
+ // See columnPayloadSize() for why it is not a timestamp on ingress.
+ case QWP_COLUMN_TYPE.LONG:
+ case QWP_COLUMN_TYPE.DATE:
+ for (const value of column.values) {
+ writer.writeBigInt64(BigInt(value as number | bigint));
+ }
+ return;
+ case QWP_COLUMN_TYPE.TIMESTAMP:
+ case QWP_COLUMN_TYPE.TIMESTAMP_NANOS: {
+ if (!options.gorilla) {
+ for (const value of column.values) {
+ writer.writeBigInt64(BigInt(value as bigint));
+ }
+ return;
+ }
+ const gorilla = plannedGorilla(column, options);
+ if (gorilla) {
+ writer.writeUint8(QWP_ENCODING_GORILLA);
+ writer.writeBytes(gorilla);
+ } else {
+ writer.writeUint8(QWP_ENCODING_UNCOMPRESSED);
+ for (const value of column.values) {
+ writer.writeBigInt64(BigInt(value as bigint));
+ }
+ }
+ return;
+ }
+ case QWP_COLUMN_TYPE.DOUBLE:
+ for (const value of column.values) writer.writeFloat64(Number(value));
+ return;
+ case QWP_COLUMN_TYPE.UUID:
+ for (const value of column.values) {
+ writer.writeBytes(binaryValue(value, 16));
+ }
+ return;
+ case QWP_COLUMN_TYPE.LONG256:
+ for (const value of column.values) {
+ writer.writeBytes(binaryValue(value, 32));
+ }
+ return;
+ case QWP_COLUMN_TYPE.SYMBOL: {
+ if (options.deltaSymbols) {
+ for (const value of column.values) {
+ writeQwpVarint(writer, symbolId(value, options.dictionary!));
+ }
+ return;
+ }
+ const { entries, rowIds } = plannedInlineSymbols(column, options);
+ writeQwpVarint(writer, entries.length);
+ for (const entry of entries) writeQwpString(writer, entry);
+ for (const id of rowIds) writeQwpVarint(writer, id);
+ return;
+ }
+ case QWP_COLUMN_TYPE.VARCHAR:
+ case QWP_COLUMN_TYPE.BINARY: {
+ const parts = column.values.map((value) =>
+ column.type === QWP_COLUMN_TYPE.VARCHAR
+ ? encodeUtf8(value as string)
+ : binaryValue(value),
+ );
+ let cumulative = 0;
+ writer.writeUint32(0);
+ for (const part of parts) {
+ cumulative += part.length;
+ writer.writeUint32(cumulative);
+ }
+ for (const part of parts) writer.writeBytes(part);
+ return;
+ }
+ case QWP_COLUMN_TYPE.DOUBLE_ARRAY:
+ case QWP_COLUMN_TYPE.LONG_ARRAY:
+ for (const value of column.values) {
+ const array = value as QwpArrayValue;
+ writer.writeUint8(array.dimensions.length);
+ for (const dimension of array.dimensions) writer.writeUint32(dimension);
+ for (const item of array.values) {
+ if (column.type === QWP_COLUMN_TYPE.DOUBLE_ARRAY) {
+ writer.writeFloat64(Number(item));
+ } else {
+ writer.writeBigInt64(BigInt(item));
+ }
+ }
+ }
+ return;
+ case QWP_COLUMN_TYPE.GEOHASH: {
+ const precision = column.geohashPrecision ?? 1;
+ writeQwpVarint(writer, precision);
+ const width = Math.ceil(precision / 8);
+ for (const value of column.values) {
+ let remaining = BigInt(value as bigint);
+ for (let index = 0; index < width; index++) {
+ writer.writeUint8(Number(remaining & 0xffn));
+ remaining >>= 8n;
+ }
+ }
+ return;
+ }
+ case QWP_COLUMN_TYPE.DECIMAL64:
+ case QWP_COLUMN_TYPE.DECIMAL128:
+ case QWP_COLUMN_TYPE.DECIMAL256: {
+ writer.writeUint8(column.decimalScale ?? 0);
+ const width =
+ column.type === QWP_COLUMN_TYPE.DECIMAL64
+ ? 8
+ : column.type === QWP_COLUMN_TYPE.DECIMAL128
+ ? 16
+ : 32;
+ for (const value of column.values) {
+ writeSignedLittleEndian(writer, BigInt(value as bigint), width);
+ }
+ return;
+ }
+ default:
+ throw new Error("unsupported QWP column type");
+ }
+}
+
+function tableSize(
+ table: QwpTableBuffer,
+ options: ColumnEncodeOptions,
+): number {
+ let size =
+ qwpStringSize(table.name) +
+ qwpVarintSize(table.rowCount) +
+ qwpVarintSize(table.columns.length);
+ for (const column of table.columns) size += qwpStringSize(column.name) + 1;
+ for (const column of table.columns) {
+ size += columnPayloadSize(column, table.rowCount, options);
+ }
+ return size;
+}
+
+function validateTableForEncoding(table: QwpTableBuffer): void {
+ for (const column of table.columns) {
+ if (
+ column.size !== table.rowCount ||
+ column.nulls.length !== table.rowCount
+ ) {
+ throw new Error(
+ `table '${table.name}' has an unfinished row in column '${column.name}'`,
+ );
+ }
+ let nonNullCount = 0;
+ for (const isNull of column.nulls) if (!isNull) nonNullCount++;
+ if (nonNullCount !== column.values.length) {
+ throw new Error(
+ `table '${table.name}' column '${column.name}' has ${nonNullCount} non-null row(s) but ${column.values.length} value(s)`,
+ );
+ }
+ }
+}
+
+/** Encodes one QWP v1 ingress message. */
+export function encodeQwpIngressFrame(
+ tables: readonly QwpTableBuffer[],
+ options: QwpIngressEncodeOptions = {},
+): Uint8Array {
+ const dictionarySize = options.dictionary?.size;
+ try {
+ return encodeQwpIngressFrameInternal(tables, options);
+ } catch (error) {
+ if (dictionarySize !== undefined)
+ options.dictionary!.truncate(dictionarySize);
+ throw error;
+ }
+}
+
+function encodeQwpIngressFrameInternal(
+ tables: readonly QwpTableBuffer[],
+ options: QwpIngressEncodeOptions,
+): Uint8Array {
+ if (tables.length > 0xffff) {
+ throw new Error("QWP frame contains more than 65535 tables");
+ }
+ for (const table of tables) {
+ validateTableForEncoding(table);
+ if (table.rowCount > QWP_MAX_ROWS_PER_TABLE) {
+ throw new Error(
+ `table '${table.name}' contains ${table.rowCount} rows; maximum is ${QWP_MAX_ROWS_PER_TABLE}`,
+ );
+ }
+ }
+
+ const gorilla = options.gorilla ?? true;
+ const deltaSymbols = options.dictionary !== undefined;
+ if (deltaSymbols) {
+ const published = options.confirmedMaxSymbolId ?? -1;
+ if (
+ !Number.isSafeInteger(published) ||
+ published < -1 ||
+ published >= options.dictionary!.size
+ ) {
+ throw new RangeError(
+ `published symbol dictionary ID is out of range [id=${published}, size=${options.dictionary!.size}]`,
+ );
+ }
+ }
+ if (deltaSymbols) {
+ // Resolve string values before calculating the delta prefix and frame size.
+ for (const table of tables) {
+ for (const column of table.columns) {
+ if (column.type !== QWP_COLUMN_TYPE.SYMBOL) continue;
+ for (const value of column.values) {
+ if (typeof value === "string") options.dictionary!.getOrAdd(value);
+ }
+ }
+ }
+ }
+ const deltaStart = deltaSymbols
+ ? (options.confirmedMaxSymbolId ?? -1) + 1
+ : 0;
+ const dictionaryEntries = deltaSymbols
+ ? options.dictionary!.entriesFrom(deltaStart)
+ : [];
+ const columnOptions: ColumnEncodeOptions = {
+ gorilla,
+ deltaSymbols,
+ dictionary: options.dictionary,
+ // Shared by the sizing pass below and the write pass further down, so
+ // each column derives its null count, Gorilla bytes and inline symbol
+ // dictionary exactly once per frame.
+ plans: new Map(),
+ };
+
+ let flags = 0;
+ if (gorilla) flags |= QWP_FLAG_GORILLA;
+ if (deltaSymbols) flags |= QWP_FLAG_DELTA_SYMBOL_DICTIONARY;
+ if (options.deferCommit) flags |= QWP_FLAG_DEFER_COMMIT;
+
+ let payloadLength = 0;
+ if (deltaSymbols) {
+ payloadLength +=
+ qwpVarintSize(deltaStart) + qwpVarintSize(dictionaryEntries.length);
+ for (const entry of dictionaryEntries)
+ payloadLength += qwpStringSize(entry);
+ }
+ for (const table of tables) payloadLength += tableSize(table, columnOptions);
+
+ const writer = new QwpByteWriter(QWP_HEADER_SIZE + payloadLength);
+ writeQwpFrameHeader(writer, {
+ flags,
+ tableCount: tables.length,
+ payloadLength,
+ });
+ if (deltaSymbols) {
+ writeQwpVarint(writer, deltaStart);
+ writeQwpVarint(writer, dictionaryEntries.length);
+ for (const entry of dictionaryEntries) writeQwpString(writer, entry);
+ }
+ for (const table of tables) {
+ writeQwpString(writer, table.name);
+ writeQwpVarint(writer, table.rowCount);
+ writeQwpVarint(writer, table.columns.length);
+ for (const column of table.columns) {
+ writeQwpString(writer, column.name);
+ writer.writeUint8(column.type);
+ }
+ for (const column of table.columns) {
+ writeColumn(writer, column, table.rowCount, columnOptions);
+ }
+ }
+ const result = writer.toUint8Array();
+ if (result.length !== QWP_HEADER_SIZE + payloadLength) {
+ throw new Error(
+ `QWP frame size mismatch [expected=${QWP_HEADER_SIZE + payloadLength}, actual=${result.length}]`,
+ );
+ }
+ return result;
+}
+
+export interface QwpIngressSymbolDictionaryDelta {
+ readonly startId: number;
+ readonly entries: readonly string[];
+}
+
+/** Reads the connection-scoped dictionary prefix from a delta ingress frame. */
+export function decodeQwpIngressSymbolDictionaryDelta(
+ bytes: Uint8Array,
+): QwpIngressSymbolDictionaryDelta | undefined {
+ const frame = decodeQwpFrame(bytes);
+ if ((frame.flags & QWP_FLAG_DELTA_SYMBOL_DICTIONARY) === 0) return undefined;
+ const reader = new QwpByteReader(frame.payload);
+ const startId = readQwpVarintNumber(reader, "symbol dictionary start ID");
+ const count = readQwpVarintNumber(reader, "symbol dictionary entry count");
+ if (startId + count > QWP_MAX_SYMBOL_DICTIONARY_SIZE) {
+ throw new QwpProtocolError(
+ `QWP symbol dictionary exceeds maximum size ${QWP_MAX_SYMBOL_DICTIONARY_SIZE}`,
+ );
+ }
+ const entries: string[] = [];
+ for (let index = 0; index < count; index++) {
+ const length = readQwpVarintNumber(
+ reader,
+ "symbol dictionary entry length",
+ );
+ entries.push(reader.readUtf8(length, "symbol dictionary entry"));
+ }
+ return { startId, entries };
+}
+
+/** Encodes a table-less committed dictionary catch-up frame. */
+export function encodeQwpIngressSymbolDictionaryFrame(
+ startId: number,
+ entries: readonly string[],
+): Uint8Array {
+ if (!Number.isSafeInteger(startId) || startId < 0) {
+ throw new RangeError("symbol dictionary start ID must be non-negative");
+ }
+ if (startId + entries.length > QWP_MAX_SYMBOL_DICTIONARY_SIZE) {
+ throw new RangeError(
+ `symbol dictionary exceeds maximum size ${QWP_MAX_SYMBOL_DICTIONARY_SIZE}`,
+ );
+ }
+ let payloadLength = qwpVarintSize(startId) + qwpVarintSize(entries.length);
+ for (const entry of entries) payloadLength += qwpStringSize(entry);
+ const writer = new QwpByteWriter(QWP_HEADER_SIZE + payloadLength);
+ writeQwpFrameHeader(writer, {
+ flags: QWP_FLAG_DELTA_SYMBOL_DICTIONARY,
+ tableCount: 0,
+ payloadLength,
+ });
+ writeQwpVarint(writer, startId);
+ writeQwpVarint(writer, entries.length);
+ for (const entry of entries) writeQwpString(writer, entry);
+ return writer.toUint8Array();
+}
+
+export function encodeQwpIngressCommitFrame(
+ dictionary?: QwpSymbolDictionary,
+ confirmedMaxSymbolId = -1,
+): Uint8Array {
+ return encodeQwpIngressFrame([], {
+ gorilla: false,
+ dictionary,
+ confirmedMaxSymbolId,
+ });
+}
+
+/** Encodes a negotiated, side-effect-free durable-ACK progress poll. */
+export function encodeQwpDurableAckPollFrame(): Uint8Array {
+ const writer = new QwpByteWriter(QWP_HEADER_SIZE);
+ writeQwpFrameHeader(writer, {
+ flags: QWP_FLAG_DURABLE_ACK_POLL,
+ tableCount: 0,
+ payloadLength: 0,
+ });
+ return writer.toUint8Array();
+}
+
+function readIngressTables(
+ reader: QwpByteReader,
+ count: number,
+): QwpIngressTableResult[] {
+ const tables: QwpIngressTableResult[] = [];
+ for (let index = 0; index < count; index++) {
+ const nameLength = reader.readUint16("ingress table name length");
+ const name = reader.readUtf8(nameLength, "ingress table name");
+ const sequenceTransaction = reader.readBigInt64(
+ "ingress table sequence transaction",
+ );
+ tables.push({ name, sequenceTransaction });
+ }
+ return tables;
+}
+
+/** Decodes an ingress ACK, durable ACK, or NACK WebSocket payload. */
+export function decodeQwpIngressResponse(
+ payload: Uint8Array,
+): QwpIngressResponse {
+ const reader = new QwpByteReader(payload);
+ const status = reader.readUint8("ingress response status");
+
+ if (status === QWP_STATUS.DURABLE_ACK) {
+ const count = reader.readUint16("durable ACK table count");
+ const tables = readIngressTables(reader, count);
+ reader.expectEnd("durable ACK");
+ return { status, sequence: null, tables };
+ }
+
+ const sequence = reader.readBigUint64("ingress response sequence");
+ if (status === QWP_STATUS.OK) {
+ const count = reader.readUint16("ACK table count");
+ const tables = readIngressTables(reader, count);
+ reader.expectEnd("ingress ACK");
+ return { status, sequence, tables };
+ }
+
+ const messageLength = reader.readUint16("NACK message length");
+ if (messageLength > QWP_MAX_ERROR_MESSAGE_LENGTH) {
+ throw new QwpProtocolError(
+ `QWP error message exceeds ${QWP_MAX_ERROR_MESSAGE_LENGTH} bytes`,
+ );
+ }
+ const errorMessage = reader.readUtf8(messageLength, "NACK message");
+ reader.expectEnd("ingress NACK");
+ return { status, sequence, tables: [], errorMessage };
+}
diff --git a/packages/client-core/src/_qwp/_core/result-batch.ts b/packages/client-core/src/_qwp/_core/result-batch.ts
new file mode 100644
index 0000000..7c4a309
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/result-batch.ts
@@ -0,0 +1,1956 @@
+import { decodeUtf8, QwpByteReader } from "./bytes";
+import {
+ QWP_COLUMN_TYPE,
+ QWP_FLAG_DELTA_SYMBOL_DICTIONARY,
+ QWP_FLAG_GORILLA,
+ QWP_FLAG_ZSTD,
+ QWP_MAX_CELLS_PER_BATCH,
+ QWP_MAX_COLUMNS_PER_TABLE,
+ QWP_MAX_IDENTIFIER_BYTES,
+ QWP_RESET_MASK_DICTIONARY,
+ QwpColumnType,
+} from "./constants";
+import { QwpResultBatchMessage } from "./egress";
+import { QwpProtocolError } from "./errors";
+import { readQwpVarint } from "./varint";
+import { decompressQwpZstdFrame } from "./zstd";
+
+const MAX_ARRAY_DIMENSION_LENGTH = (1 << 28) - 1;
+const MAX_ARRAY_ELEMENTS = 268_435_327;
+// Matches QuestDB's connection-scoped symbol dictionary limit.
+const MAX_CONNECTION_SYMBOLS = 2_000_000;
+const MAX_ROWS_PER_BATCH = 1_048_576;
+
+export interface QwpDecimalValue {
+ unscaled: bigint;
+ scale: number;
+}
+
+export interface QwpUuidValue {
+ low: bigint;
+ high: bigint;
+}
+
+export interface QwpLong256Value {
+ /** Little-endian 64-bit words; word 0 is least significant. */
+ words: readonly [bigint, bigint, bigint, bigint];
+}
+
+export interface QwpGeohashValue {
+ bits: bigint;
+ precisionBits: number;
+}
+
+export interface QwpResultArrayValue {
+ dimensions: readonly number[];
+ values: readonly number[] | readonly bigint[];
+}
+
+export type QwpResultValue =
+ | boolean
+ | number
+ | bigint
+ | string
+ | Uint8Array
+ | QwpDecimalValue
+ | QwpUuidValue
+ | QwpLong256Value
+ | QwpGeohashValue
+ | QwpResultArrayValue
+ | null;
+
+export interface QwpResultColumnSchema {
+ name: string;
+ type: QwpColumnType;
+}
+
+export interface QwpResultColumn extends QwpResultColumnSchema {
+ values: readonly QwpResultValue[];
+ scale?: number;
+ precisionBits?: number;
+}
+
+export class QwpResultBatch {
+ constructor(
+ readonly requestId: bigint,
+ readonly batchSequence: bigint,
+ readonly tableName: string,
+ readonly rowCount: number,
+ readonly columns: readonly QwpResultColumn[],
+ ) {}
+
+ get(rowIndex: number, columnIndex: number): QwpResultValue {
+ if (
+ !Number.isInteger(rowIndex) ||
+ rowIndex < 0 ||
+ rowIndex >= this.rowCount
+ ) {
+ throw new RangeError(`row index out of range: ${rowIndex}`);
+ }
+ const column = this.columns[columnIndex];
+ if (!column)
+ throw new RangeError(`column index out of range: ${columnIndex}`);
+ return column.values[rowIndex];
+ }
+
+ *rows(): IterableIterator {
+ for (let row = 0; row < this.rowCount; row++) {
+ yield this.columns.map((column) => column.values[row]);
+ }
+ }
+}
+
+class QwpResultColumnViewLayout {
+ schema!: QwpResultColumnSchema;
+ rowCount = 0;
+ nonNullCount = 0;
+ nullBitmap?: Uint8Array;
+ nonNullIndexes?: Int32Array;
+ values?: Uint8Array;
+ valuesView?: DataView;
+ stringBytes?: Uint8Array;
+ symbolDictionary?: readonly string[];
+ symbolRowIds?: Int32Array;
+ arrayOffsets?: Int32Array;
+ arrayLengths?: Int32Array;
+ scale?: number;
+ precisionBits?: number;
+ private timestampStorage?: Uint8Array;
+ readonly localSymbols: string[] = [];
+
+ reset(schema: QwpResultColumnSchema, rowCount: number): void {
+ this.schema = schema;
+ this.rowCount = rowCount;
+ this.nonNullCount = 0;
+ this.nullBitmap = undefined;
+ this.values = undefined;
+ this.valuesView = undefined;
+ this.stringBytes = undefined;
+ this.symbolDictionary = undefined;
+ this.scale = undefined;
+ this.precisionBits = undefined;
+ this.localSymbols.length = 0;
+ }
+
+ release(): void {
+ // Drop frame-backed references immediately. Capacity-bearing scratch
+ // arrays remain attached to the layout for the next batch.
+ this.nullBitmap = undefined;
+ this.values = undefined;
+ this.valuesView = undefined;
+ this.stringBytes = undefined;
+ this.symbolDictionary = undefined;
+ this.localSymbols.length = 0;
+ }
+
+ setValues(bytes: Uint8Array): void {
+ this.values = bytes;
+ this.valuesView = new DataView(
+ bytes.buffer,
+ bytes.byteOffset,
+ bytes.byteLength,
+ );
+ }
+
+ ensureNonNullIndexes(size: number): Int32Array {
+ this.nonNullIndexes = ensureInt32Capacity(this.nonNullIndexes, size);
+ return this.nonNullIndexes;
+ }
+
+ ensureSymbolRowIds(size: number): Int32Array {
+ this.symbolRowIds = ensureInt32Capacity(this.symbolRowIds, size);
+ return this.symbolRowIds;
+ }
+
+ ensureArrayOffsets(size: number): Int32Array {
+ this.arrayOffsets = ensureInt32Capacity(this.arrayOffsets, size);
+ return this.arrayOffsets;
+ }
+
+ ensureArrayLengths(size: number): Int32Array {
+ this.arrayLengths = ensureInt32Capacity(this.arrayLengths, size);
+ return this.arrayLengths;
+ }
+
+ timestampBytes(size: number): Uint8Array {
+ if (!this.timestampStorage || this.timestampStorage.byteLength < size) {
+ let capacity = Math.max(64, this.timestampStorage?.byteLength ?? 0);
+ while (capacity < size) capacity *= 2;
+ this.timestampStorage = new Uint8Array(capacity);
+ }
+ return this.timestampStorage.subarray(0, size);
+ }
+
+ isNull(row: number): boolean {
+ const bitmap = this.nullBitmap;
+ return bitmap !== undefined && (bitmap[row >>> 3] & (1 << (row & 7))) !== 0;
+ }
+
+ denseIndex(row: number): number {
+ return this.nullBitmap ? this.nonNullIndexes![row] : row;
+ }
+}
+
+function ensureInt32Capacity(
+ current: Int32Array | undefined,
+ size: number,
+): Int32Array {
+ if (current && current.length >= size) return current;
+ let capacity = Math.max(16, current?.length ?? 0);
+ while (capacity < size) capacity *= 2;
+ return new Int32Array(capacity);
+}
+
+/**
+ * Reusable, zero-copy view over one QWP result column.
+ *
+ * The view and every byte slice returned from it are valid only while the
+ * surrounding queryViews() callback is running. Copy data that must outlive
+ * the callback.
+ */
+export class QwpResultColumnView {
+ /** @internal */
+ constructor(
+ private readonly batch: QwpResultBatchView,
+ readonly columnIndex: number,
+ ) {}
+
+ get name(): string {
+ return this.layout().schema.name;
+ }
+
+ get type(): QwpColumnType {
+ return this.layout().schema.type;
+ }
+
+ get rowCount(): number {
+ return this.layout().rowCount;
+ }
+
+ get nonNullCount(): number {
+ return this.layout().nonNullCount;
+ }
+
+ get scale(): number | undefined {
+ return this.layout().scale;
+ }
+
+ get precisionBits(): number | undefined {
+ return this.layout().precisionBits;
+ }
+
+ /** Fixed-width stride, zero for bit-packed BOOLEAN, or -1 when variable. */
+ get bytesPerValue(): number {
+ const layout = this.layout();
+ switch (layout.schema.type) {
+ case QWP_COLUMN_TYPE.BOOLEAN:
+ return 0;
+ case QWP_COLUMN_TYPE.BYTE:
+ return 1;
+ case QWP_COLUMN_TYPE.SHORT:
+ case QWP_COLUMN_TYPE.CHAR:
+ return 2;
+ case QWP_COLUMN_TYPE.INT:
+ case QWP_COLUMN_TYPE.FLOAT:
+ case QWP_COLUMN_TYPE.IPV4:
+ return 4;
+ case QWP_COLUMN_TYPE.LONG:
+ case QWP_COLUMN_TYPE.DOUBLE:
+ case QWP_COLUMN_TYPE.DATE:
+ case QWP_COLUMN_TYPE.TIMESTAMP:
+ case QWP_COLUMN_TYPE.TIMESTAMP_NANOS:
+ case QWP_COLUMN_TYPE.DECIMAL64:
+ return 8;
+ case QWP_COLUMN_TYPE.UUID:
+ case QWP_COLUMN_TYPE.DECIMAL128:
+ return 16;
+ case QWP_COLUMN_TYPE.LONG256:
+ case QWP_COLUMN_TYPE.DECIMAL256:
+ return 32;
+ case QWP_COLUMN_TYPE.GEOHASH:
+ return Math.ceil(layout.precisionBits! / 8);
+ default:
+ return -1;
+ }
+ }
+
+ isNull(rowIndex: number): boolean {
+ const layout = this.checkedLayout(rowIndex);
+ return layout.isNull(rowIndex);
+ }
+
+ nonNullIndex(rowIndex: number): number {
+ const layout = this.checkedLayout(rowIndex);
+ return layout.isNull(rowIndex) ? -1 : layout.denseIndex(rowIndex);
+ }
+
+ /** Raw per-row NULL bitmap, without copying. Undefined means no NULLs. */
+ nullBitmapBytes(): Uint8Array | undefined {
+ return this.layout().nullBitmap;
+ }
+
+ /**
+ * Raw packed non-null values. Fixed-width values use QWP little-endian
+ * layout; booleans are bit-packed and variable-width columns contain their
+ * uint32 offset table. SYMBOL returns undefined because IDs are varints.
+ */
+ valuesBytes(): Uint8Array | undefined {
+ return this.layout().values;
+ }
+
+ /** Concatenated VARCHAR/BINARY payload bytes, without copying. */
+ stringBytes(): Uint8Array | undefined {
+ return this.layout().stringBytes;
+ }
+
+ /** Reusable dense-index table; only the first rowCount entries are valid. */
+ nonNullIndexView(): Int32Array | undefined {
+ const layout = this.layout();
+ return layout.nullBitmap
+ ? layout.nonNullIndexes!.subarray(0, layout.rowCount)
+ : undefined;
+ }
+
+ /** Reusable per-row SYMBOL IDs; NULL-row entries are unspecified. */
+ symbolIdView(): Int32Array | undefined {
+ const layout = this.layout();
+ this.requireType(layout, QWP_COLUMN_TYPE.SYMBOL);
+ return layout.symbolRowIds?.subarray(0, layout.rowCount);
+ }
+
+ getBoolean(rowIndex: number): boolean {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.BOOLEAN,
+ );
+ if (dense < 0) return false;
+ return (layout.values![dense >>> 3] & (1 << (dense & 7))) !== 0;
+ }
+
+ getByte(rowIndex: number): number {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.BYTE,
+ );
+ return dense < 0 ? 0 : layout.valuesView!.getInt8(dense);
+ }
+
+ getShort(rowIndex: number): number {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.SHORT,
+ );
+ return dense < 0 ? 0 : layout.valuesView!.getInt16(dense * 2, true);
+ }
+
+ getChar(rowIndex: number): string {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.CHAR,
+ );
+ return dense < 0
+ ? "\0"
+ : String.fromCharCode(layout.valuesView!.getUint16(dense * 2, true));
+ }
+
+ getInt(rowIndex: number): number {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.INT,
+ QWP_COLUMN_TYPE.IPV4,
+ );
+ return dense < 0 ? 0 : layout.valuesView!.getInt32(dense * 4, true);
+ }
+
+ getFloat(rowIndex: number): number {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.FLOAT,
+ );
+ return dense < 0
+ ? Number.NaN
+ : layout.valuesView!.getFloat32(dense * 4, true);
+ }
+
+ getDouble(rowIndex: number): number {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.DOUBLE,
+ );
+ return dense < 0
+ ? Number.NaN
+ : layout.valuesView!.getFloat64(dense * 8, true);
+ }
+
+ getLong(rowIndex: number): bigint {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.LONG,
+ QWP_COLUMN_TYPE.DATE,
+ QWP_COLUMN_TYPE.TIMESTAMP,
+ QWP_COLUMN_TYPE.TIMESTAMP_NANOS,
+ );
+ return dense < 0 ? 0n : layout.valuesView!.getBigInt64(dense * 8, true);
+ }
+
+ /** Zero-copy UTF-8 bytes for a VARCHAR value. */
+ getUtf8View(rowIndex: number): Uint8Array | null {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.VARCHAR,
+ );
+ return dense < 0 ? null : variableWidthValue(layout, dense);
+ }
+
+ getString(rowIndex: number): string | null {
+ const layout = this.checkedLayout(rowIndex);
+ if (layout.schema.type === QWP_COLUMN_TYPE.SYMBOL) {
+ return this.getSymbol(rowIndex);
+ }
+ this.requireType(layout, QWP_COLUMN_TYPE.VARCHAR);
+ if (layout.isNull(rowIndex)) return null;
+ return decodeUtf8(variableWidthValue(layout, layout.denseIndex(rowIndex)));
+ }
+
+ /** Zero-copy BINARY bytes. */
+ getBinaryView(rowIndex: number): Uint8Array | null {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.BINARY,
+ );
+ return dense < 0 ? null : variableWidthValue(layout, dense);
+ }
+
+ getSymbolId(rowIndex: number): number {
+ const layout = this.checkedLayout(rowIndex);
+ this.requireType(layout, QWP_COLUMN_TYPE.SYMBOL);
+ return layout.isNull(rowIndex) ? -1 : layout.symbolRowIds![rowIndex];
+ }
+
+ getSymbol(rowIndex: number): string | null {
+ const layout = this.checkedLayout(rowIndex);
+ this.requireType(layout, QWP_COLUMN_TYPE.SYMBOL);
+ return layout.isNull(rowIndex)
+ ? null
+ : layout.symbolDictionary![layout.symbolRowIds![rowIndex]];
+ }
+
+ getSymbolForId(symbolId: number): string {
+ const layout = this.layout();
+ this.requireType(layout, QWP_COLUMN_TYPE.SYMBOL);
+ const dictionary = layout.symbolDictionary!;
+ if (
+ !Number.isInteger(symbolId) ||
+ symbolId < 0 ||
+ symbolId >= dictionary.length
+ ) {
+ throw new RangeError(`symbol ID out of range: ${symbolId}`);
+ }
+ return dictionary[symbolId];
+ }
+
+ get symbolDictionarySize(): number {
+ const layout = this.layout();
+ this.requireType(layout, QWP_COLUMN_TYPE.SYMBOL);
+ return layout.symbolDictionary!.length;
+ }
+
+ getUuidLow(rowIndex: number): bigint {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.UUID,
+ );
+ return dense < 0 ? 0n : layout.valuesView!.getBigUint64(dense * 16, true);
+ }
+
+ getUuidHigh(rowIndex: number): bigint {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.UUID,
+ );
+ return dense < 0
+ ? 0n
+ : layout.valuesView!.getBigUint64(dense * 16 + 8, true);
+ }
+
+ getLong256Word(rowIndex: number, wordIndex: number): bigint {
+ if (!Number.isInteger(wordIndex) || wordIndex < 0 || wordIndex > 3) {
+ throw new RangeError(`LONG256 word index out of range: ${wordIndex}`);
+ }
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.LONG256,
+ );
+ return dense < 0
+ ? 0n
+ : layout.valuesView!.getBigInt64(dense * 32 + wordIndex * 8, true);
+ }
+
+ getDecimalUnscaled(rowIndex: number): bigint {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.DECIMAL64,
+ QWP_COLUMN_TYPE.DECIMAL128,
+ QWP_COLUMN_TYPE.DECIMAL256,
+ );
+ if (dense < 0) return 0n;
+ const width = fixedTypeWidth(layout.schema.type);
+ return signedLittleEndianValue(layout.values!, dense * width, width);
+ }
+
+ getGeohashBits(rowIndex: number): bigint {
+ const { layout, dense } = this.valuePosition(
+ rowIndex,
+ QWP_COLUMN_TYPE.GEOHASH,
+ );
+ if (dense < 0) return 0n;
+ const width = Math.ceil(layout.precisionBits! / 8);
+ return unsignedLittleEndianValue(layout.values!, dense * width, width);
+ }
+
+ /** Zero-copy encoded ARRAY row, including dimension header. */
+ getArrayView(rowIndex: number): Uint8Array | null {
+ const layout = this.checkedLayout(rowIndex);
+ this.requireType(
+ layout,
+ QWP_COLUMN_TYPE.DOUBLE_ARRAY,
+ QWP_COLUMN_TYPE.LONG_ARRAY,
+ );
+ if (layout.isNull(rowIndex)) return null;
+ const offset = layout.arrayOffsets![rowIndex];
+ return layout.values!.subarray(
+ offset,
+ offset + layout.arrayLengths![rowIndex],
+ );
+ }
+
+ getArrayDimensionCount(rowIndex: number): number {
+ const layout = this.checkedLayout(rowIndex);
+ this.requireType(
+ layout,
+ QWP_COLUMN_TYPE.DOUBLE_ARRAY,
+ QWP_COLUMN_TYPE.LONG_ARRAY,
+ );
+ return layout.isNull(rowIndex)
+ ? 0
+ : layout.values![layout.arrayOffsets![rowIndex]];
+ }
+
+ /** Lazily materializes one cell; prefer typed/raw accessors on hot paths. */
+ get(rowIndex: number): QwpResultValue {
+ const layout = this.checkedLayout(rowIndex);
+ if (layout.isNull(rowIndex)) return null;
+ const dense = layout.denseIndex(rowIndex);
+ const view = layout.valuesView;
+ switch (layout.schema.type) {
+ case QWP_COLUMN_TYPE.BOOLEAN:
+ return (layout.values![dense >>> 3] & (1 << (dense & 7))) !== 0;
+ case QWP_COLUMN_TYPE.BYTE:
+ return view!.getInt8(dense);
+ case QWP_COLUMN_TYPE.SHORT:
+ return view!.getInt16(dense * 2, true);
+ case QWP_COLUMN_TYPE.CHAR:
+ return String.fromCharCode(view!.getUint16(dense * 2, true));
+ case QWP_COLUMN_TYPE.INT:
+ case QWP_COLUMN_TYPE.IPV4:
+ return view!.getInt32(dense * 4, true);
+ case QWP_COLUMN_TYPE.FLOAT:
+ return view!.getFloat32(dense * 4, true);
+ case QWP_COLUMN_TYPE.DOUBLE:
+ return view!.getFloat64(dense * 8, true);
+ case QWP_COLUMN_TYPE.LONG:
+ case QWP_COLUMN_TYPE.DATE:
+ case QWP_COLUMN_TYPE.TIMESTAMP:
+ case QWP_COLUMN_TYPE.TIMESTAMP_NANOS:
+ return view!.getBigInt64(dense * 8, true);
+ case QWP_COLUMN_TYPE.VARCHAR:
+ return decodeUtf8(variableWidthValue(layout, dense));
+ case QWP_COLUMN_TYPE.BINARY:
+ return variableWidthValue(layout, dense);
+ case QWP_COLUMN_TYPE.SYMBOL:
+ return layout.symbolDictionary![layout.symbolRowIds![rowIndex]];
+ case QWP_COLUMN_TYPE.UUID:
+ return {
+ low: view!.getBigUint64(dense * 16, true),
+ high: view!.getBigUint64(dense * 16 + 8, true),
+ };
+ case QWP_COLUMN_TYPE.LONG256:
+ return {
+ words: [
+ view!.getBigInt64(dense * 32, true),
+ view!.getBigInt64(dense * 32 + 8, true),
+ view!.getBigInt64(dense * 32 + 16, true),
+ view!.getBigInt64(dense * 32 + 24, true),
+ ],
+ };
+ case QWP_COLUMN_TYPE.DECIMAL64:
+ case QWP_COLUMN_TYPE.DECIMAL128:
+ case QWP_COLUMN_TYPE.DECIMAL256: {
+ const width = fixedTypeWidth(layout.schema.type);
+ return {
+ unscaled: signedLittleEndianValue(
+ layout.values!,
+ dense * width,
+ width,
+ ),
+ scale: layout.scale!,
+ };
+ }
+ case QWP_COLUMN_TYPE.GEOHASH: {
+ const width = Math.ceil(layout.precisionBits! / 8);
+ return {
+ bits: unsignedLittleEndianValue(layout.values!, dense * width, width),
+ precisionBits: layout.precisionBits!,
+ };
+ }
+ case QWP_COLUMN_TYPE.DOUBLE_ARRAY:
+ case QWP_COLUMN_TYPE.LONG_ARRAY:
+ return readArrayValue(
+ new QwpByteReader(this.getArrayView(rowIndex)!),
+ layout.schema.type,
+ );
+ default:
+ throw new QwpProtocolError(
+ `unsupported QWP result column type: ${String(layout.schema.type)}`,
+ );
+ }
+ }
+
+ private layout(): QwpResultColumnViewLayout {
+ return this.batch.layout(this.columnIndex);
+ }
+
+ private checkedLayout(rowIndex: number): QwpResultColumnViewLayout {
+ const layout = this.layout();
+ if (
+ !Number.isInteger(rowIndex) ||
+ rowIndex < 0 ||
+ rowIndex >= layout.rowCount
+ ) {
+ throw new RangeError(`row index out of range: ${rowIndex}`);
+ }
+ return layout;
+ }
+
+ private requireType(
+ layout: QwpResultColumnViewLayout,
+ type1: QwpColumnType,
+ type2?: QwpColumnType,
+ type3?: QwpColumnType,
+ type4?: QwpColumnType,
+ ): void {
+ const actual = layout.schema.type;
+ if (
+ actual !== type1 &&
+ actual !== type2 &&
+ actual !== type3 &&
+ actual !== type4
+ ) {
+ throw new TypeError(
+ `column '${layout.schema.name}' has QWP type 0x${actual.toString(16)}`,
+ );
+ }
+ }
+
+ private valuePosition(
+ rowIndex: number,
+ type1: QwpColumnType,
+ type2?: QwpColumnType,
+ type3?: QwpColumnType,
+ type4?: QwpColumnType,
+ ): { layout: QwpResultColumnViewLayout; dense: number } {
+ const layout = this.checkedLayout(rowIndex);
+ this.requireType(layout, type1, type2, type3, type4);
+ return {
+ layout,
+ dense: layout.isNull(rowIndex) ? -1 : layout.denseIndex(rowIndex),
+ };
+ }
+}
+
+/** Callback invoked by QwpResultBatchView.forEachRow(). */
+export type QwpResultRowViewCallback = (row: QwpResultRowView) => void;
+
+/**
+ * Reusable row-pinned facade over a QwpResultBatchView.
+ *
+ * The batch owns one instance and re-points it in place. It is valid only
+ * while the surrounding queryViews() callback is running, and must not be
+ * retained across forEachRow() iterations. Byte and array views returned by
+ * its accessors remain zero-copy and have the same lifetime.
+ */
+export class QwpResultRowView {
+ private _rowIndex = -1;
+
+ /** @internal */
+ constructor(private readonly parent: QwpResultBatchView) {}
+
+ /** Parent batch, primarily for column metadata. */
+ get batch(): QwpResultBatchView {
+ // Validate the shared batch before exposing it through a retained row.
+ void this.parent.rowCount;
+ return this.parent;
+ }
+
+ /** Zero-based row currently pinned by this reusable view. */
+ get rowIndex(): number {
+ void this.parent.rowCount;
+ return this._rowIndex;
+ }
+
+ /** Re-points this flyweight at a row and returns the same instance. */
+ of(rowIndex: number): this {
+ const rowCount = this.parent.rowCount;
+ if (!Number.isInteger(rowIndex) || rowIndex < 0 || rowIndex >= rowCount) {
+ throw new RangeError(`row index out of range: ${rowIndex}`);
+ }
+ this._rowIndex = rowIndex;
+ return this;
+ }
+
+ isNull(columnIndex: number): boolean {
+ return this.column(columnIndex).isNull(this._rowIndex);
+ }
+
+ get(columnIndex: number): QwpResultValue {
+ return this.column(columnIndex).get(this._rowIndex);
+ }
+
+ getBoolean(columnIndex: number): boolean {
+ return this.column(columnIndex).getBoolean(this._rowIndex);
+ }
+
+ getByte(columnIndex: number): number {
+ return this.column(columnIndex).getByte(this._rowIndex);
+ }
+
+ getShort(columnIndex: number): number {
+ return this.column(columnIndex).getShort(this._rowIndex);
+ }
+
+ getChar(columnIndex: number): string {
+ return this.column(columnIndex).getChar(this._rowIndex);
+ }
+
+ getInt(columnIndex: number): number {
+ return this.column(columnIndex).getInt(this._rowIndex);
+ }
+
+ getFloat(columnIndex: number): number {
+ return this.column(columnIndex).getFloat(this._rowIndex);
+ }
+
+ getDouble(columnIndex: number): number {
+ return this.column(columnIndex).getDouble(this._rowIndex);
+ }
+
+ getLong(columnIndex: number): bigint {
+ return this.column(columnIndex).getLong(this._rowIndex);
+ }
+
+ /** Zero-copy UTF-8 bytes for a VARCHAR value. */
+ getUtf8View(columnIndex: number): Uint8Array | null {
+ return this.column(columnIndex).getUtf8View(this._rowIndex);
+ }
+
+ getString(columnIndex: number): string | null {
+ return this.column(columnIndex).getString(this._rowIndex);
+ }
+
+ /** Zero-copy BINARY bytes. */
+ getBinaryView(columnIndex: number): Uint8Array | null {
+ return this.column(columnIndex).getBinaryView(this._rowIndex);
+ }
+
+ getSymbolId(columnIndex: number): number {
+ return this.column(columnIndex).getSymbolId(this._rowIndex);
+ }
+
+ getSymbol(columnIndex: number): string | null {
+ return this.column(columnIndex).getSymbol(this._rowIndex);
+ }
+
+ getUuidLow(columnIndex: number): bigint {
+ return this.column(columnIndex).getUuidLow(this._rowIndex);
+ }
+
+ getUuidHigh(columnIndex: number): bigint {
+ return this.column(columnIndex).getUuidHigh(this._rowIndex);
+ }
+
+ getLong256Word(columnIndex: number, wordIndex: number): bigint {
+ return this.column(columnIndex).getLong256Word(this._rowIndex, wordIndex);
+ }
+
+ getDecimalUnscaled(columnIndex: number): bigint {
+ return this.column(columnIndex).getDecimalUnscaled(this._rowIndex);
+ }
+
+ getGeohashBits(columnIndex: number): bigint {
+ return this.column(columnIndex).getGeohashBits(this._rowIndex);
+ }
+
+ /** Zero-copy encoded ARRAY row, including its dimension header. */
+ getArrayView(columnIndex: number): Uint8Array | null {
+ return this.column(columnIndex).getArrayView(this._rowIndex);
+ }
+
+ getArrayDimensionCount(columnIndex: number): number {
+ return this.column(columnIndex).getArrayDimensionCount(this._rowIndex);
+ }
+
+ private column(columnIndex: number): QwpResultColumnView {
+ return this.parent.column(columnIndex);
+ }
+}
+
+/**
+ * Batch-owned reusable view delivered by QwpEgressSession.queryViews().
+ * Access is invalid after the callback returns. materialize() creates an
+ * independently owned QwpResultBatch when retention is required.
+ */
+export class QwpResultBatchView {
+ private active = false;
+ private _requestId = -1n;
+ private _batchSequence = -1n;
+ private _tableName = "";
+ private _rowCount = 0;
+ private layouts: QwpResultColumnViewLayout[] = [];
+ private readonly columnViews: QwpResultColumnView[] = [];
+ private readonly columnViewPool: QwpResultColumnView[] = [];
+ private rowView?: QwpResultRowView;
+
+ get valid(): boolean {
+ return this.active;
+ }
+
+ get requestId(): bigint {
+ this.assertValid();
+ return this._requestId;
+ }
+
+ get batchSequence(): bigint {
+ this.assertValid();
+ return this._batchSequence;
+ }
+
+ get tableName(): string {
+ this.assertValid();
+ return this._tableName;
+ }
+
+ get rowCount(): number {
+ this.assertValid();
+ return this._rowCount;
+ }
+
+ get columnCount(): number {
+ this.assertValid();
+ return this.layouts.length;
+ }
+
+ get columns(): readonly QwpResultColumnView[] {
+ this.assertValid();
+ return this.columnViews;
+ }
+
+ column(columnIndex: number): QwpResultColumnView {
+ this.assertValid();
+ const column = this.columnViews[columnIndex];
+ if (!column) {
+ throw new RangeError(`column index out of range: ${columnIndex}`);
+ }
+ return column;
+ }
+
+ get(rowIndex: number, columnIndex: number): QwpResultValue {
+ return this.column(columnIndex).get(rowIndex);
+ }
+
+ /**
+ * Returns the batch-owned reusable row view pinned to rowIndex. Every call
+ * returns the same object re-pointed at the requested row.
+ */
+ row(rowIndex: number): QwpResultRowView {
+ this.assertValid();
+ return this.reusableRowView().of(rowIndex);
+ }
+
+ /**
+ * Visits rows in index order with one re-pointed row view. The callback is
+ * synchronous; copy values that must survive the current invocation.
+ */
+ forEachRow(callback: QwpResultRowViewCallback): void {
+ this.assertValid();
+ if (this._rowCount === 0) return;
+ const rowView = this.reusableRowView();
+ for (let rowIndex = 0; rowIndex < this._rowCount; rowIndex++) {
+ callback(rowView.of(rowIndex));
+ }
+ }
+
+ materialize(): QwpResultBatch {
+ this.assertValid();
+ return new QwpResultBatch(
+ this._requestId,
+ this._batchSequence,
+ this._tableName,
+ this._rowCount,
+ this.columnViews.map((column) => ({
+ name: column.name,
+ type: column.type,
+ values: Array.from({ length: this._rowCount }, (_, row) => {
+ const value = column.get(row);
+ // Binary values are zero-copy slices in the view API. materialize()
+ // promises independently owned data, so detach those slices here.
+ return value instanceof Uint8Array ? value.slice() : value;
+ }),
+ ...(column.scale === undefined ? {} : { scale: column.scale }),
+ ...(column.precisionBits === undefined
+ ? {}
+ : { precisionBits: column.precisionBits }),
+ })),
+ );
+ }
+
+ /** Invalidates the view. Normally called automatically after queryViews(). */
+ release(): void {
+ if (!this.active) return;
+ this.active = false;
+ for (const layout of this.layouts) layout.release();
+ }
+
+ /** @internal */
+ reset(
+ requestId: bigint,
+ batchSequence: bigint,
+ tableName: string,
+ rowCount: number,
+ layouts: QwpResultColumnViewLayout[],
+ ): this {
+ this._requestId = requestId;
+ this._batchSequence = batchSequence;
+ this._tableName = tableName;
+ this._rowCount = rowCount;
+ this.layouts = layouts;
+ while (this.columnViewPool.length < layouts.length) {
+ this.columnViewPool.push(
+ new QwpResultColumnView(this, this.columnViewPool.length),
+ );
+ }
+ this.columnViews.length = layouts.length;
+ for (let index = 0; index < layouts.length; index++) {
+ this.columnViews[index] = this.columnViewPool[index];
+ }
+ this.active = true;
+ return this;
+ }
+
+ /** @internal */
+ layout(columnIndex: number): QwpResultColumnViewLayout {
+ this.assertValid();
+ const layout = this.layouts[columnIndex];
+ if (!layout) {
+ throw new RangeError(`column index out of range: ${columnIndex}`);
+ }
+ return layout;
+ }
+
+ private assertValid(): void {
+ if (!this.active) {
+ throw new Error(
+ "QWP result batch view is no longer valid; copy or materialize values inside the queryViews callback",
+ );
+ }
+ }
+
+ private reusableRowView(): QwpResultRowView {
+ return (this.rowView ??= new QwpResultRowView(this));
+ }
+}
+
+function variableWidthValue(
+ layout: QwpResultColumnViewLayout,
+ denseIndex: number,
+): Uint8Array {
+ const offsets = layout.valuesView!;
+ const start = offsets.getUint32(denseIndex * 4, true);
+ const end = offsets.getUint32((denseIndex + 1) * 4, true);
+ return layout.stringBytes!.subarray(start, end);
+}
+
+function fixedTypeWidth(type: QwpColumnType): number {
+ switch (type) {
+ case QWP_COLUMN_TYPE.DECIMAL64:
+ return 8;
+ case QWP_COLUMN_TYPE.DECIMAL128:
+ return 16;
+ case QWP_COLUMN_TYPE.DECIMAL256:
+ return 32;
+ default:
+ throw new TypeError(`QWP type 0x${type.toString(16)} is not decimal`);
+ }
+}
+
+// The scale the server sends is a single byte, so it must be bounded like the
+// encoder bounds it (QwpTableBuffer.setDecimalScale) and QWP_DECIMAL_MAX_SCALE
+// exports it. An unchecked 255 decodes to a value off by up to 10^237.
+function decimalMaxScale(type: QwpColumnType): number {
+ switch (type) {
+ case QWP_COLUMN_TYPE.DECIMAL64:
+ return 18;
+ case QWP_COLUMN_TYPE.DECIMAL128:
+ return 38;
+ case QWP_COLUMN_TYPE.DECIMAL256:
+ return 76;
+ default:
+ throw new TypeError(`QWP type 0x${type.toString(16)} is not decimal`);
+ }
+}
+
+function readDecimalScale(reader: QwpByteReader, type: QwpColumnType): number {
+ const scale = reader.readUint8("decimal scale");
+ const maximum = decimalMaxScale(type);
+ if (scale > maximum) {
+ throw new QwpProtocolError(
+ `decimal scale out of range: ${scale} (max ${maximum})`,
+ );
+ }
+ return scale;
+}
+
+function unsignedLittleEndianValue(
+ bytes: Uint8Array,
+ offset = 0,
+ length = bytes.length - offset,
+): bigint {
+ let value = 0n;
+ for (let index = 0; index < length; index++) {
+ value |= BigInt(bytes[offset + index]) << BigInt(index * 8);
+ }
+ return value;
+}
+
+function signedLittleEndianValue(
+ bytes: Uint8Array,
+ offset = 0,
+ length = bytes.length - offset,
+): bigint {
+ const value = unsignedLittleEndianValue(bytes, offset, length);
+ const bits = BigInt(length * 8);
+ const sign = 1n << (bits - 1n);
+ return (value & sign) === 0n ? value : value - (1n << bits);
+}
+
+interface NullLayout {
+ nulls: boolean[];
+ nonNullCount: number;
+}
+
+class QwpBitReader {
+ private bitPosition = 0;
+
+ constructor(private readonly bytes: Uint8Array) {}
+
+ get bytesConsumed(): number {
+ return Math.ceil(this.bitPosition / 8);
+ }
+
+ readBit(): number {
+ if (this.bitPosition >= this.bytes.length * 8) {
+ throw new QwpProtocolError("truncated QWP Gorilla bitstream");
+ }
+ const result =
+ (this.bytes[this.bitPosition >>> 3] >>> (this.bitPosition & 7)) & 1;
+ this.bitPosition++;
+ return result;
+ }
+
+ readSigned(bitCount: number): bigint {
+ let value = 0n;
+ for (let bit = 0; bit < bitCount; bit++) {
+ if (this.readBit() !== 0) value |= 1n << BigInt(bit);
+ }
+ const sign = 1n << BigInt(bitCount - 1);
+ return (value & sign) === 0n ? value : value - (1n << BigInt(bitCount));
+ }
+}
+
+function readCount(
+ reader: QwpByteReader,
+ maximum: number,
+ label: string,
+): number {
+ const value = readQwpVarint(reader);
+ if (value > BigInt(maximum)) {
+ throw new QwpProtocolError(`${label} out of range: ${value}`);
+ }
+ return Number(value);
+}
+
+function readNullLayout(reader: QwpByteReader, rowCount: number): NullLayout {
+ const flag = reader.readUint8("column null flag");
+ if (flag !== 0 && flag !== 1) {
+ throw new QwpProtocolError(`invalid column null flag: ${flag}`);
+ }
+ const nulls = new Array(rowCount).fill(false);
+ if (flag === 0) return { nulls, nonNullCount: rowCount };
+
+ const bitmap = reader.readBytes(
+ Math.ceil(rowCount / 8),
+ "column null bitmap",
+ );
+ let nonNullCount = rowCount;
+ for (let row = 0; row < rowCount; row++) {
+ if ((bitmap[row >>> 3] & (1 << (row & 7))) !== 0) {
+ nulls[row] = true;
+ nonNullCount--;
+ }
+ }
+ return { nulls, nonNullCount };
+}
+
+function expandNulls(
+ dense: readonly T[],
+ layout: NullLayout,
+): QwpResultValue[] {
+ const values = new Array(layout.nulls.length);
+ let denseIndex = 0;
+ for (let row = 0; row < layout.nulls.length; row++) {
+ values[row] = layout.nulls[row] ? null : dense[denseIndex++];
+ }
+ return values;
+}
+
+function readSignedLittleEndian(
+ reader: QwpByteReader,
+ byteCount: number,
+ label: string,
+): bigint {
+ const bytes = reader.readBytes(byteCount, label);
+ let value = 0n;
+ for (let index = 0; index < byteCount; index++) {
+ value |= BigInt(bytes[index]) << BigInt(index * 8);
+ }
+ const bits = BigInt(byteCount * 8);
+ const sign = 1n << (bits - 1n);
+ return (value & sign) === 0n ? value : value - (1n << bits);
+}
+
+function readStringValues(
+ reader: QwpByteReader,
+ count: number,
+ binary: boolean,
+): (string | Uint8Array)[] {
+ const offsets = new Array(count + 1);
+ for (let index = 0; index <= count; index++) {
+ offsets[index] = reader.readUint32("variable-width column offset");
+ }
+ if (offsets[0] !== 0) {
+ throw new QwpProtocolError(
+ "variable-width column must start at offset zero",
+ );
+ }
+ for (let index = 1; index < offsets.length; index++) {
+ if (offsets[index] < offsets[index - 1]) {
+ throw new QwpProtocolError(
+ `variable-width column offsets are not monotonic at index ${index}`,
+ );
+ }
+ }
+ const bytes = reader.readBytes(offsets[count], "variable-width column data");
+ const values = new Array(count);
+ for (let index = 0; index < count; index++) {
+ const value = bytes.subarray(offsets[index], offsets[index + 1]);
+ values[index] = binary ? value.slice() : decodeUtf8(value);
+ }
+ return values;
+}
+
+function decodeGorillaValues(reader: QwpByteReader, count: number): bigint[] {
+ if (count < 3) {
+ throw new QwpProtocolError(
+ `Gorilla-encoded column has fewer than three values: ${count}`,
+ );
+ }
+ const first = reader.readBigInt64("first Gorilla timestamp");
+ const second = reader.readBigInt64("second Gorilla timestamp");
+ const values = [first, second];
+ const bits = new QwpBitReader(
+ reader.bytes.subarray(reader.position, reader.position + reader.remaining),
+ );
+ let previousTimestamp = second;
+ let previousDelta = BigInt.asIntN(64, second - first);
+ for (let index = 2; index < count; index++) {
+ let deltaOfDelta: bigint;
+ let prefixOnes = 0;
+ while (prefixOnes < 4 && bits.readBit() !== 0) prefixOnes++;
+ switch (prefixOnes) {
+ case 0:
+ deltaOfDelta = 0n;
+ break;
+ case 1:
+ deltaOfDelta = bits.readSigned(7);
+ break;
+ case 2:
+ deltaOfDelta = bits.readSigned(9);
+ break;
+ case 3:
+ deltaOfDelta = bits.readSigned(12);
+ break;
+ default:
+ deltaOfDelta = bits.readSigned(32);
+ }
+ const delta = BigInt.asIntN(64, previousDelta + deltaOfDelta);
+ const timestamp = BigInt.asIntN(64, previousTimestamp + delta);
+ values.push(timestamp);
+ previousDelta = delta;
+ previousTimestamp = timestamp;
+ }
+ reader.readBytes(bits.bytesConsumed, "Gorilla bitstream");
+ return values;
+}
+
+function readTimestampValues(
+ reader: QwpByteReader,
+ count: number,
+ gorilla: boolean,
+): bigint[] {
+ if (!gorilla) {
+ return Array.from({ length: count }, () =>
+ reader.readBigInt64("timestamp value"),
+ );
+ }
+ const encoding = reader.readUint8("timestamp encoding");
+ if (encoding === 0) {
+ return Array.from({ length: count }, () =>
+ reader.readBigInt64("timestamp value"),
+ );
+ }
+ if (encoding !== 1) {
+ throw new QwpProtocolError(`unknown timestamp encoding: ${encoding}`);
+ }
+ return decodeGorillaValues(reader, count);
+}
+
+function readArrayValue(
+ reader: QwpByteReader,
+ type: QwpColumnType,
+): QwpResultArrayValue {
+ const dimensions = reader.readUint8("array dimension count");
+ if (dimensions < 1 || dimensions > 32) {
+ throw new QwpProtocolError(
+ `array dimension count out of range: ${dimensions}`,
+ );
+ }
+ const shape = new Array(dimensions);
+ let elementCount = 1;
+ for (let index = 0; index < dimensions; index++) {
+ const length = reader.readInt32("array dimension length");
+ if (length < 0 || length > MAX_ARRAY_DIMENSION_LENGTH) {
+ throw new QwpProtocolError(
+ `array dimension length out of range: ${length}`,
+ );
+ }
+ shape[index] = length;
+ elementCount *= length;
+ if (elementCount > MAX_ARRAY_ELEMENTS) {
+ throw new QwpProtocolError(
+ `array element count exceeds ${MAX_ARRAY_ELEMENTS}`,
+ );
+ }
+ }
+ if (elementCount > Math.floor(reader.remaining / 8)) {
+ throw new QwpProtocolError("truncated array payload");
+ }
+ if (type === QWP_COLUMN_TYPE.DOUBLE_ARRAY) {
+ return {
+ dimensions: shape,
+ values: Array.from({ length: elementCount }, () =>
+ reader.readFloat64("double array element"),
+ ),
+ };
+ }
+ return {
+ dimensions: shape,
+ values: Array.from({ length: elementCount }, () =>
+ reader.readBigInt64("long array element"),
+ ),
+ };
+}
+
+interface PreparedResultBatch {
+ readonly reader: QwpByteReader;
+ readonly tableName: string;
+ readonly rowCount: number;
+ readonly deltaMode: boolean;
+}
+
+/** Stateful decoder for connection-scoped QWP result batches. */
+export class QwpResultBatchDecoder {
+ private readonly symbolDictionary: string[] = [];
+ private readonly viewBatches: QwpResultBatchView[] = [];
+ private readonly viewLayouts: QwpResultColumnViewLayout[][] = [];
+ private readonly viewLayoutPools: QwpResultColumnViewLayout[][] = [];
+ private schema?: QwpResultColumnSchema[];
+ private expectedBatchSequence = 0n;
+
+ resetQuerySchema(): void {
+ for (const batch of this.viewBatches) batch.release();
+ this.schema = undefined;
+ this.expectedBatchSequence = 0n;
+ }
+
+ applyCacheReset(resetMask: number): void {
+ if ((resetMask & QWP_RESET_MASK_DICTIONARY) !== 0) {
+ this.symbolDictionary.length = 0;
+ }
+ }
+
+ /** @internal Drops frame-backed references after a failed slot decode. */
+ releaseView(slot: number): void {
+ this.viewBatches[slot]?.release();
+ for (const layout of this.viewLayoutPools[slot] ?? []) layout.release();
+ }
+
+ decode(message: QwpResultBatchMessage): QwpResultBatch {
+ const { reader, tableName, rowCount, deltaMode } = this.prepare(message);
+
+ const columns = this.schema!.map((column) =>
+ this.readColumn(reader, column, rowCount, deltaMode, message.flags),
+ );
+ reader.expectEnd("RESULT_BATCH");
+ this.expectedBatchSequence++;
+ return new QwpResultBatch(
+ message.requestId,
+ message.batchSequence,
+ tableName,
+ rowCount,
+ columns,
+ );
+ }
+
+ /**
+ * Decodes into one slot from a reusable batch/column-view pool without
+ * materializing a JavaScript value array. Reusing the same slot invalidates
+ * its prior view; callers must not reuse a slot until its consumer releases
+ * the preceding batch.
+ */
+ decodeView(message: QwpResultBatchMessage, slot = 0): QwpResultBatchView {
+ if (!Number.isSafeInteger(slot) || slot < 0) {
+ throw new RangeError(
+ "QWP result view slot must be a non-negative integer",
+ );
+ }
+ const viewBatch = (this.viewBatches[slot] ??= new QwpResultBatchView());
+ const viewLayouts = (this.viewLayouts[slot] ??= []);
+ const viewLayoutPool = (this.viewLayoutPools[slot] ??= []);
+ viewBatch.release();
+ const { reader, tableName, rowCount, deltaMode } = this.prepare(message);
+ const schema = this.schema!;
+ while (viewLayoutPool.length < schema.length) {
+ viewLayoutPool.push(new QwpResultColumnViewLayout());
+ }
+ viewLayouts.length = schema.length;
+ for (let index = 0; index < schema.length; index++) {
+ const layout = viewLayoutPool[index];
+ viewLayouts[index] = layout;
+ layout.reset(schema[index], rowCount);
+ this.readColumnView(reader, layout, deltaMode, message.flags);
+ }
+ reader.expectEnd("RESULT_BATCH");
+ this.expectedBatchSequence++;
+ return viewBatch.reset(
+ message.requestId,
+ message.batchSequence,
+ tableName,
+ rowCount,
+ viewLayouts,
+ );
+ }
+
+ private prepare(message: QwpResultBatchMessage): PreparedResultBatch {
+ if (message.tableCount !== 1) {
+ throw new QwpProtocolError(
+ `RESULT_BATCH must contain exactly one table, got ${message.tableCount}`,
+ );
+ }
+ if (message.batchSequence !== this.expectedBatchSequence) {
+ throw new QwpProtocolError(
+ `unexpected RESULT_BATCH sequence [expected=${this.expectedBatchSequence}, actual=${message.batchSequence}]`,
+ );
+ }
+
+ const body =
+ (message.flags & QWP_FLAG_ZSTD) !== 0
+ ? decompressQwpZstdFrame(message.body)
+ : message.body;
+ const reader = new QwpByteReader(body);
+ const deltaMode = (message.flags & QWP_FLAG_DELTA_SYMBOL_DICTIONARY) !== 0;
+ if (deltaMode) this.readDeltaDictionary(reader, body.length);
+
+ const tableNameLength = readCount(
+ reader,
+ QWP_MAX_IDENTIFIER_BYTES,
+ "table name length",
+ );
+ const tableName = reader.readUtf8(tableNameLength, "table name");
+ const rowCount = readCount(reader, MAX_ROWS_PER_BATCH, "result row count");
+
+ if (message.batchSequence === 0n) {
+ const columnCount = readCount(
+ reader,
+ QWP_MAX_COLUMNS_PER_TABLE,
+ "result column count",
+ );
+ this.schema = Array.from({ length: columnCount }, () => {
+ const nameLength = readCount(
+ reader,
+ QWP_MAX_IDENTIFIER_BYTES,
+ "column name length",
+ );
+ const name = reader.readUtf8(nameLength, "column name");
+ const type = reader.readUint8("column type") as QwpColumnType;
+ if (!Object.values(QWP_COLUMN_TYPE).includes(type)) {
+ throw new QwpProtocolError(
+ `unsupported QWP result column type: 0x${type.toString(16)}`,
+ );
+ }
+ return { name, type };
+ });
+ } else if (!this.schema) {
+ throw new QwpProtocolError(
+ "continuation RESULT_BATCH arrived before its schema-bearing batch",
+ );
+ }
+ // Each dimension passed its own cap; the grid they describe still has to
+ // be one this client will allocate. Checked before any column is read,
+ // because reading one is what allocates.
+ const cells = rowCount * this.schema.length;
+ if (cells > QWP_MAX_CELLS_PER_BATCH) {
+ throw new QwpProtocolError(
+ `RESULT_BATCH declares ${cells} cells, above the client cap ${QWP_MAX_CELLS_PER_BATCH} [rows=${rowCount}, columns=${this.schema.length}]`,
+ );
+ }
+ return { reader, tableName, rowCount, deltaMode };
+ }
+
+ private readColumnView(
+ reader: QwpByteReader,
+ layout: QwpResultColumnViewLayout,
+ deltaMode: boolean,
+ flags: number,
+ ): void {
+ this.readNullView(reader, layout);
+ const count = layout.nonNullCount;
+ const type = layout.schema.type;
+ switch (type) {
+ case QWP_COLUMN_TYPE.BOOLEAN:
+ layout.setValues(
+ reader.readBytes(Math.ceil(count / 8), "boolean values"),
+ );
+ return;
+ case QWP_COLUMN_TYPE.BYTE:
+ this.readFixedView(reader, layout, count, 1, "byte values");
+ return;
+ case QWP_COLUMN_TYPE.SHORT:
+ case QWP_COLUMN_TYPE.CHAR:
+ this.readFixedView(reader, layout, count, 2, "short values");
+ return;
+ case QWP_COLUMN_TYPE.INT:
+ case QWP_COLUMN_TYPE.FLOAT:
+ case QWP_COLUMN_TYPE.IPV4:
+ this.readFixedView(reader, layout, count, 4, "int values");
+ return;
+ case QWP_COLUMN_TYPE.LONG:
+ case QWP_COLUMN_TYPE.DOUBLE:
+ this.readFixedView(reader, layout, count, 8, "long values");
+ return;
+ case QWP_COLUMN_TYPE.DATE:
+ case QWP_COLUMN_TYPE.TIMESTAMP:
+ case QWP_COLUMN_TYPE.TIMESTAMP_NANOS:
+ this.readTimestampView(reader, layout, flags);
+ return;
+ case QWP_COLUMN_TYPE.VARCHAR:
+ case QWP_COLUMN_TYPE.BINARY:
+ this.readVariableWidthView(reader, layout);
+ return;
+ case QWP_COLUMN_TYPE.SYMBOL:
+ this.readSymbolView(reader, layout, deltaMode);
+ return;
+ case QWP_COLUMN_TYPE.UUID:
+ this.readFixedView(reader, layout, count, 16, "UUID values");
+ return;
+ case QWP_COLUMN_TYPE.LONG256:
+ this.readFixedView(reader, layout, count, 32, "LONG256 values");
+ return;
+ case QWP_COLUMN_TYPE.DECIMAL64:
+ case QWP_COLUMN_TYPE.DECIMAL128:
+ case QWP_COLUMN_TYPE.DECIMAL256: {
+ layout.scale = readDecimalScale(reader, type);
+ this.readFixedView(
+ reader,
+ layout,
+ count,
+ fixedTypeWidth(type),
+ "decimal values",
+ );
+ return;
+ }
+ case QWP_COLUMN_TYPE.GEOHASH: {
+ layout.precisionBits = readCount(reader, 60, "geohash precision");
+ if (layout.precisionBits < 1) {
+ throw new QwpProtocolError(
+ `geohash precision out of range: ${layout.precisionBits}`,
+ );
+ }
+ this.readFixedView(
+ reader,
+ layout,
+ count,
+ Math.ceil(layout.precisionBits / 8),
+ "geohash values",
+ );
+ return;
+ }
+ case QWP_COLUMN_TYPE.DOUBLE_ARRAY:
+ case QWP_COLUMN_TYPE.LONG_ARRAY:
+ this.readArrayView(reader, layout);
+ return;
+ default:
+ throw new QwpProtocolError(
+ `unsupported QWP result column type: ${String(type)}`,
+ );
+ }
+ }
+
+ private readNullView(
+ reader: QwpByteReader,
+ layout: QwpResultColumnViewLayout,
+ ): void {
+ const flag = reader.readUint8("column null flag");
+ if (flag !== 0 && flag !== 1) {
+ throw new QwpProtocolError(`invalid column null flag: ${flag}`);
+ }
+ if (flag === 0) {
+ layout.nonNullCount = layout.rowCount;
+ return;
+ }
+ const bitmap = reader.readBytes(
+ Math.ceil(layout.rowCount / 8),
+ "column null bitmap",
+ );
+ layout.nullBitmap = bitmap;
+ const indexes = layout.ensureNonNullIndexes(layout.rowCount);
+ let dense = 0;
+ for (let row = 0; row < layout.rowCount; row++) {
+ if ((bitmap[row >>> 3] & (1 << (row & 7))) !== 0) {
+ indexes[row] = -1;
+ } else {
+ indexes[row] = dense++;
+ }
+ }
+ layout.nonNullCount = dense;
+ }
+
+ private readFixedView(
+ reader: QwpByteReader,
+ layout: QwpResultColumnViewLayout,
+ count: number,
+ width: number,
+ label: string,
+ ): void {
+ layout.setValues(reader.readBytes(count * width, label));
+ }
+
+ private readVariableWidthView(
+ reader: QwpByteReader,
+ layout: QwpResultColumnViewLayout,
+ ): void {
+ const count = layout.nonNullCount;
+ const offsets = reader.readBytes(
+ (count + 1) * 4,
+ "variable-width column offsets",
+ );
+ const view = new DataView(
+ offsets.buffer,
+ offsets.byteOffset,
+ offsets.byteLength,
+ );
+ if (view.getUint32(0, true) !== 0) {
+ throw new QwpProtocolError(
+ "variable-width column must start at offset zero",
+ );
+ }
+ let previous = 0;
+ for (let index = 1; index <= count; index++) {
+ const offset = view.getUint32(index * 4, true);
+ if (offset < previous) {
+ throw new QwpProtocolError(
+ `variable-width column offsets are not monotonic at index ${index}`,
+ );
+ }
+ previous = offset;
+ }
+ layout.setValues(offsets);
+ layout.stringBytes = reader.readBytes(
+ previous,
+ "variable-width column data",
+ );
+ }
+
+ private readSymbolView(
+ reader: QwpByteReader,
+ layout: QwpResultColumnViewLayout,
+ deltaMode: boolean,
+ ): void {
+ let dictionary: readonly string[];
+ if (deltaMode) {
+ dictionary = this.symbolDictionary;
+ } else {
+ const size = readCount(reader, layout.rowCount, "symbol dictionary size");
+ const local = layout.localSymbols;
+ for (let index = 0; index < size; index++) {
+ const length = readCount(reader, reader.remaining, "symbol length");
+ local.push(reader.readUtf8(length, "symbol"));
+ }
+ dictionary = local;
+ }
+ layout.symbolDictionary = dictionary;
+ const ids = layout.ensureSymbolRowIds(layout.rowCount);
+ for (let row = 0; row < layout.rowCount; row++) {
+ if (layout.isNull(row)) continue;
+ const id = readCount(reader, dictionary.length, "symbol ID");
+ if (id >= dictionary.length) {
+ throw new QwpProtocolError(`symbol ID out of range: ${id}`);
+ }
+ ids[row] = id;
+ }
+ }
+
+ private readArrayView(
+ reader: QwpByteReader,
+ layout: QwpResultColumnViewLayout,
+ ): void {
+ const start = reader.position;
+ const offsets = layout.ensureArrayOffsets(layout.rowCount);
+ const lengths = layout.ensureArrayLengths(layout.rowCount);
+ for (let row = 0; row < layout.rowCount; row++) {
+ if (layout.isNull(row)) {
+ offsets[row] = 0;
+ lengths[row] = 0;
+ continue;
+ }
+ const rowStart = reader.position;
+ const dimensions = reader.readUint8("array dimension count");
+ if (dimensions < 1 || dimensions > 32) {
+ throw new QwpProtocolError(
+ `array dimension count out of range: ${dimensions}`,
+ );
+ }
+ let elementCount = 1;
+ for (let index = 0; index < dimensions; index++) {
+ const length = reader.readInt32("array dimension length");
+ if (length < 0 || length > MAX_ARRAY_DIMENSION_LENGTH) {
+ throw new QwpProtocolError(
+ `array dimension length out of range: ${length}`,
+ );
+ }
+ elementCount *= length;
+ if (elementCount > MAX_ARRAY_ELEMENTS) {
+ throw new QwpProtocolError(
+ `array element count exceeds ${MAX_ARRAY_ELEMENTS}`,
+ );
+ }
+ }
+ reader.readBytes(elementCount * 8, "array payload");
+ offsets[row] = rowStart - start;
+ lengths[row] = reader.position - rowStart;
+ }
+ layout.setValues(reader.bytes.subarray(start, reader.position));
+ }
+
+ private readTimestampView(
+ reader: QwpByteReader,
+ layout: QwpResultColumnViewLayout,
+ flags: number,
+ ): void {
+ const count = layout.nonNullCount;
+ if ((flags & QWP_FLAG_GORILLA) === 0) {
+ this.readFixedView(reader, layout, count, 8, "timestamp values");
+ return;
+ }
+ const encoding = reader.readUint8("timestamp encoding");
+ if (encoding === 0) {
+ this.readFixedView(reader, layout, count, 8, "timestamp values");
+ return;
+ }
+ if (encoding !== 1) {
+ throw new QwpProtocolError(`unknown timestamp encoding: ${encoding}`);
+ }
+ if (count < 3) {
+ throw new QwpProtocolError(
+ `Gorilla-encoded column has fewer than three values: ${count}`,
+ );
+ }
+ const bytes = layout.timestampBytes(count * 8);
+ const decoded = new DataView(
+ bytes.buffer,
+ bytes.byteOffset,
+ bytes.byteLength,
+ );
+ const first = reader.readBigInt64("first Gorilla timestamp");
+ const second = reader.readBigInt64("second Gorilla timestamp");
+ decoded.setBigInt64(0, first, true);
+ decoded.setBigInt64(8, second, true);
+ const bits = new QwpBitReader(
+ reader.bytes.subarray(
+ reader.position,
+ reader.position + reader.remaining,
+ ),
+ );
+ let previousTimestamp = second;
+ let previousDelta = BigInt.asIntN(64, second - first);
+ for (let index = 2; index < count; index++) {
+ let deltaOfDelta: bigint;
+ let prefixOnes = 0;
+ while (prefixOnes < 4 && bits.readBit() !== 0) prefixOnes++;
+ switch (prefixOnes) {
+ case 0:
+ deltaOfDelta = 0n;
+ break;
+ case 1:
+ deltaOfDelta = bits.readSigned(7);
+ break;
+ case 2:
+ deltaOfDelta = bits.readSigned(9);
+ break;
+ case 3:
+ deltaOfDelta = bits.readSigned(12);
+ break;
+ default:
+ deltaOfDelta = bits.readSigned(32);
+ }
+ const delta = BigInt.asIntN(64, previousDelta + deltaOfDelta);
+ const timestamp = BigInt.asIntN(64, previousTimestamp + delta);
+ decoded.setBigInt64(index * 8, timestamp, true);
+ previousDelta = delta;
+ previousTimestamp = timestamp;
+ }
+ reader.readBytes(bits.bytesConsumed, "Gorilla bitstream");
+ layout.setValues(bytes);
+ }
+
+ private readColumn(
+ reader: QwpByteReader,
+ schema: QwpResultColumnSchema,
+ rowCount: number,
+ deltaMode: boolean,
+ flags: number,
+ ): QwpResultColumn {
+ const layout = readNullLayout(reader, rowCount);
+ const count = layout.nonNullCount;
+ let dense: QwpResultValue[];
+ let scale: number | undefined;
+ let precisionBits: number | undefined;
+
+ switch (schema.type) {
+ case QWP_COLUMN_TYPE.BOOLEAN: {
+ const bytes = reader.readBytes(Math.ceil(count / 8), "boolean values");
+ dense = Array.from(
+ { length: count },
+ (_, index) => (bytes[index >>> 3] & (1 << (index & 7))) !== 0,
+ );
+ break;
+ }
+ case QWP_COLUMN_TYPE.BYTE:
+ dense = Array.from({ length: count }, () =>
+ reader.readInt8("byte value"),
+ );
+ break;
+ case QWP_COLUMN_TYPE.SHORT:
+ dense = Array.from({ length: count }, () =>
+ reader.readInt16("short value"),
+ );
+ break;
+ case QWP_COLUMN_TYPE.CHAR:
+ dense = Array.from({ length: count }, () =>
+ String.fromCharCode(reader.readUint16("char value")),
+ );
+ break;
+ case QWP_COLUMN_TYPE.INT:
+ case QWP_COLUMN_TYPE.IPV4:
+ dense = Array.from({ length: count }, () =>
+ reader.readInt32("int value"),
+ );
+ break;
+ case QWP_COLUMN_TYPE.FLOAT:
+ dense = Array.from({ length: count }, () =>
+ reader.readFloat32("float value"),
+ );
+ break;
+ case QWP_COLUMN_TYPE.DOUBLE:
+ dense = Array.from({ length: count }, () =>
+ reader.readFloat64("double value"),
+ );
+ break;
+ case QWP_COLUMN_TYPE.LONG:
+ dense = Array.from({ length: count }, () =>
+ reader.readBigInt64("long value"),
+ );
+ break;
+ case QWP_COLUMN_TYPE.DATE:
+ case QWP_COLUMN_TYPE.TIMESTAMP:
+ case QWP_COLUMN_TYPE.TIMESTAMP_NANOS:
+ dense = readTimestampValues(
+ reader,
+ count,
+ (flags & QWP_FLAG_GORILLA) !== 0,
+ );
+ break;
+ case QWP_COLUMN_TYPE.VARCHAR:
+ dense = readStringValues(reader, count, false);
+ break;
+ case QWP_COLUMN_TYPE.BINARY:
+ dense = readStringValues(reader, count, true);
+ break;
+ case QWP_COLUMN_TYPE.SYMBOL:
+ dense = this.readSymbols(reader, count, rowCount, deltaMode);
+ break;
+ case QWP_COLUMN_TYPE.UUID:
+ dense = Array.from({ length: count }, () => ({
+ low: reader.readBigUint64("UUID low bits"),
+ high: reader.readBigUint64("UUID high bits"),
+ }));
+ break;
+ case QWP_COLUMN_TYPE.LONG256:
+ dense = Array.from({ length: count }, () => ({
+ words: [
+ reader.readBigInt64("LONG256 word 0"),
+ reader.readBigInt64("LONG256 word 1"),
+ reader.readBigInt64("LONG256 word 2"),
+ reader.readBigInt64("LONG256 word 3"),
+ ] as const,
+ }));
+ break;
+ case QWP_COLUMN_TYPE.DECIMAL64:
+ case QWP_COLUMN_TYPE.DECIMAL128:
+ case QWP_COLUMN_TYPE.DECIMAL256: {
+ scale = readDecimalScale(reader, schema.type);
+ const bytes =
+ schema.type === QWP_COLUMN_TYPE.DECIMAL64
+ ? 8
+ : schema.type === QWP_COLUMN_TYPE.DECIMAL128
+ ? 16
+ : 32;
+ dense = Array.from({ length: count }, () => ({
+ unscaled: readSignedLittleEndian(reader, bytes, "decimal value"),
+ scale: scale!,
+ }));
+ break;
+ }
+ case QWP_COLUMN_TYPE.GEOHASH: {
+ precisionBits = readCount(reader, 60, "geohash precision");
+ if (precisionBits < 1) {
+ throw new QwpProtocolError(
+ `geohash precision out of range: ${precisionBits}`,
+ );
+ }
+ const byteCount = Math.ceil(precisionBits / 8);
+ dense = Array.from({ length: count }, () => {
+ const bytes = reader.readBytes(byteCount, "geohash value");
+ let bits = 0n;
+ for (let index = 0; index < bytes.length; index++) {
+ bits |= BigInt(bytes[index]) << BigInt(index * 8);
+ }
+ return { bits, precisionBits: precisionBits! };
+ });
+ break;
+ }
+ case QWP_COLUMN_TYPE.DOUBLE_ARRAY:
+ case QWP_COLUMN_TYPE.LONG_ARRAY:
+ dense = Array.from({ length: count }, () =>
+ readArrayValue(reader, schema.type),
+ );
+ break;
+ default:
+ throw new QwpProtocolError(
+ `unsupported QWP result column type: ${String(schema.type)}`,
+ );
+ }
+
+ return {
+ ...schema,
+ values: expandNulls(dense, layout),
+ ...(scale === undefined ? {} : { scale }),
+ ...(precisionBits === undefined ? {} : { precisionBits }),
+ };
+ }
+
+ private readDeltaDictionary(
+ reader: QwpByteReader,
+ decompressedPayloadBytes: number,
+ ): void {
+ const start = readCount(
+ reader,
+ MAX_CONNECTION_SYMBOLS,
+ "delta dictionary start",
+ );
+ const count = readCount(
+ reader,
+ MAX_CONNECTION_SYMBOLS,
+ "delta dictionary count",
+ );
+ if (start !== this.symbolDictionary.length) {
+ throw new QwpProtocolError(
+ `delta symbol dictionary is out of sync [expected=${this.symbolDictionary.length}, actual=${start}]`,
+ );
+ }
+ if (start + count > MAX_CONNECTION_SYMBOLS) {
+ throw new QwpProtocolError(
+ `symbol dictionary exceeds ${MAX_CONNECTION_SYMBOLS} entries`,
+ );
+ }
+ // Each declared entry occupies at least one length byte in the decompressed
+ // body. Check that structural lower bound before the loop, because reading
+ // an entry is what allocates. The compressed length is not a valid bound:
+ // a legitimate dictionary with repetitive symbols may compress below its
+ // entry count.
+ if (count > decompressedPayloadBytes) {
+ throw new QwpProtocolError(
+ `delta symbol dictionary declares ${count} entries, above the ${decompressedPayloadBytes}-byte decompressed payload`,
+ );
+ }
+ for (let index = 0; index < count; index++) {
+ const length = readCount(reader, reader.remaining, "symbol length");
+ this.symbolDictionary.push(reader.readUtf8(length, "symbol"));
+ }
+ }
+
+ private readSymbols(
+ reader: QwpByteReader,
+ count: number,
+ rowCount: number,
+ deltaMode: boolean,
+ ): string[] {
+ let dictionary: readonly string[];
+ if (deltaMode) {
+ dictionary = this.symbolDictionary;
+ } else {
+ const size = readCount(reader, rowCount, "symbol dictionary size");
+ const local = new Array(size);
+ for (let index = 0; index < size; index++) {
+ const length = readCount(reader, reader.remaining, "symbol length");
+ local[index] = reader.readUtf8(length, "symbol");
+ }
+ dictionary = local;
+ }
+ return Array.from({ length: count }, () => {
+ const id = readCount(reader, dictionary.length, "symbol ID");
+ if (id >= dictionary.length) {
+ throw new QwpProtocolError(`symbol ID out of range: ${id}`);
+ }
+ return dictionary[id];
+ });
+ }
+}
diff --git a/packages/client-core/src/_qwp/_core/symbol-dictionary.ts b/packages/client-core/src/_qwp/_core/symbol-dictionary.ts
new file mode 100644
index 0000000..cfa70b0
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/symbol-dictionary.ts
@@ -0,0 +1,62 @@
+import { QWP_MAX_SYMBOL_DICTIONARY_SIZE } from "./constants";
+
+/** Connection-scoped QWP symbol dictionary. IDs are dense from zero. */
+export class QwpSymbolDictionary {
+ private readonly ids = new Map();
+ private readonly values: string[] = [];
+
+ get size(): number {
+ return this.values.length;
+ }
+
+ getOrAdd(value: string): number {
+ const existing = this.ids.get(value);
+ if (existing !== undefined) return existing;
+ if (this.values.length >= QWP_MAX_SYMBOL_DICTIONARY_SIZE) {
+ throw new Error(
+ `symbol dictionary exceeds maximum size ${QWP_MAX_SYMBOL_DICTIONARY_SIZE}`,
+ );
+ }
+ const id = this.values.length;
+ this.ids.set(value, id);
+ this.values.push(value);
+ return id;
+ }
+
+ valueAt(id: number): string | undefined {
+ return this.values[id];
+ }
+
+ /** Appends positionally without de-duplicating recovered entries. */
+ addRecovered(value: string): number {
+ if (this.values.length >= QWP_MAX_SYMBOL_DICTIONARY_SIZE) {
+ throw new Error(
+ `symbol dictionary exceeds maximum size ${QWP_MAX_SYMBOL_DICTIONARY_SIZE}`,
+ );
+ }
+ const id = this.values.length;
+ this.values.push(value);
+ this.ids.set(value, id);
+ return id;
+ }
+
+ entriesFrom(startId: number): string[] {
+ return this.values.slice(Math.max(0, startId));
+ }
+
+ /** Rolls back entries added while preparing a frame that was not published. */
+ truncate(size: number): void {
+ if (!Number.isSafeInteger(size) || size < 0 || size > this.values.length) {
+ throw new RangeError(`invalid symbol dictionary size ${size}`);
+ }
+ if (size === this.values.length) return;
+ this.values.length = size;
+ this.ids.clear();
+ this.values.forEach((value, id) => this.ids.set(value, id));
+ }
+
+ reset(): void {
+ this.ids.clear();
+ this.values.length = 0;
+ }
+}
diff --git a/packages/client-core/src/_qwp/_core/table.ts b/packages/client-core/src/_qwp/_core/table.ts
new file mode 100644
index 0000000..44eeb66
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/table.ts
@@ -0,0 +1,318 @@
+import {
+ QWP_COLUMN_TYPE,
+ QWP_MAX_ARRAY_DIMENSIONS,
+ QWP_MAX_COLUMNS_PER_TABLE,
+ QWP_MAX_TABLE_NAME_LENGTH,
+ QwpColumnType,
+} from "./constants";
+import {
+ qwpColumnNameKey,
+ validateQwpColumnName,
+ validateQwpTableName,
+} from "./identifiers";
+
+export interface QwpSymbolValue {
+ id: number;
+ text: string;
+}
+
+export interface QwpArrayValue {
+ dimensions: number[];
+ values: (number | bigint)[];
+}
+
+export interface QwpColumnBuffer {
+ name: string;
+ type: QwpColumnType;
+ /** Non-null values only; QWP compacts values around the null bitmap. */
+ values: unknown[];
+ /** One entry per row; true means NULL. */
+ nulls: boolean[];
+ /** Rows accounted for so far, including nulls. */
+ size: number;
+ geohashPrecision?: number;
+ decimalScale?: number;
+}
+
+/** Mutable columnar staging area for one QWP ingress table. */
+export class QwpTableBuffer {
+ readonly name: string;
+ private readonly maxNameLength: number;
+ private readonly columnList: QwpColumnBuffer[] = [];
+ private readonly columnsByName = new Map();
+ private rows = 0;
+ // Memoizes the non-null value offset each column's slice starts from, reused
+ // while a caller walks the table in ascending `start` slices. See sliceRows().
+ private sliceValueOffsets?: {
+ rows: number;
+ start: number;
+ offsets: number[];
+ };
+
+ constructor(name: string, maxNameLength = QWP_MAX_TABLE_NAME_LENGTH) {
+ if (!Number.isSafeInteger(maxNameLength) || maxNameLength < 1) {
+ throw new RangeError("maxNameLength must be a positive safe integer");
+ }
+ validateQwpTableName(name, maxNameLength);
+ this.name = name;
+ this.maxNameLength = maxNameLength;
+ }
+
+ get rowCount(): number {
+ return this.rows;
+ }
+
+ get columns(): readonly QwpColumnBuffer[] {
+ return this.columnList;
+ }
+
+ /**
+ * Returns null when the current row already contains this column. The first
+ * value wins, matching the existing Sender API.
+ */
+ getOrCreateColumn(
+ name: string,
+ type: QwpColumnType,
+ // The caller may pass the key it already holds -- the flush path iterates a
+ // Map already keyed by it -- to skip a per-cell rebuild. It must equal
+ // qwpColumnNameKey(name); it defaults to it when omitted.
+ nameKey: string = qwpColumnNameKey(name),
+ ): QwpColumnBuffer | null {
+ const designatedTimestamp =
+ name.length === 0 &&
+ (type === QWP_COLUMN_TYPE.TIMESTAMP ||
+ type === QWP_COLUMN_TYPE.TIMESTAMP_NANOS);
+ if (!name && !designatedTimestamp) {
+ throw new Error("column name cannot be empty");
+ }
+
+ const existing = this.columnsByName.get(nameKey);
+ if (existing) {
+ if (existing.type !== type) {
+ throw new Error(
+ `column type mismatch for '${name}' [existing=${existing.type}, received=${type}]`,
+ );
+ }
+ if (existing.size > this.rows) return null;
+ existing.nulls.push(false);
+ existing.size++;
+ return existing;
+ }
+
+ if (!designatedTimestamp) validateQwpColumnName(name, this.maxNameLength);
+ if (this.columnList.length >= QWP_MAX_COLUMNS_PER_TABLE) {
+ throw new Error(
+ `column count exceeds maximum ${QWP_MAX_COLUMNS_PER_TABLE}`,
+ );
+ }
+
+ const column: QwpColumnBuffer = {
+ name,
+ type,
+ values: [],
+ nulls: new Array(this.rows).fill(true),
+ size: this.rows,
+ };
+ column.nulls.push(false);
+ column.size++;
+ this.columnList.push(column);
+ this.columnsByName.set(nameKey, column);
+ return column;
+ }
+
+ /** Closes the current row and back-fills missing columns with nulls. */
+ nextRow(): void {
+ this.rows++;
+ for (const column of this.columnList) {
+ while (column.size < this.rows) {
+ column.nulls.push(true);
+ column.size++;
+ }
+ }
+ }
+
+ setGeohashPrecision(column: QwpColumnBuffer, precision: number): void {
+ if (column.type !== QWP_COLUMN_TYPE.GEOHASH) {
+ throw new Error("geohash precision can only be set on a GEOHASH column");
+ }
+ if (!Number.isInteger(precision) || precision < 1 || precision > 60) {
+ throw new Error(
+ `invalid geohash precision ${precision}; expected 1 through 60`,
+ );
+ }
+ if (column.geohashPrecision === undefined) {
+ column.geohashPrecision = precision;
+ } else if (column.geohashPrecision !== precision) {
+ throw new Error(
+ `geohash precision mismatch [existing=${column.geohashPrecision}, received=${precision}]`,
+ );
+ }
+ }
+
+ setDecimalScale(column: QwpColumnBuffer, scale: number): number {
+ const maximum =
+ column.type === QWP_COLUMN_TYPE.DECIMAL64
+ ? 18
+ : column.type === QWP_COLUMN_TYPE.DECIMAL128
+ ? 38
+ : column.type === QWP_COLUMN_TYPE.DECIMAL256
+ ? 76
+ : undefined;
+ if (maximum === undefined) {
+ throw new Error("decimal scale can only be set on a DECIMAL column");
+ }
+ if (!Number.isInteger(scale) || scale < 0 || scale > maximum) {
+ throw new Error(
+ `invalid decimal scale ${scale}; expected 0 through ${maximum}`,
+ );
+ }
+ if (column.decimalScale === undefined) column.decimalScale = scale;
+ return column.decimalScale;
+ }
+
+ /** Truncates every column back to the last completed row. */
+ rollbackRow(): void {
+ for (const column of this.columnList) {
+ while (column.size > this.rows) {
+ const wasNull = column.nulls.pop();
+ column.size--;
+ if (wasNull === false) column.values.pop();
+ }
+ }
+ for (let index = this.columnList.length - 1; index >= 0; index--) {
+ const column = this.columnList[index];
+ if (this.rows === 0 && column.size === 0) {
+ this.columnsByName.delete(qwpColumnNameKey(column.name));
+ this.columnList.splice(index, 1);
+ }
+ }
+ }
+
+ /**
+ * Copies a completed half-open row range into an independent table buffer.
+ * Compact column values and their null bitmaps are sliced together, so the
+ * result can be encoded without materialising rows first.
+ */
+ sliceRows(start: number, end: number): QwpTableBuffer {
+ if (
+ !Number.isSafeInteger(start) ||
+ !Number.isSafeInteger(end) ||
+ start < 0 ||
+ end < start ||
+ end > this.rows
+ ) {
+ throw new RangeError(
+ `invalid QWP table row range [start=${start}, end=${end}, rows=${this.rows}]`,
+ );
+ }
+
+ const result = new QwpTableBuffer(this.name, this.maxNameLength);
+ result.rows = end - start;
+ // `values` holds non-null entries only, so a row index becomes a value
+ // index by skipping the nulls before it. A column with no nulls at all
+ // needs no scan (the common case), and for a sparse one the offset before
+ // `start` is memoized and advanced across slices rather than recounted from
+ // row 0 -- otherwise a caller walking the table in ascending slices
+ // (encodeUdpDatagrams, the ingress batch-cap search) is quadratic in its
+ // row count all over again.
+ const valueStarts = this.nonNullValueOffsets(start);
+ for (let index = 0; index < this.columnList.length; index++) {
+ const column = this.columnList[index];
+ const valueStart = valueStarts[index];
+ let valueEnd: number;
+ if (column.values.length === column.size) {
+ valueEnd = end;
+ } else {
+ valueEnd = valueStart;
+ for (let row = start; row < end; row++) {
+ if (!column.nulls[row]) valueEnd++;
+ }
+ }
+ const sliced: QwpColumnBuffer = {
+ name: column.name,
+ type: column.type,
+ values: column.values.slice(valueStart, valueEnd),
+ nulls: column.nulls.slice(start, end),
+ size: end - start,
+ geohashPrecision: column.geohashPrecision,
+ decimalScale: column.decimalScale,
+ };
+ result.columnList.push(sliced);
+ result.columnsByName.set(qwpColumnNameKey(sliced.name), sliced);
+ }
+ return result;
+ }
+
+ /**
+ * The non-null value count in rows `[0, start)` for each column -- the value
+ * index at which a slice starting at `start` begins. Recomputing this from
+ * row 0 on every call makes sliceRows() O(start), so the previous result is
+ * reused and advanced only over the newly covered rows when `start` moves
+ * forward, keeping an ascending walk linear. A dense column needs no scan;
+ * its value index equals the row index.
+ */
+ private nonNullValueOffsets(start: number): number[] {
+ const columns = this.columnList;
+ const cache = this.sliceValueOffsets;
+ const reuse =
+ cache !== undefined &&
+ cache.rows === this.rows &&
+ cache.offsets.length === columns.length &&
+ cache.start <= start;
+ const from = reuse ? cache.start : 0;
+ const offsets = reuse ? cache.offsets : new Array(columns.length);
+ for (let index = 0; index < columns.length; index++) {
+ const column = columns[index];
+ if (column.values.length === column.size) {
+ offsets[index] = start;
+ continue;
+ }
+ const nulls = column.nulls;
+ let offset = reuse ? offsets[index] : 0;
+ for (let row = from; row < start; row++) {
+ if (!nulls[row]) offset++;
+ }
+ offsets[index] = offset;
+ }
+ this.sliceValueOffsets = { rows: this.rows, start, offsets };
+ return offsets;
+ }
+
+ reset(): void {
+ this.columnList.length = 0;
+ this.columnsByName.clear();
+ this.rows = 0;
+ this.sliceValueOffsets = undefined;
+ }
+}
+
+export function flattenQwpArray(value: unknown[]): QwpArrayValue {
+ const dimensions: number[] = [];
+ let level: unknown = value;
+ while (Array.isArray(level)) {
+ dimensions.push(level.length);
+ level = level[0];
+ }
+ if (dimensions.length === 0 || dimensions.length > QWP_MAX_ARRAY_DIMENSIONS) {
+ throw new Error(
+ `QWP array must have between 1 and ${QWP_MAX_ARRAY_DIMENSIONS} dimensions`,
+ );
+ }
+
+ const values: (number | bigint)[] = [];
+ const walk = (node: unknown, depth: number): void => {
+ if (depth === dimensions.length) {
+ if (typeof node !== "number" && typeof node !== "bigint") {
+ throw new Error("QWP array elements must be numbers or bigints");
+ }
+ values.push(node);
+ return;
+ }
+ if (!Array.isArray(node) || node.length !== dimensions[depth]) {
+ throw new Error("irregular QWP array shape");
+ }
+ for (const child of node) walk(child, depth + 1);
+ };
+ walk(value, 0);
+ return { dimensions, values };
+}
diff --git a/packages/client-core/src/_qwp/_core/varint.ts b/packages/client-core/src/_qwp/_core/varint.ts
new file mode 100644
index 0000000..bb73d3f
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/varint.ts
@@ -0,0 +1,85 @@
+import { QwpByteReader, QwpByteWriter } from "./bytes";
+import { QwpProtocolError } from "./errors";
+
+const MAX_UINT64 = 0xffffffffffffffffn;
+
+function toBigInt(value: number | bigint): bigint {
+ if (typeof value === "number") {
+ if (!Number.isSafeInteger(value) || value < 0) {
+ throw new RangeError(
+ `varint requires a non-negative safe integer, got ${value}`,
+ );
+ }
+ return BigInt(value);
+ }
+ if (value < 0n || value > MAX_UINT64) {
+ throw new RangeError(`varint is outside the uint64 range: ${value}`);
+ }
+ return value;
+}
+
+/** Returns the encoded byte count of an unsigned LEB128 uint64. */
+export function qwpVarintSize(value: number | bigint): number {
+ let remaining = toBigInt(value);
+ let size = 1;
+ while (remaining >= 0x80n) {
+ remaining >>= 7n;
+ size++;
+ }
+ return size;
+}
+
+/** Writes an unsigned LEB128 uint64. */
+export function writeQwpVarint(
+ writer: QwpByteWriter,
+ value: number | bigint,
+): void {
+ let remaining = toBigInt(value);
+ while (remaining >= 0x80n) {
+ writer.writeUint8(Number(remaining & 0x7fn) | 0x80);
+ remaining >>= 7n;
+ }
+ writer.writeUint8(Number(remaining));
+}
+
+/** Reads an unsigned LEB128 uint64. */
+export function readQwpVarint(reader: QwpByteReader): bigint {
+ let value = 0n;
+ for (let index = 0; index < 10; index++) {
+ const byte = reader.readUint8("varint");
+ if (index === 9 && (byte & 0xfe) !== 0) {
+ throw new QwpProtocolError("QWP varint exceeds uint64 range");
+ }
+ value |= BigInt(byte & 0x7f) << BigInt(index * 7);
+ if ((byte & 0x80) === 0) return value;
+ }
+ throw new QwpProtocolError("QWP varint exceeds 10 bytes");
+}
+
+export function readQwpVarintNumber(
+ reader: QwpByteReader,
+ label = "varint",
+): number {
+ const value = readQwpVarint(reader);
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
+ throw new QwpProtocolError(
+ `${label} exceeds JavaScript's safe integer range`,
+ );
+ }
+ return Number(value);
+}
+
+export function encodeQwpVarint(value: number | bigint): Uint8Array {
+ const writer = new QwpByteWriter(qwpVarintSize(value));
+ writeQwpVarint(writer, value);
+ return writer.toUint8Array();
+}
+
+export function decodeQwpVarint(
+ bytes: Uint8Array,
+ offset = 0,
+): { value: bigint; offset: number } {
+ const reader = new QwpByteReader(bytes, offset);
+ const value = readQwpVarint(reader);
+ return { value, offset: reader.position };
+}
diff --git a/packages/client-core/src/_qwp/_core/zstd.ts b/packages/client-core/src/_qwp/_core/zstd.ts
new file mode 100644
index 0000000..3afa49f
--- /dev/null
+++ b/packages/client-core/src/_qwp/_core/zstd.ts
@@ -0,0 +1,316 @@
+import { decompress } from "fzstd";
+import { QwpProtocolError } from "./errors";
+
+/** Matches the Java client's per-connection decompression safety cap. */
+export const QWP_MAX_ZSTD_DECOMPRESSED_SIZE = 64 * 1024 * 1024;
+
+const ZSTD_MAGIC = 0xfd2fb528;
+const ZSTD_MAX_BLOCK_SIZE = 128 * 1024;
+
+/**
+ * Bytes of marker appended after the declared content, and the value they
+ * carry. fzstd decodes into the output buffer without reporting how far it
+ * got, so the marker is how the decoded length is observed: a run this long
+ * cannot be faked by a frame that stops early, because everything past what
+ * the frame wrote is the untouched zero tail of the buffer.
+ */
+const ZSTD_SIZE_MARKER_BYTES = 8;
+/**
+ * The marker written past a frame's declared content size, as an eight-byte
+ * raw block rather than a repeated byte.
+ *
+ * A run of one byte cannot say where it starts. A frame that ran long by k
+ * bytes pushes the marker to contentSize + k, leaving its own k bytes in front
+ * of it -- and a repeated-byte marker still matched at contentSize whenever
+ * those k bytes happened to be that byte. Only min(k, 8) of them had to,
+ * so overshooting by one needed a single byte with probability 1/256, and
+ * 0xa5 is a legal UTF-8 continuation byte, so a VARCHAR ending in one collided
+ * by accident.
+ *
+ * These eight bytes are distinct, so no proper prefix of the pattern equals a
+ * proper suffix of it and no shift can reproduce it. The last byte is non-zero
+ * so the scan for a short frame's marker still stops at the marker.
+ */
+const ZSTD_SIZE_MARKER = Uint8Array.of(
+ 0xa5,
+ 0x5a,
+ 0xc3,
+ 0x3c,
+ 0x69,
+ 0x96,
+ 0x0f,
+ 0xf0,
+);
+/**
+ * Room past the marker, so a frame that overshoots by up to this much still
+ * lands its marker inside the buffer and gets a report of the size it really
+ * decoded rather than a bare decompression failure.
+ */
+const ZSTD_SIZE_SLACK_BYTES = 8;
+/** How far back the marker is looked for when reporting a size mismatch. */
+const ZSTD_SIZE_SEARCH_BYTES = 64 * 1024;
+
+interface ZstdFrameInfo {
+ readonly contentSize: number;
+ readonly dataOffset: number;
+ readonly checksum: boolean;
+}
+
+interface ZstdBlockLayout {
+ /** Offset of the header of the block flagged last. */
+ readonly lastBlockOffset: number;
+ /** First byte after the last block, so before any content checksum. */
+ readonly blocksEnd: number;
+}
+
+function requireAvailable(
+ bytes: Uint8Array,
+ offset: number,
+ length: number,
+ label: string,
+): void {
+ if (offset < 0 || length < 0 || offset + length > bytes.byteLength) {
+ throw new QwpProtocolError(`truncated zstd ${label}`);
+ }
+}
+
+function readLittleEndian(
+ bytes: Uint8Array,
+ offset: number,
+ length: number,
+): bigint {
+ requireAvailable(bytes, offset, length, "frame header");
+ let value = 0n;
+ for (let index = 0; index < length; index++) {
+ value |= BigInt(bytes[offset + index]) << BigInt(index * 8);
+ }
+ return value;
+}
+
+function inspectZstdFrame(frame: Uint8Array): ZstdFrameInfo {
+ if (frame.byteLength > QWP_MAX_ZSTD_DECOMPRESSED_SIZE) {
+ throw new QwpProtocolError(
+ `zstd frame size ${frame.byteLength} exceeds client cap ${QWP_MAX_ZSTD_DECOMPRESSED_SIZE}`,
+ );
+ }
+ requireAvailable(frame, 0, 5, "frame header");
+ if (Number(readLittleEndian(frame, 0, 4)) !== ZSTD_MAGIC) {
+ throw new QwpProtocolError("invalid zstd frame magic");
+ }
+
+ const descriptor = frame[4];
+ if ((descriptor & 0x08) !== 0) {
+ throw new QwpProtocolError("zstd frame uses its reserved descriptor bit");
+ }
+ const singleSegment = (descriptor & 0x20) !== 0;
+ const checksum = (descriptor & 0x04) !== 0;
+ const dictionaryIdFlag = descriptor & 0x03;
+ const contentSizeFlag = descriptor >>> 6;
+ let offset = 5;
+
+ let windowSize: bigint | undefined;
+ if (!singleSegment) {
+ requireAvailable(frame, offset, 1, "window descriptor");
+ const windowDescriptor = frame[offset++];
+ const base = 1n << BigInt(10 + (windowDescriptor >>> 3));
+ windowSize = base + (base >> 3n) * BigInt(windowDescriptor & 0x07);
+ }
+
+ const dictionaryIdSize = dictionaryIdFlag === 3 ? 4 : dictionaryIdFlag;
+ requireAvailable(frame, offset, dictionaryIdSize, "dictionary ID");
+ if (dictionaryIdSize !== 0) {
+ throw new QwpProtocolError(
+ "zstd frames using an external dictionary are not supported",
+ );
+ }
+ offset += dictionaryIdSize;
+
+ const contentSizeBytes =
+ contentSizeFlag === 0 ? (singleSegment ? 1 : 0) : 1 << contentSizeFlag;
+ if (contentSizeBytes === 0) {
+ throw new QwpProtocolError(
+ "zstd frame is missing its declared content size",
+ );
+ }
+ let contentSize = readLittleEndian(frame, offset, contentSizeBytes);
+ offset += contentSizeBytes;
+ if (contentSizeFlag === 1) contentSize += 256n;
+
+ const cap = BigInt(QWP_MAX_ZSTD_DECOMPRESSED_SIZE);
+ if (contentSize > cap) {
+ throw new QwpProtocolError(
+ `zstd frame content size ${contentSize} exceeds client cap ${cap}`,
+ );
+ }
+ if (windowSize !== undefined && windowSize > cap) {
+ throw new QwpProtocolError(
+ `zstd frame window size ${windowSize} exceeds client cap ${cap}`,
+ );
+ }
+ return { contentSize: Number(contentSize), dataOffset: offset, checksum };
+}
+
+function validateSingleZstdFrame(
+ frame: Uint8Array,
+ info: ZstdFrameInfo,
+): ZstdBlockLayout {
+ let offset = info.dataOffset;
+ let lastBlockOffset = info.dataOffset;
+ let lastBlock = false;
+ while (!lastBlock) {
+ requireAvailable(frame, offset, 3, "block header");
+ const header =
+ frame[offset] | (frame[offset + 1] << 8) | (frame[offset + 2] << 16);
+ lastBlockOffset = offset;
+ offset += 3;
+ lastBlock = (header & 1) !== 0;
+ const blockType = (header >>> 1) & 0x03;
+ if (blockType === 3) {
+ throw new QwpProtocolError("zstd frame contains a reserved block type");
+ }
+ const blockSize = header >>> 3;
+ if (blockSize > ZSTD_MAX_BLOCK_SIZE) {
+ throw new QwpProtocolError(
+ `zstd block size ${blockSize} exceeds format maximum ${ZSTD_MAX_BLOCK_SIZE}`,
+ );
+ }
+ const encodedSize = blockType === 1 ? 1 : blockSize;
+ requireAvailable(frame, offset, encodedSize, "block body");
+ offset += encodedSize;
+ }
+ const blocksEnd = offset;
+ if (info.checksum) {
+ requireAvailable(frame, offset, 4, "content checksum");
+ offset += 4;
+ }
+ if (offset !== frame.byteLength) {
+ throw new QwpProtocolError(
+ `zstd body must contain exactly one frame [frameBytes=${offset}, actual=${frame.byteLength}]`,
+ );
+ }
+ return { lastBlockOffset, blocksEnd };
+}
+
+/**
+ * Reframes the blocks with a single-segment header, an eight-byte size marker
+ * appended as a final RLE block, and the content size that marker needs.
+ *
+ * fzstd sizes its output from the declared content size and never reports how
+ * far it actually got, so the marker is what makes the decoded length
+ * observable: it lands wherever the frame's own output ends, which is the
+ * declared content size and nowhere else for a frame that means what its
+ * header says. Those bytes are also the headroom that lets an over-long frame
+ * write past the declared size instead of being silently truncated into it.
+ *
+ * A single-segment window of the output's size is sufficient for all valid
+ * frames because no match can refer before the decoded content, and it is what
+ * makes fzstd decode in place: given a window that spans the whole output, it
+ * resolves matches against the output itself instead of shifting a separate
+ * window buffer down after every block, which is quadratic in the content
+ * size. That shift cost a 4 KB frame declaring 64 MiB about 1.5 seconds.
+ */
+function frameWithSizeMarker(
+ frame: Uint8Array,
+ info: ZstdFrameInfo,
+ layout: ZstdBlockLayout,
+): Uint8Array {
+ // Magic, then a single-segment descriptor with an 8-byte content size. The
+ // checksum flag is dropped along with the trailing checksum bytes: nothing
+ // verifies them, and the marker has to be the frame's last block.
+ const headerSize = 4 + 1 + 8;
+ const markerSize = 3 + ZSTD_SIZE_MARKER_BYTES;
+ const blocks = frame.subarray(info.dataOffset, layout.blocksEnd);
+ const reframed = new Uint8Array(headerSize + blocks.byteLength + markerSize);
+ reframed.set(frame.subarray(0, 4));
+ reframed[4] = 0xe0;
+ let size = BigInt(
+ info.contentSize + ZSTD_SIZE_MARKER_BYTES + ZSTD_SIZE_SLACK_BYTES,
+ );
+ for (let index = 0; index < 8; index++) {
+ reframed[5 + index] = Number(size & 0xffn);
+ size >>= 8n;
+ }
+ reframed.set(blocks, headerSize);
+ // The marker block is the last one now, so the block that was carries the
+ // flag no longer.
+ reframed[headerSize + (layout.lastBlockOffset - info.dataOffset)] &= ~1;
+ const marker = headerSize + blocks.byteLength;
+ // Raw block, not RLE: the marker has to be eight chosen bytes, and an RLE
+ // block can only repeat one.
+ const header = 1 | (0 << 1) | (ZSTD_SIZE_MARKER_BYTES << 3);
+ reframed[marker] = header & 0xff;
+ reframed[marker + 1] = (header >>> 8) & 0xff;
+ reframed[marker + 2] = (header >>> 16) & 0xff;
+ reframed.set(ZSTD_SIZE_MARKER, marker + 3);
+ return reframed;
+}
+
+function hasSizeMarkerAt(output: Uint8Array, offset: number): boolean {
+ if (offset < 0 || offset + ZSTD_SIZE_MARKER_BYTES > output.byteLength) {
+ return false;
+ }
+ for (let index = 0; index < ZSTD_SIZE_MARKER_BYTES; index++) {
+ if (output[offset + index] !== ZSTD_SIZE_MARKER[index]) return false;
+ }
+ return true;
+}
+
+/**
+ * Where the marker landed, searched downwards from `from`, or -1.
+ *
+ * Only a diagnostic: whether the frame is well formed at all was already
+ * settled by testing the declared offset. fzstd stages a block's literals in
+ * the unwritten tail of the output buffer, so that tail is not reliably zero
+ * and the marker cannot be found by scanning back over zeros. The search is
+ * bounded because a hostile frame chooses how far off its output ends.
+ */
+function findSizeMarker(output: Uint8Array, from: number): number {
+ const start = Math.min(from, output.byteLength - ZSTD_SIZE_MARKER_BYTES);
+ const floor = Math.max(0, start - ZSTD_SIZE_SEARCH_BYTES);
+ for (let offset = start; offset >= floor; offset--) {
+ if (hasSizeMarkerAt(output, offset)) return offset;
+ }
+ return -1;
+}
+
+/** Rejects a frame whose output did not end where its header said it would. */
+function requireDeclaredSize(output: Uint8Array, contentSize: number): void {
+ // The marker lands exactly where the frame's own output ended, and no shift
+ // of it can spell itself, so this is the whole test: it holds for a frame
+ // that means what its header says and for no other. A frame that overshot by
+ // more than the slack could not land its marker inside the buffer at all,
+ // and fzstd has already rejected it by the time we get here.
+ if (hasSizeMarkerAt(output, contentSize)) return;
+ const decoded = findSizeMarker(output, contentSize + ZSTD_SIZE_SLACK_BYTES);
+ if (decoded >= 0 && decoded !== contentSize) {
+ throw new QwpProtocolError(
+ decoded < contentSize
+ ? `zstd decompressed size ${decoded} does not match frame content size ${contentSize}`
+ : `zstd output exceeds declared content size ${contentSize} by ${decoded - contentSize}`,
+ );
+ }
+ throw new QwpProtocolError(
+ `zstd output exceeds declared content size ${contentSize}`,
+ );
+}
+
+/** Decompresses the single bounded Zstd frame carried by a RESULT_BATCH. */
+export function decompressQwpZstdFrame(frame: Uint8Array): Uint8Array {
+ const info = inspectZstdFrame(frame);
+ const layout = validateSingleZstdFrame(frame, info);
+ let output: Uint8Array;
+ try {
+ // fzstd allocates the output itself, from the declared content size the
+ // marker is accounted for in. Handing it a buffer of our own instead costs
+ // more than the decompression does: it compares that argument against a
+ // sentinel with `!=`, and coercing a 64 MiB Uint8Array to a string for
+ // that comparison took 840 ms where the whole decode takes 8 ms.
+ output = decompress(frameWithSizeMarker(frame, info, layout));
+ } catch (error) {
+ if (error instanceof QwpProtocolError) throw error;
+ const detail = error instanceof Error ? `: ${error.message}` : "";
+ throw new QwpProtocolError(`zstd decompression failed${detail}`);
+ }
+ requireDeclaredSize(output, info.contentSize);
+ return output.subarray(0, info.contentSize);
+}
diff --git a/packages/client-core/src/_qwp/_internal/async-queue.ts b/packages/client-core/src/_qwp/_internal/async-queue.ts
new file mode 100644
index 0000000..42a4a28
--- /dev/null
+++ b/packages/client-core/src/_qwp/_internal/async-queue.ts
@@ -0,0 +1,111 @@
+interface PendingNext {
+ resolve: (result: IteratorResult) => void;
+ reject: (error: unknown) => void;
+}
+
+interface QueueBarrier {
+ readonly kind: "barrier";
+ readonly resolve: () => void;
+ readonly reject: (error: unknown) => void;
+}
+
+interface QueueValue {
+ readonly kind: "value";
+ readonly value: T;
+}
+
+/** Single-consumer async queue used to preserve WebSocket message ordering. */
+export class QwpAsyncQueue implements AsyncIterable {
+ private readonly values: (QueueValue | QueueBarrier)[] = [];
+ private readonly pending: PendingNext[] = [];
+ private ended = false;
+ private failure: unknown;
+ private iteratorCreated = false;
+
+ push(value: T): void {
+ if (this.ended || this.failure !== undefined) return;
+ const pending = this.pending.shift();
+ if (pending) {
+ pending.resolve({ value, done: false });
+ } else {
+ this.values.push({ kind: "value", value });
+ }
+ }
+
+ end(): void {
+ if (this.ended || this.failure !== undefined) return;
+ this.ended = true;
+ this.settleBarriers();
+ for (const pending of this.pending.splice(0)) {
+ pending.resolve({ value: undefined, done: true });
+ }
+ }
+
+ fail(error: unknown): void {
+ if (this.ended || this.failure !== undefined) return;
+ this.failure = error;
+ this.settleBarriers(error);
+ for (const pending of this.pending.splice(0)) pending.reject(error);
+ }
+
+ /** Drops and returns values not yet handed to the single consumer. */
+ clear(): T[] {
+ const dropped: T[] = [];
+ for (const entry of this.values.splice(0)) {
+ if (entry.kind === "value") dropped.push(entry.value);
+ else entry.resolve();
+ }
+ return dropped;
+ }
+
+ /** Resolves once the consumer asks for the item after this queue position. */
+ barrier(): Promise {
+ if (this.ended || this.failure !== undefined || this.pending.length > 0) {
+ return Promise.resolve();
+ }
+ return new Promise((resolve, reject) => {
+ this.values.push({ kind: "barrier", resolve, reject });
+ });
+ }
+
+ [Symbol.asyncIterator](): AsyncIterator {
+ if (this.iteratorCreated) {
+ throw new Error("QWP message streams support only one consumer");
+ }
+ this.iteratorCreated = true;
+ return {
+ next: () => this.next(),
+ };
+ }
+
+ private next(): Promise> {
+ while (true) {
+ const entry = this.values.shift();
+ if (!entry) break;
+ if (entry.kind === "value") {
+ return Promise.resolve({ value: entry.value, done: false });
+ }
+ entry.resolve();
+ }
+ if (this.failure !== undefined) return Promise.reject(this.failure);
+ if (this.ended) {
+ return Promise.resolve({ value: undefined, done: true });
+ }
+ return new Promise((resolve, reject) => {
+ this.pending.push({ resolve, reject });
+ });
+ }
+
+ private settleBarriers(error?: unknown): void {
+ const entries = this.values.splice(0);
+ for (const entry of entries) {
+ if (entry.kind === "value") {
+ this.values.push(entry);
+ } else if (error === undefined) {
+ entry.resolve();
+ } else {
+ entry.reject(error);
+ }
+ }
+ }
+}
diff --git a/packages/client-core/src/_qwp/_internal/egress-limits.ts b/packages/client-core/src/_qwp/_internal/egress-limits.ts
new file mode 100644
index 0000000..1afc651
--- /dev/null
+++ b/packages/client-core/src/_qwp/_internal/egress-limits.ts
@@ -0,0 +1,17 @@
+import { QWP_MAX_BATCH_ROWS_UPPER_BOUND } from "../_core";
+
+export function validateQwpMaxBatchRows(
+ value: number | undefined,
+): number | undefined {
+ if (value === undefined) return undefined;
+ if (
+ !Number.isSafeInteger(value) ||
+ value < 1 ||
+ value > QWP_MAX_BATCH_ROWS_UPPER_BOUND
+ ) {
+ throw new RangeError(
+ `maxBatchRows must be an integer between 1 and ${QWP_MAX_BATCH_ROWS_UPPER_BOUND}`,
+ );
+ }
+ return value;
+}
diff --git a/packages/client-core/src/_qwp/_internal/egress-routing.ts b/packages/client-core/src/_qwp/_internal/egress-routing.ts
new file mode 100644
index 0000000..5d15aec
--- /dev/null
+++ b/packages/client-core/src/_qwp/_internal/egress-routing.ts
@@ -0,0 +1,140 @@
+import {
+ decodeQwpEgressMessage,
+ QWP_SERVER_ROLE,
+ QwpProtocolError,
+} from "../_core";
+import {
+ QwpBinaryConnection,
+ QwpConnectionFactory,
+ QwpSendClosedError,
+} from "../transport";
+import {
+ createQwpFailoverConnectionFactory,
+ QwpFailoverSelectionOptions,
+ QwpValidatedConnection,
+} from "./failover";
+
+/**
+ * Creates an egress endpoint walker that validates authoritative SERVER_INFO
+ * topology before exposing a connection. Reading the frame here works in both
+ * Node and browsers; the frame is replayed to the normal session consumer.
+ */
+export function createQwpEgressFailoverConnectionFactory(
+ preferredUrl: string | URL,
+ failoverUrls: readonly (string | URL)[] | undefined,
+ connect: (
+ endpoint: string | URL,
+ signal?: AbortSignal,
+ ) => Promise,
+ routing: QwpFailoverSelectionOptions,
+ serverInfoTimeoutMs: number,
+): QwpConnectionFactory {
+ return createQwpFailoverConnectionFactory(
+ preferredUrl,
+ failoverUrls,
+ connect,
+ {
+ ...routing,
+ validateConnection: (connection) =>
+ readAndReplayServerInfo(connection, serverInfoTimeoutMs),
+ },
+ );
+}
+
+async function readAndReplayServerInfo(
+ connection: QwpBinaryConnection,
+ timeoutMs: number,
+): Promise {
+ const iterator = connection.messages[Symbol.asyncIterator]();
+ let timer: ReturnType | undefined;
+ const timeout = new Promise((_resolve, reject) => {
+ timer = setTimeout(
+ () => reject(new Error("timed out waiting for QWP SERVER_INFO")),
+ timeoutMs,
+ );
+ });
+ try {
+ const result = await Promise.race([iterator.next(), timeout]);
+ if (result.done) {
+ throw new QwpSendClosedError(await connection.closed);
+ }
+ const serverInfo = decodeQwpEgressMessage(result.value);
+ if (serverInfo.kind !== "server-info") {
+ throw new QwpProtocolError(
+ "QWP egress connection did not begin with SERVER_INFO",
+ );
+ }
+ const serverRole = serverRoleName(serverInfo.role);
+ const serverZone =
+ serverInfo.zoneId ?? connection.handshake.serverZone ?? undefined;
+ return {
+ connection: prependMessage(connection, result.value, iterator, {
+ serverRole,
+ serverZone,
+ }),
+ serverRole,
+ serverZone,
+ };
+ } finally {
+ if (timer) clearTimeout(timer);
+ }
+}
+
+function serverRoleName(role: number): string {
+ switch (role) {
+ case QWP_SERVER_ROLE.STANDALONE:
+ return "STANDALONE";
+ case QWP_SERVER_ROLE.PRIMARY:
+ return "PRIMARY";
+ case QWP_SERVER_ROLE.REPLICA:
+ return "REPLICA";
+ case QWP_SERVER_ROLE.PRIMARY_CATCHUP:
+ return "PRIMARY_CATCHUP";
+ default:
+ return `UNKNOWN(${role})`;
+ }
+}
+
+function prependMessage(
+ connection: QwpBinaryConnection,
+ first: Uint8Array,
+ iterator: AsyncIterator,
+ topology: { readonly serverRole: string; readonly serverZone?: string },
+): QwpBinaryConnection {
+ let consumed = false;
+ const messages: AsyncIterable = {
+ async *[Symbol.asyncIterator]() {
+ if (consumed) {
+ throw new QwpProtocolError(
+ "QWP connection messages already have a consumer",
+ );
+ }
+ consumed = true;
+ yield first;
+ while (true) {
+ const result = await iterator.next();
+ if (result.done) return;
+ yield result.value;
+ }
+ },
+ };
+ const wrapped: QwpBinaryConnection = {
+ messages,
+ closed: connection.closed,
+ handshake: { ...connection.handshake, ...topology },
+ endpoint: connection.endpoint,
+ get ingressSymbolDictionary() {
+ return connection.ingressSymbolDictionary;
+ },
+ get ingressDeltaSymbolDictionaryEnabled() {
+ return connection.ingressDeltaSymbolDictionaryEnabled;
+ },
+ send: (payload) => connection.send(payload),
+ close: (code, reason) => connection.close(code, reason),
+ };
+ if (connection.ping) wrapped.ping = () => connection.ping!();
+ if (connection.getIngressMetrics) {
+ wrapped.getIngressMetrics = () => connection.getIngressMetrics!();
+ }
+ return wrapped;
+}
diff --git a/packages/client-core/src/_qwp/_internal/failover.ts b/packages/client-core/src/_qwp/_internal/failover.ts
new file mode 100644
index 0000000..c6d0ee9
--- /dev/null
+++ b/packages/client-core/src/_qwp/_internal/failover.ts
@@ -0,0 +1,451 @@
+import {
+ QWP_TARGET,
+ QWP_UPGRADE_ERROR_KIND,
+ QwpBinaryConnection,
+ QwpConnectionFactory,
+ QwpEgressRoutingOptions,
+ QwpFailoverAttempt,
+ QwpFailoverError,
+ QwpRoleMismatchError,
+ QwpTarget,
+ QwpUpgradeError,
+} from "../transport";
+
+const HOST_STATE = {
+ HEALTHY: 0,
+ UNKNOWN: 1,
+ TRANSIENT_REJECT: 2,
+ TRANSPORT_ERROR: 3,
+ TOPOLOGY_REJECT: 4,
+} as const;
+
+type HostState = (typeof HOST_STATE)[keyof typeof HOST_STATE];
+
+const ZONE_TIER = {
+ SAME: 0,
+ UNKNOWN: 1,
+ OTHER: 2,
+} as const;
+
+type ZoneTier = (typeof ZONE_TIER)[keyof typeof ZONE_TIER];
+
+interface QwpEndpointHealth {
+ state: HostState;
+ zoneTier: ZoneTier;
+ lastSuccessEpoch: number;
+}
+
+/**
+ * Shared endpoint classifications used by independent connection walkers.
+ * Each factory keeps its own sweep cursor while publishing observations here,
+ * so concurrent pooled sessions and orphan drainers cannot steal attempts from
+ * one another but immediately benefit from one another's health discoveries.
+ */
+export class QwpFailoverHealthTracker {
+ private readonly endpointKeys: readonly string[];
+ private readonly health: QwpEndpointHealth[];
+ private successEpoch = 0;
+
+ constructor(
+ preferredUrl: string | URL,
+ failoverUrls: readonly (string | URL)[] | undefined,
+ private readonly target: QwpTarget,
+ private readonly configuredZone: string | undefined,
+ ) {
+ this.endpointKeys = endpointKeys(preferredUrl, failoverUrls);
+ const zoneBlind = this.zoneBlind;
+ this.health = this.endpointKeys.map(() => ({
+ state: HOST_STATE.UNKNOWN,
+ zoneTier: zoneBlind ? ZONE_TIER.SAME : ZONE_TIER.UNKNOWN,
+ lastSuccessEpoch: 0,
+ }));
+ }
+
+ assertCompatible(
+ preferredUrl: string | URL,
+ failoverUrls: readonly (string | URL)[] | undefined,
+ target: QwpTarget,
+ configuredZone: string | undefined,
+ ): void {
+ const keys = endpointKeys(preferredUrl, failoverUrls);
+ if (
+ target !== this.target ||
+ configuredZone !== this.configuredZone ||
+ keys.length !== this.endpointKeys.length ||
+ keys.some((key, index) => key !== this.endpointKeys[index])
+ ) {
+ throw new RangeError(
+ "QWP failover health tracker does not match the endpoint routing configuration",
+ );
+ }
+ }
+
+ newRoundCursor(deferredEndpoint?: number): QwpFailoverRoundCursor {
+ return new QwpFailoverRoundCursor(this.health, deferredEndpoint);
+ }
+
+ /**
+ * Starts a recovery round with stale classifications forgotten. The newest
+ * successful same-zone endpoint stays healthy, matching the Java client's
+ * locality-aware stickiness; learned zone tiers persist across rounds.
+ */
+ forgetClassifications(): void {
+ let stickyIndex = -1;
+ let newestSuccess = -1;
+ for (let index = 0; index < this.health.length; index++) {
+ const health = this.health[index];
+ if (
+ health.state === HOST_STATE.HEALTHY &&
+ health.zoneTier === ZONE_TIER.SAME &&
+ health.lastSuccessEpoch > newestSuccess
+ ) {
+ stickyIndex = index;
+ newestSuccess = health.lastSuccessEpoch;
+ }
+ }
+ for (let index = 0; index < this.health.length; index++) {
+ if (index !== stickyIndex) this.health[index].state = HOST_STATE.UNKNOWN;
+ }
+ }
+
+ recordFailure(index: number, error: unknown): void {
+ const health = this.health[index];
+ if (error instanceof QwpUpgradeError) {
+ this.recordZone(index, error.serverZone);
+ if (error.kind === QWP_UPGRADE_ERROR_KIND.ROLE_REJECTED) {
+ health.state =
+ normalizeRole(error.serverRole) === "PRIMARY_CATCHUP"
+ ? HOST_STATE.TRANSIENT_REJECT
+ : HOST_STATE.TOPOLOGY_REJECT;
+ return;
+ }
+ }
+ health.state = HOST_STATE.TRANSPORT_ERROR;
+ }
+
+ recordSuccess(index: number): void {
+ const health = this.health[index];
+ health.state = HOST_STATE.HEALTHY;
+ health.lastSuccessEpoch = ++this.successEpoch;
+ }
+
+ recordZone(index: number, serverZone: string | undefined): void {
+ const normalized = normalizeZone(serverZone);
+ if (!normalized) return;
+ this.health[index].zoneTier =
+ this.zoneBlind || normalized === this.configuredZone
+ ? ZONE_TIER.SAME
+ : ZONE_TIER.OTHER;
+ }
+
+ recordMidStreamFailure(index: number): void {
+ const health = this.health[index];
+ if (health.state === HOST_STATE.HEALTHY) {
+ health.state = HOST_STATE.TRANSPORT_ERROR;
+ }
+ }
+
+ recordTransientReject(index: number): void {
+ this.health[index].state = HOST_STATE.TRANSIENT_REJECT;
+ }
+
+ private get zoneBlind(): boolean {
+ return (
+ this.configuredZone === undefined || this.target === QWP_TARGET.PRIMARY
+ );
+ }
+}
+
+class QwpFailoverRoundCursor {
+ private readonly attempted = new Set();
+
+ constructor(
+ private readonly health: readonly QwpEndpointHealth[],
+ private readonly deferredEndpoint?: number,
+ ) {}
+
+ next(): number | undefined {
+ const selected = pickNextEndpoint(
+ this.health,
+ this.attempted,
+ this.deferredEndpoint,
+ );
+ if (selected === undefined) return undefined;
+ this.attempted.add(selected);
+ return selected;
+ }
+
+ get exhausted(): boolean {
+ return this.attempted.size === this.health.length;
+ }
+}
+
+export interface QwpValidatedConnection {
+ readonly connection: QwpBinaryConnection;
+ readonly serverRole?: string;
+ readonly serverZone?: string;
+}
+
+export interface QwpFailoverSelectionOptions extends QwpEgressRoutingOptions {
+ /** @internal Reads protocol-level topology metadata when headers are hidden. */
+ validateConnection?: (
+ connection: QwpBinaryConnection,
+ ) => Promise;
+ /** @internal Shares classifications without sharing a walker's cursor. */
+ healthTracker?: QwpFailoverHealthTracker;
+ /** @internal Background walkers must not reset shared classifications. */
+ resetClassificationsAfterExhaustion?: boolean;
+}
+
+/** Creates a health ledger that can be shared by independent walkers. */
+export function createQwpFailoverHealthTracker(
+ preferredUrl: string | URL,
+ failoverUrls: readonly (string | URL)[] | undefined,
+ options: QwpEgressRoutingOptions = {},
+): QwpFailoverHealthTracker {
+ return new QwpFailoverHealthTracker(
+ preferredUrl,
+ failoverUrls,
+ normalizeTarget(options.target),
+ normalizeZone(options.zone),
+ );
+}
+
+/**
+ * Creates a stateful endpoint walker ordered by health and then zone affinity.
+ * Every invocation still performs a complete sweep, so stale role/health data
+ * can never permanently exclude an endpoint whose state has changed.
+ */
+export function createQwpFailoverConnectionFactory(
+ preferredUrl: string | URL,
+ failoverUrls: readonly (string | URL)[] | undefined,
+ connect: (
+ endpoint: string | URL,
+ signal?: AbortSignal,
+ ) => Promise,
+ options: QwpFailoverSelectionOptions = {},
+): QwpConnectionFactory {
+ const endpoints = [preferredUrl, ...(failoverUrls ?? [])];
+ const target = normalizeTarget(options.target);
+ const configuredZone = normalizeZone(options.zone);
+ const healthTracker =
+ options.healthTracker ??
+ new QwpFailoverHealthTracker(
+ preferredUrl,
+ failoverUrls,
+ target,
+ configuredZone,
+ );
+ healthTracker.assertCompatible(
+ preferredUrl,
+ failoverUrls,
+ target,
+ configuredZone,
+ );
+ const resetClassificationsAfterExhaustion =
+ options.resetClassificationsAfterExhaustion !== false;
+ let deferredEndpoint: number | undefined;
+ let resetClassificationsBeforeSweep = false;
+
+ return async (signal?: AbortSignal): Promise => {
+ if (
+ resetClassificationsBeforeSweep &&
+ resetClassificationsAfterExhaustion
+ ) {
+ healthTracker.forgetClassifications();
+ }
+ resetClassificationsBeforeSweep = false;
+ const attempts: QwpFailoverAttempt[] = [];
+ const deferredForSweep = deferredEndpoint;
+ deferredEndpoint = undefined;
+ const cursor = healthTracker.newRoundCursor(deferredForSweep);
+
+ while (true) {
+ const index = cursor.next();
+ if (index === undefined) break;
+ const endpoint = endpoints[index];
+ let candidate: QwpBinaryConnection | undefined;
+ try {
+ candidate = await connect(endpoint, signal);
+ let validated: QwpValidatedConnection = {
+ connection: candidate,
+ serverRole: candidate.handshake.serverRole,
+ serverZone: candidate.handshake.serverZone,
+ };
+ if (options.validateConnection) {
+ const protocolValidated = await options.validateConnection(candidate);
+ validated = {
+ connection: protocolValidated.connection,
+ serverRole:
+ protocolValidated.serverRole ?? candidate.handshake.serverRole,
+ serverZone:
+ protocolValidated.serverZone ?? candidate.handshake.serverZone,
+ };
+ }
+ candidate = validated.connection;
+ healthTracker.recordZone(index, validated.serverZone);
+ if (!matchesTarget(validated.serverRole, target)) {
+ throw new QwpRoleMismatchError(
+ target,
+ validated.serverRole,
+ endpoint,
+ validated.serverZone,
+ );
+ }
+ healthTracker.recordSuccess(index);
+ resetClassificationsBeforeSweep = cursor.exhausted;
+ return observeConnectionHealth(
+ candidate,
+ () => {
+ healthTracker.recordMidStreamFailure(index);
+ },
+ () => {
+ healthTracker.recordTransientReject(index);
+ deferredEndpoint = index;
+ },
+ );
+ } catch (error) {
+ healthTracker.recordFailure(index, error);
+ attempts.push({ endpoint, error });
+ if (candidate) await candidate.close().catch(() => undefined);
+ // tryNextEndpoint is a tri-state: only an explicit false short-circuits
+ // the sweep. A browser cannot see the HTTP response, so every refused,
+ // reset, or non-101 upgrade it reports is `undefined`; treating that as
+ // "stop" would make failoverUrls unreachable in browsers. This matches
+ // isRetryableReconnectError(), which reads the sibling `retryable` flag
+ // of the same tri-state as `!== false`.
+ if (
+ error instanceof QwpUpgradeError &&
+ error.tryNextEndpoint === false
+ ) {
+ throw error;
+ }
+ }
+ }
+ resetClassificationsBeforeSweep = true;
+ if (attempts.length === 1) throw attempts[0].error;
+ throw new QwpFailoverError(attempts);
+ };
+}
+
+function normalizeTarget(target: QwpTarget | undefined): QwpTarget {
+ const effective = target ?? QWP_TARGET.ANY;
+ if (
+ effective !== QWP_TARGET.ANY &&
+ effective !== QWP_TARGET.PRIMARY &&
+ effective !== QWP_TARGET.REPLICA
+ ) {
+ throw new RangeError("target must be one of: any, primary, replica");
+ }
+ return effective;
+}
+
+function normalizeZone(zone: string | undefined): string | undefined {
+ const normalized = zone?.trim().toLowerCase();
+ return normalized || undefined;
+}
+
+function normalizeRole(role: string | undefined): string | undefined {
+ const normalized = role?.trim().toUpperCase().replace(/-/g, "_");
+ return normalized || undefined;
+}
+
+function matchesTarget(role: string | undefined, target: QwpTarget): boolean {
+ if (target === QWP_TARGET.ANY) return true;
+ const normalized = normalizeRole(role);
+ // An endpoint that declares no role is accepted whatever the target. Egress
+ // always learns one from SERVER_INFO, but ingress reads it from an upgrade
+ // response header that an older server may not send and a proxy may strip,
+ // and refusing to write to a node purely because it stayed silent would take
+ // a working deployment offline. A server that does know its role still
+ // rejects a misdirected write itself, with the 421 this client classifies as
+ // ROLE_REJECTED.
+ if (normalized === undefined) return true;
+ if (target === QWP_TARGET.REPLICA) return normalized === "REPLICA";
+ return (
+ normalized === "PRIMARY" ||
+ normalized === "PRIMARY_CATCHUP" ||
+ normalized === "STANDALONE"
+ );
+}
+
+function pickNextEndpoint(
+ health: readonly QwpEndpointHealth[],
+ attempted: ReadonlySet,
+ deferredEndpoint?: number,
+): number | undefined {
+ let selected = -1;
+ for (let index = 0; index < health.length; index++) {
+ if (attempted.has(index) || index === deferredEndpoint) continue;
+ if (selected < 0 || compareHealth(health[index], health[selected]) < 0) {
+ selected = index;
+ }
+ }
+ if (selected >= 0) return selected;
+ if (deferredEndpoint !== undefined && !attempted.has(deferredEndpoint)) {
+ return deferredEndpoint;
+ }
+ return undefined;
+}
+
+function compareHealth(
+ left: QwpEndpointHealth,
+ right: QwpEndpointHealth,
+): number {
+ if (left.state !== right.state) return left.state - right.state;
+ if (left.zoneTier !== right.zoneTier) return left.zoneTier - right.zoneTier;
+ return 0;
+}
+
+function observeConnectionHealth(
+ connection: QwpBinaryConnection,
+ demoteEndpoint: () => void,
+ deprioritizeEndpoint: () => void,
+): QwpBinaryConnection {
+ void connection.closed.then((info) => {
+ if (!info.wasClean) demoteEndpoint();
+ }, demoteEndpoint);
+ const observed: QwpBinaryConnection = {
+ messages: connection.messages,
+ closed: connection.closed,
+ handshake: connection.handshake,
+ endpoint: connection.endpoint,
+ get ingressSymbolDictionary() {
+ return connection.ingressSymbolDictionary;
+ },
+ get ingressDeltaSymbolDictionaryEnabled() {
+ return connection.ingressDeltaSymbolDictionaryEnabled;
+ },
+ deprioritizeEndpoint,
+ send: async (payload) => {
+ try {
+ await connection.send(payload);
+ } catch (error) {
+ demoteEndpoint();
+ throw error;
+ }
+ },
+ close: (code, reason) => connection.close(code, reason),
+ };
+ if (connection.ping) {
+ observed.ping = async () => {
+ try {
+ await connection.ping!();
+ } catch (error) {
+ demoteEndpoint();
+ throw error;
+ }
+ };
+ }
+ if (connection.getIngressMetrics) {
+ observed.getIngressMetrics = () => connection.getIngressMetrics!();
+ }
+ return observed;
+}
+
+function endpointKeys(
+ preferredUrl: string | URL,
+ failoverUrls: readonly (string | URL)[] | undefined,
+): readonly string[] {
+ return [preferredUrl, ...(failoverUrls ?? [])].map(String);
+}
diff --git a/packages/client-core/src/_qwp/_internal/notification-dispatcher.ts b/packages/client-core/src/_qwp/_internal/notification-dispatcher.ts
new file mode 100644
index 0000000..3eb1d7b
--- /dev/null
+++ b/packages/client-core/src/_qwp/_internal/notification-dispatcher.ts
@@ -0,0 +1,143 @@
+import { isPromiseLike } from "./safe-callback";
+
+export interface QwpNotificationDispatcherMetrics {
+ readonly pending: number;
+ readonly delivered: number;
+ readonly dropped: number;
+ readonly closing: boolean;
+ readonly closed: boolean;
+}
+
+/**
+ * Browser-safe, bounded callback mailbox.
+ *
+ * One notification is delivered per event-loop turn so protocol work already
+ * queued by the WebSocket is not performed inside user callback stacks. When
+ * the inbox fills, the oldest pending notification is discarded and the most
+ * recent state is retained, matching the Java QWP dispatchers.
+ */
+export class QwpNotificationDispatcher {
+ private readonly queue: T[] = [];
+ private timer?: ReturnType;
+ private closeTimer?: ReturnType;
+ private closePromise?: Promise;
+ private resolveClose?: () => void;
+ private dispatching = false;
+ private closing = false;
+ private closed = false;
+ private delivered = 0;
+ private dropped = 0;
+
+ constructor(
+ private readonly handler: (notification: T) => unknown,
+ private readonly capacity: number,
+ ) {
+ if (!Number.isSafeInteger(capacity) || capacity < 1) {
+ throw new RangeError(
+ "QWP notification inbox capacity must be a positive safe integer",
+ );
+ }
+ }
+
+ get metrics(): QwpNotificationDispatcherMetrics {
+ return Object.freeze({
+ pending: this.queue.length,
+ delivered: this.delivered,
+ dropped: this.dropped,
+ closing: this.closing,
+ closed: this.closed,
+ });
+ }
+
+ /** Non-blocking enqueue with drop-oldest overflow. */
+ offer(notification: T): boolean {
+ if (this.closing || this.closed) return false;
+ if (this.queue.length >= this.capacity) {
+ this.queue.shift();
+ this.dropped++;
+ }
+ this.queue.push(notification);
+ this.schedule();
+ return true;
+ }
+
+ /**
+ * Stops accepting new notifications and best-effort drains the retained
+ * tail. Any entries still pending at the deadline are counted as dropped.
+ */
+ close(drainDeadlineMs = 100): Promise {
+ if (this.closePromise) return this.closePromise;
+ if (!Number.isFinite(drainDeadlineMs) || drainDeadlineMs < 0) {
+ return Promise.reject(
+ new RangeError(
+ "QWP notification drain deadline must be non-negative and finite",
+ ),
+ );
+ }
+ this.closing = true;
+ this.closePromise = new Promise((resolve) => {
+ this.resolveClose = resolve;
+ });
+ if (this.queue.length === 0 && !this.dispatching) {
+ this.finishClose();
+ return this.closePromise;
+ }
+ this.schedule();
+ this.closeTimer = setTimeout(() => {
+ this.closeTimer = undefined;
+ this.dropped += this.queue.length;
+ this.queue.length = 0;
+ if (!this.dispatching) this.finishClose();
+ }, drainDeadlineMs);
+ unrefTimer(this.closeTimer);
+ return this.closePromise;
+ }
+
+ private schedule(): void {
+ if (this.timer || this.dispatching || this.closed) return;
+ this.timer = setTimeout(() => {
+ this.timer = undefined;
+ this.dispatchOne();
+ }, 0);
+ unrefTimer(this.timer);
+ }
+
+ private dispatchOne(): void {
+ if (this.closed || this.dispatching) return;
+ const notification = this.queue.shift();
+ if (notification === undefined) {
+ if (this.closing) this.finishClose();
+ return;
+ }
+ this.dispatching = true;
+ this.delivered++;
+ try {
+ const result = this.handler(notification);
+ if (isPromiseLike(result)) void result.then(undefined, () => undefined);
+ } catch {
+ // Observability callbacks never participate in protocol progress.
+ } finally {
+ this.dispatching = false;
+ }
+ if (this.queue.length > 0) {
+ this.schedule();
+ } else if (this.closing) {
+ this.finishClose();
+ }
+ }
+
+ private finishClose(): void {
+ if (this.closed) return;
+ this.closed = true;
+ if (this.timer) clearTimeout(this.timer);
+ if (this.closeTimer) clearTimeout(this.closeTimer);
+ this.timer = undefined;
+ this.closeTimer = undefined;
+ this.resolveClose?.();
+ this.resolveClose = undefined;
+ }
+}
+
+function unrefTimer(timer: ReturnType): void {
+ (timer as ReturnType & { unref?: () => void }).unref?.();
+}
diff --git a/packages/client-core/src/_qwp/_internal/reconnect-backoff.ts b/packages/client-core/src/_qwp/_internal/reconnect-backoff.ts
new file mode 100644
index 0000000..2089829
--- /dev/null
+++ b/packages/client-core/src/_qwp/_internal/reconnect-backoff.ts
@@ -0,0 +1,9 @@
+/**
+ * Applies full jitter to an exponential-backoff ceiling. Full jitter keeps the
+ * configured maximum a hard upper bound while spreading clients throughout
+ * every retry window after a shared outage.
+ */
+export function jitterReconnectDelayMs(ceilingMs: number): number {
+ if (ceilingMs <= 0) return 0;
+ return Math.floor(Math.random() * ceilingMs);
+}
diff --git a/packages/client-core/src/_qwp/_internal/reconnecting-egress-connection.ts b/packages/client-core/src/_qwp/_internal/reconnecting-egress-connection.ts
new file mode 100644
index 0000000..ec33fc1
--- /dev/null
+++ b/packages/client-core/src/_qwp/_internal/reconnecting-egress-connection.ts
@@ -0,0 +1,738 @@
+import {
+ decodeQwpEgressMessage,
+ QWP_EGRESS_MESSAGE,
+ QwpProtocolError,
+ QwpServerInfoMessage,
+} from "../_core";
+import {
+ QWP_RECONNECT_EVENT_KIND,
+ QWP_UPGRADE_ERROR_KIND,
+ QwpBinaryConnection,
+ QwpConnectionCloseInfo,
+ QwpConnectionFactory,
+ QwpEgressReplayResetEvent,
+ QwpFailoverError,
+ QwpHandshakeMetadata,
+ QwpReconnectEvent,
+ QwpReconnectExhaustedError,
+ QwpReconnectOptions,
+ QwpSendClosedError,
+ QwpUpgradeError,
+} from "../transport";
+import { QwpAsyncQueue } from "./async-queue";
+import { jitterReconnectDelayMs } from "./reconnect-backoff";
+import { safelyInvoke } from "./safe-callback";
+
+type ReplayResetHandler = (
+ event: QwpEgressReplayResetEvent,
+) => void | Promise;
+type ConnectionResetHandler = (
+ serverInfo: QwpServerInfoMessage,
+) => void | Promise;
+type QueryRequestEncoder = (
+ serverInfo: QwpServerInfoMessage,
+ requestId: bigint,
+) => Uint8Array | Promise;
+
+class ReplayResetCallbackError extends Error {
+ readonly cause: unknown;
+
+ constructor(cause: unknown) {
+ super("QWP egress replay reset callback failed");
+ this.name = "ReplayResetCallbackError";
+ this.cause = cause;
+ }
+}
+
+class ReplayStateError extends QwpProtocolError {
+ readonly cause?: unknown;
+
+ constructor(message: string, cause?: unknown) {
+ super(message);
+ this.name = "ReplayStateError";
+ this.cause = cause;
+ }
+}
+
+/**
+ * Reconnects an egress wire and replays the in-flight request and its control
+ * messages. Statements may therefore be executed more than once when their
+ * outcome was lost with the connection.
+ */
+export class QwpReconnectingEgressConnection implements QwpBinaryConnection {
+ private readonly messagesQueue = new QwpAsyncQueue();
+ private readonly maxAttempts: number;
+ private readonly initialBackoffMs: number;
+ private readonly maxBackoffMs: number;
+ private readonly maxDurationMs: number;
+ private readonly resolveClosed: (info: QwpConnectionCloseInfo) => void;
+ private connection?: QwpBinaryConnection;
+ private connectingCandidate?: QwpBinaryConnection;
+ private connectAbort?: AbortController;
+ private lastHandshake?: QwpHandshakeMetadata;
+ private lastEndpoint?: string | URL;
+ private initialServerInfo?: QwpServerInfoMessage;
+ private currentServerInfo?: QwpServerInfoMessage;
+ private outboundReplay: Uint8Array[] = [];
+ private protocolRecoveries = 0;
+ private protocolRecoveryStartedAt = 0;
+ private generation = 0;
+ private sendTail: Promise = Promise.resolve();
+ private reconnectTask?: Promise;
+ private terminalError?: Error;
+ private cancelBackoff?: () => void;
+ private closing = false;
+ private closedSettled = false;
+ readonly messages: AsyncIterable = this.messagesQueue;
+ readonly closed: Promise;
+
+ private constructor(
+ private readonly factory: QwpConnectionFactory,
+ private readonly reconnectOptions: QwpReconnectOptions,
+ private readonly serverInfoTimeoutMs: number,
+ private readonly onConnectionReset: ConnectionResetHandler,
+ private readonly encodeQueryRequest: QueryRequestEncoder,
+ private readonly onReplayReset?: ReplayResetHandler,
+ private readonly retryInitialConnection = true,
+ ) {
+ this.maxAttempts = reconnectOptions.maxAttempts ?? 8;
+ this.initialBackoffMs = reconnectOptions.initialBackoffMs ?? 50;
+ this.maxBackoffMs = reconnectOptions.maxBackoffMs ?? 1_000;
+ this.maxDurationMs = reconnectOptions.maxDurationMs ?? 30_000;
+ validateReconnectPolicy(
+ this.maxAttempts,
+ this.initialBackoffMs,
+ this.maxBackoffMs,
+ this.maxDurationMs,
+ );
+ let resolveClosed!: (info: QwpConnectionCloseInfo) => void;
+ this.closed = new Promise((resolve) => {
+ resolveClosed = resolve;
+ });
+ this.resolveClosed = resolveClosed;
+ }
+
+ static async connect(
+ factory: QwpConnectionFactory,
+ reconnectOptions: QwpReconnectOptions,
+ serverInfoTimeoutMs: number,
+ onConnectionReset: ConnectionResetHandler,
+ encodeQueryRequest: QueryRequestEncoder,
+ onReplayReset?: ReplayResetHandler,
+ retryInitialConnection = true,
+ signal?: AbortSignal,
+ ): Promise {
+ const reconnecting = new QwpReconnectingEgressConnection(
+ factory,
+ reconnectOptions,
+ serverInfoTimeoutMs,
+ onConnectionReset,
+ encodeQueryRequest,
+ onReplayReset,
+ retryInitialConnection,
+ );
+ const abortOpening = (): void => {
+ void reconnecting.close().catch(() => undefined);
+ };
+ try {
+ if (signal?.aborted) throw new QwpSendClosedError();
+ signal?.addEventListener("abort", abortOpening, { once: true });
+ await reconnecting.connectLoop(undefined, false);
+ if (signal?.aborted) throw new QwpSendClosedError();
+ return reconnecting;
+ } catch (error) {
+ await reconnecting.close().catch(() => undefined);
+ throw error;
+ } finally {
+ signal?.removeEventListener("abort", abortOpening);
+ }
+ }
+
+ get handshake(): QwpHandshakeMetadata {
+ if (!this.lastHandshake)
+ throw new Error("QWP connection is not established");
+ return this.lastHandshake;
+ }
+
+ get endpoint(): string | URL | undefined {
+ return this.lastEndpoint;
+ }
+
+ send(payload: Uint8Array): Promise {
+ if (this.terminalError) return Promise.reject(this.terminalError);
+ if (this.closing) return Promise.reject(new QwpSendClosedError());
+ const copy = payload.slice();
+ const sending = this.sendTail.then(async () => {
+ this.throwIfUnavailable();
+ const connection = await this.requireConnection();
+ const prepared = await this.prepareOutboundQuery(copy);
+ this.trackOutbound(prepared);
+ try {
+ await connection.send(prepared);
+ } catch (error) {
+ await this.requestReconnect(error, connection);
+ }
+ });
+ this.sendTail = sending.catch(() => undefined);
+ return sending;
+ }
+
+ async close(code = 1000, reason = ""): Promise {
+ if (this.closing) {
+ await this.closed;
+ return;
+ }
+ this.closing = true;
+ this.cancelBackoff?.();
+ this.messagesQueue.end();
+ const connection = this.connection;
+ // Tears down a connect that is still negotiating. Without this the socket
+ // and its deadline outlive close(), keeping the event loop open for up to
+ // connectTimeoutMs/authTimeoutMs after close() has already resolved.
+ this.connectAbort?.abort();
+ const connectingCandidate = this.connectingCandidate;
+ this.connection = undefined;
+ this.connectingCandidate = undefined;
+ let closeInfo: QwpConnectionCloseInfo = {
+ code,
+ reason,
+ wasClean: code === 1000,
+ };
+ if (connection) {
+ try {
+ await connection.close(code, reason);
+ closeInfo = await connection.closed;
+ } catch {
+ // Preserve the requested close result when transport shutdown races.
+ }
+ }
+ if (connectingCandidate && connectingCandidate !== connection) {
+ await connectingCandidate.close(code, reason).catch(() => undefined);
+ }
+ this.settleClosed(closeInfo);
+ }
+
+ private async connectLoop(
+ initialCause: unknown,
+ reconnecting: boolean,
+ skipQueueBarrier = false,
+ ): Promise {
+ const outageStarted = Date.now();
+ const previousEndpoint = this.lastEndpoint;
+ let attempt = 0;
+ let backoffMs = this.initialBackoffMs;
+ let lastError = initialCause;
+ if (reconnecting) {
+ this.emitEvent({
+ kind: QWP_RECONNECT_EVENT_KIND.RECONNECTING,
+ attempt: 0,
+ previousEndpoint,
+ cause: initialCause,
+ });
+ if (backoffMs > 0) {
+ await this.waitForBackoff(jitterReconnectDelayMs(backoffMs));
+ backoffMs = Math.min(Math.max(backoffMs * 2, 1), this.maxBackoffMs);
+ }
+ }
+
+ while (!this.closing) {
+ if (attempt > 0 && backoffMs > 0) {
+ await this.waitForBackoff(jitterReconnectDelayMs(backoffMs));
+ backoffMs = Math.min(Math.max(backoffMs * 2, 1), this.maxBackoffMs);
+ }
+ this.throwIfUnavailable();
+ attempt++;
+ let candidate: QwpBinaryConnection | undefined;
+ try {
+ const abort = new AbortController();
+ this.connectAbort = abort;
+ try {
+ candidate = await this.factory(abort.signal);
+ } finally {
+ if (this.connectAbort === abort) this.connectAbort = undefined;
+ }
+ this.connectingCandidate = candidate;
+ if (this.closing) {
+ await candidate.close().catch(() => undefined);
+ throw new QwpSendClosedError();
+ }
+ const iterator = candidate.messages[Symbol.asyncIterator]();
+ const serverInfoPayload = await this.readServerInfo(
+ iterator,
+ candidate,
+ );
+ const serverInfo = decodeQwpEgressMessage(serverInfoPayload);
+ if (serverInfo.kind !== "server-info") {
+ throw new QwpProtocolError(
+ "QWP egress connection did not begin with SERVER_INFO",
+ );
+ }
+ if (reconnecting) {
+ this.validateServerInfo(serverInfo, candidate);
+ await this.replayInto(
+ candidate,
+ serverInfo,
+ previousEndpoint,
+ initialCause,
+ skipQueueBarrier,
+ );
+ } else {
+ this.initialServerInfo = serverInfo;
+ this.currentServerInfo = serverInfo;
+ this.messagesQueue.push(serverInfoPayload);
+ }
+ if (this.closing) throw new QwpSendClosedError();
+ this.currentServerInfo = serverInfo;
+ this.install(candidate, iterator);
+ this.connectingCandidate = undefined;
+ if (reconnecting) {
+ this.emitEvent({
+ kind:
+ previousEndpoint !== undefined &&
+ String(previousEndpoint) !== String(candidate.endpoint)
+ ? QWP_RECONNECT_EVENT_KIND.FAILED_OVER
+ : QWP_RECONNECT_EVENT_KIND.RECONNECTED,
+ attempt,
+ endpoint: candidate.endpoint,
+ previousEndpoint,
+ });
+ } else {
+ this.emitEvent({
+ kind: QWP_RECONNECT_EVENT_KIND.CONNECTED,
+ attempt: 0,
+ endpoint: candidate.endpoint,
+ });
+ }
+ return;
+ } catch (error) {
+ lastError = error;
+ if (this.connectingCandidate === candidate) {
+ this.connectingCandidate = undefined;
+ }
+ if (candidate) await candidate.close().catch(() => undefined);
+ this.emitEvent({
+ kind: QWP_RECONNECT_EVENT_KIND.ATTEMPT_FAILED,
+ attempt,
+ endpoint: candidate?.endpoint,
+ previousEndpoint,
+ cause: error,
+ });
+ if (!isRetryableReconnectError(error)) throw error;
+ if (!reconnecting && !this.retryInitialConnection) throw error;
+ const attemptsExhausted =
+ this.maxAttempts > 0 && attempt >= this.maxAttempts;
+ const durationExhausted =
+ this.maxDurationMs > 0 &&
+ Date.now() - outageStarted >= this.maxDurationMs;
+ if (attemptsExhausted || durationExhausted) {
+ throw new QwpReconnectExhaustedError(attempt, lastError);
+ }
+ }
+ }
+ throw new QwpSendClosedError();
+ }
+
+ private install(
+ connection: QwpBinaryConnection,
+ iterator: AsyncIterator,
+ ): void {
+ this.connection = connection;
+ this.lastHandshake = connection.handshake;
+ this.lastEndpoint = connection.endpoint;
+ const generation = ++this.generation;
+ void this.pump(connection, iterator, generation);
+ }
+
+ private async pump(
+ connection: QwpBinaryConnection,
+ iterator: AsyncIterator,
+ generation: number,
+ ): Promise {
+ try {
+ while (true) {
+ const next = await iterator.next();
+ if (next.done) break;
+ if (this.closing || this.connection !== connection) return;
+ const message = decodeQwpEgressMessage(next.value);
+ if (message.kind === "server-info") {
+ throw new QwpProtocolError("received duplicate QWP SERVER_INFO");
+ } else if (
+ message.kind === "result-end" ||
+ message.kind === "exec-done" ||
+ message.kind === "query-error"
+ ) {
+ const activeRequestId = replayRequestId(this.outboundReplay);
+ if (activeRequestId === message.requestId) this.outboundReplay = [];
+ }
+ this.messagesQueue.push(next.value);
+ }
+ if (this.closing || this.connection !== connection) return;
+ await this.requestReconnect(
+ new QwpSendClosedError(await connection.closed),
+ connection,
+ ).catch((reconnectError) => this.failTerminal(reconnectError));
+ return;
+ } catch (error) {
+ if (
+ this.closing ||
+ this.connection !== connection ||
+ generation !== this.generation
+ ) {
+ return;
+ }
+ await this.requestReconnect(
+ error,
+ connection,
+ error instanceof QwpProtocolError ? 1002 : 1000,
+ error instanceof QwpProtocolError ? "invalid QWP egress message" : "",
+ ).catch((reconnectError) => this.failTerminal(reconnectError));
+ }
+ }
+
+ private async readServerInfo(
+ iterator: AsyncIterator,
+ connection: QwpBinaryConnection,
+ ): Promise {
+ let timer: ReturnType | undefined;
+ const timeout = new Promise((_resolve, reject) => {
+ timer = setTimeout(
+ () =>
+ reject(new Error("timed out waiting for QWP reconnect SERVER_INFO")),
+ this.serverInfoTimeoutMs,
+ );
+ });
+ try {
+ const result = await Promise.race([iterator.next(), timeout]);
+ if (result.done) {
+ throw new QwpSendClosedError(await connection.closed);
+ }
+ return result.value;
+ } finally {
+ if (timer) clearTimeout(timer);
+ }
+ }
+
+ private validateServerInfo(
+ serverInfo: QwpServerInfoMessage,
+ connection: QwpBinaryConnection,
+ ): void {
+ const initial = this.initialServerInfo;
+ if (!initial) {
+ throw new ReplayStateError(
+ "QWP reconnect started before the initial SERVER_INFO was received",
+ );
+ }
+ if (
+ initial.clusterId &&
+ serverInfo.clusterId &&
+ initial.clusterId !== serverInfo.clusterId
+ ) {
+ throw new QwpUpgradeError(
+ `QWP reconnect target belongs to a different cluster [expected=${initial.clusterId}, actual=${serverInfo.clusterId}]`,
+ {
+ kind: QWP_UPGRADE_ERROR_KIND.CAPABILITY_MISMATCH,
+ retryable: true,
+ tryNextEndpoint: true,
+ url: connection.endpoint,
+ },
+ );
+ }
+ }
+
+ private async replayInto(
+ connection: QwpBinaryConnection,
+ serverInfo: QwpServerInfoMessage,
+ previousEndpoint: string | URL | undefined,
+ cause: unknown,
+ skipQueueBarrier: boolean,
+ ): Promise {
+ if (this.outboundReplay.length === 0) {
+ // A terminal response may already be queued. Let the bounded session
+ // consume it before resetting connection-scoped decoder state.
+ if (skipQueueBarrier) this.messagesQueue.clear();
+ else await this.messagesQueue.barrier();
+ await this.onConnectionReset(serverInfo);
+ return;
+ }
+ // An active operation will be replayed from its request. Drop raw stale
+ // messages before resetting the decoded queue; waiting for a barrier here
+ // can deadlock when that queue is deliberately at its client-side bound.
+ this.messagesQueue.clear();
+ await this.onConnectionReset(serverInfo);
+ const requestId = replayRequestId(this.outboundReplay);
+ if (requestId === undefined) {
+ throw new ReplayStateError(
+ "QWP egress replay is missing its QUERY_REQUEST",
+ );
+ }
+ if (this.onReplayReset) {
+ try {
+ await this.onReplayReset({
+ requestId,
+ serverInfo,
+ previousEndpoint,
+ endpoint: connection.endpoint,
+ cause,
+ });
+ } catch (error) {
+ throw new ReplayResetCallbackError(error);
+ }
+ }
+ const request = await this.encodeReplayRequest(serverInfo, requestId);
+ validateEncodedRequest(request, requestId);
+ const preparedRequest = request.slice();
+ this.outboundReplay[0] = preparedRequest;
+ for (const payload of this.outboundReplay) await connection.send(payload);
+ }
+
+ private async prepareOutboundQuery(payload: Uint8Array): Promise {
+ if (payload[0] !== QWP_EGRESS_MESSAGE.QUERY_REQUEST) return payload;
+ const requestId = replayRequestId([payload]);
+ const serverInfo = this.currentServerInfo;
+ if (requestId === undefined || !serverInfo) {
+ throw new QwpProtocolError(
+ "QWP QUERY_REQUEST cannot be prepared before SERVER_INFO",
+ );
+ }
+ const encoded = await this.encodeReplayRequest(serverInfo, requestId);
+ validateEncodedRequest(encoded, requestId);
+ return encoded.slice();
+ }
+
+ private async encodeReplayRequest(
+ serverInfo: QwpServerInfoMessage,
+ requestId: bigint,
+ ): Promise {
+ try {
+ return await this.encodeQueryRequest(serverInfo, requestId);
+ } catch (error) {
+ throw new ReplayStateError(
+ `QWP egress could not reconstruct active request ID ${requestId}`,
+ error,
+ );
+ }
+ }
+
+ /** @internal Replaces a connection whose server response was invalid. */
+ async recoverProtocolFailure(error: QwpProtocolError): Promise {
+ this.throwIfUnavailable();
+ const connection = this.connection;
+ if (!connection) throw new QwpSendClosedError();
+ // Reconnecting replays the same QUERY_REQUEST, so a response this client
+ // cannot decode reproduces on the replacement connection. Each connect
+ // SUCCEEDS, so connectLoop's own budget is never consumed and the retry
+ // would otherwise run forever, rotating the whole cluster. Charge these
+ // recoveries to the same maxAttempts/maxDurationMs budget instead, the way
+ // the Java client counts every re-submission of one execute() against
+ // failover_max_attempts and failover_max_duration.
+ if (this.protocolRecoveries === 0) {
+ this.protocolRecoveryStartedAt = Date.now();
+ }
+ this.protocolRecoveries++;
+ // `>` not `>=`: maxAttempts counts reconnects here, as it does in
+ // connectLoop, so maxAttempts=1 still permits one recovery.
+ const attemptsExhausted =
+ this.maxAttempts > 0 && this.protocolRecoveries > this.maxAttempts;
+ const durationExhausted =
+ this.maxDurationMs > 0 &&
+ Date.now() - this.protocolRecoveryStartedAt >= this.maxDurationMs;
+ if (attemptsExhausted || durationExhausted) {
+ const exhausted = new QwpReconnectExhaustedError(
+ this.protocolRecoveries,
+ error,
+ );
+ this.failTerminal(exhausted);
+ throw exhausted;
+ }
+ try {
+ await this.requestReconnect(
+ error,
+ connection,
+ 1002,
+ "invalid QWP egress message",
+ true,
+ );
+ } catch (reconnectError) {
+ this.failTerminal(reconnectError);
+ throw reconnectError;
+ }
+ }
+
+ private trackOutbound(payload: Uint8Array): void {
+ switch (payload[0]) {
+ case QWP_EGRESS_MESSAGE.QUERY_REQUEST:
+ this.outboundReplay = [payload];
+ // A new application query is fresh progress, matching the Java
+ // client's per-execute() scoping. Replay does not come through here,
+ // so a request that keeps poisoning still exhausts its budget.
+ this.protocolRecoveries = 0;
+ break;
+ case QWP_EGRESS_MESSAGE.CREDIT:
+ case QWP_EGRESS_MESSAGE.CANCEL:
+ if (this.outboundReplay.length > 0) this.outboundReplay.push(payload);
+ break;
+ }
+ }
+
+ private async requireConnection(): Promise {
+ if (this.reconnectTask) await this.reconnectTask;
+ this.throwIfUnavailable();
+ if (!this.connection) throw new QwpSendClosedError();
+ return this.connection;
+ }
+
+ private async requestReconnect(
+ cause: unknown,
+ failedConnection: QwpBinaryConnection,
+ closeCode = 1000,
+ closeReason = "",
+ skipQueueBarrier = false,
+ ): Promise {
+ if (this.closing) throw new QwpSendClosedError();
+ if (this.connection && this.connection !== failedConnection) return;
+ if (this.reconnectTask) {
+ const activeReconnect = this.reconnectTask;
+ await activeReconnect;
+ if (this.connection === failedConnection && !this.closing) {
+ await this.requestReconnect(
+ cause,
+ failedConnection,
+ closeCode,
+ closeReason,
+ skipQueueBarrier,
+ );
+ }
+ return;
+ }
+
+ this.connection = undefined;
+ if (closeCode !== 1000) failedConnection.deprioritizeEndpoint?.();
+ void failedConnection.close(closeCode, closeReason).catch(() => undefined);
+ const reconnecting = this.connectLoop(cause, true, skipQueueBarrier);
+ this.reconnectTask = reconnecting;
+ try {
+ await reconnecting;
+ } finally {
+ if (this.reconnectTask === reconnecting) this.reconnectTask = undefined;
+ }
+ }
+
+ private async waitForBackoff(delayMs: number): Promise {
+ await new Promise((resolve) => {
+ const timer = setTimeout(() => {
+ if (this.cancelBackoff === cancel) this.cancelBackoff = undefined;
+ resolve();
+ }, delayMs);
+ const cancel = (): void => {
+ clearTimeout(timer);
+ if (this.cancelBackoff === cancel) this.cancelBackoff = undefined;
+ resolve();
+ };
+ this.cancelBackoff = cancel;
+ });
+ }
+
+ private emitEvent(event: Omit): void {
+ // Contain synchronous throws and rejected promises alike: a failing
+ // observer, sync or async, must never interfere with replay progress.
+ safelyInvoke(this.reconnectOptions.onEvent, {
+ ...event,
+ timestampMs: Date.now(),
+ });
+ }
+
+ private throwIfUnavailable(): void {
+ if (this.terminalError) throw this.terminalError;
+ if (this.closing) throw new QwpSendClosedError();
+ }
+
+ private failTerminal(error: unknown): void {
+ if (this.terminalError) return;
+ this.terminalError =
+ error instanceof Error
+ ? error
+ : new Error(`QWP reconnect failed: ${error}`);
+ this.cancelBackoff?.();
+ this.messagesQueue.fail(this.terminalError);
+ this.settleClosed({
+ code: 1011,
+ reason: this.terminalError.message,
+ wasClean: false,
+ });
+ void this.connection
+ ?.close(1011, "QWP reconnect failed")
+ .catch(() => undefined);
+ }
+
+ private settleClosed(info: QwpConnectionCloseInfo): void {
+ if (this.closedSettled) return;
+ this.closedSettled = true;
+ this.resolveClosed(info);
+ }
+}
+
+function replayRequestId(payloads: readonly Uint8Array[]): bigint | undefined {
+ const query = payloads.find(
+ (payload) => payload[0] === QWP_EGRESS_MESSAGE.QUERY_REQUEST,
+ );
+ if (!query || query.byteLength < 9) return undefined;
+ return new DataView(
+ query.buffer,
+ query.byteOffset,
+ query.byteLength,
+ ).getBigUint64(1, true);
+}
+
+function validateEncodedRequest(
+ payload: Uint8Array,
+ expectedRequestId: bigint,
+): void {
+ const requestId = replayRequestId([payload]);
+ if (requestId !== expectedRequestId) {
+ throw new ReplayStateError(
+ `QWP query encoder returned the wrong request [expected=${expectedRequestId}, actual=${requestId ?? "missing"}]`,
+ );
+ }
+}
+
+function validateReconnectPolicy(
+ maxAttempts: number,
+ initialBackoffMs: number,
+ maxBackoffMs: number,
+ maxDurationMs: number,
+): void {
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 0) {
+ throw new RangeError(
+ "reconnect maxAttempts must be a non-negative safe integer",
+ );
+ }
+ for (const [name, value] of [
+ ["initialBackoffMs", initialBackoffMs],
+ ["maxBackoffMs", maxBackoffMs],
+ ["maxDurationMs", maxDurationMs],
+ ] as const) {
+ if (!Number.isFinite(value) || value < 0) {
+ throw new RangeError(
+ `reconnect ${name} must be a non-negative finite number`,
+ );
+ }
+ }
+ if (maxBackoffMs < initialBackoffMs) {
+ throw new RangeError(
+ "reconnect maxBackoffMs must be greater than or equal to initialBackoffMs",
+ );
+ }
+}
+
+function isRetryableReconnectError(error: unknown): boolean {
+ if (error instanceof QwpUpgradeError) return error.retryable !== false;
+ if (error instanceof QwpFailoverError) {
+ return error.attempts.some((attempt) =>
+ isRetryableReconnectError(attempt.error),
+ );
+ }
+ return !(
+ error instanceof ReplayStateError ||
+ error instanceof ReplayResetCallbackError
+ );
+}
diff --git a/packages/client-core/src/_qwp/_internal/reconnecting-ingress-connection.ts b/packages/client-core/src/_qwp/_internal/reconnecting-ingress-connection.ts
new file mode 100644
index 0000000..822a60d
--- /dev/null
+++ b/packages/client-core/src/_qwp/_internal/reconnecting-ingress-connection.ts
@@ -0,0 +1,2370 @@
+import {
+ decodeQwpFrame,
+ decodeQwpIngressResponse,
+ decodeQwpIngressSymbolDictionaryDelta,
+ encodeQwpIngressSymbolDictionaryFrame,
+ QWP_FLAG_DEFER_COMMIT,
+ QWP_FLAG_DELTA_SYMBOL_DICTIONARY,
+ QWP_FLAG_DURABLE_ACK_POLL,
+ QWP_HEADER_SIZE,
+ QWP_STATUS,
+ QwpProtocolError,
+ qwpVarintSize,
+ utf8Length,
+} from "../_core";
+import {
+ QWP_INITIAL_CONNECT_MODE,
+ QWP_RECONNECT_EVENT_KIND,
+ QWP_UPGRADE_ERROR_KIND,
+ QwpBinaryConnection,
+ QwpConnectionCloseInfo,
+ QwpConnectionFactory,
+ QwpDurableAckUnavailableError,
+ QwpFailoverError,
+ QwpHandshakeMetadata,
+ QwpIngressReplayRecord,
+ QwpIngressReplayReference,
+ QwpIngressReplayStore,
+ QwpIngressTransportMetrics,
+ QwpInitialConnectMode,
+ QwpMemoryReplayAppendTimeoutError,
+ QwpMemoryReplayFrameTooLargeError,
+ QwpReconnectEvent,
+ QwpReconnectExhaustedError,
+ QwpReconnectOptions,
+ QwpReplayDictionaryError,
+ QwpReplayDictionaryPersistenceError,
+ QwpReplayRejectedError,
+ QwpSendClosedError,
+ QwpUnrecoverableReplayDictionaryError,
+ QwpUpgradeError,
+} from "../transport";
+import { QwpAsyncQueue } from "./async-queue";
+import { jitterReconnectDelayMs } from "./reconnect-backoff";
+import { QwpNotificationDispatcher } from "./notification-dispatcher";
+import {
+ createQwpProtocolViolationSenderError,
+ createQwpSenderError,
+ defaultQwpSenderErrorHandler,
+ qwpSenderErrorCategory,
+ QWP_SENDER_ERROR_CATEGORY,
+ type QwpSenderError,
+} from "../sender-error";
+
+const DEFAULT_CATCH_UP_CAP_GAP_MIN_ESCALATION_WINDOW_MS = 300_000;
+const MAX_CATCH_UP_CAP_GAP_ATTEMPTS = 16;
+const DEFAULT_ORPHAN_DURABLE_ACK_MISMATCH_MAX_DURATION_MS = 300_000;
+const MAX_ORPHAN_DURABLE_ACK_MISMATCH_ATTEMPTS = 16;
+const DEFAULT_MEMORY_REPLAY_MAX_BYTES = 128 * 1024 * 1024;
+const DEFAULT_MEMORY_REPLAY_APPEND_DEADLINE_MS = 30_000;
+// Charge a conservative fixed amount so even empty/very small opaque frames
+// cannot grow the replay Map without bound. Payload arrays are not copied by
+// the store, so the configured budget primarily tracks live frame storage.
+const MEMORY_REPLAY_RECORD_OVERHEAD_BYTES = 64;
+
+type ConnectAttemptPolicy = "single" | "configured" | "unbounded";
+
+export class QwpCatchUpCapGapError extends RangeError {
+ constructor(
+ readonly symbolId: number,
+ readonly frameLength: number,
+ readonly maxBatchSizeBytes: number,
+ details?: {
+ attempt: number;
+ episodeMs: number;
+ minEscalationWindowMs: number;
+ exhausted: boolean;
+ },
+ ) {
+ super(
+ `symbol dictionary entry exceeds reconnect target batch cap [id=${symbolId}, frameLength=${frameLength}, max=${maxBatchSizeBytes}` +
+ (details
+ ? `, attempt=${details.attempt}/${MAX_CATCH_UP_CAP_GAP_ATTEMPTS}, episodeMs=${details.episodeMs}/${details.minEscalationWindowMs}]${
+ details.exhausted
+ ? "; the data must be resent after the cap is raised"
+ : "; retrying because a larger-cap node may return"
+ }`
+ : "]"),
+ );
+ this.name = "QwpCatchUpCapGapError";
+ }
+}
+
+export class QwpDurableAckPersistentFailureError extends Error {
+ constructor(
+ readonly attempts: number,
+ readonly episodeMs: number,
+ readonly cause: QwpDurableAckUnavailableError,
+ ) {
+ super(
+ `QWP durable ACK remained unavailable for an orphan replay slot [attempts=${attempts}/${MAX_ORPHAN_DURABLE_ACK_MISMATCH_ATTEMPTS}, episodeMs=${episodeMs}]: ${cause.message}`,
+ );
+ this.name = "QwpDurableAckPersistentFailureError";
+ }
+}
+
+interface ReplayFrame extends Omit {
+ // Assigned inside send()'s serialized tail, immediately before the journal
+ // append, so a frame that never reaches the store consumes no sequence.
+ frameSequence: bigint;
+ payload?: Uint8Array;
+ readonly clientSequence?: bigint;
+ ackDelivered: boolean;
+ transmitted: boolean;
+ durableTargets?: Map;
+ dictionaryCatchup?: boolean;
+}
+
+type LoadedReplayRecord = QwpIngressReplayReference & {
+ readonly payload?: Uint8Array;
+};
+
+type LazyReplayStore = QwpIngressReplayStore &
+ Required>;
+
+interface RecoveredDiscardTail {
+ readonly startSequence: bigint;
+ readonly tipSequence: bigint;
+ readonly predecessorSequence?: bigint;
+}
+
+class RetriableIngressNackError extends Error {
+ constructor(
+ readonly frameSequence: bigint,
+ readonly status: number,
+ readonly retryDelayMs: number,
+ message?: string,
+ ) {
+ super(
+ `QuestDB temporarily rejected QWP frame [frameSequence=${frameSequence}, status=0x${status.toString(16)}]${
+ message ? `: ${message}` : ""
+ }`,
+ );
+ this.name = "RetriableIngressNackError";
+ }
+}
+
+class RetriableIngressConnectionError extends Error {
+ readonly cause: unknown;
+
+ constructor(
+ readonly retryDelayMs: number,
+ cause: unknown,
+ ) {
+ super(
+ cause instanceof Error
+ ? cause.message
+ : `QWP ingress connection was lost: ${cause}`,
+ );
+ this.name = "RetriableIngressConnectionError";
+ this.cause = cause;
+ }
+}
+
+class QwpMemoryReplayStore implements QwpIngressReplayStore {
+ private readonly records = new Map();
+ private readonly symbols: string[] = [];
+ private readonly capacityWaiters = new Set<{
+ resolve: () => void;
+ reject: (error: Error) => void;
+ timer: ReturnType;
+ }>();
+ private usedBytes = 0;
+ private closing = false;
+ private totalBackpressureStalls = 0;
+ private totalAppendTimeouts = 0;
+
+ constructor(
+ readonly maxBytes = DEFAULT_MEMORY_REPLAY_MAX_BYTES,
+ private readonly appendDeadlineMs = DEFAULT_MEMORY_REPLAY_APPEND_DEADLINE_MS,
+ ) {}
+
+ get metrics() {
+ return {
+ maxBytes: this.maxBytes,
+ usedBytes: this.usedBytes,
+ waitingAppends: this.capacityWaiters.size,
+ totalBackpressureStalls: this.totalBackpressureStalls,
+ totalAppendTimeouts: this.totalAppendTimeouts,
+ } as const;
+ }
+
+ async load(): Promise {
+ return Array.from(this.records, ([frameSequence, payload]) => ({
+ frameSequence,
+ payload: payload.slice(),
+ }));
+ }
+
+ async append(record: QwpIngressReplayRecord): Promise {
+ if (this.closing) throw new QwpSendClosedError();
+ if (this.records.has(record.frameSequence)) {
+ throw new Error(
+ `QWP memory replay sequence already exists [frameSequence=${record.frameSequence}]`,
+ );
+ }
+ const requiredBytes =
+ record.payload.byteLength + MEMORY_REPLAY_RECORD_OVERHEAD_BYTES;
+ if (requiredBytes > this.maxBytes) {
+ throw new QwpMemoryReplayFrameTooLargeError(
+ this.maxBytes,
+ record.payload.byteLength,
+ requiredBytes,
+ );
+ }
+ if (this.usedBytes + requiredBytes > this.maxBytes) {
+ this.totalBackpressureStalls++;
+ const deadline = Date.now() + this.appendDeadlineMs;
+ while (this.usedBytes + requiredBytes > this.maxBytes) {
+ const remainingMs = deadline - Date.now();
+ if (remainingMs <= 0) {
+ this.totalAppendTimeouts++;
+ throw new QwpMemoryReplayAppendTimeoutError(
+ this.maxBytes,
+ this.usedBytes,
+ requiredBytes,
+ this.appendDeadlineMs,
+ );
+ }
+ await this.waitForCapacity(remainingMs, requiredBytes);
+ if (this.closing) throw new QwpSendClosedError();
+ }
+ }
+ // send() already made the replay-owned payload copy. Sharing it between
+ // the connection and this accounting store avoids doubling the backlog.
+ this.records.set(record.frameSequence, record.payload);
+ this.usedBytes += requiredBytes;
+ }
+
+ async acknowledgeThrough(frameSequence: bigint): Promise {
+ for (const sequence of this.records.keys()) {
+ if (sequence > frameSequence) break;
+ const payload = this.records.get(sequence)!;
+ this.usedBytes -=
+ payload.byteLength + MEMORY_REPLAY_RECORD_OVERHEAD_BYTES;
+ this.records.delete(sequence);
+ }
+ this.releaseCapacityWaiters();
+ }
+
+ async loadSymbolDictionary(): Promise {
+ return this.symbols.slice();
+ }
+
+ async appendSymbolDictionary(
+ startId: number,
+ entries: readonly string[],
+ ): Promise {
+ if (startId !== this.symbols.length) {
+ throw new QwpReplayDictionaryError(
+ `memory replay dictionary is not dense [expected=${this.symbols.length}, received=${startId}]`,
+ );
+ }
+ this.symbols.push(...entries);
+ }
+
+ async close(): Promise {
+ if (this.closing) return;
+ this.closing = true;
+ const error = new QwpSendClosedError();
+ for (const waiter of this.capacityWaiters) {
+ clearTimeout(waiter.timer);
+ waiter.reject(error);
+ }
+ this.capacityWaiters.clear();
+ this.records.clear();
+ this.symbols.length = 0;
+ this.usedBytes = 0;
+ }
+
+ private waitForCapacity(
+ timeoutMs: number,
+ requiredBytes: number,
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ const waiter = {
+ resolve: () => {
+ clearTimeout(waiter.timer);
+ this.capacityWaiters.delete(waiter);
+ resolve();
+ },
+ reject: (error: Error) => {
+ clearTimeout(waiter.timer);
+ this.capacityWaiters.delete(waiter);
+ reject(error);
+ },
+ timer: undefined as unknown as ReturnType,
+ };
+ waiter.timer = setTimeout(() => {
+ this.totalAppendTimeouts++;
+ waiter.reject(
+ new QwpMemoryReplayAppendTimeoutError(
+ this.maxBytes,
+ this.usedBytes,
+ requiredBytes,
+ this.appendDeadlineMs,
+ ),
+ );
+ }, timeoutMs);
+ this.capacityWaiters.add(waiter);
+ });
+ }
+
+ private releaseCapacityWaiters(): void {
+ for (const waiter of [...this.capacityWaiters]) waiter.resolve();
+ }
+}
+
+/**
+ * Reconnects an ingress wire and translates its per-connection ACK sequence
+ * back to stable replay records. Replay is deliberately at-least-once: a frame
+ * accepted by the server whose ACK was lost may be sent again.
+ */
+export class QwpReconnectingIngressConnection implements QwpBinaryConnection {
+ private readonly messagesQueue = new QwpAsyncQueue();
+ private readonly frames = new Map();
+ private readonly durableWatermarks = new Map();
+ private readonly symbolDictionary: string[];
+ private readonly store: QwpIngressReplayStore;
+ private readonly lazyReplayStore?: LazyReplayStore;
+ private readonly maxAttempts: number;
+ private readonly initialBackoffMs: number;
+ private readonly maxBackoffMs: number;
+ private readonly maxDurationMs: number;
+ private readonly maxFrameRejections: number;
+ private readonly poisonMinEscalationWindowMs: number;
+ private readonly catchUpCapGapMinEscalationWindowMs: number;
+ private readonly orphanDurableAckMismatchMaxDurationMs: number;
+ private readonly localMaxBatchSizeBytes?: number;
+ private readonly connectionDispatcher?: QwpNotificationDispatcher;
+ private readonly errorDispatcher?: QwpNotificationDispatcher;
+ private readonly resolveClosed: (info: QwpConnectionCloseInfo) => void;
+ private connection?: QwpBinaryConnection;
+ private connectingCandidate?: QwpBinaryConnection;
+ private connectAbort?: AbortController;
+ private lastHandshake?: QwpHandshakeMetadata;
+ private lastEndpoint?: string | URL;
+ // Wire log for the current connection, indexed by wire sequence minus
+ // wireFramesBase. Acknowledged frames are dropped and the base advances, so
+ // the log stays proportional to what is still unacknowledged rather than to
+ // everything ever sent on the connection.
+ private wireFrames: ReplayFrame[] = [];
+ private wireFramesBase = 0;
+ private nextFrameSequence = 0n;
+ private nextClientSequence = 0n;
+ private publishedFrameSequence = -1n;
+ private acknowledgedFrameSequence = -1n;
+ private highestOkFrameSequence = -1n;
+ private poisonFrameSequence?: bigint;
+ private poisonFirstStrikeMs = 0;
+ private poisonStrikes = 0;
+ /** Elapsed connection-outage time withheld from the escalation window. */
+ private poisonOutageMs = 0;
+ private poisonOutageStartedMs = 0;
+ private catchUpCapGapAttempts = 0;
+ private catchUpCapGapFirstMs = 0;
+ private durableAckMismatchAttempts = 0;
+ private durableAckMismatchFirstMs = 0;
+ private progressAtLastExemptRecycle = -1n;
+ private zeroProgressRecycles = 0;
+ private recoveredDiscardTail?: RecoveredDiscardTail;
+ private generation = 0;
+ private sendTail: Promise = Promise.resolve();
+ private drainTail: Promise = Promise.resolve();
+ private reconnectTask?: Promise;
+ private storeClosePromise?: Promise;
+ private terminalError?: Error;
+ private cancelBackoff?: () => void;
+ private closing = false;
+ private closedSettled = false;
+ private totalFramesSent = 0;
+ private totalBytesSent = 0;
+ private totalFramesReplayed = 0;
+ private totalBytesReplayed = 0;
+ private totalReconnectAttempts = 0;
+ private totalReconnectsSucceeded = 0;
+ private totalFailovers = 0;
+ private totalReconnectErrors = 0;
+ private totalServerNacks = 0;
+ private hasEverConnected = false;
+ private deltaSymbolDictionaryEnabled: boolean;
+ readonly messages: AsyncIterable = this.messagesQueue;
+ readonly closed: Promise;
+ readonly managesIngressSenderErrors = true;
+ ping?: () => Promise;
+
+ private constructor(
+ private readonly factory: QwpConnectionFactory,
+ private readonly reconnectOptions: QwpReconnectOptions,
+ store: QwpIngressReplayStore,
+ records: readonly LoadedReplayRecord[],
+ symbolDictionary: readonly string[],
+ recoveredDiscardTail: RecoveredDiscardTail | undefined,
+ localMaxBatchSizeBytes?: number,
+ private readonly backgroundStoreAndForward = false,
+ private readonly orphanStoreAndForward = false,
+ orphanDurableAckMismatchMaxDurationMs = DEFAULT_ORPHAN_DURABLE_ACK_MISMATCH_MAX_DURATION_MS,
+ catchUpCapGapMinEscalationWindowMs = DEFAULT_CATCH_UP_CAP_GAP_MIN_ESCALATION_WINDOW_MS,
+ connectionListenerInboxCapacity = 64,
+ errorInboxCapacity = 256,
+ onSenderError?: (error: QwpSenderError) => void,
+ ) {
+ this.store = store;
+ this.lazyReplayStore = isLazyReplayStore(store) ? store : undefined;
+ this.symbolDictionary = [...symbolDictionary];
+ this.deltaSymbolDictionaryEnabled =
+ store.loadSymbolDictionary !== undefined &&
+ store.appendSymbolDictionary !== undefined;
+ this.recoveredDiscardTail = recoveredDiscardTail;
+ this.localMaxBatchSizeBytes = localMaxBatchSizeBytes;
+ this.maxAttempts = reconnectOptions.maxAttempts ?? 3;
+ this.initialBackoffMs = reconnectOptions.initialBackoffMs ?? 100;
+ this.maxBackoffMs = reconnectOptions.maxBackoffMs ?? 5_000;
+ this.maxDurationMs = reconnectOptions.maxDurationMs ?? 30_000;
+ this.maxFrameRejections = reconnectOptions.maxFrameRejections ?? 4;
+ // WRITE_ERROR and INTERNAL_ERROR are RETRIABLE by policy, but the only
+ // thing separating "this frame is poison" from "the server cannot write
+ // right now" is how long the rejection persists. Five seconds did not
+ // separate them at all: a concurrent DDL, a checkpoint or a briefly full
+ // server volume outlives it easily, and with the reconnect backoff capped
+ // at maxBackoffMs four strikes accumulate well inside that window -- so a
+ // transient server-side fault permanently killed a running producer.
+ this.poisonMinEscalationWindowMs =
+ reconnectOptions.poisonMinEscalationWindowMs ?? 300_000;
+ this.catchUpCapGapMinEscalationWindowMs =
+ catchUpCapGapMinEscalationWindowMs;
+ this.orphanDurableAckMismatchMaxDurationMs =
+ orphanDurableAckMismatchMaxDurationMs;
+ if (reconnectOptions.onEvent) {
+ this.connectionDispatcher = new QwpNotificationDispatcher(
+ reconnectOptions.onEvent,
+ connectionListenerInboxCapacity,
+ );
+ }
+ this.errorDispatcher = new QwpNotificationDispatcher(
+ onSenderError ?? defaultQwpSenderErrorHandler,
+ errorInboxCapacity,
+ );
+ validateReconnectPolicy(
+ this.maxAttempts,
+ this.initialBackoffMs,
+ this.maxBackoffMs,
+ this.maxDurationMs,
+ this.maxFrameRejections,
+ this.poisonMinEscalationWindowMs,
+ this.catchUpCapGapMinEscalationWindowMs,
+ );
+ let resolveClosed!: (info: QwpConnectionCloseInfo) => void;
+ this.closed = new Promise((resolve) => {
+ resolveClosed = resolve;
+ });
+ this.resolveClosed = resolveClosed;
+
+ let previous = -1n;
+ for (const record of records) {
+ if (record.frameSequence < 0n || record.frameSequence <= previous) {
+ throw new Error(
+ "QWP replay store records must have strictly increasing non-negative sequences",
+ );
+ }
+ if (
+ !Number.isSafeInteger(record.payloadLength) ||
+ record.payloadLength < 0 ||
+ (record.payload !== undefined &&
+ record.payload.byteLength !== record.payloadLength)
+ ) {
+ throw new Error(
+ `QWP replay store returned an invalid payload length [frameSequence=${record.frameSequence}, payloadLength=${record.payloadLength}]`,
+ );
+ }
+ const frame: ReplayFrame = {
+ frameSequence: record.frameSequence,
+ payloadLength: record.payloadLength,
+ payload: record.payload?.slice(),
+ ackDelivered: true,
+ transmitted: true,
+ };
+ this.frames.set(frame.frameSequence, frame);
+ previous = frame.frameSequence;
+ }
+ if (records.length > 0) {
+ this.acknowledgedFrameSequence = records[0].frameSequence - 1n;
+ }
+ this.nextFrameSequence = previous + 1n;
+ this.publishedFrameSequence = previous;
+ }
+
+ static async connect(
+ factory: QwpConnectionFactory,
+ reconnectOptions: QwpReconnectOptions,
+ replayStore?: QwpIngressReplayStore,
+ localMaxBatchSizeBytes?: number,
+ memoryReplayMaxBytes = DEFAULT_MEMORY_REPLAY_MAX_BYTES,
+ memoryReplayAppendDeadlineMs = DEFAULT_MEMORY_REPLAY_APPEND_DEADLINE_MS,
+ backgroundStoreAndForward = false,
+ initialConnectMode: QwpInitialConnectMode = backgroundStoreAndForward
+ ? QWP_INITIAL_CONNECT_MODE.ASYNC
+ : QWP_INITIAL_CONNECT_MODE.SYNC,
+ orphanStoreAndForward = false,
+ orphanDurableAckMismatchMaxDurationMs = DEFAULT_ORPHAN_DURABLE_ACK_MISMATCH_MAX_DURATION_MS,
+ catchUpCapGapMinEscalationWindowMs = DEFAULT_CATCH_UP_CAP_GAP_MIN_ESCALATION_WINDOW_MS,
+ initialConnection?: Promise,
+ connectionListenerInboxCapacity = 64,
+ errorInboxCapacity = 256,
+ onSenderError?: (error: QwpSenderError) => void,
+ signal?: AbortSignal,
+ ): Promise {
+ const store: QwpIngressReplayStore =
+ replayStore ??
+ new QwpMemoryReplayStore(
+ memoryReplayMaxBytes,
+ memoryReplayAppendDeadlineMs,
+ );
+ let connection: QwpReconnectingIngressConnection | undefined;
+ // close() aborts this while a connect is still negotiating. Without it the
+ // caller returns from close() and this keeps going: a persistent store
+ // takes its slot lock after the sender is gone and holds it for the rest
+ // of the connect budget, and the abandoned session goes on to send frames
+ // and even quarantine directories.
+ const abortError = () =>
+ signal?.reason ?? new Error("QWP connect was aborted");
+ if (signal?.aborted) throw abortError();
+ try {
+ const lazyStore = isLazyReplayStore(store) ? store : undefined;
+ const records: readonly LoadedReplayRecord[] = lazyStore
+ ? await lazyStore.loadReferences()
+ : (await store.load()).map((record) => ({
+ ...record,
+ payloadLength: record.payload.byteLength,
+ }));
+ const sortedRecords = [...records].sort((a, b) =>
+ a.frameSequence < b.frameSequence
+ ? -1
+ : a.frameSequence > b.frameSequence
+ ? 1
+ : 0,
+ );
+ let persistedSymbolDictionary: readonly string[] = [];
+ let persistedSymbolDictionaryFailure: unknown;
+ if (store.loadSymbolDictionary) {
+ try {
+ persistedSymbolDictionary = await store.loadSymbolDictionary();
+ } catch (error) {
+ if (!store.replaceSymbolDictionary) throw error;
+ persistedSymbolDictionaryFailure = error;
+ }
+ }
+ const loadPayload = (record: LoadedReplayRecord) =>
+ record.payload
+ ? Promise.resolve(record.payload)
+ : lazyStore!.readPayload(record.frameSequence);
+ const recoveredDiscardTail = await analyzeRecoveredDiscardTail(
+ sortedRecords,
+ loadPayload,
+ );
+ const symbolDictionary = await recoverSymbolDictionary(
+ sortedRecords,
+ loadPayload,
+ persistedSymbolDictionary,
+ recoveredDiscardTail,
+ store,
+ persistedSymbolDictionaryFailure,
+ );
+ connection = new QwpReconnectingIngressConnection(
+ factory,
+ reconnectOptions,
+ store,
+ sortedRecords,
+ symbolDictionary,
+ recoveredDiscardTail,
+ localMaxBatchSizeBytes,
+ backgroundStoreAndForward,
+ orphanStoreAndForward,
+ orphanDurableAckMismatchMaxDurationMs,
+ catchUpCapGapMinEscalationWindowMs,
+ connectionListenerInboxCapacity,
+ errorInboxCapacity,
+ onSenderError,
+ );
+ // The store's lock is held from here on, so an abort has something to
+ // release and must reach the connect that is about to run.
+ if (signal?.aborted) throw abortError();
+ const onAbort = () => {
+ void connection?.close().catch(() => undefined);
+ };
+ signal?.addEventListener("abort", onAbort, { once: true });
+ try {
+ await connection.retireRecoveredDiscardTailIfReady();
+ if (
+ backgroundStoreAndForward &&
+ initialConnectMode === QWP_INITIAL_CONNECT_MODE.ASYNC
+ ) {
+ connection.startBackgroundConnect();
+ } else {
+ await connection.connectLoopOrCatchUp(
+ initialConnectMode,
+ initialConnection,
+ backgroundStoreAndForward,
+ orphanStoreAndForward,
+ );
+ }
+ } finally {
+ signal?.removeEventListener("abort", onAbort);
+ }
+ if (signal?.aborted) throw abortError();
+ return connection;
+ } catch (error) {
+ await connection?.close().catch(() => undefined);
+ if (!connection) {
+ const opened = await initialConnection?.catch(() => undefined);
+ await opened?.close().catch(() => undefined);
+ await store.close().catch(() => undefined);
+ }
+ throw error;
+ }
+ }
+
+ /** The foreground connect, with Java's catch-up fallback around it. */
+ private async connectLoopOrCatchUp(
+ initialConnectMode: QwpInitialConnectMode,
+ initialConnection: Promise | undefined,
+ backgroundStoreAndForward: boolean,
+ orphanStoreAndForward: boolean,
+ ): Promise {
+ try {
+ await this.connectLoop(
+ undefined,
+ false,
+ initialConnectMode === QWP_INITIAL_CONNECT_MODE.OFF
+ ? "single"
+ : "configured",
+ initialConnection,
+ );
+ } catch (error) {
+ if (
+ backgroundStoreAndForward &&
+ !orphanStoreAndForward &&
+ error instanceof QwpCatchUpCapGapError
+ ) {
+ // Java returns the foreground sender once the wire has connected,
+ // then moves recovered-dictionary catch-up to its unbounded I/O
+ // loop. Do the same instead of making OFF/SYNC construction wait
+ // forever for a larger-cap node.
+ this.startBackgroundConnect();
+ } else {
+ throw error;
+ }
+ }
+ }
+
+ get handshake(): QwpHandshakeMetadata {
+ if (!this.lastHandshake) {
+ if (this.backgroundStoreAndForward) return { qwpVersion: 1 };
+ throw new Error("QWP connection is not established");
+ }
+ return this.lastHandshake;
+ }
+
+ get endpoint(): string | URL | undefined {
+ return this.lastEndpoint;
+ }
+
+ get ingressSymbolDictionary(): readonly string[] {
+ return this.symbolDictionary.slice();
+ }
+
+ get ingressDeltaSymbolDictionaryEnabled(): boolean {
+ return this.deltaSymbolDictionaryEnabled;
+ }
+
+ getIngressMetrics(): QwpIngressTransportMetrics {
+ let pendingReplayBytes = 0;
+ for (const frame of this.frames.values()) {
+ pendingReplayBytes += frame.payloadLength;
+ }
+ const memoryMetrics =
+ this.store instanceof QwpMemoryReplayStore
+ ? this.store.metrics
+ : undefined;
+ return Object.freeze({
+ publishedFrameSequence: this.publishedFrameSequence,
+ acknowledgedFrameSequence: this.acknowledgedFrameSequence,
+ pendingReplayFrames: this.frames.size,
+ pendingReplayBytes,
+ memoryReplayMaxBytes: memoryMetrics?.maxBytes,
+ memoryReplayUsedBytes: memoryMetrics?.usedBytes,
+ waitingMemoryReplayAppends: memoryMetrics?.waitingAppends ?? 0,
+ totalMemoryReplayBackpressureStalls:
+ memoryMetrics?.totalBackpressureStalls ?? 0,
+ totalMemoryReplayAppendTimeouts: memoryMetrics?.totalAppendTimeouts ?? 0,
+ totalFramesSent: this.totalFramesSent,
+ totalBytesSent: this.totalBytesSent,
+ totalFramesReplayed: this.totalFramesReplayed,
+ totalBytesReplayed: this.totalBytesReplayed,
+ totalReconnectAttempts: this.totalReconnectAttempts,
+ totalReconnectsSucceeded: this.totalReconnectsSucceeded,
+ totalFailovers: this.totalFailovers,
+ totalReconnectErrors: this.totalReconnectErrors,
+ totalServerNacks: this.totalServerNacks,
+ deliveredConnectionNotifications:
+ this.connectionDispatcher?.metrics.delivered ?? 0,
+ droppedConnectionNotifications:
+ this.connectionDispatcher?.metrics.dropped ?? 0,
+ deliveredErrorNotifications: this.errorDispatcher?.metrics.delivered ?? 0,
+ droppedErrorNotifications: this.errorDispatcher?.metrics.dropped ?? 0,
+ });
+ }
+
+ getIngressFrameSequence(clientSequence: bigint): bigint | undefined {
+ for (const frame of this.frames.values()) {
+ if (frame.clientSequence === clientSequence) return frame.frameSequence;
+ }
+ return undefined;
+ }
+
+ skipIngressClientSequence(): void {
+ // Only the client sequence is reserved. The skipped frame never reaches
+ // the journal, so consuming a frame sequence here would leave a hole that
+ // makes every later append non-contiguous.
+ this.nextClientSequence++;
+ }
+
+ send(payload: Uint8Array): Promise {
+ if (this.terminalError) return Promise.reject(this.terminalError);
+ if (this.closing) return Promise.reject(new QwpSendClosedError());
+ const frame: ReplayFrame = {
+ // Placeholder; the real sequence is allocated in the tail below, once
+ // the journal has accepted the frame.
+ frameSequence: -1n,
+ clientSequence: this.nextClientSequence++,
+ payload: payload.slice(),
+ payloadLength: payload.byteLength,
+ ackDelivered: false,
+ transmitted: false,
+ };
+ const publishing = this.sendTail.then(async () => {
+ this.throwIfUnavailable();
+ const delta = readSymbolDictionaryDelta(frame.payload!);
+ if (delta) {
+ if (!this.deltaSymbolDictionaryEnabled) {
+ throw new QwpReplayDictionaryError(
+ "QWP delta symbol dictionaries are disabled because replay dictionary persistence is unavailable; encode symbols with full inline dictionaries",
+ );
+ }
+ await this.persistSymbolDictionaryDelta(delta);
+ }
+ // Sends are serialized on sendTail, so allocating here rather than at
+ // call time keeps frame sequences dense and in append order. A rejected
+ // append -- an exhausted journal, a missed append deadline -- must not
+ // consume one: the store enforces contiguity, so a hole would make every
+ // later append fail until the journal drained completely.
+ const frameSequence = this.nextFrameSequence;
+ await this.store.append({
+ frameSequence,
+ payload: frame.payload!,
+ });
+ this.nextFrameSequence = frameSequence + 1n;
+ frame.frameSequence = frameSequence;
+ this.frames.set(frame.frameSequence, frame);
+ this.publishedFrameSequence = frame.frameSequence;
+ if (this.backgroundStoreAndForward) {
+ if (this.lazyReplayStore) frame.payload = undefined;
+ this.enqueueDrain(frame);
+ return;
+ }
+ try {
+ await this.transmit(frame);
+ } catch (error) {
+ this.failTerminal(error);
+ throw error;
+ }
+ });
+ this.sendTail = publishing.catch(() => undefined);
+ return publishing;
+ }
+
+ private enqueueDrain(frame: ReplayFrame): void {
+ const draining = this.drainTail.then(async () => {
+ if (this.closing) return;
+ await this.transmit(frame);
+ });
+ this.drainTail = draining.catch((error: unknown) => {
+ if (!this.closing) this.failTerminal(error);
+ });
+ }
+
+ private startBackgroundConnect(): void {
+ const connecting = this.connectLoop(undefined, false, "unbounded");
+ this.reconnectTask = connecting;
+ void connecting
+ .catch((error: unknown) => {
+ if (!this.closing) this.failTerminal(error);
+ })
+ .finally(() => {
+ if (this.reconnectTask === connecting) this.reconnectTask = undefined;
+ });
+ }
+
+ async close(code = 1000, reason = ""): Promise {
+ if (this.closing) {
+ await this.closed;
+ return;
+ }
+ this.closing = true;
+ this.cancelBackoff?.();
+ this.messagesQueue.end();
+ const connection = this.connection;
+ // Tears down a connect that is still negotiating. Without this the socket
+ // and its deadline outlive close(), keeping the event loop open for up to
+ // connectTimeoutMs/authTimeoutMs after close() has already resolved.
+ this.connectAbort?.abort();
+ const connectingCandidate = this.connectingCandidate;
+ this.connection = undefined;
+ this.connectingCandidate = undefined;
+ let closeInfo: QwpConnectionCloseInfo = {
+ code,
+ reason,
+ wasClean: code === 1000,
+ };
+ if (connection) {
+ try {
+ await connection.close(code, reason);
+ closeInfo = await connection.closed;
+ } catch {
+ // The persistent store still has to close after a transport close race.
+ }
+ }
+ if (connectingCandidate && connectingCandidate !== connection) {
+ await connectingCandidate.close(code, reason).catch(() => undefined);
+ }
+ try {
+ await this.closeStore();
+ } finally {
+ this.releaseMemoryReplayReferences();
+ await Promise.all([
+ this.connectionDispatcher?.close(),
+ this.errorDispatcher?.close(),
+ ]);
+ this.settleClosed(closeInfo);
+ }
+ }
+
+ private async connectLoop(
+ initialCause: unknown,
+ reconnecting: boolean,
+ attemptPolicy: ConnectAttemptPolicy = this.backgroundStoreAndForward
+ ? "unbounded"
+ : "configured",
+ initialConnection?: Promise,
+ ): Promise {
+ const outageStarted = Date.now();
+ const previousEndpoint = this.lastEndpoint;
+ let attempt = 0;
+ let backoffMs = this.initialBackoffMs;
+ let lastError = initialCause;
+ let primaryUnavailableAttempts = 0;
+ if (reconnecting) {
+ this.emitEvent({
+ kind: QWP_RECONNECT_EVENT_KIND.RECONNECTING,
+ attempt: 0,
+ previousEndpoint,
+ cause: initialCause,
+ });
+ }
+
+ const initialRetryDelayMs = reconnectDelayMs(initialCause);
+ if (initialRetryDelayMs > 0) {
+ await this.waitForBackoff(jitterReconnectDelayMs(initialRetryDelayMs));
+ } else if (reconnecting && backoffMs > 0) {
+ await this.waitForBackoff(jitterReconnectDelayMs(backoffMs));
+ backoffMs = Math.min(Math.max(backoffMs * 2, 1), this.maxBackoffMs);
+ }
+
+ while (!this.closing) {
+ if (attempt > 0 && backoffMs > 0) {
+ await this.waitForBackoff(jitterReconnectDelayMs(backoffMs));
+ backoffMs = Math.min(Math.max(backoffMs * 2, 1), this.maxBackoffMs);
+ }
+ this.throwIfUnavailable();
+ attempt++;
+ if (reconnecting) this.totalReconnectAttempts++;
+ let candidate: QwpBinaryConnection | undefined;
+ try {
+ if (attempt === 1 && initialConnection) {
+ candidate = await initialConnection;
+ } else {
+ const abort = new AbortController();
+ this.connectAbort = abort;
+ try {
+ candidate = await this.factory(abort.signal);
+ } finally {
+ if (this.connectAbort === abort) this.connectAbort = undefined;
+ }
+ }
+ this.hasEverConnected = true;
+ this.connectingCandidate = candidate;
+ if (this.closing) {
+ await candidate.close().catch(() => undefined);
+ throw new QwpSendClosedError();
+ }
+ const replayed = await this.replayInto(candidate);
+ if (this.closing) throw new QwpSendClosedError();
+ this.install(candidate, replayed);
+ // The server is reachable again, so the escalation window resumes.
+ this.endPoisonOutage();
+ this.resetCatchUpCapGapEpisode();
+ this.resetDurableAckMismatchEpisode();
+ this.connectingCandidate = undefined;
+ if (reconnecting) {
+ this.totalReconnectsSucceeded++;
+ const failedOver =
+ previousEndpoint !== undefined &&
+ String(previousEndpoint) !== String(candidate.endpoint);
+ if (failedOver) this.totalFailovers++;
+ this.emitEvent({
+ kind: failedOver
+ ? QWP_RECONNECT_EVENT_KIND.FAILED_OVER
+ : QWP_RECONNECT_EVENT_KIND.RECONNECTED,
+ attempt,
+ endpoint: candidate.endpoint,
+ previousEndpoint,
+ });
+ } else {
+ this.emitEvent({
+ kind: QWP_RECONNECT_EVENT_KIND.CONNECTED,
+ attempt: 0,
+ endpoint: candidate.endpoint,
+ });
+ }
+ return;
+ } catch (error) {
+ // A poison frame is meant to identify a connection that repeatedly
+ // accepts the same replay head and then rejects it or disappears. A
+ // failed connection/replay attempt breaks that sequence, so the
+ // outage must not supply the escalation dwell time -- but the strikes
+ // already earned have to survive it. Wiping the episode here made the
+ // canonical poison case unreachable: a frame that takes the server
+ // down guarantees the next connect fails, which reset the count
+ // before it could ever reach maxFrameRejections.
+ this.beginPoisonOutage();
+ if (reconnecting) this.totalReconnectErrors++;
+ lastError = error;
+ if (this.connectingCandidate === candidate) {
+ this.connectingCandidate = undefined;
+ }
+ if (candidate) await candidate.close().catch(() => undefined);
+ this.emitEvent({
+ kind: QWP_RECONNECT_EVENT_KIND.ATTEMPT_FAILED,
+ attempt,
+ endpoint: candidate?.endpoint,
+ previousEndpoint,
+ cause: error,
+ });
+ const capGapError =
+ error instanceof QwpCatchUpCapGapError
+ ? this.applyCatchUpCapGapPolicy(error)
+ : undefined;
+ if (!capGapError) this.resetCatchUpCapGapEpisode();
+ if (capGapError?.exhausted) throw capGapError.error;
+ if (
+ capGapError &&
+ !this.orphanStoreAndForward &&
+ attemptPolicy !== "unbounded"
+ ) {
+ throw capGapError.error;
+ }
+ const durableAckMismatch = durableAckUnavailableCause(error);
+ if (
+ durableAckMismatch &&
+ (!this.backgroundStoreAndForward || attemptPolicy !== "unbounded")
+ ) {
+ this.resetDurableAckMismatchEpisode();
+ throw durableAckMismatch;
+ }
+ const durableAckPolicy =
+ durableAckMismatch &&
+ this.backgroundStoreAndForward &&
+ attemptPolicy === "unbounded"
+ ? this.applyDurableAckMismatchPolicy(durableAckMismatch)
+ : undefined;
+ if (!durableAckPolicy) this.resetDurableAckMismatchEpisode();
+ if (durableAckPolicy?.exhausted) throw durableAckPolicy.error;
+ if (
+ this.orphanStoreAndForward &&
+ attemptPolicy === "unbounded" &&
+ isPrimaryUnavailableError(error)
+ ) {
+ primaryUnavailableAttempts++;
+ this.emitEvent({
+ kind: QWP_RECONNECT_EVENT_KIND.PRIMARY_UNAVAILABLE,
+ attempt: primaryUnavailableAttempts,
+ previousEndpoint,
+ cause: error,
+ });
+ }
+ if (!durableAckPolicy && !this.isRetryableReconnectError(error)) {
+ throw error;
+ }
+ const attemptsExhausted =
+ attemptPolicy === "single" ||
+ (attemptPolicy === "configured" &&
+ this.maxAttempts > 0 &&
+ attempt >= this.maxAttempts);
+ const durationExhausted =
+ attemptPolicy === "configured" &&
+ this.maxDurationMs > 0 &&
+ Date.now() - outageStarted >= this.maxDurationMs;
+ if (attemptsExhausted || durationExhausted) {
+ if (attemptPolicy === "single") throw error;
+ throw new QwpReconnectExhaustedError(attempt, lastError);
+ }
+ }
+ }
+ throw new QwpSendClosedError();
+ }
+
+ private applyCatchUpCapGapPolicy(error: QwpCatchUpCapGapError): {
+ exhausted: boolean;
+ error: QwpCatchUpCapGapError;
+ } {
+ // Foreground SF owns producer data and must wait for a larger-cap node.
+ if (!this.orphanStoreAndForward) {
+ return { exhausted: false, error };
+ }
+ const now = monotonicNowMs();
+ if (this.catchUpCapGapAttempts === 0) {
+ this.catchUpCapGapFirstMs = now;
+ }
+ this.catchUpCapGapAttempts++;
+ const episodeMs = Math.max(0, now - this.catchUpCapGapFirstMs);
+ const exhausted =
+ this.catchUpCapGapAttempts >= MAX_CATCH_UP_CAP_GAP_ATTEMPTS &&
+ episodeMs >= this.catchUpCapGapMinEscalationWindowMs;
+ return {
+ exhausted,
+ error: new QwpCatchUpCapGapError(
+ error.symbolId,
+ error.frameLength,
+ error.maxBatchSizeBytes,
+ {
+ attempt: this.catchUpCapGapAttempts,
+ episodeMs,
+ minEscalationWindowMs: this.catchUpCapGapMinEscalationWindowMs,
+ exhausted,
+ },
+ ),
+ };
+ }
+
+ private resetCatchUpCapGapEpisode(): void {
+ this.catchUpCapGapAttempts = 0;
+ this.catchUpCapGapFirstMs = 0;
+ }
+
+ private applyDurableAckMismatchPolicy(error: QwpDurableAckUnavailableError): {
+ exhausted: boolean;
+ error: QwpDurableAckUnavailableError | QwpDurableAckPersistentFailureError;
+ } {
+ const now = monotonicNowMs();
+ if (this.durableAckMismatchAttempts === 0) {
+ this.durableAckMismatchFirstMs = now;
+ }
+ this.durableAckMismatchAttempts++;
+ const episodeMs = Math.max(0, now - this.durableAckMismatchFirstMs);
+ const durationExhausted =
+ this.orphanDurableAckMismatchMaxDurationMs > 0 &&
+ episodeMs >= this.orphanDurableAckMismatchMaxDurationMs;
+ const exhausted =
+ this.orphanStoreAndForward &&
+ (this.durableAckMismatchAttempts >=
+ MAX_ORPHAN_DURABLE_ACK_MISMATCH_ATTEMPTS ||
+ durationExhausted);
+ if (exhausted) {
+ const persistent = new QwpDurableAckPersistentFailureError(
+ this.durableAckMismatchAttempts,
+ episodeMs,
+ error,
+ );
+ this.emitEvent({
+ kind: QWP_RECONNECT_EVENT_KIND.DURABLE_ACK_PERSISTENT_FAILURE,
+ attempt: this.durableAckMismatchAttempts,
+ previousEndpoint: this.lastEndpoint,
+ cause: persistent,
+ episodeMs,
+ });
+ return { exhausted: true, error: persistent };
+ }
+ this.emitEvent({
+ kind: QWP_RECONNECT_EVENT_KIND.DURABLE_ACK_UNAVAILABLE,
+ attempt: this.durableAckMismatchAttempts,
+ previousEndpoint: this.lastEndpoint,
+ cause: error,
+ episodeMs,
+ });
+ return { exhausted: false, error };
+ }
+
+ private resetDurableAckMismatchEpisode(): void {
+ this.durableAckMismatchAttempts = 0;
+ this.durableAckMismatchFirstMs = 0;
+ }
+
+ private isRetryableReconnectError(error: unknown): boolean {
+ if (
+ this.backgroundStoreAndForward &&
+ !this.orphanStoreAndForward &&
+ this.hasEverConnected &&
+ isEndpointPolicyFailure(error)
+ ) {
+ return true;
+ }
+ return isRetryableReconnectError(error);
+ }
+
+ private async replayInto(
+ connection: QwpBinaryConnection,
+ ): Promise {
+ const replayed: ReplayFrame[] = [];
+ const cap = minimumDefined(
+ connection.handshake.maxBatchSizeBytes,
+ this.localMaxBatchSizeBytes,
+ );
+ this.durableWatermarks.clear();
+ for (const payload of dictionaryCatchupFrames(this.symbolDictionary, cap)) {
+ const frame: ReplayFrame = {
+ frameSequence: -1n,
+ payload,
+ payloadLength: payload.byteLength,
+ ackDelivered: true,
+ transmitted: true,
+ dictionaryCatchup: true,
+ };
+ replayed.push(frame);
+ await this.sendPhysical(connection, payload, false);
+ }
+ for (const frame of this.frames.values()) {
+ if (!frame.transmitted) continue;
+ if (this.isRecoveredDiscardFrame(frame.frameSequence)) continue;
+ frame.durableTargets = undefined;
+ if (cap !== undefined && frame.payloadLength > cap) {
+ throw new RangeError(
+ `persisted QWP frame exceeds reconnect target batch cap [size=${frame.payloadLength}, max=${cap}]`,
+ );
+ }
+ const payload = await this.readFramePayload(frame);
+ replayed.push(frame);
+ await this.sendPhysical(connection, payload, true);
+ }
+ return replayed;
+ }
+
+ private install(
+ connection: QwpBinaryConnection,
+ wireFrames: ReplayFrame[],
+ ): void {
+ this.hasEverConnected = true;
+ this.connection = connection;
+ this.lastHandshake = connection.handshake;
+ this.lastEndpoint = connection.endpoint;
+ this.wireFrames = wireFrames;
+ this.wireFramesBase = 0;
+ if (connection.ping && !this.ping) {
+ // Assigned only when the initial transport supports PING so browser
+ // connections keep the optional capability genuinely absent.
+ this.ping = () => this.pingWithReconnect();
+ }
+ const generation = ++this.generation;
+ void this.pump(connection, generation);
+ }
+
+ private async pump(
+ connection: QwpBinaryConnection,
+ generation: number,
+ ): Promise {
+ try {
+ for await (const payload of connection.messages) {
+ if (
+ this.closing ||
+ this.connection !== connection ||
+ generation !== this.generation
+ ) {
+ return;
+ }
+ let translated: Uint8Array | undefined;
+ try {
+ translated = await this.translateResponse(payload);
+ } catch (error) {
+ if (
+ error instanceof RetriableIngressNackError ||
+ error instanceof QwpProtocolError ||
+ error instanceof QwpReplayRejectedError
+ ) {
+ throw error;
+ }
+ // The wire payload decoded successfully. Failures from this point
+ // are local replay-store/bookkeeping failures, not evidence that
+ // the server rejected the head frame.
+ if (isRetryableReconnectError(error)) {
+ // A journal fault here is usually transient: a briefly full or
+ // read-only filesystem parks maintenanceFailure for about a
+ // second and the store clears it on the next successful batch.
+ // failTerminal() is permanent, so latching would brick a running
+ // producer for the rest of the process lifetime -- the outcome
+ // the store-level retry exists to prevent. transmitOnce() routes
+ // the identical class to requestReconnect() for that reason and
+ // this path has to agree. acknowledgeThrough() persists its
+ // cursor before it mutates anything, so a failure here leaves
+ // exactly the state a crash at this instant would leave, and
+ // replay resumes from the persisted watermark.
+ await this.requestReconnect(error, connection).catch(
+ (reconnectError) => this.failTerminal(reconnectError),
+ );
+ return;
+ }
+ this.failTerminal(error);
+ await connection
+ .close(1011, "QWP ingress response processing failed")
+ .catch(() => undefined);
+ return;
+ }
+ if (translated) this.messagesQueue.push(translated);
+ if (this.terminalError) return;
+ }
+ if (
+ this.closing ||
+ this.connection !== connection ||
+ generation !== this.generation
+ ) {
+ return;
+ }
+ const info = await connection.closed;
+ const cause = this.classifyConnectionLoss(
+ new QwpSendClosedError(info),
+ info,
+ );
+ if (cause instanceof QwpProtocolError) {
+ this.failTerminal(cause);
+ await connection
+ .close(1002, "poisoned QWP ingress frame")
+ .catch(() => undefined);
+ return;
+ }
+ await this.requestReconnect(cause, connection).catch((reconnectError) =>
+ this.failTerminal(reconnectError),
+ );
+ return;
+ } catch (error) {
+ if (
+ this.closing ||
+ this.connection !== connection ||
+ generation !== this.generation
+ ) {
+ return;
+ }
+ if (
+ error instanceof QwpProtocolError ||
+ error instanceof QwpReplayRejectedError
+ ) {
+ this.failTerminal(error);
+ await connection
+ .close(1002, "terminal QWP response")
+ .catch(() => undefined);
+ return;
+ }
+ const cause =
+ error instanceof RetriableIngressNackError
+ ? error
+ : this.classifyConnectionLoss(
+ error,
+ error instanceof QwpSendClosedError ? error.closeInfo : undefined,
+ );
+ if (cause instanceof QwpProtocolError) {
+ this.failTerminal(cause);
+ await connection
+ .close(1002, "poisoned QWP ingress frame")
+ .catch(() => undefined);
+ return;
+ }
+ await this.requestReconnect(cause, connection).catch((reconnectError) => {
+ this.failTerminal(reconnectError);
+ });
+ }
+ }
+
+ private async translateResponse(
+ payload: Uint8Array,
+ ): Promise {
+ const response = decodeQwpIngressResponse(payload);
+ if (response.status === QWP_STATUS.DURABLE_ACK) {
+ for (const table of response.tables) {
+ const current = this.durableWatermarks.get(table.name);
+ if (current === undefined || table.sequenceTransaction > current) {
+ this.durableWatermarks.set(table.name, table.sequenceTransaction);
+ }
+ }
+ await this.trimDurablePrefix();
+ return payload;
+ }
+ if (response.sequence === null) {
+ throw new QwpProtocolError("QWP response is missing its wire sequence");
+ }
+ if (response.sequence < 0n) {
+ throw new QwpProtocolError(
+ `QWP response sequence is negative: ${response.sequence}`,
+ );
+ }
+ const highestWireIndex = this.wireFramesBase + this.wireFrames.length - 1;
+ if (response.sequence > BigInt(highestWireIndex)) {
+ // Reject an over-range sequence rather than clamping it, matching the null
+ // and negative guards above. A frame is logged here before it is sent, so
+ // a conforming server can only acknowledge a sequence it has received,
+ // never one beyond the last frame sent. Clamping a bogus over-range value
+ // onto the newest in-flight frame would retire every unacknowledged frame
+ // below it and delete journal records the server never confirmed -- the
+ // watermark must never advance past an unacknowledged frame.
+ throw new QwpProtocolError(
+ `QWP response sequence is beyond the last frame sent: ${response.sequence} > ${highestWireIndex}`,
+ );
+ }
+ const wireIndex = Number(response.sequence);
+ const localIndex = wireIndex - this.wireFramesBase;
+ const frame = localIndex >= 0 ? this.wireFrames[localIndex] : undefined;
+ if (!frame) {
+ // Either nothing has been sent on this connection yet, or this sequence
+ // was covered by an earlier cumulative ACK and trimmed. A duplicate OK
+ // has already been delivered; a NACK still has to be reported.
+ if (response.status === QWP_STATUS.OK) return undefined;
+ this.totalServerNacks++;
+ const pending = this.pendingFsnRange();
+ this.emitSenderError(
+ createQwpSenderError(response, {
+ messageSequence: response.sequence ?? undefined,
+ fromFsn: pending?.from,
+ toFsn: pending?.to,
+ }),
+ );
+ if (isRetriableIngressStatus(response.status)) {
+ throw new RetriableIngressNackError(
+ -1n,
+ response.status,
+ this.nextExemptRecycleDelay(),
+ response.errorMessage,
+ );
+ }
+ throw new QwpProtocolError(
+ `QuestDB rejected ingress before any frame was sent [status=0x${response.status.toString(16)}]${
+ response.errorMessage ? `: ${response.errorMessage}` : ""
+ }`,
+ );
+ }
+
+ if (response.status === QWP_STATUS.OK) {
+ if (frame.dictionaryCatchup) return undefined;
+ const covered = this.wireFrames.slice(0, localIndex + 1);
+ const clientTarget = findLastClientFrame(covered);
+ const shouldDeliver = covered.some(
+ (candidate) =>
+ candidate.clientSequence !== undefined && !candidate.ackDelivered,
+ );
+ for (const candidate of covered) candidate.ackDelivered = true;
+ if (frame.frameSequence > this.highestOkFrameSequence) {
+ this.highestOkFrameSequence = frame.frameSequence;
+ }
+ this.clearPoisonThrough(frame.frameSequence);
+ if (this.handshake.durableAckEnabled) {
+ frame.durableTargets = new Map(
+ response.tables.map((table) => [
+ table.name,
+ table.sequenceTransaction,
+ ]),
+ );
+ await this.trimDurablePrefix();
+ } else {
+ await this.acknowledgeThrough(frame.frameSequence);
+ }
+ // ACKs are cumulative, so nothing reads the covered prefix again.
+ // Dropping it keeps both the log and the payloads it pins bounded, and
+ // keeps each ACK proportional to the frames it actually covers.
+ this.wireFrames.splice(0, localIndex + 1);
+ this.wireFramesBase += localIndex + 1;
+ if (!shouldDeliver || clientTarget?.clientSequence === undefined) {
+ return undefined;
+ }
+ return rewriteResponseSequence(payload, clientTarget.clientSequence);
+ }
+
+ this.totalServerNacks++;
+ const pending = frame.dictionaryCatchup
+ ? this.pendingFsnRange()
+ : undefined;
+ this.emitSenderError(
+ createQwpSenderError(response, {
+ messageSequence: response.sequence,
+ fromFsn: pending?.from ?? frame.frameSequence,
+ toFsn: pending?.to ?? frame.frameSequence,
+ }),
+ );
+
+ if (isRetriableIngressStatus(response.status)) {
+ const exempt =
+ frame.dictionaryCatchup ||
+ response.status === QWP_STATUS.NOT_WRITABLE ||
+ // A DICTIONARY_GAP is the server asking for symbol catch-up, not a
+ // verdict on this frame. The catch-up it triggers has not been sent
+ // yet, so charging the frame a strike condemns it before the recovery
+ // it asked for has been attempted.
+ response.status === QWP_STATUS.DICTIONARY_GAP ||
+ qwpSenderErrorCategory(response.status) ===
+ QWP_SENDER_ERROR_CATEGORY.UNKNOWN;
+ if (exempt) {
+ this.resetPoisonEpisode();
+ throw new RetriableIngressNackError(
+ frame.frameSequence,
+ response.status,
+ this.nextExemptRecycleDelay(),
+ response.errorMessage,
+ );
+ }
+ if (this.recordPoisonStrike(frame.frameSequence)) {
+ this.emitSenderError(
+ createQwpProtocolViolationSenderError(
+ `frame remained rejected after ${this.poisonStrikes} attempts${
+ response.errorMessage ? `: ${response.errorMessage}` : ""
+ }`,
+ frame.frameSequence,
+ ),
+ );
+ throw new QwpReplayRejectedError(
+ frame.frameSequence,
+ response.status,
+ `frame remained rejected after ${this.poisonStrikes} attempts${
+ response.errorMessage ? `: ${response.errorMessage}` : ""
+ }`,
+ );
+ }
+ throw new RetriableIngressNackError(
+ frame.frameSequence,
+ response.status,
+ cappedExponentialBackoff(
+ this.initialBackoffMs,
+ this.maxBackoffMs,
+ this.poisonStrikes - 1,
+ ),
+ response.errorMessage,
+ );
+ }
+
+ if (frame.dictionaryCatchup) {
+ const error = new QwpProtocolError(
+ `QuestDB rejected QWP symbol dictionary catch-up [status=0x${response.status.toString(16)}]${
+ response.errorMessage ? `: ${response.errorMessage}` : ""
+ }`,
+ );
+ this.failTerminal(error);
+ return undefined;
+ }
+
+ const replayError = new QwpReplayRejectedError(
+ frame.frameSequence,
+ response.status,
+ response.errorMessage,
+ );
+ if (frame.clientSequence === undefined) {
+ this.failTerminal(replayError);
+ return undefined;
+ }
+ const translated = rewriteResponseSequence(payload, frame.clientSequence);
+ this.messagesQueue.push(translated);
+ this.failTerminal(replayError);
+ return undefined;
+ }
+
+ private async trimDurablePrefix(): Promise {
+ let lastCovered: bigint | undefined;
+ for (const frame of this.frames.values()) {
+ // Successful ingress ACKs are cumulative. Deferred frames therefore
+ // have no checkpoint of their own; a later commit-bearing ACK covers
+ // them and its durable targets retire the whole preceding range.
+ if (!frame.durableTargets) continue;
+ if (!areTargetsCovered(frame.durableTargets, this.durableWatermarks)) {
+ break;
+ }
+ lastCovered = frame.frameSequence;
+ }
+ if (lastCovered !== undefined) await this.acknowledgeThrough(lastCovered);
+ }
+
+ private clearPoisonThrough(frameSequence: bigint): void {
+ if (
+ this.poisonFrameSequence === undefined ||
+ frameSequence < this.poisonFrameSequence
+ ) {
+ return;
+ }
+ this.resetPoisonEpisode();
+ }
+
+ private resetPoisonEpisode(): void {
+ this.poisonFrameSequence = undefined;
+ this.poisonFirstStrikeMs = 0;
+ this.poisonStrikes = 0;
+ this.poisonOutageMs = 0;
+ this.poisonOutageStartedMs = 0;
+ }
+
+ /**
+ * Marks the start of a connection-establishment outage. The strikes a frame
+ * has already earned survive it -- otherwise a frame that takes the server
+ * down can never escalate, because the very crash it causes makes the next
+ * connect fail and wipes the episode. Only the dwell the outage would have
+ * contributed is withheld, which is what the escalation window is for.
+ */
+ private beginPoisonOutage(): void {
+ if (this.poisonFrameSequence === undefined) return;
+ if (this.poisonOutageStartedMs === 0) {
+ this.poisonOutageStartedMs = Date.now();
+ }
+ }
+
+ /** Banks the elapsed outage so it cannot count toward the escalation window. */
+ private endPoisonOutage(): void {
+ if (this.poisonOutageStartedMs === 0) return;
+ this.poisonOutageMs += Date.now() - this.poisonOutageStartedMs;
+ this.poisonOutageStartedMs = 0;
+ }
+
+ private recordPoisonStrike(frameSequence: bigint): boolean {
+ const now = Date.now();
+ this.endPoisonOutage();
+ if (this.poisonFrameSequence === frameSequence) {
+ this.poisonStrikes++;
+ } else {
+ this.poisonFrameSequence = frameSequence;
+ this.poisonStrikes = 1;
+ this.poisonFirstStrikeMs = now;
+ this.poisonOutageMs = 0;
+ this.poisonOutageStartedMs = 0;
+ }
+ const connectedDwellMs =
+ now - this.poisonFirstStrikeMs - this.poisonOutageMs;
+ return (
+ this.poisonStrikes >= this.maxFrameRejections &&
+ connectedDwellMs >= this.poisonMinEscalationWindowMs
+ );
+ }
+
+ private classifyConnectionLoss(
+ cause: unknown,
+ closeInfo?: QwpConnectionCloseInfo,
+ ): Error {
+ const exempt =
+ closeInfo?.code === 1000 ||
+ closeInfo?.code === 1001 ||
+ closeInfo?.code === 1012 ||
+ closeInfo?.code === 1013;
+ if (exempt) this.resetPoisonEpisode();
+ const head = exempt ? undefined : this.currentPoisonHead();
+ if (!head) {
+ return new RetriableIngressConnectionError(
+ this.nextExemptRecycleDelay(),
+ cause,
+ );
+ }
+ if (this.recordPoisonStrike(head.frameSequence)) {
+ const closeDetail = closeInfo
+ ? `code=${closeInfo.code}, reason=${closeInfo.reason}`
+ : "transport ended without an orderly close";
+ const message = `QWP ingress frame repeatedly caused a non-orderly connection loss [frameSequence=${head.frameSequence}, strikes=${this.poisonStrikes}, ${closeDetail}]`;
+ this.emitSenderError(
+ createQwpProtocolViolationSenderError(
+ message,
+ head.frameSequence,
+ this.nextFrameSequence - 1n,
+ ),
+ );
+ return new QwpProtocolError(message);
+ }
+ return new RetriableIngressConnectionError(
+ cappedExponentialBackoff(
+ this.initialBackoffMs,
+ this.maxBackoffMs,
+ this.poisonStrikes - 1,
+ ),
+ cause,
+ );
+ }
+
+ private currentPoisonHead(): ReplayFrame | undefined {
+ const progress =
+ this.highestOkFrameSequence > this.acknowledgedFrameSequence
+ ? this.highestOkFrameSequence
+ : this.acknowledgedFrameSequence;
+ return this.wireFrames.find(
+ (frame) => !frame.dictionaryCatchup && frame.frameSequence > progress,
+ );
+ }
+
+ private nextExemptRecycleDelay(): number {
+ const progress =
+ this.highestOkFrameSequence > this.acknowledgedFrameSequence
+ ? this.highestOkFrameSequence
+ : this.acknowledgedFrameSequence;
+ if (progress > this.progressAtLastExemptRecycle) {
+ this.zeroProgressRecycles = 0;
+ }
+ this.progressAtLastExemptRecycle = progress;
+ const level = this.zeroProgressRecycles++;
+ if (level === 0) return 0;
+ return cappedExponentialBackoff(
+ this.initialBackoffMs,
+ this.maxBackoffMs,
+ level - 1,
+ );
+ }
+
+ private async acknowledgeThrough(frameSequence: bigint): Promise {
+ await this.acknowledgeStoredFramesThrough(frameSequence);
+ await this.retireRecoveredDiscardTailIfReady();
+ }
+
+ private async acknowledgeStoredFramesThrough(
+ frameSequence: bigint,
+ ): Promise {
+ await this.store.acknowledgeThrough(frameSequence);
+ for (const sequence of this.frames.keys()) {
+ if (sequence > frameSequence) break;
+ this.frames.delete(sequence);
+ }
+ if (frameSequence > this.acknowledgedFrameSequence) {
+ this.acknowledgedFrameSequence = frameSequence;
+ }
+ }
+
+ private isRecoveredDiscardFrame(frameSequence: bigint): boolean {
+ const tail = this.recoveredDiscardTail;
+ return (
+ tail !== undefined &&
+ frameSequence >= tail.startSequence &&
+ frameSequence <= tail.tipSequence
+ );
+ }
+
+ private async retireRecoveredDiscardTailIfReady(): Promise {
+ const tail = this.recoveredDiscardTail;
+ if (!tail) return;
+ if (
+ tail.predecessorSequence !== undefined &&
+ this.frames.has(tail.predecessorSequence)
+ ) {
+ return;
+ }
+ await this.acknowledgeStoredFramesThrough(tail.tipSequence);
+ this.recoveredDiscardTail = undefined;
+ }
+
+ private async persistSymbolDictionaryDelta(
+ delta: NonNullable>,
+ ): Promise {
+ if (
+ !this.store.loadSymbolDictionary ||
+ !this.store.appendSymbolDictionary
+ ) {
+ throw new QwpReplayDictionaryError(
+ "QWP delta symbol dictionaries require a replay store with dictionary persistence",
+ );
+ }
+ if (delta.startId > this.symbolDictionary.length) {
+ throw new QwpReplayDictionaryError(
+ `QWP symbol dictionary has a gap [expectedAtMost=${this.symbolDictionary.length}, received=${delta.startId}]`,
+ );
+ }
+ const overlap = Math.min(
+ this.symbolDictionary.length - delta.startId,
+ delta.entries.length,
+ );
+ for (let index = 0; index < overlap; index++) {
+ const id = delta.startId + index;
+ if (this.symbolDictionary[id] !== delta.entries[index]) {
+ throw new QwpReplayDictionaryError(
+ `QWP symbol dictionary conflicts at ID ${id}`,
+ );
+ }
+ }
+ const firstNewEntry = Math.max(
+ this.symbolDictionary.length - delta.startId,
+ 0,
+ );
+ const newEntries = delta.entries.slice(firstNewEntry);
+ if (newEntries.length === 0) return;
+ const startId = this.symbolDictionary.length;
+ try {
+ await this.store.appendSymbolDictionary(startId, newEntries);
+ } catch (error) {
+ this.deltaSymbolDictionaryEnabled = false;
+ throw new QwpReplayDictionaryPersistenceError(error);
+ }
+ this.symbolDictionary.push(...newEntries);
+ }
+
+ private async transmit(frame: ReplayFrame): Promise {
+ // Loops when a reconnect completes while this frame's payload is being
+ // read; see the currency check below.
+ for (;;) {
+ if (await this.transmitOnce(frame)) return;
+ }
+ }
+
+ /** Returns false when a reconnect invalidated the captured connection. */
+ private async transmitOnce(frame: ReplayFrame): Promise {
+ const connection = await this.requireConnection();
+ const generation = this.generation;
+ const cap = minimumDefined(
+ connection.handshake.maxBatchSizeBytes,
+ this.localMaxBatchSizeBytes,
+ );
+ if (cap !== undefined && frame.payloadLength > cap) {
+ // Data the producer already handed over is never reclassified as
+ // unsendable because a failover landed on a smaller-cap node: that would
+ // invent a terminal for a frame an earlier node would have taken. Treat
+ // it as a connection-level failure, exactly as replayInto() does with the
+ // identical check, so the reconnect loop keeps looking for a node that
+ // can take it. Marking it transmitted is what puts it in replayInto()'s
+ // resend set; it is deliberately not pushed onto the wire log, because
+ // nothing reached the wire and the log is indexed by wire sequence.
+ frame.transmitted = true;
+ await this.requestReconnect(
+ new RangeError(
+ `QWP frame exceeds reconnect target batch cap [size=${frame.payloadLength}, max=${cap}]`,
+ ),
+ connection,
+ );
+ return true;
+ }
+ let payload: Uint8Array;
+ try {
+ payload = await this.readFramePayload(frame);
+ } catch (error) {
+ // A journal read can fail transiently: a briefly full or read-only
+ // filesystem parks maintenanceFailure for about a second, and the store
+ // clears it on the next successful batch. enqueueDrain's only handler is
+ // failTerminal, so letting this escape would brick a running producer for
+ // the rest of the process lifetime -- the very outcome the store-level
+ // retry was added to prevent. replayInto() makes the identical read and
+ // connectLoop retries its failures, so route this one the same way.
+ // Deterministic corruption still escapes and stays terminal.
+ if (!isRetryableReconnectError(error)) throw error;
+ // Nothing reached the wire, so the frame is deliberately kept off the
+ // wire log; marking it transmitted is what puts it in replayInto()'s
+ // resend set, exactly as the batch-cap branch above does.
+ frame.transmitted = true;
+ await this.requestReconnect(error, connection);
+ return true;
+ }
+ // The journal read above yields, and with a lazy store it can park behind
+ // an fsyncing append for longer than a jittered reconnect takes. install()
+ // swaps this.wireFrames wholesale and resets wireFramesBase, while
+ // replayInto() skipped this frame because it was not transmitted yet.
+ // Pushing it now would log it against the replacement connection's wire
+ // sequence while sending it on the dead one, so the replacement's next
+ // cumulative ACK would retire a frame no server ever received and delete
+ // its journal record. Retry against the current connection instead.
+ if (this.connection !== connection || this.generation !== generation) {
+ return false;
+ }
+ frame.transmitted = true;
+ this.wireFrames.push(frame);
+ try {
+ await this.sendPhysical(connection, payload, false);
+ if (this.lazyReplayStore) frame.payload = undefined;
+ } catch (error) {
+ await this.requestReconnect(error, connection);
+ if (this.lazyReplayStore) frame.payload = undefined;
+ }
+ return true;
+ }
+
+ private async readFramePayload(frame: ReplayFrame): Promise {
+ let payload = frame.payload;
+ if (!payload) {
+ if (!this.lazyReplayStore) {
+ throw new QwpProtocolError(
+ `QWP replay payload is unavailable [frameSequence=${frame.frameSequence}]`,
+ );
+ }
+ payload = await this.lazyReplayStore.readPayload(frame.frameSequence);
+ }
+ if (payload.byteLength !== frame.payloadLength) {
+ throw new QwpProtocolError(
+ `persisted QWP frame length changed [frameSequence=${frame.frameSequence}, expected=${frame.payloadLength}, received=${payload.byteLength}]`,
+ );
+ }
+ return payload;
+ }
+
+ private async sendPhysical(
+ connection: QwpBinaryConnection,
+ payload: Uint8Array,
+ replayed: boolean,
+ ): Promise {
+ this.totalFramesSent++;
+ this.totalBytesSent += payload.byteLength;
+ if (replayed) {
+ this.totalFramesReplayed++;
+ this.totalBytesReplayed += payload.byteLength;
+ }
+ await connection.send(payload);
+ }
+
+ private async requireConnection(): Promise {
+ if (this.reconnectTask) await this.reconnectTask;
+ this.throwIfUnavailable();
+ if (!this.connection) throw new QwpSendClosedError();
+ return this.connection;
+ }
+
+ private async requestReconnect(
+ cause: unknown,
+ failedConnection: QwpBinaryConnection,
+ ): Promise {
+ if (this.closing) throw new QwpSendClosedError();
+ if (this.connection && this.connection !== failedConnection) return;
+ if (this.reconnectTask) {
+ const activeReconnect = this.reconnectTask;
+ await activeReconnect;
+ if (this.connection === failedConnection && !this.closing) {
+ await this.requestReconnect(cause, failedConnection);
+ }
+ return;
+ }
+
+ if (
+ cause instanceof RetriableIngressNackError &&
+ cause.status === QWP_STATUS.NOT_WRITABLE
+ ) {
+ // NOT_WRITABLE describes this node, not the replayed frame. Preserve the
+ // frame and make the next factory sweep start at another endpoint.
+ failedConnection.deprioritizeEndpoint?.();
+ }
+ this.connection = undefined;
+ void failedConnection.close().catch(() => undefined);
+ const reconnecting = this.connectLoop(
+ cause,
+ true,
+ this.backgroundStoreAndForward ? "unbounded" : "configured",
+ );
+ this.reconnectTask = reconnecting;
+ try {
+ await reconnecting;
+ } finally {
+ if (this.reconnectTask === reconnecting) this.reconnectTask = undefined;
+ }
+ }
+
+ private async pingWithReconnect(): Promise {
+ const connection = await this.requireConnection();
+ if (!connection.ping) {
+ throw new Error("QWP reconnect target does not support WebSocket PING");
+ }
+ try {
+ await connection.ping();
+ } catch (error) {
+ await this.requestReconnect(error, connection);
+ const replacement = await this.requireConnection();
+ if (!replacement.ping) {
+ throw new Error("QWP reconnect target does not support WebSocket PING");
+ }
+ await replacement.ping();
+ }
+ }
+
+ private async waitForBackoff(delayMs: number): Promise {
+ await new Promise((resolve) => {
+ const timer = setTimeout(() => {
+ if (this.cancelBackoff === cancel) this.cancelBackoff = undefined;
+ resolve();
+ }, delayMs);
+ const cancel = (): void => {
+ clearTimeout(timer);
+ if (this.cancelBackoff === cancel) this.cancelBackoff = undefined;
+ resolve();
+ };
+ this.cancelBackoff = cancel;
+ });
+ }
+
+ private emitEvent(event: Omit): void {
+ this.connectionDispatcher?.offer({
+ ...event,
+ timestampMs: Date.now(),
+ });
+ }
+
+ private emitSenderError(error: QwpSenderError): void {
+ this.errorDispatcher?.offer(error);
+ }
+
+ private pendingFsnRange(): { from: bigint; to: bigint } | undefined {
+ const iterator = this.frames.keys();
+ const first = iterator.next();
+ if (first.done) return undefined;
+ let to = first.value;
+ for (const frameSequence of iterator) to = frameSequence;
+ return { from: first.value, to };
+ }
+
+ private throwIfUnavailable(): void {
+ if (this.terminalError) throw this.terminalError;
+ if (this.closing) throw new QwpSendClosedError();
+ }
+
+ private failTerminal(error: unknown): void {
+ if (this.terminalError) return;
+ this.terminalError =
+ error instanceof Error
+ ? error
+ : new Error(`QWP reconnect failed: ${error}`);
+ this.cancelBackoff?.();
+ this.messagesQueue.fail(this.terminalError);
+ this.settleClosed({
+ code: 1011,
+ reason: this.terminalError.message,
+ wasClean: false,
+ });
+ void this.closeStore()
+ .catch(() => undefined)
+ .finally(() => this.releaseMemoryReplayReferences());
+ void this.connection
+ ?.close(1011, "QWP reconnect failed")
+ .catch(() => undefined);
+ }
+
+ private settleClosed(info: QwpConnectionCloseInfo): void {
+ if (this.closedSettled) return;
+ this.closedSettled = true;
+ this.resolveClosed(info);
+ }
+
+ private closeStore(): Promise {
+ if (!this.storeClosePromise) {
+ this.storeClosePromise = Promise.resolve().then(() => this.store.close());
+ }
+ return this.storeClosePromise;
+ }
+
+ private releaseMemoryReplayReferences(): void {
+ if (!(this.store instanceof QwpMemoryReplayStore)) return;
+ this.frames.clear();
+ this.wireFrames = [];
+ this.wireFramesBase = 0;
+ this.symbolDictionary.length = 0;
+ this.durableWatermarks.clear();
+ }
+}
+
+function isLazyReplayStore(
+ store: QwpIngressReplayStore,
+): store is LazyReplayStore {
+ return (
+ typeof store.loadReferences === "function" &&
+ typeof store.readPayload === "function"
+ );
+}
+
+function readSymbolDictionaryDelta(payload: Uint8Array) {
+ // Preserve support for opaque/custom payloads used with the low-level API.
+ if (
+ payload.byteLength < QWP_HEADER_SIZE ||
+ (payload[5] & QWP_FLAG_DELTA_SYMBOL_DICTIONARY) === 0
+ ) {
+ return undefined;
+ }
+ return decodeQwpIngressSymbolDictionaryDelta(payload);
+}
+
+async function analyzeRecoveredDiscardTail(
+ records: readonly LoadedReplayRecord[],
+ loadPayload: (record: LoadedReplayRecord) => Promise,
+): Promise {
+ let boundaryIndex = -1;
+ for (let index = 0; index < records.length; index++) {
+ if (isRecoveredCommitBarrier(await loadPayload(records[index]))) {
+ boundaryIndex = index;
+ }
+ }
+ if (boundaryIndex === records.length - 1) return undefined;
+ return {
+ startSequence: records[boundaryIndex + 1].frameSequence,
+ tipSequence: records[records.length - 1].frameSequence,
+ predecessorSequence:
+ boundaryIndex < 0 ? undefined : records[boundaryIndex].frameSequence,
+ };
+}
+
+function isRecoveredCommitBarrier(payload: Uint8Array): boolean {
+ try {
+ const frame = decodeQwpFrame(payload);
+ if ((frame.flags & QWP_FLAG_DEFER_COMMIT) !== 0) return false;
+ // A durable-ACK poll is side-effect-free and cannot cover deferred data
+ // before it. Treat an exact poll as transparent during the recovery scan.
+ if (
+ frame.flags === QWP_FLAG_DURABLE_ACK_POLL &&
+ frame.tableCount === 0 &&
+ frame.payloadLength === 0
+ ) {
+ return false;
+ }
+ return true;
+ } catch {
+ // Opaque low-level payloads and malformed QWP records are never silently
+ // retired. They remain replay barriers and preserve the existing behavior.
+ return true;
+ }
+}
+
+async function recoverSymbolDictionary(
+ records: readonly LoadedReplayRecord[],
+ loadPayload: (record: LoadedReplayRecord) => Promise,
+ persistedDictionary: readonly string[],
+ discardTail: RecoveredDiscardTail | undefined,
+ store: QwpIngressReplayStore,
+ persistedDictionaryFailure?: unknown,
+): Promise {
+ const hasDictionaryPersistence =
+ store.loadSymbolDictionary !== undefined &&
+ store.appendSymbolDictionary !== undefined;
+ let recoveredFromPersisted = true;
+ let dictionary: string[];
+ try {
+ dictionary = await reconstructSymbolDictionary(
+ records,
+ loadPayload,
+ persistedDictionary,
+ discardTail,
+ hasDictionaryPersistence,
+ persistedDictionaryFailure,
+ );
+ } catch (error) {
+ if (!store.replaceSymbolDictionary || persistedDictionary.length === 0) {
+ throw error;
+ }
+ // A structurally valid sidecar can still belong to an older dictionary
+ // generation. Only discard it when the committed frames independently
+ // reconstruct a complete dense dictionary from ID zero.
+ dictionary = await reconstructSymbolDictionary(
+ records,
+ loadPayload,
+ [],
+ discardTail,
+ hasDictionaryPersistence,
+ error,
+ );
+ recoveredFromPersisted = false;
+ }
+ const replacePersistedDictionary =
+ persistedDictionaryFailure !== undefined || !recoveredFromPersisted;
+ if (replacePersistedDictionary) {
+ try {
+ await store.replaceSymbolDictionary!(dictionary);
+ } catch (error) {
+ throw new QwpReplayDictionaryError(
+ "could not replace the unusable QWP symbol dictionary from surviving frame deltas",
+ error,
+ );
+ }
+ } else if (dictionary.length > persistedDictionary.length) {
+ try {
+ await store.appendSymbolDictionary!(
+ persistedDictionary.length,
+ dictionary.slice(persistedDictionary.length),
+ );
+ } catch (error) {
+ throw new QwpReplayDictionaryError(
+ "could not heal the recovered QWP symbol dictionary from surviving frame deltas",
+ error,
+ );
+ }
+ }
+ return dictionary;
+}
+
+async function reconstructSymbolDictionary(
+ records: readonly LoadedReplayRecord[],
+ loadPayload: (record: LoadedReplayRecord) => Promise,
+ baseline: readonly string[],
+ discardTail: RecoveredDiscardTail | undefined,
+ hasDictionaryPersistence: boolean,
+ recoveryCause?: unknown,
+): Promise {
+ const dictionary = [...baseline];
+ const dictionaryIds = new Map(dictionary.map((entry, id) => [entry, id]));
+ for (const record of records) {
+ // A wholly deferred recovery tail is retired locally and never replayed.
+ // Its dictionary additions therefore cannot make a committed prefix safe.
+ if (
+ discardTail !== undefined &&
+ record.frameSequence >= discardTail.startSequence
+ ) {
+ break;
+ }
+ let delta: ReturnType;
+ try {
+ delta = readSymbolDictionaryDelta(await loadPayload(record));
+ } catch (error) {
+ throw new QwpUnrecoverableReplayDictionaryError(
+ `persisted QWP frame contains an invalid symbol dictionary delta [sequence=${record.frameSequence}]`,
+ recoveryCause ?? error,
+ );
+ }
+ if (!delta) continue;
+ if (!hasDictionaryPersistence) {
+ throw new QwpUnrecoverableReplayDictionaryError(
+ "persisted QWP delta frames require a replay store with dictionary persistence",
+ );
+ }
+ if (delta.startId > dictionary.length) {
+ throw new QwpUnrecoverableReplayDictionaryError(
+ `persisted QWP frame references a symbol dictionary gap that cannot be reconstructed [startId=${delta.startId}, dictionarySize=${dictionary.length}]`,
+ recoveryCause,
+ );
+ }
+ delta.entries.forEach((entry, index) => {
+ const id = delta.startId + index;
+ const existing = dictionary[id];
+ if (existing !== undefined && existing !== entry) {
+ throw new QwpUnrecoverableReplayDictionaryError(
+ `persisted QWP frame conflicts with symbol dictionary at ID ${id}`,
+ recoveryCause,
+ );
+ }
+ if (id === dictionary.length) {
+ const duplicateId = dictionaryIds.get(entry);
+ if (duplicateId !== undefined) {
+ throw new QwpUnrecoverableReplayDictionaryError(
+ `persisted QWP frame assigns symbol dictionary value ${JSON.stringify(entry)} to both ID ${duplicateId} and ID ${id}`,
+ recoveryCause,
+ );
+ }
+ dictionary.push(entry);
+ dictionaryIds.set(entry, id);
+ }
+ });
+ }
+ return dictionary;
+}
+
+function dictionaryCatchupFrames(
+ entries: readonly string[],
+ maxBatchSizeBytes?: number,
+): Uint8Array[] {
+ if (entries.length === 0) return [];
+ if (maxBatchSizeBytes === undefined) {
+ return [encodeQwpIngressSymbolDictionaryFrame(0, entries)];
+ }
+ const result: Uint8Array[] = [];
+ let startId = 0;
+ while (startId < entries.length) {
+ let count = 0;
+ let entriesSize = 0;
+ while (startId + count < entries.length) {
+ const entryLength = utf8Length(entries[startId + count]);
+ const nextEntriesSize =
+ entriesSize + qwpVarintSize(entryLength) + entryLength;
+ const nextCount = count + 1;
+ const size =
+ QWP_HEADER_SIZE +
+ qwpVarintSize(startId) +
+ qwpVarintSize(nextCount) +
+ nextEntriesSize;
+ if (size > maxBatchSizeBytes) break;
+ count = nextCount;
+ entriesSize = nextEntriesSize;
+ }
+ if (count === 0) {
+ const entryLength = utf8Length(entries[startId]);
+ const frameLength =
+ QWP_HEADER_SIZE +
+ qwpVarintSize(startId) +
+ qwpVarintSize(1) +
+ qwpVarintSize(entryLength) +
+ entryLength;
+ throw new QwpCatchUpCapGapError(startId, frameLength, maxBatchSizeBytes);
+ }
+ result.push(
+ encodeQwpIngressSymbolDictionaryFrame(
+ startId,
+ entries.slice(startId, startId + count),
+ ),
+ );
+ startId += count;
+ }
+ return result;
+}
+
+function minimumDefined(
+ first: number | undefined,
+ second: number | undefined,
+): number | undefined {
+ return first === undefined
+ ? second
+ : second === undefined
+ ? first
+ : Math.min(first, second);
+}
+
+function monotonicNowMs(): number {
+ return typeof performance === "undefined" ? Date.now() : performance.now();
+}
+
+function validateReconnectPolicy(
+ maxAttempts: number,
+ initialBackoffMs: number,
+ maxBackoffMs: number,
+ maxDurationMs: number,
+ maxFrameRejections: number,
+ poisonMinEscalationWindowMs: number,
+ catchUpCapGapMinEscalationWindowMs: number,
+): void {
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 0) {
+ throw new RangeError(
+ "reconnect maxAttempts must be a non-negative safe integer",
+ );
+ }
+ for (const [name, value] of [
+ ["initialBackoffMs", initialBackoffMs],
+ ["maxBackoffMs", maxBackoffMs],
+ ["maxDurationMs", maxDurationMs],
+ ["poisonMinEscalationWindowMs", poisonMinEscalationWindowMs],
+ ["catchUpCapGapMinEscalationWindowMs", catchUpCapGapMinEscalationWindowMs],
+ ] as const) {
+ if (!Number.isFinite(value) || value < 0) {
+ throw new RangeError(
+ `reconnect ${name} must be a non-negative finite number`,
+ );
+ }
+ }
+ if (maxBackoffMs < initialBackoffMs) {
+ throw new RangeError(
+ "reconnect maxBackoffMs must be greater than or equal to initialBackoffMs",
+ );
+ }
+ if (!Number.isSafeInteger(maxFrameRejections) || maxFrameRejections < 1) {
+ throw new RangeError(
+ "reconnect maxFrameRejections must be a positive safe integer",
+ );
+ }
+}
+
+function isRetryableReconnectError(error: unknown): boolean {
+ if (error instanceof QwpUpgradeError) return error.retryable !== false;
+ if (error instanceof QwpFailoverError) {
+ return error.attempts.some((attempt) =>
+ isRetryableReconnectError(attempt.error),
+ );
+ }
+ if (
+ error instanceof QwpReplayRejectedError ||
+ error instanceof QwpProtocolError
+ ) {
+ return false;
+ }
+ // Replay-store errors are declared in the Node-only layer, so the journal's
+ // own verdict -- structural corruption, or a slot lock another process took
+ // over -- is read structurally through the `retryable` flag those classes
+ // carry. Every reconnect/replay path must honour the same verdict.
+ return (
+ (error as { retryable?: unknown } | null | undefined)?.retryable !== false
+ );
+}
+
+function isEndpointPolicyFailure(error: unknown): boolean {
+ if (error instanceof QwpUpgradeError) return true;
+ return (
+ error instanceof QwpFailoverError &&
+ error.attempts.some((attempt) => isEndpointPolicyFailure(attempt.error))
+ );
+}
+
+/** Returns a durable-ACK gap only when it accounts for the whole failure. */
+function durableAckUnavailableCause(
+ error: unknown,
+): QwpDurableAckUnavailableError | undefined {
+ if (error instanceof QwpDurableAckUnavailableError) return error;
+ if (!(error instanceof QwpFailoverError) || error.attempts.length === 0) {
+ return undefined;
+ }
+ let cause: QwpDurableAckUnavailableError | undefined;
+ for (const attempt of error.attempts) {
+ const attemptCause = durableAckUnavailableCause(attempt.error);
+ if (!attemptCause) return undefined;
+ cause ??= attemptCause;
+ }
+ return cause;
+}
+
+function isPrimaryUnavailableError(error: unknown): boolean {
+ if (error instanceof QwpUpgradeError) {
+ return error.kind === QWP_UPGRADE_ERROR_KIND.ROLE_REJECTED;
+ }
+ return (
+ error instanceof QwpFailoverError &&
+ error.attempts.length > 0 &&
+ error.attempts.every((attempt) => isPrimaryUnavailableError(attempt.error))
+ );
+}
+
+function reconnectDelayMs(error: unknown): number {
+ return error instanceof RetriableIngressNackError ||
+ error instanceof RetriableIngressConnectionError
+ ? error.retryDelayMs
+ : 0;
+}
+
+function isRetriableIngressStatus(status: number): boolean {
+ return (
+ status !== QWP_STATUS.SCHEMA_MISMATCH &&
+ status !== QWP_STATUS.PARSE_ERROR &&
+ status !== QWP_STATUS.SECURITY_ERROR
+ );
+}
+
+function cappedExponentialBackoff(
+ initialMs: number,
+ maximumMs: number,
+ exponent: number,
+): number {
+ if (initialMs === 0 || maximumMs === 0) return 0;
+ return Math.min(initialMs * 2 ** Math.min(exponent, 52), maximumMs);
+}
+
+function findLastClientFrame(
+ frames: readonly ReplayFrame[],
+): ReplayFrame | undefined {
+ for (let index = frames.length - 1; index >= 0; index--) {
+ if (frames[index].clientSequence !== undefined) return frames[index];
+ }
+ return undefined;
+}
+
+function rewriteResponseSequence(
+ payload: Uint8Array,
+ sequence: bigint,
+): Uint8Array {
+ const translated = payload.slice();
+ new DataView(
+ translated.buffer,
+ translated.byteOffset,
+ translated.byteLength,
+ ).setBigUint64(1, sequence, true);
+ return translated;
+}
+
+function areTargetsCovered(
+ targets: ReadonlyMap,
+ watermarks: ReadonlyMap,
+): boolean {
+ for (const [table, target] of targets) {
+ const watermark = watermarks.get(table);
+ if (watermark === undefined || watermark < target) return false;
+ }
+ return true;
+}
diff --git a/packages/client-core/src/_qwp/_internal/safe-callback.ts b/packages/client-core/src/_qwp/_internal/safe-callback.ts
new file mode 100644
index 0000000..69f4eda
--- /dev/null
+++ b/packages/client-core/src/_qwp/_internal/safe-callback.ts
@@ -0,0 +1,60 @@
+/**
+ * Containment for user-supplied observability callbacks.
+ *
+ * Notification callbacks (reconnect events, sender errors, recovery reports)
+ * run purely for their side effects and must never interfere with protocol
+ * progress. A synchronous throw is easy to contain with try/catch, but an
+ * `async` callback returns a promise: if it rejects, the rejection escapes the
+ * surrounding try/catch and Node treats it as an unhandled rejection, which
+ * terminates the host process by default (Node >= 15). This helper contains
+ * both failure modes so a broken callback can never crash the client's host or
+ * stall protocol work.
+ */
+
+/**
+ * Invokes an observability callback without letting a synchronous throw or a
+ * rejected promise (from an `async` callback) escape. On either failure the
+ * optional {@link onFailure} handler runs; it is itself guarded so it can never
+ * re-escape the containment it backs.
+ */
+export function safelyInvoke(
+ callback: ((event: T) => unknown) | undefined,
+ event: T,
+ onFailure?: (error: unknown) => void,
+): void {
+ if (!callback) return;
+ try {
+ const result = callback(event);
+ if (isPromiseLike(result)) {
+ void result.then(undefined, (error) => reportFailure(onFailure, error));
+ }
+ } catch (error) {
+ reportFailure(onFailure, error);
+ }
+}
+
+function reportFailure(
+ onFailure: ((error: unknown) => void) | undefined,
+ error: unknown,
+): void {
+ if (!onFailure) return;
+ try {
+ onFailure(error);
+ } catch {
+ // A failing fallback must not re-escape the containment it backs.
+ }
+}
+
+/**
+ * Minimal Promises/A+ thenable test. A genuine thenable only guarantees a
+ * `then` method, so `then(undefined, onRejected)` — not `catch` — is the
+ * portable way to attach a rejection handler.
+ */
+export function isPromiseLike(value: unknown): value is PromiseLike {
+ return (
+ value !== null &&
+ (typeof value === "object" || typeof value === "function") &&
+ "then" in value &&
+ typeof value.then === "function"
+ );
+}
diff --git a/packages/client-core/src/_qwp/_internal/websocket-connection.ts b/packages/client-core/src/_qwp/_internal/websocket-connection.ts
new file mode 100644
index 0000000..206b6f8
--- /dev/null
+++ b/packages/client-core/src/_qwp/_internal/websocket-connection.ts
@@ -0,0 +1,623 @@
+import { QwpProtocolError } from "../_core";
+import {
+ QWP_UPGRADE_ERROR_KIND,
+ QWP_UPGRADE_TIMEOUT_PHASE,
+ QwpBinaryConnection,
+ QwpConnectionCloseInfo,
+ QwpHandshakeMetadata,
+ QwpSendClosedError,
+ QwpSendError,
+ QwpSendTimeoutError,
+ QwpUpgradeError,
+} from "../transport";
+import { QwpAsyncQueue } from "./async-queue";
+
+interface QwpWebSocketMessageEvent {
+ data: unknown;
+}
+
+interface QwpWebSocketCloseEvent {
+ code?: number;
+ reason?: string;
+ wasClean?: boolean;
+}
+
+export interface QwpWebSocketLike {
+ binaryType: string;
+ readonly readyState: number;
+ /** WebSocket subprotocol selected by the server, or an empty string. */
+ readonly protocol?: string;
+ /** Number of application bytes queued by WHATWG-compatible WebSockets. */
+ readonly bufferedAmount?: number;
+ send(data: Uint8Array): void;
+ /** Node adapter hook for the `ws.send(data, callback)` completion signal. */
+ sendWithCallback?(data: Uint8Array, callback: (error?: Error) => void): void;
+ /** Node WebSocket implementations may expose control-frame PING. */
+ ping?(): void;
+ /** Node WebSocket implementations may support immediate termination. */
+ terminate?(): void;
+ close(code?: number, reason?: string): void;
+ addEventListener(
+ type: "open",
+ listener: (event: unknown) => void,
+ options?: { once?: boolean },
+ ): void;
+ addEventListener(
+ type: "message",
+ listener: (event: QwpWebSocketMessageEvent) => void,
+ ): void;
+ addEventListener(
+ type: "error",
+ listener: (event: unknown) => void,
+ options?: { once?: boolean },
+ ): void;
+ addEventListener(
+ type: "close",
+ listener: (event: QwpWebSocketCloseEvent) => void,
+ options?: { once?: boolean },
+ ): void;
+ /** Optional cleanup hook implemented by browser WebSocket and Node `ws`. */
+ removeEventListener?(type: "open", listener: (event: unknown) => void): void;
+ removeEventListener?(
+ type: "message",
+ listener: (event: QwpWebSocketMessageEvent) => void,
+ ): void;
+ removeEventListener?(type: "error", listener: (event: unknown) => void): void;
+ removeEventListener?(
+ type: "close",
+ listener: (event: QwpWebSocketCloseEvent) => void,
+ ): void;
+}
+
+export interface QwpWebSocketOpenOptions {
+ url: string | URL;
+ connectTimeoutMs?: number;
+ /** Node-only HTTP authentication and WebSocket upgrade deadline. */
+ authTimeoutMs?: number;
+ /** Resolves after the Node TCP/TLS transport has connected. */
+ transportConnected?: Promise;
+ sendTimeoutMs?: number;
+ closeTimeoutMs?: number;
+ completeHandshake: () => QwpHandshakeMetadata;
+ /** Node adapters use this to surface non-101 HTTP responses from `ws`. */
+ openingFailure?: Promise;
+ /** Browsers hide the HTTP response behind a generic WebSocket error event. */
+ opaqueErrors?: boolean;
+ /**
+ * Tears the pending upgrade down immediately. Without it a close() issued
+ * while the peer has accepted the TCP connection but not answered the
+ * upgrade leaves the socket and its deadline alive until that deadline
+ * fires, which keeps the Node event loop open long after close() resolved.
+ */
+ signal?: AbortSignal;
+}
+
+const WEBSOCKET_OPEN = 1;
+const WEBSOCKET_CLOSED = 3;
+const BUFFERED_AMOUNT_POLL_MS = 4;
+const DEFAULT_TIMEOUT_MS = 15_000;
+
+export function validateQwpWebSocketTimeouts(options: {
+ connectTimeoutMs?: number;
+ authTimeoutMs?: number;
+ sendTimeoutMs?: number;
+ closeTimeoutMs?: number;
+}): void {
+ for (const [name, value] of [
+ ["connectTimeoutMs", options.connectTimeoutMs],
+ ["authTimeoutMs", options.authTimeoutMs],
+ ["sendTimeoutMs", options.sendTimeoutMs],
+ ["closeTimeoutMs", options.closeTimeoutMs],
+ ] as const) {
+ if (value !== undefined && (!Number.isFinite(value) || value <= 0)) {
+ throw new RangeError(`${name} must be a positive finite number`);
+ }
+ }
+}
+
+async function normalizeBinaryMessage(data: unknown): Promise {
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
+ if (ArrayBuffer.isView(data)) {
+ return new Uint8Array(
+ data.buffer,
+ data.byteOffset,
+ data.byteLength,
+ ).slice();
+ }
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
+ return new Uint8Array(await data.arrayBuffer());
+ }
+ throw new QwpProtocolError("QWP WebSocket received a non-binary message");
+}
+
+/** Wraps a WHATWG-style WebSocket and resolves once its opening handshake succeeds. */
+/** Absorbs a socket `error` raised before the real listeners are attached. */
+const ignoreSocketError = (): void => undefined;
+
+export function openQwpWebSocket(
+ socket: QwpWebSocketLike,
+ options: QwpWebSocketOpenOptions,
+): Promise {
+ try {
+ validateQwpWebSocketTimeouts(options);
+ } catch (error) {
+ try {
+ // Tearing down a CONNECTING socket makes `ws` emit `error`, and nothing
+ // has subscribed to this one yet. Absorb it rather than let an
+ // EventEmitter with no listener rethrow it into the process.
+ socket.addEventListener("error", ignoreSocketError);
+ if (socket.terminate) socket.terminate();
+ else if (socket.readyState !== WEBSOCKET_CLOSED) socket.close();
+ } catch {
+ // Configuration validation remains authoritative.
+ }
+ return Promise.reject(error);
+ }
+ const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_TIMEOUT_MS;
+ // Opening a connection is two deadlines: connectTimeoutMs covers the TCP/TLS
+ // transport, and authTimeoutMs takes over for the upgrade and authentication
+ // exchange the moment transportConnected resolves. A caller who narrows only
+ // the first is bounding how long establishing one connection may take, and
+ // the upgrade is part of that -- inheriting keeps an explicit 200 ms from
+ // being exceeded 75x by a default nobody chose, which is what a peer that
+ // accepts TCP and never answers the upgrade used to cost. Setting
+ // authTimeoutMs restores an independent budget for the slower phase.
+ const authTimeoutMs =
+ options.authTimeoutMs ?? options.connectTimeoutMs ?? DEFAULT_TIMEOUT_MS;
+ const sendTimeoutMs = options.sendTimeoutMs ?? DEFAULT_TIMEOUT_MS;
+ const closeTimeoutMs = options.closeTimeoutMs ?? DEFAULT_TIMEOUT_MS;
+
+ const messages = new QwpAsyncQueue();
+ let resolveClosed!: (info: QwpConnectionCloseInfo) => void;
+ const closed = new Promise((resolve) => {
+ resolveClosed = resolve;
+ });
+ let opened = false;
+ let openingSettled = false;
+ let messageTail: Promise = Promise.resolve();
+ let sendTail: Promise = Promise.resolve();
+ let terminalSendError: QwpSendError | undefined;
+ let rejectActiveSend: ((error: QwpSendError) => void) | undefined;
+ let closeSettled = false;
+ let closeTask: Promise | undefined;
+ let cleanupTask: Promise = Promise.resolve();
+ let removeSocketListeners = (): void => undefined;
+
+ const failSends = (error: QwpSendError): QwpSendError => {
+ terminalSendError ??= error;
+ rejectActiveSend?.(terminalSendError);
+ return terminalSendError;
+ };
+
+ const settleClosed = (info: QwpConnectionCloseInfo): void => {
+ if (closeSettled) return;
+ closeSettled = true;
+ resolveClosed(info);
+ if (opened) failSends(new QwpSendClosedError(info));
+ removeSocketListeners();
+ cleanupTask = (async () => {
+ let timer: ReturnType | undefined;
+ const timedOut = await Promise.race([
+ messageTail.then(
+ () => false,
+ () => false,
+ ),
+ new Promise((resolve) => {
+ timer = setTimeout(() => resolve(true), closeTimeoutMs);
+ }),
+ ]);
+ if (timer) clearTimeout(timer);
+ if (timedOut) messageTail = Promise.resolve();
+ messages.end();
+ })();
+ };
+
+ const closeSocket = (code = 1000, reason = ""): Promise => {
+ if (closeTask) return closeTask;
+ closeTask = (async () => {
+ const requestedInfo: QwpConnectionCloseInfo = {
+ code,
+ reason,
+ wasClean: code === 1000,
+ };
+ if (opened) failSends(new QwpSendClosedError(requestedInfo));
+ if (socket.readyState === WEBSOCKET_CLOSED) {
+ settleClosed(requestedInfo);
+ } else {
+ try {
+ socket.close(code, reason);
+ } catch {
+ try {
+ socket.terminate?.();
+ } catch {
+ // The synthetic close below still releases local resources.
+ }
+ settleClosed({
+ code: 1006,
+ reason: "QWP WebSocket close failed",
+ wasClean: false,
+ });
+ }
+ }
+ if (!closeSettled) {
+ let timer: ReturnType | undefined;
+ const timedOut = await Promise.race([
+ closed.then(() => false),
+ new Promise((resolve) => {
+ timer = setTimeout(() => resolve(true), closeTimeoutMs);
+ }),
+ ]);
+ if (timer) clearTimeout(timer);
+ if (timedOut && !closeSettled) {
+ try {
+ socket.terminate?.();
+ } catch {
+ // Local state must still settle when forced termination throws.
+ }
+ settleClosed({
+ code: 1006,
+ reason: `QWP WebSocket close timed out after ${closeTimeoutMs}ms`,
+ wasClean: false,
+ });
+ }
+ }
+ await cleanupTask;
+ })();
+ return closeTask;
+ };
+
+ const abortAfterSendFailure = (): void => {
+ void closeSocket(1011, "QWP send failed");
+ };
+
+ const sendWithBackpressure = (payload: Uint8Array): Promise => {
+ if (terminalSendError) return Promise.reject(terminalSendError);
+ if (socket.readyState !== WEBSOCKET_OPEN) {
+ return Promise.reject(failSends(new QwpSendClosedError()));
+ }
+
+ return new Promise((resolveSend, rejectSend) => {
+ let settled = false;
+ let drainPoll: ReturnType | undefined;
+
+ const settle = (error?: QwpSendError): void => {
+ if (settled) return;
+ settled = true;
+ if (drainPoll) clearTimeout(drainPoll);
+ clearTimeout(sendTimeout);
+ if (rejectActiveSend === rejectPending) rejectActiveSend = undefined;
+ if (error) rejectSend(error);
+ else resolveSend();
+ };
+ const rejectPending = (error: QwpSendError): void => settle(error);
+ const failSend = (error: QwpSendError): void => {
+ settle(failSends(error));
+ abortAfterSendFailure();
+ };
+
+ rejectActiveSend = rejectPending;
+ const sendTimeout = setTimeout(() => {
+ const bufferedAmount = socket.bufferedAmount;
+ failSend(
+ new QwpSendTimeoutError(
+ sendTimeoutMs,
+ typeof bufferedAmount === "number" ? bufferedAmount : undefined,
+ ),
+ );
+ }, sendTimeoutMs);
+
+ if (socket.sendWithCallback) {
+ try {
+ socket.sendWithCallback(payload, (error) => {
+ if (error) {
+ failSend(
+ new QwpSendError(
+ "QWP WebSocket send failed; delivery outcome is unknown",
+ error,
+ ),
+ );
+ } else {
+ settle();
+ }
+ });
+ } catch (error) {
+ failSend(
+ new QwpSendError(
+ "QWP WebSocket send failed before it could be queued",
+ error,
+ ),
+ );
+ }
+ return;
+ }
+
+ const initialBufferedAmount = socket.bufferedAmount;
+ try {
+ socket.send(payload);
+ } catch (error) {
+ failSend(
+ new QwpSendError(
+ "QWP WebSocket send failed before it could be queued",
+ error,
+ ),
+ );
+ return;
+ }
+
+ if (typeof initialBufferedAmount !== "number") {
+ // Backwards compatibility for custom adapters without a drain signal.
+ settle();
+ return;
+ }
+
+ const waitForDrain = (): void => {
+ if (socket.readyState !== WEBSOCKET_OPEN) {
+ settle(failSends(new QwpSendClosedError()));
+ return;
+ }
+ if (
+ typeof socket.bufferedAmount !== "number" ||
+ socket.bufferedAmount <= initialBufferedAmount
+ ) {
+ settle();
+ return;
+ }
+ drainPoll = setTimeout(waitForDrain, BUFFERED_AMOUNT_POLL_MS);
+ };
+ waitForDrain();
+ });
+ };
+
+ return new Promise((resolve, reject) => {
+ let timeout: ReturnType | undefined;
+
+ const armOpeningTimeout = (
+ timeoutMs: number,
+ phase?: "connect" | "authentication",
+ ): void => {
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(() => {
+ const message =
+ phase === QWP_UPGRADE_TIMEOUT_PHASE.CONNECT
+ ? `QWP TCP/TLS connection timed out after ${timeoutMs}ms`
+ : phase === QWP_UPGRADE_TIMEOUT_PHASE.AUTHENTICATION
+ ? `QWP authentication/WebSocket upgrade timed out after ${timeoutMs}ms`
+ : `QWP WebSocket connection timed out after ${timeoutMs}ms`;
+ failOpening(
+ new QwpUpgradeError(message, {
+ kind: QWP_UPGRADE_ERROR_KIND.TIMEOUT,
+ retryable: true,
+ tryNextEndpoint: true,
+ url: options.url,
+ timeoutPhase: phase,
+ }),
+ 1000,
+ "QWP connection timeout",
+ );
+ }, timeoutMs);
+ };
+
+ const failOpening = (
+ error: Error,
+ closeCode = 1000,
+ closeReason = "QWP upgrade failed",
+ ): void => {
+ if (openingSettled) return;
+ openingSettled = true;
+ if (timeout) clearTimeout(timeout);
+ options.signal?.removeEventListener("abort", abortOpening);
+ void closeSocket(closeCode, closeReason);
+ reject(error);
+ };
+
+ const abortOpening = (): void => {
+ failOpening(
+ new QwpSendClosedError(),
+ 1000,
+ "QWP connection closed while connecting",
+ );
+ };
+ // Aborting closes the socket, and closing a CONNECTING `ws` socket makes it
+ // emit `error` on the next tick. This executor attaches the socket's
+ // listeners last, so acting on an already-aborted signal here would leave
+ // that event unhandled and terminate the process. A failover sweep hands
+ // the same signal to every remaining endpoint after close() aborts it, so
+ // this is the ordinary shape for a multi-address client, not a rare race.
+ // Record the abort and apply it once the listeners are in place.
+ let abortedBeforeListening = false;
+ if (options.signal) {
+ if (options.signal.aborted) {
+ abortedBeforeListening = true;
+ } else {
+ options.signal.addEventListener("abort", abortOpening, { once: true });
+ }
+ }
+
+ armOpeningTimeout(
+ connectTimeoutMs,
+ options.transportConnected
+ ? QWP_UPGRADE_TIMEOUT_PHASE.CONNECT
+ : undefined,
+ );
+ void options.transportConnected?.then(
+ () => {
+ if (openingSettled) return;
+ armOpeningTimeout(
+ authTimeoutMs,
+ QWP_UPGRADE_TIMEOUT_PHASE.AUTHENTICATION,
+ );
+ },
+ (error: unknown) => {
+ failOpening(
+ new QwpUpgradeError(
+ "QWP TCP/TLS transport failed while establishing a connection",
+ {
+ kind: QWP_UPGRADE_ERROR_KIND.TRANSPORT,
+ retryable: true,
+ tryNextEndpoint: true,
+ url: options.url,
+ cause: error,
+ },
+ ),
+ );
+ },
+ );
+
+ const onOpen = (): void => {
+ if (openingSettled) return;
+ let handshake: QwpHandshakeMetadata;
+ try {
+ handshake = Object.freeze({ ...options.completeHandshake() });
+ } catch (error) {
+ failOpening(
+ error instanceof Error
+ ? error
+ : new Error("QWP WebSocket upgrade validation failed"),
+ 1000,
+ "QWP upgrade validation failed",
+ );
+ return;
+ }
+ openingSettled = true;
+ opened = true;
+ if (timeout) clearTimeout(timeout);
+ options.signal?.removeEventListener("abort", abortOpening);
+ const connection: QwpBinaryConnection = {
+ messages,
+ closed,
+ handshake,
+ endpoint: options.url,
+ send(payload: Uint8Array): Promise {
+ const sending = sendTail.then(() => sendWithBackpressure(payload));
+ sendTail = sending.catch(() => undefined);
+ return sending;
+ },
+ async close(code = 1000, reason = ""): Promise {
+ await closeSocket(code, reason);
+ },
+ };
+ if (socket.ping) {
+ connection.ping = async (): Promise => {
+ if (socket.readyState !== WEBSOCKET_OPEN) {
+ throw new Error("QWP WebSocket is not open");
+ }
+ socket.ping!();
+ };
+ }
+ resolve(connection);
+ };
+
+ const onMessage = (event: QwpWebSocketMessageEvent): void => {
+ if (openingSettled && !opened) return;
+ messageTail = messageTail
+ .then(async () =>
+ messages.push(await normalizeBinaryMessage(event.data)),
+ )
+ .catch((error: unknown) => {
+ messages.fail(error);
+ void closeSocket(1002, "invalid QWP payload");
+ });
+ };
+
+ options.openingFailure?.catch((error: unknown) => {
+ failOpening(
+ error instanceof Error
+ ? error
+ : new QwpUpgradeError("QWP WebSocket upgrade failed", {
+ kind: QWP_UPGRADE_ERROR_KIND.TRANSPORT,
+ retryable: true,
+ tryNextEndpoint: true,
+ url: options.url,
+ cause: error,
+ }),
+ );
+ });
+
+ const onError = (event: unknown): void => {
+ if (opened) {
+ const eventError = (event as { error?: unknown }).error;
+ failSends(
+ new QwpSendError(
+ "QWP WebSocket transport error while sending",
+ eventError ?? event,
+ ),
+ );
+ messages.fail(new Error("QWP WebSocket transport error"));
+ abortAfterSendFailure();
+ return;
+ }
+ const opaque = options.opaqueErrors === true;
+ const eventError = (event as { error?: unknown }).error;
+ const error = new QwpUpgradeError(
+ opaque
+ ? "QWP WebSocket upgrade failed; the browser did not expose the HTTP response"
+ : "QWP WebSocket transport error during upgrade",
+ {
+ kind: opaque
+ ? QWP_UPGRADE_ERROR_KIND.OPAQUE
+ : QWP_UPGRADE_ERROR_KIND.TRANSPORT,
+ retryable: opaque ? undefined : true,
+ tryNextEndpoint: opaque ? undefined : true,
+ url: options.url,
+ cause: eventError ?? event,
+ },
+ );
+ failOpening(error);
+ };
+
+ const onClose = (event: QwpWebSocketCloseEvent): void => {
+ clearTimeout(timeout);
+ const info = {
+ code: event.code ?? 1006,
+ reason: event.reason ?? "",
+ wasClean: event.wasClean ?? false,
+ };
+ settleClosed(info);
+ if (!opened) {
+ failOpening(
+ new QwpUpgradeError(
+ `QWP WebSocket closed during handshake [code=${info.code}, reason=${info.reason}]`,
+ {
+ kind: options.opaqueErrors
+ ? QWP_UPGRADE_ERROR_KIND.OPAQUE
+ : QWP_UPGRADE_ERROR_KIND.TRANSPORT,
+ retryable: options.opaqueErrors ? undefined : true,
+ tryNextEndpoint: options.opaqueErrors ? undefined : true,
+ url: options.url,
+ closeCode: info.code,
+ },
+ ),
+ );
+ return;
+ }
+ };
+
+ removeSocketListeners = (): void => {
+ try {
+ socket.removeEventListener?.("open", onOpen);
+ socket.removeEventListener?.("message", onMessage);
+ socket.removeEventListener?.("error", onError);
+ socket.removeEventListener?.("close", onClose);
+ } catch {
+ // Transport cleanup must not make connection close reject.
+ }
+ };
+ try {
+ socket.binaryType = "arraybuffer";
+ socket.addEventListener("open", onOpen, { once: true });
+ socket.addEventListener("message", onMessage);
+ socket.addEventListener("error", onError);
+ socket.addEventListener("close", onClose, { once: true });
+ } catch (error) {
+ failOpening(
+ error instanceof Error
+ ? error
+ : new Error("failed to configure QWP WebSocket listeners"),
+ );
+ }
+ // Safe now: `onError` is attached, so the close this triggers has a
+ // subscriber. A failed attachment above already settled the opening, and
+ // failOpening() is idempotent, so this is a no-op in that case.
+ if (abortedBeforeListening) abortOpening();
+ });
+}
diff --git a/packages/client-core/src/_qwp/client.ts b/packages/client-core/src/_qwp/client.ts
new file mode 100644
index 0000000..a27e592
--- /dev/null
+++ b/packages/client-core/src/_qwp/client.ts
@@ -0,0 +1,922 @@
+import {
+ QwpEgressQuery,
+ QwpEgressQueryOptions,
+ QwpEgressSession,
+ QwpEgressViewQuery,
+ QwpResultBatchViewHandler,
+} from "./egress-session";
+import { QwpSender } from "./sender";
+import { QwpHandshakeMetadata } from "./transport";
+import type {
+ QwpNegotiatedEgressCompression,
+ QwpServerInfoMessage,
+} from "./_core";
+
+const DEFAULT_POOL_MIN = 1;
+const DEFAULT_POOL_MAX = 4;
+const DEFAULT_ACQUIRE_TIMEOUT_MS = 5_000;
+const DEFAULT_IDLE_TIMEOUT_MS = 60_000;
+const DEFAULT_MAX_LIFETIME_MS = 30 * 60_000;
+const DEFAULT_HOUSEKEEPING_INTERVAL_MS = 5_000;
+const MIN_HOUSEKEEPING_INTERVAL_MS = 100;
+const MAX_CLOSE_CREATION_WAIT_MS = 5_000;
+const MAX_CLOSE_LEASE_WAIT_MS = 5_000;
+
+export interface QwpClientPoolOptions {
+ /** Warm ingress connections created by connect(). Defaults to 1. */
+ senderPoolMin?: number;
+ /** Maximum concurrently borrowed ingress senders. Defaults to 4. */
+ senderPoolMax?: number;
+ /** Warm egress connections created by connect(). Defaults to 1. */
+ queryPoolMin?: number;
+ /** Maximum concurrently borrowed query connections. Defaults to 4. */
+ queryPoolMax?: number;
+ /** Idle time before an excess pooled connection is closed. Defaults to 60s; zero disables. */
+ idleTimeoutMs?: number;
+ /** Maximum pooled connection age before recycling it while idle. Defaults to 30m; zero disables. */
+ maxLifetimeMs?: number;
+ /** Idle/lifetime sweep interval. Defaults to 5s and must be at least 100ms. */
+ housekeepingIntervalMs?: number;
+ /**
+ * Maximum wait for a returned pool slot and for leases during shutdown.
+ * The shutdown wait is capped at 5 seconds. Defaults to 5 seconds.
+ */
+ acquireTimeoutMs?: number;
+}
+
+export interface QwpClientFactories {
+ createSender(slot: number, signal?: AbortSignal): Promise;
+ createQuerySession(
+ slot: number,
+ signal?: AbortSignal,
+ ): Promise;
+ /** @internal Coordinates stable persistent sender slots with recovery. */
+ senderSlotReservation?: QwpPoolSlotReservation;
+ /** @internal Starts runtime-specific background services on first use. */
+ start?(): void | Promise;
+ /** @internal Stops runtime-specific background services during close. */
+ close?(): void | Promise;
+}
+
+/** @internal Cross-owner reservation for stable pooled sender slot indexes. */
+export interface QwpPoolSlotReservation {
+ tryReserve(slot: number): boolean;
+ release(slot: number): void;
+ onAvailable(listener: () => void): () => void;
+}
+
+export interface QwpResourcePoolMetrics {
+ readonly minimum: number;
+ readonly maximum: number;
+ readonly total: number;
+ readonly available: number;
+ readonly leased: number;
+ readonly creating: number;
+ readonly waiting: number;
+}
+
+export interface QwpClientMetrics {
+ readonly senders: QwpResourcePoolMetrics;
+ readonly queries: QwpResourcePoolMetrics;
+ readonly closing: boolean;
+ readonly closed: boolean;
+}
+
+/** A bounded QWP pool could not provide a connection before its deadline. */
+export class QwpPoolAcquireTimeoutError extends Error {
+ constructor(
+ readonly resource: "sender" | "query",
+ readonly timeoutMs: number,
+ ) {
+ super(
+ `timed out waiting for a QWP ${resource} from the pool after ${timeoutMs}ms`,
+ );
+ this.name = "QwpPoolAcquireTimeoutError";
+ }
+}
+
+/** A pooled resource failed while a new slot was being connected. */
+export class QwpPoolResourceError extends Error {
+ readonly cause: unknown;
+
+ constructor(
+ readonly resource: "sender" | "query",
+ cause: unknown,
+ ) {
+ super(
+ `failed to create pooled QWP ${resource}${
+ cause instanceof Error ? `: ${cause.message}` : ""
+ }`,
+ );
+ this.name = "QwpPoolResourceError";
+ this.cause = cause;
+ }
+}
+
+/** The owning QWP client, or one of its returned lease handles, is closed. */
+export class QwpClientClosedError extends Error {
+ constructor(message = "QWP client is closed") {
+ super(message);
+ this.name = "QwpClientClosedError";
+ }
+}
+
+interface ValidatedPoolOptions {
+ readonly senderPoolMin: number;
+ readonly senderPoolMax: number;
+ readonly queryPoolMin: number;
+ readonly queryPoolMax: number;
+ readonly acquireTimeoutMs: number;
+ readonly idleTimeoutMs: number;
+ readonly maxLifetimeMs: number;
+ readonly housekeepingIntervalMs: number;
+}
+
+interface PoolEntry {
+ readonly slot: number;
+ readonly value: T;
+ readonly createdAtMs: number;
+ idleSinceMs: number;
+ leased: boolean;
+ destroyPromise?: Promise;
+}
+
+interface PoolWaiter {
+ readonly resolve: () => void;
+ readonly reject: (error: unknown) => void;
+ readonly timer?: ReturnType;
+}
+
+interface PoolCloseWaiter {
+ readonly resolve: () => void;
+ readonly timer: ReturnType;
+}
+
+class QwpResourcePool {
+ private readonly all = new Map>();
+ private readonly available: PoolEntry[] = [];
+ private readonly creatingSlots = new Set();
+ private readonly destroyingSlots = new Set();
+ private readonly creationOperations = new Set>();
+ private readonly creationAbortControllers = new Set();
+ private readonly waiters = new Set();
+ private readonly closeWaiters = new Set();
+ private readonly reservedSlots = new Set();
+ private readonly unsubscribeSlotAvailability?: () => void;
+ private closePromise?: Promise;
+ private pendingLeaseTeardowns = 0;
+ private closed = false;
+
+ constructor(
+ private readonly resource: "sender" | "query",
+ private readonly minimum: number,
+ private readonly maximum: number,
+ private readonly acquireTimeoutMs: number,
+ private readonly idleTimeoutMs: number,
+ private readonly maxLifetimeMs: number,
+ private readonly createResource: (
+ slot: number,
+ signal: AbortSignal,
+ ) => Promise,
+ private readonly destroyResource: (resource: T) => Promise,
+ private readonly closeLeasedOnShutdown = false,
+ private readonly slotReservation?: QwpPoolSlotReservation,
+ ) {
+ this.unsubscribeSlotAvailability = slotReservation?.onAvailable(() =>
+ this.wakeWaiters(),
+ );
+ }
+
+ get metrics(): QwpResourcePoolMetrics {
+ return Object.freeze({
+ minimum: this.minimum,
+ maximum: this.maximum,
+ total: this.all.size,
+ available: this.available.length,
+ leased: Array.from(this.all.values()).filter((entry) => entry.leased)
+ .length,
+ creating: this.creatingSlots.size,
+ waiting: this.waiters.size,
+ });
+ }
+
+ async prewarm(): Promise {
+ const needed = Math.max(
+ 0,
+ this.minimum - this.all.size - this.creatingSlots.size,
+ );
+ const acquired = await Promise.allSettled(
+ Array.from({ length: needed }, () => this.acquire()),
+ );
+ await Promise.all(
+ acquired.map((result) =>
+ result.status === "fulfilled"
+ ? this.release(result.value, true)
+ : Promise.resolve(),
+ ),
+ );
+ const failure = acquired.find(
+ (result): result is PromiseRejectedResult => result.status === "rejected",
+ );
+ if (failure) throw failure.reason;
+ }
+
+ async acquire(): Promise> {
+ const deadline = Date.now() + this.acquireTimeoutMs;
+ while (true) {
+ this.throwIfClosed();
+ const available = this.available.shift();
+ if (available) {
+ available.leased = true;
+ return available;
+ }
+ const slot = this.reserveSlot();
+ if (slot !== undefined) return this.createLeased(slot);
+ const remaining = deadline - Date.now();
+ if (remaining <= 0) {
+ throw new QwpPoolAcquireTimeoutError(
+ this.resource,
+ this.acquireTimeoutMs,
+ );
+ }
+ await this.waitForChange(remaining);
+ }
+ }
+
+ async release(entry: PoolEntry
HTTP transport implementation using Node.js built-in http/https modules.
--Supports both HTTP and HTTPS protocols with configurable authentication.