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
@@ -0,0 +1,285 @@
1
+ // Licensed to the Apache Software Foundation (ASF) under one
2
+ // or more contributor license agreements. See the NOTICE file
3
+ // distributed with this work for additional information
4
+ // regarding copyright ownership. The ASF licenses this file
5
+ // to you under the Apache License, Version 2.0 (the
6
+ // "License"); you may not use this file except in compliance
7
+ // with the License. You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing,
12
+ // software distributed under the License is distributed on an
13
+ // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ // KIND, either express or implied. See the License for the
15
+ // specific language governing permissions and limitations
16
+ // under the License.
17
+ import assert from 'node:assert/strict';
18
+ import { describe, it } from 'node:test';
19
+ import { COMMAND_CODE } from '../wire/command.code.js';
20
+ import { VsrSession } from '../wire/vsr/index.js';
21
+ import { HEADER_SIZE, REQUEST_OFFSET, EMPTY_PARTITION_CONTEXT } from '../wire/vsr/header.js';
22
+ import { MutationLanes, LaneBlockedError, OperationNotRetainedError } from './client.operation.js';
23
+ import { ResponseError } from '../wire/error.utils.js';
24
+ const body = Buffer.from('frozen body');
25
+ const frameSize = HEADER_SIZE + body.length;
26
+ const session = () => { const state = new VsrSession(1n); state.bind(42n); return state; };
27
+ const enter = (lanes, state, lane, signal = new AbortController().signal, deadline = Date.now() + 1_000) => lanes.enter(lane, COMMAND_CODE.CreateStream, body, EMPTY_PARTITION_CONTEXT, true, state, deadline, signal);
28
+ describe('mutation admission and retained ownership', () => {
29
+ it('retains the request identifier for a replay and mints one for a restart', async () => {
30
+ const lanes = new MutationLanes(1, frameSize);
31
+ const state = session();
32
+ const operation = await enter(lanes, state, 'metadata');
33
+ const span = [REQUEST_OFFSET.requestChecksum, REQUEST_OFFSET.requestChecksum + 16];
34
+ const first = Buffer.from(operation.frame.buffer.subarray(...span));
35
+ const replay = operation.frame.buffer;
36
+ assert.deepEqual(replay.subarray(...span), first);
37
+ await lanes.restart(operation, body, EMPTY_PARTITION_CONTEXT, state, Date.now() + 1_000, new AbortController().signal);
38
+ assert.notDeepEqual(operation.frame.buffer.subarray(...span), first);
39
+ lanes.complete(operation);
40
+ });
41
+ it('retains one lane while independent groups keep their own order', async () => {
42
+ const lanes = new MutationLanes(3, 3 * frameSize);
43
+ const state = session();
44
+ const first = await enter(lanes, state, 'first');
45
+ first.uncertain = true;
46
+ lanes.retain(first);
47
+ await assert.rejects(enter(lanes, state, 'first'), (error) => error instanceof LaneBlockedError && error.operationId === first.id);
48
+ const other = await enter(lanes, state, 'other');
49
+ let admitted = false;
50
+ const next = enter(lanes, state, 'other').then((operation) => { admitted = true; return operation; });
51
+ await new Promise((resolve) => setImmediate(resolve));
52
+ assert.equal(admitted, false);
53
+ lanes.complete(other);
54
+ lanes.complete(await next);
55
+ assert.equal(lanes.retained(first.id), first);
56
+ lanes.complete(first);
57
+ });
58
+ it('preserves FIFO when waiters at the head, middle and tail cancel', async () => {
59
+ const count = 512;
60
+ const lanes = new MutationLanes(1, frameSize);
61
+ const state = session();
62
+ const active = await enter(lanes, state, 'queued');
63
+ const controllers = Array.from({ length: count }, () => new AbortController());
64
+ const admitted = [];
65
+ const pending = controllers.map((controller, index) => enter(lanes, state, 'queued', controller.signal, Date.now() + 5_000).then((operation) => {
66
+ admitted.push(index);
67
+ lanes.complete(operation);
68
+ }, (error) => { assert.equal(controller.signal.aborted, true, String(error)); }));
69
+ const cancelled = new Set([0, Math.floor(count / 2), count - 1]);
70
+ for (const index of cancelled)
71
+ controllers[index].abort();
72
+ lanes.complete(active);
73
+ await Promise.all(pending);
74
+ assert.deepEqual(admitted, controllers.map((_, index) => index).filter((index) => !cancelled.has(index)));
75
+ });
76
+ it('rejects a mutation that cannot fit a positive byte budget', async () => {
77
+ const lanes = new MutationLanes(1, 1);
78
+ await assert.rejects(enter(lanes, session(), 'metadata'), ResponseError);
79
+ });
80
+ for (const limit of ['count', 'bytes']) {
81
+ it(`keeps progress polls ordered without consuming the full mutation ${limit} budget`, async () => {
82
+ const lanes = new MutationLanes(limit === 'count' ? 1 : 3, (limit === 'bytes' ? 1 : 3) * frameSize);
83
+ const state = session();
84
+ const cancel = new AbortController();
85
+ const pending = [];
86
+ const enterProgress = (lane) => lanes.enter(lane, COMMAND_CODE.PollMessages, body, EMPTY_PARTITION_CONTEXT, false, state, Date.now() + 1_000, cancel.signal);
87
+ try {
88
+ const retained = await enter(lanes, state, 'retained');
89
+ retained.uncertain = true;
90
+ lanes.retain(retained);
91
+ await assert.rejects(enterProgress('retained'), (error) => error instanceof LaneBlockedError && error.operationId === retained.id);
92
+ const first = await enterProgress('progress');
93
+ const originalId = first.id;
94
+ await lanes.restart(first, body, EMPTY_PARTITION_CONTEXT, state, Date.now() + 1_000, cancel.signal);
95
+ assert.equal(first.id, originalId + 1n);
96
+ let progressAdmitted = false;
97
+ const progress = enterProgress('progress').then((operation) => { progressAdmitted = true; return operation; });
98
+ pending.push(progress);
99
+ let mutationAdmitted = false;
100
+ const mutation = enter(lanes, state, 'other', cancel.signal).then((operation) => { mutationAdmitted = true; return operation; });
101
+ pending.push(mutation);
102
+ await new Promise((resolve) => setImmediate(resolve));
103
+ assert.equal(progressAdmitted, false);
104
+ assert.equal(mutationAdmitted, false);
105
+ lanes.complete(first);
106
+ lanes.complete(await progress);
107
+ await new Promise((resolve) => setImmediate(resolve));
108
+ assert.equal(mutationAdmitted, false);
109
+ lanes.complete(retained);
110
+ lanes.complete(await mutation);
111
+ }
112
+ finally {
113
+ cancel.abort();
114
+ lanes.reset();
115
+ await Promise.allSettled(pending);
116
+ }
117
+ });
118
+ }
119
+ it('admits an independent lane when only a later same-lane waiter fits the free bytes', async () => {
120
+ const sizes = { retained: 900, active: 300, large: 700, small: 500, independent: 500 };
121
+ const lanes = new MutationLanes(2, 1_500);
122
+ const state = session();
123
+ const cancel = new AbortController();
124
+ const pending = [];
125
+ const admitted = [];
126
+ const enterFrame = (lane, size) => lanes.enter(lane, COMMAND_CODE.CreateStream, Buffer.alloc(size - HEADER_SIZE), EMPTY_PARTITION_CONTEXT, true, state, Date.now() + 1_000, cancel.signal);
127
+ try {
128
+ const retained = await enterFrame('retained', sizes.retained);
129
+ retained.uncertain = true;
130
+ lanes.retain(retained);
131
+ const active = await enterFrame('active', sizes.active);
132
+ const large = enterFrame('queued', sizes.large).then((operation) => { admitted.push('large'); return operation; });
133
+ const small = enterFrame('queued', sizes.small).then((operation) => { admitted.push('small'); return operation; });
134
+ const independent = enterFrame('independent', sizes.independent).then((operation) => { admitted.push('independent'); return operation; });
135
+ pending.push(large, small, independent);
136
+ lanes.complete(active);
137
+ await new Promise((resolve) => setImmediate(resolve));
138
+ assert.deepEqual(admitted, ['independent']);
139
+ lanes.complete(await independent);
140
+ lanes.complete(retained);
141
+ lanes.complete(await large);
142
+ lanes.complete(await small);
143
+ assert.deepEqual(admitted, ['independent', 'large', 'small']);
144
+ }
145
+ finally {
146
+ cancel.abort();
147
+ lanes.reset();
148
+ await Promise.allSettled(pending);
149
+ }
150
+ });
151
+ it('releases logical count immediately but pins bytes through the final writer alias', async () => {
152
+ const lanes = new MutationLanes(1, 2 * frameSize);
153
+ const state = session();
154
+ const first = await enter(lanes, state, 'first');
155
+ first.frame.retain();
156
+ lanes.complete(first);
157
+ const second = await enter(lanes, state, 'second');
158
+ assert.equal(second.id, first.id + 1n);
159
+ second.frame.retain();
160
+ lanes.complete(second);
161
+ let admitted = false;
162
+ const third = enter(lanes, state, 'third').then((operation) => { admitted = true; return operation; });
163
+ await new Promise((resolve) => setImmediate(resolve));
164
+ assert.equal(admitted, false);
165
+ first.frame.release();
166
+ lanes.complete(await third);
167
+ second.frame.release();
168
+ });
169
+ it('checks oversize and expired admission before allocating an operation identity', async () => {
170
+ const state = session();
171
+ const lanes = new MutationLanes(1, HEADER_SIZE);
172
+ await assert.rejects(enter(lanes, state, 'oversize'), (error) => error instanceof ResponseError && error.errorCode === 80);
173
+ assert.equal(state.encode(COMMAND_CODE.Ping, Buffer.alloc(0)).readBigUInt64LE(REQUEST_OFFSET.request), 1n);
174
+ const expired = new MutationLanes(1, frameSize);
175
+ await assert.rejects(enter(expired, state, 'expired', undefined, Date.now() - 1), (error) => error instanceof ResponseError && error.errorCode === 58);
176
+ assert.equal(state.encode(COMMAND_CODE.Ping, Buffer.alloc(0)).readBigUInt64LE(REQUEST_OFFSET.request), 2n);
177
+ });
178
+ it('reports a queued admission deadline as not accepted without consuming an identity', async () => {
179
+ const state = session();
180
+ const lanes = new MutationLanes(1, frameSize);
181
+ const first = await enter(lanes, state, 'first');
182
+ await assert.rejects(enter(lanes, state, 'second', undefined, Date.now() + 10), (error) => error instanceof ResponseError && error.errorCode === 58);
183
+ lanes.complete(first);
184
+ const next = await enter(lanes, state, 'second');
185
+ assert.equal(next.id, first.id + 1n);
186
+ lanes.complete(next);
187
+ });
188
+ it('keeps a restarting lane while its old writer pins the entire byte budget', async () => {
189
+ const state = session();
190
+ const lanes = new MutationLanes(1, frameSize);
191
+ const first = await enter(lanes, state, 'first');
192
+ const originalId = first.id;
193
+ const writer = first.frame;
194
+ writer.retain();
195
+ let restarted = false;
196
+ const restart = lanes.restart(first, body, EMPTY_PARTITION_CONTEXT, state, Date.now() + 1_000, first.lifetime.signal)
197
+ .then(() => { restarted = true; });
198
+ let admitted = false;
199
+ const later = enter(lanes, state, 'first').then((operation) => { admitted = true; return operation; });
200
+ await new Promise((resolve) => setImmediate(resolve));
201
+ assert.equal(restarted, false);
202
+ assert.equal(admitted, false);
203
+ writer.release();
204
+ await restart;
205
+ assert.equal(first.id, originalId + 1n);
206
+ assert.equal(admitted, false);
207
+ lanes.complete(first);
208
+ const next = await later;
209
+ assert.equal(next.id, first.id + 1n);
210
+ lanes.complete(next);
211
+ lanes.complete(await enter(lanes, state, 'independent'));
212
+ });
213
+ it('keeps a restart through a reset and frees the bytes of its old writer once', async () => {
214
+ const state = session();
215
+ const lanes = new MutationLanes(1, frameSize);
216
+ const first = await enter(lanes, state, 'first');
217
+ const id = first.id;
218
+ const writer = first.frame;
219
+ writer.retain();
220
+ const restart = lanes.restart(first, body, EMPTY_PARTITION_CONTEXT, state, Date.now() + 1_000, first.lifetime.signal);
221
+ lanes.reset();
222
+ writer.release();
223
+ await restart;
224
+ assert.ok(lanes.owns(first));
225
+ assert.notEqual(first.id, id);
226
+ lanes.complete(first);
227
+ lanes.complete(await enter(lanes, state, 'first'));
228
+ });
229
+ it('cancels budget waiters without copying bodies or consuming identities', async () => {
230
+ const state = session();
231
+ const lanes = new MutationLanes(1, frameSize);
232
+ const first = await enter(lanes, state, 'first');
233
+ const cancel = new AbortController();
234
+ const waiting = enter(lanes, state, 'other', cancel.signal);
235
+ cancel.abort();
236
+ await assert.rejects(waiting);
237
+ lanes.complete(first);
238
+ const admitted = await enter(lanes, state, 'other');
239
+ assert.equal(admitted.id, first.id + 1n);
240
+ lanes.complete(admitted);
241
+ });
242
+ it('serializes concurrent resolvers and frees the slot but not the lane of an abandoned running replay', async () => {
243
+ const lanes = new MutationLanes(1, 3 * frameSize);
244
+ const state = session();
245
+ const operation = await enter(lanes, state, 'first');
246
+ operation.uncertain = true;
247
+ lanes.retain(operation);
248
+ const first = await lanes.acquireResolution(operation, Date.now() + 1_000, operation.lifetime.signal);
249
+ const waiting = lanes.acquireResolution(operation, Date.now() + 1_000, operation.lifetime.signal);
250
+ lanes.abandon(operation.id);
251
+ assert.throws(() => lanes.retained(operation.id), OperationNotRetainedError);
252
+ lanes.complete(await enter(lanes, state, 'other', undefined, Date.now() + 200));
253
+ let admitted = false;
254
+ const next = enter(lanes, state, 'first').then((later) => { admitted = true; return later; });
255
+ await new Promise((resolve) => setImmediate(resolve));
256
+ assert.equal(admitted, false);
257
+ first();
258
+ await assert.rejects(waiting, OperationNotRetainedError);
259
+ lanes.complete(await next);
260
+ const held = await enter(lanes, state, 'other');
261
+ await assert.rejects(enter(lanes, state, 'last', undefined, Date.now() + 20), (error) => error instanceof ResponseError && error.errorCode === 58);
262
+ lanes.complete(held);
263
+ });
264
+ it('clears retained admission on reset without losing writer byte ownership', async () => {
265
+ const state = session();
266
+ const lanes = new MutationLanes(1, frameSize);
267
+ const old = await enter(lanes, state, 'first');
268
+ old.frame.retain();
269
+ old.uncertain = true;
270
+ lanes.retain(old);
271
+ lanes.reset();
272
+ assert.throws(() => lanes.retained(old.id), OperationNotRetainedError);
273
+ state.reset();
274
+ state.bind(43n);
275
+ let admitted = false;
276
+ const fresh = enter(lanes, state, 'fresh').then((operation) => { admitted = true; return operation; });
277
+ await new Promise((resolve) => setImmediate(resolve));
278
+ assert.equal(admitted, false);
279
+ old.frame.release();
280
+ const next = await fresh;
281
+ assert.ok(next.id > old.id);
282
+ lanes.complete(next);
283
+ });
284
+ });
285
+ //# sourceMappingURL=client.operation.test.js.map
@@ -1,233 +1,116 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import type { ClientConfigOrString, ClientCredentials, CommandResponse, PasswordCredentials, RawClient, SendCommandOptions, TokenCredentials } from '../client/client.type.js';
3
- export declare class VsrResponseTimeoutError extends Error {
4
- constructor(timeout: number);
5
- }
6
- /**
7
- * Manages command execution and response handling for the Iggy server.
8
- * Implements command queuing, authentication, and heartbeat functionality.
9
- */
2
+ import type { ClientConfigOrString, ClientCredentials, CommandResponse, PasswordCredentials, RawClient, SendCommandOptions, TokenCredentials } from './client.type.js';
3
+ export { LaneBlockedError, NotConnectedError, OperationNotRetainedError, OperationUncertainError, VsrResponseTimeoutError } from './client.operation.js';
4
+ /** One logical session owns every link, mutation lane and retained operation. */
10
5
  export declare class CommandResponseStream extends EventEmitter {
11
- /** Client configuration */
12
- private options;
13
- /** Underlying connection to the server */
14
- private connection;
15
- /** Queue of pending command jobs */
16
- private _execQueue;
17
- /** Consensus session used by VSR framing */
18
- private vsrSession;
19
- /** Shared authentication attempt for concurrent callers */
6
+ private readonly options;
7
+ private readonly vsrSession;
8
+ private readonly lanes;
9
+ private readonly replicas;
10
+ private readonly partitionRoutes;
11
+ private readonly changed;
12
+ private readonly lifetime;
13
+ private sessionLifetime;
14
+ private bootstrap?;
15
+ private bootstrapPromise?;
16
+ /**
17
+ * What the running bootstrap reports when it gives up: the bind failure of
18
+ * the last endpoint that accepted a dial, else NotConnectedError with the
19
+ * last dial failure.
20
+ */
21
+ private bootstrapFailure?;
20
22
  private authenticationPromise?;
21
- /** Whether a login is already being moved to the leader */
22
- private settlingLeader;
23
- /**
24
- * The leader re-check a refused request started, shared with every other
25
- * request refused by the same node so one demotion moves the client once.
26
- */
27
- private leaderMoveInFlight?;
23
+ private sessionAction;
24
+ private recoveryPromise?;
25
+ private rosterPromise?;
28
26
  /**
29
- * Set when a roster walk just redirected this client, so the login that
30
- * re-authenticates it stays on the dialed node instead of settling back on
31
- * the metadata leader whose partition replica refused the request.
27
+ * Whom the client signs in as by itself: the caller's last sign-in, else the
28
+ * configured credentials while the caller never signed in. A logout, a
29
+ * sign-in of the caller that fails and a refused sign-in clear it.
32
30
  */
33
- private walkSettleSuppressed;
31
+ private signInCredentials?;
34
32
  /**
35
- * Refusals handed out to callers that have not decided what to do with them
36
- * yet. The queue holds while any are outstanding: the caller of a refused
37
- * command re-checks the leader, and a command written in the meantime goes
38
- * out on the socket that check is about to replace.
33
+ * Raised by each sign-in and sign-out of the caller. A call continues under
34
+ * a new session only while the lineage it started with is unchanged.
39
35
  */
40
- private leaderMovesUndecided;
41
- /** How long a leaderless roster is polled before settling in place */
42
- private leaderlessWaitBudget;
43
- /** Delay between roster reads while the cluster elects */
44
- private leaderlessPollInterval;
45
- /** Calls that have acquired this stream but have not fully settled */
36
+ private sessionLineage;
37
+ private knownEndpoints;
38
+ private readonly configuredEndpoint;
39
+ private metadataReplica?;
40
+ private metadataWatermark;
41
+ private routingGeneration;
42
+ private activeDials;
46
43
  private pendingSubmissions;
47
- /** Whether the stream is currently processing a command */
48
- busy: boolean;
49
- /** Whether the client has been authenticated */
44
+ private heartbeatIntervalHandler?;
50
45
  isAuthenticated: boolean;
51
- /** Authenticated user ID */
52
46
  userId?: number;
53
- /** Heartbeat interval timer handle */
54
- heartbeatIntervalHandler?: NodeJS.Timeout;
55
- /** Whether a heartbeat ping is still awaiting its response */
56
- private heartbeatInFlight;
57
- private rememberedCredentials?;
58
- private clustered?;
59
- private metadataWatermark;
60
- private routingGeneration;
61
- private pollRoutes;
62
- private pollConnections;
63
- /**
64
- * Creates a new CommandResponseStream.
65
- *
66
- * @param options - Client configuration
67
- */
68
- constructor(options: ClientConfigOrString);
69
- /**
70
- * Initializes the stream by setting up heartbeat and connection event handlers.
71
- */
72
- _init(): void;
73
- /**
74
- * Re-submits queued commands through the full send path, so each one
75
- * reconnects, re-authenticates and re-checks the leader as if it had just
76
- * been called.
77
- *
78
- * Only for a drop the client caused. Nothing here was written, so there is no
79
- * outcome in doubt: a command still in the queue when the socket is replaced
80
- * would otherwise fail with a lost-connection error the caller can do nothing
81
- * about.
82
- */
83
- private _reissueQueue;
84
- /**
85
- * Sends a command to the server.
86
- * Automatically handles connection, authentication and leader settlement.
87
- *
88
- * @param command - Command code to send
89
- * @param payload - Command payload buffer
90
- * @param options - Response and queue options
91
- * @returns Promise resolving to the command response
92
- */
47
+ constructor(config: ClientConfigOrString);
48
+ get lineage(): number;
49
+ get busy(): boolean;
93
50
  sendCommand(command: number, payload: Buffer, options?: SendCommandOptions): Promise<CommandResponse>;
94
- private _queueCommand;
95
- /**
96
- * Re-reads the roster and moves to the leader it names.
97
- *
98
- * Best effort: an unreadable roster, or one that still names this node,
99
- * leaves the client where it is and the refused request is re-issued anyway.
100
- *
101
- * Single-flighted, and concurrent callers share the outcome instead of
102
- * failing: several commands are refused by the same demoted node, and each
103
- * starting its own redirect would move the client once per command. The
104
- * first redirect's `'disconnected'` also fails the others' roster reads, so
105
- * a caller that raced one would report a refusal it never had to.
106
- *
107
- * @returns Whether the client moved
108
- */
109
- private _followLeaderMove;
110
- /** Whether a leader move is being decided or carried out. */
111
- private _movePending;
112
- /**
113
- * Releases the queue hold one refusal took, and drains what was held back
114
- * once the last of them is decided.
115
- */
116
- private _releaseUndecidedMove;
117
- private _rememberRoster;
118
- private _pollOnPrimary;
119
- private _pollRoutingControl;
120
- private _observeMetadataReply;
51
+ resolveOperation(operationId: bigint, options?: SendCommandOptions): Promise<void>;
52
+ abandonOperation(operationId: bigint): void;
53
+ private replay;
54
+ private drive;
55
+ private exchange;
56
+ private control;
57
+ private routeFor;
58
+ /**
59
+ * Whether the replica that answered a namespace miss on `link` is the
60
+ * metadata primary. The belief can predate a view change, so one roster read
61
+ * on `link`, without a retry or another link, must still name that replica.
62
+ * No lookup reaches a believed metadata primary without a link, so then that
63
+ * roster also moves the belief.
64
+ */
65
+ private isMetadataPrimary;
66
+ private rememberRoute;
67
+ private invalidateRoute;
68
+ private primaryLink;
69
+ private readyLink;
70
+ private refreshRoster;
71
+ private installRoster;
72
+ private runReplica;
73
+ /**
74
+ * Only a heartbeat restarts a link that used its budget, so without one the
75
+ * link stays down for good.
76
+ */
77
+ private budgetFailure;
78
+ private connectionFor;
79
+ private connectBootstrap;
80
+ private serializeSession;
81
+ private login;
82
+ private recoverSession;
83
+ /**
84
+ * Waits for the sign-in that runs or is queued now, and fails unless it binds
85
+ * a session of the call's lineage. Only the client's own sign-in continues a
86
+ * call: after a sign-in or sign-out of the caller, or a refused sign-in, the
87
+ * call ends with 40, and at its deadline with 58.
88
+ */
89
+ private nextSession;
90
+ /**
91
+ * The `sessionReset` event says whether the reset ended a signed-in session.
92
+ * A reset after a refused sign-in ends none.
93
+ */
94
+ private resetSession;
95
+ private ensureLineage;
96
+ private ensureGeneration;
97
+ private observeMetadata;
121
98
  private _rememberCredentials;
122
- private _signInCredentials;
123
- private _clearPollRouting;
124
- /**
125
- * Processes queued commands sequentially.
126
- * Emits 'finishQueue' when all commands are processed.
127
- *
128
- */
129
- _processQueue(): Promise<void>;
130
- private _emitFinishQueue;
131
- /**
132
- * Processes a single command by writing it to the connection and waiting for response.
133
- *
134
- * @param command - Command code
135
- * @param payload - Command payload
136
- * @param handleResp - Whether to parse the response
137
- * @param deadline - When the whole request gives up, shared with the leader
138
- * re-checks and the re-issues after a move
139
- * @returns Promise resolving to the command response
140
- */
141
- _processNext(command: number, payload: Buffer, handleResp?: boolean, deadline?: number): Promise<CommandResponse>;
142
- private _processVsrLogin;
143
- private _processVsr;
144
- private _exchange;
145
- private isUnloggedCommand;
146
- /**
147
- * Moves a freshly authenticated session to the cluster leader.
148
- *
149
- * Only the leader accepts replicated commands, and the roster read is
150
- * auth-gated, so the topology cannot be inspected before a login binds a
151
- * session. The redirect drops that session along with the socket, so the
152
- * login is replayed on the leader and its answer supersedes the one from the
153
- * node the client dialed. Leadership can move between the roster read and
154
- * the replay, so each freshly bound hop rechecks the roster under a bounded
155
- * redirect budget.
156
- *
157
- * @returns The leader's login response, or undefined when the client stays
158
- */
159
- private _settleOnLeader;
160
- /**
161
- * Reads the cluster roster and picks the endpoint to settle on.
162
- *
163
- * Best effort: an unreadable roster, `Unauthenticated` included (the session
164
- * died between the login and this read), keeps the client on its current
165
- * node instead of failing a login that already succeeded.
166
- */
167
- private _readLeaderEndpoint;
168
- /**
169
- * Fails all queued commands with the given error.
170
- *
171
- * @param err - Error to reject all queued commands with
172
- */
173
- _failQueue(err: Error): void;
174
- private _resetSession;
175
99
  hold(): () => void;
176
- /**
177
- * Authenticates the client with the server.
178
- *
179
- * @param creds - Authentication credentials (token or password)
180
- * @returns True if authentication succeeded
181
- */
182
- authenticate(creds: ClientCredentials): Promise<boolean>;
183
- private _authenticate;
184
- /**
185
- * Authenticates using username and password.
186
- *
187
- * @param creds - Password credentials
188
- * @returns Login response with user ID
189
- */
190
- _authWithPassword(creds: PasswordCredentials): Promise<{
100
+ authenticate(credentials: ClientCredentials): Promise<boolean>;
101
+ private loginByCaller;
102
+ /** Only SDK recovery shares a sign-in; callers always own a fresh attempt. */
103
+ private signIn;
104
+ _authWithPassword(credentials: PasswordCredentials): Promise<{
191
105
  userId: number;
192
106
  }>;
193
- /**
194
- * Authenticates using a token.
195
- *
196
- * @param creds - Token credentials
197
- * @returns Login response with user ID
198
- */
199
- _authWithToken(creds: TokenCredentials): Promise<{
107
+ _authWithToken(credentials: TokenCredentials): Promise<{
200
108
  userId: number;
201
109
  }>;
202
- /**
203
- * Sends a ping command to the server.
204
- *
205
- * @returns Ping response
206
- */
207
110
  ping(): Promise<boolean>;
208
- /**
209
- * Starts sending periodic heartbeat pings to keep the connection alive.
210
- *
211
- * @param interval - Heartbeat interval in milliseconds
212
- */
213
111
  heartbeat(interval?: number): void;
214
- /**
215
- * Returns the underlying socket as a readable stream.
216
- *
217
- * @returns The connection socket
218
- */
219
112
  getReadStream(): import("node:net").Socket;
220
- /**
221
- * Destroys the stream and cleans up resources.
222
- * Stops heartbeat and destroys the connection.
223
- */
224
113
  destroy(): void;
225
114
  }
226
- /**
227
- * Creates a new RawClient instance.
228
- *
229
- * @param options - Client configuration
230
- * @returns RawClient instance
231
- */
232
115
  export declare function getRawClient(options: ClientConfigOrString): RawClient;
233
116
  //# sourceMappingURL=client.socket.d.ts.map