apache-iggy 0.10.0 → 0.10.1-edge.2

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 (157) hide show
  1. package/README.md +248 -28
  2. package/dist/client/client.config.d.ts +2 -0
  3. package/dist/client/client.config.js +13 -2
  4. package/dist/client/client.config.test.js +15 -2
  5. package/dist/client/client.connection-string.js +13 -1
  6. package/dist/client/client.connection-string.test.js +43 -1
  7. package/dist/client/client.connection.d.ts +33 -135
  8. package/dist/client/client.connection.js +190 -453
  9. package/dist/client/client.connection.test.js +244 -669
  10. package/dist/client/client.d.ts +4 -35
  11. package/dist/client/client.js +8 -101
  12. package/dist/client/client.operation.d.ts +120 -0
  13. package/dist/client/client.operation.js +428 -0
  14. package/dist/client/client.operation.test.d.ts +2 -0
  15. package/dist/client/client.operation.test.js +285 -0
  16. package/dist/client/client.socket.d.ts +92 -209
  17. package/dist/client/client.socket.js +1205 -981
  18. package/dist/client/client.socket.test.js +4865 -679
  19. package/dist/client/client.type.d.ts +65 -22
  20. package/dist/client/index.d.ts +1 -0
  21. package/dist/client/index.js +1 -0
  22. package/dist/client.e2e.js +3 -5
  23. package/dist/e2e/resource-options.e2e.d.ts +2 -0
  24. package/dist/{wire/stream/purge-stream.command.js → e2e/resource-options.e2e.js} +12 -20
  25. package/dist/e2e/resource-options.suite.d.ts +15 -0
  26. package/dist/e2e/resource-options.suite.js +272 -0
  27. package/dist/e2e/tcp.cluster.e2e.js +1 -0
  28. package/dist/e2e/tcp.consumer-stream.e2e.js +4 -0
  29. package/dist/e2e/tcp.parallel.e2e.js +5 -6
  30. package/dist/e2e/tcp.stream.e2e.js +0 -3
  31. package/dist/e2e/tcp.system.e2e.js +10 -1
  32. package/dist/e2e/tcp.token.e2e.js +8 -2
  33. package/dist/e2e/tcp.topic.e2e.js +0 -3
  34. package/dist/e2e/tcp.user.e2e.js +46 -3
  35. package/dist/e2e/tls.system.e2e.js +21 -12
  36. package/dist/index.d.ts +2 -1
  37. package/dist/index.js +1 -1
  38. package/dist/stream/consumer-stream.js +97 -12
  39. package/dist/wire/cluster/cluster.type.d.ts +2 -2
  40. package/dist/wire/cluster/cluster.utils.d.ts +1 -0
  41. package/dist/wire/cluster/cluster.utils.js +4 -1
  42. package/dist/wire/command-set.d.ts +8 -8
  43. package/dist/wire/command-set.js +18 -9
  44. package/dist/wire/command.code.d.ts +3 -10
  45. package/dist/wire/command.code.js +9 -11
  46. package/dist/wire/command.utils.d.ts +3 -2
  47. package/dist/wire/command.utils.js +3 -2
  48. package/dist/wire/error.code.js +17 -1
  49. package/dist/wire/error.code.test.js +28 -1
  50. package/dist/wire/error.utils.d.ts +4 -2
  51. package/dist/wire/error.utils.js +6 -3
  52. package/dist/wire/identifier.utils.d.ts +2 -0
  53. package/dist/wire/identifier.utils.js +2 -0
  54. package/dist/wire/index.d.ts +1 -0
  55. package/dist/wire/index.js +1 -0
  56. package/dist/wire/message/header.type.d.ts +1 -1
  57. package/dist/wire/message/message-batch.test.js +11 -5
  58. package/dist/wire/message/message.utils.d.ts +0 -10
  59. package/dist/wire/message/message.utils.js +1 -22
  60. package/dist/wire/message/poll-messages.command.d.ts +12 -1
  61. package/dist/wire/message/poll-messages.command.js +64 -16
  62. package/dist/wire/message/poll-messages.command.test.js +143 -5
  63. package/dist/wire/message/poll.utils.d.ts +4 -0
  64. package/dist/wire/message/poll.utils.js +6 -1
  65. package/dist/wire/message/send-messages.command.d.ts +3 -3
  66. package/dist/wire/message/send-messages.command.js +103 -3
  67. package/dist/wire/message/send-messages.command.test.js +113 -2
  68. package/dist/wire/offset/delete-offset.command.d.ts +11 -4
  69. package/dist/wire/offset/delete-offset.command.js +1 -1
  70. package/dist/wire/offset/get-offset.command.d.ts +1 -1
  71. package/dist/wire/offset/offset-write.command.test.d.ts +2 -0
  72. package/dist/wire/offset/offset-write.command.test.js +43 -0
  73. package/dist/wire/offset/offset.utils.js +10 -6
  74. package/dist/wire/offset/store-offset.command.d.ts +10 -0
  75. package/dist/wire/offset/store-offset.command.js +1 -1
  76. package/dist/wire/options.utils.d.ts +46 -0
  77. package/dist/wire/options.utils.js +57 -0
  78. package/dist/wire/partition/partition-route.d.ts +12 -0
  79. package/dist/wire/partition/partition-route.js +40 -0
  80. package/dist/wire/partition/partition-route.test.d.ts +2 -0
  81. package/dist/wire/partition/partition-route.test.js +42 -0
  82. package/dist/wire/session/login-with-token.command.d.ts +2 -2
  83. package/dist/wire/session/login-with-token.command.js +7 -2
  84. package/dist/wire/session/login.command.d.ts +2 -2
  85. package/dist/wire/session/login.command.js +7 -2
  86. package/dist/wire/stream/create-stream.command.d.ts +5 -2
  87. package/dist/wire/stream/create-stream.command.js +20 -2
  88. package/dist/wire/stream/index.d.ts +1 -1
  89. package/dist/wire/stream/index.js +1 -1
  90. package/dist/wire/stream/stream-options.d.ts +77 -0
  91. package/dist/wire/stream/stream-options.js +101 -0
  92. package/dist/wire/stream/stream-options.test.d.ts +2 -0
  93. package/dist/wire/stream/stream-options.test.js +103 -0
  94. package/dist/wire/stream/stream.utils.d.ts +9 -0
  95. package/dist/wire/stream/stream.utils.js +17 -3
  96. package/dist/wire/stream/update-stream.command.d.ts +23 -5
  97. package/dist/wire/stream/update-stream.command.js +10 -17
  98. package/dist/wire/system/describe-options.command.d.ts +23 -6
  99. package/dist/wire/system/describe-options.command.js +21 -2
  100. package/dist/wire/system/get-stats.command.d.ts +17 -0
  101. package/dist/wire/system/get-stats.command.js +69 -16
  102. package/dist/wire/system/get-stats.command.test.d.ts +2 -0
  103. package/dist/wire/system/get-stats.command.test.js +97 -0
  104. package/dist/wire/system/index.d.ts +2 -0
  105. package/dist/wire/system/index.js +2 -0
  106. package/dist/wire/system/snapshot.command.d.ts +64 -0
  107. package/dist/wire/system/snapshot.command.js +76 -0
  108. package/dist/wire/system/snapshot.command.test.d.ts +2 -0
  109. package/dist/wire/system/snapshot.command.test.js +86 -0
  110. package/dist/wire/system/update-resource.command.d.ts +53 -0
  111. package/dist/wire/system/update-resource.command.js +130 -0
  112. package/dist/wire/system/update-resource.command.test.d.ts +2 -0
  113. package/dist/wire/system/update-resource.command.test.js +497 -0
  114. package/dist/wire/token/create-token.command.d.ts +1 -1
  115. package/dist/wire/token/create-token.command.js +1 -1
  116. package/dist/wire/token/create-token.command.test.js +6 -0
  117. package/dist/wire/topic/create-topic.command.d.ts +20 -12
  118. package/dist/wire/topic/create-topic.command.js +11 -4
  119. package/dist/wire/topic/index.d.ts +1 -2
  120. package/dist/wire/topic/index.js +1 -2
  121. package/dist/wire/topic/topic.utils.d.ts +14 -5
  122. package/dist/wire/topic/topic.utils.js +26 -11
  123. package/dist/wire/topic/topic.utils.test.js +13 -13
  124. package/dist/wire/topic/update-topic.command.d.ts +22 -15
  125. package/dist/wire/topic/update-topic.command.js +12 -34
  126. package/dist/wire/user/create-user.command.d.ts +12 -2
  127. package/dist/wire/user/create-user.command.js +7 -3
  128. package/dist/wire/user/index.d.ts +1 -0
  129. package/dist/wire/user/index.js +1 -0
  130. package/dist/wire/user/update-user.command.d.ts +25 -7
  131. package/dist/wire/user/update-user.command.js +18 -31
  132. package/dist/wire/user/user.utils.d.ts +2 -0
  133. package/dist/wire/user/user.utils.js +5 -3
  134. package/dist/wire/vsr/header.d.ts +23 -8
  135. package/dist/wire/vsr/header.js +32 -10
  136. package/dist/wire/vsr/header.test.js +5 -1
  137. package/dist/wire/vsr/index.d.ts +8 -2
  138. package/dist/wire/vsr/index.js +44 -11
  139. package/dist/wire/vsr/operation.d.ts +18 -6
  140. package/dist/wire/vsr/operation.js +46 -16
  141. package/dist/wire/vsr/operation.test.js +32 -8
  142. package/dist/wire/vsr/register.d.ts +10 -6
  143. package/dist/wire/vsr/register.js +24 -7
  144. package/dist/wire/vsr/register.test.js +28 -8
  145. package/dist/wire/vsr/reply.d.ts +4 -1
  146. package/dist/wire/vsr/reply.js +42 -7
  147. package/dist/wire/vsr/reply.test.js +11 -1
  148. package/dist/wire/vsr/session.d.ts +9 -10
  149. package/dist/wire/vsr/session.js +32 -23
  150. package/dist/wire/vsr/session.test.js +33 -7
  151. package/dist/wire/vsr/vsr.test.js +57 -6
  152. package/package.json +6 -7
  153. package/dist/wire/message/flush-unsaved-buffers.command.d.ts +0 -26
  154. package/dist/wire/message/flush-unsaved-buffers.command.js +0 -36
  155. package/dist/wire/stream/purge-stream.command.d.ts +0 -22
  156. package/dist/wire/topic/purge-topic.command.d.ts +0 -24
  157. package/dist/wire/topic/purge-topic.command.js +0 -39
