apache-iggy 0.10.0-edge.4 → 0.10.0-edge.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +55 -10
  3. package/dist/client/client.config.d.ts +2 -2
  4. package/dist/client/client.config.js +23 -0
  5. package/dist/client/client.config.test.js +29 -0
  6. package/dist/client/client.connection-string.d.ts +14 -0
  7. package/dist/client/client.connection-string.js +188 -0
  8. package/dist/client/client.connection-string.test.d.ts +2 -0
  9. package/dist/client/client.connection-string.test.js +209 -0
  10. package/dist/client/client.connection.d.ts +74 -5
  11. package/dist/client/client.connection.js +253 -58
  12. package/dist/client/client.connection.test.js +413 -3
  13. package/dist/client/client.d.ts +7 -7
  14. package/dist/client/client.debug.js +0 -1
  15. package/dist/client/client.js +24 -25
  16. package/dist/client/client.socket.d.ts +59 -4
  17. package/dist/client/client.socket.js +350 -31
  18. package/dist/client/client.socket.test.js +469 -1
  19. package/dist/client/client.type.d.ts +37 -13
  20. package/dist/client/client.type.js +0 -1
  21. package/dist/client/client.utils.js +0 -1
  22. package/dist/client/client.utils.test.js +0 -1
  23. package/dist/client/index.d.ts +1 -0
  24. package/dist/client/index.js +1 -1
  25. package/dist/client.e2e.js +0 -1
  26. package/dist/constant.d.ts +3 -0
  27. package/dist/constant.js +19 -0
  28. package/dist/debug-send.js +0 -1
  29. package/dist/debug.js +0 -1
  30. package/dist/duration.utils.d.ts +23 -0
  31. package/dist/duration.utils.js +302 -0
  32. package/dist/duration.utils.test.d.ts +2 -0
  33. package/dist/duration.utils.test.js +140 -0
  34. package/dist/e2e/tcp.client.e2e.js +0 -1
  35. package/dist/e2e/tcp.cluster.e2e.js +0 -1
  36. package/dist/e2e/tcp.connection-string.e2e.d.ts +2 -0
  37. package/dist/e2e/tcp.connection-string.e2e.js +117 -0
  38. package/dist/e2e/tcp.consumer-group.e2e.js +0 -1
  39. package/dist/e2e/tcp.consumer-stream.e2e.js +0 -1
  40. package/dist/e2e/tcp.parallel.e2e.js +0 -1
  41. package/dist/e2e/tcp.raw.e2e.js +0 -1
  42. package/dist/e2e/tcp.send-message.e2e.js +0 -1
  43. package/dist/e2e/tcp.stream.e2e.js +0 -1
  44. package/dist/e2e/tcp.system.e2e.js +0 -1
  45. package/dist/e2e/tcp.token.e2e.js +0 -1
  46. package/dist/e2e/tcp.topic.e2e.js +0 -1
  47. package/dist/e2e/tcp.user.e2e.js +0 -1
  48. package/dist/e2e/test-client.utils.js +0 -1
  49. package/dist/e2e/tls.system.e2e.js +41 -1
  50. package/dist/index.js +0 -1
  51. package/dist/stream/consumer-stream.d.ts +3 -3
  52. package/dist/stream/consumer-stream.js +0 -1
  53. package/dist/stream/index.js +0 -1
  54. package/dist/tcp.e2e.js +0 -1
  55. package/dist/tcp.sm.utils.js +0 -1
  56. package/dist/tls.system.e2e.js +0 -1
  57. package/dist/type.utils.js +0 -1
  58. package/dist/wire/client/client.utils.js +0 -1
  59. package/dist/wire/client/get-client.command.js +0 -1
  60. package/dist/wire/client/get-clients.command.js +0 -1
  61. package/dist/wire/client/get-me.command.js +0 -1
  62. package/dist/wire/client/index.js +0 -1
  63. package/dist/wire/cluster/cluster.type.js +0 -1
  64. package/dist/wire/cluster/cluster.utils.js +0 -1
  65. package/dist/wire/cluster/get-cluster-metadata.command.js +0 -1
  66. package/dist/wire/cluster/index.js +0 -1
  67. package/dist/wire/command-set.js +0 -1
  68. package/dist/wire/command-set.test.js +0 -1
  69. package/dist/wire/command.code.js +0 -1
  70. package/dist/wire/command.utils.js +0 -1
  71. package/dist/wire/consumer-group/create-group.command.js +0 -1
  72. package/dist/wire/consumer-group/create-group.command.test.js +0 -1
  73. package/dist/wire/consumer-group/delete-group.command.js +0 -1
  74. package/dist/wire/consumer-group/ensure-group.virtual.command.js +0 -1
  75. package/dist/wire/consumer-group/get-group.command.js +0 -1
  76. package/dist/wire/consumer-group/get-groups.command.js +0 -1
  77. package/dist/wire/consumer-group/group.command.test.js +0 -1
  78. package/dist/wire/consumer-group/group.utils.js +0 -1
  79. package/dist/wire/consumer-group/index.js +0 -1
  80. package/dist/wire/consumer-group/join-group.command.js +0 -1
  81. package/dist/wire/consumer-group/leave-group.command.js +0 -1
  82. package/dist/wire/consumer-group/sync-group.command.js +0 -1
  83. package/dist/wire/consumer-group/sync-group.command.test.js +0 -1
  84. package/dist/wire/error.code.js +2 -1
  85. package/dist/wire/error.code.test.js +3 -0
  86. package/dist/wire/error.utils.js +0 -1
  87. package/dist/wire/identifier.utils.js +0 -1
  88. package/dist/wire/identifier.utils.test.js +0 -1
  89. package/dist/wire/index.js +0 -1
  90. package/dist/wire/message/flush-unsaved-buffers.command.js +0 -1
  91. package/dist/wire/message/header.type.d.ts +1 -1
  92. package/dist/wire/message/header.type.js +0 -1
  93. package/dist/wire/message/header.utils.js +0 -1
  94. package/dist/wire/message/header.utils.test.js +0 -1
  95. package/dist/wire/message/iggy-header.utils.js +0 -1
  96. package/dist/wire/message/index.js +0 -1
  97. package/dist/wire/message/message-batch.test.js +0 -1
  98. package/dist/wire/message/message.utils.d.ts +14 -0
  99. package/dist/wire/message/message.utils.js +34 -7
  100. package/dist/wire/message/message.utils.test.d.ts +2 -0
  101. package/dist/wire/message/message.utils.test.js +107 -0
  102. package/dist/wire/message/partitioning.utils.js +0 -1
  103. package/dist/wire/message/poll-messages.command.js +0 -1
  104. package/dist/wire/message/poll-messages.command.test.js +0 -1
  105. package/dist/wire/message/poll.utils.js +0 -1
  106. package/dist/wire/message/send-messages.command.js +0 -1
  107. package/dist/wire/message/send-messages.command.test.js +0 -1
  108. package/dist/wire/number.utils.d.ts +10 -5
  109. package/dist/wire/number.utils.js +16 -9
  110. package/dist/wire/number.utils.test.d.ts +2 -0
  111. package/dist/wire/number.utils.test.js +55 -0
  112. package/dist/wire/offset/delete-offset.command.js +0 -1
  113. package/dist/wire/offset/get-offset.command.js +0 -1
  114. package/dist/wire/offset/index.js +0 -1
  115. package/dist/wire/offset/offset.utils.js +0 -1
  116. package/dist/wire/offset/store-offset.command.js +0 -1
  117. package/dist/wire/options.utils.js +0 -1
  118. package/dist/wire/options.utils.test.js +0 -1
  119. package/dist/wire/partition/create-partition.command.js +0 -1
  120. package/dist/wire/partition/delete-partition.command.js +0 -1
  121. package/dist/wire/partition/index.js +0 -1
  122. package/dist/wire/partition/partition.utils.js +0 -1
  123. package/dist/wire/partition/partition.utils.test.js +0 -1
  124. package/dist/wire/segment/delete-segments.command.js +0 -1
  125. package/dist/wire/segment/index.js +0 -1
  126. package/dist/wire/segment/segment.utils.js +0 -1
  127. package/dist/wire/serialize.utils.js +0 -1
  128. package/dist/wire/session/index.js +0 -1
  129. package/dist/wire/session/login-with-token.command.js +0 -1
  130. package/dist/wire/session/login-with-token.command.test.js +0 -1
  131. package/dist/wire/session/login.command.js +0 -1
  132. package/dist/wire/session/login.command.test.js +0 -1
  133. package/dist/wire/session/login.utils.js +0 -1
  134. package/dist/wire/session/logout.command.js +0 -1
  135. package/dist/wire/stream/create-stream.command.js +0 -1
  136. package/dist/wire/stream/create-stream.command.test.js +0 -1
  137. package/dist/wire/stream/delete-stream.command.js +0 -1
  138. package/dist/wire/stream/ensure-stream.virtual.command.js +0 -1
  139. package/dist/wire/stream/get-stream.command.js +0 -1
  140. package/dist/wire/stream/get-streams.command.js +0 -1
  141. package/dist/wire/stream/index.js +0 -1
  142. package/dist/wire/stream/purge-stream.command.js +0 -1
  143. package/dist/wire/stream/stream.utils.js +0 -1
  144. package/dist/wire/stream/update-stream.command.js +0 -1
  145. package/dist/wire/system/describe-options.command.js +0 -1
  146. package/dist/wire/system/get-stats.command.js +0 -1
  147. package/dist/wire/system/index.js +0 -1
  148. package/dist/wire/system/ping.command.js +0 -1
  149. package/dist/wire/token/create-token.command.js +0 -1
  150. package/dist/wire/token/create-token.command.test.js +0 -1
  151. package/dist/wire/token/delete-token.command.js +0 -1
  152. package/dist/wire/token/delete-token.command.test.js +0 -1
  153. package/dist/wire/token/get-tokens.command.js +0 -1
  154. package/dist/wire/token/index.js +0 -1
  155. package/dist/wire/token/token.utils.js +0 -1
  156. package/dist/wire/topic/create-topic.command.js +0 -1
  157. package/dist/wire/topic/create-topic.command.test.js +0 -1
  158. package/dist/wire/topic/delete-topic.command.js +0 -1
  159. package/dist/wire/topic/ensure-topic.virtual.command.js +0 -1
  160. package/dist/wire/topic/get-topic.command.js +0 -1
  161. package/dist/wire/topic/get-topics.command.js +0 -1
  162. package/dist/wire/topic/index.js +0 -1
  163. package/dist/wire/topic/purge-topic.command.js +0 -1
  164. package/dist/wire/topic/topic.utils.js +0 -1
  165. package/dist/wire/topic/topic.utils.test.js +0 -1
  166. package/dist/wire/topic/update-topic.command.js +0 -1
  167. package/dist/wire/user/change-password.command.js +0 -1
  168. package/dist/wire/user/create-user.command.js +0 -1
  169. package/dist/wire/user/create-user.command.test.js +0 -1
  170. package/dist/wire/user/delete-user.command.js +0 -1
  171. package/dist/wire/user/get-user.command.js +0 -1
  172. package/dist/wire/user/get-users.command.js +0 -1
  173. package/dist/wire/user/index.js +0 -1
  174. package/dist/wire/user/permissions.utils.js +0 -1
  175. package/dist/wire/user/permissions.utils.test.js +0 -1
  176. package/dist/wire/user/update-permissions.command.js +0 -1
  177. package/dist/wire/user/update-user.command.js +0 -1
  178. package/dist/wire/user/user.utils.js +0 -1
  179. package/dist/wire/uuid.utils.js +0 -1
  180. package/dist/wire/vsr/header.d.ts +4 -2
  181. package/dist/wire/vsr/header.js +4 -3
  182. package/dist/wire/vsr/index.js +5 -5
  183. package/dist/wire/vsr/operation.d.ts +0 -2
  184. package/dist/wire/vsr/operation.js +0 -4
  185. package/dist/wire/vsr/operation.test.js +1 -3
  186. package/dist/wire/vsr/register.js +0 -1
  187. package/dist/wire/vsr/reply.js +0 -1
  188. package/dist/wire/vsr/session.d.ts +3 -3
  189. package/dist/wire/vsr/session.js +3 -4
  190. package/dist/wire/vsr/vsr.test.js +15 -5
  191. package/package.json +5 -5
