Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add .setPriority() for updating priority of a queued promise function #209

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a949f8d
#208 - add update priority logic, add promise started event
RaishavHanspal Jul 10, 2024
4cdcbf7
#208 - update for uidAssigner
RaishavHanspal Jul 10, 2024
8383aac
#208 - add test cases, add linting fixes
RaishavHanspal Jul 11, 2024
a60bfc1
#208 - review fixes, remove started event changes
RaishavHanspal Jul 12, 2024
cafe886
#208 - review fixes - add test case, update setPriority method
RaishavHanspal Jul 18, 2024
6a370fe
#208 - throw error if undefined item in queue while setting priority
RaishavHanspal Jul 19, 2024
ccb7a53
#208 - update spacing
RaishavHanspal Jul 19, 2024
686f0d8
#208 - use enqueue function in setPriority
RaishavHanspal Jul 19, 2024
ecc003d
feedback fixes - add test cases, update code comments, update readme
RaishavHanspal Nov 20, 2024
f7aa85a
Update options.ts
sindresorhus Nov 20, 2024
ff4a177
update readme, update comments
RaishavHanspal Nov 21, 2024
6baad95
Merge branch 'feature/update-priority' of https://github.com/RaishavH…
RaishavHanspal Nov 21, 2024
52cbeef
update spacing
RaishavHanspal Nov 21, 2024
a62603c
match doc comment for .setPriority with readme
RaishavHanspal Nov 27, 2024
46965f7
Update readme.md
sindresorhus Dec 3, 2024
ee1ab21
Update index.ts
sindresorhus Dec 3, 2024
33a2d36
Update test.ts
sindresorhus Dec 3, 2024
253d67f
Update index.ts
sindresorhus Dec 4, 2024
4b70ec7
implement review comments - update id to index, few message/comment u…
RaishavHanspal Jan 3, 2025
a0d0812
resolve conflicts
RaishavHanspal Jan 3, 2025
21a7501
revert back index to id
RaishavHanspal Jan 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ Default: `0`

Priority of operation. Operations with greater priority will be scheduled first.

##### id

Type `string`

Unique identifier for the promise function, used to update its priority before execution. If not specified, it is auto-assigned as an incrementing bigint starting from 1n.
RaishavHanspal marked this conversation as resolved.
Show resolved Hide resolved

##### signal

