@tmlmobilidade/utils 20260722.1043.2 → 20260723.1350.16
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/dist/batching/batch-writer.d.ts +10 -0
- package/dist/batching/batch-writer.js +35 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/run-with-concurrency.d.ts +12 -0
- package/dist/run-with-concurrency.js +30 -0
- package/dist/with-retry.d.ts +20 -0
- package/dist/with-retry.js +39 -0
- package/package.json +1 -1
|
@@ -49,6 +49,7 @@ export declare class BatchWriter<T> {
|
|
|
49
49
|
private batchTimeoutTimer;
|
|
50
50
|
private idleTimeoutTimer;
|
|
51
51
|
private sessionTimer;
|
|
52
|
+
private flushInProgress;
|
|
52
53
|
constructor(params: BatchWriterParams<T>);
|
|
53
54
|
/**
|
|
54
55
|
* Flushes the current batch of data.
|
|
@@ -57,6 +58,15 @@ export declare class BatchWriter<T> {
|
|
|
57
58
|
* @param callback Optional callback to execute after the flush is complete, receiving the flushed data as a parameter
|
|
58
59
|
*/
|
|
59
60
|
flush(callback?: (data?: T[]) => Promise<void>): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Timer-triggered flush that guarantees the buffer is emptied even when a
|
|
63
|
+
* flush was already in progress. If the guarded flush() coalesced into a
|
|
64
|
+
* running flush, data written during that flush would strand on an idle tail
|
|
65
|
+
* (timers are cleared at flush start and only re-armed by the next write()).
|
|
66
|
+
* Re-flush while data remains and no timer is pending.
|
|
67
|
+
*/
|
|
68
|
+
private drain;
|
|
69
|
+
private runFlush;
|
|
60
70
|
/**
|
|
61
71
|
* Helper method to perform insert operations with retry logic for transient errors.
|
|
62
72
|
* This method will attempt to insert the data using the provided insert function,
|
|
@@ -10,6 +10,10 @@ export class BatchWriter {
|
|
|
10
10
|
batchTimeoutTimer = null;
|
|
11
11
|
idleTimeoutTimer = null;
|
|
12
12
|
sessionTimer = new Timer();
|
|
13
|
+
// ponytail: single in-flight guard, not a queue. Serializes overlapping
|
|
14
|
+
// flushes (timer-triggered vs batch-full) so inserts never run concurrently
|
|
15
|
+
// and callers applying backpressure await the same promise.
|
|
16
|
+
flushInProgress = null;
|
|
13
17
|
constructor(params) {
|
|
14
18
|
if (!params.title)
|
|
15
19
|
throw new Error('BATCHWRITER: Title is required.');
|
|
@@ -26,6 +30,35 @@ export class BatchWriter {
|
|
|
26
30
|
* @param callback Optional callback to execute after the flush is complete, receiving the flushed data as a parameter
|
|
27
31
|
*/
|
|
28
32
|
async flush(callback) {
|
|
33
|
+
// If a flush is already running, await it instead of starting a
|
|
34
|
+
// concurrent insert. This is the backpressure seam: the change-stream
|
|
35
|
+
// handler awaits write() -> flush() and cannot outrun the insert.
|
|
36
|
+
if (this.flushInProgress) {
|
|
37
|
+
await this.flushInProgress;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.flushInProgress = this.runFlush(callback);
|
|
41
|
+
try {
|
|
42
|
+
await this.flushInProgress;
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
this.flushInProgress = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Timer-triggered flush that guarantees the buffer is emptied even when a
|
|
50
|
+
* flush was already in progress. If the guarded flush() coalesced into a
|
|
51
|
+
* running flush, data written during that flush would strand on an idle tail
|
|
52
|
+
* (timers are cleared at flush start and only re-armed by the next write()).
|
|
53
|
+
* Re-flush while data remains and no timer is pending.
|
|
54
|
+
*/
|
|
55
|
+
async drain(callback) {
|
|
56
|
+
await this.flush(callback);
|
|
57
|
+
if (this.dataBucketAlwaysAvailable.length > 0 && !this.idleTimeoutTimer && !this.batchTimeoutTimer && !this.flushInProgress) {
|
|
58
|
+
await this.flush(callback);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async runFlush(callback) {
|
|
29
62
|
try {
|
|
30
63
|
//
|
|
31
64
|
const flushTimer = new Timer();
|
|
@@ -148,7 +181,7 @@ export class BatchWriter {
|
|
|
148
181
|
if (this.params.idle_timeout && this.params.idle_timeout > 0 && !this.idleTimeoutTimer) {
|
|
149
182
|
this.idleTimeoutTimer = setTimeout(async () => {
|
|
150
183
|
console.info(`BATCHWRITER [${this.params.title}]: Idle timeout reached. Flushing data...`);
|
|
151
|
-
await this.
|
|
184
|
+
await this.drain(flushCallback);
|
|
152
185
|
}, this.params.idle_timeout);
|
|
153
186
|
}
|
|
154
187
|
//
|
|
@@ -157,7 +190,7 @@ export class BatchWriter {
|
|
|
157
190
|
if (this.params.batch_timeout && this.params.batch_timeout > 0 && !this.batchTimeoutTimer) {
|
|
158
191
|
this.batchTimeoutTimer = setTimeout(async () => {
|
|
159
192
|
console.info(`BATCHWRITER [${this.params.title}]: Batch timeout reached. Flushing data...`);
|
|
160
|
-
await this.
|
|
193
|
+
await this.drain(flushCallback);
|
|
161
194
|
}, this.params.batch_timeout);
|
|
162
195
|
}
|
|
163
196
|
//
|
package/dist/index.d.ts
CHANGED
|
@@ -9,5 +9,7 @@ export * from './numbers/index.js';
|
|
|
9
9
|
export * from './objects/index.js';
|
|
10
10
|
export * from './permissions.js';
|
|
11
11
|
export * from './run-on-interval.js';
|
|
12
|
+
export * from './run-with-concurrency.js';
|
|
12
13
|
export * from './singleton-proxy.js';
|
|
13
14
|
export * from './validate-query-params.js';
|
|
15
|
+
export * from './with-retry.js';
|
package/dist/index.js
CHANGED
|
@@ -9,5 +9,7 @@ export * from './numbers/index.js';
|
|
|
9
9
|
export * from './objects/index.js';
|
|
10
10
|
export * from './permissions.js';
|
|
11
11
|
export * from './run-on-interval.js';
|
|
12
|
+
export * from './run-with-concurrency.js';
|
|
12
13
|
export * from './singleton-proxy.js';
|
|
13
14
|
export * from './validate-query-params.js';
|
|
15
|
+
export * from './with-retry.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run an async function over all items in the array, with a specific concurrency limit.
|
|
3
|
+
*
|
|
4
|
+
* @param items - The array of items to process.
|
|
5
|
+
* @param concurrency - The maximum number of concurrent executions.
|
|
6
|
+
* @param fn - Async function to run for each item. Receives the item and its index.
|
|
7
|
+
* @returns A Promise resolving to an array of PromiseSettledResult objects, preserving input order.
|
|
8
|
+
*
|
|
9
|
+
* Each result is a { status, value } or { status, reason } object, as from Promise.allSettled().
|
|
10
|
+
* The pool will never run more than `concurrency` functions at once.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runWithConcurrency<T, R>(items: T[], concurrency: number, fn: (item: T, index: number) => Promise<R>): Promise<PromiseSettledResult<R>[]>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run an async function over all items in the array, with a specific concurrency limit.
|
|
3
|
+
*
|
|
4
|
+
* @param items - The array of items to process.
|
|
5
|
+
* @param concurrency - The maximum number of concurrent executions.
|
|
6
|
+
* @param fn - Async function to run for each item. Receives the item and its index.
|
|
7
|
+
* @returns A Promise resolving to an array of PromiseSettledResult objects, preserving input order.
|
|
8
|
+
*
|
|
9
|
+
* Each result is a { status, value } or { status, reason } object, as from Promise.allSettled().
|
|
10
|
+
* The pool will never run more than `concurrency` functions at once.
|
|
11
|
+
*/
|
|
12
|
+
export async function runWithConcurrency(items, concurrency, fn) {
|
|
13
|
+
if (items.length === 0)
|
|
14
|
+
return [];
|
|
15
|
+
const results = new Array(items.length);
|
|
16
|
+
let next = 0;
|
|
17
|
+
async function worker() {
|
|
18
|
+
while (next < items.length) {
|
|
19
|
+
const index = next++;
|
|
20
|
+
try {
|
|
21
|
+
results[index] = { status: 'fulfilled', value: await fn(items[index], index) };
|
|
22
|
+
}
|
|
23
|
+
catch (reason) {
|
|
24
|
+
results[index] = { reason, status: 'rejected' };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface RetryOptions {
|
|
2
|
+
baseDelayMs?: number;
|
|
3
|
+
maxAttempts?: number;
|
|
4
|
+
maxDelayMs?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Executes an asynchronous function with retry logic.
|
|
8
|
+
*
|
|
9
|
+
* Retries the provided async function up to the specified number of attempts, waiting with exponential backoff between each attempt.
|
|
10
|
+
*
|
|
11
|
+
* @template T The type of the resolved value.
|
|
12
|
+
* @param fn - The asynchronous function to execute.
|
|
13
|
+
* @param options - Retry options.
|
|
14
|
+
* @param options.baseDelayMs - Initial delay in milliseconds before the first retry (default: 100).
|
|
15
|
+
* @param options.maxAttempts - Maximum number of attempts (default: 3).
|
|
16
|
+
* @param options.maxDelayMs - Maximum delay between retries in milliseconds (default: 2000).
|
|
17
|
+
* @returns A Promise resolving to the function's return value.
|
|
18
|
+
* @throws The last error thrown by the retried function after exhausting all attempts.
|
|
19
|
+
*/
|
|
20
|
+
export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
/* * */
|
|
3
|
+
/**
|
|
4
|
+
* Executes an asynchronous function with retry logic.
|
|
5
|
+
*
|
|
6
|
+
* Retries the provided async function up to the specified number of attempts, waiting with exponential backoff between each attempt.
|
|
7
|
+
*
|
|
8
|
+
* @template T The type of the resolved value.
|
|
9
|
+
* @param fn - The asynchronous function to execute.
|
|
10
|
+
* @param options - Retry options.
|
|
11
|
+
* @param options.baseDelayMs - Initial delay in milliseconds before the first retry (default: 100).
|
|
12
|
+
* @param options.maxAttempts - Maximum number of attempts (default: 3).
|
|
13
|
+
* @param options.maxDelayMs - Maximum delay between retries in milliseconds (default: 2000).
|
|
14
|
+
* @returns A Promise resolving to the function's return value.
|
|
15
|
+
* @throws The last error thrown by the retried function after exhausting all attempts.
|
|
16
|
+
*/
|
|
17
|
+
export async function withRetry(fn, options = { baseDelayMs: 100, maxAttempts: 3, maxDelayMs: 2000 }) {
|
|
18
|
+
//
|
|
19
|
+
// Last error.
|
|
20
|
+
let lastError;
|
|
21
|
+
//
|
|
22
|
+
// Helper function to sleep for the given delay.
|
|
23
|
+
const sleep = (delay) => new Promise(resolve => setTimeout(resolve, delay));
|
|
24
|
+
//
|
|
25
|
+
// Main retry loop.
|
|
26
|
+
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
|
|
27
|
+
try {
|
|
28
|
+
return await fn();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
lastError = error;
|
|
32
|
+
if (attempt >= options.maxAttempts)
|
|
33
|
+
throw error;
|
|
34
|
+
const delay = Math.min(options.maxDelayMs, options.baseDelayMs * 2 ** (attempt - 1));
|
|
35
|
+
await sleep(delay);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw lastError;
|
|
39
|
+
}
|