Skip to content

v2.19.0

Choose a tag to compare

@mariuz mariuz released this 20 Oct 20:19
· 11 commits to master since this release

Minor release that adds a configurable fetchSize option (connection-level, URI query parameter, and per-query) and fixes two bugs in the result-fetch and response-delivery paths:

  • Stack overflow in fetchAll with large batches: doSynchronousLoop now runs as an iterative trampoline instead of recursing on every synchronous next() call.
  • Swallowed RangeError from user callbacks: decodeResponse and the socket 'data' handler no longer mistake a RangeError thrown by consumer callback code for a truncated wire packet.
  • New fetchSize option (#313): Configure how many rows are requested per op_fetch round-trip (default 200, max 65535).

The default behavior (fetchSize: 200) is unchanged.


Key Changes

1. Configurable fetchSize option (#313, #443)

Rows were previously always fetched 200 per op_fetch, with each batch requiring a network round-trip. On high-latency links, large result sets spent most of their time waiting for the next batch (e.g., fetching 20,000 rows over a 50 ms RTT link drops from 5.35 s at 200 to 0.60 s at 5000).

fetchSize can be configured in three ways:

  • Connection option: options.fetchSize = 5000
  • Connection URI: firebird://localhost/employee?fetchSize=5000
  • Per-query option: db.query(sql, params, cb, { fetchSize: 5000 })

It applies to query, execute, sequentially, queryStream, and connection.fetch() when no explicit count is passed. Non-positive or non-integer values fall back to 200, and values above 65535 are clamped to 65535 (p_sqldata_messages is an unsigned 16-bit wire field).

2. Iterative trampoline in doSynchronousLoop (#443)

fetchAll hands each fetched batch to doSynchronousLoop, which previously invoked the per-row callback recursively. With batches of a few thousand rows (or a deep per-row consumer), synchronous next() calls grew the call stack until RangeError: Maximum call stack size exceeded was thrown.

doSynchronousLoop now runs as a while loop trampoline:

  • A synchronous next() marks the current row as handled and advances the loop in constant stack space.
  • An asynchronous next() exits the loop and resumes when next() is called.
  • Each row receives its own guarded next() so duplicate or stale calls cannot skip rows or restart the loop after an error.

3. Consumer RangeError isolation in decodeResponse and socket 'data' handler (#443)

Previously, decodeResponse and the socket 'data' handler treated any RangeError as an incomplete packet ("wait for more data"). Because the result callback chain runs synchronously inside both, a RangeError thrown by user code (such as new Date(NaN).toISOString()) was silently swallowed, and the already-consumed bytes were re-queued for decoding on the next 'data' event—corrupting protocol state or hanging the query.

  • decodeResponse now tracks whether the response has been delivered to cb and only treats a RangeError as an incomplete packet prior to delivery.
  • The socket 'data' handler catches exceptions thrown by the dequeued response callback, continues decoding any remaining responses in the current buffer, and rethrows the callback error via process.nextTick.

Pull Requests

  • #443 fix(fetch): stack overflow and swallowed RangeErrors in fetchAll; add fetchSize option — @JohnnyBill
  • #442 chore(deps-dev): bump source-map-js from 1.2.1 to 1.2.2 — @dependabot

Installation

npm install node-firebird@2.19.0

Full Changelog: v2.18.2...v2.19.0