@@ -1,6 +1,7 @@
1
1
  import type { Readable } from 'stream';
2
2
  import { type TcpNetConnectOpts } from 'node:net';
3
3
  import { type ConnectionOptions } from 'node:tls';
4
+ import type { PartitionContext } from '../wire/vsr/header.js';
4
5
  /**
5
6
  * TCP socket connection options.
6
7
  * Alias for Node.js TcpNetConnectOpts, what net.createConnection accepts.
@@ -29,23 +30,33 @@ export type CommandResponse = {
29
30
  data: Buffer;
30
31
  };
31
32
  export type SendCommandOptions = {
33
+ /** Cancels this call without closing its shared replica link. */
34
+ signal?: AbortSignal;
32
35
  /** Whether the response uses the standard command response decoder */
33
36
  handleResponse?: boolean;
34
- /** Whether to append rather than prepend the command to the queue */
37
+ /** @deprecated Multiplexed calls ignore this queue-order hint. */
35
38
  last?: boolean;
36
- /**
37
- * Whether a not-admitted refusal re-checks the leader and re-issues the
38
- * command. False for the roster read a re-check itself runs: answering a
39
- * leader check with another leader check would recurse.
40
- */
39
+ /** @deprecated The shared session owner manages replica routing. */
41
40
  followsLeaderMoves?: boolean;
