apache-iggy 0.10.0-edge.7 → 0.10.0
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 +16 -9
- package/dist/client/client.connection.d.ts +1 -1
- package/dist/client/client.connection.js +2 -2
- package/dist/client/client.socket.d.ts +12 -0
- package/dist/client/client.socket.js +271 -5
- package/dist/client/client.socket.test.js +604 -0
- package/dist/wire/command.code.d.ts +4 -0
- package/dist/wire/command.code.js +4 -0
- package/dist/wire/message/header.type.d.ts +1 -1
- package/dist/wire/message/poll-messages.command.js +4 -1
- package/dist/wire/message/poll-messages.command.test.js +11 -0
- package/dist/wire/token/token.utils.js +3 -7
- package/dist/wire/token/token.utils.test.d.ts +2 -0
- package/dist/wire/token/token.utils.test.js +67 -0
- package/dist/wire/vsr/header.d.ts +1 -0
- package/dist/wire/vsr/header.js +1 -0
- package/dist/wire/vsr/index.d.ts +4 -0
- package/dist/wire/vsr/index.js +1 -1
- package/package.json +2 -2
|
@@ -21,6 +21,13 @@ import { createServer } from 'node:net';
|
|
|
21
21
|
import { describe, it } from 'node:test';
|
|
22
22
|
import { createServer as createTlsServer } from 'node:tls';
|
|
23
23
|
import { COMMAND_CODE } from '../wire/command.code.js';
|
|
24
|
+
import { LOGIN, LOGIN_WITH_TOKEN, LOGOUT } from '../wire/index.js';
|
|
25
|
+
import { UPDATE_USER } from '../wire/user/update-user.command.js';
|
|
26
|
+
import { CHANGE_PASSWORD } from '../wire/user/change-password.command.js';
|
|
27
|
+
import { POLL_MESSAGES } from '../wire/message/poll-messages.command.js';
|
|
28
|
+
import { PollingStrategy } from '../wire/message/poll.utils.js';
|
|
29
|
+
import { ConsumerKind } from '../wire/offset/offset.utils.js';
|
|
30
|
+
import { readWireName } from '../wire/vsr/index.js';
|
|
24
31
|
import { ResponseError } from '../wire/error.utils.js';
|
|
25
32
|
import { Command, EVICTION_OFFSET, EvictionReason, HEADER_SIZE, REPLY_OFFSET, REQUEST_OFFSET } from '../wire/vsr/header.js';
|
|
26
33
|
import { Operation } from '../wire/vsr/operation.js';
|
|
@@ -208,6 +215,603 @@ const compressLeaderlessPoll = (client, budget) => {
|
|
|
208
215
|
settlement.leaderlessWaitBudget = budget;
|
|
209
216
|
settlement.leaderlessPollInterval = 1;
|
|
210
217
|
};
|
|
218
|
+
const groupPollPayload = (autocommit = true, partitionId = 0) => POLL_MESSAGES.serialize({
|
|
219
|
+
streamId: 1,
|
|
220
|
+
topicId: 1,
|
|
221
|
+
consumer: { kind: ConsumerKind.Group, id: 1 },
|
|
222
|
+
partitionId,
|
|
223
|
+
pollingStrategy: PollingStrategy.Next,
|
|
224
|
+
count: 1,
|
|
225
|
+
autocommit
|
|
226
|
+
});
|
|
227
|
+
const routingBody = (request, port, watermark) => {
|
|
228
|
+
const attachment = Buffer.alloc(32);
|
|
229
|
+
request.copy(attachment, 0, REQUEST_OFFSET.client, REQUEST_OFFSET.client + 16);
|
|
230
|
+
request.copy(attachment, 16, REQUEST_OFFSET.session, REQUEST_OFFSET.session + 8);
|
|
231
|
+
attachment.writeBigUInt64LE(watermark, 24);
|
|
232
|
+
const metadata = singleNodeMetadataBody(port);
|
|
233
|
+
const nodeOffset = 4 + metadata.readUInt32LE(0) + 4;
|
|
234
|
+
return Buffer.concat([attachment, metadata.subarray(nodeOffset)]);
|
|
235
|
+
};
|
|
236
|
+
const countCommand = (server, code) => server.frames.filter((frame) => frame.readUInt8(REQUEST_OFFSET.operation) === Operation.NonReplicated &&
|
|
237
|
+
frame.readUInt32LE(REQUEST_OFFSET.reserved) === code).length;
|
|
238
|
+
const registerCredentialsBody = (frame) => {
|
|
239
|
+
const sdkName = readWireName(frame, HEADER_SIZE + 4);
|
|
240
|
+
const sdkVersion = readWireName(frame, sdkName.next);
|
|
241
|
+
return frame.subarray(sdkVersion.next);
|
|
242
|
+
};
|
|
243
|
+
const startPollCluster = async (primaryOverride, coordinatorOverride, transport = 'TCP') => {
|
|
244
|
+
const primary = await startVsrServer((frame, socket) => {
|
|
245
|
+
if (primaryOverride?.(frame, socket))
|
|
246
|
+
return;
|
|
247
|
+
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.PollMessagesOnPrimary) {
|
|
248
|
+
socket.write(replyFrame(Operation.NonReplicated, Buffer.from('primary')));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
singleNodeHandler(primary.port)(frame, socket);
|
|
252
|
+
}, transport);
|
|
253
|
+
const coordinator = await startVsrServer((frame, socket) => {
|
|
254
|
+
if (coordinatorOverride?.(frame, socket))
|
|
255
|
+
return;
|
|
256
|
+
const code = frame.readUInt32LE(REQUEST_OFFSET.reserved);
|
|
257
|
+
if (code === COMMAND_CODE.GetClusterMetadata) {
|
|
258
|
+
socket.write(replyFrame(Operation.NonReplicated, twoNodeMetadataBody(primary.port, coordinator.port)));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (code === COMMAND_CODE.GetPollRouting) {
|
|
262
|
+
socket.write(replyFrame(Operation.NonReplicated, routingBody(frame, primary.port, 1n)));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
singleNodeHandler(coordinator.port)(frame, socket);
|
|
266
|
+
}, transport);
|
|
267
|
+
const config = {
|
|
268
|
+
...vsrConfig(coordinator.port), transport, heartbeatInterval: 0,
|
|
269
|
+
options: {
|
|
270
|
+
...vsrConfig(coordinator.port).options,
|
|
271
|
+
...(transport === 'TLS' ? { ca: TLS_CA_CERTIFICATE, servername: 'localhost' } : {})
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
const client = new CommandResponseStream(config);
|
|
275
|
+
return {
|
|
276
|
+
client, primary, coordinator,
|
|
277
|
+
close: async () => {
|
|
278
|
+
client.destroy();
|
|
279
|
+
await Promise.all([coordinator.close(), primary.close()]);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
describe('primary auto-commit polling', () => {
|
|
284
|
+
for (const transport of ['TCP', 'TLS']) {
|
|
285
|
+
for (const refused of [false, true]) {
|
|
286
|
+
it(`preserves raw poll responses over ${transport}${refused ? ' after a refusal' : ''}`, async () => {
|
|
287
|
+
const expected = replyFrame(Operation.NonReplicated, Buffer.from('raw primary'));
|
|
288
|
+
let polls = 0;
|
|
289
|
+
const cluster = await startPollCluster((frame, socket) => {
|
|
290
|
+
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.PollMessagesOnPrimary)
|
|
291
|
+
return false;
|
|
292
|
+
polls += 1;
|
|
293
|
+
socket.write(refused && polls === 1
|
|
294
|
+
? replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58)
|
|
295
|
+
: expected);
|
|
296
|
+
return true;
|
|
297
|
+
}, undefined, transport);
|
|
298
|
+
let resets = 0;
|
|
299
|
+
cluster.client.on('sessionReset', () => { resets += 1; });
|
|
300
|
+
try {
|
|
301
|
+
for (let poll = 0; poll < 2; poll += 1) {
|
|
302
|
+
const response = await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload(), { handleResponse: false });
|
|
303
|
+
assert.ok(Buffer.isBuffer(response), 'raw polls must return the complete VSR frame');
|
|
304
|
+
assert.deepEqual(response, expected);
|
|
305
|
+
}
|
|
306
|
+
assert.equal(polls, refused ? 3 : 2);
|
|
307
|
+
assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPollRouting), refused ? 2 : 1);
|
|
308
|
+
assert.equal(countCommand(cluster.primary, COMMAND_CODE.AttachConsumerSession), refused ? 2 : 1);
|
|
309
|
+
assert.equal(resets, 0);
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
await cluster.close();
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const transport of ['TCP', 'TLS']) {
|
|
318
|
+
it(`retains the coordinator session and reuses authenticated primary polls over ${transport}`, async () => {
|
|
319
|
+
const cluster = await startPollCluster(undefined, undefined, transport);
|
|
320
|
+
const { client, primary, coordinator } = cluster;
|
|
321
|
+
let resets = 0;
|
|
322
|
+
client.on('sessionReset', () => { resets += 1; });
|
|
323
|
+
try {
|
|
324
|
+
const first = await client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
325
|
+
const second = await client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
326
|
+
assert.equal(first.data.toString(), 'primary');
|
|
327
|
+
assert.equal(second.data.toString(), 'primary');
|
|
328
|
+
assert.equal(countCommand(coordinator, COMMAND_CODE.GetPollRouting), 1);
|
|
329
|
+
assert.equal(countCommand(coordinator, COMMAND_CODE.PollMessages), 0);
|
|
330
|
+
assert.equal(countCommand(primary, COMMAND_CODE.PollMessagesOnPrimary), 2);
|
|
331
|
+
assert.equal(countCommand(primary, COMMAND_CODE.AttachConsumerSession), 1);
|
|
332
|
+
assert.equal(countCommand(primary, COMMAND_CODE.GetClusterMetadata), 0, 'auxiliary login must stay on the partition primary');
|
|
333
|
+
assert.equal(primary.frames.filter((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register).length, 1);
|
|
334
|
+
const route = coordinator.frames.find((frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.GetPollRouting);
|
|
335
|
+
const attach = primary.frames.find((frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.AttachConsumerSession);
|
|
336
|
+
assert.deepEqual(attach.subarray(HEADER_SIZE, HEADER_SIZE + 16), route.subarray(REQUEST_OFFSET.client, REQUEST_OFFSET.client + 16));
|
|
337
|
+
assert.equal(attach.readBigUInt64LE(HEADER_SIZE + 16), TEST_SESSION);
|
|
338
|
+
assert.notDeepEqual(attach.subarray(REQUEST_OFFSET.client, REQUEST_OFFSET.client + 16), route.subarray(REQUEST_OFFSET.client, REQUEST_OFFSET.client + 16), 'the auxiliary must authenticate under its own VSR identity');
|
|
339
|
+
assert.equal(resets, 0);
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
await cluster.close();
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
it('preserves manual-commit and standalone poll paths', async () => {
|
|
347
|
+
const cluster = await startPollCluster();
|
|
348
|
+
const server = await startVsrServer((frame, socket) => singleNodeHandler(server.port)(frame, socket));
|
|
349
|
+
const standalone = new CommandResponseStream({ ...vsrConfig(server.port), heartbeatInterval: 0 });
|
|
350
|
+
try {
|
|
351
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload(false));
|
|
352
|
+
await standalone.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
353
|
+
assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.PollMessages), 1);
|
|
354
|
+
assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPollRouting), 0);
|
|
355
|
+
assert.equal(countCommand(server, COMMAND_CODE.PollMessages), 1);
|
|
356
|
+
assert.equal(countCommand(server, COMMAND_CODE.GetPollRouting), 0);
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
standalone.destroy();
|
|
360
|
+
await cluster.close();
|
|
361
|
+
await server.close();
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
it('refreshes the attachment after a metadata acknowledgement but ignores partition commits', async () => {
|
|
365
|
+
const cluster = await startPollCluster(undefined, (frame, socket) => {
|
|
366
|
+
const operation = frame[REQUEST_OFFSET.operation];
|
|
367
|
+
if (operation !== Operation.UpdateTopic && operation !== Operation.SendMessages)
|
|
368
|
+
return false;
|
|
369
|
+
const response = replyFrame(operation, operation === Operation.UpdateTopic ? Buffer.alloc(4) : Buffer.alloc(0));
|
|
370
|
+
response.writeBigUInt64LE(operation === Operation.UpdateTopic ? 15n : 900n, REPLY_OFFSET.commit);
|
|
371
|
+
socket.write(response);
|
|
372
|
+
return true;
|
|
373
|
+
});
|
|
374
|
+
try {
|
|
375
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
376
|
+
await cluster.client.sendCommand(COMMAND_CODE.SendMessages, Buffer.alloc(0));
|
|
377
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
378
|
+
assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPollRouting), 1);
|
|
379
|
+
await cluster.client.sendCommand(COMMAND_CODE.UpdateTopic, Buffer.alloc(0), { handleResponse: false });
|
|
380
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
381
|
+
const attachments = cluster.primary.frames.filter((frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.AttachConsumerSession);
|
|
382
|
+
assert.deepEqual(attachments.map((frame) => frame.readBigUInt64LE(HEADER_SIZE + 24)), [1n, 15n]);
|
|
383
|
+
assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPollRouting), 2);
|
|
384
|
+
await cluster.client.sendCommand(LOGOUT.code, LOGOUT.serialize());
|
|
385
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
386
|
+
const replacement = cluster.primary.frames.filter((frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.AttachConsumerSession).at(-1);
|
|
387
|
+
assert.equal(replacement.readBigUInt64LE(HEADER_SIZE + 24), 15n, 'a new coordinator session must retain previously acknowledged metadata');
|
|
388
|
+
}
|
|
389
|
+
finally {
|
|
390
|
+
await cluster.close();
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
it('refreshes a not-admitted route without moving the coordinator', async () => {
|
|
394
|
+
let polls = 0;
|
|
395
|
+
const cluster = await startPollCluster((frame, socket) => {
|
|
396
|
+
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.PollMessagesOnPrimary)
|
|
397
|
+
return false;
|
|
398
|
+
polls += 1;
|
|
399
|
+
if (polls > 1)
|
|
400
|
+
return false;
|
|
401
|
+
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58));
|
|
402
|
+
return true;
|
|
403
|
+
});
|
|
404
|
+
let resets = 0;
|
|
405
|
+
cluster.client.on('sessionReset', () => { resets += 1; });
|
|
406
|
+
try {
|
|
407
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
408
|
+
assert.equal(polls, 2);
|
|
409
|
+
assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPollRouting), 2);
|
|
410
|
+
assert.equal(countCommand(cluster.primary, COMMAND_CODE.AttachConsumerSession), 2);
|
|
411
|
+
assert.equal(resets, 0);
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
await cluster.close();
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
for (const refusing of [COMMAND_CODE.GetPollRouting, COMMAND_CODE.AttachConsumerSession,
|
|
418
|
+
COMMAND_CODE.PollMessagesOnPrimary]) {
|
|
419
|
+
it(`reports not accepted after command ${refusing} refuses the whole poll budget`, async () => {
|
|
420
|
+
const refuse = (frame, socket) => {
|
|
421
|
+
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== refusing)
|
|
422
|
+
return false;
|
|
423
|
+
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 58));
|
|
424
|
+
return true;
|
|
425
|
+
};
|
|
426
|
+
const cluster = await startPollCluster(refuse, refuse);
|
|
427
|
+
let resets = 0;
|
|
428
|
+
cluster.client.on('sessionReset', () => { resets += 1; });
|
|
429
|
+
try {
|
|
430
|
+
await assert.rejects(cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload(), { deadline: Date.now() + 250 }), (error) => error instanceof ResponseError && error.errorCode === 58);
|
|
431
|
+
assert.ok(countCommand(cluster.coordinator, COMMAND_CODE.GetPollRouting) >= 2);
|
|
432
|
+
if (refusing === COMMAND_CODE.GetPollRouting)
|
|
433
|
+
assert.equal(cluster.primary.frames.length, 0);
|
|
434
|
+
if (refusing === COMMAND_CODE.AttachConsumerSession)
|
|
435
|
+
assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessagesOnPrimary), 0);
|
|
436
|
+
assert.equal(resets, 0);
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
await cluster.close();
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
it('reports a typed error when the primary has no TCP endpoint', async () => {
|
|
444
|
+
const cluster = await startPollCluster(undefined, (frame, socket) => {
|
|
445
|
+
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.GetPollRouting)
|
|
446
|
+
return false;
|
|
447
|
+
socket.write(replyFrame(Operation.NonReplicated, routingBody(frame, 0, 1n)));
|
|
448
|
+
return true;
|
|
449
|
+
});
|
|
450
|
+
let resets = 0;
|
|
451
|
+
cluster.client.on('sessionReset', () => { resets += 1; });
|
|
452
|
+
try {
|
|
453
|
+
await assert.rejects(cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()), (error) => error instanceof ResponseError && error.errorCode === 5);
|
|
454
|
+
assert.equal(cluster.primary.frames.length, 0);
|
|
455
|
+
assert.equal(resets, 0);
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
await cluster.close();
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
for (const standalone of [false, true]) {
|
|
462
|
+
for (const failure of ['empty reply', 'empty roster', 'unavailable']) {
|
|
463
|
+
it(`requires valid topology after ${failure} before ${standalone ? 'standalone' : 'cluster'} polling`, async () => {
|
|
464
|
+
const clusterName = Buffer.from('cluster-without-nodes');
|
|
465
|
+
const emptyRoster = Buffer.alloc(4 + clusterName.length + 4);
|
|
466
|
+
emptyRoster.writeUInt32LE(clusterName.length, 0);
|
|
467
|
+
clusterName.copy(emptyRoster, 4);
|
|
468
|
+
let rejectMetadata = true;
|
|
469
|
+
let coordinatorPort = 0;
|
|
470
|
+
const cluster = await startPollCluster(undefined, (frame, socket) => {
|
|
471
|
+
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.GetClusterMetadata)
|
|
472
|
+
return false;
|
|
473
|
+
if (rejectMetadata) {
|
|
474
|
+
socket.write(replyFrame(Operation.NonReplicated, failure === 'empty roster' ? emptyRoster : Buffer.alloc(0), failure === 'unavailable' ? 5 : 0));
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
if (standalone) {
|
|
478
|
+
socket.write(replyFrame(Operation.NonReplicated, singleNodeMetadataBody(coordinatorPort)));
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
return false;
|
|
482
|
+
});
|
|
483
|
+
coordinatorPort = cluster.coordinator.port;
|
|
484
|
+
let resets = 0;
|
|
485
|
+
cluster.client.on('sessionReset', () => { resets += 1; });
|
|
486
|
+
try {
|
|
487
|
+
await cluster.client.authenticate(vsrConfig(coordinatorPort).credentials);
|
|
488
|
+
await assert.rejects(cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload()));
|
|
489
|
+
assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.PollMessages), 0);
|
|
490
|
+
assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPollRouting), 0);
|
|
491
|
+
assert.equal(cluster.primary.frames.length, 0);
|
|
492
|
+
rejectMetadata = false;
|
|
493
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
494
|
+
rejectMetadata = true;
|
|
495
|
+
const refresh = cluster.client.sendCommand(COMMAND_CODE.GetClusterMetadata, Buffer.alloc(0));
|
|
496
|
+
if (failure === 'unavailable')
|
|
497
|
+
await assert.rejects(refresh, (error) => error instanceof ResponseError && error.errorCode === 5);
|
|
498
|
+
else
|
|
499
|
+
await refresh;
|
|
500
|
+
const reads = countCommand(cluster.coordinator, COMMAND_CODE.GetClusterMetadata);
|
|
501
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
502
|
+
assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetClusterMetadata), reads);
|
|
503
|
+
assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.PollMessages), standalone ? 2 : 0);
|
|
504
|
+
assert.equal(countCommand(cluster.coordinator, COMMAND_CODE.GetPollRouting), standalone ? 0 : 1);
|
|
505
|
+
assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessagesOnPrimary), standalone ? 0 : 2);
|
|
506
|
+
assert.equal(resets, 0);
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
await cluster.close();
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
it('refreshes a stalled auxiliary TLS dial within the shared poll budget', async () => {
|
|
515
|
+
const held = [];
|
|
516
|
+
const silent = createServer((socket) => {
|
|
517
|
+
held.push(socket);
|
|
518
|
+
socket.resume();
|
|
519
|
+
});
|
|
520
|
+
silent.listen(0, '127.0.0.1');
|
|
521
|
+
await once(silent, 'listening');
|
|
522
|
+
const silentPort = silent.address().port;
|
|
523
|
+
let routes = 0;
|
|
524
|
+
const cluster = await startPollCluster(undefined, (frame, socket) => {
|
|
525
|
+
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.GetPollRouting || ++routes > 1)
|
|
526
|
+
return false;
|
|
527
|
+
socket.write(replyFrame(Operation.NonReplicated, routingBody(frame, silentPort, 1n)));
|
|
528
|
+
return true;
|
|
529
|
+
}, 'TLS');
|
|
530
|
+
let resets = 0;
|
|
531
|
+
cluster.client.on('sessionReset', () => { resets += 1; });
|
|
532
|
+
try {
|
|
533
|
+
const response = await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload(), { deadline: Date.now() + 5_000 });
|
|
534
|
+
assert.equal(response.data.toString(), 'primary');
|
|
535
|
+
assert.equal(routes, 2);
|
|
536
|
+
assert.equal(held.length, 1);
|
|
537
|
+
assert.equal(held[0].destroyed, true);
|
|
538
|
+
assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessagesOnPrimary), 1);
|
|
539
|
+
assert.equal(resets, 0);
|
|
540
|
+
}
|
|
541
|
+
finally {
|
|
542
|
+
await cluster.close();
|
|
543
|
+
held.forEach((socket) => socket.destroy());
|
|
544
|
+
await new Promise((resolve) => silent.close(() => resolve()));
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
for (const operation of [Operation.TruncatePartition, Operation.DeleteSegments]) {
|
|
548
|
+
it(`raises the attachment floor for a DeleteSegments acknowledgement with operation ${operation}`, async () => {
|
|
549
|
+
const cluster = await startPollCluster(undefined, (frame, socket) => {
|
|
550
|
+
if (frame[REQUEST_OFFSET.operation] !== Operation.DeleteSegments)
|
|
551
|
+
return false;
|
|
552
|
+
const response = replyFrame(operation, operation === Operation.TruncatePartition ? Buffer.alloc(4) : Buffer.alloc(0));
|
|
553
|
+
response.writeBigUInt64LE(50n, REPLY_OFFSET.commit);
|
|
554
|
+
socket.write(response);
|
|
555
|
+
return true;
|
|
556
|
+
});
|
|
557
|
+
try {
|
|
558
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
559
|
+
await cluster.client.sendCommand(COMMAND_CODE.DeleteSegments, Buffer.alloc(0));
|
|
560
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
561
|
+
const attachments = cluster.primary.frames.filter((frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.AttachConsumerSession);
|
|
562
|
+
assert.deepEqual(attachments.map((frame) => frame.readBigUInt64LE(HEADER_SIZE + 24)), [1n, 50n]);
|
|
563
|
+
assert.equal(cluster.primary.frames.filter((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register).length, 1);
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
await cluster.close();
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
it('rechecks metadata after waiting behind another primary poll', async () => {
|
|
571
|
+
let firstPoll;
|
|
572
|
+
const admitted = new Promise((resolve) => { firstPoll = resolve; });
|
|
573
|
+
let polls = 0;
|
|
574
|
+
const cluster = await startPollCluster((frame, socket) => {
|
|
575
|
+
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.PollMessagesOnPrimary || ++polls !== 1)
|
|
576
|
+
return false;
|
|
577
|
+
firstPoll(socket);
|
|
578
|
+
return true;
|
|
579
|
+
}, (frame, socket) => {
|
|
580
|
+
if (frame[REQUEST_OFFSET.operation] !== Operation.UpdateTopic)
|
|
581
|
+
return false;
|
|
582
|
+
const response = replyFrame(Operation.UpdateTopic, Buffer.alloc(4));
|
|
583
|
+
response.writeBigUInt64LE(21n, REPLY_OFFSET.commit);
|
|
584
|
+
socket.write(response);
|
|
585
|
+
return true;
|
|
586
|
+
});
|
|
587
|
+
try {
|
|
588
|
+
const first = cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
589
|
+
const socket = await admitted;
|
|
590
|
+
const second = cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
591
|
+
await cluster.client.sendCommand(COMMAND_CODE.UpdateTopic, Buffer.alloc(0));
|
|
592
|
+
assert.equal(polls, 1, 'concurrent polls must share a serialized primary connection');
|
|
593
|
+
socket.write(replyFrame(Operation.NonReplicated, Buffer.from('primary')));
|
|
594
|
+
await Promise.all([first, second]);
|
|
595
|
+
assert.equal(polls, 2);
|
|
596
|
+
const attachments = cluster.primary.frames.filter((frame) => frame.readUInt32LE(REQUEST_OFFSET.reserved) === COMMAND_CODE.AttachConsumerSession);
|
|
597
|
+
assert.deepEqual(attachments.map((frame) => frame.readBigUInt64LE(HEADER_SIZE + 24)), [1n, 21n]);
|
|
598
|
+
}
|
|
599
|
+
finally {
|
|
600
|
+
await cluster.close();
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
for (const failure of ['disconnect', 'not committed', 'timeout', 'partial reply', 'stale session', 'unauthenticated']) {
|
|
604
|
+
it(`does not replay an uncertain primary poll after ${failure}`, async () => {
|
|
605
|
+
let polls = 0;
|
|
606
|
+
const cluster = await startPollCluster((frame, socket) => {
|
|
607
|
+
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.PollMessagesOnPrimary)
|
|
608
|
+
return false;
|
|
609
|
+
polls += 1;
|
|
610
|
+
if (polls > 1)
|
|
611
|
+
return false;
|
|
612
|
+
if (failure === 'disconnect')
|
|
613
|
+
socket.destroy();
|
|
614
|
+
if (failure === 'not committed')
|
|
615
|
+
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 57));
|
|
616
|
+
if (failure === 'stale session')
|
|
617
|
+
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 30));
|
|
618
|
+
if (failure === 'unauthenticated')
|
|
619
|
+
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), 40));
|
|
620
|
+
if (failure === 'partial reply')
|
|
621
|
+
socket.write(replyFrame(Operation.NonReplicated, Buffer.from('partial')).subarray(0, HEADER_SIZE + 1));
|
|
622
|
+
return true;
|
|
623
|
+
});
|
|
624
|
+
let resets = 0;
|
|
625
|
+
cluster.client.on('sessionReset', () => { resets += 1; });
|
|
626
|
+
try {
|
|
627
|
+
await assert.rejects(cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload(), { deadline: Date.now() + 300 }), (error) => error instanceof ResponseError && error.errorCode === 57);
|
|
628
|
+
assert.equal(polls, 1);
|
|
629
|
+
assert.equal(resets, 0);
|
|
630
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
631
|
+
assert.equal(polls, 2);
|
|
632
|
+
assert.equal(countCommand(cluster.primary, COMMAND_CODE.AttachConsumerSession), 2);
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
await cluster.close();
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
for (const status of [30, 40]) {
|
|
640
|
+
it(`rebuilds an auxiliary rejected with session status ${status} before poll admission`, async () => {
|
|
641
|
+
let attachments = 0;
|
|
642
|
+
const cluster = await startPollCluster((frame, socket) => {
|
|
643
|
+
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.AttachConsumerSession || ++attachments > 1)
|
|
644
|
+
return false;
|
|
645
|
+
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), status));
|
|
646
|
+
return true;
|
|
647
|
+
});
|
|
648
|
+
let resets = 0;
|
|
649
|
+
cluster.client.on('sessionReset', () => { resets += 1; });
|
|
650
|
+
try {
|
|
651
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
652
|
+
assert.equal(attachments, 2);
|
|
653
|
+
assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessagesOnPrimary), 1);
|
|
654
|
+
assert.equal(cluster.primary.frames.filter((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register).length, 2);
|
|
655
|
+
assert.equal(resets, 0);
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
await cluster.close();
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
it(`replaces the coordinator socket after a control session status ${status}`, async () => {
|
|
662
|
+
const registered = new Set();
|
|
663
|
+
let routes = 0;
|
|
664
|
+
const cluster = await startPollCluster(undefined, (frame, socket) => {
|
|
665
|
+
if (frame[REQUEST_OFFSET.operation] === Operation.Register) {
|
|
666
|
+
assert.equal(registered.has(socket), false, 'a fresh VSR identity cannot register on the old bound transport');
|
|
667
|
+
registered.add(socket);
|
|
668
|
+
}
|
|
669
|
+
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.GetPollRouting || ++routes > 1)
|
|
670
|
+
return false;
|
|
671
|
+
socket.write(replyFrame(Operation.NonReplicated, Buffer.alloc(0), status));
|
|
672
|
+
return true;
|
|
673
|
+
});
|
|
674
|
+
try {
|
|
675
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
676
|
+
assert.equal(registered.size, 2);
|
|
677
|
+
assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessagesOnPrimary), 1);
|
|
678
|
+
}
|
|
679
|
+
finally {
|
|
680
|
+
await cluster.close();
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
it('uses manual login and updated credentials for new primary connections', async () => {
|
|
685
|
+
const cluster = await startPollCluster(undefined, (frame, socket) => {
|
|
686
|
+
const operation = frame[REQUEST_OFFSET.operation];
|
|
687
|
+
if (operation !== Operation.UpdateUser && operation !== Operation.ChangePassword)
|
|
688
|
+
return false;
|
|
689
|
+
socket.write(replyFrame(operation, Buffer.alloc(4)));
|
|
690
|
+
return true;
|
|
691
|
+
});
|
|
692
|
+
try {
|
|
693
|
+
await cluster.client.sendCommand(LOGIN.code, LOGIN.serialize({ username: 'manual', password: 'first' }));
|
|
694
|
+
await cluster.client.sendCommand(UPDATE_USER.code, UPDATE_USER.serialize({ userId: 7, username: 'renamed' }));
|
|
695
|
+
await cluster.client.sendCommand(CHANGE_PASSWORD.code, CHANGE_PASSWORD.serialize({
|
|
696
|
+
userId: 7, currentPassword: 'first', newPassword: 'second'
|
|
697
|
+
}));
|
|
698
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
699
|
+
const login = cluster.primary.frames.find((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register);
|
|
700
|
+
const credentials = registerCredentialsBody(login);
|
|
701
|
+
const username = readWireName(credentials, 0);
|
|
702
|
+
const password = readWireName(credentials, username.next);
|
|
703
|
+
assert.equal(username.value, 'renamed');
|
|
704
|
+
assert.equal(password.value, 'second');
|
|
705
|
+
await cluster.client.sendCommand(LOGOUT.code, LOGOUT.serialize());
|
|
706
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
707
|
+
const replacement = cluster.primary.frames.filter((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register).at(-1);
|
|
708
|
+
assert.equal(readWireName(registerCredentialsBody(replacement), 0).value, 'iggy');
|
|
709
|
+
}
|
|
710
|
+
finally {
|
|
711
|
+
await cluster.close();
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
it('authenticates auxiliary connections with the active personal access token', async () => {
|
|
715
|
+
const cluster = await startPollCluster();
|
|
716
|
+
try {
|
|
717
|
+
await cluster.client.sendCommand(LOGIN_WITH_TOKEN.code, LOGIN_WITH_TOKEN.serialize({ token: 'manual-token' }));
|
|
718
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
719
|
+
const login = cluster.primary.frames.find((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register);
|
|
720
|
+
const sdkName = readWireName(login, HEADER_SIZE + 4);
|
|
721
|
+
const sdkVersion = readWireName(login, sdkName.next);
|
|
722
|
+
assert.equal(readWireName(login, sdkVersion.next).value, 'manual-token');
|
|
723
|
+
}
|
|
724
|
+
finally {
|
|
725
|
+
await cluster.close();
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
for (const manual of [
|
|
729
|
+
{ command: LOGIN.code, payload: LOGIN.serialize({ username: 'manual', password: 'first' }), identity: 'manual' },
|
|
730
|
+
{ command: LOGIN_WITH_TOKEN.code, payload: LOGIN_WITH_TOKEN.serialize({ token: 'manual-token' }), identity: 'manual-token' }
|
|
731
|
+
]) {
|
|
732
|
+
it(`retains ${manual.identity} across recovery and restores configured credentials after logout`, async () => {
|
|
733
|
+
let coordinatorSocket;
|
|
734
|
+
const cluster = await startPollCluster(undefined, (_frame, socket) => {
|
|
735
|
+
coordinatorSocket = socket;
|
|
736
|
+
return false;
|
|
737
|
+
});
|
|
738
|
+
try {
|
|
739
|
+
await cluster.client.sendCommand(manual.command, manual.payload);
|
|
740
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
741
|
+
const reset = once(cluster.client, 'sessionReset');
|
|
742
|
+
coordinatorSocket.destroy();
|
|
743
|
+
await reset;
|
|
744
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
745
|
+
for (const server of [cluster.coordinator, cluster.primary]) {
|
|
746
|
+
const login = server.frames.filter((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register).at(-1);
|
|
747
|
+
assert.equal(readWireName(registerCredentialsBody(login), 0).value, manual.identity);
|
|
748
|
+
}
|
|
749
|
+
await cluster.client.sendCommand(LOGOUT.code, LOGOUT.serialize());
|
|
750
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
751
|
+
for (const server of [cluster.coordinator, cluster.primary]) {
|
|
752
|
+
const login = server.frames.filter((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register).at(-1);
|
|
753
|
+
const credentials = registerCredentialsBody(login);
|
|
754
|
+
const username = readWireName(credentials, 0);
|
|
755
|
+
assert.equal(username.value, 'iggy');
|
|
756
|
+
assert.equal(readWireName(credentials, username.next).value, 'iggy');
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
finally {
|
|
760
|
+
await cluster.close();
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
it('updates the configured identity after its password and username change', async () => {
|
|
765
|
+
const cluster = await startPollCluster(undefined, (frame, socket) => {
|
|
766
|
+
const operation = frame[REQUEST_OFFSET.operation];
|
|
767
|
+
if (operation !== Operation.UpdateUser && operation !== Operation.ChangePassword)
|
|
768
|
+
return false;
|
|
769
|
+
socket.write(replyFrame(operation, Buffer.alloc(4)));
|
|
770
|
+
return true;
|
|
771
|
+
});
|
|
772
|
+
try {
|
|
773
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
774
|
+
await cluster.client.sendCommand(UPDATE_USER.code, UPDATE_USER.serialize({ userId: 7, username: 'renamed' }));
|
|
775
|
+
await cluster.client.sendCommand(CHANGE_PASSWORD.code, CHANGE_PASSWORD.serialize({
|
|
776
|
+
userId: 7, currentPassword: 'iggy', newPassword: 'second'
|
|
777
|
+
}));
|
|
778
|
+
await cluster.client.sendCommand(LOGOUT.code, LOGOUT.serialize());
|
|
779
|
+
await cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
780
|
+
for (const server of [cluster.coordinator, cluster.primary]) {
|
|
781
|
+
const login = server.frames.filter((frame) => frame[REQUEST_OFFSET.operation] === Operation.Register).at(-1);
|
|
782
|
+
const credentials = registerCredentialsBody(login);
|
|
783
|
+
const username = readWireName(credentials, 0);
|
|
784
|
+
assert.equal(username.value, 'renamed');
|
|
785
|
+
assert.equal(readWireName(credentials, username.next).value, 'second');
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
finally {
|
|
789
|
+
await cluster.close();
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
it('closes a waiting primary exchange on coordinator logout without replay', async () => {
|
|
793
|
+
let admitted;
|
|
794
|
+
const pendingAdmission = new Promise((resolve) => { admitted = resolve; });
|
|
795
|
+
const cluster = await startPollCluster((frame) => {
|
|
796
|
+
if (frame.readUInt32LE(REQUEST_OFFSET.reserved) !== COMMAND_CODE.PollMessagesOnPrimary)
|
|
797
|
+
return false;
|
|
798
|
+
admitted();
|
|
799
|
+
return true;
|
|
800
|
+
});
|
|
801
|
+
try {
|
|
802
|
+
const polling = cluster.client.sendCommand(POLL_MESSAGES.code, groupPollPayload());
|
|
803
|
+
const failed = assert.rejects(polling, (error) => error instanceof ResponseError && error.errorCode === 57);
|
|
804
|
+
await pendingAdmission;
|
|
805
|
+
await cluster.client.sendCommand(LOGOUT.code, LOGOUT.serialize());
|
|
806
|
+
await failed;
|
|
807
|
+
assert.equal(countCommand(cluster.primary, COMMAND_CODE.PollMessagesOnPrimary), 1);
|
|
808
|
+
assert.equal(cluster.client.isAuthenticated, false);
|
|
809
|
+
}
|
|
810
|
+
finally {
|
|
811
|
+
await cluster.close();
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
});
|
|
211
815
|
describe('VSR client socket', () => {
|
|
212
816
|
it('exchanges VSR frames over TLS', async () => {
|
|
213
817
|
const server = await startVsrServer((frame, socket) => singleNodeHandler(server.port)(frame, socket), 'TLS');
|
|
@@ -4,6 +4,7 @@ export declare const COMMAND_CODE: {
|
|
|
4
4
|
GetSnapshot: number;
|
|
5
5
|
GetClusterMetadata: number;
|
|
6
6
|
DescribeOptions: number;
|
|
7
|
+
AttachConsumerSession: number;
|
|
7
8
|
GetMe: number;
|
|
8
9
|
GetClient: number;
|
|
9
10
|
GetClients: number;
|
|
@@ -25,9 +26,12 @@ export declare const COMMAND_CODE: {
|
|
|
25
26
|
PollMessages: number;
|
|
26
27
|
SendMessages: number;
|
|
27
28
|
FlushUnsavedBuffers: number;
|
|
29
|
+
GetPollRouting: number;
|
|
30
|
+
PollMessagesOnPrimary: number;
|
|
28
31
|
GetOffset: number;
|
|
29
32
|
StoreOffset: number;
|
|
30
33
|
DeleteConsumerOffset: number;
|
|
34
|
+
GetConsumerOffsetRouting: number;
|
|
31
35
|
GetStream: number;
|
|
32
36
|
GetStreams: number;
|
|
33
37
|
CreateStream: number;
|
|
@@ -21,6 +21,7 @@ export const COMMAND_CODE = {
|
|
|
21
21
|
GetSnapshot: 11, // @TODO GET_SNAPSHOT_FILE_CODE: u32 = 11
|
|
22
22
|
GetClusterMetadata: 12,
|
|
23
23
|
DescribeOptions: 13,
|
|
24
|
+
AttachConsumerSession: 14,
|
|
24
25
|
GetMe: 20,
|
|
25
26
|
GetClient: 21,
|
|
26
27
|
GetClients: 22,
|
|
@@ -42,9 +43,12 @@ export const COMMAND_CODE = {
|
|
|
42
43
|
PollMessages: 100,
|
|
43
44
|
SendMessages: 101,
|
|
44
45
|
FlushUnsavedBuffers: 102,
|
|
46
|
+
GetPollRouting: 103,
|
|
47
|
+
PollMessagesOnPrimary: 104,
|
|
45
48
|
GetOffset: 120,
|
|
46
49
|
StoreOffset: 121,
|
|
47
50
|
DeleteConsumerOffset: 122,
|
|
51
|
+
GetConsumerOffsetRouting: 123,
|
|
48
52
|
GetStream: 200,
|
|
49
53
|
GetStreams: 201,
|
|
50
54
|
CreateStream: 202,
|
|
@@ -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 | 13 | 3 | 4 | 5 | 8 | 6 | 7 | 9 |
|
|
30
|
+
export declare const ReverseHeaderKind: Record<2 | 1 | 10 | 11 | 12 | 13 | 14 | 3 | 4 | 5 | 8 | 6 | 7 | 9 | 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 */
|