@tanstack/pacer 0.21.1 → 0.23.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.
Files changed (101) hide show
  1. package/README.md +21 -6
  2. package/dist/async-batcher.d.ts +11 -14
  3. package/dist/async-batcher.js +186 -132
  4. package/dist/async-debouncer.d.ts +10 -13
  5. package/dist/async-debouncer.js +187 -146
  6. package/dist/async-queuer.d.ts +12 -14
  7. package/dist/async-queuer.js +384 -259
  8. package/dist/async-rate-limiter.d.ts +9 -12
  9. package/dist/async-rate-limiter.js +198 -147
  10. package/dist/async-retryer.d.ts +10 -10
  11. package/dist/async-retryer.js +209 -188
  12. package/dist/async-throttler.d.ts +10 -13
  13. package/dist/async-throttler.js +215 -161
  14. package/dist/batcher.d.ts +5 -8
  15. package/dist/batcher.js +107 -87
  16. package/dist/debouncer.d.ts +6 -9
  17. package/dist/debouncer.js +99 -86
  18. package/dist/event-client.d.ts +8 -11
  19. package/dist/event-client.js +1 -2
  20. package/dist/index.js +1 -1
  21. package/dist/queuer.d.ts +8 -10
  22. package/dist/queuer.js +277 -207
  23. package/dist/rate-limiter.d.ts +6 -9
  24. package/dist/rate-limiter.js +127 -109
  25. package/dist/throttler.d.ts +6 -9
  26. package/dist/throttler.js +127 -90
  27. package/dist/types.d.ts +4 -6
  28. package/dist/utils.d.ts +3 -6
  29. package/dist/utils.js +1 -2
  30. package/package.json +23 -70
  31. package/dist/async-batcher.cjs +0 -337
  32. package/dist/async-batcher.cjs.map +0 -1
  33. package/dist/async-batcher.d.cts +0 -344
  34. package/dist/async-batcher.js.map +0 -1
  35. package/dist/async-debouncer.cjs +0 -330
  36. package/dist/async-debouncer.cjs.map +0 -1
  37. package/dist/async-debouncer.d.cts +0 -300
  38. package/dist/async-debouncer.js.map +0 -1
  39. package/dist/async-queuer.cjs +0 -484
  40. package/dist/async-queuer.cjs.map +0 -1
  41. package/dist/async-queuer.d.cts +0 -440
  42. package/dist/async-queuer.js.map +0 -1
  43. package/dist/async-rate-limiter.cjs +0 -373
  44. package/dist/async-rate-limiter.cjs.map +0 -1
  45. package/dist/async-rate-limiter.d.cts +0 -357
  46. package/dist/async-rate-limiter.js.map +0 -1
  47. package/dist/async-retryer.cjs +0 -374
  48. package/dist/async-retryer.cjs.map +0 -1
  49. package/dist/async-retryer.d.cts +0 -319
  50. package/dist/async-retryer.js.map +0 -1
  51. package/dist/async-throttler.cjs +0 -347
  52. package/dist/async-throttler.cjs.map +0 -1
  53. package/dist/async-throttler.d.cts +0 -320
  54. package/dist/async-throttler.js.map +0 -1
  55. package/dist/batcher.cjs +0 -200
  56. package/dist/batcher.cjs.map +0 -1
  57. package/dist/batcher.d.cts +0 -180
  58. package/dist/batcher.js.map +0 -1
  59. package/dist/debouncer.cjs +0 -203
  60. package/dist/debouncer.cjs.map +0 -1
  61. package/dist/debouncer.d.cts +0 -167
  62. package/dist/debouncer.js.map +0 -1
  63. package/dist/event-client.cjs +0 -64
  64. package/dist/event-client.cjs.map +0 -1
  65. package/dist/event-client.d.cts +0 -66
  66. package/dist/event-client.js.map +0 -1
  67. package/dist/index.cjs +0 -52
  68. package/dist/index.d.cts +0 -15
  69. package/dist/queuer.cjs +0 -401
  70. package/dist/queuer.cjs.map +0 -1
  71. package/dist/queuer.d.cts +0 -345
  72. package/dist/queuer.js.map +0 -1
  73. package/dist/rate-limiter.cjs +0 -263
  74. package/dist/rate-limiter.cjs.map +0 -1
  75. package/dist/rate-limiter.d.cts +0 -215
  76. package/dist/rate-limiter.js.map +0 -1
  77. package/dist/throttler.cjs +0 -215
  78. package/dist/throttler.cjs.map +0 -1
  79. package/dist/throttler.d.cts +0 -207
  80. package/dist/throttler.js.map +0 -1
  81. package/dist/types.cjs +0 -0
  82. package/dist/types.d.cts +0 -13
  83. package/dist/utils.cjs +0 -14
  84. package/dist/utils.cjs.map +0 -1
  85. package/dist/utils.d.cts +0 -8
  86. package/dist/utils.js.map +0 -1
  87. package/src/async-batcher.ts +0 -594
  88. package/src/async-debouncer.ts +0 -565
  89. package/src/async-queuer.ts +0 -925
  90. package/src/async-rate-limiter.ts +0 -647
  91. package/src/async-retryer.ts +0 -684
  92. package/src/async-throttler.ts +0 -633
  93. package/src/batcher.ts +0 -329
  94. package/src/debouncer.ts +0 -334
  95. package/src/event-client.ts +0 -129
  96. package/src/index.ts +0 -24
  97. package/src/queuer.ts +0 -740
  98. package/src/rate-limiter.ts +0 -429
  99. package/src/throttler.ts +0 -380
  100. package/src/types.ts +0 -12
  101. package/src/utils.ts +0 -12