42
41
  /**
43
42
  * When the whole request gives up, as an epoch timestamp in milliseconds.
44
- * Set when a command already carries a budget -- one re-issued after a
45
- * leader move keeps the budget it was first submitted with, rather than
46
- * opening a second one on top of it. Defaults to a fresh response timeout.
43
+ * Includes admission, discovery, link waits and retries. Defaults to 30 seconds.
47
44
  */
48
45
  deadline?: number;
46
+ /**
47
+ * Partition context of a poll, an offset store or delete, or a send to an
48
+ * explicit partition, taken from an earlier reply. It replaces the context
49
+ * the client would capture, stays on every retry, and is never refreshed
50
+ * after a history refusal (87): work from one partition incarnation must
51
+ * not land in another.
52
+ */
53
+ context?: PartitionContext;
54
+ /**
55
+ * The `RawClient.lineage` that a call of several commands started with. A
56
+ * command of an older lineage fails with status 40 and sends nothing, so no
57
+ * part of the call runs under a session the caller opened after it started.
58
+ */
59
+ lineage?: number;
49
60
  };
50
61
  /**
51
62
  * Low-level client interface for communicating with the Iggy server.
@@ -56,11 +67,35 @@ export type RawClient = {
56
67
  sendCommand: (code: number, payload: Buffer, options?: SendCommandOptions) => Promise<CommandResponse>;
57
68
  /** Whether the client has been authenticated */
58
69
  isAuthenticated: boolean;
59
- /** Authenticates the client with the server */
70
+ /** Raised by each sign-in and logout of the caller. */
71
+ readonly lineage?: number;
72
+ /** Uses caller credentials; any failed attempt withdraws automatic sign-in. */
60
73
  authenticate: (c: ClientCredentials) => Promise<boolean>;
74
+ /** Typed login delegates validation to the session owner when supported. */
75
+ _authWithPassword?: (c: PasswordCredentials) => Promise<{
76
+ userId: number;
77
+ }>;
78
+ _authWithToken?: (c: TokenCredentials) => Promise<{
79
+ userId: number;
80
+ }>;
81
+ /**
82
+ * Resolves the original frozen operation; never sends it under a fresh identity.
83
+ * The client keeps the proved outcomes of its last `maxPendingOperations`
84
+ * replays, also across a session reset, so a later resolve of the same
85
+ * operation returns the same outcome without a request.
86
+ * `OperationNotRetainedError` means that a session reset or an abandon came
87
+ * before a replay proved the outcome, or that the proved outcome aged out.
88
+ */
89
+ resolveOperation?: (operationId: bigint, options?: SendCommandOptions) => Promise<void>;
90
+ /**
91
+ * Releases a retained lane without asserting that the operation did not commit.
92
+ * A replay that is already running keeps the lane until it ends, and the
93
+ * client keeps the outcome that it proves.
94
+ */
95
+ abandonOperation?: (operationId: bigint) => void;
61
96
  /** Destroys the client connection */
62
97
  destroy: () => void;
63
- /** Holds a pooled client across multiple command submissions */
98
+ /** Tracks one facade operation across multiple command submissions. */
64
99
  hold?: () => () => void;
65
100
  /** Registers an event listener */
66
101
  on: (ev: string, cb: (e?: unknown) => void) => void;
@@ -71,7 +106,7 @@ export type RawClient = {
71
106
  };
72
107
  /**
73
108
  * Function type that provides a RawClient instance.
74
- * Used for dependency injection and connection pooling.
109
+ * Used for dependency injection.
75
110
  */
76
111
  export type ClientProvider = () => Promise<RawClient>;
77
112
  /**
@@ -90,14 +125,15 @@ export type ReconnectOption = {
90
125
  /** Whether automatic reconnection is enabled */
91
126
  enabled: boolean;
92
127
  /**
93
- * Milliseconds to wait between passes. The first pass runs at once when more
94
- * than one endpoint is known.
128
+ * Initial reconnect delay in milliseconds. Each replica uses exponential
129
+ * backoff capped at 10 seconds with jitter.
95
130
  */
96
131
  interval: number;
97
132
  /**
98
- * Maximum number of passes over the known endpoints. One pass dials the
99
- * endpoint the client is on, the endpoint it was configured with, and every
100
- * node the roster named, so this counts passes rather than dials.
133
+ * Maximum retries per replica after the first attempt. Bootstrap retries
134
+ * sweep the configured seed and remembered endpoints. The budget limits one
135
+ * reconnect burst: while the client stays signed in, each heartbeat restarts
136
+ * a replica link that used its budget, with a fresh budget.
101
137
  */