package/NOTICE CHANGED
@@ -1,4 +1,4 @@
1
- Apache Iggy (Incubating)
1
+ Apache Iggy
2
2
  Copyright 2026 The Apache Software Foundation
3
3
 
4
4
  This product includes software developed at
package/README.md CHANGED
@@ -10,12 +10,6 @@
10
10
 
11
11
  Apache Iggy Node.js client written in typescript, it currently only supports tcp & tls transports.
12
12
 
13
- > Apache Iggy (Incubating) is an effort undergoing incubation at the Apache Software Foundation (ASF), sponsored by the Apache Incubator PMC.
14
- >
15
- > Incubation is required of all newly accepted projects until a further review indicates that the infrastructure, communications, and decision making process have stabilized in a manner consistent with other successful ASF projects.
16
- >
17
- > While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF.
18
-
19
13
  diclaimer: although all iggy commands & basic client/stream are implemented this is still a WIP, provided as is, and has still a long way to go to be considered "battle tested".
20
14
 
21
15
  note: This lib started as _iggy-bin_ ( [github](https://github.com/T1B0/iggy-bin) / [npm](https://www.npmjs.com/package/iggy-bin)) before migrating under iggy-rs org. package iggy-bin@v1.3.4 is equivalent to @iggy.rs/sdk@v1.0.3 and migrating again under apache iggy monorepo ( [github](https://github.com/apache/iggy/tree/master/foreign/node) and is now published on npmjs as apache-iggy
@@ -73,6 +67,8 @@ new session.
73
67
 
74
68
  The client pings every `heartbeatInterval` milliseconds, 5000 by default, which
75
69
  keeps an idle session alive when the server's `[heartbeat]` eviction is enabled.
70
+ `heartbeatInterval` also accepts a duration expression such as `"10s"` or
71
+ `"1h 30m"`, like the Rust SDK.
76
72
  The server evicts a connection silent for 36 s, which is 1.2 x its 30 s
77
73
  heartbeat interval. Raising the client interval past that window, or setting it
78
74
  to 0 to disable client heartbeats, exposes an idle consumer-group member to
@@ -111,6 +107,43 @@ const client = new Client({
111
107
  const stats = await client.system.getStats();
112
108
  ```
113
109
 
110
+ ### Connection strings
111
+
112
+ Every client constructor (except `SimpleClient` see note) also accepts a
113
+ connection string instead of a config object:
114
+
115
+ ```ts
116
+ import { Client } from "apache-iggy";
117
+
118
+ const client = new Client("iggy://iggy:iggy@127.0.0.1:8090");
119
+ const stats = await client.system.getStats();
120
+ ```
121
+
122
+ Supported schemes are `iggy://` (TCP, default) and `iggy+tcp://`. Credentials
123
+ are `username:password` or a single personal access token. Options mirror the
124
+ other SDKs: `tls`, `tls_domain`, `tls_ca_file`, `reconnection_retries`,
125
+ `reconnection_interval`, `heartbeat_interval` and `nodelay`. `reestablish_after`
126
+ is accepted for format compatibility but has no Node equivalent.
127
+
128
+ note: `SimpleClient` does not accept a connection string: it wraps an existing
129
+ `RawClient` instance rather than building one from configuration. Pass the
130
+ connection string to `Client`, `SingleClient` or `getRawClient` and hand the
131
+ resulting raw client to `SimpleClient` if needed.
132
+
133
+ ### option limits
134
+
135
+ | option | limit |
136
+ | --- | --- |
137
+ | `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 |
138
+ | `heartbeat_interval` | duration up to `2147483647ms` (Node's largest timer delay); `0` disables heartbeats |
139
+ | `reconnection_interval` | positive duration (`ms`, `s`, `m`, `h`) up to `2147483647ms` (Node's largest timer delay); zero spellings are rejected. Defaults to `1s` |
140
+ | port in the authority | decimal up to `65535` |
141
+
142
+ Durations accept the same expressions as the Rust SDK, for example `500ms`,
143
+ `10s`, `1h 30m`, `5d`, `2w`, `1y`; matching is case-insensitive and
144
+ `0`, `unlimited`, `disabled` and `none` map to zero. Unit-less numbers such as
145
+ `5` are rejected.
146
+
114
147
  ## use sources
115
148
 
116
149
  ### Install
@@ -128,7 +161,7 @@ npm run build
128
161
  ### test
129
162
 
130
163
  note: use env var `IGGY_TCP_ADDRESS="host:port"` to set the server
131
- address for bdd and e2e tests.
164
+ address for e2e tests. bdd tests need more variables, see below.
132
165
 
133
166
  #### unit tests
134
167
 
@@ -146,15 +179,27 @@ npm run test:e2e
146
179
 
147
180
  #### bdd tests
148
181
 
149
- bdd test expect an iggy-server at tcp://127.0.0.1:8090
182
+ the bdd suite has no defaults and fails when `IGGY_TCP_ADDRESS`,
183
+ `IGGY_ROOT_USERNAME` or `IGGY_ROOT_PASSWORD` is missing. from the repository
184
+ root run
150
185
 
151
186
  ```bash
152
- npm run test:bdd
187
+ ./scripts/run-bdd-tests.sh node
153
188
  ```
154
189
 
190
+ the script starts the server and sets every variable, so none of them have to be
191
+ exported by hand. to iterate against a server you started yourself, see
192
+ [src/bdd/README.md](./src/bdd/README.md).
193
+
155
194
  #### run all test
156
195
 
157
- `npm run test` runs unit, bdd and e2e tests suite (expect an iggy-server at tcp://127.0.0.1:8090)
196
+ `npm run test` runs unit, bdd and e2e tests suite against an iggy-server at
197
+ tcp://127.0.0.1:8090, started with the same root credentials
198
+
199
+ ```bash
200
+ IGGY_TCP_ADDRESS=127.0.0.1:8090 IGGY_ROOT_USERNAME=iggy IGGY_ROOT_PASSWORD=iggy \
201
+ npm run test
202
+ ```
158
203
 
159
204
  ### lint
160
205
 
@@ -1,4 +1,4 @@
1
- import type { ClientConfig } from './client.type.js';
1
+ import type { ClientConfig, ClientConfigOrString } from './client.type.js';
2
2
  export declare const DEFAULT_MAX_RESPONSE_FRAME_SIZE: number;
3
3
  /**
4
4
  * The server evicts a connection silent for 36 s (1.2 x its 30 s heartbeat
@@ -7,5 +7,5 @@ export declare const DEFAULT_MAX_RESPONSE_FRAME_SIZE: number;
7
7
  export declare const DEFAULT_HEARTBEAT_INTERVAL: number;
8
8
  /** Node's largest timer delay: setInterval clamps anything above it to 1 ms. */
9
9
  export declare const MAX_HEARTBEAT_INTERVAL = 2147483647;
10
- export declare const normalizeClientConfig: (config: ClientConfig) => ClientConfig;
10
+ export declare const normalizeClientConfig: (config: ClientConfigOrString) => ClientConfig;
11
11
  //# sourceMappingURL=client.config.d.ts.map
@@ -14,6 +14,8 @@
14
14
  // KIND, either express or implied. See the License for the
15
15
  // specific language governing permissions and limitations
16
16
  // under the License.
17
+ import { MAX_U32 } from '../constant.js';
18
+ import { parseConnectionString } from './client.connection-string.js';
17
19
  export const DEFAULT_MAX_RESPONSE_FRAME_SIZE = 64 * 1024 * 1024;
18
20
  /**
19
21
  * The server evicts a connection silent for 36 s (1.2 x its 30 s heartbeat
@@ -23,6 +25,8 @@ export const DEFAULT_HEARTBEAT_INTERVAL = 5 * 1000;
23
25
  /** Node's largest timer delay: setInterval clamps anything above it to 1 ms. */
24
26
  export const MAX_HEARTBEAT_INTERVAL = 2_147_483_647;
25
27
  export const normalizeClientConfig = (config) => {
28
+ if (typeof config === 'string')
29
+ config = parseConnectionString(config);
26
30
  const maxResponseFrameSize = config.maxResponseFrameSize ?? DEFAULT_MAX_RESPONSE_FRAME_SIZE;
27
31
  if (!Number.isSafeInteger(maxResponseFrameSize) ||
28
32
  maxResponseFrameSize < 256)
@@ -38,6 +42,25 @@ export const normalizeClientConfig = (config) => {
38
42
  heartbeatInterval < 0 ||
39
43
  heartbeatInterval > MAX_HEARTBEAT_INTERVAL)
40
44
  throw new TypeError(`heartbeatInterval must be a safe integer of milliseconds between 0 and ${MAX_HEARTBEAT_INTERVAL} (0 disables heartbeats)`);
45
+ // Unlike the heartbeat, 0 is not a disable here: an immediate retry delay
46
+ // turns reconnection into a hot loop. The ceiling guards the same
47
+ // setInterval clamp, which would turn a long backoff into a 1 ms spin.
48
+ // A disabled reconnect never schedules a retry, so the interval check
49
+ // applies only when enabled; callers commonly pass a zero interval with
50
+ // enabled: false. maxRetries stays bounded either way, matching the
51
+ // connection-string path.
52
+ if (config.reconnect !== undefined) {
53
+ const { enabled, interval, maxRetries } = config.reconnect;
54
+ if (!Number.isSafeInteger(maxRetries) ||
55
+ maxRetries < 0 ||
56
+ maxRetries > MAX_U32)
57
+ throw new TypeError(`reconnect.maxRetries must be a non-negative integer of at most ${MAX_U32}`);
58
+ if (enabled &&
59
+ (!Number.isSafeInteger(interval) ||
60
+ interval < 1 ||
61
+ interval > MAX_HEARTBEAT_INTERVAL))
62
+ throw new TypeError(`reconnect.interval must be a safe integer of milliseconds between 1 and ${MAX_HEARTBEAT_INTERVAL}`);
63
+ }
41
64
  return {
42
65
  ...config,
43
66
  options: { ...config.options },
@@ -16,6 +16,7 @@
16
16
  // under the License.
17
17
  import assert from 'node:assert/strict';
18
18
  import { describe, it } from 'node:test';
19
+ import { MAX_U32 } from '../constant.js';
19
20
  import { DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_MAX_RESPONSE_FRAME_SIZE, normalizeClientConfig } from './client.config.js';
20
21
  const config = () => ({
21
22
  transport: 'TCP',
@@ -44,6 +45,34 @@ describe('normalizeClientConfig', () => {
44
45
  heartbeatInterval
45
46
  }), /heartbeatInterval/);
46
47
  });
48
+ it('accepts a usable reconnect interval', () => {
49
+ const reconnect = { enabled: true, interval: 1000, maxRetries: 3 };
50
+ assert.deepEqual(normalizeClientConfig({ ...config(), reconnect }).reconnect, reconnect);
51
+ });
52
+ it('rejects unusable reconnect intervals', () => {
53
+ for (const interval of [
54
+ 0, -1000, Number.NaN, 1.5, 2_147_483_648, Number.MAX_VALUE
55
+ ])
56
+ assert.throws(() => normalizeClientConfig({
57
+ ...config(),
58
+ reconnect: { enabled: true, interval, maxRetries: 1 }
59
+ }), /reconnect\.interval/);
60
+ });
61
+ it('skips the interval check when reconnect is disabled', () => {
62
+ assert.doesNotThrow(() => normalizeClientConfig({
63
+ ...config(),
64
+ reconnect: { enabled: false, interval: 0, maxRetries: 0 }
65
+ }));
66
+ });
67
+ it('rejects unusable reconnect maxRetries', () => {
68
+ for (const maxRetries of [
69
+ -1, Number.NaN, 1.5, MAX_U32 + 1, Number.MAX_VALUE
70
+ ])
71
+ assert.throws(() => normalizeClientConfig({
72
+ ...config(),
73
+ reconnect: { enabled: true, interval: 1000, maxRetries }
74
+ }), /reconnect\.maxRetries/);
75
+ });
47
76
  it('restricts the client to one pooled connection', () => {
48
77
  const normalized = normalizeClientConfig(config());
49
78
  assert.deepEqual(normalized.poolSize, { min: 1, max: 1 });
@@ -0,0 +1,14 @@
1
+ import type { ClientConfig } from './client.type.js';
2
+ /** Parses a duration with the exact grammar of the Rust SDK's
3
+ * `IggyDuration::from_str`, returning whole milliseconds.
4
+ */
5
+ export declare const parseDuration: (value: string) => number;
6
+ /**
7
+ * Parses an Iggy connection string into a client configuration.
8
+ *
9
+ * Supports `iggy://` and `iggy+tcp://`; the Node SDK implements TCP/TLS only.
10
+ * Credentials are either `username:password` or a single personal access
11
+ * token before the `@`. TLS is enabled with `tls=true`.
12
+ */
13
+ export declare const parseConnectionString: (connectionString: string) => ClientConfig;
14
+ //# sourceMappingURL=client.connection-string.d.ts.map
@@ -0,0 +1,188 @@
1
+ // Licensed to the Apache Software Foundation (ASF) under one
2
+ // or more contributor license agreements. See the NOTICE file
3
+ // distributed with this work for additional information
4
+ // regarding copyright ownership. The ASF licenses this file
5
+ // to you under the Apache License, Version 2.0 (the
6
+ // "License"); you may not use this file except in compliance
7
+ // with the License. You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing,
12
+ // software distributed under the License is distributed on an
13
+ // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ // KIND, either express or implied. See the License for the
15
+ // specific language governing permissions and limitations
16
+ // under the License.
17
+ import { MAX_U32 } from '../constant.js';
18
+ import { nanosecondsToMilliseconds, parseIggyDurationNanoseconds } from '../duration.utils.js';
19
+ const DEFAULT_PROTOCOL = 'iggy';
20
+ const SCHEME_PREFIX = 'iggy+';
21
+ const SUPPORTED_PROTOCOLS = ['tcp'];
22
+ /** Reconnection defaults carried by every connection string.
23
+ *
24
+ * Mirrors TcpConnectionStringOptions: retries default to unlimited and the
25
+ * interval to 1s. Deliberately not DefaultReconnectOption, whose 12/5s pair
26
+ * only applies to object configs.
27
+ */
28
+ const CONNECTION_STRING_RECONNECT = {
29
+ enabled: true,
30
+ interval: 1000,
31
+ maxRetries: MAX_U32
32
+ };
33
+ /** Parses a duration with the exact grammar of the Rust SDK's
34
+ * `IggyDuration::from_str`, returning whole milliseconds.
35
+ */
36
+ export const parseDuration = (value) => {
37
+ try {
38
+ return nanosecondsToMilliseconds(parseIggyDurationNanoseconds(value));
39
+ }
40
+ catch (error) {
41
+ throw new TypeError(`invalid duration "${value}"`, { cause: error });
42
+ }
43
+ };
44
+ /**
45
+ * Parses an Iggy connection string into a client configuration.
46
+ *
47
+ * Supports `iggy://` and `iggy+tcp://`; the Node SDK implements TCP/TLS only.
48
+ * Credentials are either `username:password` or a single personal access
49
+ * token before the `@`. TLS is enabled with `tls=true`.
50
+ */
51
+ export const parseConnectionString = (connectionString) => {
52
+ if (typeof connectionString !== 'string' || connectionString.length === 0)
53
+ throw new TypeError('connection string must be a non-empty string');
54
+ const protocolParts = connectionString.split('://');
55
+ if (protocolParts.length !== 2)
56
+ throw new TypeError('invalid connection string');
57
+ const scheme = protocolParts[0];
58
+ const protocol = scheme === DEFAULT_PROTOCOL
59
+ ? 'tcp'
60
+ : scheme.startsWith(SCHEME_PREFIX)
61
+ ? scheme.slice(SCHEME_PREFIX.length)
62
+ : undefined;
63
+ if (protocol === undefined)
64
+ throw new TypeError('invalid connection string');
65
+ if (!SUPPORTED_PROTOCOLS.includes(protocol))
66
+ throw new TypeError(`unsupported transport "${protocol}", Node SDK supports tcp only`);
67
+ const parts = protocolParts[1].split('@');
68
+ if (parts.length !== 2)
69
+ throw new TypeError('invalid connection string');
70
+ const credentials = parts[0].split(':');
71
+ const tokenCredentials = credentials.length === 1;
72
+ if (!tokenCredentials && credentials.length !== 2)
73
+ throw new TypeError('invalid connection string');
74
+ const username = credentials[0];
75
+ const password = credentials[1] ?? '';
76
+ if (!tokenCredentials && (username.length === 0 || password.length === 0))
77
+ throw new TypeError('invalid connection string');
78
+ const serverAndOptions = parts[1].split('?');
79
+ if (serverAndOptions.length > 2)
80
+ throw new TypeError('invalid connection string');
81
+ const serverAddress = serverAndOptions[0];
82
+ // One match covers both `[ipv6]:port` and `host:port`, where the
83
+ // unbracketed host may not contain a colon. Multi-colon authorities such
84
+ // as `2001:db8::1:8090` or `host:8090:9090` are rejected
85
+ const addressMatch = /^(?:\[([^\]]+)\]|([^:\[\]]+)):(\d+)$/.exec(serverAddress);
86
+ if (!addressMatch)
87
+ throw new TypeError('invalid connection string');
88
+ const host = addressMatch[1] ?? addressMatch[2];
89
+ const port = Number(addressMatch[3]);
90
+ if (port > 65535)
91
+ throw new TypeError('invalid connection string');
92
+ const options = serverAndOptions.length === 2
93
+ ? parseConnectionOptions(serverAndOptions[1])
94
+ : {
95
+ tls: false,
96
+ reconnect: { ...CONNECTION_STRING_RECONNECT }
97
+ };
98
+ const { tls, reconnect, heartbeatInterval, ...transportOptions } = options;
99
+ const config = {
100
+ transport: tls ? 'TLS' : 'TCP',
101
+ options: {
102
+ host,
103
+ port: Number(port),
104
+ ...transportOptions
105
+ },
106
+ credentials: tokenCredentials
107
+ ? { token: username }
108
+ : { username, password },
109
+ // Always present on connection strings: Rust applies its unlimited/1s
110
+ // reconnection defaults even without query options.
111
+ reconnect
112
+ };
113
+ if (heartbeatInterval !== undefined)
114
+ config.heartbeatInterval = heartbeatInterval;
115
+ return config;
116
+ };
117
+ const parseConnectionOptions = (optionsString) => {
118
+ const parsed = {
119
+ tls: false,
120
+ reconnect: { ...CONNECTION_STRING_RECONNECT }
121
+ };
122
+ for (const option of optionsString.split('&')) {
123
+ const optionParts = option.split('=');
124
+ if (optionParts.length !== 2)
125
+ throw new TypeError('invalid connection string');
126
+ const [name, value] = optionParts;
127
+ switch (name) {
128
+ case 'tls':
129
+ parsed.tls = parseBoolean(name, value);
130
+ break;
131
+ case 'nodelay':
132
+ parsed.noDelay = parseBoolean(name, value);
133
+ break;
134
+ case 'tls_domain':
135
+ parsed.servername = value;
136
+ break;
137
+ case 'tls_ca_file':
138
+ // The path is stored unread; the certificate is loaded when the
139
+ // TLS socket is created, so plain-TCP configs never touch the
140
+ // filesystem (matches the Rust SDK).
141
+ parsed.caFile = value;
142
+ break;
143
+ case 'reconnection_retries': {
144
+ // Values above u32::MAX are rejected like the Rust SDK's u32
145
+ // overflow; otherwise they would act as a second, undocumented
146
+ // spelling of "unlimited".
147
+ const maxRetries = value === 'unlimited'
148
+ ? MAX_U32
149
+ : parseNumber(name, value);
150
+ if (maxRetries > MAX_U32)
151
+ throw new TypeError(`option "${name}" must be at most ${MAX_U32}`);
152
+ parsed.reconnect.maxRetries = maxRetries;
153
+ break;
154
+ }
155
+ case 'reconnection_interval': {
156
+ const interval = parseDuration(value);
157
+ // With retries defaulting to unlimited, a zero interval would turn
158
+ // the reconnect loop into an unbounded hot loop.
159
+ if (interval <= 0)
160
+ throw new TypeError(`option "${name}" must be positive`);
161
+ parsed.reconnect.interval = interval;
162
+ break;
163
+ }
164
+ case 'reestablish_after':
165
+ // No Node equivalent: validated as a duration like the Rust SDK
166
+ // does, then discarded.
167
+ parseDuration(value);
168
+ break;
169
+ case 'heartbeat_interval':
170
+ parsed.heartbeatInterval = parseDuration(value);
171
+ break;
172
+ default:
173
+ throw new TypeError(`unknown option "${name}"`);
174
+ }
175
+ }
176
+ return parsed;
177
+ };
178
+ const parseBoolean = (name, value) => {
179
+ if (value !== 'true' && value !== 'false')
180
+ throw new TypeError(`option "${name}" must be true or false`);
181
+ return value === 'true';
182
+ };
183
+ const parseNumber = (name, value) => {
184
+ if (!/^\d+$/.test(value))
185
+ throw new TypeError(`option "${name}" must be a non-negative integer`);
186
+ return Number(value);
187
+ };
188
+ //# sourceMappingURL=client.connection-string.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=client.connection-string.test.d.ts.map
@@ -0,0 +1,209 @@
1
+ // Licensed to the Apache Software Foundation (ASF) under one
2
+ // or more contributor license agreements. See the NOTICE file
3
+ // distributed with this work for additional information
4
+ // regarding copyright ownership. The ASF licenses this file
5
+ // to you under the Apache License, Version 2.0 (the
6
+ // "License"); you may not use this file except in compliance
7
+ // with the License. You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing,
12
+ // software distributed under the License is distributed on an
13
+ // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ // KIND, either express or implied. See the License for the
15
+ // specific language governing permissions and limitations
16
+ // under the License.
17
+ import assert from 'node:assert/strict';
18
+ import { describe, it } from 'node:test';
19
+ import { MAX_U32 } from '../constant.js';
20
+ import { parseConnectionString, parseDuration } from './client.connection-string.js';
21
+ import { DEFAULT_HEARTBEAT_INTERVAL, normalizeClientConfig } from './client.config.js';
22
+ describe('parseConnectionString', () => {
23
+ it('parses the default scheme with password credentials', () => {
24
+ assert.deepEqual(parseConnectionString('iggy://iggy:secret@127.0.0.1:8090'), {
25
+ transport: 'TCP',
26
+ options: { host: '127.0.0.1', port: 8090 },
27
+ credentials: { username: 'iggy', password: 'secret' },
28
+ reconnect: { enabled: true, interval: 1000, maxRetries: MAX_U32 }
29
+ });
30
+ });
31
+ it('parses the explicit tcp scheme with a personal access token', () => {
32
+ assert.deepEqual(parseConnectionString('iggy+tcp://iggypat-1234567890abcdef@localhost:8090'), {
33
+ transport: 'TCP',
34
+ options: { host: 'localhost', port: 8090 },
35
+ credentials: { token: 'iggypat-1234567890abcdef' },
36
+ reconnect: { enabled: true, interval: 1000, maxRetries: MAX_U32 }
37
+ });
38
+ });
39
+ it('maps tls options to the TLS transport', () => {
40
+ assert.deepEqual(parseConnectionString('iggy://iggy:secret@localhost:8090?tls=true&tls_domain=iggy.apache.org'), {
41
+ transport: 'TLS',
42
+ options: {
43
+ host: 'localhost',
44
+ port: 8090,
45
+ servername: 'iggy.apache.org'
46
+ },
47
+ credentials: { username: 'iggy', password: 'secret' },
48
+ reconnect: { enabled: true, interval: 1000, maxRetries: MAX_U32 }
49
+ });
50
+ });
51
+ it('maps reconnection and heartbeat options', () => {
52
+ assert.deepEqual(parseConnectionString('iggy+tcp://iggy:secret@localhost:8090' +
53
+ '?reconnection_retries=3&reconnection_interval=5s&heartbeat_interval=10s'), {
54
+ transport: 'TCP',
55
+ options: { host: 'localhost', port: 8090 },
56
+ credentials: { username: 'iggy', password: 'secret' },
57
+ reconnect: {
58
+ enabled: true,
59
+ maxRetries: 3,
60
+ interval: 5000
61
+ },
62
+ heartbeatInterval: 10000
63
+ });
64
+ });
65
+ it('applies unlimited/1s reconnection defaults to partial options', () => {
66
+ // retries alone keep the 1s interval; interval alone keeps unlimited.
67
+ assert.deepEqual(parseConnectionString('iggy://iggy:secret@localhost:8090?reconnection_retries=3').reconnect, { enabled: true, interval: 1000, maxRetries: 3 });
68
+ assert.deepEqual(parseConnectionString('iggy://iggy:secret@localhost:8090?reconnection_interval=5s').reconnect, { enabled: true, interval: 5000, maxRetries: MAX_U32 });
69
+ });
70
+ it('maps nodelay to the socket option', () => {
71
+ assert.equal(parseConnectionString('iggy://iggy:secret@localhost:8090?nodelay=true')
72
+ .options.noDelay, true);
73
+ });
74
+ it('maps unlimited retries to the u32 ceiling', () => {
75
+ assert.equal(parseConnectionString('iggy://iggy:secret@localhost:8090?reconnection_retries=unlimited').reconnect?.maxRetries, MAX_U32);
76
+ });
77
+ it('accepts retry counts up to u32::MAX and rejects overflow', () => {
78
+ assert.equal(parseConnectionString(`iggy://iggy:secret@localhost:8090?reconnection_retries=${MAX_U32}`).reconnect?.maxRetries, MAX_U32);
79
+ for (const value of [
80
+ 'iggy://iggy:secret@localhost:8090?reconnection_retries=4294967296',
81
+ 'iggy://iggy:secret@localhost:8090?reconnection_retries=99999999999999'
82
+ ])
83
+ assert.throws(() => parseConnectionString(value), TypeError);
84
+ });
85
+ it('rejects a non-positive reconnection interval', () => {
86
+ // Zero spellings parse but are rejected by the positivity bound.
87
+ for (const value of ['0', '0ms', 'none'])
88
+ assert.throws(() => parseConnectionString(`iggy://iggy:secret@localhost:8090?reconnection_interval=${value}`), /must be positive/);
89
+ // Negative durations shall not parse
90
+ assert.throws(() => parseConnectionString('iggy://iggy:secret@localhost:8090?reconnection_interval=-1s'), TypeError);
91
+ });
92
+ it('ignores reestablish_after for format compatibility', () => {
93
+ assert.deepEqual(parseConnectionString('iggy://iggy:secret@localhost:8090?reestablish_after=10s'), {
94
+ transport: 'TCP',
95
+ options: { host: 'localhost', port: 8090 },
96
+ credentials: { username: 'iggy', password: 'secret' },
97
+ reconnect: { enabled: true, interval: 1000, maxRetries: MAX_U32 }
98
+ });
99
+ });
100
+ it('rejects unsupported transports', () => {
101
+ for (const value of [
102
+ 'iggy+quic://iggy:secret@localhost:8090',
103
+ 'iggy+ws://iggy:secret@localhost:8090'
104
+ ])
105
+ assert.throws(() => parseConnectionString(value), /unsupported transport/);
106
+ });
107
+ it('rejects malformed connection strings', () => {
108
+ for (const value of [
109
+ '',
110
+ 'iggy',
111
+ 'iggy://',
112
+ 'iggy://:secret@localhost:8090',
113
+ 'iggy://iggy:@localhost:8090',
114
+ 'iggy://iggy:secret@localhost',
115
+ 'iggy://iggy:secret@:8090',
116
+ 'iggy://iggy:secret@localhost:port',
117
+ 'iggy://iggy:secret@localhost:70000',
118
+ 'iggy://iggy:secret@localhost:8090?unknown=value',
119
+ 'iggy://iggy:secret@localhost:8090?tls=maybe',
120
+ 'iggy://iggy:secret@localhost:8090?reconnection_retries=three',
121
+ 'iggy://iggy:secret@[::1:8090',
122
+ 'iggy://iggy:secret@[]:8090',
123
+ 'iggy://iggy:secret@[::1]x:8090',
124
+ 'iggy://iggy:secret@2001:db8::1:8090',
125
+ 'iggy://iggy:secret@host:8090:9090',
126
+ 'iggy://iggy:secret@localhost:8090?',
127
+ 'iggy://iggy:secret@localhost:8090?&',
128
+ 'iggy://iggy:secret@localhost:8090?reestablish_after=garbage'
129
+ ])
130
+ assert.throws(() => parseConnectionString(value), TypeError);
131
+ });
132
+ it('never includes the connection string in error messages', () => {
133
+ const secrets = ['hunter2', 'iggypat-1234567890abcdef'];
134
+ for (const value of [
135
+ 'iggy://iggy:hunter2@localhost',
136
+ `iggy+tcp://iggypat-1234567890abcdef@localhost`,
137
+ 'iggy://iggy:hunter2@localhost:8090?unknown=value',
138
+ 'iggy://iggy:hunter2@localhost:8090?tls=maybe',
139
+ 'iggy://iggy:hunter2@localhost:8090?reconnection_retries=three',
140
+ 'iggy://iggy:hunter2@localhost:70000'
141
+ ]) {
142
+ try {
143
+ parseConnectionString(value);
144
+ assert.fail(`expected "${value}" to be rejected`);
145
+ }
146
+ catch (error) {
147
+ assert.ok(error instanceof TypeError);
148
+ for (const secret of secrets)
149
+ assert.ok(!error.message.includes(secret), `error message leaked a secret: ${error.message}`);
150
+ }
151
+ }
152
+ });
153
+ it('parses IPv6 host addresses without their brackets', () => {
154
+ assert.deepEqual(parseConnectionString('iggy://iggy:secret@[::1]:8090').options, { host: '::1', port: 8090 });
155
+ });
156
+ it('stores tls_ca_file as a path without reading it at parse time', () => {
157
+ assert.deepEqual(parseConnectionString('iggy://iggy:secret@localhost:8090' +
158
+ '?tls=true&tls_ca_file=/does/not/exist.pem').options, {
159
+ host: 'localhost',
160
+ port: 8090,
161
+ caFile: '/does/not/exist.pem'
162
+ });
163
+ });
164
+ });
165
+ describe('parseDuration', () => {
166
+ it('converts supported units to milliseconds', () => {
167
+ assert.equal(parseDuration('500ms'), 500);
168
+ assert.equal(parseDuration('5s'), 5000);
169
+ assert.equal(parseDuration('2m'), 120000);
170
+ assert.equal(parseDuration('1h'), 3600000);
171
+ assert.equal(parseDuration('0.5s'), 500);
172
+ assert.equal(parseDuration('1h 1m 1s'), 3661000);
173
+ assert.equal(parseDuration('1h30m'), 5400000);
174
+ assert.equal(parseDuration('5d'), 432000000);
175
+ assert.equal(parseDuration('2w'), 1209600000);
176
+ assert.equal(parseDuration('1y'), 31557600000);
177
+ assert.equal(parseDuration('5sec'), 5000);
178
+ assert.equal(parseDuration('5msec'), 5);
179
+ // Fractional results are rounded to whole milliseconds.
180
+ assert.equal(parseDuration('1.005s'), 1005);
181
+ assert.equal(parseDuration('5usec'), 0);
182
+ assert.equal(parseDuration('500nsec'), 0);
183
+ for (const zero of ['0', 'unlimited', 'disabled', 'none', 'UNLIMITED'])
184
+ assert.equal(parseDuration(zero), 0);
185
+ });
186
+ it('rejects unsupported durations', () => {
187
+ for (const value of ['5', '-1s', 'ms', '', 'abc', 's'])
188
+ assert.throws(() => parseDuration(value), /invalid duration/);
189
+ });
190
+ });
191
+ describe('normalizeClientConfig with connection strings', () => {
192
+ it('applies client defaults to the parsed config', () => {
193
+ const normalized = normalizeClientConfig('iggy://iggy:secret@localhost:8090');
194
+ assert.equal(normalized.transport, 'TCP');
195
+ assert.equal(normalized.options.host, 'localhost');
196
+ assert.equal(normalized.options.port, 8090);
197
+ assert.deepEqual(normalized.credentials, {
198
+ username: 'iggy',
199
+ password: 'secret'
200
+ });
201
+ assert.equal(normalized.heartbeatInterval, DEFAULT_HEARTBEAT_INTERVAL);
202
+ assert.deepEqual(normalized.poolSize, { min: 1, max: 1 });
203
+ });
204
+ it('rejects reconnect intervals beyond the node timer ceiling', () => {
205
+ // Parses to 3_600_000_000 ms; setInterval would clamp it back to 1 ms.
206
+ assert.throws(() => normalizeClientConfig('iggy://iggy:secret@localhost:8090?reconnection_interval=1000h'), /reconnect\.interval/);
207
+ });
208
+ });
209
+ //# sourceMappingURL=client.connection-string.test.js.map