@@ -1,6 +1,6 @@
1
1
  import { parseFunctionOrValue } from "./utils.js";
2
- import { emitChange, pacerEventClient } from "./event-client.js";
3
2
  import { AsyncRetryer } from "./async-retryer.js";
3
+ import { emitChange, pacerEventClient } from "./event-client.js";
4
4
  import { Store } from "@tanstack/store";
5
5
 
6
6
  //#region src/async-queuer.ts
@@ -9,7 +9,7 @@ function getDefaultAsyncQueuerState() {
9
9
  activeItems: [],
10
10
  addItemCount: 0,
11
11
  errorCount: 0,
12
- executeCount: 0,
12
+ executionCount: 0,
13
13
  expirationCount: 0,
14
14
  isEmpty: true,
15
15
  isExecuting: false,
@@ -21,7 +21,7 @@ function getDefaultAsyncQueuerState() {
21
21
  lastResult: null,
22
22
  pendingTick: false,
23
23
  rejectionCount: 0,
24
- settledCount: 0,
24
+ settleCount: 0,
25
25
  size: 0,
26
26
  status: "idle",
27
27
  successCount: 0
@@ -111,255 +111,14 @@ const defaultOptions = {
111
111
  * ```
112
112
  */
113
113
  var AsyncQueuer = class {
114
- #timeoutIds;
114
+ fn;
115
+ store = new Store(getDefaultAsyncQueuerState());
116
+ key;
117
+ options;
118
+ asyncRetryers = /* @__PURE__ */ new Map();
119
+ #timeoutIds = /* @__PURE__ */ new Set();
115
120
  constructor(fn, initialOptions = {}) {
116
121
  this.fn = fn;
117
- this.store = new Store(getDefaultAsyncQueuerState());
118
- this.asyncRetryers = /* @__PURE__ */ new Map();
119
- this.#timeoutIds = /* @__PURE__ */ new Set();
120
- this.setOptions = (newOptions) => {
121
- this.options = {
122
- ...this.options,
123
- ...newOptions
124
- };
125
- };
126
- this.#setState = (newState) => {
127
- this.store.setState((state) => {
128
- const combinedState = {
129
- ...state,
130
- ...newState
131
- };
132
- const { activeItems, items, isRunning } = combinedState;
133
- const size = items.length;
134
- const isFull = size >= (this.options.maxSize ?? Infinity);
135
- const isEmpty = size === 0;
136
- const isIdle = isRunning && isEmpty && activeItems.length === 0;
137
- const status = isIdle ? "idle" : isRunning ? "running" : "stopped";
138
- return {
139
- ...combinedState,
140
- isEmpty,
141
- isFull,
142
- isIdle,
143
- size,
144
- status
145
- };
146
- });
147
- emitChange("AsyncQueuer", this);
148
- };
149
- this.#getWait = () => {
150
- return parseFunctionOrValue(this.options.wait ?? 0, this);
151
- };
152
- this.#getConcurrency = () => {
153
- return parseFunctionOrValue(this.options.concurrency ?? 1, this);
154
- };
155
- this.#tick = () => {
156
- if (!this.store.state.isRunning) {
157
- this.#setState({ pendingTick: false });
158
- return;
159
- }
160
- this.#setState({ pendingTick: true });
161
- this.#checkExpiredItems();
162
- const activeItems = this.store.state.activeItems;
163
- while (activeItems.length < this.#getConcurrency() && this.store.state.items.length > 0) {
164
- const nextItem = this.peekNextItem();
165
- if (!nextItem) break;
166
- activeItems.push(nextItem);
167
- this.#setState({ activeItems });
168
- (async () => {
169
- await this.execute();
170
- const wait = this.#getWait();
171
- if (wait > 0) {
172
- const timeoutId = setTimeout(() => this.#tick(), wait);
173
- this.#timeoutIds.add(timeoutId);
174
- return;
175
- }
176
- this.#tick();
177
- })();
178
- }
179
- this.#setState({ pendingTick: false });
180
- };
181
- this.addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => {
182
- this.#setState({ addItemCount: this.store.state.addItemCount + 1 });
183
- if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
184
- this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
185
- this.options.onReject?.(item, this);
186
- return false;
187
- }
188
- const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item.priority;
189
- const items = this.store.state.items;
190
- const itemTimestamps = this.store.state.itemTimestamps;
191
- if (priority !== void 0) {
192
- const insertIndex = items.findIndex((existing) => {
193
- return (this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing.priority) < priority;
194
- });
195
- if (insertIndex === -1) {
196
- items.push(item);
197
- itemTimestamps.push(Date.now());
198
- } else {
199
- items.splice(insertIndex, 0, item);
200
- itemTimestamps.splice(insertIndex, 0, Date.now());
201
- }
202
- } else if (position === "front") {
203
- items.unshift(item);
204
- itemTimestamps.unshift(Date.now());
205
- } else {
206
- items.push(item);
207
- itemTimestamps.push(Date.now());
208
- }
209
- this.#setState({
210
- items,
211
- itemTimestamps
212
- });
213
- if (runOnItemsChange) this.options.onItemsChange?.(this);
214
- if (this.store.state.isRunning && !this.store.state.pendingTick) this.#tick();
215
- return true;
216
- };
217
- this.getNextItem = (position = this.options.getItemsFrom ?? "front") => {
218
- const { items, itemTimestamps } = this.store.state;
219
- let item;
220
- if (this.options.getPriority !== defaultOptions.getPriority || position === "front") {
221
- item = items[0];
222
- if (item !== void 0) this.#setState({
223
- items: items.slice(1),
224
- itemTimestamps: itemTimestamps.slice(1)
225
- });
226
- } else {
227
- item = items[items.length - 1];
228
- if (item !== void 0) this.#setState({
229
- items: items.slice(0, -1),
230
- itemTimestamps: itemTimestamps.slice(0, -1)
231
- });
232
- }
233
- if (item !== void 0) this.options.onItemsChange?.(this);
234
- return item;
235
- };
236
- this.#getAllItems = () => {
237
- const items = this.peekAllItems();
238
- this.clear();
239
- return items;
240
- };
241
- this.execute = async (position) => {
242
- const item = this.getNextItem(position);
243
- if (item !== void 0) {
244
- const currentExecuteCount = this.store.state.executeCount + 1;
245
- this.#setState({
246
- executeCount: currentExecuteCount,
247
- isExecuting: true
248
- });
249
- try {
250
- const currentAsyncRetryer = new AsyncRetryer(this.fn, {
251
- ...this.options.asyncRetryerOptions,
252
- key: `${this.key}-retryer-${currentExecuteCount}`
253
- });
254
- this.asyncRetryers.set(currentExecuteCount, currentAsyncRetryer);
255
- const lastResult = await currentAsyncRetryer.execute(item);
256
- this.#setState({
257
- successCount: this.store.state.successCount + 1,
258
- lastResult
259
- });
260
- this.options.onSuccess?.(lastResult, item, this);
261
- } catch (error) {
262
- this.#setState({ errorCount: this.store.state.errorCount + 1 });
263
- this.options.onError?.(error, item, this);
264
- if (this.options.throwOnError) throw error;
265
- } finally {
266
- this.asyncRetryers.delete(currentExecuteCount);
267
- this.#setState({
268
- activeItems: this.store.state.activeItems.filter((activeItem) => activeItem !== item),
269
- isExecuting: false,
270
- settledCount: this.store.state.settledCount + 1
271
- });
272
- this.options.onSettled?.(item, this);
273
- }
274
- }
275
- return item;
276
- };
277
- this.flush = async (numberOfItems = this.store.state.items.length, position) => {
278
- this.#clearTimeouts();
279
- await Promise.all(Array.from({ length: numberOfItems }, () => this.execute(position)));
280
- };
281
- this.flushAsBatch = async (batchFunction) => {
282
- this.#clearTimeouts();
283
- await batchFunction(this.#getAllItems());
284
- };
285
- this.#checkExpiredItems = () => {
286
- if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) return;
287
- const now = Date.now();
288
- const expiredIndices = [];
289
- for (let i = 0; i < this.store.state.items.length; i++) {
290
- const timestamp = this.store.state.itemTimestamps[i];
291
- if (timestamp === void 0) continue;
292
- const item = this.store.state.items[i];
293
- if (item === void 0) continue;
294
- if (this.options.getIsExpired !== defaultOptions.getIsExpired ? this.options.getIsExpired(item, timestamp) : now - timestamp > (this.options.expirationDuration ?? Infinity)) expiredIndices.push(i);
295
- }
296
- for (let i = expiredIndices.length - 1; i >= 0; i--) {
297
- const index = expiredIndices[i];
298
- if (index === void 0) continue;
299
- const expiredItem = this.store.state.items[index];
300
- if (expiredItem === void 0) continue;
301
- const newItems = [...this.store.state.items];
302
- const newTimestamps = [...this.store.state.itemTimestamps];
303
- newItems.splice(index, 1);
304
- newTimestamps.splice(index, 1);
305
- this.#setState({
306
- items: newItems,
307
- itemTimestamps: newTimestamps,
308
- expirationCount: this.store.state.expirationCount + 1
309
- });
310
- this.options.onExpire?.(expiredItem, this);
311
- }
312
- if (expiredIndices.length > 0) this.options.onItemsChange?.(this);
313
- };
314
- this.peekNextItem = (position = "front") => {
315
- if (position === "front") return this.store.state.items[0];
316
- return this.store.state.items[this.store.state.items.length - 1];
317
- };
318
- this.peekAllItems = () => {
319
- return [...this.peekActiveItems(), ...this.peekPendingItems()];
320
- };
321
- this.peekActiveItems = () => {
322
- return [...this.store.state.activeItems];
323
- };
324
- this.peekPendingItems = () => {
325
- return [...this.store.state.items];
326
- };
327
- this.start = () => {
328
- this.#setState({ isRunning: true });
329
- if (!this.store.state.pendingTick && this.store.state.items.length > 0) this.#tick();
330
- };
331
- this.stop = () => {
332
- this.#clearTimeouts();
333
- this.#setState({
334
- isRunning: false,
335
- pendingTick: false
336
- });
337
- };
338
- this.#clearTimeouts = () => {
339
- this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId));
340
- this.#timeoutIds.clear();
341
- };
342
- this.clear = () => {
343
- this.#setState({
344
- items: [],
345
- itemTimestamps: []
346
- });
347
- this.options.onItemsChange?.(this);
348
- };
349
- this.getAbortSignal = (executeCount) => {
350
- const count = executeCount ?? this.store.state.executeCount;
351
- return this.asyncRetryers.get(count)?.getAbortSignal() ?? null;
352
- };
353
- this.abort = () => {
354
- this.asyncRetryers.forEach((retryer) => retryer.abort());
355
- this.asyncRetryers.clear();
356
- this.#setState({ isExecuting: false });
357
- };
358
- this.reset = () => {
359
- this.#setState(getDefaultAsyncQueuerState());
360
- this.options.onItemsChange?.(this);
361
- this.asyncRetryers.forEach((retryer) => retryer.reset());
362
- };
363
122
  this.key = initialOptions.key;
364
123
  this.options = {
365
124
  ...defaultOptions,
@@ -384,28 +143,395 @@ var AsyncQueuer = class {
384
143
  this.setOptions(e.payload.options);
385
144
  });
386
145
  }
387
- #setState;
146
+ /**
147
+ * Updates the queuer options. New options are merged with existing options.
148
+ */
149
+ setOptions = (newOptions) => {
150
+ this.options = {
151
+ ...this.options,
152
+ ...newOptions
153
+ };
154
+ };
155
+ #setState = (newState) => {
156
+ this.store.setState((state) => {
157
+ const combinedState = {
158
+ ...state,
159
+ ...newState
160
+ };
161
+ const { activeItems, items, isRunning } = combinedState;
162
+ const size = items.length;
163
+ const isFull = size >= (this.options.maxSize ?? Infinity);
164
+ const isEmpty = size === 0;
165
+ const isIdle = isRunning && isEmpty && activeItems.length === 0;
166
+ const status = isIdle ? "idle" : isRunning ? "running" : "stopped";
167
+ return {
168
+ ...combinedState,
169
+ isEmpty,
170
+ isFull,
171
+ isIdle,
172
+ size,
173
+ status
174
+ };
175
+ });
176
+ emitChange("AsyncQueuer", this);
177
+ };
388
178
  /**
389
179
  * Returns the current wait time (in milliseconds) between processing items.
390
180
  * If a function is provided, it is called with the queuer instance.
391
181
  */
392
- #getWait;
182
+ #getWait = () => {
183
+ return parseFunctionOrValue(this.options.wait ?? 0, this);
184
+ };
393
185
  /**
394
186
  * Returns the current concurrency limit for processing items.
395
187
  * If a function is provided, it is called with the queuer instance.
396
188
  */
397
- #getConcurrency;
189
+ #getConcurrency = () => {
190
+ return parseFunctionOrValue(this.options.concurrency ?? 1, this);
191
+ };
398
192
  /**
399
193
  * Processes items in the queue up to the concurrency limit. Internal use only.
400
194
  */
401
- #tick;
402
- #getAllItems;
195
+ #tick = () => {
196
+ if (!this.store.state.isRunning) {
197
+ this.#setState({ pendingTick: false });
198
+ return;
199
+ }
200
+ this.#setState({ pendingTick: true });
201
+ this.#checkExpiredItems();
202
+ let scheduledAsyncWork = false;
203
+ const activeItems = [...this.store.state.activeItems];
204
+ while (activeItems.length < this.#getConcurrency() && this.store.state.items.length > 0) {
205
+ const nextItem = this.peekNextItem();
206
+ if (nextItem === void 0) break;
207
+ activeItems.push(nextItem);
208
+ this.#setState({ activeItems: [...activeItems] });
209
+ scheduledAsyncWork = true;
210
+ (async () => {
211
+ try {
212
+ await this.execute();
213
+ } catch {}
214
+ const wait = this.#getWait();
215
+ if (wait > 0) {
216
+ const timeoutId = setTimeout(() => {
217
+ this.#timeoutIds.delete(timeoutId);
218
+ this.#tick();
219
+ }, wait);
220
+ this.#timeoutIds.add(timeoutId);
221
+ return;
222
+ }
223
+ this.#tick();
224
+ })();
225
+ }
226
+ if (!scheduledAsyncWork) this.#setState({ pendingTick: false });
227
+ };
228
+ /**
229
+ * Adds an item to the queue. If the queue is full, the item is rejected and onReject is called.
230
+ * Items can be inserted based on priority or at the front/back depending on configuration.
231
+ * `undefined` cannot be queued (it is the internal "no item" sentinel) and is always rejected.
232
+ *
233
+ * @example
234
+ * ```ts
235
+ * queuer.addItem({ value: 'task', priority: 10 });
236
+ * queuer.addItem('task2', 'front');
237
+ * ```
238
+ */
239
+ addItem = (item, position = this.options.addItemsTo ?? "back", runOnItemsChange = true) => {
240
+ this.#setState({ addItemCount: this.store.state.addItemCount + 1 });
241
+ if (item === void 0) {
242
+ this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
243
+ this.options.onReject?.(item, this);
244
+ return false;
245
+ }
246
+ if (this.store.state.items.length >= (this.options.maxSize ?? Infinity)) {
247
+ this.#setState({ rejectionCount: this.store.state.rejectionCount + 1 });
248
+ this.options.onReject?.(item, this);
249
+ return false;
250
+ }
251
+ const priority = this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(item) : item?.priority;
252
+ const items = this.store.state.items;
253
+ const itemTimestamps = this.store.state.itemTimestamps;
254
+ if (priority !== void 0) {
255
+ const insertIndex = items.findIndex((existing) => {
256
+ return (this.options.getPriority !== defaultOptions.getPriority ? this.options.getPriority(existing) : existing?.priority) < priority;
257
+ });
258
+ if (insertIndex === -1) {
259
+ items.push(item);
260
+ itemTimestamps.push(Date.now());
261
+ } else {
262
+ items.splice(insertIndex, 0, item);
263
+ itemTimestamps.splice(insertIndex, 0, Date.now());
264
+ }
265
+ } else if (position === "front") {
266
+ items.unshift(item);
267
+ itemTimestamps.unshift(Date.now());
268
+ } else {
269
+ items.push(item);
270
+ itemTimestamps.push(Date.now());
271
+ }
272
+ this.#setState({
273
+ items,
274
+ itemTimestamps
275
+ });
276
+ if (runOnItemsChange) this.options.onItemsChange?.(this);
277
+ if (this.store.state.isRunning && !this.store.state.pendingTick) this.#tick();
278
+ return true;
279
+ };
280
+ /**
281
+ * Removes and returns the next item from the queue without executing the task function.
282
+ * Use for manual queue management. Normally, use execute() to process items.
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * // FIFO
287
+ * queuer.getNextItem();
288
+ * // LIFO
289
+ * queuer.getNextItem('back');
290
+ * ```
291
+ */
292
+ getNextItem = (position = this.options.getItemsFrom ?? "front") => {
293
+ const { items, itemTimestamps } = this.store.state;
294
+ let item;
295
+ if (this.options.getPriority !== defaultOptions.getPriority || position === "front") {
296
+ item = items[0];
297
+ if (item !== void 0) this.#setState({
298
+ items: items.slice(1),
299
+ itemTimestamps: itemTimestamps.slice(1)
300
+ });
301
+ } else {
302
+ item = items[items.length - 1];
303
+ if (item !== void 0) this.#setState({
304
+ items: items.slice(0, -1),
305
+ itemTimestamps: itemTimestamps.slice(0, -1)
306
+ });
307
+ }
308
+ if (item !== void 0) this.options.onItemsChange?.(this);
309
+ return item;
310
+ };
311
+ #getAllItems = () => {
312
+ const items = this.peekAllItems();
313
+ this.clear();
314
+ return items;
315
+ };
316
+ /**
317
+ * Removes and returns the next item from the queue and executes the task function with it.
318
+ *
319
+ * @example
320
+ * ```ts
321
+ * queuer.execute();
322
+ * // LIFO
323
+ * queuer.execute('back');
324
+ * ```
325
+ */
326
+ execute = async (position) => {
327
+ const item = this.getNextItem(position);
328
+ if (item !== void 0) {
329
+ const currentExecutionCount = this.store.state.executionCount + 1;
330
+ this.#setState({
331
+ executionCount: currentExecutionCount,
332
+ isExecuting: true
333
+ });
334
+ try {
335
+ const currentAsyncRetryer = new AsyncRetryer(this.fn, this.options.asyncRetryerOptions);
336
+ this.asyncRetryers.set(currentExecutionCount, currentAsyncRetryer);
337
+ const lastResult = await currentAsyncRetryer.execute(item);
338
+ this.#setState({
339
+ successCount: this.store.state.successCount + 1,
340
+ lastResult
341
+ });
342
+ this.options.onSuccess?.(lastResult, item, this);
343
+ } catch (error) {
344
+ this.#setState({ errorCount: this.store.state.errorCount + 1 });
345
+ this.options.onError?.(error, item, this);
346
+ if (this.options.throwOnError) throw error;
347
+ } finally {
348
+ this.asyncRetryers.delete(currentExecutionCount);
349
+ const remainingActiveItems = [...this.store.state.activeItems];
350
+ const activeItemIndex = remainingActiveItems.indexOf(item);
351
+ if (activeItemIndex !== -1) remainingActiveItems.splice(activeItemIndex, 1);
352
+ this.#setState({
353
+ activeItems: remainingActiveItems,
354
+ isExecuting: this.asyncRetryers.size > 0,
355
+ settleCount: this.store.state.settleCount + 1
356
+ });
357
+ this.options.onSettled?.(item, this);
358
+ }
359
+ }
360
+ return item;
361
+ };
362
+ /**
363
+ * Processes a specified number of items to execute immediately with no wait time
364
+ * If no numberOfItems is provided, all items will be processed
365
+ */
366
+ flush = async (numberOfItems = this.store.state.items.length, position) => {
367
+ this.#clearTimeouts();
368
+ const results = await Promise.allSettled(Array.from({ length: numberOfItems }, () => this.execute(position)));
369
+ this.#setState({ pendingTick: false });
370
+ if (this.store.state.isRunning && this.store.state.items.length > 0) {
371
+ const wait = this.#getWait();
372
+ if (wait > 0) {
373
+ this.#setState({ pendingTick: true });
374
+ const timeoutId = setTimeout(() => {
375
+ this.#timeoutIds.delete(timeoutId);
376
+ this.#tick();
377
+ }, wait);
378
+ this.#timeoutIds.add(timeoutId);
379
+ } else this.#tick();
380
+ }
381
+ const failure = results.find((result) => result.status === "rejected");
382
+ if (failure) throw failure.reason;
383
+ };
384
+ /**
385
+ * Processes all items in the queue as a batch using the provided function as an argument
386
+ * The queue is cleared after processing
387
+ */
388
+ flushAsBatch = async (batchFunction) => {
389
+ this.#clearTimeouts();
390
+ const items = this.#getAllItems();
391
+ try {
392
+ await batchFunction(items);
393
+ } finally {
394
+ this.#setState({ pendingTick: false });
395
+ if (this.store.state.isRunning && this.store.state.items.length > 0) this.#tick();
396
+ }
397
+ };
403
398
  /**
404
399
  * Checks for expired items in the queue and removes them. Calls onExpire for each expired item.
405
400
  * Internal use only.
406
401
  */
