queen-mq 1.0.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -4
- package/client-v2/Queen.js +68 -0
- package/client-v2/admin/Admin.js +15 -1
- package/client-v2/buffer/BufferManager.js +18 -6
- package/client-v2/buffer/MessageBuffer.js +17 -1
- package/client-v2/buffer/sinks.js +89 -0
- package/client-v2/builders/QueueBuilder.js +175 -16
- package/client-v2/consumer/ConsumerManager.js +79 -12
- package/client-v2/ephemeral/Ephemeral.js +551 -0
- package/client-v2/index.js +12 -0
- package/client-v2/streams/Stream.js +3 -0
- package/client-v2/streams/runtime/Runner.js +26 -3
- package/client-v2/utils/autopilot.js +168 -0
- package/client-v2/utils/conflation.js +118 -0
- package/client-v2/utils/defaults.js +16 -3
- package/package.json +3 -3
- package/test-v2/autopilot-unit/autopilotWire.test.js +410 -0
- package/test-v2/conflation-unit/conflationWire.test.js +398 -0
- package/test-v2/ephemeral-unit/_planServer.js +73 -0
- package/test-v2/ephemeral-unit/durableSinkPin.test.js +112 -0
- package/test-v2/ephemeral-unit/ephemeralBuffer.test.js +305 -0
- package/test-v2/ephemeral-unit/ephemeralWire.test.js +398 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ephemeral wire contract (EPHEMERAL_QUEUES.md §3.1, §4).
|
|
3
|
+
*
|
|
4
|
+
* These assert the EXACT request every verb of `queen.ephemeral` puts on the
|
|
5
|
+
* socket, against a scripted plan server. The family is eight routes with no
|
|
6
|
+
* stored procedure and no schema behind them: a misspelled field is not a
|
|
7
|
+
* compile error anywhere, it is a 400 -- or worse, a knob the broker never
|
|
8
|
+
* reads -- on somebody's first ephemeral queue.
|
|
9
|
+
*
|
|
10
|
+
* deepEqual, not "contains": an EXTRA field is as much a contract break as a
|
|
11
|
+
* missing one. `partition:null` sent because the caller did not name one, a
|
|
12
|
+
* `wait=false` that did not need to be on the query string, a `transactionId`
|
|
13
|
+
* copied from the durable push wire -- all of them would ship silently if these
|
|
14
|
+
* were subset checks.
|
|
15
|
+
*
|
|
16
|
+
* The rules pinned here that live in the CLIENT and nowhere else:
|
|
17
|
+
* * a message on this wire is `{payload}` and nothing else -- the envelope
|
|
18
|
+
* carries queue and partition, unlike the durable push, which repeats them
|
|
19
|
+
* on every item;
|
|
20
|
+
* * what the caller did not ask for does not reach the query string, so the
|
|
21
|
+
* broker's own defaults keep owning it (§3.1's `?` on every parameter but
|
|
22
|
+
* `queue`);
|
|
23
|
+
* * `wait:true` DOES send an explicit `timeout`, because the HTTP deadline is
|
|
24
|
+
* set past it and a client that let the two defaults drift apart would
|
|
25
|
+
* abort requests the broker was about to answer;
|
|
26
|
+
* * every 404 on the family is one error with `.code ===
|
|
27
|
+
* EPHEMERAL_UNSUPPORTED` (§4) -- the broker's missing route and the old
|
|
28
|
+
* proxy's `route_blocked` are the same fact to a caller;
|
|
29
|
+
* * empty is an empty ARRAY, never null: `{messages}` is destructured on the
|
|
30
|
+
* first line of every consumer ever written against this API.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { describe, it } from 'node:test'
|
|
34
|
+
import assert from 'node:assert/strict'
|
|
35
|
+
|
|
36
|
+
import { EPHEMERAL_UNSUPPORTED, EPHEMERAL_QUEUE_NOT_FOUND } from '../../client-v2/index.js'
|
|
37
|
+
import { withEphemeral, ok, pushed, popped, frame, acked, OLD_BROKER, OLD_PROXY, QUEUE_NOT_FOUND } from './_planServer.js'
|
|
38
|
+
|
|
39
|
+
const QUEUE = 'test-ephemeral'
|
|
40
|
+
|
|
41
|
+
describe('ephemeral wire — declaration', () => {
|
|
42
|
+
it('configure sends the queue and its options under `options`', async () => {
|
|
43
|
+
await withEphemeral([ok({ queue: QUEUE })], async (eph, hits) => {
|
|
44
|
+
await eph.configure(QUEUE, {
|
|
45
|
+
maxBytes: 1048576,
|
|
46
|
+
maxLength: 1000,
|
|
47
|
+
policy: 'dropOldest',
|
|
48
|
+
ttlSeconds: 30,
|
|
49
|
+
leaseSeconds: 15,
|
|
50
|
+
retryLimit: 2,
|
|
51
|
+
windowBuffer: { ms: 20, count: 50 }
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
assert.equal(hits.length, 1)
|
|
55
|
+
assert.equal(hits[0].method, 'POST')
|
|
56
|
+
assert.equal(hits[0].url, '/api/v1/ephemeral/configure')
|
|
57
|
+
assert.deepEqual(hits[0].body, {
|
|
58
|
+
queue: QUEUE,
|
|
59
|
+
options: {
|
|
60
|
+
maxBytes: 1048576,
|
|
61
|
+
maxLength: 1000,
|
|
62
|
+
policy: 'dropOldest',
|
|
63
|
+
ttlSeconds: 30,
|
|
64
|
+
leaseSeconds: 15,
|
|
65
|
+
retryLimit: 2,
|
|
66
|
+
windowBuffer: { ms: 20, count: 50 }
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('configure sends only the options it was given', async () => {
|
|
73
|
+
await withEphemeral([ok({ queue: QUEUE }), ok({ queue: QUEUE })], async (eph, hits) => {
|
|
74
|
+
await eph.configure(QUEUE, { ttlSeconds: 30 })
|
|
75
|
+
assert.deepEqual(hits[0].body, { queue: QUEUE, options: { ttlSeconds: 30 } })
|
|
76
|
+
|
|
77
|
+
// A bare declaration: exists, with the tenant defaults.
|
|
78
|
+
await eph.configure(QUEUE)
|
|
79
|
+
assert.deepEqual(hits[1].body, { queue: QUEUE, options: {} })
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('configure refuses an option this client does not know instead of dropping it', async () => {
|
|
84
|
+
// Every one of these knobs bounds something. A silently ignored `ttlSecond`
|
|
85
|
+
// is a ring that grows until a global budget answers 503.
|
|
86
|
+
await withEphemeral([], async (eph, hits) => {
|
|
87
|
+
await assert.rejects(() => eph.configure(QUEUE, { ttlSecond: 30 }), (e) => {
|
|
88
|
+
assert.match(e.message, /ttlSecond/)
|
|
89
|
+
return true
|
|
90
|
+
})
|
|
91
|
+
assert.equal(hits.length, 0, 'nothing may reach the broker')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('reset and delete name the queue where each route expects it', async () => {
|
|
96
|
+
await withEphemeral([ok({ dropped: 7 }), ok({})], async (eph, hits) => {
|
|
97
|
+
const res = await eph.reset(QUEUE)
|
|
98
|
+
assert.equal(hits[0].method, 'POST')
|
|
99
|
+
assert.equal(hits[0].url, '/api/v1/ephemeral/reset')
|
|
100
|
+
assert.deepEqual(hits[0].body, { queue: QUEUE })
|
|
101
|
+
assert.equal(res.dropped, 7)
|
|
102
|
+
|
|
103
|
+
await eph.delete(QUEUE)
|
|
104
|
+
assert.equal(hits[1].method, 'DELETE')
|
|
105
|
+
assert.equal(hits[1].url, `/api/v1/ephemeral/queue/${QUEUE}`)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('percent-encodes a queue name that would otherwise change the path', async () => {
|
|
110
|
+
await withEphemeral([ok({}), ok({})], async (eph, hits) => {
|
|
111
|
+
await eph.delete('inbox/7')
|
|
112
|
+
assert.equal(hits[0].url, '/api/v1/ephemeral/queue/inbox%2F7')
|
|
113
|
+
|
|
114
|
+
await eph.depth('inbox/7')
|
|
115
|
+
assert.equal(hits[1].url, '/api/v1/ephemeral/queues/inbox%2F7/depth')
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('refuses a missing queue name before spending a request', async () => {
|
|
120
|
+
await withEphemeral([], async (eph, hits) => {
|
|
121
|
+
await assert.rejects(() => eph.configure('', {}))
|
|
122
|
+
await assert.rejects(() => eph.push(undefined, [{ a: 1 }]))
|
|
123
|
+
await assert.rejects(() => eph.pop(null))
|
|
124
|
+
await assert.rejects(() => eph.ack(42, ['e:1']))
|
|
125
|
+
assert.equal(hits.length, 0)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('ephemeral wire — push', () => {
|
|
131
|
+
it('push sends the flat envelope with payload-only messages', async () => {
|
|
132
|
+
await withEphemeral([pushed(2)], async (eph, hits) => {
|
|
133
|
+
const res = await eph.push(QUEUE, [{ a: 1 }, { a: 2 }])
|
|
134
|
+
|
|
135
|
+
assert.equal(hits.length, 1)
|
|
136
|
+
assert.equal(hits[0].method, 'POST')
|
|
137
|
+
assert.equal(hits[0].url, '/api/v1/ephemeral/push')
|
|
138
|
+
assert.deepEqual(hits[0].body, { queue: QUEUE, messages: [{ payload: { a: 1 } }, { payload: { a: 2 } }] })
|
|
139
|
+
assert.equal(res.pushed, 2)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('push omits `partition` when the caller named none, and sends it when they did', async () => {
|
|
144
|
+
await withEphemeral([pushed(1), pushed(1)], async (eph, hits) => {
|
|
145
|
+
await eph.push(QUEUE, { a: 1 })
|
|
146
|
+
assert.ok(!('partition' in hits[0].body), 'which partition an unpartitioned push lands on is the broker\'s rule')
|
|
147
|
+
|
|
148
|
+
await eph.push(QUEUE, { a: 1 }, { partition: 'room-7' })
|
|
149
|
+
assert.deepEqual(hits[1].body, { queue: QUEUE, partition: 'room-7', messages: [{ payload: { a: 1 } }] })
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('push accepts the durable push sugar: a bare value, {data}, or {payload}', async () => {
|
|
154
|
+
await withEphemeral([pushed(4)], async (eph, hits) => {
|
|
155
|
+
await eph.push(QUEUE, [{ a: 1 }, { data: { a: 2 } }, { payload: { a: 3 } }, 'plain'])
|
|
156
|
+
assert.deepEqual(hits[0].body.messages, [
|
|
157
|
+
{ payload: { a: 1 } },
|
|
158
|
+
{ payload: { a: 2 } },
|
|
159
|
+
{ payload: { a: 3 } },
|
|
160
|
+
{ payload: 'plain' }
|
|
161
|
+
])
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('push carries no transactionId — there is no dedup index to hold one', async () => {
|
|
166
|
+
await withEphemeral([pushed(1)], async (eph, hits) => {
|
|
167
|
+
await eph.push(QUEUE, [{ a: 1 }])
|
|
168
|
+
const sent = hits[0].body.messages[0]
|
|
169
|
+
assert.deepEqual(Object.keys(sent), ['payload'], 'a message on this wire is {payload} and nothing else')
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('push of nothing answers pushed:0 without spending a request', async () => {
|
|
174
|
+
await withEphemeral([], async (eph, hits) => {
|
|
175
|
+
assert.deepEqual(await eph.push(QUEUE, []), { pushed: 0 })
|
|
176
|
+
assert.equal(hits.length, 0)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('push refuses a null message rather than inventing a null payload', async () => {
|
|
181
|
+
await withEphemeral([], async (eph, hits) => {
|
|
182
|
+
await assert.rejects(() => eph.push(QUEUE, [null]), /null/)
|
|
183
|
+
assert.equal(hits.length, 0)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('ephemeral wire — pop', () => {
|
|
189
|
+
it('pop sends the queue and nothing else when nothing else was asked for', async () => {
|
|
190
|
+
await withEphemeral([popped(QUEUE, [frame(1)])], async (eph, hits) => {
|
|
191
|
+
const res = await eph.pop(QUEUE)
|
|
192
|
+
|
|
193
|
+
assert.equal(hits[0].method, 'GET')
|
|
194
|
+
assert.equal(hits[0].url, `/api/v1/ephemeral/pop?queue=${QUEUE}`)
|
|
195
|
+
assert.equal(res.queue, QUEUE)
|
|
196
|
+
assert.equal(res.messages.length, 1)
|
|
197
|
+
assert.equal(res.messages[0].id, 'e:beef:Default:1')
|
|
198
|
+
assert.equal(res.messages[0].attempts, 0)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('pop puts every declared parameter on the query string, in order', async () => {
|
|
203
|
+
await withEphemeral([popped(QUEUE, [])], async (eph, hits) => {
|
|
204
|
+
await eph.pop(QUEUE, { partition: 'room-7', batch: 10, wait: true, timeout: 2000, group: 'workers', autoAck: true })
|
|
205
|
+
assert.equal(
|
|
206
|
+
hits[0].url,
|
|
207
|
+
`/api/v1/ephemeral/pop?queue=${QUEUE}&partition=room-7&batch=10&wait=true&timeout=2000&group=workers&autoAck=true`
|
|
208
|
+
)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('pop sends an explicit timeout whenever it waits, and none when it does not', async () => {
|
|
213
|
+
// The HTTP deadline is set PAST the server's, so the broker's long poll
|
|
214
|
+
// always ends the request first. Letting the two defaults drift apart is
|
|
215
|
+
// how a client starts aborting requests that were about to be answered.
|
|
216
|
+
await withEphemeral([popped(QUEUE, []), popped(QUEUE, [])], async (eph, hits) => {
|
|
217
|
+
await eph.pop(QUEUE, { wait: true })
|
|
218
|
+
assert.equal(hits[0].url, `/api/v1/ephemeral/pop?queue=${QUEUE}&wait=true&timeout=30000`)
|
|
219
|
+
|
|
220
|
+
await eph.pop(QUEUE, { batch: 5 })
|
|
221
|
+
assert.equal(hits[1].url, `/api/v1/ephemeral/pop?queue=${QUEUE}&batch=5`, 'no wait, no timeout, no autoAck')
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('pop accepts `timeoutMillis` as the same milliseconds, and refuses both spellings', async () => {
|
|
226
|
+
await withEphemeral([popped(QUEUE, [])], async (eph, hits) => {
|
|
227
|
+
await eph.pop(QUEUE, { wait: true, timeoutMillis: 1500 })
|
|
228
|
+
assert.equal(hits[0].url, `/api/v1/ephemeral/pop?queue=${QUEUE}&wait=true&timeout=1500`)
|
|
229
|
+
|
|
230
|
+
await assert.rejects(() => eph.pop(QUEUE, { wait: true, timeout: 1000, timeoutMillis: 1500 }))
|
|
231
|
+
assert.equal(hits.length, 1)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('pop returns an EMPTY ARRAY on a timeout, and on a bodiless 204', async () => {
|
|
236
|
+
await withEphemeral([popped(QUEUE, []), { status: 204 }], async (eph) => {
|
|
237
|
+
const empty = await eph.pop(QUEUE, { wait: true, timeout: 10 })
|
|
238
|
+
assert.deepEqual(empty, { queue: QUEUE, messages: [] })
|
|
239
|
+
|
|
240
|
+
const noContent = await eph.pop(QUEUE)
|
|
241
|
+
assert.deepEqual(noContent, { queue: QUEUE, messages: [] }, 'a 204 must not become `null` for the caller to trip over')
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
describe('ephemeral wire — ack', () => {
|
|
247
|
+
it('ack sends the ids under `acks`, with the group beside them', async () => {
|
|
248
|
+
await withEphemeral([acked({ id: 'e:beef:Default:1', outcome: 'acked' })], async (eph, hits) => {
|
|
249
|
+
const res = await eph.ack(QUEUE, [frame(1)], { group: 'workers' })
|
|
250
|
+
|
|
251
|
+
assert.equal(hits[0].method, 'POST')
|
|
252
|
+
assert.equal(hits[0].url, '/api/v1/ephemeral/ack')
|
|
253
|
+
assert.deepEqual(hits[0].body, { queue: QUEUE, group: 'workers', acks: [{ id: 'e:beef:Default:1' }] })
|
|
254
|
+
assert.equal(res.results[0].outcome, 'acked')
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('ack takes popped messages, bare id strings, or the wire objects themselves', async () => {
|
|
259
|
+
await withEphemeral([acked(), acked()], async (eph, hits) => {
|
|
260
|
+
await eph.ack(QUEUE, 'e:beef:Default:9')
|
|
261
|
+
assert.deepEqual(hits[0].body, { queue: QUEUE, acks: [{ id: 'e:beef:Default:9' }] })
|
|
262
|
+
|
|
263
|
+
await eph.ack(QUEUE, [{ id: 'a', status: 'retry' }, { id: 'b', status: 'failed', error: 'boom' }])
|
|
264
|
+
assert.deepEqual(hits[1].body.acks, [
|
|
265
|
+
{ id: 'a', status: 'retry' },
|
|
266
|
+
{ id: 'b', status: 'failed', error: 'boom' }
|
|
267
|
+
])
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('ack maps the boolean sugar and lets a per-message status win over the call-wide one', async () => {
|
|
272
|
+
await withEphemeral([acked(), acked()], async (eph, hits) => {
|
|
273
|
+
await eph.ack(QUEUE, ['a', 'b'], { status: false, error: 'handler threw' })
|
|
274
|
+
assert.deepEqual(hits[0].body.acks, [
|
|
275
|
+
{ id: 'a', status: 'failed', error: 'handler threw' },
|
|
276
|
+
{ id: 'b', status: 'failed', error: 'handler threw' }
|
|
277
|
+
])
|
|
278
|
+
|
|
279
|
+
await eph.ack(QUEUE, ['a', { id: 'b', status: 'retry' }], { status: true })
|
|
280
|
+
assert.deepEqual(hits[1].body.acks, [
|
|
281
|
+
{ id: 'a', status: 'completed' },
|
|
282
|
+
{ id: 'b', status: 'retry' }
|
|
283
|
+
], 'the per-message status is the one the caller wrote LAST and closest to the message')
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('ack omits `group` in queue mode and refuses an ack with no id', async () => {
|
|
288
|
+
await withEphemeral([acked()], async (eph, hits) => {
|
|
289
|
+
await eph.ack(QUEUE, ['a'])
|
|
290
|
+
assert.ok(!('group' in hits[0].body), 'groupless is queue mode, not a group named null')
|
|
291
|
+
|
|
292
|
+
await assert.rejects(() => eph.ack(QUEUE, [{ partition: 'Default' }]), /id/)
|
|
293
|
+
assert.equal(hits.length, 1)
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('ack of nothing answers empty results without spending a request', async () => {
|
|
298
|
+
await withEphemeral([], async (eph, hits) => {
|
|
299
|
+
assert.deepEqual(await eph.ack(QUEUE, []), { results: [] })
|
|
300
|
+
assert.equal(hits.length, 0)
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
describe('ephemeral wire — status', () => {
|
|
306
|
+
it('queues and depth are plain GETs on the status routes', async () => {
|
|
307
|
+
await withEphemeral([ok({ queues: [] }), ok({ queue: QUEUE, depth: 3, bytes: 120 })], async (eph, hits) => {
|
|
308
|
+
await eph.queues()
|
|
309
|
+
assert.equal(hits[0].method, 'GET')
|
|
310
|
+
assert.equal(hits[0].url, '/api/v1/ephemeral/queues')
|
|
311
|
+
|
|
312
|
+
const depth = await eph.depth(QUEUE)
|
|
313
|
+
assert.equal(hits[1].url, `/api/v1/ephemeral/queues/${QUEUE}/depth`)
|
|
314
|
+
assert.equal(depth.depth, 3)
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
describe('ephemeral wire — the two kinds of 404', () => {
|
|
320
|
+
// The status alone cannot tell these apart, which is exactly why the mapping
|
|
321
|
+
// reads the body's CODE:
|
|
322
|
+
//
|
|
323
|
+
// * no SDK negotiates a version (§4, §8), so a pre-1.1 broker (routes never
|
|
324
|
+
// registered) and a pre-1.1 proxy (`route_blocked`, fails closed on
|
|
325
|
+
// unknown API paths) both answer 404, and to a caller those are one fact:
|
|
326
|
+
// upgrade;
|
|
327
|
+
// * a broker that DOES support the family answers 404 with
|
|
328
|
+
// `ephemeral_queue_not_found` when `depth` names a queue that is not
|
|
329
|
+
// there — the only verb that can, since push and pop create implicitly,
|
|
330
|
+
// `reset` answers dropped:0 and `delete` answers deleted:false.
|
|
331
|
+
//
|
|
332
|
+
// Collapsing the second into the first sends somebody chasing a broker
|
|
333
|
+
// version over a queue name typo.
|
|
334
|
+
const oneClearError = (e) => {
|
|
335
|
+
assert.equal(e.code, EPHEMERAL_UNSUPPORTED)
|
|
336
|
+
assert.equal(e.message, 'broker/proxy does not support ephemeral queues (requires >= 1.1)')
|
|
337
|
+
assert.equal(e.status, 404)
|
|
338
|
+
return true
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
it('maps a missing broker route to the one clear error', async () => {
|
|
342
|
+
await withEphemeral([OLD_BROKER], async (eph, hits) => {
|
|
343
|
+
await assert.rejects(() => eph.pop(QUEUE), oneClearError)
|
|
344
|
+
assert.equal(hits.length, 1, 'a 404 is not retried')
|
|
345
|
+
}, { retryAttempts: 1 })
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('maps the old proxy `route_blocked` to the same error, and keeps the original as `.cause`', async () => {
|
|
349
|
+
await withEphemeral([OLD_PROXY], async (eph) => {
|
|
350
|
+
await assert.rejects(() => eph.configure(QUEUE, { ttlSeconds: 5 }), (e) => {
|
|
351
|
+
oneClearError(e)
|
|
352
|
+
assert.equal(e.cause.code, 'route_blocked', 'the proxy verdict is kept, it is just not what the caller branches on')
|
|
353
|
+
return true
|
|
354
|
+
})
|
|
355
|
+
}, { retryAttempts: 1 })
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('a 404 for a MISSING QUEUE is its own error, not "your broker is too old"', async () => {
|
|
359
|
+
await withEphemeral([QUEUE_NOT_FOUND], async (eph, hits) => {
|
|
360
|
+
await assert.rejects(() => eph.depth(QUEUE), (e) => {
|
|
361
|
+
assert.equal(e.code, EPHEMERAL_QUEUE_NOT_FOUND)
|
|
362
|
+
assert.equal(e.status, 404)
|
|
363
|
+
assert.equal(e.queue, QUEUE, 'the error names the queue that was not found')
|
|
364
|
+
assert.match(e.message, /does not exist/)
|
|
365
|
+
assert.doesNotMatch(e.message, /1\.1/, 'a missing queue must not read as a version problem')
|
|
366
|
+
// Nothing the HTTP layer surfaced is lost by the mapping.
|
|
367
|
+
assert.equal(e.cause.status, 404)
|
|
368
|
+
assert.equal(e.cause.code, 'ephemeral_queue_not_found')
|
|
369
|
+
assert.equal(e.cause.message, 'ephemeral queue not found')
|
|
370
|
+
return true
|
|
371
|
+
})
|
|
372
|
+
assert.equal(hits[0].url, `/api/v1/ephemeral/queues/${QUEUE}/depth`)
|
|
373
|
+
}, { retryAttempts: 1 })
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('tells the two 404s apart on the same verb, by the body and not the status', async () => {
|
|
377
|
+
// The regression this pins: `depth` answering a real 404 while the routes
|
|
378
|
+
// are demonstrably present, because the very next call succeeds.
|
|
379
|
+
await withEphemeral([QUEUE_NOT_FOUND, OLD_BROKER], async (eph) => {
|
|
380
|
+
await assert.rejects(() => eph.depth(QUEUE), e => e.code === EPHEMERAL_QUEUE_NOT_FOUND)
|
|
381
|
+
await assert.rejects(() => eph.depth(QUEUE), e => e.code === EPHEMERAL_UNSUPPORTED)
|
|
382
|
+
}, { retryAttempts: 1 })
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('leaves every other refusal alone, code and status intact', async () => {
|
|
386
|
+
// 403 feature_gated (the cloud grant, §5.1) and 429 queue_full (§1.6) are
|
|
387
|
+
// live answers of a broker that DOES support the family. Mapping them to
|
|
388
|
+
// "upgrade" would send an operator chasing a version that is already right.
|
|
389
|
+
const gated = { status: 403, body: { error: 'ephemeral is not enabled for this plan', code: 'feature_gated' } }
|
|
390
|
+
await withEphemeral([gated], async (eph) => {
|
|
391
|
+
await assert.rejects(() => eph.push(QUEUE, [{ a: 1 }]), (e) => {
|
|
392
|
+
assert.equal(e.status, 403)
|
|
393
|
+
assert.equal(e.code, 'feature_gated')
|
|
394
|
+
return true
|
|
395
|
+
})
|
|
396
|
+
}, { retryAttempts: 1 })
|
|
397
|
+
})
|
|
398
|
+
})
|