queen-mq 2.1.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,211 @@
1
+ /**
2
+ * The framed pop answer: what a broker sends for a pop with `frames=true`.
3
+ * The messages come as the stored records, each still the zstd frame the
4
+ * producer (or the broker) made, and are unpacked here.
5
+ *
6
+ * "QPF1" | u8 1 | u32 len, the answer's own keys as one JSON object
7
+ * u16 len, queue | u16 len, leaseId | u16 len, consumerGroup | u32 claims
8
+ * per claim: u64 partitionId | u16 len, partition | u64 start | u64 end | u32 deliveryAttempt | u32 records
9
+ * per record: u64 base_offset | i64 created_at_us | u32 count | u8 codec (0 raw, 1 zstd) | u32 len | bytes
10
+ *
11
+ * All integers little-endian. A record's bytes are `count` frames:
12
+ *
13
+ * u32 body_len | u8 flags | u8[16] message id | [u8[16] trace id, flag 1]
14
+ * | u16 len, transactionId | [u16 len, producer, flag 2] | payload
15
+ *
16
+ * Message i of a record has offset base_offset+i and belongs to the delivery
17
+ * when that lies in the claim's start..=end. The JSON object in front holds what
18
+ * the JSON answer has beside its messages: the conflation echo and the
19
+ * autopilot's choice.
20
+ */
21
+
22
+ import { uuidText } from './pushBinary.js'
23
+ import { zstdDecompress } from './zstd.js'
24
+
25
+ export const POP_FRAMES_CONTENT_TYPE = 'application/x-queen-pop'
26
+
27
+ const MAGIC = 'QPF1'
28
+ const FLAG_TRACE = 1
29
+ const FLAG_PRODUCER = 2
30
+ const FLAG_ENCRYPTED = 4
31
+
32
+ const utf8 = new TextDecoder()
33
+
34
+ function damaged(what) {
35
+ return new Error(`framed pop answer: ${what}`)
36
+ }
37
+
38
+ /** Reads little-endian fields off a byte array; a read past the end throws. */
39
+ class In {
40
+ constructor(bytes, short = 'truncated') {
41
+ this.bytes = bytes
42
+ this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
43
+ this.at = 0
44
+ this.short = short
45
+ }
46
+
47
+ take(length) {
48
+ if (this.bytes.length - this.at < length) throw damaged(this.short)
49
+ const slice = this.bytes.subarray(this.at, this.at + length)
50
+ this.at += length
51
+ return slice
52
+ }
53
+
54
+ u8() {
55
+ return this.take(1)[0]
56
+ }
57
+
58
+ u16() {
59
+ this.take(2)
60
+ return this.view.getUint16(this.at - 2, true)
61
+ }
62
+
63
+ u32() {
64
+ this.take(4)
65
+ return this.view.getUint32(this.at - 4, true)
66
+ }
67
+
68
+ u64() {
69
+ this.take(8)
70
+ return this.view.getBigUint64(this.at - 8, true)
71
+ }
72
+
73
+ i64() {
74
+ this.take(8)
75
+ return this.view.getBigInt64(this.at - 8, true)
76
+ }
77
+
78
+ text() {
79
+ return utf8.decode(this.take(this.u16()))
80
+ }
81
+
82
+ rest() {
83
+ return this.bytes.subarray(this.at)
84
+ }
85
+ }
86
+
87
+ /** Whether a pop answer's bytes are the framed one. */
88
+ export function isPopFrames(bytes) {
89
+ if (!bytes || bytes.length < MAGIC.length + 1) return false
90
+ for (let i = 0; i < MAGIC.length; i++) {
91
+ if (bytes[i] !== MAGIC.charCodeAt(i)) return false
92
+ }
93
+ return true
94
+ }
95
+
96
+ /**
97
+ * Unpack a framed pop answer into the object the JSON answer parses to:
98
+ *
99
+ * { success, queue, partition, partitionId, leaseId, consumerGroup,
100
+ * messages: [{ id, transactionId, traceId, data, producerSub, createdAt,
101
+ * partitionId, partition, leaseId, consumerGroup,
102
+ * deliveryAttempt, offset }],
103
+ * partitionsClaimed, ...the answer's own keys }
104
+ *
105
+ * The messages are in delivery order. Everything that reads a pop answer
106
+ * (the conflation check, the autopilot's choice, the consumer loop) reads
107
+ * this object as it reads the JSON one.
108
+ *
109
+ * @param {Uint8Array} bytes
110
+ * @returns {Promise<object>}
111
+ */
112
+ export async function decodePopFrames(bytes) {
113
+ if (!isPopFrames(bytes)) throw damaged('not a framed pop answer')
114
+ const input = new In(bytes)
115
+ input.take(MAGIC.length)
116
+ const version = input.u8()
117
+ let own = null
118
+ if (version === 1) {
119
+ const text = utf8.decode(input.take(input.u32()))
120
+ if (text.length > 0) {
121
+ try {
122
+ own = JSON.parse(text)
123
+ } catch (error) {
124
+ throw damaged(`its own keys: ${error.message}`)
125
+ }
126
+ }
127
+ } else if (version !== 0) {
128
+ throw damaged(`unknown version ${version}`)
129
+ }
130
+
131
+ const queue = input.text()
132
+ const leaseId = input.text()
133
+ const consumerGroup = input.text()
134
+ const claims = input.u32()
135
+ const messages = []
136
+ let firstPartition = ''
137
+ let firstPartitionId = ''
138
+
139
+ for (let c = 0; c < claims; c++) {
140
+ const partitionId = input.u64().toString()
141
+ const partition = input.text()
142
+ const start = input.u64()
143
+ const end = input.u64()
144
+ const deliveryAttempt = input.u32()
145
+ const records = input.u32()
146
+ if (c === 0) {
147
+ firstPartition = partition
148
+ firstPartitionId = partitionId
149
+ }
150
+ for (let r = 0; r < records; r++) {
151
+ const base = input.u64()
152
+ const createdAt = new Date(Number(input.i64() / 1000n)).toISOString()
153
+ const count = input.u32()
154
+ const codec = input.u8()
155
+ let raw = input.take(input.u32())
156
+ if (codec === 1) {
157
+ try {
158
+ raw = await zstdDecompress(raw)
159
+ } catch (error) {
160
+ throw damaged(`zstd: ${error.message}`)
161
+ }
162
+ } else if (codec !== 0) {
163
+ throw damaged(`unknown codec ${codec}`)
164
+ }
165
+ const frames = new In(raw, `a record is shorter than its ${count} frames`)
166
+ for (let i = 0; i < count; i++) {
167
+ const frame = new In(frames.take(frames.u32()), 'a frame is malformed')
168
+ const flags = frame.u8()
169
+ const id = frame.take(16)
170
+ const trace = flags & FLAG_TRACE ? frame.take(16) : null
171
+ const transactionId = frame.text()
172
+ const producerSub = flags & FLAG_PRODUCER ? frame.text() : null
173
+ if (flags & FLAG_ENCRYPTED) throw damaged('a frame is encrypted')
174
+ const offset = base + BigInt(i)
175
+ if (offset < start || offset > end) continue
176
+ const payload = frame.rest()
177
+ messages.push({
178
+ id: uuidText(id),
179
+ transactionId,
180
+ traceId: trace ? uuidText(trace) : null,
181
+ data: payload.length === 0 ? null : JSON.parse(utf8.decode(payload)),
182
+ producerSub,
183
+ createdAt,
184
+ partitionId,
185
+ partition,
186
+ leaseId,
187
+ consumerGroup,
188
+ deliveryAttempt,
189
+ offset: Number(offset)
190
+ })
191
+ }
192
+ }
193
+ }
194
+
195
+ const answer = {
196
+ success: true,
197
+ queue,
198
+ partition: firstPartition,
199
+ partitionId: firstPartitionId,
200
+ leaseId,
201
+ consumerGroup,
202
+ messages,
203
+ partitionsClaimed: claims
204
+ }
205
+ if (own && typeof own === 'object' && !Array.isArray(own)) {
206
+ // `frames` only says the answer is this one; the JSON answer has no such key.
207
+ const { frames: _frames, ...keys } = own
208
+ Object.assign(answer, keys)
209
+ }
210
+ return answer
211
+ }
@@ -0,0 +1,367 @@
1
+ /**
2
+ * The binary push body: an alternative to the JSON body of POST /api/v1/push.
3
+ *
4
+ * "QPB1" | u8 flags | [u32 raw_len, when zstd] | body
5
+ * body: u32 groups | per group: u16 len, queue | u16 len, partition | u32 n
6
+ * | n x ([u16 len, transactionId, when flag 4] | u32 len, payload)
7
+ *
8
+ * All integers little-endian. flags: 1 = the body is one zstd frame of raw_len
9
+ * bytes, 2 = answer with the counts only, 4 = every item carries its
10
+ * transaction id (empty = the broker mints it). No trace id. Each payload is
11
+ * one JSON document.
12
+ *
13
+ * With flag 8 (encodePushFrames) a group is the messages as the broker stores
14
+ * them, compressed here once:
15
+ *
16
+ * body: u32 groups | per group: u16 len, queue | u16 len, partition | u32 n | u32 z_len | z
17
+ * z: one zstd frame of n frames: u32 body_len | u8 0 | u8[16] message id
18
+ * | u16 len, transactionId | payload
19
+ *
20
+ * The message id is a version-7 UUID minted by the client; a message without
21
+ * its own transaction id uses the id's text. The broker checks every frame and
22
+ * keeps the compressed bytes as they are, so it does not compress them again.
23
+ *
24
+ * A broker that does not know this body answers 400; a client sends it only
25
+ * when `binaryPush` is set, so by default a push is the JSON request every
26
+ * broker knows.
27
+ *
28
+ * The binary transaction body (encodeTransactionFrames), an alternative to the
29
+ * JSON body of POST /api/v1/transaction, carries the pushes the same way:
30
+ *
31
+ * "QTB1" | u8 flags | u32 len, head | u32 groups | the groups, as with flag 8 above
32
+ *
33
+ * head is the JSON transaction body without its push operations: the acks in
34
+ * `operations`, `requiredLeases`, `kv`, `timers`. flags: 2 = the answer leaves
35
+ * out the pushes' and the acks' results and counts them (`pushed`,
36
+ * `duplicate`, `acked`); the riders' results stay. In the answer the head's
37
+ * operations come first and the groups' messages after them, in the order
38
+ * written; the riders follow, as in the JSON body.
39
+ */
40
+
41
+ import { v7 as uuidv7 } from 'uuid'
42
+ import { zstdCompress } from './zstd.js'
43
+
44
+ export const PUSH_BINARY_CONTENT_TYPE = 'application/x-queen-push'
45
+
46
+ const MAGIC = Uint8Array.of(0x51, 0x50, 0x42, 0x31) // "QPB1"
47
+ const TXN_MAGIC = Uint8Array.of(0x51, 0x54, 0x42, 0x31) // "QTB1"
48
+ const FLAG_ZSTD = 1
49
+ const FLAG_COUNTS = 2
50
+ const FLAG_TXN = 4
51
+ const FLAG_FRAMES = 8
52
+
53
+ const utf8 = new TextEncoder()
54
+ const HEX = '0123456789abcdef'
55
+
56
+ /** A message does not fit the body: a name or transaction id over 65535 bytes. */
57
+ function tooLong(what) {
58
+ const error = new Error(`${what} longer than 65535 bytes`)
59
+ error.tooLong = true
60
+ return error
61
+ }
62
+
63
+ /** A growing byte buffer; strings are encoded straight into it. */
64
+ class Out {
65
+ constructor(size = 1024) {
66
+ this.bytes = new Uint8Array(size)
67
+ this.view = new DataView(this.bytes.buffer)
68
+ this.length = 0
69
+ }
70
+
71
+ need(extra) {
72
+ if (this.length + extra <= this.bytes.length) return
73
+ const grown = new Uint8Array(Math.max(this.bytes.length * 2, this.length + extra))
74
+ grown.set(this.bytes.subarray(0, this.length))
75
+ this.bytes = grown
76
+ this.view = new DataView(grown.buffer)
77
+ }
78
+
79
+ u8(value) {
80
+ this.need(1)
81
+ this.bytes[this.length++] = value
82
+ }
83
+
84
+ u32(value) {
85
+ this.need(4)
86
+ this.view.setUint32(this.length, value, true)
87
+ this.length += 4
88
+ }
89
+
90
+ /** Four bytes to fill in later with `setU32`; returns where they are. */
91
+ reserveU32() {
92
+ const at = this.length
93
+ this.u32(0)
94
+ return at
95
+ }
96
+
97
+ setU32(at, value) {
98
+ this.view.setUint32(at, value, true)
99
+ }
100
+
101
+ raw(bytes) {
102
+ this.need(bytes.length)
103
+ this.bytes.set(bytes, this.length)
104
+ this.length += bytes.length
105
+ }
106
+
107
+ /** `text` as UTF-8, no length in front; returns its length in bytes. */
108
+ text(text) {
109
+ this.need(text.length * 3)
110
+ const { written } = utf8.encodeInto(text, this.bytes.subarray(this.length))
111
+ this.length += written
112
+ return written
113
+ }
114
+
115
+ /** `text` with its u16 length in front. */
116
+ text16(text, what) {
117
+ this.need(2)
118
+ const at = this.length
119
+ this.length += 2
120
+ const written = this.text(text)
121
+ if (written > 0xffff) throw tooLong(what)
122
+ this.view.setUint16(at, written, true)
123
+ }
124
+
125
+ /** `text` with its u32 length in front. */
126
+ text32(text) {
127
+ const at = this.reserveU32()
128
+ this.setU32(at, this.text(text))
129
+ }
130
+
131
+ done() {
132
+ return this.bytes.subarray(0, this.length)
133
+ }
134
+ }
135
+
136
+ function checkBatch(batch) {
137
+ if (batch.transactionIds && batch.transactionIds.length !== batch.payloads.length) {
138
+ throw new Error(`${batch.transactionIds.length} transaction ids for ${batch.payloads.length} payloads`)
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Encode batches as a binary push body.
144
+ *
145
+ * @param {Array<{queue: string, partition: string, payloads: string[], transactionIds?: string[]}>} batches
146
+ * one partition's messages each; a payload is the text of one JSON document,
147
+ * and transactionIds, when given, holds one per payload ('' = the broker
148
+ * mints it)
149
+ * @param {object} [options]
150
+ * @param {boolean} [options.compress=false] the body is zstd-compressed
151
+ * @param {boolean} [options.counts=false] the broker answers
152
+ * `{count, queued, duplicate, error}` instead of one object per message
153
+ * @returns {Promise<Uint8Array>}
154
+ */
155
+ export async function encodePushBinary(batches, { compress = false, counts = false } = {}) {
156
+ const withTxn = batches.some(batch => batch.transactionIds)
157
+ const flags = (counts ? FLAG_COUNTS : 0) | (withTxn ? FLAG_TXN : 0)
158
+ const body = new Out()
159
+ body.u32(batches.length)
160
+ for (const batch of batches) {
161
+ checkBatch(batch)
162
+ body.text16(batch.queue, 'queue name')
163
+ body.text16(batch.partition, 'partition name')
164
+ body.u32(batch.payloads.length)
165
+ for (let i = 0; i < batch.payloads.length; i++) {
166
+ if (withTxn) body.text16(batch.transactionIds ? batch.transactionIds[i] : '', 'transaction id')
167
+ body.text32(batch.payloads[i])
168
+ }
169
+ }
170
+
171
+ const out = new Out(MAGIC.length + 5 + (compress ? 0 : body.length))
172
+ out.raw(MAGIC)
173
+ if (!compress) {
174
+ out.u8(flags)
175
+ out.raw(body.done())
176
+ return out.done()
177
+ }
178
+ out.u8(flags | FLAG_ZSTD)
179
+ out.u32(body.length)
180
+ out.raw(await zstdCompress(body.done()))
181
+ return out.done()
182
+ }
183
+
184
+ /**
185
+ * Encode batches as a binary push body whose groups are the messages framed
186
+ * as the broker stores them and compressed once, here. Same batches as
187
+ * `encodePushBinary`; `counts` asks for the short answer.
188
+ *
189
+ * @returns {Promise<Uint8Array>}
190
+ */
191
+ export async function encodePushFrames(batches, { counts = false } = {}) {
192
+ const out = new Out()
193
+ out.raw(MAGIC)
194
+ out.u8(FLAG_FRAMES | (counts ? FLAG_COUNTS : 0))
195
+ return appendFrameGroups(out, batches)
196
+ }
197
+
198
+ /**
199
+ * Encode a binary transaction body: `head`, the JSON transaction body without
200
+ * its push operations (its text; empty for none), then batches as groups of
201
+ * messages framed as the broker stores them and compressed once, here.
202
+ * `counts`: the broker counts the pushes and the acks in its answer instead of
203
+ * rendering one result for each.
204
+ *
205
+ * @returns {Promise<Uint8Array>}
206
+ */
207
+ export async function encodeTransactionFrames(head, batches, { counts = false } = {}) {
208
+ const out = new Out()
209
+ out.raw(TXN_MAGIC)
210
+ out.u8(counts ? FLAG_COUNTS : 0)
211
+ out.text32(head || '')
212
+ return appendFrameGroups(out, batches)
213
+ }
214
+
215
+ /**
216
+ * Appends `u32 groups | per group: u16 len, queue | u16 len, partition | u32 n
217
+ * | u32 z_len | z` to `out`: each batch's messages framed as the broker stores
218
+ * them, with a version-7 id minted here, as one zstd frame.
219
+ */
220
+ async function appendFrameGroups(out, batches) {
221
+ out.u32(batches.length)
222
+ const frames = new Out()
223
+ for (const batch of batches) {
224
+ checkBatch(batch)
225
+ frames.length = 0
226
+ for (let i = 0; i < batch.payloads.length; i++) {
227
+ const bodyLength = frames.reserveU32()
228
+ frames.u8(0)
229
+ frames.need(16)
230
+ const id = frames.length
231
+ uuidv7(undefined, frames.bytes, id)
232
+ frames.length += 16
233
+ const txn = batch.transactionIds ? batch.transactionIds[i] : ''
234
+ frames.text16(txn || uuidText(frames.bytes, id), 'transaction id')
235
+ frames.text(batch.payloads[i])
236
+ frames.setU32(bodyLength, frames.length - bodyLength - 4)
237
+ }
238
+ out.text16(batch.queue, 'queue name')
239
+ out.text16(batch.partition, 'partition name')
240
+ out.u32(batch.payloads.length)
241
+ const compressed = await zstdCompress(frames.done())
242
+ out.u32(compressed.length)
243
+ out.raw(compressed)
244
+ }
245
+ return out.done()
246
+ }
247
+
248
+ /** The lower-case hyphenated text of the 16 id bytes at `at`. */
249
+ export function uuidText(bytes, at = 0) {
250
+ let text = ''
251
+ for (let i = 0; i < 16; i++) {
252
+ if (i === 4 || i === 6 || i === 8 || i === 10) text += '-'
253
+ const byte = bytes[at + i]
254
+ text += HEX[byte >> 4] + HEX[byte & 0x0f]
255
+ }
256
+ return text
257
+ }
258
+
259
+ /**
260
+ * The items of a JSON push (`{queue, partition, payload, transactionId}`) as a
261
+ * binary push body, or null when one of them cannot travel in it: a trace id,
262
+ * a transaction id that is not a string, a payload JSON does not carry, a name
263
+ * over 65535 bytes. The caller then sends the JSON body, and the broker
264
+ * answers that as it always did. Consecutive items of one partition share a
265
+ * group, so the order of the items is the order on the wire.
266
+ *
267
+ * `compress`: the messages framed and compressed here, kept by the broker as
268
+ * they are.
269
+ */
270
+ export async function binaryPushBody(items, compress) {
271
+ if (items.length === 0) return null
272
+ const batches = []
273
+ for (const item of items) {
274
+ if (!addItem(batches, item)) return null
275
+ }
276
+ try {
277
+ return compress ? await encodePushFrames(batches) : await encodePushBinary(batches)
278
+ } catch (error) {
279
+ // A name too long for the body: the JSON push says so in the broker's words.
280
+ if (error.tooLong) return null
281
+ throw error
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Adds one pushed item to `batches`, in the last one when it is the same
287
+ * partition's. False when the item cannot travel in a binary body.
288
+ */
289
+ function addItem(batches, item) {
290
+ if (!item || typeof item !== 'object' || typeof item.queue !== 'string') return false
291
+ if (item.traceId !== undefined && item.traceId !== null) return false
292
+ const transactionId = item.transactionId ?? ''
293
+ if (typeof transactionId !== 'string') return false
294
+ const partition = item.partition ?? 'Default'
295
+ if (typeof partition !== 'string') return false
296
+ const payload = JSON.stringify(item.payload)
297
+ if (payload === undefined) return false
298
+ let last = batches[batches.length - 1]
299
+ if (!last || last.queue !== item.queue || last.partition !== partition) {
300
+ last = { queue: item.queue, partition, payloads: [], transactionIds: [] }
301
+ batches.push(last)
302
+ }
303
+ last.payloads.push(payload)
304
+ last.transactionIds.push(transactionId)
305
+ return true
306
+ }
307
+
308
+ /**
309
+ * A JSON transaction body (`{operations, requiredLeases, kv, timers}`) as the
310
+ * binary one, when its pushes can travel in it: there is at least one, and
311
+ * none carries a trace id. Everything but the pushes stays in the JSON head;
312
+ * consecutive pushed items of one partition share a group, so the pushes keep
313
+ * their order. Null otherwise: the caller then sends the JSON body.
314
+ *
315
+ * `order` is null when the broker's answer lists the operations as the caller
316
+ * added them (every other operation before every push). Otherwise `order[k]`
317
+ * is the place, among the caller's operations, of the k-th result the broker
318
+ * sends: it lists the head's operations first, then the pushes.
319
+ *
320
+ * @returns {Promise<{body: Uint8Array, order: number[]|null}|null>}
321
+ */
322
+ export async function transactionFrames(request) {
323
+ if (!request || typeof request !== 'object' || !Array.isArray(request.operations)) return null
324
+ const head = { ...request, operations: [] }
325
+ const batches = []
326
+ const headAt = []
327
+ const pushAt = []
328
+ let flat = 0
329
+ for (const operation of request.operations) {
330
+ if (!operation || operation.type !== 'push') {
331
+ head.operations.push(operation)
332
+ headAt.push(flat++)
333
+ continue
334
+ }
335
+ // A push names its items, or is one item itself.
336
+ const items = Array.isArray(operation.items) ? operation.items : [operation]
337
+ for (const item of items) {
338
+ if (!addItem(batches, item)) return null
339
+ pushAt.push(flat++)
340
+ }
341
+ }
342
+ if (batches.length === 0) return null
343
+ let body
344
+ try {
345
+ body = await encodeTransactionFrames(JSON.stringify(head), batches)
346
+ } catch (error) {
347
+ if (error.tooLong) return null
348
+ throw error
349
+ }
350
+ const order = [...headAt, ...pushAt]
351
+ return { body, order: order.every((at, k) => at === k) ? null : order }
352
+ }
353
+
354
+ /**
355
+ * Puts the results of a binary transaction back in the order the caller added
356
+ * its operations in, and gives each its own index again. The riders' results,
357
+ * after the operations', are already in place.
358
+ */
359
+ export function restoreResultOrder(answer, order) {
360
+ if (!order || !answer || !Array.isArray(answer.results) || answer.results.length < order.length) return
361
+ const results = answer.results.slice()
362
+ order.forEach((at, k) => {
363
+ const result = answer.results[k]
364
+ results[at] = result && typeof result === 'object' ? { ...result, index: at } : result
365
+ })
366
+ answer.results = results
367
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * zstd for the binary push body and the framed pop answer.
3
+ *
4
+ * It comes from node:zlib (Node 22.15 and later), loaded on first use through
5
+ * a guarded dynamic import, as HttpClient loads undici: a bundle for a runtime
6
+ * without it still loads, and fails only when a push is compressed or a framed
7
+ * pop holds a compressed record.
8
+ */
9
+
10
+ let loading = null
11
+
12
+ async function load() {
13
+ if (loading === null) {
14
+ loading = import('node:zlib').then(
15
+ (zlib) => (typeof zlib.zstdCompressSync === 'function' && typeof zlib.zstdDecompressSync === 'function' ? zlib : null),
16
+ () => null
17
+ )
18
+ }
19
+ const zlib = await loading
20
+ if (!zlib) {
21
+ throw new Error('zstd is not available in this runtime (node:zlib of Node 22.15 or later): pushCompression and framedPop need it')
22
+ }
23
+ return zlib
24
+ }
25
+
26
+ /**
27
+ * One zstd frame of `raw`, at the fastest level. The frame carries its content
28
+ * size, which the broker asks of a frame it stores as it came.
29
+ */
30
+ export async function zstdCompress(raw) {
31
+ const zlib = await load()
32
+ return zlib.zstdCompressSync(raw, {
33
+ params: { [zlib.constants.ZSTD_c_compressionLevel]: 1 },
34
+ pledgedSrcSize: raw.length
35
+ })
36
+ }
37
+
38
+ export async function zstdDecompress(frame) {
39
+ const zlib = await load()
40
+ return zlib.zstdDecompressSync(frame)
41
+ }