407
- #checkExpiredItems;
408
- #clearTimeouts;
402
+ #checkExpiredItems = () => {
403
+ if ((this.options.expirationDuration ?? Infinity) === Infinity && this.options.getIsExpired === defaultOptions.getIsExpired) return;
404
+ const now = Date.now();
405
+ const expiredIndices = [];
406
+ for (let i = 0; i < this.store.state.items.length; i++) {
407
+ const timestamp = this.store.state.itemTimestamps[i];
408
+ if (timestamp === void 0) continue;
409
+ const item = this.store.state.items[i];
410
+ if (item === void 0) continue;
411
+ if (this.options.getIsExpired !== defaultOptions.getIsExpired ? this.options.getIsExpired(item, timestamp) : now - timestamp > (this.options.expirationDuration ?? Infinity)) expiredIndices.push(i);
412
+ }
413
+ for (let i = expiredIndices.length - 1; i >= 0; i--) {
414
+ const index = expiredIndices[i];
415
+ if (index === void 0) continue;
416
+ const expiredItem = this.store.state.items[index];
417
+ if (expiredItem === void 0) continue;
418
+ const newItems = [...this.store.state.items];
419
+ const newTimestamps = [...this.store.state.itemTimestamps];
420
+ newItems.splice(index, 1);
421
+ newTimestamps.splice(index, 1);
422
+ this.#setState({
423
+ items: newItems,
424
+ itemTimestamps: newTimestamps,
425
+ expirationCount: this.store.state.expirationCount + 1
426
+ });
427
+ this.options.onExpire?.(expiredItem, this);
428
+ }
429
+ if (expiredIndices.length > 0) this.options.onItemsChange?.(this);
430
+ };
431
+ /**
432
+ * Returns the next item in the queue without removing it.
433
+ *
434
+ * @example
435
+ * ```ts
436
+ * queuer.peekNextItem(); // front
437
+ * queuer.peekNextItem('back'); // back
438
+ * ```
439
+ */
440
+ peekNextItem = (position = "front") => {
441
+ if (position === "front") return this.store.state.items[0];
442
+ return this.store.state.items[this.store.state.items.length - 1];
443
+ };
444
+ /**
445
+ * Returns a copy of all items in the queue, including active and pending items.
446
+ */
447
+ peekAllItems = () => {
448
+ return [...this.peekActiveItems(), ...this.peekPendingItems()];
449
+ };
450
+ /**
451
+ * Returns the items currently being processed (active tasks).
452
+ */
453
+ peekActiveItems = () => {
454
+ return [...this.store.state.activeItems];
455
+ };
456
+ /**
457
+ * Returns the items waiting to be processed (pending tasks).
458
+ */
459
+ peekPendingItems = () => {
460
+ return [...this.store.state.items];
461
+ };
462
+ /**
463
+ * Starts processing items in the queue. If already running, does nothing.
464
+ */
465
+ start = () => {
466
+ this.#setState({ isRunning: true });
467
+ if (!this.store.state.pendingTick && this.store.state.items.length > 0) this.#tick();
468
+ };
469
+ /**
470
+ * Stops processing items in the queue. Does not clear the queue.
471
+ */
472
+ stop = () => {
473
+ this.#clearTimeouts();
474
+ this.#setState({
475
+ isRunning: false,
476
+ pendingTick: false
477
+ });
478
+ };
479
+ #clearTimeouts = () => {
480
+ this.#timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId));
481
+ this.#timeoutIds.clear();
482
+ };
483
+ /**
484
+ * Removes all pending items from the queue.
485
+ * Does NOT affect active tasks.
486
+ */
487
+ clear = () => {
488
+ this.#setState({
489
+ items: [],
490
+ itemTimestamps: []
491
+ });
492
+ this.options.onItemsChange?.(this);
493
+ };
494
+ /**
495
+ * Returns the AbortSignal for a specific execution.
496
+ * If no executionCount is provided, returns the signal for the most recent execution.
497
+ * Returns null if no execution is found or not currently executing.
498
+ *
499
+ * @param executionCount - Optional specific execution to get signal for
500
+ * @example
501
+ * ```typescript
502
+ * const queuer = new AsyncQueuer(
503
+ * async (item: string) => {
504
+ * const signal = queuer.getAbortSignal()
505
+ * if (signal) {
506
+ * const response = await fetch(`/api/process/${item}`, { signal })
507
+ * return response.json()
508
+ * }
509
+ * },
510
+ * { concurrency: 2 }
511
+ * )
512
+ * ```
513
+ */
514
+ getAbortSignal = (executionCount) => {
515
+ const count = executionCount ?? this.store.state.executionCount;
516
+ return this.asyncRetryers.get(count)?.getAbortSignal() ?? null;
517
+ };
518
+ /**
519
+ * Aborts all ongoing executions with the internal abort controllers.
520
+ * Does NOT clear out the items.
521
+ */
522
+ abort = () => {
523
+ this.asyncRetryers.forEach((retryer) => retryer.abort());
524
+ this.asyncRetryers.clear();
525
+ this.#setState({ isExecuting: false });
526
+ };
527
+ /**
528
+ * Resets the queuer state to its default values
529
+ */
530
+ reset = () => {
531
+ this.#setState(getDefaultAsyncQueuerState());
532
+ this.options.onItemsChange?.(this);
533
+ this.asyncRetryers.forEach((retryer) => retryer.reset());
534
+ };
409
535
  };
410
536
  /**
411
537
  * Creates a new AsyncQueuer instance and returns a bound addItem function for adding tasks.
@@ -477,5 +603,4 @@ function asyncQueue(fn, initialOptions) {
477
603
  }
478
604
 
479
605
  //#endregion
480
- export { AsyncQueuer, asyncQueue, asyncQueuerOptions };
481
- //# sourceMappingURL=async-queuer.js.map
606
+ export { AsyncQueuer, asyncQueue, asyncQueuerOptions };