@agentuity/queue 3.0.11 → 3.1.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/src/service.ts ADDED
@@ -0,0 +1,675 @@
1
+ /**
2
+ * @module queue
3
+ *
4
+ * Queue service for publishing messages to Agentuity queues.
5
+ *
6
+ * Used internally by `@agentuity/queue`'s `QueueClient`. App code should
7
+ * use `QueueClient` directly rather than reaching for these primitives.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { QueueClient } from '@agentuity/queue';
12
+ *
13
+ * const queue = new QueueClient();
14
+ * const result = await queue.publish('order-queue', {
15
+ * orderId: 123,
16
+ * action: 'process',
17
+ * });
18
+ * console.log(`Published message ${result.id}`);
19
+ * ```
20
+ */
21
+
22
+ import { buildUrl, toPayload, toServiceException } from '@agentuity/adapter';
23
+ import type { FetchAdapter } from '@agentuity/adapter';
24
+ import { StructuredError } from '@agentuity/adapter';
25
+ import { z } from 'zod';
26
+
27
+ /**
28
+ * Parameters for publishing a message to a queue.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const params: QueuePublishParams = {
33
+ * metadata: { priority: 'high' },
34
+ * partitionKey: 'customer-123',
35
+ * idempotencyKey: 'order-456-v1',
36
+ * ttl: 3600, // 1 hour
37
+ * };
38
+ * ```
39
+ */
40
+ export const QueuePublishParamsSchema = z.object({
41
+ /**
42
+ * Optional metadata to attach to the message.
43
+ * Can contain any JSON-serializable data for message routing or filtering.
44
+ */
45
+ metadata: z
46
+ .record(z.string(), z.unknown())
47
+ .optional()
48
+ .describe('Optional metadata to attach to the message.'),
49
+
50
+ /**
51
+ * Optional partition key for message ordering.
52
+ * Messages with the same partition key are guaranteed to be processed in order.
53
+ */
54
+ partitionKey: z.string().optional().describe('Optional partition key for message ordering.'),
55
+
56
+ /**
57
+ * Optional idempotency key for deduplication.
58
+ * If a message with the same key was recently published, it will be deduplicated.
59
+ */
60
+ idempotencyKey: z.string().optional().describe('Optional idempotency key for deduplication.'),
61
+
62
+ /**
63
+ * Optional time-to-live in seconds.
64
+ * Messages will expire and be removed after this duration.
65
+ */
66
+ ttl: z.number().optional().describe('Optional time-to-live in seconds.'),
67
+
68
+ /**
69
+ * Optional project ID for cross-project publishing.
70
+ * If not specified, uses the current project context.
71
+ */
72
+ projectId: z.string().optional().describe('Optional project ID for cross-project publishing.'),
73
+
74
+ /**
75
+ * Optional agent ID for attribution.
76
+ * If not specified, uses the current agent context.
77
+ */
78
+ agentId: z.string().optional().describe('Optional agent ID for attribution.'),
79
+
80
+ /**
81
+ * Whether to publish synchronously.
82
+ * When true, the API waits for the message to be fully persisted before returning.
83
+ * When false (default), the API returns immediately with a pending message.
84
+ */
85
+ sync: z.boolean().optional().describe('Whether to publish synchronously.'),
86
+ });
87
+
88
+ export type QueuePublishParams = z.infer<typeof QueuePublishParamsSchema>;
89
+
90
+ /**
91
+ * Result of publishing a message to a queue.
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const result = await queue.publish('my-queue', payload);
96
+ * console.log(`Message ${result.id} published at offset ${result.offset}`);
97
+ * ```
98
+ */
99
+ export const QueuePublishResultSchema = z.object({
100
+ /**
101
+ * The unique message ID (prefixed with msg_).
102
+ * Use this ID to track, acknowledge, or delete the message.
103
+ */
104
+ id: z.string().describe('The unique message ID (prefixed with msg_).'),
105
+
106
+ /**
107
+ * The sequential offset of the message in the queue.
108
+ * Offsets are monotonically increasing and can be used for log-style consumption.
109
+ */
110
+ offset: z.number().describe('The sequential offset of the message in the queue.'),
111
+
112
+ /**
113
+ * ISO 8601 timestamp when the message was published.
114
+ */
115
+ publishedAt: z.string().describe('ISO 8601 timestamp when the message was published.'),
116
+ });
117
+
118
+ export type QueuePublishResult = z.infer<typeof QueuePublishResultSchema>;
119
+
120
+ /**
121
+ * Parameters for creating a queue.
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * const result = await queue.createQueue('my-queue', {
126
+ * queueType: 'pubsub',
127
+ * settings: { defaultTtlSeconds: 86400 },
128
+ * });
129
+ * ```
130
+ */
131
+ export const QueueCreateParamsSchema = z.object({
132
+ /**
133
+ * Type of queue to create.
134
+ * - `worker`: Messages are consumed by exactly one consumer with acknowledgment.
135
+ * - `pubsub`: Messages are broadcast to all subscribers.
136
+ * @default 'worker'
137
+ */
138
+ queueType: z.enum(['worker', 'pubsub']).optional().describe('Type of queue to create.'),
139
+
140
+ /**
141
+ * Optional description of the queue's purpose.
142
+ */
143
+ description: z.string().optional().describe("Optional description of the queue's purpose."),
144
+
145
+ /**
146
+ * Optional settings to customize queue behavior.
147
+ * Only provided fields are applied; others use server defaults.
148
+ */
149
+ settings: z
150
+ .object({
151
+ /** Default time-to-live for messages in seconds. Null means no expiration. */
152
+ defaultTtlSeconds: z
153
+ .number()
154
+ .nullable()
155
+ .optional()
156
+ .describe('Default time-to-live for messages in seconds. Null means no expiration.'),
157
+ /** Time in seconds a message is invisible after being received. */
158
+ defaultVisibilityTimeoutSeconds: z
159
+ .number()
160
+ .optional()
161
+ .describe('Time in seconds a message is invisible after being received.'),
162
+ /** Maximum number of delivery attempts before moving to DLQ. */
163
+ defaultMaxRetries: z
164
+ .number()
165
+ .optional()
166
+ .describe('Maximum number of delivery attempts before moving to DLQ.'),
167
+ /** Maximum number of messages a single client can process concurrently. */
168
+ maxInFlightPerClient: z
169
+ .number()
170
+ .optional()
171
+ .describe('Maximum number of messages a single client can process concurrently.'),
172
+ /** Retention period for acknowledged messages in seconds. */
173
+ retentionSeconds: z
174
+ .number()
175
+ .optional()
176
+ .describe('Retention period for acknowledged messages in seconds.'),
177
+ })
178
+ .optional()
179
+ .describe('Optional settings to customize queue behavior.'),
180
+ });
181
+
182
+ export type QueueCreateParams = z.infer<typeof QueueCreateParamsSchema>;
183
+
184
+ /**
185
+ * Result of creating a queue.
186
+ */
187
+ export const QueueCreateResultSchema = z.object({
188
+ /** The queue name. */
189
+ name: z.string().describe('The queue name.'),
190
+ /** The queue type ('worker' or 'pubsub'). */
191
+ queueType: z.string().describe("The queue type ('worker' or 'pubsub')."),
192
+ });
193
+
194
+ export type QueueCreateResult = z.infer<typeof QueueCreateResultSchema>;
195
+
196
+ /**
197
+ * Queue service interface for publishing messages.
198
+ *
199
+ * Provides a simple publish-only surface suitable for app handlers.
200
+ * Implemented by `QueueStorageService` (cloud) and `LocalQueueStorage`
201
+ * (local dev).
202
+ *
203
+ * For full queue management (create queues, consume messages, manage
204
+ * destinations), use the `@agentuity/queue` package directly.
205
+ *
206
+ * @example
207
+ * ```typescript
208
+ * import { QueueClient } from '@agentuity/queue';
209
+ *
210
+ * const queue = new QueueClient();
211
+ *
212
+ * await queue.publish('notifications', {
213
+ * type: 'email',
214
+ * to: 'user@example.com',
215
+ * subject: 'Welcome!',
216
+ * });
217
+ * ```
218
+ */
219
+ export interface QueueService {
220
+ /**
221
+ * Publish a message to a queue.
222
+ *
223
+ * The payload can be a string or an object. Objects are automatically
224
+ * JSON-stringified before publishing.
225
+ *
226
+ * @param queueName - The name of the queue to publish to
227
+ * @param payload - The message payload (string or JSON-serializable object)
228
+ * @param params - Optional publish parameters (metadata, TTL, etc.)
229
+ * @returns The publish result with message ID and offset
230
+ * @throws {QueueNotFoundError} If the queue does not exist
231
+ * @throws {QueueValidationError} If validation fails (invalid name, payload too large, etc.)
232
+ * @throws {QueuePublishError} If the publish operation fails
233
+ *
234
+ * @example Publishing a simple message
235
+ * ```typescript
236
+ * const result = await queue.publish('my-queue', 'Hello, World!');
237
+ * ```
238
+ *
239
+ * @example Publishing with options
240
+ * ```typescript
241
+ * const result = await queue.publish('my-queue', { task: 'process' }, {
242
+ * metadata: { priority: 'high' },
243
+ * idempotencyKey: 'task-123',
244
+ * ttl: 3600,
245
+ * });
246
+ * ```
247
+ */
248
+ publish(
249
+ queueName: string,
250
+ payload: string | object,
251
+ params?: QueuePublishParams
252
+ ): Promise<QueuePublishResult>;
253
+
254
+ /**
255
+ * Create a queue with idempotent semantics.
256
+ *
257
+ * If the queue already exists, this returns successfully without error.
258
+ * Safe to call multiple times — uses an internal cache to avoid redundant API calls.
259
+ *
260
+ * @param queueName - The name of the queue to create
261
+ * @param params - Optional creation parameters (queue type, settings, etc.)
262
+ * @returns The create result with queue name and type
263
+ * @throws {QueueValidationError} If the queue name is invalid
264
+ *
265
+ * @example Creating a worker queue
266
+ * ```typescript
267
+ * const result = await queue.createQueue('task-queue');
268
+ * ```
269
+ *
270
+ * @example Creating a pubsub queue with settings
271
+ * ```typescript
272
+ * const result = await queue.createQueue('events', {
273
+ * queueType: 'pubsub',
274
+ * settings: { defaultTtlSeconds: 86400 },
275
+ * });
276
+ * ```
277
+ */
278
+ createQueue(queueName: string, params?: QueueCreateParams): Promise<QueueCreateResult>;
279
+
280
+ /**
281
+ * Delete a queue.
282
+ *
283
+ * Permanently deletes a queue and all its messages. This action cannot be undone.
284
+ * If the queue has already been deleted or does not exist, a {@link QueueNotFoundError} is thrown.
285
+ *
286
+ * @param queueName - The name of the queue to delete
287
+ * @throws {QueueNotFoundError} If the queue does not exist
288
+ * @throws {QueueValidationError} If the queue name is invalid
289
+ *
290
+ * @example Deleting a queue
291
+ * ```typescript
292
+ * await queue.deleteQueue('old-queue');
293
+ * ```
294
+ */
295
+ deleteQueue(queueName: string): Promise<void>;
296
+ }
297
+
298
+ // ============================================================================
299
+ // Errors
300
+ // ============================================================================
301
+
302
+ /**
303
+ * Error thrown when a publish operation fails.
304
+ *
305
+ * This is a general error for publish failures that aren't specifically
306
+ * validation or not-found errors.
307
+ */
308
+ export const QueuePublishError = StructuredError('QueuePublishError');
309
+
310
+ /**
311
+ * Error thrown when a queue creation operation fails.
312
+ *
313
+ * This is a general error for create failures that aren't specifically
314
+ * validation, not-found, or conflict (409) errors.
315
+ */
316
+ export const QueueCreateError = StructuredError('QueueCreateError');
317
+
318
+ /**
319
+ * Error thrown when a queue is not found.
320
+ *
321
+ * @example
322
+ * ```typescript
323
+ * try {
324
+ * await queue.publish('non-existent', 'payload');
325
+ * } catch (error) {
326
+ * if (error instanceof QueueNotFoundError) {
327
+ * console.error('Queue does not exist');
328
+ * }
329
+ * }
330
+ * ```
331
+ */
332
+ export const QueueNotFoundError = StructuredError('QueueNotFoundError');
333
+
334
+ /**
335
+ * Error thrown when validation fails.
336
+ *
337
+ * Contains the field name and optionally the invalid value for debugging.
338
+ */
339
+ export const QueueValidationError = StructuredError('QueueValidationError')<{
340
+ /** The field that failed validation */
341
+ field: string;
342
+ /** The invalid value (for debugging) */
343
+ value?: unknown;
344
+ }>();
345
+
346
+ // ============================================================================
347
+ // Internal Validation
348
+ // ============================================================================
349
+
350
+ const MAX_QUEUE_NAME_LENGTH = 256;
351
+ const MAX_PAYLOAD_SIZE = 1048576;
352
+ const MAX_PARTITION_KEY_LENGTH = 256;
353
+ const MAX_IDEMPOTENCY_KEY_LENGTH = 256;
354
+ const VALID_QUEUE_NAME_REGEX = /^[a-z_][a-z0-9_-]*$/;
355
+
356
+ /** @internal */
357
+ function validateQueueNameInternal(name: string): void {
358
+ if (!name || name.length === 0) {
359
+ throw new QueueValidationError({
360
+ message: 'Queue name cannot be empty',
361
+ field: 'queueName',
362
+ value: name,
363
+ });
364
+ }
365
+ if (name.length > MAX_QUEUE_NAME_LENGTH) {
366
+ throw new QueueValidationError({
367
+ message: `Queue name must not exceed ${MAX_QUEUE_NAME_LENGTH} characters`,
368
+ field: 'queueName',
369
+ value: name,
370
+ });
371
+ }
372
+ if (!VALID_QUEUE_NAME_REGEX.test(name)) {
373
+ throw new QueueValidationError({
374
+ message:
375
+ 'Queue name must start with a letter or underscore and contain only lowercase letters, digits, underscores, and hyphens',
376
+ field: 'queueName',
377
+ value: name,
378
+ });
379
+ }
380
+ }
381
+
382
+ /** @internal */
383
+ function validatePayloadInternal(payload: string): void {
384
+ if (!payload || payload.length === 0) {
385
+ throw new QueueValidationError({
386
+ message: 'Payload cannot be empty',
387
+ field: 'payload',
388
+ });
389
+ }
390
+ if (payload.length > MAX_PAYLOAD_SIZE) {
391
+ throw new QueueValidationError({
392
+ message: `Payload size exceeds ${MAX_PAYLOAD_SIZE} byte limit (${payload.length} bytes)`,
393
+ field: 'payload',
394
+ value: payload.length,
395
+ });
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Unwraps the standard API response envelope.
401
+ *
402
+ * The queue server returns responses wrapped in:
403
+ * { success: true, data: { [key]: { ...actual data... } } }
404
+ *
405
+ * Since `fromResponse()` parses the raw JSON body, `res.data` from the
406
+ * adapter is the full envelope. This helper extracts the nested data by key.
407
+ *
408
+ * Falls back to treating the input as already-unwrapped data (e.g., in tests
409
+ * using mock adapters that provide flat data directly).
410
+ *
411
+ * @internal
412
+ */
413
+ function unwrapApiResponse<T>(data: unknown, key: string): T {
414
+ if (data !== null && typeof data === 'object') {
415
+ const body = data as Record<string, unknown>;
416
+ if (body.success === true && typeof body.data === 'object' && body.data !== null) {
417
+ const envelope = body.data as Record<string, unknown>;
418
+ if (key in envelope) {
419
+ return envelope[key] as T;
420
+ }
421
+ }
422
+ }
423
+ return data as T;
424
+ }
425
+
426
+ // ============================================================================
427
+ // QueueStorageService Implementation
428
+ // ============================================================================
429
+
430
+ /**
431
+ * HTTP-based implementation of the {@link QueueService} interface.
432
+ *
433
+ * This service communicates with the Agentuity Queue API to publish
434
+ * messages. App code should use `QueueClient` from `@agentuity/queue`
435
+ * rather than instantiating this class directly.
436
+ */
437
+ export class QueueStorageService implements QueueService {
438
+ #adapter: FetchAdapter;
439
+ #baseUrl: string;
440
+ #knownQueues = new Set<string>();
441
+
442
+ /**
443
+ * Creates a new QueueStorageService.
444
+ *
445
+ * @param baseUrl - The base URL of the Queue API
446
+ * @param adapter - The fetch adapter for making HTTP requests
447
+ */
448
+ constructor(baseUrl: string, adapter: FetchAdapter) {
449
+ this.#adapter = adapter;
450
+ this.#baseUrl = baseUrl;
451
+ }
452
+
453
+ /**
454
+ * @inheritdoc
455
+ */
456
+ async publish(
457
+ queueName: string,
458
+ payload: string | object,
459
+ params?: QueuePublishParams
460
+ ): Promise<QueuePublishResult> {
461
+ // Validate inputs before sending to API
462
+ validateQueueNameInternal(queueName);
463
+
464
+ const [body] = await toPayload(payload);
465
+ const payloadStr = typeof payload === 'string' ? payload : (body as string);
466
+ validatePayloadInternal(payloadStr);
467
+
468
+ // Validate optional params
469
+ if (params?.partitionKey && params.partitionKey.length > MAX_PARTITION_KEY_LENGTH) {
470
+ throw new QueueValidationError({
471
+ message: `Partition key must not exceed ${MAX_PARTITION_KEY_LENGTH} characters`,
472
+ field: 'partitionKey',
473
+ value: params.partitionKey.length,
474
+ });
475
+ }
476
+ if (params?.idempotencyKey && params.idempotencyKey.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
477
+ throw new QueueValidationError({
478
+ message: `Idempotency key must not exceed ${MAX_IDEMPOTENCY_KEY_LENGTH} characters`,
479
+ field: 'idempotencyKey',
480
+ value: params.idempotencyKey.length,
481
+ });
482
+ }
483
+ if (params?.ttl !== undefined && params.ttl < 0) {
484
+ throw new QueueValidationError({
485
+ message: 'TTL cannot be negative',
486
+ field: 'ttl',
487
+ value: params.ttl,
488
+ });
489
+ }
490
+
491
+ const basePath = `/queue/messages/publish/${encodeURIComponent(queueName)}`;
492
+ const url = buildUrl(this.#baseUrl, params?.sync ? `${basePath}?sync=true` : basePath);
493
+
494
+ const requestBody: Record<string, unknown> = {
495
+ payload: typeof payload === 'string' ? payload : body,
496
+ };
497
+
498
+ if (params?.metadata) {
499
+ requestBody.metadata = params.metadata;
500
+ }
501
+ if (params?.partitionKey) {
502
+ requestBody.partition_key = params.partitionKey;
503
+ }
504
+ if (params?.idempotencyKey) {
505
+ requestBody.idempotency_key = params.idempotencyKey;
506
+ }
507
+ if (params?.ttl !== undefined) {
508
+ requestBody.ttl_seconds = params.ttl;
509
+ }
510
+ if (params?.projectId) {
511
+ requestBody.project_id = params.projectId;
512
+ }
513
+ if (params?.agentId) {
514
+ requestBody.agent_id = params.agentId;
515
+ }
516
+
517
+ const signal = AbortSignal.timeout(30_000);
518
+ const res = await this.#adapter.invoke<QueuePublishResult>(url, {
519
+ method: 'POST',
520
+ signal,
521
+ body: JSON.stringify(requestBody),
522
+ contentType: 'application/json',
523
+ telemetry: {
524
+ name: 'agentuity.queue.publish',
525
+ attributes: {
526
+ queueName,
527
+ },
528
+ },
529
+ });
530
+
531
+ if (res.ok) {
532
+ const data = unwrapApiResponse<Record<string, unknown>>(res.data, 'message');
533
+ const result = QueuePublishResultSchema.safeParse({
534
+ id: data.id,
535
+ offset: data.offset,
536
+ publishedAt: data.published_at,
537
+ });
538
+ if (result.success) {
539
+ return result.data;
540
+ }
541
+ throw new QueuePublishError({
542
+ message: `Queue publish returned an unexpected response format: ${result.error.message}`,
543
+ });
544
+ }
545
+
546
+ if (res.response.status === 404) {
547
+ throw new QueueNotFoundError({
548
+ message: `Queue not found: ${queueName}`,
549
+ });
550
+ }
551
+
552
+ throw await toServiceException('POST', url, res.response);
553
+ }
554
+
555
+ /**
556
+ * @inheritdoc
557
+ */
558
+ async createQueue(queueName: string, params?: QueueCreateParams): Promise<QueueCreateResult> {
559
+ validateQueueNameInternal(queueName);
560
+
561
+ if (this.#knownQueues.has(queueName)) {
562
+ return {
563
+ name: queueName,
564
+ queueType: params?.queueType ?? 'worker',
565
+ };
566
+ }
567
+
568
+ const url = buildUrl(this.#baseUrl, '/queue/create');
569
+
570
+ const requestBody: Record<string, unknown> = {
571
+ name: queueName,
572
+ queue_type: params?.queueType ?? 'worker',
573
+ };
574
+
575
+ if (params?.description !== undefined) {
576
+ requestBody.description = params.description;
577
+ }
578
+
579
+ if (params?.settings) {
580
+ const settings: Record<string, unknown> = {};
581
+ if (params.settings.defaultTtlSeconds !== undefined) {
582
+ settings.default_ttl_seconds = params.settings.defaultTtlSeconds;
583
+ }
584
+ if (params.settings.defaultVisibilityTimeoutSeconds !== undefined) {
585
+ settings.default_visibility_timeout_seconds =
586
+ params.settings.defaultVisibilityTimeoutSeconds;
587
+ }
588
+ if (params.settings.defaultMaxRetries !== undefined) {
589
+ settings.default_max_retries = params.settings.defaultMaxRetries;
590
+ }
591
+ if (params.settings.maxInFlightPerClient !== undefined) {
592
+ settings.max_in_flight_per_client = params.settings.maxInFlightPerClient;
593
+ }
594
+ if (params.settings.retentionSeconds !== undefined) {
595
+ settings.retention_seconds = params.settings.retentionSeconds;
596
+ }
597
+ if (Object.keys(settings).length > 0) {
598
+ requestBody.settings = settings;
599
+ }
600
+ }
601
+
602
+ const signal = AbortSignal.timeout(30_000);
603
+ const res = await this.#adapter.invoke<QueueCreateResult>(url, {
604
+ method: 'POST',
605
+ signal,
606
+ body: JSON.stringify(requestBody),
607
+ contentType: 'application/json',
608
+ telemetry: {
609
+ name: 'agentuity.queue.create',
610
+ attributes: {
611
+ queueName,
612
+ },
613
+ },
614
+ });
615
+
616
+ if (res.ok) {
617
+ const data = unwrapApiResponse<Record<string, unknown>>(res.data, 'queue');
618
+ const result = QueueCreateResultSchema.safeParse({
619
+ name: data.name,
620
+ queueType: data.queue_type,
621
+ });
622
+ if (result.success) {
623
+ this.#knownQueues.add(queueName);
624
+ return result.data;
625
+ }
626
+ throw new QueueCreateError({
627
+ message: `Queue create returned an unexpected response format: ${result.error.message}`,
628
+ });
629
+ }
630
+
631
+ if (res.response.status === 409) {
632
+ this.#knownQueues.add(queueName);
633
+ return {
634
+ name: queueName,
635
+ queueType: params?.queueType ?? 'worker',
636
+ };
637
+ }
638
+
639
+ throw await toServiceException('POST', url, res.response);
640
+ }
641
+
642
+ /**
643
+ * @inheritdoc
644
+ */
645
+ async deleteQueue(queueName: string): Promise<void> {
646
+ validateQueueNameInternal(queueName);
647
+
648
+ const url = buildUrl(this.#baseUrl, `/queue/delete/${encodeURIComponent(queueName)}`);
649
+
650
+ const signal = AbortSignal.timeout(30_000);
651
+ const res = await this.#adapter.invoke<void>(url, {
652
+ method: 'DELETE',
653
+ signal,
654
+ telemetry: {
655
+ name: 'agentuity.queue.delete',
656
+ attributes: {
657
+ queueName,
658
+ },
659
+ },
660
+ });
661
+
662
+ if (res.ok) {
663
+ this.#knownQueues.delete(queueName);
664
+ return;
665
+ }
666
+
667
+ if (res.response.status === 404) {
668
+ throw new QueueNotFoundError({
669
+ message: `Queue not found: ${queueName}`,
670
+ });
671
+ }
672
+
673
+ throw await toServiceException('DELETE', url, res.response);
674
+ }
675
+ }