apache-iggy 0.10.0 → 0.10.1-edge.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +248 -28
  2. package/dist/client/client.config.d.ts +2 -0
  3. package/dist/client/client.config.js +13 -2
  4. package/dist/client/client.config.test.js +15 -2
  5. package/dist/client/client.connection-string.js +13 -1
  6. package/dist/client/client.connection-string.test.js +43 -1
  7. package/dist/client/client.connection.d.ts +33 -135
  8. package/dist/client/client.connection.js +190 -453
  9. package/dist/client/client.connection.test.js +244 -669
  10. package/dist/client/client.d.ts +4 -35
  11. package/dist/client/client.js +8 -101
  12. package/dist/client/client.operation.d.ts +120 -0
  13. package/dist/client/client.operation.js +428 -0
  14. package/dist/client/client.operation.test.d.ts +2 -0
  15. package/dist/client/client.operation.test.js +285 -0
  16. package/dist/client/client.socket.d.ts +92 -209
  17. package/dist/client/client.socket.js +1205 -981
  18. package/dist/client/client.socket.test.js +4865 -679
  19. package/dist/client/client.type.d.ts +65 -22
  20. package/dist/client/index.d.ts +1 -0
  21. package/dist/client/index.js +1 -0
  22. package/dist/client.e2e.js +3 -5
  23. package/dist/e2e/resource-options.e2e.d.ts +2 -0
  24. package/dist/{wire/stream/purge-stream.command.js → e2e/resource-options.e2e.js} +12 -20
  25. package/dist/e2e/resource-options.suite.d.ts +15 -0
  26. package/dist/e2e/resource-options.suite.js +272 -0
  27. package/dist/e2e/tcp.cluster.e2e.js +1 -0
  28. package/dist/e2e/tcp.consumer-stream.e2e.js +4 -0
  29. package/dist/e2e/tcp.parallel.e2e.js +5 -6
  30. package/dist/e2e/tcp.stream.e2e.js +0 -3
  31. package/dist/e2e/tcp.system.e2e.js +10 -1
  32. package/dist/e2e/tcp.token.e2e.js +8 -2
  33. package/dist/e2e/tcp.topic.e2e.js +0 -3
  34. package/dist/e2e/tcp.user.e2e.js +46 -3
  35. package/dist/e2e/tls.system.e2e.js +21 -12
  36. package/dist/index.d.ts +2 -1
  37. package/dist/index.js +1 -1
  38. package/dist/stream/consumer-stream.js +97 -12
  39. package/dist/wire/cluster/cluster.type.d.ts +2 -2
  40. package/dist/wire/cluster/cluster.utils.d.ts +1 -0
  41. package/dist/wire/cluster/cluster.utils.js +4 -1
  42. package/dist/wire/command-set.d.ts +8 -8
  43. package/dist/wire/command-set.js +18 -9
  44. package/dist/wire/command.code.d.ts +3 -10
  45. package/dist/wire/command.code.js +9 -11
  46. package/dist/wire/command.utils.d.ts +3 -2
  47. package/dist/wire/command.utils.js +3 -2
  48. package/dist/wire/error.code.js +17 -1
  49. package/dist/wire/error.code.test.js +28 -1
  50. package/dist/wire/error.utils.d.ts +4 -2
  51. package/dist/wire/error.utils.js +6 -3
  52. package/dist/wire/identifier.utils.d.ts +2 -0
  53. package/dist/wire/identifier.utils.js +2 -0
  54. package/dist/wire/index.d.ts +1 -0
  55. package/dist/wire/index.js +1 -0
  56. package/dist/wire/message/header.type.d.ts +1 -1
  57. package/dist/wire/message/message-batch.test.js +11 -5
  58. package/dist/wire/message/message.utils.d.ts +0 -10
  59. package/dist/wire/message/message.utils.js +1 -22
  60. package/dist/wire/message/poll-messages.command.d.ts +12 -1
  61. package/dist/wire/message/poll-messages.command.js +64 -16
  62. package/dist/wire/message/poll-messages.command.test.js +143 -5
  63. package/dist/wire/message/poll.utils.d.ts +4 -0
  64. package/dist/wire/message/poll.utils.js +6 -1
  65. package/dist/wire/message/send-messages.command.d.ts +3 -3
  66. package/dist/wire/message/send-messages.command.js +103 -3
  67. package/dist/wire/message/send-messages.command.test.js +113 -2
  68. package/dist/wire/offset/delete-offset.command.d.ts +11 -4
  69. package/dist/wire/offset/delete-offset.command.js +1 -1
  70. package/dist/wire/offset/get-offset.command.d.ts +1 -1
  71. package/dist/wire/offset/offset-write.command.test.d.ts +2 -0
  72. package/dist/wire/offset/offset-write.command.test.js +43 -0
  73. package/dist/wire/offset/offset.utils.js +10 -6
  74. package/dist/wire/offset/store-offset.command.d.ts +10 -0
  75. package/dist/wire/offset/store-offset.command.js +1 -1
  76. package/dist/wire/options.utils.d.ts +46 -0
  77. package/dist/wire/options.utils.js +57 -0
  78. package/dist/wire/partition/partition-route.d.ts +12 -0
  79. package/dist/wire/partition/partition-route.js +40 -0
  80. package/dist/wire/partition/partition-route.test.d.ts +2 -0
  81. package/dist/wire/partition/partition-route.test.js +42 -0
  82. package/dist/wire/session/login-with-token.command.d.ts +2 -2
  83. package/dist/wire/session/login-with-token.command.js +7 -2
  84. package/dist/wire/session/login.command.d.ts +2 -2
  85. package/dist/wire/session/login.command.js +7 -2
  86. package/dist/wire/stream/create-stream.command.d.ts +5 -2
  87. package/dist/wire/stream/create-stream.command.js +20 -2
  88. package/dist/wire/stream/index.d.ts +1 -1
  89. package/dist/wire/stream/index.js +1 -1
  90. package/dist/wire/stream/stream-options.d.ts +77 -0
  91. package/dist/wire/stream/stream-options.js +101 -0
  92. package/dist/wire/stream/stream-options.test.d.ts +2 -0
  93. package/dist/wire/stream/stream-options.test.js +103 -0
  94. package/dist/wire/stream/stream.utils.d.ts +9 -0
  95. package/dist/wire/stream/stream.utils.js +17 -3
  96. package/dist/wire/stream/update-stream.command.d.ts +23 -5
  97. package/dist/wire/stream/update-stream.command.js +10 -17
  98. package/dist/wire/system/describe-options.command.d.ts +23 -6
  99. package/dist/wire/system/describe-options.command.js +21 -2
  100. package/dist/wire/system/get-stats.command.d.ts +17 -0
  101. package/dist/wire/system/get-stats.command.js +69 -16
  102. package/dist/wire/system/get-stats.command.test.d.ts +2 -0
  103. package/dist/wire/system/get-stats.command.test.js +97 -0
  104. package/dist/wire/system/index.d.ts +2 -0
  105. package/dist/wire/system/index.js +2 -0
  106. package/dist/wire/system/snapshot.command.d.ts +64 -0
  107. package/dist/wire/system/snapshot.command.js +76 -0
  108. package/dist/wire/system/snapshot.command.test.d.ts +2 -0
  109. package/dist/wire/system/snapshot.command.test.js +86 -0
  110. package/dist/wire/system/update-resource.command.d.ts +53 -0
  111. package/dist/wire/system/update-resource.command.js +130 -0
  112. package/dist/wire/system/update-resource.command.test.d.ts +2 -0
  113. package/dist/wire/system/update-resource.command.test.js +497 -0
  114. package/dist/wire/token/create-token.command.d.ts +1 -1
  115. package/dist/wire/token/create-token.command.js +1 -1
  116. package/dist/wire/token/create-token.command.test.js +6 -0
  117. package/dist/wire/topic/create-topic.command.d.ts +20 -12
  118. package/dist/wire/topic/create-topic.command.js +11 -4
  119. package/dist/wire/topic/index.d.ts +1 -2
  120. package/dist/wire/topic/index.js +1 -2
  121. package/dist/wire/topic/topic.utils.d.ts +14 -5
  122. package/dist/wire/topic/topic.utils.js +26 -11
  123. package/dist/wire/topic/topic.utils.test.js +13 -13
  124. package/dist/wire/topic/update-topic.command.d.ts +22 -15
  125. package/dist/wire/topic/update-topic.command.js +12 -34
  126. package/dist/wire/user/create-user.command.d.ts +12 -2
  127. package/dist/wire/user/create-user.command.js +7 -3
  128. package/dist/wire/user/index.d.ts +1 -0
  129. package/dist/wire/user/index.js +1 -0
  130. package/dist/wire/user/update-user.command.d.ts +25 -7
  131. package/dist/wire/user/update-user.command.js +18 -31
  132. package/dist/wire/user/user.utils.d.ts +2 -0
  133. package/dist/wire/user/user.utils.js +5 -3
  134. package/dist/wire/vsr/header.d.ts +23 -8
  135. package/dist/wire/vsr/header.js +32 -10
  136. package/dist/wire/vsr/header.test.js +5 -1
  137. package/dist/wire/vsr/index.d.ts +8 -2
  138. package/dist/wire/vsr/index.js +44 -11
  139. package/dist/wire/vsr/operation.d.ts +18 -6
  140. package/dist/wire/vsr/operation.js +46 -16
  141. package/dist/wire/vsr/operation.test.js +32 -8
  142. package/dist/wire/vsr/register.d.ts +10 -6
  143. package/dist/wire/vsr/register.js +24 -7
  144. package/dist/wire/vsr/register.test.js +28 -8
  145. package/dist/wire/vsr/reply.d.ts +4 -1
  146. package/dist/wire/vsr/reply.js +42 -7
  147. package/dist/wire/vsr/reply.test.js +11 -1
  148. package/dist/wire/vsr/session.d.ts +9 -10
  149. package/dist/wire/vsr/session.js +32 -23
  150. package/dist/wire/vsr/session.test.js +33 -7
  151. package/dist/wire/vsr/vsr.test.js +57 -6
  152. package/package.json +6 -7
  153. package/dist/wire/message/flush-unsaved-buffers.command.d.ts +0 -26
  154. package/dist/wire/message/flush-unsaved-buffers.command.js +0 -36
  155. package/dist/wire/stream/purge-stream.command.d.ts +0 -22
  156. package/dist/wire/topic/purge-topic.command.d.ts +0 -24
  157. package/dist/wire/topic/purge-topic.command.js +0 -39