[`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) for cancellation of the operation. When aborted, it will be removed from the queue and the `queue.add()` call will reject with an [error](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/reason). If the operation is already running, the signal will need to be handled by the operation itself.
Expand Down Expand Up @@ -236,6 +242,44 @@ console.log(queue.sizeBy({priority: 0}));
//=> 1
```

#### .setPriority(id, priority)

Updates the priority of a promise function by its id, affecting its execution order. Requires a defined concurrency limit to take effect.

For example, this can be used to prioritize a promise function to run earlier.

```js
import PQueue from 'p-queue';

const queue = new PQueue({concurrency: 1});

queue.add(async () => '🦄', {priority: 1});
queue.add(async () => '🦀', {priority: 0, id: '🦀'});
queue.add(async () => '🦄', {priority: 1});
queue.add(async () => '🦄', {priority: 1});

queue.setPriority('🦀', 2);
```

In this case, the promise function with `id: '🦀'` runs second.

You can also deprioritize a promise function to delay its execution:

```js
import PQueue from 'p-queue';

const queue = new PQueue({concurrency: 1});

queue.add(async () => '🦄', {priority: 1});
queue.add(async () => '🦀', {priority: 1, id: '🦀'});
queue.add(async () => '🦄');
queue.add(async () => '🦄', {priority: 0});

queue.setPriority('🦀', -1);
```

Here, the promise function with `id: '🦀'` executes last.

#### .pending

Number of running items (no longer in the queue).
Expand Down
46 changes: 46 additions & 0 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@

readonly #throwOnTimeout: boolean;

// Use to assign a unique identifier to a promise function, if not explicitly specified
#idAssigner = 1n;

/**
Per-operation timeout in milliseconds. Operations fulfill once `timeout` elapses if they haven't already.

Expand Down Expand Up @@ -228,12 +231,55 @@
});
}

/**
Updates the priority of a promise function by its id, affecting its execution order. Requires a defined concurrency limit to take effect.

For example, this can be used to prioritize a promise function to run earlier.

```js
import PQueue from 'p-queue';

const queue = new PQueue({concurrency: 1});

queue.add(async () => '🦄', {priority: 1});
queue.add(async () => '🦀', {priority: 0, id: '🦀'});
queue.add(async () => '🦄', {priority: 1});
queue.add(async () => '🦄', {priority: 1});

queue.setPriority('🦀', 2);
```

Check failure on line 251 in source/index.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

Trailing spaces not allowed.

Check failure on line 251 in source/index.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

Trailing spaces not allowed.
In this case, the promise function with `id: '🦀'` runs second.

You can also deprioritize a promise function to delay its execution:

```js
import PQueue from 'p-queue';

const queue = new PQueue({concurrency: 1});

queue.add(async () => '🦄', {priority: 1});
queue.add(async () => '🦀', {priority: 1, id: '🦀'});
queue.add(async () => '🦄');
queue.add(async () => '🦄', {priority: 0});

queue.setPriority('🦀', -1);
```
Here, the promise function with `id: '🦀'` executes last.
*/
setPriority(id: string, priority: number) {
RaishavHanspal marked this conversation as resolved.
Show resolved Hide resolved
this.#queue.setPriority(id, priority);
}

/**
Adds a sync or async task to the queue. Always returns a promise.
*/
async add<TaskResultType>(function_: Task<TaskResultType>, options: {throwOnTimeout: true} & Exclude<EnqueueOptionsType, 'throwOnTimeout'>): Promise<TaskResultType>;
async add<TaskResultType>(function_: Task<TaskResultType>, options?: Partial<EnqueueOptionsType>): Promise<TaskResultType | void>;
async add<TaskResultType>(function_: Task<TaskResultType>, options: Partial<EnqueueOptionsType> = {}): Promise<TaskResultType | void> {
// In case `id` is not defined.
options.id ??= (this.#idAssigner++).toString();

options = {
timeout: this.timeout,
throwOnTimeout: this.#throwOnTimeout,
Expand Down
5 changes: 5 additions & 0 deletions source/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export type QueueAddOptions = {
@default 0
*/
readonly priority?: number;

/**
Unique identifier for the promise function, used to update its priority before execution. If not specified, it is auto-assigned as an incrementing bigint starting from 1n.
RaishavHanspal marked this conversation as resolved.
Show resolved Hide resolved
*/
id?: string;
RaishavHanspal marked this conversation as resolved.
Show resolved Hide resolved
RaishavHanspal marked this conversation as resolved.
Show resolved Hide resolved
RaishavHanspal marked this conversation as resolved.
Show resolved Hide resolved
} & TaskOptions & TimeoutOptions;

export type TaskOptions = {
Expand Down
13 changes: 12 additions & 1 deletion source/priority-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ export default class PriorityQueue implements Queue<RunFunction, PriorityQueueOp

const element = {
priority: options.priority,
id: options.id,
run,
};

if (this.size && this.#queue[this.size - 1]!.priority! >= options.priority!) {
if (this.size === 0 || this.#queue[this.size - 1]!.priority! >= options.priority!) {
this.#queue.push(element);
return;
}
Expand All @@ -32,6 +33,16 @@ export default class PriorityQueue implements Queue<RunFunction, PriorityQueueOp
this.#queue.splice(index, 0, element);
}

setPriority(id: string, priority: number) {
const existingIndex: number = this.#queue.findIndex((element: Readonly<PriorityQueueOptions>) => element.id === id);
RaishavHanspal marked this conversation as resolved.
Show resolved Hide resolved
if (existingIndex === -1) {
throw new Error('Invalid Index - No promise function of specified id available in the queue.');
RaishavHanspal marked this conversation as resolved.
Show resolved Hide resolved
}

const [item] = this.#queue.splice(existingIndex, 1);
this.enqueue(item!.run, {priority, id});
}

dequeue(): RunFunction | undefined {
const item = this.#queue.shift();
return item?.run;
Expand Down
1 change: 1 addition & 0 deletions source/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export type Queue<Element, Options> = {
filter: (options: Readonly<Partial<Options>>) => Element[];
dequeue: () => Element | undefined;
enqueue: (run: Element, options?: Partial<Options>) => void;
setPriority: (id: string, priority: number) => void;
};
163 changes: 162 additions & 1 deletion test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import inRange from 'in-range';
import timeSpan from 'time-span';
import randomInt from 'random-int';
import pDefer from 'p-defer';
import PQueue, {AbortError} from '../source/index.js';
import PQueue from '../source/index.js';

const fixture = Symbol('fixture');

Expand Down Expand Up @@ -1134,3 +1134,164 @@ test('aborting multiple jobs at the same time', async t => {
await t.throwsAsync(task2, {instanceOf: DOMException});
t.like(queue, {size: 0, pending: 0});
});

test('.setPriority() - execute a promise before planned', async t => {
const result: string[] = [];
const queue = new PQueue({concurrency: 1});
queue.add(async () => {
await delay(400);
result.push('🐌');
}, {id: '🐌'});
queue.add(async () => {
await delay(400);
result.push('🦆');
}, {id: '🦆'});
queue.add(async () => {
await delay(400);
result.push('🐢');
}, {id: '🐢'});
queue.setPriority('🐢', 1);
await queue.onIdle();
t.deepEqual(result, ['🐌', '🐢', '🦆']);
});

test('.setPriority() - execute a promise after planned', async t => {
const result: string[] = [];
const queue = new PQueue({concurrency: 1});
queue.add(async () => {
await delay(400);
result.push('🐌');
}, {id: '🐌'});
queue.add(async () => {
await delay(400);
result.push('🦆');
}, {id: '🦆'});
queue.add(async () => {
await delay(400);
result.push('🦆');
}, {id: '🦆'});
queue.add(async () => {
await delay(400);
result.push('🐢');
}, {id: '🐢'});
queue.add(async () => {
await delay(400);
result.push('🦆');
}, {id: '🦆'});
queue.add(async () => {
await delay(400);
result.push('🦆');
}, {id: '🦆'});
queue.setPriority('🐢', -1);
await queue.onIdle();
t.deepEqual(result, ['🐌', '🦆', '🦆', '🦆', '🦆', '🐢']);
});

test('.setPriority() - execute a promise before planned - concurrency 2', async t => {
const result: string[] = [];
const queue = new PQueue({concurrency: 2});
queue.add(async () => {
await delay(400);
result.push('🐌');
}, {id: '🐌'});
queue.add(async () => {
await delay(400);
result.push('🦆');
}, {id: '🦆'});
queue.add(async () => {
await delay(400);
result.push('🐢');
}, {id: '🐢'});
queue.add(async () => {
await delay(400);
result.push('⚡️');
}, {id: '⚡️'});
queue.setPriority('⚡️', 1);
await queue.onIdle();
t.deepEqual(result, ['🐌', '🦆', '⚡️', '🐢']);
});

test('.setPriority() - execute a promise before planned - concurrency 3', async t => {
const result: string[] = [];
const queue = new PQueue({concurrency: 3});
queue.add(async () => {
await delay(400);
result.push('🐌');
}, {id: '🐌'});
queue.add(async () => {
await delay(400);
result.push('🦆');
}, {id: '🦆'});
queue.add(async () => {
await delay(400);
result.push('🐢');
}, {id: '🐢'});
queue.add(async () => {
await delay(400);
result.push('⚡️');
}, {id: '⚡️'});
queue.add(async () => {
await delay(400);
result.push('🦀');
}, {id: '🦀'});
queue.setPriority('🦀', 1);
await queue.onIdle();
t.deepEqual(result, ['🐌', '🦆', '🐢', '🦀', '⚡️']);
});

test('.setPriority() - execute a multiple promise before planned, with variable priority', async t => {
const result: string[] = [];
const queue = new PQueue({concurrency: 2});
queue.add(async () => {
await delay(400);
result.push('🐌');
}, {id: '🐌'});
queue.add(async () => {
await delay(400);
result.push('🦆');
}, {id: '🦆'});
queue.add(async () => {
await delay(400);
result.push('🐢');
}, {id: '🐢'});
queue.add(async () => {
await delay(400);
result.push('⚡️');
}, {id: '⚡️'});
queue.add(async () => {
await delay(400);
result.push('🦀');
}, {id: '🦀'});
queue.setPriority('⚡️', 1);
queue.setPriority('🦀', 2);
await queue.onIdle();
t.deepEqual(result, ['🐌', '🦆', '🦀', '⚡️', '🐢']);
});

test('.setPriority() - execute a promise before planned - concurrency 3 and unspecified `id`', async t => {
const result: string[] = [];
const queue = new PQueue({concurrency: 3});
queue.add(async () => {
await delay(400);
result.push('🐌');
});
queue.add(async () => {
await delay(400);
result.push('🦆');
});
queue.add(async () => {
await delay(400);
result.push('🐢');
});
queue.add(async () => {
await delay(400);
result.push('⚡️');
});
queue.add(async () => {
await delay(400);
result.push('🦀');
});
queue.setPriority('5', 1);
await queue.onIdle();
t.deepEqual(result, ['🐌', '🦆', '🐢', '🦀', '⚡️']);
});
Loading