apache-iggy 0.9.0-edge.1 → 0.10.0-edge.1
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.
- package/README.md +15 -10
- package/dist/client/client.config.d.ts +2 -4
- package/dist/client/client.config.js +2 -8
- package/dist/client/client.config.test.js +5 -22
- package/dist/client/client.connection.d.ts +0 -8
- package/dist/client/client.connection.js +2 -15
- package/dist/client/client.connection.test.js +18 -17
- package/dist/client/client.frame.d.ts +2 -5
- package/dist/client/client.frame.js +12 -22
- package/dist/client/client.frame.test.js +38 -53
- package/dist/client/client.socket.d.ts +1 -4
- package/dist/client/client.socket.js +2 -18
- package/dist/client/client.socket.test.js +0 -1
- package/dist/client/client.type.d.ts +0 -6
- package/dist/client/client.utils.d.ts +0 -29
- package/dist/client/client.utils.js +0 -56
- package/dist/client/client.utils.test.js +9 -20
- package/dist/e2e/tcp.cluster.e2e.js +5 -16
- package/dist/e2e/tcp.consumer-group.e2e.js +1 -5
- package/dist/e2e/tcp.consumer-stream.e2e.js +1 -5
- package/dist/e2e/tcp.send-message.e2e.js +0 -11
- package/dist/e2e/test-client.utils.js +0 -2
- package/dist/e2e/tls.system.e2e.js +3 -5
- package/dist/wire/cluster/cluster.type.d.ts +2 -2
- package/dist/wire/command-set.test.js +0 -1
- package/dist/wire/message/header.type.d.ts +1 -1
- package/dist/wire/message/poll-messages.command.js +1 -2
- package/dist/wire/vsr/namespace.js +2 -0
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -32,18 +32,17 @@ npm i --save apache-iggy
|
|
|
32
32
|
|
|
33
33
|
### Response frame limit
|
|
34
34
|
|
|
35
|
-
**Compatibility note:** response frames larger than `maxResponseFrameSize` (default 64 MiB) are
|
|
35
|
+
**Compatibility note:** response frames larger than `maxResponseFrameSize` (default 64 MiB) are rejected and close the connection. Raise the limit in the client configuration when polling very large batches.
|
|
36
36
|
|
|
37
37
|
### VSR framing
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
The SDK speaks the VSR wire protocol exclusively and requires an Iggy VSR
|
|
40
|
+
server:
|
|
41
41
|
|
|
42
42
|
```typescript
|
|
43
43
|
import { SimpleClient, getRawClient } from "apache-iggy";
|
|
44
44
|
|
|
45
45
|
const config = {
|
|
46
|
-
protocol: "vsr" as const,
|
|
47
46
|
transport: "TCP" as const,
|
|
48
47
|
options: { host: "127.0.0.1", port: 8090 },
|
|
49
48
|
credentials: { username: "iggy", password: "iggy" },
|
|
@@ -52,12 +51,18 @@ const client = new SimpleClient(getRawClient(config));
|
|
|
52
51
|
const stats = await client.system.getStats();
|
|
53
52
|
```
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
for classifying or rejecting extension commands.
|
|
54
|
+
Codes absent from the SDK command table use `Operation::NonReplicated` and
|
|
55
|
+
carry the command code in the request header's reserved field. The server
|
|
56
|
+
remains authoritative for classifying or rejecting extension commands.
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
Sends must use explicit `Partitioning.PartitionId` partitioning: the client
|
|
59
|
+
routes each request to a partition-scoped namespace, so broker-side balancing
|
|
60
|
+
(`Partitioning.Balanced`) and key hashing (`Partitioning.MessageKey`) are
|
|
61
|
+
rejected before the request is sent.
|
|
62
|
+
<!-- TODO(hubcio): Balanced and MessageKey partitioning to be implemented;
|
|
63
|
+
not decided yet whether it'll be on server side or client side. -->
|
|
64
|
+
|
|
65
|
+
VSR works over TCP and TLS. It restricts `Client` to one pooled connection because authentication, request sequencing, and consumer-group assignments belong to one consensus session. Configurations requesting more than one pooled connection fail before a socket is opened.
|
|
61
66
|
|
|
62
67
|
VSR authentication translates the existing password and personal-access-token
|
|
63
68
|
login APIs into the register handshake required by the consensus protocol. A
|
|
@@ -68,7 +73,7 @@ new session.
|
|
|
68
73
|
|
|
69
74
|
When the server's `[heartbeat]` eviction is enabled, configure the client's `heartbeatInterval` below the server heartbeat interval. Client heartbeats are disabled when `heartbeatInterval` is unset.
|
|
70
75
|
|
|
71
|
-
`sendBinaryRequest(code, payload)`
|
|
76
|
+
`sendBinaryRequest(code, payload)` sends an arbitrary command code. Known replicated commands use their registered operation, while unknown codes reach the server as non-replicated requests and are rejected by servers that do not register them.
|
|
72
77
|
|
|
73
78
|
```typescript
|
|
74
79
|
import { ResponseError } from "apache-iggy";
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type { ClientConfig
|
|
1
|
+
import type { ClientConfig } from './client.type.js';
|
|
2
2
|
export declare const DEFAULT_MAX_RESPONSE_FRAME_SIZE: number;
|
|
3
|
-
export declare const normalizeClientConfig: (config: ClientConfig) => ClientConfig
|
|
4
|
-
protocol: Protocol;
|
|
5
|
-
};
|
|
3
|
+
export declare const normalizeClientConfig: (config: ClientConfig) => ClientConfig;
|
|
6
4
|
//# sourceMappingURL=client.config.d.ts.map
|
|
@@ -15,24 +15,18 @@
|
|
|
15
15
|
// specific language governing permissions and limitations
|
|
16
16
|
// under the License.
|
|
17
17
|
export const DEFAULT_MAX_RESPONSE_FRAME_SIZE = 64 * 1024 * 1024;
|
|
18
|
-
const isProtocol = (value) => value === 'classic' || value === 'vsr';
|
|
19
18
|
export const normalizeClientConfig = (config) => {
|
|
20
|
-
const protocol = config.protocol ?? 'classic';
|
|
21
|
-
if (!isProtocol(protocol))
|
|
22
|
-
throw new TypeError(`unsupported wire protocol: ${String(protocol)}`);
|
|
23
19
|
const maxResponseFrameSize = config.maxResponseFrameSize ?? DEFAULT_MAX_RESPONSE_FRAME_SIZE;
|
|
24
20
|
if (!Number.isSafeInteger(maxResponseFrameSize) ||
|
|
25
21
|
maxResponseFrameSize < 256)
|
|
26
22
|
throw new TypeError('maxResponseFrameSize must be a safe integer of at least 256 bytes');
|
|
27
|
-
if (
|
|
28
|
-
((config.poolSize?.min ?? 1) > 1 || (config.poolSize?.max ?? 1) > 1))
|
|
23
|
+
if ((config.poolSize?.min ?? 1) > 1 || (config.poolSize?.max ?? 1) > 1)
|
|
29
24
|
throw new TypeError('VSR clients currently support exactly one pooled connection');
|
|
30
25
|
return {
|
|
31
26
|
...config,
|
|
32
|
-
protocol,
|
|
33
27
|
options: { ...config.options },
|
|
34
28
|
maxResponseFrameSize,
|
|
35
|
-
|
|
29
|
+
poolSize: { min: 1, max: 1 }
|
|
36
30
|
};
|
|
37
31
|
};
|
|
38
32
|
//# sourceMappingURL=client.config.js.map
|
|
@@ -23,40 +23,23 @@ const config = () => ({
|
|
|
23
23
|
credentials: { username: 'iggy', password: 'iggy' }
|
|
24
24
|
});
|
|
25
25
|
describe('normalizeClientConfig', () => {
|
|
26
|
-
it('
|
|
27
|
-
const normalized = normalizeClientConfig(
|
|
28
|
-
...config(),
|
|
29
|
-
poolSize: { min: 2, max: 4 }
|
|
30
|
-
});
|
|
31
|
-
assert.equal(normalized.protocol, 'classic');
|
|
32
|
-
assert.deepEqual(normalized.poolSize, { min: 2, max: 4 });
|
|
26
|
+
it('applies the default response frame limit', () => {
|
|
27
|
+
const normalized = normalizeClientConfig(config());
|
|
33
28
|
assert.equal(normalized.maxResponseFrameSize, DEFAULT_MAX_RESPONSE_FRAME_SIZE);
|
|
34
29
|
});
|
|
35
|
-
it('restricts
|
|
36
|
-
const normalized = normalizeClientConfig(
|
|
37
|
-
...config(),
|
|
38
|
-
protocol: 'vsr'
|
|
39
|
-
});
|
|
30
|
+
it('restricts the client to one pooled connection', () => {
|
|
31
|
+
const normalized = normalizeClientConfig(config());
|
|
40
32
|
assert.deepEqual(normalized.poolSize, { min: 1, max: 1 });
|
|
41
33
|
assert.throws(() => normalizeClientConfig({
|
|
42
34
|
...config(),
|
|
43
|
-
protocol: 'vsr',
|
|
44
35
|
poolSize: { max: 2 }
|
|
45
36
|
}), /exactly one pooled connection/);
|
|
46
37
|
});
|
|
47
|
-
it('
|
|
48
|
-
assert.throws(() => normalizeClientConfig({
|
|
49
|
-
...config(),
|
|
50
|
-
protocol: 'auto'
|
|
51
|
-
}), /unsupported wire protocol/);
|
|
52
|
-
});
|
|
53
|
-
it('supports VSR over TLS', () => {
|
|
38
|
+
it('supports TLS transport', () => {
|
|
54
39
|
const normalized = normalizeClientConfig({
|
|
55
40
|
...config(),
|
|
56
|
-
protocol: 'vsr',
|
|
57
41
|
transport: 'TLS'
|
|
58
42
|
});
|
|
59
|
-
assert.equal(normalized.protocol, 'vsr');
|
|
60
43
|
assert.equal(normalized.transport, 'TLS');
|
|
61
44
|
assert.deepEqual(normalized.poolSize, { min: 1, max: 1 });
|
|
62
45
|
});
|
|
@@ -79,14 +79,6 @@ export declare class IggyConnection extends EventEmitter {
|
|
|
79
79
|
* @param data - Incoming data buffer
|
|
80
80
|
*/
|
|
81
81
|
_onData(data: Buffer): void;
|
|
82
|
-
/**
|
|
83
|
-
* Writes a command to the socket.
|
|
84
|
-
*
|
|
85
|
-
* @param command - Command code
|
|
86
|
-
* @param payload - Command payload
|
|
87
|
-
* @returns True if the write was successful
|
|
88
|
-
*/
|
|
89
|
-
writeCommand(command: number, payload: Buffer): void;
|
|
90
82
|
writeFrame(frame: Buffer): void;
|
|
91
83
|
}
|
|
92
84
|
//# sourceMappingURL=client.connection.d.ts.map
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
import { EventEmitter } from 'node:events';
|
|
19
19
|
import { createConnection } from 'node:net';
|
|
20
20
|
import { connect as TLSConnect } from 'node:tls';
|
|
21
|
-
import { serializeCommand } from './client.utils.js';
|
|
22
21
|
import { debug } from './client.debug.js';
|
|
23
22
|
import { DEFAULT_MAX_RESPONSE_FRAME_SIZE } from './client.config.js';
|
|
24
23
|
import { ProtocolFrameError, ResponseFrameDecoder } from './client.frame.js';
|
|
@@ -121,7 +120,7 @@ export class IggyConnection extends EventEmitter {
|
|
|
121
120
|
this.reconnectCount = 0;
|
|
122
121
|
this.connectPromise = undefined;
|
|
123
122
|
this.reconnectPromise = undefined;
|
|
124
|
-
this.responseDecoder = new ResponseFrameDecoder(config.
|
|
123
|
+
this.responseDecoder = new ResponseFrameDecoder(config.maxResponseFrameSize ?? DEFAULT_MAX_RESPONSE_FRAME_SIZE);
|
|
125
124
|
this.socket = this._installSocket(getTransport(config));
|
|
126
125
|
}
|
|
127
126
|
/**
|
|
@@ -361,8 +360,7 @@ export class IggyConnection extends EventEmitter {
|
|
|
361
360
|
debug('ONDATA', typeof data, Buffer.isBuffer(data), data?.length, this.responseDecoder.hasBufferedData);
|
|
362
361
|
try {
|
|
363
362
|
for (const response of this.responseDecoder.push(data)) {
|
|
364
|
-
if (
|
|
365
|
-
peekCommand(response) === Command2.Eviction)
|
|
363
|
+
if (peekCommand(response) === Command2.Eviction)
|
|
366
364
|
this.emit('eviction', evictionError(response));
|
|
367
365
|
else
|
|
368
366
|
this.emit('response', response);
|
|
@@ -374,17 +372,6 @@ export class IggyConnection extends EventEmitter {
|
|
|
374
372
|
this.socket.destroy();
|
|
375
373
|
}
|
|
376
374
|
}
|
|
377
|
-
/**
|
|
378
|
-
* Writes a command to the socket.
|
|
379
|
-
*
|
|
380
|
-
* @param command - Command code
|
|
381
|
-
* @param payload - Command payload
|
|
382
|
-
* @returns True if the write was successful
|
|
383
|
-
*/
|
|
384
|
-
writeCommand(command, payload) {
|
|
385
|
-
const cmd = serializeCommand(command, payload);
|
|
386
|
-
this.socket.write(cmd);
|
|
387
|
-
}
|
|
388
375
|
writeFrame(frame) {
|
|
389
376
|
this.socket.write(frame);
|
|
390
377
|
}
|
|
@@ -21,6 +21,8 @@ import { createServer, } from 'node:net';
|
|
|
21
21
|
import { describe, it } from 'node:test';
|
|
22
22
|
import { ProtocolFrameError } from './client.frame.js';
|
|
23
23
|
import { IggyConnection } from './client.connection.js';
|
|
24
|
+
import { Command2, HEADER_SIZE, REPLY_OFFSET } from '../wire/vsr/header.js';
|
|
25
|
+
const FRAME_LIMIT = 2 * HEADER_SIZE;
|
|
24
26
|
const startServer = async () => {
|
|
25
27
|
const server = createServer();
|
|
26
28
|
server.listen(0, '127.0.0.1');
|
|
@@ -28,7 +30,6 @@ const startServer = async () => {
|
|
|
28
30
|
return server;
|
|
29
31
|
};
|
|
30
32
|
const connectionConfig = (server) => ({
|
|
31
|
-
protocol: 'classic',
|
|
32
33
|
transport: 'TCP',
|
|
33
34
|
options: {
|
|
34
35
|
host: '127.0.0.1',
|
|
@@ -36,8 +37,15 @@ const connectionConfig = (server) => ({
|
|
|
36
37
|
},
|
|
37
38
|
credentials: { username: 'iggy', password: 'iggy' },
|
|
38
39
|
reconnect: { enabled: false, interval: 0, maxRetries: 0 },
|
|
39
|
-
maxResponseFrameSize:
|
|
40
|
+
maxResponseFrameSize: FRAME_LIMIT
|
|
40
41
|
});
|
|
42
|
+
const replyFrame = (body) => {
|
|
43
|
+
const frame = Buffer.alloc(HEADER_SIZE + body.length);
|
|
44
|
+
frame.writeUInt32LE(frame.length, REPLY_OFFSET.size);
|
|
45
|
+
frame.writeUInt8(Command2.Reply, REPLY_OFFSET.command);
|
|
46
|
+
body.copy(frame, HEADER_SIZE);
|
|
47
|
+
return frame;
|
|
48
|
+
};
|
|
41
49
|
const closeConnection = async (connection, server) => {
|
|
42
50
|
connection._destroy();
|
|
43
51
|
if (!connection.socket.destroyed)
|
|
@@ -59,7 +67,7 @@ describe('IggyConnection', () => {
|
|
|
59
67
|
await closeConnection(connection, server);
|
|
60
68
|
}
|
|
61
69
|
});
|
|
62
|
-
it('shares connection attempts, recognizes endpoints, and writes
|
|
70
|
+
it('shares connection attempts, recognizes endpoints, and writes frames', async () => {
|
|
63
71
|
const server = await startServer();
|
|
64
72
|
const received = new Promise((resolve) => {
|
|
65
73
|
server.once('connection', (socket) => {
|
|
@@ -78,10 +86,9 @@ describe('IggyConnection', () => {
|
|
|
78
86
|
host: 'broker.example'
|
|
79
87
|
};
|
|
80
88
|
assert.equal(connection.isConnectedTo('broker.example', server.address().port), true);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
assert.
|
|
84
|
-
assert.deepEqual(command.subarray(8), Buffer.from('payload'));
|
|
89
|
+
const frame = replyFrame(Buffer.from('payload'));
|
|
90
|
+
connection.writeFrame(frame);
|
|
91
|
+
assert.deepEqual(await received, frame);
|
|
85
92
|
}
|
|
86
93
|
finally {
|
|
87
94
|
await closeConnection(connection, server);
|
|
@@ -92,16 +99,13 @@ describe('IggyConnection', () => {
|
|
|
92
99
|
const connection = new IggyConnection(connectionConfig(server));
|
|
93
100
|
try {
|
|
94
101
|
await connection.connect();
|
|
95
|
-
const
|
|
96
|
-
const frame = Buffer.alloc(8 + body.length);
|
|
97
|
-
frame.writeUInt32LE(body.length, 4);
|
|
98
|
-
body.copy(frame, 8);
|
|
102
|
+
const frame = replyFrame(Buffer.from('response'));
|
|
99
103
|
const response = once(connection, 'response');
|
|
100
104
|
connection._onData(frame.subarray(0, 6));
|
|
101
105
|
connection._onData(frame.subarray(6));
|
|
102
106
|
assert.deepEqual((await response)[0], frame);
|
|
103
|
-
const malformed = Buffer.alloc(
|
|
104
|
-
malformed.writeUInt32LE(
|
|
107
|
+
const malformed = replyFrame(Buffer.alloc(0));
|
|
108
|
+
malformed.writeUInt32LE(FRAME_LIMIT + 1, REPLY_OFFSET.size);
|
|
105
109
|
const error = once(connection, 'error');
|
|
106
110
|
connection._onData(malformed);
|
|
107
111
|
assert.ok((await error)[0] instanceof ProtocolFrameError);
|
|
@@ -178,10 +182,7 @@ describe('IggyConnection', () => {
|
|
|
178
182
|
assert.equal(await first, connection);
|
|
179
183
|
assert.equal(connection.connected, true);
|
|
180
184
|
assert.equal(connections, 2);
|
|
181
|
-
const
|
|
182
|
-
const frame = Buffer.alloc(8 + body.length);
|
|
183
|
-
frame.writeUInt32LE(body.length, 4);
|
|
184
|
-
body.copy(frame, 8);
|
|
185
|
+
const frame = replyFrame(Buffer.from('fresh'));
|
|
185
186
|
const response = once(connection, 'response');
|
|
186
187
|
oldSocket.emit('data', Buffer.alloc(8));
|
|
187
188
|
connection._onData(frame);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Protocol } from './client.type.js';
|
|
2
1
|
export declare class ProtocolFrameError extends Error {
|
|
3
2
|
constructor(message: string);
|
|
4
3
|
}
|
|
@@ -6,21 +5,19 @@ export type ExtractedFrames = {
|
|
|
6
5
|
frames: Buffer[];
|
|
7
6
|
remainder: Buffer;
|
|
8
7
|
};
|
|
9
|
-
export declare const extractResponseFrames: (
|
|
8
|
+
export declare const extractResponseFrames: (buffer: Buffer, maximumFrameSize: number) => ExtractedFrames;
|
|
10
9
|
/**
|
|
11
10
|
* Incrementally extracts response frames without repeatedly copying an
|
|
12
11
|
* incomplete frame as new socket chunks arrive.
|
|
13
12
|
*/
|
|
14
13
|
export declare class ResponseFrameDecoder {
|
|
15
|
-
private readonly protocol;
|
|
16
14
|
private readonly maximumFrameSize;
|
|
17
|
-
private readonly headerSize;
|
|
18
15
|
private chunks;
|
|
19
16
|
private chunkIndex;
|
|
20
17
|
private chunkOffset;
|
|
21
18
|
private bufferedLength;
|
|
22
19
|
private expectedFrameSize?;
|
|
23
|
-
constructor(
|
|
20
|
+
constructor(maximumFrameSize: number);
|
|
24
21
|
get hasBufferedData(): boolean;
|
|
25
22
|
clear(): void;
|
|
26
23
|
push(data: Buffer): Buffer[];
|
|
@@ -14,34 +14,28 @@
|
|
|
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 { HEADER_SIZE
|
|
18
|
-
const CLASSIC_HEADER_SIZE = 8;
|
|
17
|
+
import { HEADER_SIZE, readSize } from '../wire/vsr/header.js';
|
|
19
18
|
export class ProtocolFrameError extends Error {
|
|
20
19
|
constructor(message) {
|
|
21
20
|
super(message);
|
|
22
21
|
this.name = 'ProtocolFrameError';
|
|
23
22
|
}
|
|
24
23
|
}
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
? readVsrSize(header)
|
|
30
|
-
: CLASSIC_HEADER_SIZE + header.readUInt32LE(4);
|
|
31
|
-
if (declaredSize < headerSize)
|
|
32
|
-
throw new ProtocolFrameError(`declared ${protocol} frame size ${declaredSize} is below header size`);
|
|
24
|
+
const declaredFrameSize = (header, maximumFrameSize) => {
|
|
25
|
+
const declaredSize = readSize(header);
|
|
26
|
+
if (declaredSize < HEADER_SIZE)
|
|
27
|
+
throw new ProtocolFrameError(`declared frame size ${declaredSize} is below header size`);
|
|
33
28
|
if (declaredSize > maximumFrameSize)
|
|
34
|
-
throw new ProtocolFrameError(`declared
|
|
29
|
+
throw new ProtocolFrameError(`declared frame size ${declaredSize} exceeds ` +
|
|
35
30
|
`the ${maximumFrameSize} byte limit`);
|
|
36
31
|
return declaredSize;
|
|
37
32
|
};
|
|
38
|
-
export const extractResponseFrames = (
|
|
39
|
-
const headerSize = headerSizeFor(protocol);
|
|
33
|
+
export const extractResponseFrames = (buffer, maximumFrameSize) => {
|
|
40
34
|
const frames = [];
|
|
41
35
|
let offset = 0;
|
|
42
|
-
while (buffer.length - offset >=
|
|
36
|
+
while (buffer.length - offset >= HEADER_SIZE) {
|
|
43
37
|
const available = buffer.length - offset;
|
|
44
|
-
const declaredSize = declaredFrameSize(
|
|
38
|
+
const declaredSize = declaredFrameSize(buffer.subarray(offset, offset + HEADER_SIZE), maximumFrameSize);
|
|
45
39
|
if (available < declaredSize)
|
|
46
40
|
break;
|
|
47
41
|
frames.push(buffer.subarray(offset, offset + declaredSize));
|
|
@@ -59,18 +53,14 @@ export const extractResponseFrames = (protocol, buffer, maximumFrameSize) => {
|
|
|
59
53
|
* incomplete frame as new socket chunks arrive.
|
|
60
54
|
*/
|
|
61
55
|
export class ResponseFrameDecoder {
|
|
62
|
-
protocol;
|
|
63
56
|
maximumFrameSize;
|
|
64
|
-
headerSize;
|
|
65
57
|
chunks;
|
|
66
58
|
chunkIndex;
|
|
67
59
|
chunkOffset;
|
|
68
60
|
bufferedLength;
|
|
69
61
|
expectedFrameSize;
|
|
70
|
-
constructor(
|
|
71
|
-
this.protocol = protocol;
|
|
62
|
+
constructor(maximumFrameSize) {
|
|
72
63
|
this.maximumFrameSize = maximumFrameSize;
|
|
73
|
-
this.headerSize = headerSizeFor(protocol);
|
|
74
64
|
this.chunks = [];
|
|
75
65
|
this.chunkIndex = 0;
|
|
76
66
|
this.chunkOffset = 0;
|
|
@@ -95,9 +85,9 @@ export class ResponseFrameDecoder {
|
|
|
95
85
|
const frames = [];
|
|
96
86
|
while (true) {
|
|
97
87
|
if (this.expectedFrameSize === undefined) {
|
|
98
|
-
if (this.bufferedLength <
|
|
88
|
+
if (this.bufferedLength < HEADER_SIZE)
|
|
99
89
|
break;
|
|
100
|
-
this.expectedFrameSize = declaredFrameSize(this.
|
|
90
|
+
this.expectedFrameSize = declaredFrameSize(this.peek(HEADER_SIZE), this.maximumFrameSize);
|
|
101
91
|
}
|
|
102
92
|
if (this.bufferedLength < this.expectedFrameSize)
|
|
103
93
|
break;
|
|
@@ -19,12 +19,6 @@ import { describe, it } from 'node:test';
|
|
|
19
19
|
import { extractResponseFrames, ProtocolFrameError, ResponseFrameDecoder } from './client.frame.js';
|
|
20
20
|
import { Command2, HEADER_SIZE, REPLY_OFFSET } from '../wire/vsr/header.js';
|
|
21
21
|
const LIMIT = 1024;
|
|
22
|
-
const classicFrame = (body) => {
|
|
23
|
-
const frame = Buffer.alloc(8 + body.length);
|
|
24
|
-
frame.writeUInt32LE(body.length, 4);
|
|
25
|
-
body.copy(frame, 8);
|
|
26
|
-
return frame;
|
|
27
|
-
};
|
|
28
22
|
const vsrFrame = (body) => {
|
|
29
23
|
const frame = Buffer.alloc(HEADER_SIZE + body.length);
|
|
30
24
|
frame.writeUInt32LE(frame.length, REPLY_OFFSET.size);
|
|
@@ -33,59 +27,50 @@ const vsrFrame = (body) => {
|
|
|
33
27
|
return frame;
|
|
34
28
|
};
|
|
35
29
|
describe('extractResponseFrames', () => {
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const headerSize = protocol === 'classic' ? 8 : HEADER_SIZE;
|
|
41
|
-
for (let split = 0; split < headerSize; split += 1) {
|
|
42
|
-
const first = extractResponseFrames(protocol, frame.subarray(0, split), LIMIT);
|
|
43
|
-
assert.equal(first.frames.length, 0);
|
|
44
|
-
const second = extractResponseFrames(protocol, Buffer.concat([first.remainder, frame.subarray(split)]), LIMIT);
|
|
45
|
-
assert.deepEqual(second.frames, [frame]);
|
|
46
|
-
assert.equal(second.remainder.length, 0);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
it(`buffers a fragmented ${protocol} body`, () => {
|
|
50
|
-
const frame = makeFrame(Buffer.from('payload'));
|
|
51
|
-
const split = frame.length - 2;
|
|
52
|
-
const first = extractResponseFrames(protocol, frame.subarray(0, split), LIMIT);
|
|
30
|
+
it('buffers headers split at every boundary', () => {
|
|
31
|
+
const frame = vsrFrame(Buffer.from('payload'));
|
|
32
|
+
for (let split = 0; split < HEADER_SIZE; split += 1) {
|
|
33
|
+
const first = extractResponseFrames(frame.subarray(0, split), LIMIT);
|
|
53
34
|
assert.equal(first.frames.length, 0);
|
|
54
|
-
const second = extractResponseFrames(
|
|
35
|
+
const second = extractResponseFrames(Buffer.concat([first.remainder, frame.subarray(split)]), LIMIT);
|
|
55
36
|
assert.deepEqual(second.frames, [frame]);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
37
|
+
assert.equal(second.remainder.length, 0);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
it('buffers a fragmented body', () => {
|
|
41
|
+
const frame = vsrFrame(Buffer.from('payload'));
|
|
42
|
+
const split = frame.length - 2;
|
|
43
|
+
const first = extractResponseFrames(frame.subarray(0, split), LIMIT);
|
|
44
|
+
assert.equal(first.frames.length, 0);
|
|
45
|
+
const second = extractResponseFrames(Buffer.concat([first.remainder, frame.subarray(split)]), LIMIT);
|
|
46
|
+
assert.deepEqual(second.frames, [frame]);
|
|
47
|
+
});
|
|
48
|
+
it('extracts coalesced frames and a partial tail', () => {
|
|
49
|
+
const first = vsrFrame(Buffer.from('one'));
|
|
50
|
+
const second = vsrFrame(Buffer.from('two'));
|
|
51
|
+
const third = vsrFrame(Buffer.from('three'));
|
|
52
|
+
const input = Buffer.concat([first, second, third.subarray(0, 3)]);
|
|
53
|
+
const extracted = extractResponseFrames(input, LIMIT);
|
|
54
|
+
assert.deepEqual(extracted.frames, [first, second]);
|
|
55
|
+
assert.deepEqual(extracted.remainder, third.subarray(0, 3));
|
|
56
|
+
assert.equal(extracted.remainder.buffer, input.buffer);
|
|
57
|
+
});
|
|
58
|
+
it('rejects a size below the header', () => {
|
|
69
59
|
const frame = vsrFrame(Buffer.alloc(0));
|
|
70
60
|
frame.writeUInt32LE(0, REPLY_OFFSET.size);
|
|
71
|
-
assert.throws(() => extractResponseFrames(
|
|
61
|
+
assert.throws(() => extractResponseFrames(frame, LIMIT), ProtocolFrameError);
|
|
72
62
|
});
|
|
73
|
-
it('rejects an oversized
|
|
63
|
+
it('rejects an oversized frame before buffering its body', () => {
|
|
74
64
|
const header = vsrFrame(Buffer.alloc(0));
|
|
75
65
|
header.writeUInt32LE(LIMIT + 1, REPLY_OFFSET.size);
|
|
76
|
-
assert.throws(() => extractResponseFrames(
|
|
77
|
-
});
|
|
78
|
-
it('rejects an oversized classic frame before buffering its body', () => {
|
|
79
|
-
const header = classicFrame(Buffer.alloc(0));
|
|
80
|
-
header.writeUInt32LE(LIMIT, 4);
|
|
81
|
-
assert.throws(() => extractResponseFrames('classic', header, LIMIT), ProtocolFrameError);
|
|
66
|
+
assert.throws(() => extractResponseFrames(header, LIMIT), ProtocolFrameError);
|
|
82
67
|
});
|
|
83
68
|
});
|
|
84
69
|
describe('ResponseFrameDecoder', () => {
|
|
85
70
|
it('decodes bytewise input without losing coalesced frames', () => {
|
|
86
|
-
const decoder = new ResponseFrameDecoder(
|
|
87
|
-
const first =
|
|
88
|
-
const second =
|
|
71
|
+
const decoder = new ResponseFrameDecoder(LIMIT);
|
|
72
|
+
const first = vsrFrame(Buffer.from('first'));
|
|
73
|
+
const second = vsrFrame(Buffer.from('second'));
|
|
89
74
|
const input = Buffer.concat([first, second]);
|
|
90
75
|
const frames = [];
|
|
91
76
|
for (const byte of input)
|
|
@@ -94,16 +79,16 @@ describe('ResponseFrameDecoder', () => {
|
|
|
94
79
|
assert.equal(decoder.hasBufferedData, false);
|
|
95
80
|
});
|
|
96
81
|
it('clears a partial frame', () => {
|
|
97
|
-
const decoder = new ResponseFrameDecoder(
|
|
98
|
-
decoder.push(vsrFrame(Buffer.from('body')).subarray(0,
|
|
82
|
+
const decoder = new ResponseFrameDecoder(LIMIT);
|
|
83
|
+
decoder.push(vsrFrame(Buffer.from('body')).subarray(0, HEADER_SIZE - 2));
|
|
99
84
|
assert.equal(decoder.hasBufferedData, true);
|
|
100
85
|
decoder.clear();
|
|
101
86
|
assert.equal(decoder.hasBufferedData, false);
|
|
102
87
|
});
|
|
103
88
|
it('rejects an oversized frame as soon as its header is complete', () => {
|
|
104
|
-
const decoder = new ResponseFrameDecoder(
|
|
105
|
-
const header = Buffer.alloc(
|
|
106
|
-
header.writeUInt32LE(LIMIT,
|
|
89
|
+
const decoder = new ResponseFrameDecoder(LIMIT);
|
|
90
|
+
const header = vsrFrame(Buffer.alloc(0));
|
|
91
|
+
header.writeUInt32LE(LIMIT + 1, REPLY_OFFSET.size);
|
|
107
92
|
assert.throws(() => decoder.push(header), ProtocolFrameError);
|
|
108
93
|
});
|
|
109
94
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import type { ClientConfig, ClientCredentials, CommandResponse, PasswordCredentials,
|
|
2
|
+
import type { ClientConfig, ClientCredentials, CommandResponse, PasswordCredentials, RawClient, SendCommandOptions, TokenCredentials } from '../client/client.type.js';
|
|
3
3
|
export declare class VsrResponseTimeoutError extends Error {
|
|
4
4
|
constructor(timeout: number);
|
|
5
5
|
}
|
|
@@ -8,8 +8,6 @@ export declare class VsrResponseTimeoutError extends Error {
|
|
|
8
8
|
* Implements command queuing, authentication, and heartbeat functionality.
|
|
9
9
|
*/
|
|
10
10
|
export declare class CommandResponseStream extends EventEmitter {
|
|
11
|
-
/** Server wire protocol used by this connection */
|
|
12
|
-
readonly protocol: Protocol;
|
|
13
11
|
/** Client configuration */
|
|
14
12
|
private options;
|
|
15
13
|
/** Underlying connection to the server */
|
|
@@ -69,7 +67,6 @@ export declare class CommandResponseStream extends EventEmitter {
|
|
|
69
67
|
*/
|
|
70
68
|
_processNext(command: number, payload: Buffer, handleResp?: boolean): Promise<CommandResponse>;
|
|
71
69
|
private _processVsrLogin;
|
|
72
|
-
private _processClassic;
|
|
73
70
|
private _processVsr;
|
|
74
71
|
private _exchange;
|
|
75
72
|
private isUnloggedCommand;
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
// under the License.
|
|
17
17
|
//
|
|
18
18
|
import { EventEmitter } from 'node:events';
|
|
19
|
-
import { handleResponse } from './client.utils.js';
|
|
20
19
|
import { ResponseError, responseError } from '../wire/error.utils.js';
|
|
21
20
|
import { debug } from './client.debug.js';
|
|
22
21
|
import { IggyConnection } from './client.connection.js';
|
|
@@ -49,8 +48,6 @@ export class VsrResponseTimeoutError extends Error {
|
|
|
49
48
|
* Implements command queuing, authentication, and heartbeat functionality.
|
|
50
49
|
*/
|
|
51
50
|
export class CommandResponseStream extends EventEmitter {
|
|
52
|
-
/** Server wire protocol used by this connection */
|
|
53
|
-
protocol;
|
|
54
51
|
/** Client configuration */
|
|
55
52
|
options;
|
|
56
53
|
/** Underlying connection to the server */
|
|
@@ -82,7 +79,6 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
82
79
|
super();
|
|
83
80
|
const normalizedConfig = normalizeClientConfig(options);
|
|
84
81
|
this.options = normalizedConfig;
|
|
85
|
-
this.protocol = normalizedConfig.protocol;
|
|
86
82
|
this.connection = new IggyConnection(normalizedConfig);
|
|
87
83
|
this.busy = false;
|
|
88
84
|
this.isAuthenticated = false;
|
|
@@ -126,7 +122,7 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
126
122
|
const { handleResponse = true, last = true } = options;
|
|
127
123
|
if (!this.connection.connected)
|
|
128
124
|
await this.connection.connect();
|
|
129
|
-
if (
|
|
125
|
+
if (isLoginCommand(command))
|
|
130
126
|
await this._ensureVsrLeader();
|
|
131
127
|
if (!this.isAuthenticated && !this.isUnloggedCommand(command))
|
|
132
128
|
await this.authenticate(this.options.credentials);
|
|
@@ -191,8 +187,6 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
191
187
|
* @returns Promise resolving to the command response
|
|
192
188
|
*/
|
|
193
189
|
_processNext(command, payload, handleResp = true) {
|
|
194
|
-
if (this.options.protocol !== 'vsr')
|
|
195
|
-
return this._processClassic(command, payload, handleResp);
|
|
196
190
|
if (isLoginCommand(command) && this.isAuthenticated)
|
|
197
191
|
return this._processVsrLogin(command, payload, handleResp);
|
|
198
192
|
return this._processVsr(command, payload, handleResp);
|
|
@@ -201,15 +195,6 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
201
195
|
await this._processVsr(LOGOUT.code, LOGOUT.serialize(), true);
|
|
202
196
|
return this._processVsr(command, payload, handleResp);
|
|
203
197
|
}
|
|
204
|
-
async _processClassic(command, payload, handleResp) {
|
|
205
|
-
const response = await this._exchange(() => this.connection.writeCommand(command, payload));
|
|
206
|
-
if (!handleResp)
|
|
207
|
-
return response;
|
|
208
|
-
const parsed = handleResponse(response);
|
|
209
|
-
if (parsed.status !== 0)
|
|
210
|
-
throw responseError(command, parsed.status);
|
|
211
|
-
return parsed;
|
|
212
|
-
}
|
|
213
198
|
async _processVsr(command, payload, handleResp) {
|
|
214
199
|
let requestWritten = false;
|
|
215
200
|
try {
|
|
@@ -326,8 +311,7 @@ export class CommandResponseStream extends EventEmitter {
|
|
|
326
311
|
}
|
|
327
312
|
isUnloggedCommand(command) {
|
|
328
313
|
return UNLOGGED_COMMAND_CODE.includes(command) ||
|
|
329
|
-
|
|
330
|
-
command === COMMAND_CODE.GetClusterMetadata);
|
|
314
|
+
command === COMMAND_CODE.GetClusterMetadata;
|
|
331
315
|
}
|
|
332
316
|
async _ensureVsrLeader() {
|
|
333
317
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
@@ -161,7 +161,6 @@ const singleNodeHandler = (port) => (frame, socket) => {
|
|
|
161
161
|
socket.write(replyFrame(operation));
|
|
162
162
|
};
|
|
163
163
|
const vsrConfig = (port) => ({
|
|
164
|
-
protocol: 'vsr',
|
|
165
164
|
transport: 'TCP',
|
|
166
165
|
options: { host: '127.0.0.1', port },
|
|
167
166
|
credentials: { username: 'iggy', password: 'iggy' },
|
|
@@ -35,8 +35,6 @@ export type SendCommandOptions = {
|
|
|
35
35
|
* Provides direct access to command sending and event handling.
|
|
36
36
|
*/
|
|
37
37
|
export type RawClient = {
|
|
38
|
-
/** Server wire protocol used by this connection */
|
|
39
|
-
readonly protocol: Protocol;
|
|
40
38
|
/** Sends a command to the server and returns the response */
|
|
41
39
|
sendCommand: (code: number, payload: Buffer, options?: SendCommandOptions) => Promise<CommandResponse>;
|
|
42
40
|
/** Whether the client has been authenticated */
|
|
@@ -83,8 +81,6 @@ export type ReconnectOption = {
|
|
|
83
81
|
* Union type for transport-specific connection options.
|
|
84
82
|
*/
|
|
85
83
|
export type TransportOption = TcpOption | TlsOption;
|
|
86
|
-
/** Server wire protocol. */
|
|
87
|
-
export type Protocol = 'classic' | 'vsr';
|
|
88
84
|
/**
|
|
89
85
|
* Token-based authentication credentials.
|
|
90
86
|
*/
|
|
@@ -119,8 +115,6 @@ export type PoolSizeOption = {
|
|
|
119
115
|
* Complete client configuration for connecting to the Iggy server.
|
|
120
116
|
*/
|
|
121
117
|
export type ClientConfig = {
|
|
122
|
-
/** Server wire protocol (default: classic) */
|
|
123
|
-
protocol?: Protocol;
|
|
124
118
|
/** Transport protocol to use (TCP or TLS) */
|
|
125
119
|
transport: TransportType;
|
|
126
120
|
/** Transport-specific connection options */
|
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
import { Transform } from 'node:stream';
|
|
2
1
|
import type { CommandResponse } from './client.type.js';
|
|
3
|
-
/**
|
|
4
|
-
* Parses a raw response buffer into a structured CommandResponse.
|
|
5
|
-
* Extracts status code, length, and payload data from the buffer.
|
|
6
|
-
*
|
|
7
|
-
* @param r - Raw response buffer from the server
|
|
8
|
-
* @returns Parsed command response with status, length, and data
|
|
9
|
-
*/
|
|
10
|
-
export declare const handleResponse: (r: Buffer) => {
|
|
11
|
-
status: number;
|
|
12
|
-
length: number;
|
|
13
|
-
data: Buffer<ArrayBufferLike>;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Creates a Transform stream that parses response buffers.
|
|
17
|
-
* Transforms raw server responses into just the data payload.
|
|
18
|
-
*
|
|
19
|
-
* @returns Transform stream for processing server responses
|
|
20
|
-
*/
|
|
21
|
-
export declare const handleResponseTransform: () => Transform;
|
|
22
2
|
/**
|
|
23
3
|
* Deserializes a void response from the server.
|
|
24
4
|
* Returns true if the command succeeded with no data.
|
|
@@ -27,13 +7,4 @@ export declare const handleResponseTransform: () => Transform;
|
|
|
27
7
|
* @returns True if the response indicates success with no data
|
|
28
8
|
*/
|
|
29
9
|
export declare const deserializeVoidResponse: (r: CommandResponse) => boolean;
|
|
30
|
-
/**
|
|
31
|
-
* Serializes a command and its payload into a buffer for sending to the server.
|
|
32
|
-
* Creates the wire format: [payload_size (4 bytes)][command (4 bytes)][payload]
|
|
33
|
-
*
|
|
34
|
-
* @param command - Command code to send
|
|
35
|
-
* @param payload - Command payload buffer
|
|
36
|
-
* @returns Buffer ready to be sent to the server
|
|
37
|
-
*/
|
|
38
|
-
export declare const serializeCommand: (command: number, payload: Buffer) => Buffer<ArrayBuffer>;
|
|
39
10
|
//# sourceMappingURL=client.utils.d.ts.map
|
|
@@ -15,42 +15,6 @@
|
|
|
15
15
|
// specific language governing permissions and limitations
|
|
16
16
|
// under the License.
|
|
17
17
|
//
|
|
18
|
-
import { Transform } from 'node:stream';
|
|
19
|
-
import { translateCommandCode } from '../wire/command.code.js';
|
|
20
|
-
import { debug } from './client.debug.js';
|
|
21
|
-
/**
|
|
22
|
-
* Parses a raw response buffer into a structured CommandResponse.
|
|
23
|
-
* Extracts status code, length, and payload data from the buffer.
|
|
24
|
-
*
|
|
25
|
-
* @param r - Raw response buffer from the server
|
|
26
|
-
* @returns Parsed command response with status, length, and data
|
|
27
|
-
*/
|
|
28
|
-
export const handleResponse = (r) => {
|
|
29
|
-
const status = r.readUint32LE(0);
|
|
30
|
-
const length = r.readUint32LE(4);
|
|
31
|
-
debug('<== handleResponse', { status, length });
|
|
32
|
-
return {
|
|
33
|
-
status, length, data: r.subarray(8, 8 + length)
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Creates a Transform stream that parses response buffers.
|
|
38
|
-
* Transforms raw server responses into just the data payload.
|
|
39
|
-
*
|
|
40
|
-
* @returns Transform stream for processing server responses
|
|
41
|
-
*/
|
|
42
|
-
export const handleResponseTransform = () => new Transform({
|
|
43
|
-
transform(chunk, encoding, cb) {
|
|
44
|
-
try {
|
|
45
|
-
const r = handleResponse(chunk);
|
|
46
|
-
debug('response::', r);
|
|
47
|
-
return cb(null, r.data);
|
|
48
|
-
}
|
|
49
|
-
catch (err) {
|
|
50
|
-
return cb(new Error('handleResponseTransform error', { cause: err }), null);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
18
|
/**
|
|
55
19
|
* Deserializes a void response from the server.
|
|
56
20
|
* Returns true if the command succeeded with no data.
|
|
@@ -59,24 +23,4 @@ export const handleResponseTransform = () => new Transform({
|
|
|
59
23
|
* @returns True if the response indicates success with no data
|
|
60
24
|
*/
|
|
61
25
|
export const deserializeVoidResponse = (r) => r.status === 0 && r.data.length === 0;
|
|
62
|
-
/** Length of the command code in bytes */
|
|
63
|
-
const COMMAND_LENGTH = 4;
|
|
64
|
-
/**
|
|
65
|
-
* Serializes a command and its payload into a buffer for sending to the server.
|
|
66
|
-
* Creates the wire format: [payload_size (4 bytes)][command (4 bytes)][payload]
|
|
67
|
-
*
|
|
68
|
-
* @param command - Command code to send
|
|
69
|
-
* @param payload - Command payload buffer
|
|
70
|
-
* @returns Buffer ready to be sent to the server
|
|
71
|
-
*/
|
|
72
|
-
export const serializeCommand = (command, payload) => {
|
|
73
|
-
const payloadSize = payload.length + COMMAND_LENGTH;
|
|
74
|
-
const data = Buffer.allocUnsafe(8 + payload.length);
|
|
75
|
-
data.writeUint32LE(payloadSize, 0);
|
|
76
|
-
data.writeUint32LE(command, 4);
|
|
77
|
-
data.fill(payload, 8);
|
|
78
|
-
debug('==> CMD', command, translateCommandCode(command), 'LENGTH', payloadSize);
|
|
79
|
-
debug('FullMessage#Base64', data.toString('base64'));
|
|
80
|
-
return data;
|
|
81
|
-
};
|
|
82
26
|
//# sourceMappingURL=client.utils.js.map
|
|
@@ -17,26 +17,15 @@
|
|
|
17
17
|
//
|
|
18
18
|
import { describe, it } from 'node:test';
|
|
19
19
|
import assert from 'node:assert/strict';
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
buf.writeUInt32LE(42, 8); // trailing bytes — NOT part of this response
|
|
30
|
-
const r = handleResponse(buf);
|
|
31
|
-
assert.equal(r.data.length, 0);
|
|
32
|
-
});
|
|
33
|
-
it('deserializeVoidResponse returns true for a valid void response with trailing buffer bytes', () => {
|
|
34
|
-
const buf = Buffer.alloc(12);
|
|
35
|
-
buf.writeUInt32LE(SUCCESS, 0);
|
|
36
|
-
buf.writeUInt32LE(0, 4); // length = 0
|
|
37
|
-
buf.writeUInt32LE(42, 8); // trailing bytes
|
|
38
|
-
const r = handleResponse(buf);
|
|
39
|
-
assert.equal(deserializeVoidResponse(r), true);
|
|
20
|
+
import { deserializeVoidResponse } from './client.utils.js';
|
|
21
|
+
describe('deserializeVoidResponse', () => {
|
|
22
|
+
it('returns true only for a success status with an empty payload', () => {
|
|
23
|
+
const success = { status: 0, length: 0, data: Buffer.alloc(0) };
|
|
24
|
+
assert.equal(deserializeVoidResponse(success), true);
|
|
25
|
+
const failed = { status: 1, length: 0, data: Buffer.alloc(0) };
|
|
26
|
+
assert.equal(deserializeVoidResponse(failed), false);
|
|
27
|
+
const withData = { status: 0, length: 4, data: Buffer.alloc(4) };
|
|
28
|
+
assert.equal(deserializeVoidResponse(withData), false);
|
|
40
29
|
});
|
|
41
30
|
});
|
|
42
31
|
//# sourceMappingURL=client.utils.test.js.map
|
|
@@ -18,13 +18,14 @@
|
|
|
18
18
|
import { after, describe, it } from "node:test";
|
|
19
19
|
import assert from "node:assert/strict";
|
|
20
20
|
import { getTestClient } from "./test-client.utils.js";
|
|
21
|
-
// cluster mode
|
|
22
|
-
//
|
|
21
|
+
// The suite runs against a single-node server (cluster mode off), which
|
|
22
|
+
// reports itself as the sole healthy leader with endpoints derived from
|
|
23
|
+
// its default config.
|
|
23
24
|
const expectedMeta = {
|
|
24
|
-
name: "
|
|
25
|
+
name: "single-node",
|
|
25
26
|
nodes: [
|
|
26
27
|
{
|
|
27
|
-
name: "iggy-node
|
|
28
|
+
name: "iggy-node",
|
|
28
29
|
ip: "127.0.0.1",
|
|
29
30
|
endpoints: {
|
|
30
31
|
tcp: 8090,
|
|
@@ -35,18 +36,6 @@ const expectedMeta = {
|
|
|
35
36
|
role: "Leader",
|
|
36
37
|
status: "Healthy",
|
|
37
38
|
},
|
|
38
|
-
{
|
|
39
|
-
name: "iggy-node-2",
|
|
40
|
-
ip: "127.0.0.1",
|
|
41
|
-
endpoints: {
|
|
42
|
-
tcp: 8091,
|
|
43
|
-
quic: 8081,
|
|
44
|
-
http: 3001,
|
|
45
|
-
websocket: 8093,
|
|
46
|
-
},
|
|
47
|
-
role: "Follower",
|
|
48
|
-
status: "Healthy",
|
|
49
|
-
},
|
|
50
39
|
],
|
|
51
40
|
};
|
|
52
41
|
describe("e2e -> system", async () => {
|
|
@@ -23,9 +23,7 @@ import { generateMessages } from '../tcp.sm.utils.js';
|
|
|
23
23
|
import { getIggyAddress } from '../tcp.sm.utils.js';
|
|
24
24
|
describe('e2e -> consumer-group', async () => {
|
|
25
25
|
const [host, port] = getIggyAddress();
|
|
26
|
-
const vsr = process.env.IGGY_TEST_PROTOCOL === 'vsr';
|
|
27
26
|
const c = new SingleClient({
|
|
28
|
-
protocol: vsr ? 'vsr' : 'classic',
|
|
29
27
|
transport: 'TCP',
|
|
30
28
|
options: { host, port },
|
|
31
29
|
credentials: { username: 'iggy', password: 'iggy' }
|
|
@@ -74,9 +72,7 @@ describe('e2e -> consumer-group', async () => {
|
|
|
74
72
|
streamId: streamName,
|
|
75
73
|
topicId: topicName,
|
|
76
74
|
messages: generateMessages(mn),
|
|
77
|
-
partition:
|
|
78
|
-
? Partitioning.PartitionId((i / mn) % 3)
|
|
79
|
-
: Partitioning.MessageKey(`key-${i % 300}`)
|
|
75
|
+
partition: Partitioning.PartitionId((i / mn) % 3)
|
|
80
76
|
}));
|
|
81
77
|
}
|
|
82
78
|
payloadLength = ct;
|
|
@@ -24,9 +24,7 @@ import { sendSomeMessages, getIggyAddress } from '../tcp.sm.utils.js';
|
|
|
24
24
|
describe('e2e -> consumer-stream', async () => {
|
|
25
25
|
const [host, port] = getIggyAddress();
|
|
26
26
|
const credentials = { username: 'iggy', password: 'iggy' };
|
|
27
|
-
const vsr = process.env.IGGY_TEST_PROTOCOL === 'vsr';
|
|
28
27
|
const opt = {
|
|
29
|
-
protocol: (vsr ? 'vsr' : 'classic'),
|
|
30
28
|
transport: 'TCP',
|
|
31
29
|
options: { host, port },
|
|
32
30
|
credentials
|
|
@@ -45,9 +43,7 @@ describe('e2e -> consumer-stream', async () => {
|
|
|
45
43
|
const ct = 1000;
|
|
46
44
|
it('e2e -> consumer-stream::send-messages', async () => {
|
|
47
45
|
for (let i = 0; i < ct; i += 100) {
|
|
48
|
-
await sendSomeMessages(c.clientProvider)(streamName, topicName,
|
|
49
|
-
? Partitioning.PartitionId((i / 100) % 3)
|
|
50
|
-
: Partitioning.MessageKey(`k-${i % 300}`));
|
|
46
|
+
await sendSomeMessages(c.clientProvider)(streamName, topicName, Partitioning.PartitionId((i / 100) % 3));
|
|
51
47
|
}
|
|
52
48
|
});
|
|
53
49
|
it('e2e -> consumer-stream::poll-stream', async () => {
|
|
@@ -22,9 +22,6 @@ import { generateMessages } from '../tcp.sm.utils.js';
|
|
|
22
22
|
import { getTestClient } from './test-client.utils.js';
|
|
23
23
|
describe('e2e -> message', async () => {
|
|
24
24
|
const c = getTestClient();
|
|
25
|
-
// Only the VSR lane reaches a server that reports offsets. The classic lane
|
|
26
|
-
// runs against the legacy server, which commits without confirming.
|
|
27
|
-
const vsr = process.env.IGGY_TEST_PROTOCOL === 'vsr';
|
|
28
25
|
const streamName = 'e2e-stream-934';
|
|
29
26
|
const topicName = 'e2e-topic-832';
|
|
30
27
|
const partitionId = 0;
|
|
@@ -44,10 +41,6 @@ describe('e2e -> message', async () => {
|
|
|
44
41
|
};
|
|
45
42
|
it('e2e -> message::send', async () => {
|
|
46
43
|
const { confirmations } = await c.message.send(msg);
|
|
47
|
-
if (!vsr) {
|
|
48
|
-
assert.equal(confirmations.length, 0);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
44
|
assert.equal(confirmations.length, 1);
|
|
52
45
|
assert.equal(confirmations[0].partitionId, partitionId);
|
|
53
46
|
});
|
|
@@ -157,10 +150,6 @@ describe('e2e -> message', async () => {
|
|
|
157
150
|
...msg,
|
|
158
151
|
messages: generateMessages(3)
|
|
159
152
|
});
|
|
160
|
-
if (!vsr) {
|
|
161
|
-
assert.equal(confirmations.length, 0);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
153
|
// Landing behind the already committed batch is the part no placeholder
|
|
165
154
|
// confirmation could reproduce.
|
|
166
155
|
assert.equal(confirmations.length, 1);
|
|
@@ -19,9 +19,7 @@ import { Client } from '../client/client.js';
|
|
|
19
19
|
import { getIggyAddress } from '../tcp.sm.utils.js';
|
|
20
20
|
const credentials = { username: 'iggy', password: 'iggy' };
|
|
21
21
|
const [host, port] = getIggyAddress();
|
|
22
|
-
const protocol = process.env.IGGY_TEST_PROTOCOL === 'vsr' ? 'vsr' : 'classic';
|
|
23
22
|
export const getTestClient = () => new Client({
|
|
24
|
-
protocol,
|
|
25
23
|
transport: 'TCP',
|
|
26
24
|
options: { host, port },
|
|
27
25
|
credentials
|
|
@@ -21,11 +21,13 @@
|
|
|
21
21
|
// To run them locally:
|
|
22
22
|
//
|
|
23
23
|
// 1. Start the server with TLS:
|
|
24
|
+
// TODO(hubcio): change to iggy-server once legacy server is removed
|
|
25
|
+
// (core/server has VSR support)
|
|
24
26
|
// IGGY_ROOT_USERNAME=iggy IGGY_ROOT_PASSWORD=iggy \
|
|
25
27
|
// IGGY_TCP_TLS_ENABLED=true \
|
|
26
28
|
// IGGY_TCP_TLS_CERT_FILE=core/certs/iggy_cert.pem \
|
|
27
29
|
// IGGY_TCP_TLS_KEY_FILE=core/certs/iggy_key.pem \
|
|
28
|
-
// cargo r --bin iggy-server
|
|
30
|
+
// cargo r --bin iggy-server-ng --features vsr
|
|
29
31
|
//
|
|
30
32
|
// 2. Run the tests:
|
|
31
33
|
// cd foreign/node
|
|
@@ -46,13 +48,9 @@ const caCertPath = process.env.E2E_ROOT_CA_CERT
|
|
|
46
48
|
const getTlsClient = () => {
|
|
47
49
|
const [, port] = getIggyAddress();
|
|
48
50
|
const caCert = readFileSync(caCertPath);
|
|
49
|
-
const protocol = process.env.IGGY_TEST_PROTOCOL === 'vsr'
|
|
50
|
-
? 'vsr'
|
|
51
|
-
: 'classic';
|
|
52
51
|
// The server certificate SAN is DNS:localhost, so we connect via 'localhost'
|
|
53
52
|
// for proper hostname verification (consistent with Python and C# TLS tests).
|
|
54
53
|
return new Client({
|
|
55
|
-
protocol,
|
|
56
54
|
transport: 'TLS',
|
|
57
55
|
options: {
|
|
58
56
|
port,
|
|
@@ -17,7 +17,7 @@ export declare const ClusterNodeStatus: {
|
|
|
17
17
|
export type ClusterNodeStatus = typeof ClusterNodeStatus;
|
|
18
18
|
export type ClusterNodeStatusKey = keyof ClusterNodeStatus;
|
|
19
19
|
export type ClusterNodeStatusValue = ValueOf<ClusterNodeStatus>;
|
|
20
|
-
export declare const ReverseClusterNodeStatus: Record<0 | 2 | 1 |
|
|
20
|
+
export declare const ReverseClusterNodeStatus: Record<0 | 2 | 1 | 3 | 4, "Healthy" | "Starting" | "Stopping" | "Unreachable" | "Maintenance">;
|
|
21
21
|
export declare const TransportProtocol: {
|
|
22
22
|
readonly TCP: 1;
|
|
23
23
|
readonly QUIC: 2;
|
|
@@ -27,7 +27,7 @@ export declare const TransportProtocol: {
|
|
|
27
27
|
export type TransportProtocol = typeof TransportProtocol;
|
|
28
28
|
export type TransportProtocolKey = keyof TransportProtocol;
|
|
29
29
|
export type TransportProtocolValue = ValueOf<TransportProtocol>;
|
|
30
|
-
export declare const ReverseTransportProtocol: Record<2 | 1 |
|
|
30
|
+
export declare const ReverseTransportProtocol: Record<2 | 1 | 3 | 4, "TCP" | "QUIC" | "HTTP" | "WebSocket">;
|
|
31
31
|
export declare const isClusterNodeRole: (n: number) => n is ClusterNodeRoleValue;
|
|
32
32
|
export declare const isClusterNodeStatus: (n: number) => n is ClusterNodeStatusValue;
|
|
33
33
|
export declare const isTransportProtocol: (n: number) => n is TransportProtocolValue;
|
|
@@ -20,7 +20,6 @@ import assert from 'node:assert/strict';
|
|
|
20
20
|
import { SimpleClient } from '../client/client.js';
|
|
21
21
|
import { COMMAND_CODE } from './command.code.js';
|
|
22
22
|
const mockRawClient = () => ({
|
|
23
|
-
protocol: 'classic',
|
|
24
23
|
sendCommand: async () => {
|
|
25
24
|
throw new Error('sendCommand should not be called by the session-control guard');
|
|
26
25
|
},
|
|
@@ -27,7 +27,7 @@ export type HeaderKindId = keyof HeaderKind;
|
|
|
27
27
|
/** Numeric values of header kinds */
|
|
28
28
|
export type HeaderKindValue = ValueOf<HeaderKind>;
|
|
29
29
|
/** Reverse mapping from numeric value to header kind name */
|
|
30
|
-
export declare const ReverseHeaderKind: Record<2 | 1 | 10 | 11 | 12 |
|
|
30
|
+
export declare const ReverseHeaderKind: Record<2 | 1 | 10 | 11 | 12 | 5 | 8 | 13 | 3 | 4 | 6 | 7 | 9 | 14 | 15, "Raw" | "String" | "Bool" | "Int8" | "Int16" | "Int32" | "Int64" | "Int128" | "Uint8" | "Uint16" | "Uint32" | "Uint64" | "Uint128" | "Float" | "Double">;
|
|
31
31
|
/** Returns expected byte size for a header kind, or -1 for variable-size kinds */
|
|
32
32
|
export declare const expectedSize: (kind: number) => number;
|
|
33
33
|
/** Raw binary header value */
|
|
@@ -160,8 +160,7 @@ export const pollMessages = (getClient) => async (request) => {
|
|
|
160
160
|
const client = await getClient();
|
|
161
161
|
const release = client.hold?.();
|
|
162
162
|
try {
|
|
163
|
-
if (
|
|
164
|
-
request.consumer.kind === ConsumerKind.Group &&
|
|
163
|
+
if (request.consumer.kind === ConsumerKind.Group &&
|
|
165
164
|
request.partitionId === null) {
|
|
166
165
|
const state = getGroupState(client);
|
|
167
166
|
while (true) {
|
|
@@ -111,6 +111,8 @@ const namespaceFromIds = (stream, topic, partitionId) => {
|
|
|
111
111
|
* `SendMessages`: `[metadata_len u32][stream ident][topic ident]
|
|
112
112
|
* [partitioning kind u8, len u8, value]...`. Only explicit `PartitionId`
|
|
113
113
|
* partitioning is routable under VSR; the broker never picks a partition.
|
|
114
|
+
* TODO(hubcio): Balanced and MessageKey partitioning to be implemented;
|
|
115
|
+
* not decided yet whether it'll be on server side or client side.
|
|
114
116
|
*/
|
|
115
117
|
const namespaceFromSendMessages = (payload) => {
|
|
116
118
|
if (payload.length < 4)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apache-iggy",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0-edge.1",
|
|
5
5
|
"description": "Official Apache Iggy NodeJS SDK",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"iggy",
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
"scripts": {
|
|
38
38
|
"test:unit": "node --import @swc-node/register/esm-register --test --experimental-test-coverage './src/**/*.test.ts'",
|
|
39
39
|
"test:e2e": "node --import @swc-node/register/esm-register --test --experimental-test-coverage --test-force-exit './src/e2e/*.e2e.ts'",
|
|
40
|
-
"test:e2e:vsr": "IGGY_TEST_PROTOCOL=vsr node --import @swc-node/register/esm-register --test --experimental-test-coverage --test-force-exit './src/e2e/tcp.*.e2e.ts'",
|
|
41
40
|
"test:bdd": "cucumber-js --exit",
|
|
42
41
|
"test": "npm run test:unit && npm run test:bdd && npm run test:e2e",
|
|
43
42
|
"clean": "rm -Rf dist/",
|
|
@@ -61,7 +60,7 @@
|
|
|
61
60
|
"@cucumber/cucumber": "13.2.0",
|
|
62
61
|
"@swc-node/register": "1.12.1",
|
|
63
62
|
"@types/debug": "4.1.13",
|
|
64
|
-
"@types/node": "26.1.
|
|
63
|
+
"@types/node": "26.1.2",
|
|
65
64
|
"c8": "^12.0.0",
|
|
66
65
|
"husky": "9.1.7",
|
|
67
66
|
"typescript": "6.0.3",
|