apache-iggy 0.8.1-edge.3 → 0.9.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 +1 -5
- package/dist/client/client.config.js +0 -2
- package/dist/client/client.config.test.js +6 -3
- package/dist/client/client.socket.test.js +33 -3
- package/dist/client/client.utils.d.ts +0 -1
- package/dist/client/client.utils.js +0 -1
- package/dist/client/client.utils.test.js +1 -17
- package/dist/e2e/tcp.send-message.e2e.js +24 -1
- package/dist/e2e/tls.system.e2e.js +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/tcp.sm.utils.d.ts +1 -1
- package/dist/wire/command-set.d.ts +1 -1
- package/dist/wire/message/send-messages.command.d.ts +32 -3
- package/dist/wire/message/send-messages.command.js +41 -3
- package/dist/wire/message/send-messages.command.test.js +74 -15
- package/dist/wire/vsr/register.d.ts +1 -1
- package/dist/wire/vsr/register.js +2 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -57,11 +57,7 @@ from the SDK command table use `Operation::NonReplicated` and carry the command
|
|
|
57
57
|
code in the request header's reserved field. The server remains authoritative
|
|
58
58
|
for classifying or rejecting extension commands.
|
|
59
59
|
|
|
60
|
-
The same npm package supports both framing modes. VSR
|
|
61
|
-
only and restricts `Client` to one pooled connection because authentication,
|
|
62
|
-
request sequencing, and consumer-group assignments belong to one consensus
|
|
63
|
-
session. Configurations requesting VSR over TLS or more than one pooled
|
|
64
|
-
connection fail before a socket is opened.
|
|
60
|
+
The same npm package supports both framing modes over TCP and TLS. VSR 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.
|
|
65
61
|
|
|
66
62
|
VSR authentication translates the existing password and personal-access-token
|
|
67
63
|
login APIs into the register handshake required by the consensus protocol. A
|
|
@@ -24,8 +24,6 @@ export const normalizeClientConfig = (config) => {
|
|
|
24
24
|
if (!Number.isSafeInteger(maxResponseFrameSize) ||
|
|
25
25
|
maxResponseFrameSize < 256)
|
|
26
26
|
throw new TypeError('maxResponseFrameSize must be a safe integer of at least 256 bytes');
|
|
27
|
-
if (protocol === 'vsr' && config.transport === 'TLS')
|
|
28
|
-
throw new TypeError('VSR framing currently supports the TCP transport only');
|
|
29
27
|
if (protocol === 'vsr' &&
|
|
30
28
|
((config.poolSize?.min ?? 1) > 1 || (config.poolSize?.max ?? 1) > 1))
|
|
31
29
|
throw new TypeError('VSR clients currently support exactly one pooled connection');
|
|
@@ -50,12 +50,15 @@ describe('normalizeClientConfig', () => {
|
|
|
50
50
|
protocol: 'auto'
|
|
51
51
|
}), /unsupported wire protocol/);
|
|
52
52
|
});
|
|
53
|
-
it('
|
|
54
|
-
|
|
53
|
+
it('supports VSR over TLS', () => {
|
|
54
|
+
const normalized = normalizeClientConfig({
|
|
55
55
|
...config(),
|
|
56
56
|
protocol: 'vsr',
|
|
57
57
|
transport: 'TLS'
|
|
58
|
-
})
|
|
58
|
+
});
|
|
59
|
+
assert.equal(normalized.protocol, 'vsr');
|
|
60
|
+
assert.equal(normalized.transport, 'TLS');
|
|
61
|
+
assert.deepEqual(normalized.poolSize, { min: 1, max: 1 });
|
|
59
62
|
});
|
|
60
63
|
it('rejects unsafe response frame limits', () => {
|
|
61
64
|
for (const maxResponseFrameSize of [0, 255, 1.5, Number.MAX_VALUE])
|
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
//
|
|
18
18
|
import assert from 'node:assert/strict';
|
|
19
19
|
import { once } from 'node:events';
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
20
21
|
import { createServer } from 'node:net';
|
|
21
22
|
import { describe, it } from 'node:test';
|
|
23
|
+
import { createServer as createTlsServer } from 'node:tls';
|
|
22
24
|
import { COMMAND_CODE } from '../wire/command.code.js';
|
|
23
25
|
import { ResponseError } from '../wire/error.utils.js';
|
|
24
26
|
import { Command2, EVICTION_OFFSET, EvictionReason, HEADER_SIZE, REPLY_OFFSET, REQUEST_OFFSET } from '../wire/vsr/header.js';
|
|
@@ -26,10 +28,13 @@ import { Operation } from '../wire/vsr/operation.js';
|
|
|
26
28
|
import { VsrEvictionError } from '../wire/vsr/reply.js';
|
|
27
29
|
import { CommandResponseStream } from './client.socket.js';
|
|
28
30
|
const TEST_SESSION = 42n;
|
|
31
|
+
const TLS_CERTIFICATE = readFileSync(new URL('../../../../core/certs/iggy_cert.pem', import.meta.url));
|
|
32
|
+
const TLS_KEY = readFileSync(new URL('../../../../core/certs/iggy_key.pem', import.meta.url));
|
|
33
|
+
const TLS_CA_CERTIFICATE = readFileSync(new URL('../../../../core/certs/iggy_ca_cert.pem', import.meta.url));
|
|
29
34
|
/** Loopback server speaking just enough VSR framing for the client tests. */
|
|
30
|
-
const startVsrServer = async (handler) => {
|
|
35
|
+
const startVsrServer = async (handler, transport = 'TCP') => {
|
|
31
36
|
const frames = [];
|
|
32
|
-
const
|
|
37
|
+
const handleConnection = (socket) => {
|
|
33
38
|
let pending = Buffer.alloc(0);
|
|
34
39
|
socket.on('data', (data) => {
|
|
35
40
|
pending = Buffer.concat([pending, data]);
|
|
@@ -44,7 +49,10 @@ const startVsrServer = async (handler) => {
|
|
|
44
49
|
}
|
|
45
50
|
});
|
|
46
51
|
socket.on('error', () => { });
|
|
47
|
-
}
|
|
52
|
+
};
|
|
53
|
+
const server = transport === 'TLS'
|
|
54
|
+
? createTlsServer({ cert: TLS_CERTIFICATE, key: TLS_KEY }, handleConnection)
|
|
55
|
+
: createServer(handleConnection);
|
|
48
56
|
server.listen(0, '127.0.0.1');
|
|
49
57
|
await once(server, 'listening');
|
|
50
58
|
return {
|
|
@@ -160,6 +168,28 @@ const vsrConfig = (port) => ({
|
|
|
160
168
|
reconnect: { enabled: false, interval: 100, maxRetries: 1 }
|
|
161
169
|
});
|
|
162
170
|
describe('VSR client socket', () => {
|
|
171
|
+
it('exchanges VSR frames over TLS', async () => {
|
|
172
|
+
const server = await startVsrServer((frame, socket) => singleNodeHandler(server.port)(frame, socket), 'TLS');
|
|
173
|
+
const client = new CommandResponseStream({
|
|
174
|
+
...vsrConfig(server.port),
|
|
175
|
+
transport: 'TLS',
|
|
176
|
+
options: {
|
|
177
|
+
host: '127.0.0.1',
|
|
178
|
+
port: server.port,
|
|
179
|
+
servername: 'localhost',
|
|
180
|
+
ca: TLS_CA_CERTIFICATE
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
try {
|
|
184
|
+
const response = await client.sendCommand(60_015, Buffer.from('tls'));
|
|
185
|
+
assert.equal(response.status, 0);
|
|
186
|
+
assert.equal(server.frames.length, 3);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
client.destroy();
|
|
190
|
+
await server.close();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
163
193
|
it('registers one session before the first authenticated command', async () => {
|
|
164
194
|
const server = await startVsrServer((frame, socket) => singleNodeHandler(server.port)(frame, socket));
|
|
165
195
|
const client = new CommandResponseStream(vsrConfig(server.port));
|
|
@@ -27,7 +27,6 @@ export declare const handleResponseTransform: () => Transform;
|
|
|
27
27
|
* @returns True if the response indicates success with no data
|
|
28
28
|
*/
|
|
29
29
|
export declare const deserializeVoidResponse: (r: CommandResponse) => boolean;
|
|
30
|
-
export declare const deserializeStatusResponse: (r: CommandResponse) => boolean;
|
|
31
30
|
/**
|
|
32
31
|
* Serializes a command and its payload into a buffer for sending to the server.
|
|
33
32
|
* Creates the wire format: [payload_size (4 bytes)][command (4 bytes)][payload]
|
|
@@ -59,7 +59,6 @@ export const handleResponseTransform = () => new Transform({
|
|
|
59
59
|
* @returns True if the response indicates success with no data
|
|
60
60
|
*/
|
|
61
61
|
export const deserializeVoidResponse = (r) => r.status === 0 && r.data.length === 0;
|
|
62
|
-
export const deserializeStatusResponse = (r) => r.status === 0;
|
|
63
62
|
/** Length of the command code in bytes */
|
|
64
63
|
const COMMAND_LENGTH = 4;
|
|
65
64
|
/**
|
|
@@ -17,9 +17,8 @@
|
|
|
17
17
|
//
|
|
18
18
|
import { describe, it } from 'node:test';
|
|
19
19
|
import assert from 'node:assert/strict';
|
|
20
|
-
import { handleResponse, deserializeVoidResponse
|
|
20
|
+
import { handleResponse, deserializeVoidResponse } from './client.utils.js';
|
|
21
21
|
const SUCCESS = 0;
|
|
22
|
-
const ERROR = 1;
|
|
23
22
|
describe('handleResponse', () => {
|
|
24
23
|
it('bounds data to the length field, not the full buffer', () => {
|
|
25
24
|
// Server says: status=0, length=0, no payload.
|
|
@@ -40,19 +39,4 @@ describe('handleResponse', () => {
|
|
|
40
39
|
assert.equal(deserializeVoidResponse(r), true);
|
|
41
40
|
});
|
|
42
41
|
});
|
|
43
|
-
describe('deserializeStatusResponse', () => {
|
|
44
|
-
it('returns true when status is SUCCESS and data is empty', () => {
|
|
45
|
-
const r = { status: SUCCESS, length: 0, data: Buffer.alloc(0) };
|
|
46
|
-
assert.equal(deserializeStatusResponse(r), true);
|
|
47
|
-
});
|
|
48
|
-
it('returns true when status is SUCCESS and data is non-empty (e.g. SendMessages server payload)', () => {
|
|
49
|
-
// Key difference from deserializeVoidResponse: non-empty data is accepted.
|
|
50
|
-
const r = { status: SUCCESS, length: 4, data: Buffer.from([1, 2, 3, 4]) };
|
|
51
|
-
assert.equal(deserializeStatusResponse(r), true);
|
|
52
|
-
});
|
|
53
|
-
it('returns false when status is an error code', () => {
|
|
54
|
-
const r = { status: ERROR, length: 0, data: Buffer.alloc(0) };
|
|
55
|
-
assert.equal(deserializeStatusResponse(r), false);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
42
|
//# sourceMappingURL=client.utils.test.js.map
|
|
@@ -22,6 +22,9 @@ 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';
|
|
25
28
|
const streamName = 'e2e-stream-934';
|
|
26
29
|
const topicName = 'e2e-topic-832';
|
|
27
30
|
const partitionId = 0;
|
|
@@ -40,7 +43,13 @@ describe('e2e -> message', async () => {
|
|
|
40
43
|
partition: Partitioning.PartitionId(partitionId)
|
|
41
44
|
};
|
|
42
45
|
it('e2e -> message::send', async () => {
|
|
43
|
-
|
|
46
|
+
const { confirmations } = await c.message.send(msg);
|
|
47
|
+
if (!vsr) {
|
|
48
|
+
assert.equal(confirmations.length, 0);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
assert.equal(confirmations.length, 1);
|
|
52
|
+
assert.equal(confirmations[0].partitionId, partitionId);
|
|
44
53
|
});
|
|
45
54
|
it('e2e -> message::poll/last', async () => {
|
|
46
55
|
const pollReq = {
|
|
@@ -143,6 +152,20 @@ describe('e2e -> message', async () => {
|
|
|
143
152
|
});
|
|
144
153
|
assert.deepEqual(offset, { partitionId: 0, currentOffset: 5n, storedOffset: 2n });
|
|
145
154
|
});
|
|
155
|
+
it('e2e -> message::send/next batch', async () => {
|
|
156
|
+
const { confirmations } = await c.message.send({
|
|
157
|
+
...msg,
|
|
158
|
+
messages: generateMessages(3)
|
|
159
|
+
});
|
|
160
|
+
if (!vsr) {
|
|
161
|
+
assert.equal(confirmations.length, 0);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Landing behind the already committed batch is the part no placeholder
|
|
165
|
+
// confirmation could reproduce.
|
|
166
|
+
assert.equal(confirmations.length, 1);
|
|
167
|
+
assert.equal(confirmations[0].baseOffset, BigInt(msg.messages.length));
|
|
168
|
+
});
|
|
146
169
|
it('e2e -> message::cleanup', async () => {
|
|
147
170
|
assert.ok(await c.stream.delete({ streamId: streamName }));
|
|
148
171
|
assert.ok(await c.session.logout());
|
|
@@ -46,9 +46,13 @@ const caCertPath = process.env.E2E_ROOT_CA_CERT
|
|
|
46
46
|
const getTlsClient = () => {
|
|
47
47
|
const [, port] = getIggyAddress();
|
|
48
48
|
const caCert = readFileSync(caCertPath);
|
|
49
|
+
const protocol = process.env.IGGY_TEST_PROTOCOL === 'vsr'
|
|
50
|
+
? 'vsr'
|
|
51
|
+
: 'classic';
|
|
49
52
|
// The server certificate SAN is DNS:localhost, so we connect via 'localhost'
|
|
50
53
|
// for proper hostname verification (consistent with Python and C# TLS tests).
|
|
51
54
|
return new Client({
|
|
55
|
+
protocol,
|
|
52
56
|
transport: 'TLS',
|
|
53
57
|
options: {
|
|
54
58
|
port,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type Id, PollingStrategy, Consumer, Partitioning, HeaderValue, HeaderKeyFactory, } from "./wire/index.js";
|
|
1
|
+
export { type Id, PollingStrategy, Consumer, Partitioning, HeaderValue, HeaderKeyFactory, type SendMessagesConfirmation, type SendMessagesResponse, } from "./wire/index.js";
|
|
2
2
|
export * from "./client/index.js";
|
|
3
3
|
export * from "./stream/index.js";
|
|
4
4
|
export { DeserializeError, ResponseError } from './wire/error.utils.js';
|
package/dist/tcp.sm.utils.d.ts
CHANGED
|
@@ -55,7 +55,7 @@ export declare const generateMessages: (count?: number) => ({
|
|
|
55
55
|
}[];
|
|
56
56
|
id: string;
|
|
57
57
|
})[];
|
|
58
|
-
export declare const sendSomeMessages: (s: ClientProvider) => (streamId: Id, topicId: Id, partition: Partitioning) => Promise<
|
|
58
|
+
export declare const sendSomeMessages: (s: ClientProvider) => (streamId: Id, topicId: Id, partition: Partitioning) => Promise<import("./wire/index.js").SendMessagesResponse>;
|
|
59
59
|
export declare const formatPolledMessages: (msgs: Message[]) => {
|
|
60
60
|
id: string | bigint;
|
|
61
61
|
offset: bigint;
|
|
@@ -75,7 +75,7 @@ declare const offsetAPI: (c: ClientProvider) => {
|
|
|
75
75
|
type OffsetAPI = ReturnType<typeof offsetAPI>;
|
|
76
76
|
declare const messageAPI: (c: ClientProvider) => {
|
|
77
77
|
poll: (request: import("./message/poll-messages.command.js").PollMessages) => Promise<import("./index.js").PollMessagesResponse>;
|
|
78
|
-
send: (arg: import("./message/send-messages.command.js").SendMessages) => Promise<
|
|
78
|
+
send: (arg: import("./message/send-messages.command.js").SendMessages) => Promise<import("./message/send-messages.command.js").SendMessagesResponse>;
|
|
79
79
|
flushUnsavedBuffers: (arg: import("./message/flush-unsaved-buffers.command.js").FlushUnsavedBuffer) => Promise<boolean>;
|
|
80
80
|
};
|
|
81
81
|
type MessageAPI = ReturnType<typeof messageAPI>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Id } from '../identifier.utils.js';
|
|
2
2
|
import { type CreateMessage } from './message.utils.js';
|
|
3
3
|
import type { Partitioning } from './partitioning.utils.js';
|
|
4
|
+
import type { CommandResponse } from '../../client/client.type.js';
|
|
4
5
|
/**
|
|
5
6
|
* Parameters for the send messages command.
|
|
6
7
|
*/
|
|
@@ -14,6 +15,33 @@ export type SendMessages = {
|
|
|
14
15
|
/** Optional partitioning strategy */
|
|
15
16
|
partition?: Partitioning;
|
|
16
17
|
};
|
|
18
|
+
/** Commit confirmation for one partition written by a send. */
|
|
19
|
+
export type SendMessagesConfirmation = {
|
|
20
|
+
/** Numeric id of the stream the batch was written to */
|
|
21
|
+
streamId: number;
|
|
22
|
+
/** Numeric id of the topic the batch was written to */
|
|
23
|
+
topicId: number;
|
|
24
|
+
/** Partition the batch was written to */
|
|
25
|
+
partitionId: number;
|
|
26
|
+
/**
|
|
27
|
+
* Offset assigned to the first message of the batch in that partition.
|
|
28
|
+
*
|
|
29
|
+
* Delivery is at-least-once, so an earlier retry of the same batch may
|
|
30
|
+
* already have committed at a lower offset: this never identifies a batch
|
|
31
|
+
* uniquely. A batch is confirmed once it is committed in memory, not once it
|
|
32
|
+
* is fsynced, so a crash-restart can stamp a later batch with an offset a
|
|
33
|
+
* client has already recorded.
|
|
34
|
+
*/
|
|
35
|
+
baseOffset: bigint;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Outcome of a successful send, one confirmation per written partition. The
|
|
39
|
+
* legacy server returns an empty list: it commits without reporting offsets.
|
|
40
|
+
*/
|
|
41
|
+
export type SendMessagesResponse = {
|
|
42
|
+
/** Commit confirmations, one per partition the batch was written to */
|
|
43
|
+
confirmations: SendMessagesConfirmation[];
|
|
44
|
+
};
|
|
17
45
|
/**
|
|
18
46
|
* Send messages command definition.
|
|
19
47
|
* Publishes messages to a topic.
|
|
@@ -21,10 +49,11 @@ export type SendMessages = {
|
|
|
21
49
|
export declare const SEND_MESSAGES: {
|
|
22
50
|
code: number;
|
|
23
51
|
serialize: ({ streamId, topicId, messages, partition }: SendMessages) => Buffer<ArrayBuffer>;
|
|
24
|
-
deserialize: (r:
|
|
52
|
+
deserialize: (r: CommandResponse) => SendMessagesResponse;
|
|
25
53
|
};
|
|
26
54
|
/**
|
|
27
|
-
* Executable send messages command function.
|
|
55
|
+
* Executable send messages command function. Resolves to the commit
|
|
56
|
+
* confirmations of the written partitions, empty against the legacy server.
|
|
28
57
|
*/
|
|
29
|
-
export declare const sendMessages: (getClient: import("../../
|
|
58
|
+
export declare const sendMessages: (getClient: import("../../client/client.type.js").ClientProvider) => (arg: SendMessages) => Promise<SendMessagesResponse>;
|
|
30
59
|
//# sourceMappingURL=send-messages.command.d.ts.map
|
|
@@ -16,9 +16,46 @@
|
|
|
16
16
|
// under the License.
|
|
17
17
|
//
|
|
18
18
|
import { serializeSendMessages } from './message.utils.js';
|
|
19
|
-
import {
|
|
19
|
+
import { DeserializeError } from '../error.utils.js';
|
|
20
20
|
import { wrapCommand } from '../command.utils.js';
|
|
21
21
|
import { COMMAND_CODE } from '../command.code.js';
|
|
22
|
+
/** Size of the confirmation count prefixing the list. */
|
|
23
|
+
const CONFIRMATIONS_COUNT_SIZE = 4;
|
|
24
|
+
/**
|
|
25
|
+
* Size of one confirmation entry:
|
|
26
|
+
* `stream_id(4) + topic_id(4) + partition_id(4) + base_offset(8)`.
|
|
27
|
+
*/
|
|
28
|
+
const CONFIRMATION_SIZE = 20;
|
|
29
|
+
/**
|
|
30
|
+
* Decodes the reply body of a send: `[confirmations_count:4]` then that many
|
|
31
|
+
* `[stream_id:4 topic_id:4 partition_id:4 base_offset:8]` entries.
|
|
32
|
+
*
|
|
33
|
+
* The legacy server reports a commit by sending no body at all, so absence
|
|
34
|
+
* decodes to no confirmations instead of surfacing as a decode failure.
|
|
35
|
+
*/
|
|
36
|
+
const deserializeSendMessages = (data) => {
|
|
37
|
+
if (data.length === 0)
|
|
38
|
+
return { confirmations: [] };
|
|
39
|
+
if (data.length < CONFIRMATIONS_COUNT_SIZE)
|
|
40
|
+
throw new DeserializeError('send messages confirmation count is truncated');
|
|
41
|
+
const count = data.readUInt32LE(0);
|
|
42
|
+
const expected = CONFIRMATIONS_COUNT_SIZE + count * CONFIRMATION_SIZE;
|
|
43
|
+
if (expected > data.length)
|
|
44
|
+
throw new DeserializeError('send messages confirmation list is truncated');
|
|
45
|
+
if (expected !== data.length)
|
|
46
|
+
throw new DeserializeError('send messages confirmations have trailing bytes');
|
|
47
|
+
const confirmations = new Array(count);
|
|
48
|
+
for (let index = 0; index < count; index += 1) {
|
|
49
|
+
const at = CONFIRMATIONS_COUNT_SIZE + index * CONFIRMATION_SIZE;
|
|
50
|
+
confirmations[index] = {
|
|
51
|
+
streamId: data.readUInt32LE(at),
|
|
52
|
+
topicId: data.readUInt32LE(at + 4),
|
|
53
|
+
partitionId: data.readUInt32LE(at + 8),
|
|
54
|
+
baseOffset: data.readBigUInt64LE(at + 12)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { confirmations };
|
|
58
|
+
};
|
|
22
59
|
/**
|
|
23
60
|
* Send messages command definition.
|
|
24
61
|
* Publishes messages to a topic.
|
|
@@ -28,10 +65,11 @@ export const SEND_MESSAGES = {
|
|
|
28
65
|
serialize: ({ streamId, topicId, messages, partition }) => {
|
|
29
66
|
return serializeSendMessages(streamId, topicId, messages, partition);
|
|
30
67
|
},
|
|
31
|
-
deserialize:
|
|
68
|
+
deserialize: (r) => deserializeSendMessages(r.data)
|
|
32
69
|
};
|
|
33
70
|
/**
|
|
34
|
-
* Executable send messages command function.
|
|
71
|
+
* Executable send messages command function. Resolves to the commit
|
|
72
|
+
* confirmations of the written partitions, empty against the legacy server.
|
|
35
73
|
*/
|
|
36
74
|
export const sendMessages = wrapCommand(SEND_MESSAGES);
|
|
37
75
|
//# sourceMappingURL=send-messages.command.js.map
|
|
@@ -18,10 +18,34 @@
|
|
|
18
18
|
import { describe, it } from "node:test";
|
|
19
19
|
import assert from "node:assert/strict";
|
|
20
20
|
import { uuidv7, uuidv4 } from "uuidv7";
|
|
21
|
-
import { SEND_MESSAGES } from "./send-messages.command.js";
|
|
21
|
+
import { SEND_MESSAGES, } from "./send-messages.command.js";
|
|
22
22
|
import { HeaderValue, HeaderKeyFactory } from "./header.utils.js";
|
|
23
|
+
import { DeserializeError } from "../error.utils.js";
|
|
23
24
|
const SUCCESS = 0;
|
|
24
|
-
const
|
|
25
|
+
const CONFIRMATION_SIZE = 20;
|
|
26
|
+
const confirmation = (partitionId) => ({
|
|
27
|
+
streamId: 1,
|
|
28
|
+
topicId: 2,
|
|
29
|
+
partitionId,
|
|
30
|
+
baseOffset: 42n,
|
|
31
|
+
});
|
|
32
|
+
const serializeConfirmations = (confirmations) => {
|
|
33
|
+
const b = Buffer.allocUnsafe(4 + confirmations.length * CONFIRMATION_SIZE);
|
|
34
|
+
b.writeUInt32LE(confirmations.length, 0);
|
|
35
|
+
confirmations.forEach((c, index) => {
|
|
36
|
+
const at = 4 + index * CONFIRMATION_SIZE;
|
|
37
|
+
b.writeUInt32LE(c.streamId, at);
|
|
38
|
+
b.writeUInt32LE(c.topicId, at + 4);
|
|
39
|
+
b.writeUInt32LE(c.partitionId, at + 8);
|
|
40
|
+
b.writeBigUInt64LE(c.baseOffset, at + 12);
|
|
41
|
+
});
|
|
42
|
+
return b;
|
|
43
|
+
};
|
|
44
|
+
const response = (data) => ({
|
|
45
|
+
status: SUCCESS,
|
|
46
|
+
length: data.length,
|
|
47
|
+
data,
|
|
48
|
+
});
|
|
25
49
|
describe("SendMessages", () => {
|
|
26
50
|
describe("serialize", () => {
|
|
27
51
|
const t1 = {
|
|
@@ -150,19 +174,54 @@ describe("SendMessages", () => {
|
|
|
150
174
|
});
|
|
151
175
|
});
|
|
152
176
|
describe('deserialize', () => {
|
|
153
|
-
it('
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const r =
|
|
161
|
-
assert.
|
|
162
|
-
});
|
|
163
|
-
it('
|
|
164
|
-
const r =
|
|
165
|
-
|
|
177
|
+
it('reads one confirmation', () => {
|
|
178
|
+
const confirmations = [confirmation(3)];
|
|
179
|
+
const r = response(serializeConfirmations(confirmations));
|
|
180
|
+
assert.deepEqual(SEND_MESSAGES.deserialize(r), { confirmations });
|
|
181
|
+
});
|
|
182
|
+
it('reads every confirmation of a multi-partition send', () => {
|
|
183
|
+
const confirmations = [confirmation(0), confirmation(1), confirmation(2)];
|
|
184
|
+
const r = response(serializeConfirmations(confirmations));
|
|
185
|
+
assert.deepEqual(SEND_MESSAGES.deserialize(r), { confirmations });
|
|
186
|
+
});
|
|
187
|
+
it('reads the wire layout of a confirmation', () => {
|
|
188
|
+
const r = response(Buffer.from([
|
|
189
|
+
0x01, 0x00, 0x00, 0x00, // count
|
|
190
|
+
0x01, 0x00, 0x00, 0x00, // streamId
|
|
191
|
+
0x02, 0x00, 0x00, 0x00, // topicId
|
|
192
|
+
0x03, 0x00, 0x00, 0x00, // partitionId
|
|
193
|
+
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // baseOffset
|
|
194
|
+
]));
|
|
195
|
+
assert.deepEqual(SEND_MESSAGES.deserialize(r), {
|
|
196
|
+
confirmations: [
|
|
197
|
+
{ streamId: 1, topicId: 2, partitionId: 3, baseOffset: 4n }
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
it('reads a committed send that reports no offsets as an empty list', () => {
|
|
202
|
+
const r = response(serializeConfirmations([]));
|
|
203
|
+
assert.deepEqual(SEND_MESSAGES.deserialize(r), { confirmations: [] });
|
|
204
|
+
});
|
|
205
|
+
it('reads the bodiless legacy server reply as an empty list', () => {
|
|
206
|
+
const r = response(Buffer.alloc(0));
|
|
207
|
+
assert.deepEqual(SEND_MESSAGES.deserialize(r), { confirmations: [] });
|
|
208
|
+
});
|
|
209
|
+
it('throws on a truncated body', () => {
|
|
210
|
+
const data = serializeConfirmations([confirmation(0), confirmation(1)]);
|
|
211
|
+
for (let i = 1; i < data.length; i += 1)
|
|
212
|
+
assert.throws(() => SEND_MESSAGES.deserialize(response(data.subarray(0, i))), DeserializeError, `expected error for truncation at byte ${i}`);
|
|
213
|
+
});
|
|
214
|
+
it('throws on trailing bytes', () => {
|
|
215
|
+
const data = Buffer.concat([
|
|
216
|
+
serializeConfirmations([confirmation(1)]),
|
|
217
|
+
Buffer.from([0xFF])
|
|
218
|
+
]);
|
|
219
|
+
assert.throws(() => SEND_MESSAGES.deserialize(response(data)), DeserializeError);
|
|
220
|
+
});
|
|
221
|
+
it('throws on a count no body could hold', () => {
|
|
222
|
+
const data = Buffer.alloc(4);
|
|
223
|
+
data.writeUInt32LE(0xFFFF_FFFF, 0);
|
|
224
|
+
assert.throws(() => SEND_MESSAGES.deserialize(response(data)), DeserializeError);
|
|
166
225
|
});
|
|
167
226
|
});
|
|
168
227
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Packed protocol semver of the wire contract this port implements,
|
|
3
|
-
* `pack(0,
|
|
3
|
+
* `pack(0, 11, 0)` per `core/binary_protocol/src/version.rs`. Bump together
|
|
4
4
|
* with the Rust `IGGY_PROTOCOL_VERSION` on any wire-incompatible change.
|
|
5
5
|
*/
|
|
6
6
|
export declare const IGGY_PROTOCOL_VERSION: number;
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
import { DeserializeError } from '../error.utils.js';
|
|
26
26
|
/**
|
|
27
27
|
* Packed protocol semver of the wire contract this port implements,
|
|
28
|
-
* `pack(0,
|
|
28
|
+
* `pack(0, 11, 0)` per `core/binary_protocol/src/version.rs`. Bump together
|
|
29
29
|
* with the Rust `IGGY_PROTOCOL_VERSION` on any wire-incompatible change.
|
|
30
30
|
*/
|
|
31
|
-
export const IGGY_PROTOCOL_VERSION = (0 << 20) | (
|
|
31
|
+
export const IGGY_PROTOCOL_VERSION = (0 << 20) | (11 << 10) | 0;
|
|
32
32
|
const SDK_NAME = 'node-sdk';
|
|
33
33
|
const wireName = (value) => {
|
|
34
34
|
const bytes = Buffer.from(value, 'utf8');
|
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.9.0-edge.1",
|
|
5
5
|
"description": "Official Apache Iggy NodeJS SDK",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"iggy",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@commitlint/cli": "21.2.1",
|
|
60
60
|
"@commitlint/config-conventional": "21.2.0",
|
|
61
|
-
"@cucumber/cucumber": "13.
|
|
62
|
-
"@swc-node/register": "1.12.
|
|
61
|
+
"@cucumber/cucumber": "13.2.0",
|
|
62
|
+
"@swc-node/register": "1.12.1",
|
|
63
63
|
"@types/debug": "4.1.13",
|
|
64
64
|
"@types/node": "26.1.1",
|
|
65
65
|
"c8": "^12.0.0",
|