Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions foreign/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ new session.

The client pings every `heartbeatInterval` milliseconds, 5000 by default, which
keeps an idle session alive when the server's `[heartbeat]` eviction is enabled.
`heartbeatInterval` also accepts a duration expression such as `"10s"` or
`"1h 30m"`, like the Rust SDK.
The server evicts a connection silent for 36 s, which is 1.2 x its 30 s
heartbeat interval. Raising the client interval past that window, or setting it
to 0 to disable client heartbeats, exposes an idle consumer-group member to
Expand Down Expand Up @@ -105,6 +107,43 @@ const client = new Client({
const stats = await client.system.getStats();
```

### Connection strings

Every client constructor also accepts a connection string instead of a config
Comment thread
T1B0 marked this conversation as resolved.
object:

```ts
import { Client } from "apache-iggy";

const client = new Client("iggy://iggy:iggy@127.0.0.1:8090");
const stats = await client.system.getStats();
```

Supported schemes are `iggy://` (TCP, default) and `iggy+tcp://`. Credentials
are `username:password` or a single personal access token. Options mirror the
other SDKs: `tls`, `tls_domain`, `tls_ca_file`, `reconnection_retries`,
`reconnection_interval`, `heartbeat_interval` and `nodelay`. `reestablish_after`
is accepted for format compatibility but has no Node equivalent.

`SimpleClient` does not accept a connection string: it wraps an existing
`RawClient` instance rather than building one from configuration. Pass the
connection string to `Client`, `SingleClient` or `getRawClient` and hand the
resulting raw client to `SimpleClient` if needed.

### option limits

| option | limit |
| --- | --- |
| `reconnection_retries` | integer up to `4294967295` (u32 max); larger values are rejected like Rust's u32 overflow, and `unlimited` maps to this ceiling. Defaults to unlimited |
| `heartbeat_interval` | duration up to `2147483647ms` (Node's largest timer delay); `0` disables heartbeats |
| `reconnection_interval` | positive duration (`ms`, `s`, `m`, `h`) up to `2147483647ms` (Node's largest timer delay); zero spellings are rejected. Defaults to `1s` |
| port in the authority | decimal up to `65535` |

Durations accept the same expressions as the Rust SDK, for example `500ms`,
`10s`, `1h 30m`, `5d`, `2w`, `1y`; matching is case-insensitive and
`0`, `unlimited`, `disabled` and `none` map to zero. Unit-less numbers such as
`5` are rejected.

## use sources

### Install
Expand Down
7 changes: 7 additions & 0 deletions foreign/node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions foreign/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@node-rs/xxhash": "1.7.7",
"debug": "4.4.3",
"generic-pool": "3.9.0",
"parse-duration": "^2.1.8",
"uuidv7": "1.2.1"
},
"devDependencies": {
Expand Down
48 changes: 48 additions & 0 deletions foreign/node/src/client/client.config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,33 @@ describe('normalizeClientConfig', () => {
);
});

it('accepts duration expressions for the heartbeat interval', () => {
assert.equal(
normalizeClientConfig({ ...config(), heartbeatInterval: '10s' })
.heartbeatInterval,
10000
);
assert.equal(
normalizeClientConfig({ ...config(), heartbeatInterval: '1h 30m' })
.heartbeatInterval,
5400000
);
for (const disabled of ['0', 'unlimited', 'disabled', 'none'])
assert.equal(
normalizeClientConfig({ ...config(), heartbeatInterval: disabled })
.heartbeatInterval,
0
);
assert.throws(
() => normalizeClientConfig({ ...config(), heartbeatInterval: '1000h' }),
/between 0 and/
);
assert.throws(
() => normalizeClientConfig({ ...config(), heartbeatInterval: 'garbage' }),
/invalid duration/
);
});

it('rejects unusable heartbeat intervals', () => {
for (const heartbeatInterval of [
-1, -5000, Number.NaN, 1.5, 2_147_483_648, 2 ** 32, Number.MAX_VALUE
Expand All @@ -74,6 +101,27 @@ describe('normalizeClientConfig', () => {
);
});

it('accepts a usable reconnect interval', () => {
const reconnect = { enabled: true, interval: 1000, maxRetries: 3 };
assert.deepEqual(
normalizeClientConfig({ ...config(), reconnect }).reconnect,
reconnect
);
});

it('rejects unusable reconnect intervals', () => {
for (const interval of [
0, -1000, Number.NaN, 1.5, 2_147_483_648, Number.MAX_VALUE
])
assert.throws(
() => normalizeClientConfig({
...config(),
reconnect: { enabled: true, interval, maxRetries: 1 }
}),
/reconnect\.interval/
);
});

it('restricts the client to one pooled connection', () => {
const normalized = normalizeClientConfig(config());
assert.deepEqual(normalized.poolSize, { min: 1, max: 1 });
Expand Down
30 changes: 27 additions & 3 deletions foreign/node/src/client/client.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
// specific language governing permissions and limitations
// under the License.

import type { ClientConfig } from './client.type.js';
import type { ClientConfig, ClientConfigOrString } from './client.type.js';
import {
parseConnectionString,
parseDuration
} from './client.connection-string.js';

export const DEFAULT_MAX_RESPONSE_FRAME_SIZE = 64 * 1024 * 1024;

Expand All @@ -29,8 +33,11 @@ export const DEFAULT_HEARTBEAT_INTERVAL = 5 * 1000;
export const MAX_HEARTBEAT_INTERVAL = 2_147_483_647;

export const normalizeClientConfig = (
config: ClientConfig
config: ClientConfigOrString
): ClientConfig => {
if (typeof config === 'string')
config = parseConnectionString(config);

const maxResponseFrameSize =
config.maxResponseFrameSize ?? DEFAULT_MAX_RESPONSE_FRAME_SIZE;
if (!Number.isSafeInteger(maxResponseFrameSize) ||
Expand All @@ -48,15 +55,32 @@ export const normalizeClientConfig = (
// the default below is nullish-only, so a value that is negative, fractional
// or above MAX_HEARTBEAT_INTERVAL reaches setInterval, which clamps it to
// 1 ms and floods the server.
const heartbeatInterval =
// A duration expression is accepted like the Rust SDK's IggyDuration.
const rawHeartbeatInterval =
config.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
const heartbeatInterval = typeof rawHeartbeatInterval === 'string'
? parseDuration(rawHeartbeatInterval)
: rawHeartbeatInterval;
if (!Number.isSafeInteger(heartbeatInterval) ||
heartbeatInterval < 0 ||
heartbeatInterval > MAX_HEARTBEAT_INTERVAL)
throw new TypeError(
`heartbeatInterval must be a safe integer of milliseconds between 0 and ${MAX_HEARTBEAT_INTERVAL} (0 disables heartbeats)`
);

// Unlike the heartbeat, 0 is not a disable here: an immediate retry delay
// turns reconnection into a hot loop. The ceiling guards the same
// setInterval clamp, which would turn a long backoff into a 1 ms spin.
if (config.reconnect !== undefined) {
const { interval } = config.reconnect;
if (!Number.isSafeInteger(interval) ||
interval < 1 ||
interval > MAX_HEARTBEAT_INTERVAL)
throw new TypeError(
`reconnect.interval must be a safe integer of milliseconds between 1 and ${MAX_HEARTBEAT_INTERVAL}`
);
}

return {
...config,
options: { ...config.options },
Expand Down
Loading
Loading