@zizq-labs/zizq 0.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/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/client.d.ts +747 -0
- package/dist/client.js +902 -0
- package/dist/enqueue.d.ts +119 -0
- package/dist/enqueue.js +110 -0
- package/dist/handler.d.ts +179 -0
- package/dist/handler.js +45 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/query.d.ts +372 -0
- package/dist/query.js +653 -0
- package/dist/resources.d.ts +281 -0
- package/dist/resources.js +319 -0
- package/dist/unique-key.d.ts +58 -0
- package/dist/unique-key.js +122 -0
- package/dist/worker.d.ts +307 -0
- package/dist/worker.js +396 -0
- package/package.json +34 -0
package/dist/query.js
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
// Copyright (c) 2026 Chris Corbyn <chris@zizq.io>
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
/** Maximum page size the server will accept. */
|
|
4
|
+
const MAX_PAGE_SIZE = 2000;
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Lazy<T>
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/**
|
|
9
|
+
* Base class for lazy, async-iterable collections.
|
|
10
|
+
*
|
|
11
|
+
* Subclasses only need to implement `[Symbol.asyncIterator]`. All of the
|
|
12
|
+
* terminal helpers (`toArray`, `count`, `first`, `isEmpty`, `forEach`) and
|
|
13
|
+
* the lazy transforms (`map`, `filter`) are provided and work against the
|
|
14
|
+
* iterator.
|
|
15
|
+
*
|
|
16
|
+
* `map` and `filter` return a fresh `Lazy<U>` wrapper so that callers can
|
|
17
|
+
* continue chaining helpers (e.g. `query.map(...).filter(...).toArray()`)
|
|
18
|
+
* without having to reach for `[Symbol.asyncIterator]()` or `Array.fromAsync`
|
|
19
|
+
* manually.
|
|
20
|
+
*/
|
|
21
|
+
export class Lazy {
|
|
22
|
+
/**
|
|
23
|
+
* Collect all items into an array.
|
|
24
|
+
*
|
|
25
|
+
* Delegates to the built-in `Array.fromAsync()` helper.
|
|
26
|
+
*/
|
|
27
|
+
async toArray() {
|
|
28
|
+
return Array.fromAsync(this);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Count all items.
|
|
32
|
+
*
|
|
33
|
+
* Iterates lazily, holding only one item at a time in memory. The
|
|
34
|
+
* underlying iterator is responsible for doing this efficiently — for
|
|
35
|
+
* paginated queries this means `ceil(N / pageSize)` HTTP requests.
|
|
36
|
+
*/
|
|
37
|
+
async count() {
|
|
38
|
+
let n = 0;
|
|
39
|
+
for await (const _ of this)
|
|
40
|
+
n++;
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
/** Return the first item, or `undefined` if the collection is empty. */
|
|
44
|
+
async first() {
|
|
45
|
+
for await (const item of this)
|
|
46
|
+
return item;
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
/** Return `true` if there are no items. */
|
|
50
|
+
async isEmpty() {
|
|
51
|
+
return (await this.first()) === undefined;
|
|
52
|
+
}
|
|
53
|
+
/** Invoke `fn` for each item, awaiting async results sequentially. */
|
|
54
|
+
async forEach(fn) {
|
|
55
|
+
for await (const item of this) {
|
|
56
|
+
await fn(item);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Lazily transform each item. Returns a new `Lazy`. */
|
|
60
|
+
map(fn) {
|
|
61
|
+
return new MappedLazy(this, fn);
|
|
62
|
+
}
|
|
63
|
+
/** Lazily keep only items for which `fn` returns a truthy value. */
|
|
64
|
+
filter(fn) {
|
|
65
|
+
return new FilteredLazy(this, fn);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** @internal */
|
|
69
|
+
class MappedLazy extends Lazy {
|
|
70
|
+
source;
|
|
71
|
+
fn;
|
|
72
|
+
constructor(source, fn) {
|
|
73
|
+
super();
|
|
74
|
+
this.source = source;
|
|
75
|
+
this.fn = fn;
|
|
76
|
+
}
|
|
77
|
+
async *[Symbol.asyncIterator]() {
|
|
78
|
+
for await (const item of this.source) {
|
|
79
|
+
yield await this.fn(item);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** @internal */
|
|
84
|
+
class FilteredLazy extends Lazy {
|
|
85
|
+
source;
|
|
86
|
+
fn;
|
|
87
|
+
constructor(source, fn) {
|
|
88
|
+
super();
|
|
89
|
+
this.source = source;
|
|
90
|
+
this.fn = fn;
|
|
91
|
+
}
|
|
92
|
+
async *[Symbol.asyncIterator]() {
|
|
93
|
+
for await (const item of this.source) {
|
|
94
|
+
if (await this.fn(item))
|
|
95
|
+
yield item;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Lazy, paginating query builder for jobs.
|
|
101
|
+
*
|
|
102
|
+
* Created by `client.jobs()`. Each builder method returns a new instance
|
|
103
|
+
* (immutable). Iteration is lazy — no HTTP requests are made until the
|
|
104
|
+
* iterator is consumed or a terminal method is called.
|
|
105
|
+
*
|
|
106
|
+
* Implements `Symbol.asyncIterator`, so a `JobQuery` can be used directly
|
|
107
|
+
* in `for await...of` loops, or handed to `Array.fromAsync()` (with an
|
|
108
|
+
* optional mapper). To use the Node 22+ async iterator helpers
|
|
109
|
+
* (`.map()`, `.filter()`, etc.), obtain the iterator explicitly with
|
|
110
|
+
* `[Symbol.asyncIterator]()` first — those helpers live on
|
|
111
|
+
* `AsyncIterator.prototype`, not on async iterables.
|
|
112
|
+
*
|
|
113
|
+
* @example Iterating, mapping, and collecting
|
|
114
|
+
* ```ts
|
|
115
|
+
* // Plain loop
|
|
116
|
+
* for await (const job of client.jobs().byQueue("emails")) {
|
|
117
|
+
* console.log(job.id);
|
|
118
|
+
* }
|
|
119
|
+
*
|
|
120
|
+
* // Array.fromAsync with a mapper
|
|
121
|
+
* const ids = await Array.fromAsync(
|
|
122
|
+
* client.jobs().byQueue("emails"),
|
|
123
|
+
* (job) => job.id,
|
|
124
|
+
* );
|
|
125
|
+
*
|
|
126
|
+
* // Iterator helpers via [Symbol.asyncIterator]()
|
|
127
|
+
* const urgent = await client.jobs()
|
|
128
|
+
* .byQueue("emails")
|
|
129
|
+
* [Symbol.asyncIterator]()
|
|
130
|
+
* .filter((job) => (job.payload as any).urgent)
|
|
131
|
+
* .toArray();
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* // Count ready jobs on a queue (paginates server-side; O(1) memory)
|
|
137
|
+
* const n = await client.jobs()
|
|
138
|
+
* .byQueue("emails")
|
|
139
|
+
* .byStatus("ready")
|
|
140
|
+
* .count();
|
|
141
|
+
*
|
|
142
|
+
* // Move all jobs from one queue to another
|
|
143
|
+
* await client.jobs().byQueue("old").updateAll({ queue: "new" });
|
|
144
|
+
*
|
|
145
|
+
* // Delete dead jobs matching a payload subset
|
|
146
|
+
* await client.jobs()
|
|
147
|
+
* .byStatus("dead")
|
|
148
|
+
* .withPayloadSubset({ userId: 42 })
|
|
149
|
+
* .deleteAll();
|
|
150
|
+
*
|
|
151
|
+
* // Iterate in batches
|
|
152
|
+
* for await (const page of client.jobs().inPagesOf(100).pages()) {
|
|
153
|
+
* for (const job of page.jobs) console.log(job.id);
|
|
154
|
+
* }
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export class JobQuery extends Lazy {
|
|
158
|
+
client;
|
|
159
|
+
_id;
|
|
160
|
+
_queue;
|
|
161
|
+
_type;
|
|
162
|
+
_status;
|
|
163
|
+
_jqFilter;
|
|
164
|
+
_order;
|
|
165
|
+
_limit;
|
|
166
|
+
_pageSize;
|
|
167
|
+
/** @internal */
|
|
168
|
+
constructor(client, options = {}) {
|
|
169
|
+
super();
|
|
170
|
+
this.client = client;
|
|
171
|
+
this._id = options.id;
|
|
172
|
+
this._queue = options.queue;
|
|
173
|
+
this._type = options.type;
|
|
174
|
+
this._status = options.status;
|
|
175
|
+
this._jqFilter = options.jqFilter;
|
|
176
|
+
this._order = options.order;
|
|
177
|
+
this._limit = options.limit;
|
|
178
|
+
this._pageSize = options.pageSize;
|
|
179
|
+
}
|
|
180
|
+
// --- Filter builders ---
|
|
181
|
+
/** Filter by job ID. Replaces any existing ID filter. */
|
|
182
|
+
byId(id) {
|
|
183
|
+
return this.rebuild({ id });
|
|
184
|
+
}
|
|
185
|
+
/** Add an ID to the existing ID filter (unions with existing). */
|
|
186
|
+
addId(id) {
|
|
187
|
+
return this.rebuild({ id: concat(this._id, id) });
|
|
188
|
+
}
|
|
189
|
+
/** Filter by queue name. Replaces any existing queue filter. */
|
|
190
|
+
byQueue(queue) {
|
|
191
|
+
return this.rebuild({ queue });
|
|
192
|
+
}
|
|
193
|
+
/** Add a queue to the existing queue filter. */
|
|
194
|
+
addQueue(queue) {
|
|
195
|
+
return this.rebuild({ queue: concat(this._queue, queue) });
|
|
196
|
+
}
|
|
197
|
+
/** Filter by job type. Replaces any existing type filter. */
|
|
198
|
+
byType(type) {
|
|
199
|
+
return this.rebuild({ type });
|
|
200
|
+
}
|
|
201
|
+
/** Add a type to the existing type filter. */
|
|
202
|
+
addType(type) {
|
|
203
|
+
return this.rebuild({ type: concat(this._type, type) });
|
|
204
|
+
}
|
|
205
|
+
/** Filter by status. Replaces any existing status filter. */
|
|
206
|
+
byStatus(status) {
|
|
207
|
+
return this.rebuild({ status });
|
|
208
|
+
}
|
|
209
|
+
/** Add a status to the existing status filter. */
|
|
210
|
+
addStatus(status) {
|
|
211
|
+
return this.rebuild({ status: concat(this._status, status) });
|
|
212
|
+
}
|
|
213
|
+
/** Replace the jq payload filter expression. */
|
|
214
|
+
byJqFilter(jqFilter) {
|
|
215
|
+
return this.rebuild({ jqFilter });
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Add a jq payload filter. Combines with any existing filter via `and`.
|
|
219
|
+
*/
|
|
220
|
+
addJqFilter(jqFilter) {
|
|
221
|
+
return this.rebuild({
|
|
222
|
+
jqFilter: concat(this._jqFilter, `(${jqFilter})`).join(" and "),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Constrain results by an exact payload match.
|
|
227
|
+
*
|
|
228
|
+
* Layers onto any existing jq filter via `and`. The emitted jq
|
|
229
|
+
* expression is `. == <JSON>`, so matching is structural and
|
|
230
|
+
* key-order independent (jq's object equality sorts keys).
|
|
231
|
+
*
|
|
232
|
+
* This is syntactic sugar around `addJqFilter()` so it can be combined with
|
|
233
|
+
* other jq filter methods.
|
|
234
|
+
*/
|
|
235
|
+
withPayload(payload) {
|
|
236
|
+
return this.addJqFilter(`. == ${JSON.stringify(payload)}`);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Constrain results by a payload subset.
|
|
240
|
+
*
|
|
241
|
+
* Layers onto any existing jq filter via `and`. The emitted jq
|
|
242
|
+
* expression depends on the shape of `payload`:
|
|
243
|
+
*
|
|
244
|
+
* - **Objects**: `. | contains(<JSON>)` — deep subset check; every key in
|
|
245
|
+
* `payload` must exist in the job payload with a contained value.
|
|
246
|
+
* - **Arrays**: `.[0:N] == <JSON>` — prefix match against the first `N`
|
|
247
|
+
* elements of the job payload.
|
|
248
|
+
* - **Scalars** (number, string, boolean, null): `. == <JSON>` — equality.
|
|
249
|
+
*
|
|
250
|
+
* This is syntactic sugar around `addJqFilter()` so it can be combined with
|
|
251
|
+
* other jq filter methods.
|
|
252
|
+
*/
|
|
253
|
+
withPayloadSubset(payload) {
|
|
254
|
+
return this.addJqFilter(payloadSubsetFilter(payload));
|
|
255
|
+
}
|
|
256
|
+
// --- Ordering, limiting, paging ---
|
|
257
|
+
/** Set the sort order. */
|
|
258
|
+
order(direction) {
|
|
259
|
+
return this.rebuild({ order: direction });
|
|
260
|
+
}
|
|
261
|
+
/** Reverse the sort order. Defaults to "desc" if no order was set. */
|
|
262
|
+
reverseOrder() {
|
|
263
|
+
return this.rebuild({ order: this._order === "desc" ? "asc" : "desc" });
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Set the maximum total number of jobs to return across all pages.
|
|
267
|
+
*
|
|
268
|
+
* Also caps page fetches and limits the number of jobs touched by
|
|
269
|
+
* `updateAll` / `deleteAll`.
|
|
270
|
+
*/
|
|
271
|
+
limit(n) {
|
|
272
|
+
return this.rebuild({ limit: n });
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Set the page size for pagination.
|
|
276
|
+
*
|
|
277
|
+
* Affects how many jobs are fetched per HTTP request during iteration,
|
|
278
|
+
* and also causes `updateAll` / `deleteAll` to batch per page using
|
|
279
|
+
* ID-scoped bulk operations.
|
|
280
|
+
*/
|
|
281
|
+
inPagesOf(n) {
|
|
282
|
+
return this.rebuild({ pageSize: n });
|
|
283
|
+
}
|
|
284
|
+
// --- Terminal methods ---
|
|
285
|
+
/**
|
|
286
|
+
* Return the first matching job, or `undefined` if none.
|
|
287
|
+
*
|
|
288
|
+
* Optimised override: pushes `limit=1` to the server instead of
|
|
289
|
+
* iterating a full default page.
|
|
290
|
+
*/
|
|
291
|
+
async first() {
|
|
292
|
+
for await (const job of this.limit(1)) {
|
|
293
|
+
return job;
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Return the last matching job, or `undefined` if none.
|
|
299
|
+
*
|
|
300
|
+
* Optimised: reverses the order and fetches a single job.
|
|
301
|
+
*/
|
|
302
|
+
async last() {
|
|
303
|
+
return this.reverseOrder().first();
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Update all matching jobs.
|
|
307
|
+
*
|
|
308
|
+
* Behaviour depends on whether `limit` or `pageSize` is set:
|
|
309
|
+
*
|
|
310
|
+
* - **Unbounded**: a single bulk update request is sent with the query
|
|
311
|
+
* filters as the scope.
|
|
312
|
+
* - **Bounded**: pages are iterated and a separate bulk update is issued
|
|
313
|
+
* per page, scoped to both the query filters and the IDs on that page.
|
|
314
|
+
* This is safe against races with newly enqueued jobs and lets callers
|
|
315
|
+
* throttle large update operations via `inPagesOf`.
|
|
316
|
+
*
|
|
317
|
+
* Returns the total number of updated jobs.
|
|
318
|
+
*/
|
|
319
|
+
async updateAll(apply) {
|
|
320
|
+
const where = this.toWhere();
|
|
321
|
+
if (this._limit == null && this._pageSize == null) {
|
|
322
|
+
return this.client.updateAllJobs({ where, apply });
|
|
323
|
+
}
|
|
324
|
+
let remaining = this._limit;
|
|
325
|
+
let updated = 0;
|
|
326
|
+
for await (const page of this.pages()) {
|
|
327
|
+
let ids = page.jobs.map((j) => j.id);
|
|
328
|
+
if (remaining != null) {
|
|
329
|
+
if (remaining <= 0)
|
|
330
|
+
break;
|
|
331
|
+
if (ids.length > remaining)
|
|
332
|
+
ids = ids.slice(0, remaining);
|
|
333
|
+
}
|
|
334
|
+
if (ids.length === 0)
|
|
335
|
+
continue;
|
|
336
|
+
updated += await this.client.updateAllJobs({
|
|
337
|
+
where: { ...where, id: ids },
|
|
338
|
+
apply,
|
|
339
|
+
});
|
|
340
|
+
if (remaining != null) {
|
|
341
|
+
remaining -= ids.length;
|
|
342
|
+
if (remaining <= 0)
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return updated;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Update the first matching job.
|
|
350
|
+
*
|
|
351
|
+
* Returns 1 if a job was updated, 0 if no jobs matched.
|
|
352
|
+
*/
|
|
353
|
+
async updateOne(apply) {
|
|
354
|
+
return this.limit(1).updateAll(apply);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Delete all matching jobs.
|
|
358
|
+
*
|
|
359
|
+
* Behaviour depends on whether `limit` or `pageSize` is set:
|
|
360
|
+
*
|
|
361
|
+
* - **Unbounded**: a single bulk delete request is sent with the query
|
|
362
|
+
* filters as the scope. A query with no filters will delete *all* jobs
|
|
363
|
+
* on the server — useful in tests, dangerous in production.
|
|
364
|
+
* - **Bounded**: pages are iterated and a separate bulk delete is issued
|
|
365
|
+
* per page, scoped to both the query filters and the IDs on that page.
|
|
366
|
+
*
|
|
367
|
+
* Returns the total number of deleted jobs.
|
|
368
|
+
*/
|
|
369
|
+
async deleteAll() {
|
|
370
|
+
const where = this.toWhere();
|
|
371
|
+
if (this._limit == null && this._pageSize == null) {
|
|
372
|
+
return this.client.deleteAllJobs({ where });
|
|
373
|
+
}
|
|
374
|
+
let remaining = this._limit;
|
|
375
|
+
let deleted = 0;
|
|
376
|
+
for await (const page of this.pages()) {
|
|
377
|
+
let ids = page.jobs.map((j) => j.id);
|
|
378
|
+
if (remaining != null) {
|
|
379
|
+
if (remaining <= 0)
|
|
380
|
+
break;
|
|
381
|
+
if (ids.length > remaining)
|
|
382
|
+
ids = ids.slice(0, remaining);
|
|
383
|
+
}
|
|
384
|
+
if (ids.length === 0)
|
|
385
|
+
continue;
|
|
386
|
+
deleted += await this.client.deleteAllJobs({
|
|
387
|
+
where: { ...where, id: ids },
|
|
388
|
+
});
|
|
389
|
+
if (remaining != null) {
|
|
390
|
+
remaining -= ids.length;
|
|
391
|
+
if (remaining <= 0)
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return deleted;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Delete the first matching job.
|
|
399
|
+
*
|
|
400
|
+
* Returns 1 if a job was deleted, 0 if no jobs matched.
|
|
401
|
+
*/
|
|
402
|
+
async deleteOne() {
|
|
403
|
+
return this.limit(1).deleteAll();
|
|
404
|
+
}
|
|
405
|
+
// --- Iteration ---
|
|
406
|
+
/**
|
|
407
|
+
* Async iterator over individual jobs.
|
|
408
|
+
*
|
|
409
|
+
* Lazily paginates through results, respecting `limit` if set. Enables
|
|
410
|
+
* `for await...of` loops and is also consumable by `Array.fromAsync()`.
|
|
411
|
+
*/
|
|
412
|
+
async *[Symbol.asyncIterator]() {
|
|
413
|
+
let remaining = this._limit;
|
|
414
|
+
for await (const page of this.pages()) {
|
|
415
|
+
for (const job of page.jobs) {
|
|
416
|
+
if (remaining != null) {
|
|
417
|
+
if (remaining <= 0)
|
|
418
|
+
return;
|
|
419
|
+
remaining--;
|
|
420
|
+
}
|
|
421
|
+
yield job;
|
|
422
|
+
}
|
|
423
|
+
if (remaining != null && remaining <= 0)
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Async iterator over pages of jobs.
|
|
429
|
+
*
|
|
430
|
+
* Each yielded value is a `JobPage`. Useful when you want to process jobs
|
|
431
|
+
* in batches. Terminates once the query's `limit` has been reached (does
|
|
432
|
+
* not truncate the final page).
|
|
433
|
+
*/
|
|
434
|
+
async *pages() {
|
|
435
|
+
const effectivePageSize = clampPageSize(this._pageSize, this._limit);
|
|
436
|
+
let remaining = this._limit;
|
|
437
|
+
let page = await this.client.listJobs({
|
|
438
|
+
id: this._id,
|
|
439
|
+
queue: this._queue,
|
|
440
|
+
type: this._type,
|
|
441
|
+
status: this._status,
|
|
442
|
+
filter: this._jqFilter,
|
|
443
|
+
order: this._order,
|
|
444
|
+
limit: effectivePageSize,
|
|
445
|
+
});
|
|
446
|
+
while (page) {
|
|
447
|
+
yield page;
|
|
448
|
+
if (remaining != null) {
|
|
449
|
+
remaining -= page.jobs.length;
|
|
450
|
+
if (remaining <= 0)
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
page = await page.nextPage();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// --- Internal helpers ---
|
|
457
|
+
toWhere() {
|
|
458
|
+
return {
|
|
459
|
+
id: this._id,
|
|
460
|
+
queue: this._queue,
|
|
461
|
+
type: this._type,
|
|
462
|
+
status: this._status,
|
|
463
|
+
filter: this._jqFilter,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
rebuild(overrides) {
|
|
467
|
+
return new JobQuery(this.client, {
|
|
468
|
+
id: this._id,
|
|
469
|
+
queue: this._queue,
|
|
470
|
+
type: this._type,
|
|
471
|
+
status: this._status,
|
|
472
|
+
jqFilter: this._jqFilter,
|
|
473
|
+
order: this._order,
|
|
474
|
+
limit: this._limit,
|
|
475
|
+
pageSize: this._pageSize,
|
|
476
|
+
...overrides,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Lazy async iterator over error records with a chainable builder API.
|
|
482
|
+
*
|
|
483
|
+
* Created by `Job.errors()`. Each builder method returns a new instance
|
|
484
|
+
* (immutable). Iteration is lazy — no HTTP requests are made until the
|
|
485
|
+
* iterator is consumed.
|
|
486
|
+
*
|
|
487
|
+
* @example
|
|
488
|
+
* ```ts
|
|
489
|
+
* // Iterate all errors
|
|
490
|
+
* for await (const error of job.errors()) {
|
|
491
|
+
* console.log(`Attempt ${error.attempt}: ${error.message}`);
|
|
492
|
+
* }
|
|
493
|
+
*
|
|
494
|
+
* // With builder options
|
|
495
|
+
* const recent = await job.errors()
|
|
496
|
+
* .order("desc")
|
|
497
|
+
* .limit(5)
|
|
498
|
+
* .toArray();
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
export class ErrorQuery extends Lazy {
|
|
502
|
+
client;
|
|
503
|
+
jobId;
|
|
504
|
+
_order;
|
|
505
|
+
_limit;
|
|
506
|
+
_pageSize;
|
|
507
|
+
/** @internal */
|
|
508
|
+
constructor(client, jobId, options) {
|
|
509
|
+
super();
|
|
510
|
+
this.client = client;
|
|
511
|
+
this.jobId = jobId;
|
|
512
|
+
this._order = options?.order;
|
|
513
|
+
this._limit = options?.limit;
|
|
514
|
+
this._pageSize = options?.pageSize;
|
|
515
|
+
}
|
|
516
|
+
/** Set the sort order. Returns a new query. */
|
|
517
|
+
order(direction) {
|
|
518
|
+
return this.rebuild({ order: direction });
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Set the maximum total number of errors to return across all pages.
|
|
522
|
+
*
|
|
523
|
+
* Also used to optimise the page size — if the limit is smaller than
|
|
524
|
+
* the page size, only the needed amount is fetched.
|
|
525
|
+
*/
|
|
526
|
+
limit(n) {
|
|
527
|
+
return this.rebuild({ limit: n });
|
|
528
|
+
}
|
|
529
|
+
/** Set the page size for pagination. Returns a new query. */
|
|
530
|
+
inPagesOf(n) {
|
|
531
|
+
return this.rebuild({ pageSize: n });
|
|
532
|
+
}
|
|
533
|
+
/** Reverse the sort order. Returns a new query. */
|
|
534
|
+
reverseOrder() {
|
|
535
|
+
return this.rebuild({ order: this._order === "desc" ? "asc" : "desc" });
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Return the first error, or `undefined` if none.
|
|
539
|
+
*
|
|
540
|
+
* Optimised override: pushes `limit=1` to the server instead of
|
|
541
|
+
* iterating a full default page.
|
|
542
|
+
*/
|
|
543
|
+
async first() {
|
|
544
|
+
for await (const error of this.limit(1)) {
|
|
545
|
+
return error;
|
|
546
|
+
}
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Return the last error, or `undefined` if none.
|
|
551
|
+
*
|
|
552
|
+
* Optimised: reverses the order and fetches a single error.
|
|
553
|
+
*/
|
|
554
|
+
async last() {
|
|
555
|
+
return this.reverseOrder().first();
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Async iterator over individual error records.
|
|
559
|
+
*
|
|
560
|
+
* Lazily paginates through results, respecting `limit` if set.
|
|
561
|
+
* Enables `for await...of` loops and is consumable by `Array.fromAsync()`.
|
|
562
|
+
*/
|
|
563
|
+
async *[Symbol.asyncIterator]() {
|
|
564
|
+
let remaining = this._limit;
|
|
565
|
+
const effectivePageSize = clampPageSize(this._pageSize, this._limit);
|
|
566
|
+
let page = await this.client.listErrors(this.jobId, {
|
|
567
|
+
order: this._order,
|
|
568
|
+
limit: effectivePageSize,
|
|
569
|
+
});
|
|
570
|
+
while (true) {
|
|
571
|
+
for (const error of page) {
|
|
572
|
+
if (remaining != null) {
|
|
573
|
+
if (remaining <= 0)
|
|
574
|
+
return;
|
|
575
|
+
remaining--;
|
|
576
|
+
}
|
|
577
|
+
yield error;
|
|
578
|
+
}
|
|
579
|
+
if (!page.hasNext)
|
|
580
|
+
return;
|
|
581
|
+
if (remaining != null && remaining <= 0)
|
|
582
|
+
return;
|
|
583
|
+
page = (await page.nextPage());
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Async iterator over pages of error records.
|
|
588
|
+
*
|
|
589
|
+
* Each yielded value is an `ErrorPage`. Useful when you want to
|
|
590
|
+
* process errors in batches.
|
|
591
|
+
*/
|
|
592
|
+
async *pages() {
|
|
593
|
+
let remaining = this._limit;
|
|
594
|
+
const effectivePageSize = clampPageSize(this._pageSize, this._limit);
|
|
595
|
+
let page = await this.client.listErrors(this.jobId, {
|
|
596
|
+
order: this._order,
|
|
597
|
+
limit: effectivePageSize,
|
|
598
|
+
});
|
|
599
|
+
while (page) {
|
|
600
|
+
yield page;
|
|
601
|
+
if (remaining != null) {
|
|
602
|
+
remaining -= page.errors.length;
|
|
603
|
+
if (remaining <= 0)
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
page = await page.nextPage();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
rebuild(overrides) {
|
|
610
|
+
return new ErrorQuery(this.client, this.jobId, {
|
|
611
|
+
order: this._order,
|
|
612
|
+
limit: this._limit,
|
|
613
|
+
pageSize: this._pageSize,
|
|
614
|
+
...overrides,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
// Module-local helpers
|
|
620
|
+
// ---------------------------------------------------------------------------
|
|
621
|
+
/**
|
|
622
|
+
* Compute the effective per-request page size.
|
|
623
|
+
*
|
|
624
|
+
* Takes the smallest of the caller-provided `pageSize`, the overall
|
|
625
|
+
* `limit`, and `MAX_PAGE_SIZE`. Returns `undefined` when none are set,
|
|
626
|
+
* which lets the server apply its own default.
|
|
627
|
+
*/
|
|
628
|
+
function clampPageSize(pageSize, limit) {
|
|
629
|
+
const candidates = [pageSize, limit, MAX_PAGE_SIZE].filter((v) => v != null);
|
|
630
|
+
if (candidates.length === 0)
|
|
631
|
+
return undefined;
|
|
632
|
+
return Math.min(...candidates);
|
|
633
|
+
}
|
|
634
|
+
/** Combine two scalar-or-array values into a single flattened array. */
|
|
635
|
+
function concat(existing, next) {
|
|
636
|
+
return [].concat(existing ?? []).concat(next ?? []);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Build a jq subset filter for a payload value.
|
|
640
|
+
*
|
|
641
|
+
* - Object → `. | contains(<JSON>)`
|
|
642
|
+
* - Array → `.[0:N] == <JSON>` (prefix match)
|
|
643
|
+
* - Scalar → `. == <JSON>`
|
|
644
|
+
*/
|
|
645
|
+
function payloadSubsetFilter(payload) {
|
|
646
|
+
if (payload === null || typeof payload !== "object") {
|
|
647
|
+
return `. == ${JSON.stringify(payload)}`;
|
|
648
|
+
}
|
|
649
|
+
if (Array.isArray(payload)) {
|
|
650
|
+
return `.[0:${payload.length}] == ${JSON.stringify(payload)}`;
|
|
651
|
+
}
|
|
652
|
+
return `. | contains(${JSON.stringify(payload)})`;
|
|
653
|
+
}
|