Qyu
qyu
is a general-purpose asynchronous job queue for the browser and Node.js. It is flexible and easy to use, always accepting jobs and running them as fast as the concurrency settings dictate.
Qyu was meant for:
- large-scale web scrapers
- throttling external API calls
- restraining database bandwidth usage in long-running database background operations
- a lot more...
const { Qyu } = require('qyu');
(async () => {
const q = new Qyu({concurrency: 3});
async function performRequest(){
const {data} = await axios('https://www.example.com');
}
q(performRequest);
q(performRequest, {priority: 2});
let result = await q(performRequest);
setTimeout(() => {
for (let i=0; i<10; i++) {
q(performRequest);
}
}, 2000);
await q.whenEmpty();
})();
Features
- Always on and ready to receive additional tasks
- Concurrency setting
- Queue capacity
- Task priority
- Task timeout
- Pause/resume
- Written in TypeScript, includes built-in type definitions
- Compatible with browsers as well as Node.js
Instance Config
Defaults:
new Qyu({
concurrency: 1,
capacity: Infinity,
rampUpTime: 0
});
concurrency:
Determines the maximum number of jobs allowed to be run concurrently.
(default: 1)
const q = new Qyu({concurrency: 2});
q(job1);
q(job2);
q(job3);
capacity:
Sets a limit on the job queue length, causing any additional job queuings to be immediately rejected with a specific "ERR_CAPACITY_FULL"
type of QyuError
.
(default: Infinity)
const q = new Qyu({capacity: 5});
for (let i=0; i<6; i++) {
q(job)
.then(result => console.log(`Job ${i} complete!`, result))
.catch(err => console.error(`job ${i} error`, err));
}
rampUpTime:
If specified a non-zero number, will delay the concurrency-ramping-up time of additional job executions, one by one, as the instance attempts to reach maximum configured concurrency.
Represents number of milliseconds.
(default: 0)
const q = new Qyu({
rampUpTime: 1000,
concurrency: 3
});
q(job1);
q(job2);
q(job3);
q(job4);
Queuing options
Defaults:
q(job, {
priority: 0,
timeout: null
});
priority:
Determines order in which queued jobs will run.
Can be any positive/negative integer/float number.
The greater the priority value, the earlier it will be called relative to other jobs.
Queuings having identical priority will be put one after another in the same order in which they were passed to the instance.
(default: 0)
timeout:
If is non-zero number, will dequeue jobs that waited in queue without running for that amount of time long (in milliseconds).
additionally, when a queued job reaches it's timeout, the promise it returned from it's queuing will reject with a "ERR_JOB_TIMEOUT"
type of QyuError
.
(default: null)
API
instance(fn
[, options
])
(alias: instance#add)
Queues function fn
on instance with optional options
.
Returns: a promise that is tied to the jobs resolution or rejection value when it will be picked from queue and run.
const q = new Qyu({concurrency: 5});
q(job1);
q(job2, {priority: 2, timeout: 1000*10});
try {
const result = await q(job3);
console.log("Job 3's result:", result);
} catch (err) {
console.log('Job 3 errored:', err);
}
q(async () => {
});
instance(iterator
, mapperFn
[, options
])
(alias: instance#map)
For each iteration of iterator
(an array for example), queues mapperFn
on instance, injected with the value and the index from that iteration.
Optional options
will be supplied the same for all job queuings included in this call.
const q = new Qyu({concurrency: 3});
const files = ['/path/to/file1.png', '/path/to/file2.png', '/path/to/file3.png', '/path/to/file4.png'];
q(files, async (file, i) => {
await fs.unlink(file);
});
await q.whenEmpty();
instance#whenEmpty()
Returns: a promise that resolves if/when an instance has no running or queued jobs.
Guaranteed to resolve, regardless if one or some jobs resulted in error.
const q = new Qyu();
await q.whenEmpty();
instance#whenFree()
Returns: a promise that resolves if/when number of currently running jobs are below the concurrency limit.
Guaranteed to resolve, regardless if one or some jobs resulted in error.
const q = new Qyu();
await q.whenFree();
instance#pause()
Pauses instance's operation, so it effectively stops picking more jobs from queue.
Jobs currently running at time instance.pause()
was called keep running until finished.
const q = new Qyu({concurrency: 2});
q(job1); q(job2); q(job3);
await q.pause();
q(job4);
instance#resume()
Resumes instance's operation after a previous call to instance.pause()
.
An instance is in "resumed" mode by default when instantiated.
const q = new Qyu();
q.pause();
q.resume();
instance#empty()
Immediately empties the instance's queue from all queued jobs, rejecting the promises returned from their queuings with a "ERR_JOB_DEQUEUED"
type of QyuError
.
Jobs currently running at the time instance.empty()
was called keep running until finished.
const q = new Qyu({concurrency: 1});
q(job1); q(job2);
await q.empty();
instance#set(config
)
Update a living instance's config options in real time (concurrency
, capacity
and rampUpTime
).
Note these (expected) side effects:
- If new
concurrency
is specified and is greater than previous setting, new jobs will immediately be drawn and called from queue as much as the difference from previously set concurrency
up to the number of jobs that were held in queue at the time. - if new
concurrency
is specified and is lower then previous setting, will postpone calling additional jobs until enough active jobs finish and make the actual concurrency degrade to the new setting by itself. - if new
capacity
is specified and is lower than previous setting, will reject the last jobs in queue with a "ERR_CAPACITY_FULL"
type of QyuError
as much as the difference from the previously set capacity
.
const q = new Qyu({concurrency: 1, capacity: 3});
q(job1); q(job2); q(job3);
q.set({concurrency: 2, capacity: 2});
Examples
Web Scraper:
const { Qyu } = require('qyu');
const axios = require('axios');
const cheerio = require('cheerio');
(async () => {
const siteUrl = 'http://www.store-to-crawl.com/products';
const q = new Qyu({concurrency: 3});
for (let i=1; i<=10; i++) {
q(async () => {
let resp = await axios(siteUrl+'?page='+i);
let $ = cheerio.load(resp.data);
let products = [];
$('.product-list .product').each((i, elem) => {
let $elem = $(elem);
let title = $elem.find('.title').text();
let price = $elem.find('.price').text();
products.push({ title, price });
});
});
}
await q.whenEmpty();
})();