@durable-streams/client 0.1.5 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -18
- package/dist/index.cjs +1152 -805
- package/dist/index.d.cts +201 -33
- package/dist/index.d.ts +201 -33
- package/dist/index.js +1150 -806
- package/package.json +2 -2
- package/src/constants.ts +19 -2
- package/src/error.ts +20 -0
- package/src/idempotent-producer.ts +195 -43
- package/src/index.ts +7 -0
- package/src/response.ts +245 -35
- package/src/sse.ts +27 -5
- package/src/stream-api.ts +30 -10
- package/src/stream.ts +213 -71
- package/src/types.ts +97 -12
- package/src/utils.ts +10 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@durable-streams/client",
|
|
3
3
|
"description": "TypeScript client for the Durable Streams protocol",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"author": "Durable Stream contributors",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"fast-check": "^4.4.0",
|
|
49
49
|
"tsdown": "^0.9.0",
|
|
50
|
-
"@durable-streams/server": "0.1
|
|
50
|
+
"@durable-streams/server": "0.2.1"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=18.0.0"
|
package/src/constants.ts
CHANGED
|
@@ -26,6 +26,12 @@ export const STREAM_CURSOR_HEADER = `Stream-Cursor`
|
|
|
26
26
|
*/
|
|
27
27
|
export const STREAM_UP_TO_DATE_HEADER = `Stream-Up-To-Date`
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Response/request header indicating stream is closed (EOF).
|
|
31
|
+
* When present with value "true", the stream is permanently closed.
|
|
32
|
+
*/
|
|
33
|
+
export const STREAM_CLOSED_HEADER = `Stream-Closed`
|
|
34
|
+
|
|
29
35
|
// ============================================================================
|
|
30
36
|
// Request Headers
|
|
31
37
|
// ============================================================================
|
|
@@ -97,6 +103,11 @@ export const LIVE_QUERY_PARAM = `live`
|
|
|
97
103
|
*/
|
|
98
104
|
export const CURSOR_QUERY_PARAM = `cursor`
|
|
99
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Response header indicating SSE data encoding (e.g., base64 for binary streams).
|
|
108
|
+
*/
|
|
109
|
+
export const STREAM_SSE_DATA_ENCODING_HEADER = `stream-sse-data-encoding`
|
|
110
|
+
|
|
100
111
|
// ============================================================================
|
|
101
112
|
// SSE Control Event Fields (camelCase per PROTOCOL.md Section 5.7)
|
|
102
113
|
// ============================================================================
|
|
@@ -113,13 +124,19 @@ export const SSE_OFFSET_FIELD = `streamNextOffset`
|
|
|
113
124
|
*/
|
|
114
125
|
export const SSE_CURSOR_FIELD = `streamCursor`
|
|
115
126
|
|
|
127
|
+
/**
|
|
128
|
+
* SSE control event field for stream closed state.
|
|
129
|
+
* Note: Different from HTTP header name (camelCase vs Header-Case).
|
|
130
|
+
*/
|
|
131
|
+
export const SSE_CLOSED_FIELD = `streamClosed`
|
|
132
|
+
|
|
116
133
|
// ============================================================================
|
|
117
134
|
// Internal Constants
|
|
118
135
|
// ============================================================================
|
|
119
136
|
|
|
120
137
|
/**
|
|
121
|
-
* Content types that
|
|
122
|
-
*
|
|
138
|
+
* Content types that are natively compatible with SSE (UTF-8 text).
|
|
139
|
+
* Binary content types are also supported via automatic base64 encoding.
|
|
123
140
|
*/
|
|
124
141
|
export const SSE_COMPATIBLE_CONTENT_TYPES: ReadonlyArray<string> = [
|
|
125
142
|
`text/`,
|
package/src/error.ts
CHANGED
|
@@ -178,6 +178,26 @@ export class MissingStreamUrlError extends Error {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Error thrown when attempting to append to a closed stream.
|
|
183
|
+
*/
|
|
184
|
+
export class StreamClosedError extends DurableStreamError {
|
|
185
|
+
readonly code = `STREAM_CLOSED` as const
|
|
186
|
+
readonly status = 409
|
|
187
|
+
readonly streamClosed = true
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The final offset of the stream, if available from the response.
|
|
191
|
+
*/
|
|
192
|
+
readonly finalOffset?: string
|
|
193
|
+
|
|
194
|
+
constructor(url?: string, finalOffset?: string) {
|
|
195
|
+
super(`Cannot append to closed stream`, `STREAM_CLOSED`, 409, url)
|
|
196
|
+
this.name = `StreamClosedError`
|
|
197
|
+
this.finalOffset = finalOffset
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
181
201
|
/**
|
|
182
202
|
* Error thrown when signal option is invalid.
|
|
183
203
|
*/
|
|
@@ -17,11 +17,12 @@ import {
|
|
|
17
17
|
PRODUCER_ID_HEADER,
|
|
18
18
|
PRODUCER_RECEIVED_SEQ_HEADER,
|
|
19
19
|
PRODUCER_SEQ_HEADER,
|
|
20
|
+
STREAM_CLOSED_HEADER,
|
|
20
21
|
STREAM_OFFSET_HEADER,
|
|
21
22
|
} from "./constants"
|
|
22
23
|
import type { queueAsPromised } from "fastq"
|
|
23
24
|
import type { DurableStream } from "./stream"
|
|
24
|
-
import type { IdempotentProducerOptions, Offset } from "./types"
|
|
25
|
+
import type { CloseResult, IdempotentProducerOptions, Offset } from "./types"
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Error thrown when a producer's epoch is stale (zombie fencing).
|
|
@@ -76,12 +77,9 @@ function normalizeContentType(contentType: string | undefined): string {
|
|
|
76
77
|
|
|
77
78
|
/**
|
|
78
79
|
* Internal type for pending batch entries.
|
|
79
|
-
* Stores original data for proper JSON batching.
|
|
80
80
|
*/
|
|
81
81
|
interface PendingEntry {
|
|
82
|
-
/**
|
|
83
|
-
data: unknown
|
|
84
|
-
/** Encoded bytes for byte-stream mode */
|
|
82
|
+
/** Encoded bytes */
|
|
85
83
|
body: Uint8Array
|
|
86
84
|
}
|
|
87
85
|
|
|
@@ -141,6 +139,8 @@ export class IdempotentProducer {
|
|
|
141
139
|
readonly #queue: queueAsPromised<BatchTask>
|
|
142
140
|
readonly #maxInFlight: number
|
|
143
141
|
#closed = false
|
|
142
|
+
#closeResult: CloseResult | null = null
|
|
143
|
+
#pendingFinalMessage?: Uint8Array | string
|
|
144
144
|
|
|
145
145
|
// When autoClaim is true, we must wait for the first batch to complete
|
|
146
146
|
// before allowing pipelining (to know what epoch was claimed)
|
|
@@ -173,18 +173,37 @@ export class IdempotentProducer {
|
|
|
173
173
|
producerId: string,
|
|
174
174
|
opts?: IdempotentProducerOptions
|
|
175
175
|
) {
|
|
176
|
+
// Validate inputs
|
|
177
|
+
const epoch = opts?.epoch ?? 0
|
|
178
|
+
const maxBatchBytes = opts?.maxBatchBytes ?? 1024 * 1024 // 1MB
|
|
179
|
+
const maxInFlight = opts?.maxInFlight ?? 5
|
|
180
|
+
const lingerMs = opts?.lingerMs ?? 5
|
|
181
|
+
|
|
182
|
+
if (epoch < 0) {
|
|
183
|
+
throw new Error(`epoch must be >= 0`)
|
|
184
|
+
}
|
|
185
|
+
if (maxBatchBytes <= 0) {
|
|
186
|
+
throw new Error(`maxBatchBytes must be > 0`)
|
|
187
|
+
}
|
|
188
|
+
if (maxInFlight <= 0) {
|
|
189
|
+
throw new Error(`maxInFlight must be > 0`)
|
|
190
|
+
}
|
|
191
|
+
if (lingerMs < 0) {
|
|
192
|
+
throw new Error(`lingerMs must be >= 0`)
|
|
193
|
+
}
|
|
194
|
+
|
|
176
195
|
this.#stream = stream
|
|
177
196
|
this.#producerId = producerId
|
|
178
|
-
this.#epoch =
|
|
197
|
+
this.#epoch = epoch
|
|
179
198
|
this.#autoClaim = opts?.autoClaim ?? false
|
|
180
|
-
this.#maxBatchBytes =
|
|
181
|
-
this.#lingerMs =
|
|
199
|
+
this.#maxBatchBytes = maxBatchBytes
|
|
200
|
+
this.#lingerMs = lingerMs
|
|
182
201
|
this.#signal = opts?.signal
|
|
183
202
|
this.#onError = opts?.onError
|
|
184
203
|
this.#fetchClient =
|
|
185
204
|
opts?.fetch ?? ((...args: Parameters<typeof fetch>) => fetch(...args))
|
|
186
205
|
|
|
187
|
-
this.#maxInFlight =
|
|
206
|
+
this.#maxInFlight = maxInFlight
|
|
188
207
|
|
|
189
208
|
// When autoClaim is true, epoch is not yet known until first batch completes
|
|
190
209
|
// We block pipelining until then to avoid racing with the claim
|
|
@@ -224,12 +243,22 @@ export class IdempotentProducer {
|
|
|
224
243
|
* Errors are reported via onError callback if configured. Use flush() to
|
|
225
244
|
* wait for all pending messages to be sent.
|
|
226
245
|
*
|
|
227
|
-
* For JSON streams, pass
|
|
246
|
+
* For JSON streams, pass pre-serialized JSON strings.
|
|
228
247
|
* For byte streams, pass string or Uint8Array.
|
|
229
248
|
*
|
|
230
|
-
* @param body - Data to append (
|
|
249
|
+
* @param body - Data to append (string or Uint8Array)
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```typescript
|
|
253
|
+
* // JSON stream
|
|
254
|
+
* producer.append(JSON.stringify({ message: "hello" }));
|
|
255
|
+
*
|
|
256
|
+
* // Byte stream
|
|
257
|
+
* producer.append("raw text data");
|
|
258
|
+
* producer.append(new Uint8Array([1, 2, 3]));
|
|
259
|
+
* ```
|
|
231
260
|
*/
|
|
232
|
-
append(body: Uint8Array | string
|
|
261
|
+
append(body: Uint8Array | string): void {
|
|
233
262
|
if (this.#closed) {
|
|
234
263
|
throw new DurableStreamError(
|
|
235
264
|
`Producer is closed`,
|
|
@@ -239,35 +268,21 @@ export class IdempotentProducer {
|
|
|
239
268
|
)
|
|
240
269
|
}
|
|
241
270
|
|
|
242
|
-
const isJson =
|
|
243
|
-
normalizeContentType(this.#stream.contentType) === `application/json`
|
|
244
|
-
|
|
245
271
|
let bytes: Uint8Array
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
const json = JSON.stringify(body)
|
|
251
|
-
bytes = new TextEncoder().encode(json)
|
|
252
|
-
data = body
|
|
272
|
+
if (typeof body === `string`) {
|
|
273
|
+
bytes = new TextEncoder().encode(body)
|
|
274
|
+
} else if (body instanceof Uint8Array) {
|
|
275
|
+
bytes = body
|
|
253
276
|
} else {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
throw new DurableStreamError(
|
|
261
|
-
`Non-JSON streams require string or Uint8Array`,
|
|
262
|
-
`BAD_REQUEST`,
|
|
263
|
-
400,
|
|
264
|
-
undefined
|
|
265
|
-
)
|
|
266
|
-
}
|
|
267
|
-
data = bytes
|
|
277
|
+
throw new DurableStreamError(
|
|
278
|
+
`append() requires string or Uint8Array. For objects, use JSON.stringify().`,
|
|
279
|
+
`BAD_REQUEST`,
|
|
280
|
+
400,
|
|
281
|
+
undefined
|
|
282
|
+
)
|
|
268
283
|
}
|
|
269
284
|
|
|
270
|
-
this.#pendingBatch.push({
|
|
285
|
+
this.#pendingBatch.push({ body: bytes })
|
|
271
286
|
this.#batchBytes += bytes.length
|
|
272
287
|
|
|
273
288
|
// Check if batch should be sent immediately
|
|
@@ -306,11 +321,17 @@ export class IdempotentProducer {
|
|
|
306
321
|
}
|
|
307
322
|
|
|
308
323
|
/**
|
|
309
|
-
*
|
|
324
|
+
* Stop the producer without closing the underlying stream.
|
|
310
325
|
*
|
|
311
|
-
*
|
|
326
|
+
* Use this when you want to:
|
|
327
|
+
* - Hand off writing to another producer
|
|
328
|
+
* - Keep the stream open for future writes
|
|
329
|
+
* - Stop this producer but not signal EOF to readers
|
|
330
|
+
*
|
|
331
|
+
* Flushes any pending messages before detaching.
|
|
332
|
+
* After calling detach(), further append() calls will throw.
|
|
312
333
|
*/
|
|
313
|
-
async
|
|
334
|
+
async detach(): Promise<void> {
|
|
314
335
|
if (this.#closed) return
|
|
315
336
|
|
|
316
337
|
this.#closed = true
|
|
@@ -318,10 +339,140 @@ export class IdempotentProducer {
|
|
|
318
339
|
try {
|
|
319
340
|
await this.flush()
|
|
320
341
|
} catch {
|
|
321
|
-
// Ignore errors during
|
|
342
|
+
// Ignore errors during detach
|
|
322
343
|
}
|
|
323
344
|
}
|
|
324
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Flush pending messages and close the underlying stream (EOF).
|
|
348
|
+
*
|
|
349
|
+
* This is the typical way to end a producer session. It:
|
|
350
|
+
* 1. Flushes all pending messages
|
|
351
|
+
* 2. Optionally appends a final message
|
|
352
|
+
* 3. Closes the stream (no further appends permitted)
|
|
353
|
+
*
|
|
354
|
+
* **Idempotent**: Unlike `DurableStream.close({ body })`, this method is
|
|
355
|
+
* idempotent even with a final message because it uses producer headers
|
|
356
|
+
* for deduplication. Safe to retry on network failures.
|
|
357
|
+
*
|
|
358
|
+
* @param finalMessage - Optional final message to append atomically with close
|
|
359
|
+
* @returns CloseResult with the final offset
|
|
360
|
+
*/
|
|
361
|
+
async close(finalMessage?: Uint8Array | string): Promise<CloseResult> {
|
|
362
|
+
if (this.#closed) {
|
|
363
|
+
// Already closed - return cached result for idempotency
|
|
364
|
+
if (this.#closeResult) {
|
|
365
|
+
return this.#closeResult
|
|
366
|
+
}
|
|
367
|
+
// Retry path: flush() threw on a previous attempt, so we need to re-run
|
|
368
|
+
// the entire close sequence with the stored finalMessage
|
|
369
|
+
await this.flush()
|
|
370
|
+
const result = await this.#doClose(this.#pendingFinalMessage)
|
|
371
|
+
this.#closeResult = result
|
|
372
|
+
return result
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.#closed = true
|
|
376
|
+
|
|
377
|
+
// Store finalMessage for retry safety (if flush() throws, we can retry)
|
|
378
|
+
this.#pendingFinalMessage = finalMessage
|
|
379
|
+
|
|
380
|
+
// Flush pending messages first
|
|
381
|
+
await this.flush()
|
|
382
|
+
|
|
383
|
+
// Close the stream with optional final message
|
|
384
|
+
const result = await this.#doClose(finalMessage)
|
|
385
|
+
this.#closeResult = result
|
|
386
|
+
return result
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Actually close the stream with optional final message.
|
|
391
|
+
* Uses producer headers for idempotency.
|
|
392
|
+
*/
|
|
393
|
+
async #doClose(finalMessage?: Uint8Array | string): Promise<CloseResult> {
|
|
394
|
+
const contentType = this.#stream.contentType ?? `application/octet-stream`
|
|
395
|
+
const isJson = normalizeContentType(contentType) === `application/json`
|
|
396
|
+
|
|
397
|
+
// Build body if final message is provided
|
|
398
|
+
let body: BodyInit | undefined
|
|
399
|
+
if (finalMessage !== undefined) {
|
|
400
|
+
const bodyBytes =
|
|
401
|
+
typeof finalMessage === `string`
|
|
402
|
+
? new TextEncoder().encode(finalMessage)
|
|
403
|
+
: finalMessage
|
|
404
|
+
|
|
405
|
+
if (isJson) {
|
|
406
|
+
// For JSON mode, wrap in array
|
|
407
|
+
const jsonStr = new TextDecoder().decode(bodyBytes)
|
|
408
|
+
body = `[${jsonStr}]`
|
|
409
|
+
} else {
|
|
410
|
+
body = bodyBytes as unknown as BodyInit
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Capture the sequence number for this request (for retry safety)
|
|
415
|
+
// We only increment #nextSeq after a successful response
|
|
416
|
+
const seqForThisRequest = this.#nextSeq
|
|
417
|
+
|
|
418
|
+
// Build headers with producer info and Stream-Closed
|
|
419
|
+
const headers: Record<string, string> = {
|
|
420
|
+
"content-type": contentType,
|
|
421
|
+
[PRODUCER_ID_HEADER]: this.#producerId,
|
|
422
|
+
[PRODUCER_EPOCH_HEADER]: this.#epoch.toString(),
|
|
423
|
+
[PRODUCER_SEQ_HEADER]: seqForThisRequest.toString(),
|
|
424
|
+
[STREAM_CLOSED_HEADER]: `true`,
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const response = await this.#fetchClient(this.#stream.url, {
|
|
428
|
+
method: `POST`,
|
|
429
|
+
headers,
|
|
430
|
+
body,
|
|
431
|
+
signal: this.#signal,
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
// Handle 204 (duplicate close - idempotent success)
|
|
435
|
+
if (response.status === 204) {
|
|
436
|
+
// Only increment seq on success (retry-safe)
|
|
437
|
+
this.#nextSeq = seqForThisRequest + 1
|
|
438
|
+
const finalOffset = response.headers.get(STREAM_OFFSET_HEADER) ?? ``
|
|
439
|
+
return { finalOffset }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Handle success
|
|
443
|
+
if (response.status === 200) {
|
|
444
|
+
// Only increment seq on success (retry-safe)
|
|
445
|
+
this.#nextSeq = seqForThisRequest + 1
|
|
446
|
+
const finalOffset = response.headers.get(STREAM_OFFSET_HEADER) ?? ``
|
|
447
|
+
return { finalOffset }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Handle errors
|
|
451
|
+
if (response.status === 403) {
|
|
452
|
+
// Stale epoch
|
|
453
|
+
const currentEpochStr = response.headers.get(PRODUCER_EPOCH_HEADER)
|
|
454
|
+
const currentEpoch = currentEpochStr
|
|
455
|
+
? parseInt(currentEpochStr, 10)
|
|
456
|
+
: this.#epoch
|
|
457
|
+
|
|
458
|
+
if (this.#autoClaim) {
|
|
459
|
+
// Auto-claim: retry with epoch+1
|
|
460
|
+
const newEpoch = currentEpoch + 1
|
|
461
|
+
this.#epoch = newEpoch
|
|
462
|
+
// Reset sequence for new epoch - set to 0 so the recursive call uses seq 0
|
|
463
|
+
// (the first operation in a new epoch should be seq 0)
|
|
464
|
+
this.#nextSeq = 0
|
|
465
|
+
return this.#doClose(finalMessage)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
throw new StaleEpochError(currentEpoch)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Other errors
|
|
472
|
+
const error = await FetchError.fromResponse(response, this.#stream.url)
|
|
473
|
+
throw error
|
|
474
|
+
}
|
|
475
|
+
|
|
325
476
|
/**
|
|
326
477
|
* Increment epoch and reset sequence.
|
|
327
478
|
*
|
|
@@ -523,8 +674,9 @@ export class IdempotentProducer {
|
|
|
523
674
|
// For JSON mode: always send as array (server flattens one level)
|
|
524
675
|
// Single append: [value] → server stores value
|
|
525
676
|
// Multiple appends: [val1, val2] → server stores val1, val2
|
|
526
|
-
|
|
527
|
-
|
|
677
|
+
// Input is pre-serialized JSON strings, join them into an array
|
|
678
|
+
const jsonStrings = batch.map((e) => new TextDecoder().decode(e.body))
|
|
679
|
+
batchedBody = `[${jsonStrings.join(`,`)}]`
|
|
528
680
|
} else {
|
|
529
681
|
// For byte mode: concatenate all chunks
|
|
530
682
|
const totalSize = batch.reduce((sum, e) => sum + e.body.length, 0)
|
package/src/index.ts
CHANGED
|
@@ -65,6 +65,10 @@ export type {
|
|
|
65
65
|
HeadResult,
|
|
66
66
|
LegacyLiveMode,
|
|
67
67
|
|
|
68
|
+
// Close types
|
|
69
|
+
CloseResult,
|
|
70
|
+
CloseOptions,
|
|
71
|
+
|
|
68
72
|
// Idempotent producer types
|
|
69
73
|
IdempotentProducerOptions,
|
|
70
74
|
IdempotentAppendResult,
|
|
@@ -89,6 +93,7 @@ export {
|
|
|
89
93
|
DurableStreamError,
|
|
90
94
|
MissingStreamUrlError,
|
|
91
95
|
InvalidSignalError,
|
|
96
|
+
StreamClosedError,
|
|
92
97
|
} from "./error"
|
|
93
98
|
|
|
94
99
|
// ============================================================================
|
|
@@ -110,6 +115,7 @@ export {
|
|
|
110
115
|
STREAM_OFFSET_HEADER,
|
|
111
116
|
STREAM_CURSOR_HEADER,
|
|
112
117
|
STREAM_UP_TO_DATE_HEADER,
|
|
118
|
+
STREAM_CLOSED_HEADER,
|
|
113
119
|
STREAM_SEQ_HEADER,
|
|
114
120
|
STREAM_TTL_HEADER,
|
|
115
121
|
STREAM_EXPIRES_AT_HEADER,
|
|
@@ -117,6 +123,7 @@ export {
|
|
|
117
123
|
LIVE_QUERY_PARAM,
|
|
118
124
|
CURSOR_QUERY_PARAM,
|
|
119
125
|
SSE_COMPATIBLE_CONTENT_TYPES,
|
|
126
|
+
SSE_CLOSED_FIELD,
|
|
120
127
|
DURABLE_STREAM_PROTOCOL_QUERY_PARAMS,
|
|
121
128
|
// Idempotent producer headers
|
|
122
129
|
PRODUCER_ID_HEADER,
|