@ribajs/shopify
Version:
Shopify extension for Riba.js
285 lines (237 loc) • 6.94 kB
text/typescript
// https://github.com/sindresorhus/p-queue
import { PriorityQueue } from "./priority-queue.service.js";
export class PQueue {
public queue = new PriorityQueue();
private _carryoverConcurrencyCount: number;
private _isIntervalIgnored: boolean;
private _intervalCount = 0;
private _intervalCap: typeof Infinity;
private _interval: number;
private _intervalId?: any;
private _intervalEnd = 0;
private _timeoutId: any | null;
private _pendingCount = 0;
private _concurrency: typeof Infinity;
private _isPaused: boolean;
private _resolveEmpty: () => any;
private _resolveIdle: () => any;
constructor(options: any) {
options = Object.assign(
{
carryoverConcurrencyCount: false,
intervalCap: Infinity,
interval: 0,
concurrency: Infinity,
autoStart: true,
queueClass: PriorityQueue,
},
options,
);
if (
!(typeof options.concurrency === "number" && options.concurrency >= 1)
) {
throw new TypeError(
`Expected \`concurrency\` to be a number from 1 and up, got \`${
options.concurrency
}\` (${typeof options.concurrency})`,
);
}
if (
!(typeof options.intervalCap === "number" && options.intervalCap >= 1)
) {
throw new TypeError(
`Expected \`intervalCap\` to be a number from 1 and up, got \`${
options.intervalCap
}\` (${typeof options.intervalCap})`,
);
}
if (
!(
typeof options.interval === "number" &&
Number.isFinite(options.interval) &&
options.interval >= 0
)
) {
throw new TypeError(
`Expected \`interval\` to be a finite number >= 0, got \`${
options.interval
}\` (${typeof options.interval})`,
);
}
this._carryoverConcurrencyCount = options.carryoverConcurrencyCount;
this._isIntervalIgnored =
options.intervalCap === Infinity || options.interval === 0;
this._intervalCount = 0;
this._intervalCap = options.intervalCap;
this._interval = options.interval;
this._intervalId = undefined;
this._intervalEnd = 0;
this._timeoutId = null;
this.queue = new options.queueClass();
this._pendingCount = 0;
this._concurrency = options.concurrency;
this._isPaused = options.autoStart === false;
this._resolveEmpty = () => {};
this._resolveIdle = () => {};
}
public add(fn: any, options?: any): Promise<any> {
return new Promise((resolve, reject) => {
const run = () => {
this._pendingCount++;
this._intervalCount++;
try {
Promise.resolve(fn()).then(
(val) => {
resolve(val);
this._next();
},
(err) => {
reject(err);
this._next();
},
);
} catch (err) {
reject(err);
this._next();
}
};
this.queue.enqueue(run, options);
this._tryToStartAnother();
});
}
public addAll(fns: any, options: any) {
return Promise.all(fns.map((fn: any) => this.add(fn, options)));
}
public start() {
if (!this._isPaused) {
return;
}
this._isPaused = false;
while (this._tryToStartAnother()) {}
}
public pause() {
this._isPaused = true;
}
public clear() {
this.queue = new PriorityQueue();
}
public onEmpty() {
// Instantly resolve if the queue is empty
if (this.queue.size === 0) {
return Promise.resolve();
}
return new Promise<void>((resolve) => {
const existingResolve = this._resolveEmpty;
this._resolveEmpty = () => {
existingResolve();
resolve();
};
});
}
public onIdle(): Promise<void> {
// Instantly resolve if none pending and if nothing else is queued
if (this._pendingCount === 0 && this.queue.size === 0) {
return Promise.resolve();
}
return new Promise((resolve) => {
const existingResolve = this._resolveIdle;
this._resolveIdle = () => {
existingResolve();
resolve();
};
});
}
// private _resolveEmpty = () => { }; // eslint-disable-line
// private _resolveIdle = () => { }; // eslint-disable-line
get _doesIntervalAllowAnother() {
return this._isIntervalIgnored || this._intervalCount < this._intervalCap;
}
get _doesConcurrentAllowAnother() {
return this._pendingCount < this._concurrency;
}
private _next() {
this._pendingCount--;
this._tryToStartAnother();
}
private _resolvePromises() {
this._resolveEmpty();
this._resolveEmpty = () => {};
if (this._pendingCount === 0) {
this._resolveIdle();
this._resolveIdle = () => {};
}
}
private _onResumeInterval() {
this._onInterval();
this._initializeIntervalIfNeeded();
this._timeoutId = null;
}
private _intervalPaused() {
const now = Date.now();
if (typeof this._intervalId === undefined) {
const delay = this._intervalEnd - now;
if (delay < 0) {
// Act as the interval was done
// We don't need to resume it here,
// because it'll be resumed on line 160
this._intervalCount = this._carryoverConcurrencyCount
? this._pendingCount
: 0;
} else {
// Act as the interval is pending
if (this._timeoutId === null) {
this._timeoutId = setTimeout(() => this._onResumeInterval(), delay);
}
return true;
}
}
return false;
}
private _tryToStartAnother() {
if (this.queue.size === 0) {
// We can clear the interval ("pause")
// because we can redo it later ("resume")
clearInterval(this._intervalId);
this._intervalId = undefined;
this._resolvePromises();
return false;
}
if (!this._isPaused) {
const canInitializeInterval = !this._intervalPaused();
if (this._doesIntervalAllowAnother && this._doesConcurrentAllowAnother) {
this.queue.dequeue()();
if (canInitializeInterval) {
this._initializeIntervalIfNeeded();
}
return true;
}
}
return false;
}
private _initializeIntervalIfNeeded() {
if (this._isIntervalIgnored || this._intervalId !== null) {
return;
}
this._intervalId = setInterval(() => this._onInterval(), this._interval);
this._intervalEnd = Date.now() + this._interval;
}
private _onInterval() {
if (this._intervalCount === 0 && this._pendingCount === 0) {
clearInterval(this._intervalId);
this._intervalId = undefined;
}
this._intervalCount = this._carryoverConcurrencyCount
? this._pendingCount
: 0;
while (this._tryToStartAnother()) {}
}
get size() {
return this.queue.size;
}
get pending() {
return this._pendingCount;
}
get isPaused() {
return this._isPaused;
}
}