@@ -36,20 +36,33 @@ export const REQUEST_OFFSET = {
36
36
  size: 48,
37
37
  command: 60,
38
38
  client: 128,
39
+ requestChecksum: 144,
39
40
  timestamp: 160,
40
41
  request: 168,
41
42
  operation: 176,
42
43
  session: 184,
43
- reserved: 196
44
+ reserved: 196,
45
+ partitionIncarnation: 200,
46
+ ownerGeneration: 208,
47
+ minimumMetadataOp: 216
44
48
  };
45
49
  /** `ReplyHeader` field offsets the client reads. */
46
50
  export const REPLY_OFFSET = {
47
51
  size: 48,
48
52
  command: 60,
53
+ requestChecksum: 128,
54
+ client: 160,
55
+ op: 176,
49
56
  commit: 184,
57
+ request: 200,
50
58
  operation: 208,
51
- status: 216
59
+ status: 216,
60
+ reserved: 220,
61
+ partitionIncarnation: 224
52
62
  };
63
+ export const COMMITTED_RESULT_MARKER = 1;
64
+ export const hasCommittedResult = (header) => peekCommand(header) === Command.Reply && readStatus(header) === 0 &&
65
+ header[REPLY_OFFSET.reserved] === COMMITTED_RESULT_MARKER;
53
66
  /** `EvictionHeader` field offsets the client reads. */