102
138
  maxRetries: number;
103
139
  };
@@ -127,12 +163,12 @@ export type PasswordCredentials = {
127
163
  */
128
164
  export type ClientCredentials = TokenCredentials | PasswordCredentials;
129
165
  /**
130
- * Connection pool size configuration.
166
+ * Legacy logical-client count. Replica links are managed independently.
131
167
  */
132
168
  export type PoolSizeOption = {
133
- /** Minimum number of connections in the pool */
169
+ /** Minimum logical clients; normalized to one */
134
170
  min?: number;
135
- /** Maximum number of connections in the pool */
171
+ /** Maximum logical clients; only one is supported */
136
172
  max?: number;
137
173
  };
138
174
  /**
@@ -150,13 +186,20 @@ export type ClientConfig = {
150
186
  options: TransportOption;
151
187
  /** Authentication credentials */
152
188
  credentials: ClientCredentials;
153
- /** Connection pool size configuration */
189
+ /** Legacy logical-client count; does not limit replica links. */
154
190
  poolSize?: PoolSizeOption;
155
191
  /** Automatic reconnection configuration */
156
192
  reconnect?: ReconnectOption;
157
- /** Interval for sending heartbeat pings, in milliseconds */
193
+ /**
194
+ * Interval for sending heartbeat pings, in milliseconds. Each heartbeat also
195
+ * restarts replica links that used their reconnect budget. 0 disables both.
196
+ */
158
197
  heartbeatInterval?: number;
159
198
  /** Maximum accepted response frame size in bytes */
160
199
  maxResponseFrameSize?: number;
200
+ /** Maximum active plus retained mutations across this client. Default: 256. */
201
+ maxPendingOperations?: number;
202
+ /** Frozen mutation headers and bodies, including retained and writer-owned bytes. Default: 64 MiB. */
203
+ maxPendingOperationBytes?: number;
161
204
  };
162
205
  //# sourceMappingURL=client.type.d.ts.map
@@ -1,5 +1,6 @@
1
1
  export { Client, SimpleClient, SingleClient } from './client.js';
2
2
  export * from './client.config.js';
3
+ export { ConnectionClosedError } from './client.connection.js';
3
4
  export { parseConnectionString } from './client.connection-string.js';
4
5
  export * from './client.utils.js';
5
6
  export * from './client.socket.js';
@@ -16,6 +16,7 @@
16
16
  // under the License.
17
17
  export { Client, SimpleClient, SingleClient } from './client.js';
18
18
  export * from './client.config.js';
19
+ export { ConnectionClosedError } from './client.connection.js';
19
20
  export { parseConnectionString } from './client.connection-string.js';
20
21
  export * from './client.utils.js';
21
22
  export * from './client.socket.js';
@@ -18,7 +18,6 @@ import assert from 'node:assert/strict';
18
18
  import { Client } from './client/client.js';
