How to use the limit.pendingCount variable in p-limit? - limit

I am using p-limit package to limit the # of concurrent requests made using Promise.all.
I would like to show the progress of the total requests, and see that there is a variable limit.pendingCount in the library.
My question is how can I use that variable to report progress, when I console log that variable, it only returns a final value of 0.
(async () => {
const result = await Promise.all(promises);
// eslint-disable-next-line
console.log(limit.pendingCount);

Since you are await-ing the Promise.all call, all promises will be completed by the time you reach your console.log statement.
Try checking the pending count without await-ing the promises initially. Any code after await-ing the promises will only be executed after all promises are completed (i.e. pending count is 0).
const numThreads = 2;
const numPromises = 4;
const pLimit = require("p-limit")
const limit = pLimit(numThreads);
const promises = new Array(numPromises).fill()
.map((n, i) => i + 1)
.map(n => limit(() => new Promise(r => {
console.log(`Started Promise ${n}`);
setTimeout(r, n * 1000);
})
.then(() => console.log(`Completed Promise ${n}`))));
trackProgress('After Promises Initiated', promises);
const result = await Promise.all(promises);
trackProgress('After Promises Awaited', promises);
/**
* Prints the state of the current pending promises until all pending promises are completed. This works only for this sample; it is not production quality.
*/
function trackProgress(label, promises) {
console.log(`[${label}] Tracking started.`);
const printProgress = () => {
if (limit.pendingCount > 0) {
console.log(`[${label}] Pending: ${limit.pendingCount} of ${promises.length}`);
setTimeout(printProgress, 1000);
}
else {
console.log(`[${label}] Tracking completed.`);
}
};
printProgress();
}
/*
* Output:
* "Started Promise 1"
* "Started Promise 2"
* "[After Promises Initiated] Tracking started."
* "[After Promises Initiated] Pending: 2 of 4"
* "Completed Promise 1"
* "Started Promise 3"
* "[After Promises Initiated] Pending: 1 of 4"
* "Completed Promise 2"
* "Started Promise 4"
* "[After Promises Initiated] Tracking completed."
* "Completed Promise 3"
* "Completed Promise 4"
* "[After Promises Awaited] Tracking started."
* "[After Promises Awaited] Tracking completed."
*/
Edit:
If you are looking to do a progress tracker, you may have better luck by adding callbacks to the end of the executed promises:
const numThreads = 2;
const numPromises = 4;
const pLimit = require("p-limit");
function executeRequests() {
const limit = pLimit(numThreads);
let numCompleted = 0;
console.log(`${100 * numCompleted / numPromises}% Complete`);
const updateNumCompleted = () => {
numCompleted++;
// TODO: Instead of console.log, update UI
console.log(`${100 * numCompleted / numPromises}% Complete`);
if (numCompleted >= numPromises) {
// TODO: Instead of console.log, update UI
console.log('All promises complete');
}
};
const promises = new Array(numPromises).fill()
.map((n, i) => i + 1)
.map(n => limit(() => new Promise(r => {
console.log(`Started Promise ${n}`);
setTimeout(r, n * 1000);
})
.then(() => {
console.log(`Completed Promise ${n}`);
updateNumCompleted();
})));
Promise.all(promises);
}
executeRequests();
/*
* Output:
* "0% Complete"
* "Started Promise 1"
* "Started Promise 2"
* "Completed Promise 1"
* "25% Complete"
* "Started Promise 3"
* "Completed Promise 2"
* "50% Complete"
* "Started Promise 4"
* "Completed Promise 3"
* "75% Complete"
* "Completed Promise 4"
* "100% Complete"
* "All promises complete"
*/

Related

Poll API every 2 seconds and update state until result turns successful/failure - React

I have an array of ids and I need to loop through the array and call an API with the id. The response from the API will have a Status field which ranges from [0,1,2,-1]. I want to call the API and update the state with the responses. I have a working code which has a Promise which gets resolved only if the Status is Finished or Failed. How do I go over about updating the state even while the Status is in Started and Queued.
This is my code.
import React from 'react';
import './style.css';
import { get, includes, keys, forEach, map } from 'lodash';
const Status = {
Queued: 0,
Started: 1,
Finished: 2,
Failed: -1,
};
/**
* Keeps getting data every 2.5 seconds
* If path is Status, keep calling the API every 2.5 seconds until Status is Finished or Failed, and then resolve
* #param url
* #param path
*/
/**
* The MAP where the list of timers are stored
*/
let dataFetchingTimerMap = {};
const clearDataFetchingTimer = (key) => {
/**
* Clear the timeout
*/
clearTimeout(dataFetchingTimerMap[key]);
/**
* Delete the key
*/
delete dataFetchingTimerMap[key];
};
const setDataFetchingTimer = (key, cb, delay) => {
/**
* Save the timeout with a key
*/
dataFetchingTimerMap[key] = window.setTimeout(() => {
/**
* Delete key when it executes
*/
clearDataFetchingTimer(key);
/**
* Execute the callback (loop function)
*/
cb();
}, delay);
};
export const getDataAtIntervals = (url, path) => {
const timerKey = `${url}_${path}`;
clearTimeout(+timerKey);
return new Promise((resolve, reject) => {
(async function loop() {
try {
const resultData = await fetch(url);
const result = await resultData.json();
if (
get(result, path) &&
includes(
[Status.Finished, Status.Failed, Status.FailedWithReturnFile],
get(result, path)
)
) {
/**
* Resolve with the data
*/
return resolve(result);
}
setDataFetchingTimer(timerKey, loop, 2500);
} catch (e) {
reject(e);
}
})();
});
};
/**
* Clear every timeout
*/
export const clearGetDataAtIntervals = () =>
forEach(keys(dataFetchingTimerMap), clearDataFetchingTimer);
export default function App() {
const [state, setState] = React.useState([]);
const handleClick = async () => {
const ids = [1, 2, 3];
const responses = await Promise.all(
map(
ids,
async (id) =>
await getDataAtIntervals(
`https://jsonplaceholder.typicode.com/todos/${id}`,
'completed'
)
)
);
setState(responses);
};
return (
<div>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
Please advice. This is my Stackblitz link:
https://stackblitz.com/edit/react-dos3i3?file=src%2FApp.js,src%2Findex.js

RxJS / Redux-observables: How would I test EventEmitter?

I am trying to test that redux actions are fired when the finish event on my upload object is fired.
Here's my epic:
const uploadFileEpic = (action$, state$, dependencies$) =>
action$.pipe(
ofType(uploadActions.UPLOAD_FILE),
mergeMap(({ payload }) => {
const { file, masterHandle } = payload;
return new Observable(o => {
const upload = masterHandle.uploadFile("/", file);
const handle = upload.handle;
upload.on("finish", () => {
o.next(
uploadActions.uploadSuccess({
masterHandle
})
);
o.complete();
});
});
})
);
Here is what I have written so far. It doesn't work:
import { of } from "rxjs";
import "rxjs/add/operator/toArray";
import { EventEmitter } from "events";
import uploadActions from "../actions/upload-actions";
import uploadEpic from "./upload-epic";
test("uploadFilesEpic filesActions.UPLOAD_FILE on success", done => {
const file = { name: "f1" };
const upload = new EventEmitter();
upload.handle = "h1";
const masterHandle = {
uploadFile: jest.fn(() => upload)
};
const action$ = of(uploadActions.uploadFile({ file, masterHandle }));
upload.emit("finish");
uploadEpic(action$).subscribe(actions => {
expect(actions).toEqual(uploadActions.uploadSuccess({ masterHandle }));
done();
});
});
It says the async callback was not fired:
FAIL src/redux/epics/upload-epic.test.js (8.531s)
✓ uploadFilesEpic filesActions.UPLOAD_FILES (9ms)
✕ uploadFilesEpic filesActions.UPLOAD_FILE on success (5021ms)
● uploadFilesEpic filesActions.UPLOAD_FILE on success
Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:
23 | });
24 |
> 25 | test("uploadFilesEpic filesActions.UPLOAD_FILE on success", done => {
| ^
26 | const file = { name: "f1" };
27 | const upload = new EventEmitter();
28 | upload.handle = "h1";
at new Spec (node_modules/jest-config/node_modules/jest-jasmine2/build/jasmine/Spec.js:116:22)
at Object.test (src/redux/epics/upload-epic.test.js:25:1)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 9.297s
It makes sense to me that the test is failing but I'm not sure how to subscribe to the epic, fire the finish event, and then inspect the actions returned by the epic.
The event for finishing the upload is emitted too early in the test. The EventEmitter does not keep a buffer of events and if no one is subscribed, then the even is lost.
Move the emit "finish" to the bottom of the test.
upload.emit("finish"); // must emit after subscribing

RxJS Observable: repeat using count and then using notifier

I have an Observable that emits Either = Success | Failure:
import { Observable } from 'rxjs';
type Success = { type: 'success' };
type Failure = { type: 'failure' };
type Either = Success | Failure;
const either$ = new Observable<Either>(observer => {
console.log('subscribe');
observer.next({ type: 'failure' });
observer.complete();
return () => {
console.log('unsubscribe');
};
});
I want to allow the user to "retry" the observable when the Observable completes and the last value was Failure.
(The retry{,When} operators do not help here because they work with errors on the error channel. For this reason, I believe we should think in terms of repeat instead.)
I want to:
Repeat the Observable n times until the last value is not Failure.
Then, allow the user to repeat manually. When a repeat notifier observable (repeat$) emits, repeat the observable again.
For example:
// subscribe
// next { type: 'failure' }
// unsubscribe
// retry 2 times:
// subscribe
// next { type: 'failure' }
// unsubscribe
// subscribe
// next { type: 'failure' }
// unsubscribe
// now, wait for repeat notifications…
// on retry notification:
// subscribe
// next { type: 'failure' }
// unsubscribe
I couldn't come up with something simpler, but the code does what you want.
See https://stackblitz.com/edit/typescript-yqcejk
defer(() => {
let retries = 0;
const source = new BehaviorSubject(null);
return merge(source, repeat$.pipe(filter(() => retries <= MAX_RETRIES)))
.pipe(
concatMapTo(either$),
tap(value => {
const action = value as Either;
if (action.type === 'failure') {
if (retries < MAX_RETRIES) {
retries += 1;
source.next(null);
}
} else {
retries = 0;
}
})
)
}).subscribe(console.log);
I had to manually count retries.
The code has two sources of events source for automatic retries and repeat$ for user retries. All events are mapped to either$ using concatMapTo. As a side-effect we either next() to retry or do nothing waiting for user to retry.
User retries are suppressed using filter(() => retries >= MAX_RETRIES) until MAX_RETRIES count is reached.

How to throw stream error in custom operator?

I found cstom operator that I want to use.
This is an operator that retries http requests. Code is from Stephen Fluin: https://github.com/StephenFluin/http-operators/blob/master/operators/retryExponentialBackoff.operator.ts.
Problem is that if after all these reties it does not puts error in stream only completes.
I want it to throw an error. How to do it?
I think this part should be modified:
error(err: any) {
if (count <= maxTries) {
subscription.add(scheduler.schedule(subscribe, initialWait * Math.pow(2, count++)));
}
},
Here is whole operator's class
/**
* Repeats underlying observable on a timer
*
* #param maxTries The maximum number of attempts to make, or -1 for unlimited
* #param initialWait Number of seconds to wait for refresh
*/
export const retryExponentialBackoff = (
maxTries = -1,
initialWait = 1,
scheduler: SchedulerLike = asyncScheduler
) => <T>(
source: Observable<T>
) => {
return new Observable<T>(subscriber => {
let count = 1;
const subscription = new Subscription();
const subscribe = () =>
subscription.add(
source.subscribe({
next(value: T) {
count = 1;
subscriber.next(value);
},
error(err: any) {
if (count <= maxTries) {
subscription.add(scheduler.schedule(subscribe, initialWait * Math.pow(2, count++)));
}
},
complete() {
subscriber.complete();
},
})
);
subscribe();
return subscription;
});
};
I would try to add the error bubbling to the subscriber like so:
error(err: any) {
if (count <= maxTries) {
subscription.add(scheduler.schedule(subscribe, initialWait * Math.pow(2, count++)));
}
else {
subscriber.error(err);
}
},
So that after your maxTries count have een exhausted the error is emitted downstream.

How to batch additions to arrays/lists returned by rxjs observables?

have an observable that returns arrays/lists of things: Observable
And I have a usecase where is is a pretty costly affair for the downstream consumer of this observable to have more items added to this list. So I'd like to slow down the amount of additions that are made to this list, but not loose any.
Something like an operator that takes this observable and returns another observable with the same signature, but whenever a new list gets pushed on it and it has more items than last time, then only one or a few are added at a time.
So if the last push was a list with 3 items and next push has 3 additional items with 6 items in total, and the batch size is 1, then this one list push gets split into 3 individual pushes of lists with lengths: 4, 5, 6
So additions are batched, and this way the consumer can more easily keep up with new additions to the list. Or the consumer doesn't have to stall for so long each time while processing additional items in the array/list, because the additions are split up and spread over a configurable size of batches.
I made an addAdditionalOnIdle operator that you can apply to any rxjs observable using the pipe operator. It takes a batchSize parameter, so you can configure the batch size. It also takes a dontBatchAfterThreshold, which stops batching of the list after a certain list size, which was useful for my purposes. The result also contains a morePending value, which you can use to show a loading indicator while you know more data is incomming.
The implementation uses the new requestIdleCallback function internally to schedule the batched pushes of additional items when there is idle time in the browser. This function is not available in IE or Safari yet, but I found this excelent polyfill for it, so you can use it today anyways: https://github.com/aFarkas/requestIdleCallback :)
See the implementation and example usage of addAdditionalOnIdle below:
const { NEVER, of, Observable } = rxjs;
const { concat } = rxjs.operators;
/**
* addAdditionalOnIdle
*
* Only works on observables that produce values that are of type Array.
* Adds additional elements on window.requestIdleCallback
*
* #param batchSize The amount of values that are added on each idle callback
* #param dontBatchAfterThreshold Return all items after amount of returned items passes this threshold
*/
function addAdditionalOnIdle(
batchSize = 1,
dontBatchAfterThreshold = 22,
) {
return (source) => {
return Observable.create((observer) => {
let idleCallback;
let currentPushedItems = [];
let lastItemsReceived = [];
let sourceSubscription = source
.subscribe({
complete: () => {
observer.complete();
},
error: (error) => {
observer.error(error);
},
next: (items) => {
lastItemsReceived = items;
if (idleCallback) {
return;
}
if (lastItemsReceived.length > currentPushedItems.length) {
const idleCbFn = () => {
if (currentPushedItems.length > lastItemsReceived.length) {
observer.next({
morePending: false,
value: lastItemsReceived,
});
idleCallback = undefined;
return;
}
const to = currentPushedItems.length + batchSize;
const last = lastItemsReceived.length;
if (currentPushedItems.length < dontBatchAfterThreshold) {
for (let i = 0 ; i < to && i < last ; i++) {
currentPushedItems[i] = lastItemsReceived[i];
}
} else {
currentPushedItems = lastItemsReceived;
}
if (currentPushedItems.length < lastItemsReceived.length) {
idleCallback = window.requestIdleCallback(() => {
idleCbFn();
});
} else {
idleCallback = undefined;
}
observer.next({
morePending: currentPushedItems.length < lastItemsReceived.length,
value: currentPushedItems,
});
};
idleCallback = window.requestIdleCallback(() => {
idleCbFn();
});
} else {
currentPushedItems = lastItemsReceived;
observer.next({
morePending: false,
value: currentPushedItems,
});
}
},
});
return () => {
sourceSubscription.unsubscribe();
sourceSubscription = undefined;
lastItemsReceived = undefined;
currentPushedItems = undefined;
if (idleCallback) {
window.cancelIdleCallback(idleCallback);
idleCallback = undefined;
}
};
});
};
}
function sleep(milliseconds) {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if ((new Date().getTime() - start) > milliseconds){
break;
}
}
}
let testSource = of(
[1,2,3],
[1,2,3,4,5,6],
).pipe(
concat(NEVER)
);
testSource
.pipe(addAdditionalOnIdle(2))
.subscribe((list) => {
// Simulate a slow synchronous consumer with a busy loop sleep implementation
sleep(1000);
document.body.innerHTML += "<p>" + list.value + "</p>";
});
<script src="https://unpkg.com/rxjs#6.5.3/bundles/rxjs.umd.js"></script>

Resources