54
67
  export const EVICTION_OFFSET = {
55
68
  command: 60,
@@ -86,14 +99,19 @@ export const EvictionReason = {
86
99
  IncompatibleProtocol: 14,
87
100
  MalformedLogin: 15
88
101
  };
102
+ export const PARTITION_CONTEXT_SIZE = 24;
103
+ export const EMPTY_PARTITION_CONTEXT = Object.freeze({
104
+ incarnation: 0n,
105
+ ownerGeneration: 0n,
106
+ metadataOp: 0n
107
+ });
108
+ export const deserializePartitionContext = (data, offset = 0) => ({
109
+ incarnation: data.readBigUInt64LE(offset),
110
+ ownerGeneration: data.readBigUInt64LE(offset + 8),
111
+ metadataOp: data.readBigUInt64LE(offset + 16)
112
+ });
89
113
  const U64_MASK = 0xffffffffffffffffn;
90
- /**
91
- * Encodes a 256-byte request header. Only the six fields the server reads
92
- * are written. The checksums stay zero: the frame and body checksums are not
93
- * read on the client request path, and `request_checksum` treats zero as
94
- * unstamped, which opts out of the server's payload comparison. Stamping it is
95
- * optional -- the Rust SDK does for deduped ops, this SDK does not yet.
96
- */
114
+ /** Encodes one request identity and its captured partition context. */
97
115
  export const encodeRequestHeader = (fields) => {
98
116
  const header = Buffer.alloc(HEADER_SIZE);
99
117
  header.writeUInt32LE(fields.size, REQUEST_OFFSET.size);
@@ -104,6 +122,10 @@ export const encodeRequestHeader = (fields) => {
104
122
  header.writeBigUInt64LE(fields.request, REQUEST_OFFSET.request);
105
123
  header.writeUInt8(fields.operation, REQUEST_OFFSET.operation);
106
124
  header.writeBigUInt64LE(fields.session, REQUEST_OFFSET.session);
125
+ const context = fields.context ?? EMPTY_PARTITION_CONTEXT;
126
+ header.writeBigUInt64LE(context.incarnation, REQUEST_OFFSET.partitionIncarnation);
127
+ header.writeBigUInt64LE(context.ownerGeneration, REQUEST_OFFSET.ownerGeneration);
128
+ header.writeBigUInt64LE(context.metadataOp, REQUEST_OFFSET.minimumMetadataOp);
107
129
  if (fields.nonReplicatedCode !== undefined)
108
130
  header.writeUInt32LE(fields.nonReplicatedCode, REQUEST_OFFSET.reserved);
109
131
  return header;
@@ -115,7 +137,7 @@ export const encodeRequestHeader = (fields) => {
115
137
  export const peekCommand = (header) => header.readUInt8(REPLY_OFFSET.command);
116
138
  /** Reads the total frame size from a reply or eviction header. */
117
139
  export const readSize = (header) => header.readUInt32LE(REPLY_OFFSET.size);
118
- /** Reads the pre-commit denial status; 0 means the reply committed. */
140
+ /** Reads the pre-commit denial status; zero alone does not prove commit. */
119
141
  export const readStatus = (header) => header.readUInt32LE(REPLY_OFFSET.status);
120
142
  /** Reads the `Operation` discriminant from a reply header. */
121
143
  export const readReplyOperation = (header) => header.readUInt8(REPLY_OFFSET.operation);
@@ -26,7 +26,8 @@ describe('VSR request header', () => {
26
26
  request: 0x0102030405060708n,
27
27
  operation: 2,
28
28
  session: 0x1020304050607080n,
29
- nonReplicatedCode: 60_001
29
+ nonReplicatedCode: 60_001,
30
+ context: { incarnation: 17n, ownerGeneration: 9n, metadataOp: 52n }
30
31
  });
31
32
  assert.equal(header.length, HEADER_SIZE);
32
33
  assert.equal(header.readUInt32LE(REQUEST_OFFSET.size), HEADER_SIZE + 3);
@@ -37,6 +38,9 @@ describe('VSR request header', () => {
37
38
  assert.equal(header.readUInt8(REQUEST_OFFSET.operation), 2);
38
39
  assert.equal(header.readBigUInt64LE(REQUEST_OFFSET.session), 0x1020304050607080n);
39
40
  assert.equal(header.readUInt32LE(REQUEST_OFFSET.reserved), 60_001);
41
+ assert.equal(header.readBigUInt64LE(REQUEST_OFFSET.partitionIncarnation), 17n);
42
+ assert.equal(header.readBigUInt64LE(REQUEST_OFFSET.ownerGeneration), 9n);
43
+ assert.equal(header.readBigUInt64LE(REQUEST_OFFSET.minimumMetadataOp), 52n);
40
44
  assert.equal(header.readBigUInt64LE(REQUEST_OFFSET.timestamp), 0n);
41
45
  });
42
46
  it('leaves all unspecified and reserved bytes zero', () => {
@@ -1,15 +1,21 @@
1
1
  import type { CommandResponse } from '../../client/client.type.js';
2
+ import { type PartitionContext } from './header.js';
2
3
  export declare class VsrSession {
3
4
  private state;
4
5
  constructor(clientId?: bigint);
5
6
  reset(): void;
6
7
  bind(session: bigint): void;
7
8
  get hasActivity(): boolean;
8
- encode(command: number, payload: Buffer): Buffer;
9
+ get sessionId(): bigint | null;
10
+ get clientId(): bigint;
11
+ get bindSecret(): Buffer;
12
+ bindPayload(identity: Buffer): Buffer;
13
+ resumePayload(metadataWatermark: bigint): Buffer | undefined;
14
+ encode(command: number, payload: Buffer, context?: PartitionContext): Buffer;
9
15
  }
10
16
  export declare const decodeVsrResponse: (frame: Buffer, commandCode?: number) => CommandResponse;
11
17
  export declare const readRegisteredSession: (response: CommandResponse) => bigint;
12
- export declare const prepareVsrCommand: (command: number, payload: Buffer) => {
18
+ export declare const prepareVsrCommand: (command: number, payload: Buffer, bindSecret: Buffer) => {
13
19
  command: number;
14
20
  payload: Buffer;
15
21
  };
@@ -15,16 +15,18 @@
15
15
  // specific language governing permissions and limitations
16
16
  // under the License.
17
17
  import { createRequire } from 'node:module';
18
+ import { randomFillSync } from 'node:crypto';
18
19
  import { COMMAND_CODE } from '../command.code.js';
19
20
  import { responseError } from '../error.utils.js';
20
- import { HEADER_SIZE, encodeRequestHeader } from './header.js';
21
+ import { HEADER_SIZE, REQUEST_OFFSET, encodeRequestHeader } from './header.js';
21
22
  import { Operation, operationForCode } from './operation.js';
22
- import { deserializeLoginRegister, serializeLoginRegister, serializeLoginRegisterWithPat, } from './register.js';
23
+ import { deserializeLoginRegister, serializeBindSession, serializeLoginRegister, serializeLoginRegisterWithPat, SESSION_IDENTITY_BYTES, } from './register.js';
23
24
  import { decodeResponse } from './reply.js';
24
25
  import { ConsensusSession } from './session.js';
25
26
  const packageMetadata = createRequire(import.meta.url)('../../../package.json');
26
27
  const SDK_VERSION = packageMetadata.version;
27
28
  const MAX_U32 = 0xFFFF_FFFF;
29
+ const SESSION_IDENTITY_OFFSET = { clientLow: 0, clientHigh: 8, session: 16, metadataWatermark: 24 };
28
30
  /** `IggyError::Unauthenticated`, matching the Rust SDK's unbound-session error. */
29
31
  const UNAUTHENTICATED = 40;
30
32
  export class VsrSession {
@@ -33,7 +35,7 @@ export class VsrSession {
33
35
  this.state = new ConsensusSession(clientId);
34
36
  }
35
37
  reset() {
36
- this.state = new ConsensusSession();
38
+ this.state.reset();
37
39
  }
38
40
  bind(session) {
39
41
  this.state.bind(session);
@@ -41,7 +43,29 @@ export class VsrSession {
41
43
  get hasActivity() {
42
44
  return this.state.hasActivity;
43
45
  }
44
- encode(command, payload) {
46
+ get sessionId() {
47
+ return this.state.session;
48
+ }
49
+ get clientId() {
50
+ return this.state.clientId;
51
+ }
52
+ get bindSecret() {
53
+ return this.state.bindSecret;
54
+ }
55
+ bindPayload(identity) {
56
+ return serializeBindSession(identity, SDK_VERSION, this.state.bindSecret);
57
+ }
58
+ resumePayload(metadataWatermark) {
59
+ if (this.state.session === null)
60
+ return undefined;
61
+ const identity = Buffer.alloc(SESSION_IDENTITY_BYTES);
62
+ identity.writeBigUInt64LE(BigInt.asUintN(64, this.state.clientId), SESSION_IDENTITY_OFFSET.clientLow);
63
+ identity.writeBigUInt64LE(this.state.clientId >> 64n, SESSION_IDENTITY_OFFSET.clientHigh);
64
+ identity.writeBigUInt64LE(this.state.session, SESSION_IDENTITY_OFFSET.session);
65
+ identity.writeBigUInt64LE(metadataWatermark, SESSION_IDENTITY_OFFSET.metadataWatermark);
66
+ return this.bindPayload(identity);
67
+ }
68
+ encode(command, payload, context) {
45
69
  const operation = registerCommand(command)
46
70
  ? Operation.Register
47
71
  : operationForCode(command);
@@ -55,26 +79,35 @@ export class VsrSession {
55
79
  session = 0n;
56
80
  }
57
81
  else if (operation === Operation.NonReplicated) {
58
- request = this.state.currentRequestId();
82
+ request = this.state.nextRequestId(false);
59
83
  session = this.state.session ?? 0n;
60
84
  }
61
85
  else {
62
86
  if (this.state.session === null)
63
87
  throw responseError(command, UNAUTHENTICATED);
64
- // Partition ops consume an id too, even though no partition-plane dedup
65
- // exists yet: dedup needs each send to carry a distinct number, and the
66
- // metadata watermark tolerates the gaps.
88
+ // Partition ops consume an id too: dedup needs each send to carry a
89
+ // distinct number, and the metadata watermark tolerates the gaps.
67
90
  request = this.state.nextRequestId();
68
91
  session = this.state.session;
69
92
  }
70
93
  const header = encodeRequestHeader({
71
94
  size,
95
+ context,
72
96
  client: this.state.clientId,
73
97
  request,
74
98
  operation,
75
99
  session,
76
100
  nonReplicatedCode: operation === Operation.NonReplicated ? command : undefined,
77
101
  });
102
+ if (operation === Operation.Register) {
103
+ this.state.registerRequestIdentifier.copy(header, REQUEST_OFFSET.requestChecksum);
104
+ }
105
+ else if (operation !== Operation.NonReplicated && operation < Operation.SendMessages) {
106
+ do {
107
+ randomFillSync(header, REQUEST_OFFSET.requestChecksum, 16);
108
+ } while (header.subarray(REQUEST_OFFSET.requestChecksum, REQUEST_OFFSET.requestChecksum + 16)
109
+ .every(byte => byte === 0));
110
+ }
78
111
  return payload.length === 0 ? header : Buffer.concat([header, payload]);
79
112
  }
80
113
  }
@@ -83,20 +116,20 @@ export const decodeVsrResponse = (frame, commandCode = 0) => {
83
116
  return { status: 0, length: data.length, data };
84
117
  };
85
118
  export const readRegisteredSession = (response) => deserializeLoginRegister(response.data).session;
86
- export const prepareVsrCommand = (command, payload) => {
119
+ export const prepareVsrCommand = (command, payload, bindSecret) => {
87
120
  if (command === COMMAND_CODE.LoginUser) {
88
121
  const username = readWireName(payload, 0);
89
122
  const password = readWireName(payload, username.next);
90
123
  return {
91
124
  command: COMMAND_CODE.LoginRegister,
92
- payload: serializeLoginRegister(username.value, password.value, SDK_VERSION),
125
+ payload: serializeLoginRegister(username.value, password.value, SDK_VERSION, bindSecret),
93
126
  };
94
127
  }
95
128
  if (command === COMMAND_CODE.LoginWithAccessToken) {
96
129
  const token = readWireName(payload, 0);
97
130
  return {
98
131
  command: COMMAND_CODE.LoginRegisterWithAccessToken,
99
- payload: serializeLoginRegisterWithPat(token.value, SDK_VERSION),
132
+ payload: serializeLoginRegisterWithPat(token.value, SDK_VERSION, bindSecret),
100
133
  };
101
134
  }
102
135
  return { command, payload };
@@ -9,21 +9,22 @@ export declare const Operation: {
9
9
  readonly RemoveConsumerGroupMember: 66;
10
10
  readonly CompleteConsumerGroupRevocation: 67;
11
11
  readonly TruncatePartition: 68;
12
+ readonly FinalizeSession: 70;
13
+ readonly CompleteLifecycle: 71;
14
+ readonly UpdateStreamResolved: 72;
15
+ readonly UpdateTopicResolved: 73;
16
+ readonly UpdateUserResolved: 74;
17
+ readonly RewrapStreamKeys: 75;
12
18
  readonly CreateStream: 128;
13
- readonly UpdateStream: 129;
14
19
  readonly DeleteStream: 130;
15
- readonly PurgeStream: 131;
16
20
  readonly CreateTopic: 132;
17
- readonly UpdateTopic: 133;
18
21
  readonly DeleteTopic: 134;
19
- readonly PurgeTopic: 135;
20
22
  readonly CreatePartitions: 136;
21
23
  readonly DeletePartitions: 137;
22
24
  readonly DeleteSegments: 138;
23
25
  readonly CreateConsumerGroup: 139;
24
26
  readonly DeleteConsumerGroup: 140;
25
27
  readonly CreateUser: 141;
26
- readonly UpdateUser: 142;
27
28
  readonly DeleteUser: 143;
28
29
  readonly ChangePassword: 144;
29
30
  readonly UpdatePermissions: 145;
@@ -31,9 +32,14 @@ export declare const Operation: {
31
32
  readonly DeletePersonalAccessToken: 147;
32
33
  readonly JoinConsumerGroup: 148;
33
34
  readonly LeaveConsumerGroup: 149;
35
+ readonly UpdateResource: 150;
34
36
  readonly SendMessages: 160;
35
37
  readonly StoreConsumerOffset: 161;
36
38
  readonly DeleteConsumerOffset: 162;
39
+ readonly RetireSession: 166;
40
+ readonly InstallConsumerGroupOwner: 167;
41
+ readonly TransitionPartitionHistory: 168;
42
+ readonly RetireConsumerGroupOwners: 169;
37
43
  };
38
44
  /**
39
45
  * Whether the byte is a declared `Operation` discriminant. Replies carry a
@@ -41,7 +47,7 @@ export declare const Operation: {
41
47
  * so an undeclared value must be rejected, not classified by range.
42
48
  */
43
49
  export declare const isKnownOperation: (operation: number) => boolean;
44
- /** Internal band, never client-sent. */
50
+ /** Replica-only operations, never client-sent. */
45
51
  export declare const isInternal: (operation: number) => boolean;
46
52
  /**
47
53
  * Metadata classification is an explicit allowlist, not a range:
@@ -50,6 +56,12 @@ export declare const isInternal: (operation: number) => boolean;
50
56
  * `Operation::is_metadata`.
51
57
  */
52
58
  export declare const isMetadata: (operation: number) => boolean;
59
+ /**
60
+ * The client operation a committed reply answers. Preparation rewrites some
61
+ * requests into an internal operation, so retry identity maps it back. Port of
62
+ * `Operation::request_operation`.
63
+ */
64
+ export declare const requestOperation: (operation: number) => number;
53
65
  /** Whether a reply body leads with a committed result section. */
54
66
  export declare const isResultFramed: (operation: number) => boolean;
55
67
  /**
@@ -31,21 +31,25 @@ export const Operation = {
31
31
  RemoveConsumerGroupMember: 66,
32
32
  CompleteConsumerGroupRevocation: 67,
33
33
  TruncatePartition: 68,
34
+ FinalizeSession: 70,
35
+ CompleteLifecycle: 71,
36
+ UpdateStreamResolved: 72,
37
+ UpdateTopicResolved: 73,
38
+ UpdateUserResolved: 74,
39
+ RewrapStreamKeys: 75,
34
40
  CreateStream: 128,
35
- UpdateStream: 129,
41
+ // 129 was UpdateStream. Never reuse it.
36
42
  DeleteStream: 130,
37
- PurgeStream: 131,
38
43
  CreateTopic: 132,
39
- UpdateTopic: 133,
44
+ // 133 was UpdateTopic. Never reuse it.
40
45
  DeleteTopic: 134,
41
- PurgeTopic: 135,
42
46
  CreatePartitions: 136,
43
47
  DeletePartitions: 137,
44
48
  DeleteSegments: 138,
45
49
  CreateConsumerGroup: 139,
46
50
  DeleteConsumerGroup: 140,
47
51
  CreateUser: 141,
48
- UpdateUser: 142,
52
+ // 142 was UpdateUser. Never reuse it.
49
53
  DeleteUser: 143,
50
54
  ChangePassword: 144,
51
55
  UpdatePermissions: 145,
@@ -53,9 +57,14 @@ export const Operation = {
53
57
  DeletePersonalAccessToken: 147,
54
58
  JoinConsumerGroup: 148,
55
59
  LeaveConsumerGroup: 149,
60
+ UpdateResource: 150,
56
61
  SendMessages: 160,
57
62
  StoreConsumerOffset: 161,
58
- DeleteConsumerOffset: 162
63
+ DeleteConsumerOffset: 162,
64
+ RetireSession: 166,
65
+ InstallConsumerGroupOwner: 167,
66
+ TransitionPartitionHistory: 168,
67
+ RetireConsumerGroupOwners: 169
59
68
  };
60
69
  const INTERNAL_START = 64;
61
70
  const METADATA_START = 128;
@@ -67,7 +76,6 @@ const METADATA_START = 128;
67
76
  const REPLICATED_OPERATION = new Map([
68
77
  [COMMAND_CODE.CreateUser, Operation.CreateUser],
69
78
  [COMMAND_CODE.DeleteUser, Operation.DeleteUser],
70
- [COMMAND_CODE.UpdateUser, Operation.UpdateUser],
71
79
  [COMMAND_CODE.UpdatePermissions, Operation.UpdatePermissions],
72
80
  [COMMAND_CODE.ChangePassword, Operation.ChangePassword],
73
81
  [COMMAND_CODE.CreateAccessToken, Operation.CreatePersonalAccessToken],
@@ -77,19 +85,16 @@ const REPLICATED_OPERATION = new Map([
77
85
  [COMMAND_CODE.DeleteConsumerOffset, Operation.DeleteConsumerOffset],
78
86
  [COMMAND_CODE.CreateStream, Operation.CreateStream],
79
87
  [COMMAND_CODE.DeleteStream, Operation.DeleteStream],
80
- [COMMAND_CODE.UpdateStream, Operation.UpdateStream],
81
- [COMMAND_CODE.PurgeStream, Operation.PurgeStream],
82
88
  [COMMAND_CODE.CreateTopic, Operation.CreateTopic],
83
89
  [COMMAND_CODE.DeleteTopic, Operation.DeleteTopic],
84
- [COMMAND_CODE.UpdateTopic, Operation.UpdateTopic],
85
- [COMMAND_CODE.PurgeTopic, Operation.PurgeTopic],
86
90
  [COMMAND_CODE.CreatePartitions, Operation.CreatePartitions],
87
91
  [COMMAND_CODE.DeletePartitions, Operation.DeletePartitions],
88
92
  [COMMAND_CODE.DeleteSegments, Operation.DeleteSegments],
89
93
  [COMMAND_CODE.CreateGroup, Operation.CreateConsumerGroup],
90
94
  [COMMAND_CODE.DeleteGroup, Operation.DeleteConsumerGroup],
91
95
  [COMMAND_CODE.JoinGroup, Operation.JoinConsumerGroup],
92
- [COMMAND_CODE.LeaveGroup, Operation.LeaveConsumerGroup]
96
+ [COMMAND_CODE.LeaveGroup, Operation.LeaveConsumerGroup],
97
+ [COMMAND_CODE.UpdateResource, Operation.UpdateResource]
93
98
  ]);
94
99
  const KNOWN_OPERATIONS = new Set(Object.values(Operation));
95
100
  /**
@@ -98,8 +103,12 @@ const KNOWN_OPERATIONS = new Set(Object.values(Operation));
98
103
  * so an undeclared value must be rejected, not classified by range.
99
104
  */
100
105
  export const isKnownOperation = (operation) => KNOWN_OPERATIONS.has(operation);
101
- /** Internal band, never client-sent. */
102
- export const isInternal = (operation) => operation >= INTERNAL_START && operation < METADATA_START;
106
+ /** Replica-only operations, never client-sent. */
107
+ export const isInternal = (operation) => (operation >= INTERNAL_START && operation < METADATA_START) ||
108
+ operation === Operation.RetireSession ||
109
+ operation === Operation.InstallConsumerGroupOwner ||
110
+ operation === Operation.TransitionPartitionHistory ||
111
+ operation === Operation.RetireConsumerGroupOwners;
103
112
  /**
104
113
  * Metadata classification is an explicit allowlist, not a range:
105
114
  * `DeleteSegments` (138) sits inside the band but resolves to a partition
@@ -107,12 +116,33 @@ export const isInternal = (operation) => operation >= INTERNAL_START && operatio
107
116
  * `Operation::is_metadata`.
108
117
  */
109
118
  export const isMetadata = (operation) => {
110
- if (isInternal(operation))
119
+ if (isInternal(operation) && operation < Operation.SendMessages)
111
120
  return true;
112
121
  if (operation === Operation.DeleteSegments)
113
122
  return false;
114
123
  return operation >= METADATA_START &&
115
- operation <= Operation.LeaveConsumerGroup;
124
+ operation <= Operation.UpdateResource;
125
+ };
126
+ /**
127
+ * The client operation a committed reply answers. Preparation rewrites some
128
+ * requests into an internal operation, so retry identity maps it back. Port of
129
+ * `Operation::request_operation`.
130
+ */
131
+ export const requestOperation = (operation) => {
132
+ switch (operation) {
133
+ case Operation.CreateTopicWithAssignments:
134
+ return Operation.CreateTopic;
135
+ case Operation.CreatePartitionsWithAssignments:
136
+ return Operation.CreatePartitions;
137
+ case Operation.UpdateStreamResolved:
138
+ case Operation.UpdateTopicResolved:
139
+ case Operation.UpdateUserResolved:
140
+ return Operation.UpdateResource;
141
+ case Operation.TruncatePartition:
142
+ return Operation.DeleteSegments;
143
+ default:
144
+ return operation;
145
+ }
116
146
  };
117
147
  /** Whether a reply body leads with a committed result section. */
118
148
  export const isResultFramed = (operation) => isMetadata(operation) ||
@@ -17,23 +17,18 @@
17
17
  import assert from 'node:assert/strict';
18
18
  import { describe, it } from 'node:test';
19
19
  import { COMMAND_CODE } from '../command.code.js';
20
- import { isInternal, isKnownOperation, isMetadata, isResultFramed, Operation, operationForCode } from './operation.js';
20
+ import { isInternal, isKnownOperation, isMetadata, isResultFramed, Operation, operationForCode, requestOperation } from './operation.js';
21
21
  const replicated = new Map([
22
22
  [COMMAND_CODE.CreateStream, Operation.CreateStream],
23
- [COMMAND_CODE.UpdateStream, Operation.UpdateStream],
24
23
  [COMMAND_CODE.DeleteStream, Operation.DeleteStream],
25
- [COMMAND_CODE.PurgeStream, Operation.PurgeStream],
26
24
  [COMMAND_CODE.CreateTopic, Operation.CreateTopic],
27
- [COMMAND_CODE.UpdateTopic, Operation.UpdateTopic],
28
25
  [COMMAND_CODE.DeleteTopic, Operation.DeleteTopic],
29
- [COMMAND_CODE.PurgeTopic, Operation.PurgeTopic],
30
26
  [COMMAND_CODE.CreatePartitions, Operation.CreatePartitions],
31
27
  [COMMAND_CODE.DeletePartitions, Operation.DeletePartitions],
32
28
  [COMMAND_CODE.DeleteSegments, Operation.DeleteSegments],
33
29
  [COMMAND_CODE.CreateGroup, Operation.CreateConsumerGroup],
34
30
  [COMMAND_CODE.DeleteGroup, Operation.DeleteConsumerGroup],
35
31
  [COMMAND_CODE.CreateUser, Operation.CreateUser],
36
- [COMMAND_CODE.UpdateUser, Operation.UpdateUser],
37
32
  [COMMAND_CODE.DeleteUser, Operation.DeleteUser],
38
33
  [COMMAND_CODE.ChangePassword, Operation.ChangePassword],
39
34
  [COMMAND_CODE.UpdatePermissions, Operation.UpdatePermissions],
@@ -41,6 +36,7 @@ const replicated = new Map([
41
36
  [COMMAND_CODE.DeleteAccessToken, Operation.DeletePersonalAccessToken],
42
37
  [COMMAND_CODE.JoinGroup, Operation.JoinConsumerGroup],
43
38
  [COMMAND_CODE.LeaveGroup, Operation.LeaveConsumerGroup],
39
+ [COMMAND_CODE.UpdateResource, Operation.UpdateResource],
44
40
  [COMMAND_CODE.SendMessages, Operation.SendMessages],
45
41
  [COMMAND_CODE.StoreOffset, Operation.StoreConsumerOffset],
46
42
  [COMMAND_CODE.DeleteConsumerOffset, Operation.DeleteConsumerOffset]
@@ -68,18 +64,46 @@ describe('VSR operation classification', () => {
68
64
  assert.equal(isInternal(64), true);
69
65
  assert.equal(isInternal(63), false);
70
66
  assert.equal(isInternal(Operation.CreateStream), false);
67
+ assert.equal(isInternal(Operation.RetireSession), true);
68
+ assert.equal(isMetadata(Operation.RetireSession), false);
69
+ assert.equal(isResultFramed(Operation.RetireSession), false);
71
70
  assert.equal(isMetadata(Operation.CreateStream), true);
72
71
  assert.equal(isMetadata(Operation.LeaveConsumerGroup), true);
73
72
  assert.equal(isMetadata(Operation.DeleteSegments), false);
74
- assert.equal(isMetadata(150), false);
73
+ assert.equal(isMetadata(Operation.UpdateResource), true);
74
+ assert.equal(isResultFramed(Operation.UpdateResource), true);
75
+ for (const resolved of [Operation.UpdateStreamResolved, Operation.UpdateTopicResolved,
76
+ Operation.UpdateUserResolved, Operation.RewrapStreamKeys]) {
77
+ assert.equal(isInternal(resolved), true);
78
+ assert.equal(isMetadata(resolved), true);
79
+ assert.equal(isResultFramed(resolved), true);
80
+ }
81
+ assert.equal(isMetadata(151), false);
75
82
  assert.equal(isResultFramed(Operation.StoreConsumerOffset), true);
76
83
  assert.equal(isResultFramed(Operation.DeleteConsumerOffset), true);
77
84
  assert.equal(isResultFramed(Operation.SendMessages), false);
78
85
  });
86
+ it('maps a resolved committed operation back to the request operation', () => {
87
+ for (const [committed, request] of [
88
+ [Operation.CreateTopicWithAssignments, Operation.CreateTopic],
89
+ [Operation.CreatePartitionsWithAssignments, Operation.CreatePartitions],
90
+ [Operation.TruncatePartition, Operation.DeleteSegments],
91
+ [Operation.UpdateStreamResolved, Operation.UpdateResource],
92
+ [Operation.UpdateTopicResolved, Operation.UpdateResource],
93
+ [Operation.UpdateUserResolved, Operation.UpdateResource],
94
+ [Operation.RewrapStreamKeys, Operation.RewrapStreamKeys],
95
+ [Operation.CreateStream, Operation.CreateStream]
96
+ ])
97
+ assert.equal(requestOperation(committed), request);
98
+ });
79
99
  it('recognizes only declared operation discriminants', () => {
80
100
  assert.equal(isKnownOperation(Operation.Register), true);
81
101
  assert.equal(isKnownOperation(Operation.SendMessages), true);
82
- for (const undeclared of [69, 127, 150, 159, 163, 164, 165, 166, 255])
102
+ assert.equal(isKnownOperation(Operation.RetireSession), true);
103
+ assert.equal(isKnownOperation(Operation.FinalizeSession), true);
104
+ assert.equal(isKnownOperation(Operation.RetireConsumerGroupOwners), true);
105
+ assert.equal(isInternal(Operation.RetireConsumerGroupOwners), true);
106
+ for (const undeclared of [69, 76, 127, 129, 131, 133, 135, 142, 151, 159, 163, 164, 165, 170, 255])
83
107
  assert.equal(isKnownOperation(undeclared), false);
84
108
  });
85
109
  });
@@ -1,17 +1,21 @@
1
+ export declare const BIND_SECRET_BYTES = 32;
2
+ export declare const SESSION_IDENTITY_BYTES = 32;
1
3
  /**
2
4
  * Packed protocol semver of the wire contract this port implements,
3
- * `pack(0, 11, 0)` per `core/binary_protocol/src/version.rs`. Bump together
4
- * with the Rust `IGGY_PROTOCOL_VERSION` on any wire-incompatible change.
5
+ * `pack(0, 12, 0)` per `core/binary_protocol/src/version.rs`. Update with the
6
+ * Rust constant during stable release preparation. Edge builds sharing this
7
+ * number are not guaranteed to have compatible layouts.
5
8
  */
6
9
  export declare const IGGY_PROTOCOL_VERSION: number;
7
10
  /**
8
11
  * `LoginRegisterRequest` body:
9
- * `[ClientVersionInfo][username len u8 + bytes][password len u8 + bytes]
10
- * [context len u32][context]`.
12
+ * `[ClientVersionInfo][bind secret 32 bytes][username len u8 + bytes]
13
+ * [password len u8 + bytes][context len u32][context]`.
11
14
  */
12
- export declare const serializeLoginRegister: (username: string, password: string, sdkVersion: string) => Buffer;
15
+ export declare const serializeLoginRegister: (username: string, password: string, sdkVersion: string, bindSecret: Buffer) => Buffer;
13
16
  /** `LoginRegisterWithPatRequest` body: the PAT takes the credential slot. */
14
- export declare const serializeLoginRegisterWithPat: (token: string, sdkVersion: string) => Buffer;
17
+ export declare const serializeLoginRegisterWithPat: (token: string, sdkVersion: string, bindSecret: Buffer) => Buffer;
18
+ export declare const serializeBindSession: (identity: Buffer, sdkVersion: string, bindSecret: Buffer) => Buffer;
15
19
  /** Decoded `LoginRegisterResponse`. */
16
20
  export type LoginRegisterResponse = {
17
21
  userId: number;
@@ -22,12 +22,15 @@
22
22
  * `LoginRegister` / `LoginRegisterWithPat` and nothing else.
23
23
  */
24
24
  import { DeserializeError } from '../error.utils.js';
25
+ export const BIND_SECRET_BYTES = 32;
26
+ export const SESSION_IDENTITY_BYTES = 32;
25
27
  /**
26
28
  * Packed protocol semver of the wire contract this port implements,
27
- * `pack(0, 11, 0)` per `core/binary_protocol/src/version.rs`. Bump together
28
- * with the Rust `IGGY_PROTOCOL_VERSION` on any wire-incompatible change.
29
+ * `pack(0, 12, 0)` per `core/binary_protocol/src/version.rs`. Update with the
30
+ * Rust constant during stable release preparation. Edge builds sharing this
31
+ * number are not guaranteed to have compatible layouts.
29
32
  */
30
- export const IGGY_PROTOCOL_VERSION = (0 << 20) | (11 << 10) | 0;
33
+ export const IGGY_PROTOCOL_VERSION = (0 << 20) | (12 << 10) | 0;
31
34
  const SDK_NAME = 'node-sdk';
32
35
  const wireName = (value) => {
33
36
  const bytes = Buffer.from(value, 'utf8');
@@ -42,15 +45,17 @@ const versionInfo = (sdkVersion) => {
42
45
  };
43
46
  /**
44
47
  * `LoginRegisterRequest` body:
45
- * `[ClientVersionInfo][username len u8 + bytes][password len u8 + bytes]
46
- * [context len u32][context]`.
48
+ * `[ClientVersionInfo][bind secret 32 bytes][username len u8 + bytes]
49
+ * [password len u8 + bytes][context len u32][context]`.
47
50
  */
48
- export const serializeLoginRegister = (username, password, sdkVersion) => {
51
+ export const serializeLoginRegister = (username, password, sdkVersion, bindSecret) => {
52
+ validateBindSecret(bindSecret);
49
53
  const passwordBytes = Buffer.from(password, 'utf8');
50
54
  if (passwordBytes.length > 255)
51
55
  throw new Error('password exceeds the u8 length prefix');
52
56
  return Buffer.concat([
53
57
  versionInfo(sdkVersion),
58
+ bindSecret,
54
59
  wireName(username),
55
60
  Buffer.from([passwordBytes.length]),
56
61
  passwordBytes,
@@ -58,17 +63,29 @@ export const serializeLoginRegister = (username, password, sdkVersion) => {
58
63
  ]);
59
64
  };
60
65
  /** `LoginRegisterWithPatRequest` body: the PAT takes the credential slot. */
61
- export const serializeLoginRegisterWithPat = (token, sdkVersion) => {
66
+ export const serializeLoginRegisterWithPat = (token, sdkVersion, bindSecret) => {
67
+ validateBindSecret(bindSecret);
62
68
  const tokenBytes = Buffer.from(token, 'utf8');
63
69
  if (tokenBytes.length > 255)
64
70
  throw new Error('token exceeds the u8 length prefix');
65
71
  return Buffer.concat([
66
72
  versionInfo(sdkVersion),
73
+ bindSecret,
67
74
  Buffer.from([tokenBytes.length]),
68
75
  tokenBytes,
69
76
  Buffer.alloc(4)
70
77
  ]);
71
78
  };
79
+ export const serializeBindSession = (identity, sdkVersion, bindSecret) => {
80
+ if (identity.length !== SESSION_IDENTITY_BYTES)
81
+ throw new RangeError('session identity must be 32 bytes');
82
+ validateBindSecret(bindSecret);
83
+ return Buffer.concat([versionInfo(sdkVersion), identity, bindSecret]);
84
+ };
85
+ const validateBindSecret = (secret) => {
86
+ if (secret.length !== BIND_SECRET_BYTES)
87
+ throw new RangeError('bind secret must be 32 bytes');
88
+ };
72
89
  /**
73
90
  * `[user_id u32][session u64][server_protocol_version u32]
74
91
  * [server_version len u8 + bytes]`.