19
19
  try {
20
20
  const credentials = { username: 'iggy', password: 'iggy' };
21
- // pooled client
22
21
  const c = new Client({
23
22
  transport: 'TCP',
24
23
  options: { port: 8090, host: '127.0.0.1' },
@@ -28,8 +27,7 @@ try {
28
27
  console.log('R2', await c.user.list());
29
28
  console.log('R3', await c.client.list());
30
29
  console.log('R4', await c.system.getStats());
31
- // serial call should keep socket pool to 1
32
- assert.equal(c._pool.size, 1);
30
+ const raw = await c.clientProvider();
33
31
  const resp = await Promise.all([
34
32
  c.user.list(),
35
33
  c.stream.list(),
@@ -42,8 +40,8 @@ try {
42
40
  c.system.getStats(),
43
41
  ]);
44
42
  console.log('parallel call resp', resp);
45
- // serial call should raise socket pool to MAX_POOL
46
- assert.equal(c._pool.size, 4);
43
+ assert.equal(await c.clientProvider(), raw);
44
+ await c.destroy();
47
45
  }
48
46
  catch (err) {
49
47
  console.error('FAILED', err);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=resource-options.e2e.d.ts.map
@@ -14,23 +14,15 @@
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 { wrapCommand } from '../command.utils.js';
18
- import { COMMAND_CODE } from '../command.code.js';
19
- import { deserializeVoidResponse } from '../../client/client.utils.js';
20
- import { serializeIdentifier } from '../identifier.utils.js';
21
- /**
22
- * Purge stream command definition.
23
- * Deletes all messages from a stream while keeping the stream itself.
24
- */
25
- export const PURGE_STREAM = {
26
- code: COMMAND_CODE.PurgeStream,
27
- serialize: ({ streamId }) => {
28
- return serializeIdentifier(streamId);
29
- },
30
- deserialize: deserializeVoidResponse
31
- };
32
- /**
33
- * Executable purge stream command function.
34
- */
35
- export const purgeStream = wrapCommand(PURGE_STREAM);
36
- //# sourceMappingURL=purge-stream.command.js.map
17
+ // These tests need a server started with encryption enabled and a config key
18
+ // (IGGY_ENCRYPTION_ENABLED=true, IGGY_ENCRYPTION_KEY), so a stream created
19
+ // without a key reports a server-generated one.
20
+ import { Client } from '../client/client.js';
21
+ import { getIggyAddress } from '../tcp.sm.utils.js';
22
+ import { EncryptionKeySource } from '../wire/index.js';
23
+ import { describeResourceOptions } from './resource-options.suite.js';
24
+ const [host, port] = getIggyAddress();
25
+ describeResourceOptions('e2e -> resource options', 'e2e-ro', (credentials) => new Client({
26
+ transport: 'TCP', options: { host, port }, credentials
27
+ }), EncryptionKeySource.Server);
28
+ //# sourceMappingURL=resource-options.e2e.js.map
@@ -0,0 +1,15 @@
1
+ import type { Client } from '../client/client.js';
2
+ import { EncryptionKeySource } from '../wire/index.js';
3
+ type Credentials = {
4
+ username: string;
5
+ password: string;
6
+ };
7
+ /**
8
+ * Registers the resource options cases for one transport. `connect` builds a
9
+ * client of that transport signed in with the given credentials.
10
+ * `keylessSource` is what a stream created without a key reports: `server`
11
+ * when the server runs with encryption enabled, `none` otherwise.
12
+ */
13
+ export declare const describeResourceOptions: (label: string, prefix: string, connect: (credentials: Credentials) => Client, keylessSource: EncryptionKeySource, skip?: string | false) => Promise<void>;
14
+ export {};
15
+ //# sourceMappingURL=resource-options.suite.d.ts.map
@@ -0,0 +1,272 @@
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 { randomBytes } from 'node:crypto';
18
+ import { after, describe, it } from 'node:test';
19
+ import assert from 'node:assert/strict';
20
+ import { HeaderValue } from '../wire/message/header.utils.js';
21
+ import { ResponseError } from '../wire/error.utils.js';
22
+ import { Consumer, EncryptionKeySource, OptionsScope, PartitionResizePolicy, Partitioning, PollingStrategy, StreamEncryptionKey, UserStatus, encryptionKeyFingerprint } from '../wire/index.js';
23
+ const NEVER_RETURNED = ['encryption_key', 'encryption_key_wrapped', 'critical_keys'];
24
+ const FINGERPRINT = /^[0-9a-f]{16}$/;
25
+ const UNLIMITED = 18446744073709551615n;
26
+ const UNSUPPORTED_OPTION_KEY = 4041;
27
+ const OPTION_NOT_UPDATABLE = 4103;
28
+ const assertProjection = (stream, source, fingerprint) => {
29
+ assert.equal(stream.encryptionKeySource, source);
30
+ if (source === EncryptionKeySource.None) {
31
+ assert.equal(stream.encryptionKeyFingerprint, null);
32
+ assert.deepEqual(stream.encryptionKekFingerprints, []);
33
+ return;
34
+ }
35
+ assert.match(stream.encryptionKeyFingerprint ?? '', FINGERPRINT);
36
+ if (fingerprint !== undefined)
37
+ assert.equal(stream.encryptionKeyFingerprint, fingerprint);
38
+ assert.ok(stream.encryptionKekFingerprints.length >= 1);
39
+ for (const kek of stream.encryptionKekFingerprints)
40
+ assert.match(kek, FINGERPRINT);
41
+ };
42
+ const assertNoKeyMaterial = (stream, key) => {
43
+ for (const block of [stream.options, stream.derivedOptions]) {
44
+ for (const name of NEVER_RETURNED)
45
+ assert.equal(name in block, false, `${name} was returned`);
46
+ if (key)
47
+ for (const value of Object.values(block))
48
+ assert.ok(!(Buffer.isBuffer(value) && value.equals(key)) && value !== key.toString('base64'), 'a returned option holds the key');
49
+ }
50
+ };
51
+ /**
52
+ * Registers the resource options cases for one transport. `connect` builds a
53
+ * client of that transport signed in with the given credentials.
54
+ * `keylessSource` is what a stream created without a key reports: `server`
55
+ * when the server runs with encryption enabled, `none` otherwise.
56
+ */
57
+ export const describeResourceOptions = (label, prefix, connect, keylessSource, skip = false) => describe(label, { skip }, async () => {
58
+ const c = connect({ username: 'iggy', password: 'iggy' });
59
+ it('given stream key when creating should read matching projection', async () => {
60
+ const key = randomBytes(32);
61
+ const name = `${prefix}-keyed`;
62
+ const created = await c.stream.create({ name, encryptionKey: StreamEncryptionKey.from(key) });
63
+ try {
64
+ assertProjection(created, EncryptionKeySource.Stream, encryptionKeyFingerprint(key));
65
+ assertNoKeyMaterial(created, key);
66
+ const fetched = await c.stream.get({ streamId: name });
67
+ assert.ok(fetched);
68
+ assertProjection(fetched, EncryptionKeySource.Stream, encryptionKeyFingerprint(key));
69
+ assert.deepEqual(fetched.encryptionKekFingerprints, created.encryptionKekFingerprints);
70
+ assertNoKeyMaterial(fetched, key);
71
+ const listed = (await c.stream.list()).find((stream) => stream.id === created.id);
72
+ assert.ok(listed);
73
+ assertProjection(listed, EncryptionKeySource.Stream, encryptionKeyFingerprint(key));
74
+ assertNoKeyMaterial(listed, key);
75
+ const base64 = `${prefix}-keyed-base64`;
76
+ const fromText = await c.stream.create({
77
+ name: base64, encryptionKey: StreamEncryptionKey.from(key.toString('base64'))
78
+ });
79
+ assert.equal(fromText.encryptionKeyFingerprint, encryptionKeyFingerprint(key));
80
+ await c.stream.delete({ streamId: fromText.id });
81
+ }
82
+ finally {
83
+ await c.stream.delete({ streamId: created.id });
84
+ }
85
+ });
86
+ it(`given keyless stream when reading should report key source ${keylessSource}`, async () => {
87
+ const created = await c.stream.create({ name: `${prefix}-keyless` });
88
+ try {
89
+ assertProjection(created, keylessSource);
90
+ const fetched = await c.stream.get({ streamId: created.id });
91
+ assert.ok(fetched);
92
+ assertProjection(fetched, keylessSource, created.encryptionKeyFingerprint ?? undefined);
93
+ assertNoKeyMaterial(fetched);
94
+ }
95
+ finally {
96
+ await c.stream.delete({ streamId: created.id });
97
+ }
98
+ });
99
+ it('given stream rename when updating should preserve encryption projection', async () => {
100
+ const key = randomBytes(32);
101
+ const created = await c.stream.create({
102
+ name: `${prefix}-rename`, encryptionKey: StreamEncryptionKey.from(key)
103
+ });
104
+ try {
105
+ await c.topic.create({ streamId: created.id, name: 'events', partitionCount: 1 });
106
+ await c.message.send({
107
+ streamId: created.id, topicId: 'events', partition: Partitioning.PartitionId(0),
108
+ messages: [{ payload: 'before-rename', headers: [] }]
109
+ });
110
+ assert.ok(await c.stream.update({ streamId: created.id, name: `${prefix}-renamed` }));
111
+ await assert.rejects(c.stream.update({ streamId: created.id, reset: ['encryption_key'] }), (error) => error instanceof ResponseError &&
112
+ error.errorCode === OPTION_NOT_UPDATABLE && !error.fromServer);
113
+ const renamed = await c.stream.get({ streamId: created.id });
114
+ assert.ok(renamed);
115
+ assert.equal(renamed.id, created.id);
116
+ assert.equal(renamed.name, `${prefix}-renamed`);
117
+ assertProjection(renamed, EncryptionKeySource.Stream, encryptionKeyFingerprint(key));
118
+ assert.deepEqual(renamed.encryptionKekFingerprints, created.encryptionKekFingerprints);
119
+ const polled = await c.message.poll({
120
+ streamId: created.id, topicId: 'events', consumer: Consumer.Single, partitionId: 0,
121
+ pollingStrategy: PollingStrategy.First, count: 10, autocommit: false
122
+ });
123
+ assert.deepEqual(polled.messages.map((message) => message.payload.toString()), ['before-rename']);
124
+ }
125
+ finally {
126
+ await c.stream.delete({ streamId: created.id });
127
+ }
128
+ });
129
+ it('given topic reset when updating should preserve unmentioned values', async () => {
130
+ const stream = await c.stream.create({ name: `${prefix}-topic-reset` });
131
+ try {
132
+ const topic = await c.topic.create({
133
+ streamId: stream.id, name: 'settings', partitionCount: 1,
134
+ compressionAlgorithm: 2, messageExpiry: 3600000000n, maxTopicSize: 1073741824n
135
+ });
136
+ assert.ok(await c.topic.update({
137
+ streamId: stream.id, topicId: topic.id,
138
+ messageExpiry: 7200000000n, reset: ['max_topic_size']
139
+ }));
140
+ const updated = await c.topic.get({ streamId: stream.id, topicId: topic.id });
141
+ assert.ok(updated);
142
+ assert.equal(updated.name, 'settings');
143
+ assert.equal(updated.options.compression_algorithm, 'gzip');
144
+ assert.equal(updated.compressionAlgorithm, 2);
145
+ assert.equal(updated.options.message_expiry, 7200000000n);
146
+ assert.equal(updated.messageExpiry, 7200000000n);
147
+ assert.equal('max_topic_size' in updated.options, false);
148
+ assert.equal(updated.derivedOptions.max_topic_size, UNLIMITED);
149
+ assert.equal(updated.maxTopicSize, UNLIMITED);
150
+ assert.deepEqual(updated.derivedOptions, {
151
+ ...topic.derivedOptions, max_topic_size: UNLIMITED
152
+ });
153
+ }
154
+ finally {
155
+ await c.stream.delete({ streamId: stream.id });
156
+ }
157
+ });
158
+ it('given user options when creating and updating should preserve user behavior', async () => {
159
+ const password = 'e2e-ro-password';
160
+ const first = await c.user.create({
161
+ username: `${prefix}-user`, password, status: UserStatus.Active, options: []
162
+ });
163
+ const second = await c.user.create({
164
+ username: `${prefix}-user-taken`, password, status: UserStatus.Active
165
+ });
166
+ try {
167
+ assert.deepEqual(first.options, {});
168
+ assert.deepEqual(first.derivedOptions, {});
169
+ assert.ok(await c.user.update({
170
+ userId: first.id, username: `${prefix}-user-renamed`, status: UserStatus.Inactive
171
+ }));
172
+ const updated = await c.user.get({ userId: first.id });
173
+ assert.ok(updated);
174
+ assert.equal(updated.userName, `${prefix}-user-renamed`);
175
+ assert.equal(updated.status, 'Inactive');
176
+ assert.deepEqual(updated.options, {});
177
+ await assert.rejects(c.user.update({ userId: first.id, username: `${prefix}-user-taken`, status: UserStatus.Active }), (error) => error instanceof ResponseError && error.fromServer);
178
+ const unchanged = await c.user.get({ userId: first.id });
179
+ assert.ok(unchanged);
180
+ assert.equal(unchanged.userName, `${prefix}-user-renamed`);
181
+ assert.equal(unchanged.status, 'Inactive');
182
+ }
183
+ finally {
184
+ await c.user.delete({ userId: first.id });
185
+ await c.user.delete({ userId: second.id });
186
+ }
187
+ });
188
+ it('given logged in user rename when reconnecting should keep new username', async () => {
189
+ const password = 'e2e-ro-self-password';
190
+ const user = await c.user.create({
191
+ username: `${prefix}-self`, password, status: UserStatus.Active,
192
+ permissions: {
193
+ global: {
194
+ ManageServers: false, ReadServers: false, ManageUsers: true, ReadUsers: true,
195
+ ManageStreams: true, ReadStreams: true, ManageTopics: true, ReadTopics: true,
196
+ PollMessages: false, SendMessages: false
197
+ },
198
+ streams: []
199
+ }
200
+ });
201
+ const taken = await c.user.create({
202
+ username: `${prefix}-self-taken`, password, status: UserStatus.Active
203
+ });
204
+ const self = connect({ username: `${prefix}-self`, password });
205
+ const signedInAs = async (username) => {
206
+ await self.session.logout();
207
+ await self.session.login({ username, password });
208
+ const me = await self.client.getMe();
209
+ return (await c.user.get({ userId: me.userId }))?.userName;
210
+ };
211
+ try {
212
+ assert.equal((await self.client.getMe()).userId, user.id);
213
+ await assert.rejects(self.user.update({ userId: user.id, username: `${prefix}-self-taken` }), (error) => error instanceof ResponseError && error.fromServer);
214
+ assert.equal(await signedInAs(`${prefix}-self`), `${prefix}-self`);
215
+ const stream = await self.stream.create({ name: `${prefix}-self-stream` });
216
+ await self.stream.update({ streamId: stream.id, name: `${prefix}-self` });
217
+ const topic = await self.topic.create({ streamId: stream.id, name: 'events', partitionCount: 1 });
218
+ await self.topic.update({ streamId: stream.id, topicId: topic.id, name: `${prefix}-self-topic` });
219
+ await self.stream.delete({ streamId: stream.id });
220
+ assert.equal(await signedInAs(`${prefix}-self`), `${prefix}-self`);
221
+ assert.ok(await self.user.update({ userId: user.id, username: `${prefix}-self-renamed` }));
222
+ assert.equal(await signedInAs(`${prefix}-self-renamed`), `${prefix}-self-renamed`);
223
+ }
224
+ finally {
225
+ await self.destroy();
226
+ await c.user.delete({ userId: user.id });
227
+ await c.user.delete({ userId: taken.id });
228
+ }
229
+ });
230
+ it('given raw create-only key or unknown raw reset when updating should let the server refuse', async () => {
231
+ const stream = await c.stream.create({ name: `${prefix}-raw` });
232
+ try {
233
+ const topic = await c.topic.create({
234
+ streamId: stream.id, name: 'fixed', partitionCount: 1,
235
+ partitionResizePolicy: PartitionResizePolicy.Fixed
236
+ });
237
+ assert.equal(topic.options.partition_resize_policy, PartitionResizePolicy.Fixed);
238
+ const fromServer = (code) => (error) => error instanceof ResponseError && error.errorCode === code && error.fromServer;
239
+ await assert.rejects(c.topic.update({
240
+ streamId: stream.id, topicId: topic.id,
241
+ options: [{ key: 'compression_algorithm', value: HeaderValue.String('gzip') }]
242
+ }), fromServer(OPTION_NOT_UPDATABLE));
243
+ await assert.rejects(c.topic.update({ streamId: stream.id, topicId: topic.id, rawReset: ['future_option'] }), fromServer(UNSUPPORTED_OPTION_KEY));
244
+ const unchanged = await c.topic.get({ streamId: stream.id, topicId: topic.id });
245
+ assert.equal(unchanged?.compressionAlgorithm, topic.compressionAlgorithm);
246
+ }
247
+ finally {
248
+ await c.stream.delete({ streamId: stream.id });
249
+ }
250
+ });
251
+ it('given stream scope when describing options should report sensitivity and mutability', async () => {
252
+ const specs = await c.system.describeOptions({ scope: OptionsScope.Stream });
253
+ const byKey = new Map(specs.map((spec) => [spec.key, spec]));
254
+ assert.deepEqual([...byKey.keys()], [
255
+ 'name', 'encryption_key', 'encryption_key_source',
256
+ 'encryption_key_fingerprint', 'encryption_kek_fingerprint'
257
+ ]);
258
+ assert.equal(byKey.get('name')?.updatable, true);
259
+ assert.equal(byKey.get('name')?.secret, false);
260
+ assert.equal(byKey.get('encryption_key')?.updatable, false);
261
+ assert.equal(byKey.get('encryption_key')?.secret, true);
262
+ assert.equal(byKey.get('encryption_key')?.critical, true);
263
+ assert.equal(byKey.get('encryption_key_fingerprint')?.readOnly, true);
264
+ });
265
+ it(`${label}::cleanup`, async () => {
266
+ assert.ok(await c.session.logout());
267
+ });
268
+ after(() => {
269
+ c.destroy();
270
+ });
271
+ });
272
+ //# sourceMappingURL=resource-options.suite.js.map
@@ -24,6 +24,7 @@ const expectedMeta = {
24
24
  name: "single-node",
25
25
  nodes: [
26
26
  {
27
+ replicaId: 0,
27
28
  name: "iggy-node",
28
29
  ip: "127.0.0.1",
29
30
  endpoints: {
@@ -80,6 +80,10 @@ describe('e2e -> consumer-stream', async () => {
80
80
  });
81
81
  });
82
82
  after(async () => {
83
+ // The member that did not get the last batch can still be polling,
84
+ // and a poll that fails after the delete has no error listener.
85
+ s.destroy();
86
+ s2.destroy();
83
87
  assert.ok(await c.stream.delete({ streamId: streamName }));
84
88
  assert.ok(await c.session.logout());
85
89
  await c.destroy();
@@ -20,30 +20,29 @@ import { getTestClient } from './test-client.utils.js';
20
20
  describe('e2e -> parallel', async () => {
21
21
  const c = getTestClient();
22
22
  const baseGetMe = await c.client.getMe();
23
- const credentials = { username: 'iggy', password: 'iggy' };
24
23
  it('e2e -> parallel::mix calls', async () => {
25
24
  const resp = await Promise.all([
26
25
  c.client.getMe(),
27
26
  c.system.getStats(),
28
27
  c.system.ping(),
29
- c.session.login(credentials),
30
28
  c.client.getMe(),
31
29
  c.system.getStats(),
32
30
  c.system.ping(),
33
- c.session.login(credentials),
34
31
  c.client.getMe(),
35
32
  c.system.getStats(),
36
33
  c.system.ping(),
37
- c.session.login(credentials),
38
34
  ]);
39
- assert.ok(resp.length === 12);
35
+ assert.equal(resp.length, 9);
40
36
  assert.deepEqual(resp[0], baseGetMe);
41
37
  assert.deepEqual(Object.keys(resp[1]), [
42
38
  'processId', 'cpuUsage', 'totalCpuUsage', 'memoryUsage', 'totalMemory',
43
39
  'availableMemory', 'runTime', 'startTime', 'readBytes', 'writtenBytes',
44
40
  'messagesSizeBytes', 'streamsCount', 'topicsCount', 'partitionsCount',
45
41
  'segmentsCount', 'messagesCount', 'clientsCount', 'consumersGroupsCount',
46
- 'hostname', 'osName', 'osVersion', 'kernelVersion'
42
+ 'hostname', 'osName', 'osVersion', 'kernelVersion',
43
+ 'iggyServerVersion', 'iggyServerSemver', 'cacheMetrics',
44
+ 'threadsCount', 'freeDiskSpace', 'totalDiskSpace',
45
+ 'openFilesCount', 'openFilesLimit'
47
46
  ]);
48
47
  resp.forEach(r => assert.ok(r));
49
48
  });
@@ -40,9 +40,6 @@ describe('e2e -> stream', async () => {
40
40
  });
41
41
  assert.ok(stream);
42
42
  });
43
- it('e2e -> stream::purge', async () => {
44
- assert.ok(await c.stream.purge({ streamId: name2 }));
45
- });
46
43
  it('e2e -> stream::delete', async () => {
47
44
  assert.ok(await c.stream.delete({ streamId: name2 }));
48
45
  });
@@ -31,9 +31,18 @@ describe('e2e -> system', async () => {
31
31
  'availableMemory', 'runTime', 'startTime', 'readBytes', 'writtenBytes',
32
32
  'messagesSizeBytes', 'streamsCount', 'topicsCount', 'partitionsCount',
33
33
  'segmentsCount', 'messagesCount', 'clientsCount', 'consumersGroupsCount',
34
- 'hostname', 'osName', 'osVersion', 'kernelVersion'
34
+ 'hostname', 'osName', 'osVersion', 'kernelVersion',
35
+ 'iggyServerVersion', 'iggyServerSemver', 'cacheMetrics',
36
+ 'threadsCount', 'freeDiskSpace', 'totalDiskSpace',
37
+ 'openFilesCount', 'openFilesLimit'
35
38
  ]);
36
39
  });
40
+ it('e2e -> system::snapshot', async () => {
41
+ const data = await c.system.snapshot();
42
+ assert.ok(data.length > 0);
43
+ assert.equal(data[0], 0x50);
44
+ assert.equal(data[1], 0x4B);
45
+ });
37
46
  it('e2e -> system::logout', async () => {
38
47
  assert.ok(await c.session.logout());
39
48
  });
@@ -20,13 +20,19 @@ import { getTestClient } from './test-client.utils.js';
20
20
  describe('e2e -> token', async () => {
21
21
  const c = getTestClient();
22
22
  const tokenName = 'yolo-token-test';
23
+ const lifetimeMs = 30 * 60 * 1000;
24
+ let createdAt = 0;
23
25
  it('e2e -> token::create', async () => {
24
- const tk = await c.token.create({ name: tokenName, expiry: 1800n });
26
+ createdAt = Date.now();
27
+ const tk = await c.token.create({ name: tokenName, expiry: BigInt(lifetimeMs) * 1000n });
25
28
  assert.ok(tk.token.length > 1);
26
29
  });
27
30
  it('e2e -> token::list', async () => {
28
31
  const tks = await c.token.list();
29
- assert.ok(tks.length > 0);
32
+ const listed = tks.find((tk) => tk.name === tokenName);
33
+ assert.ok(listed?.expiry, 'the token lists its expiry');
34
+ const remainingMs = listed.expiry.getTime() - createdAt;
35
+ assert.ok(Math.abs(remainingMs - lifetimeMs) < 5000, `expiry ${remainingMs} ms after create, want ${lifetimeMs}`);
30
36
  });
31
37
  it('e2e -> token::delete', async () => {
32
38
  assert.ok(await c.token.delete({ name: tokenName }));