@libp2p/utils 5.1.1 → 5.2.0
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/src/peer-queue.d.ts +14 -0
- package/dist/src/peer-queue.d.ts.map +1 -0
- package/dist/src/peer-queue.js +16 -0
- package/dist/src/peer-queue.js.map +1 -0
- package/dist/src/queue/index.d.ts +122 -0
- package/dist/src/queue/index.d.ts.map +1 -0
- package/dist/src/queue/index.js +262 -0
- package/dist/src/queue/index.js.map +1 -0
- package/dist/src/queue/job.d.ts +25 -0
- package/dist/src/queue/job.d.ts.map +1 -0
- package/dist/src/queue/job.js +82 -0
- package/dist/src/queue/job.js.map +1 -0
- package/dist/src/queue/recipient.d.ts +10 -0
- package/dist/src/queue/recipient.d.ts.map +1 -0
- package/dist/src/queue/recipient.js +21 -0
- package/dist/src/queue/recipient.js.map +1 -0
- package/dist/src/tracked-list.d.ts +13 -0
- package/dist/src/tracked-list.d.ts.map +1 -0
- package/dist/src/tracked-list.js +11 -0
- package/dist/src/tracked-list.js.map +1 -0
- package/dist/typedoc-urls.json +29 -5
- package/package.json +18 -11
- package/src/peer-queue.ts +24 -0
- package/src/queue/index.ts +365 -0
- package/src/queue/job.ts +105 -0
- package/src/queue/recipient.ts +26 -0
- package/src/tracked-list.ts +26 -0
- package/dist/src/peer-job-queue.d.ts +0 -42
- package/dist/src/peer-job-queue.d.ts.map +0 -1
- package/dist/src/peer-job-queue.js +0 -132
- package/dist/src/peer-job-queue.js.map +0 -1
- package/src/peer-job-queue.ts +0 -187
package/dist/typedoc-urls.json
CHANGED
|
@@ -25,12 +25,36 @@
|
|
|
25
25
|
"./multiaddr/is-loopback:isLoopback": "https://libp2p.github.io/js-libp2p/functions/_libp2p_utils.multiaddr_is_loopback.isLoopback.html",
|
|
26
26
|
"isPrivate": "https://libp2p.github.io/js-libp2p/functions/_libp2p_utils.multiaddr_is_private.isPrivate.html",
|
|
27
27
|
"./multiaddr/is-private:isPrivate": "https://libp2p.github.io/js-libp2p/functions/_libp2p_utils.multiaddr_is_private.isPrivate.html",
|
|
28
|
-
"
|
|
29
|
-
"./peer-
|
|
30
|
-
"
|
|
31
|
-
"./peer-
|
|
28
|
+
"PeerQueue": "https://libp2p.github.io/js-libp2p/classes/_libp2p_utils.peer_queue.PeerQueue.html",
|
|
29
|
+
"./peer-queue:PeerQueue": "https://libp2p.github.io/js-libp2p/classes/_libp2p_utils.peer_queue.PeerQueue.html",
|
|
30
|
+
"PeerQueueOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.peer_queue.PeerQueueOptions.html",
|
|
31
|
+
"./peer-queue:PeerQueueOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.peer_queue.PeerQueueOptions.html",
|
|
32
|
+
"Queue": "https://libp2p.github.io/js-libp2p/classes/_libp2p_utils.queue.Queue.html",
|
|
33
|
+
"./queue:Queue": "https://libp2p.github.io/js-libp2p/classes/_libp2p_utils.queue.Queue.html",
|
|
34
|
+
"JobMatcher": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.queue.JobMatcher.html",
|
|
35
|
+
"./queue:JobMatcher": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.queue.JobMatcher.html",
|
|
36
|
+
"QueueAddOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.queue.QueueAddOptions.html",
|
|
37
|
+
"./queue:QueueAddOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.queue.QueueAddOptions.html",
|
|
38
|
+
"QueueEvents": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.queue.QueueEvents.html",
|
|
39
|
+
"./queue:QueueEvents": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.queue.QueueEvents.html",
|
|
40
|
+
"QueueInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.queue.QueueInit.html",
|
|
41
|
+
"./queue:QueueInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.queue.QueueInit.html",
|
|
42
|
+
"RunFunction": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.queue.RunFunction.html",
|
|
43
|
+
"./queue:RunFunction": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.queue.RunFunction.html",
|
|
44
|
+
"JobStatus": "https://libp2p.github.io/js-libp2p/types/_libp2p_utils.queue.JobStatus.html",
|
|
45
|
+
"./queue:JobStatus": "https://libp2p.github.io/js-libp2p/types/_libp2p_utils.queue.JobStatus.html",
|
|
32
46
|
"StreamProperties": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.stream_to_ma_conn.StreamProperties.html",
|
|
33
47
|
"./stream-to-ma-conn:StreamProperties": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.stream_to_ma_conn.StreamProperties.html",
|
|
34
48
|
"streamToMaConnection": "https://libp2p.github.io/js-libp2p/functions/_libp2p_utils.stream_to_ma_conn.streamToMaConnection.html",
|
|
35
|
-
"./stream-to-ma-conn:streamToMaConnection": "https://libp2p.github.io/js-libp2p/functions/_libp2p_utils.stream_to_ma_conn.streamToMaConnection.html"
|
|
49
|
+
"./stream-to-ma-conn:streamToMaConnection": "https://libp2p.github.io/js-libp2p/functions/_libp2p_utils.stream_to_ma_conn.streamToMaConnection.html",
|
|
50
|
+
"CreateTrackedListInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.tracked_list.CreateTrackedListInit.html",
|
|
51
|
+
"./tracked-list:CreateTrackedListInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.tracked_list.CreateTrackedListInit.html",
|
|
52
|
+
"trackedList": "https://libp2p.github.io/js-libp2p/functions/_libp2p_utils.tracked_list.trackedList.html",
|
|
53
|
+
"./tracked-list:trackedList": "https://libp2p.github.io/js-libp2p/functions/_libp2p_utils.tracked_list.trackedList.html",
|
|
54
|
+
"CreateTrackedMapInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.tracked_map.CreateTrackedMapInit.html",
|
|
55
|
+
"./tracked-map:CreateTrackedMapInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.tracked_map.CreateTrackedMapInit.html",
|
|
56
|
+
"TrackedMapInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.tracked_map.TrackedMapInit.html",
|
|
57
|
+
"./tracked-map:TrackedMapInit": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.tracked_map.TrackedMapInit.html",
|
|
58
|
+
"trackedMap": "https://libp2p.github.io/js-libp2p/functions/_libp2p_utils.tracked_map.trackedMap.html",
|
|
59
|
+
"./tracked-map:trackedMap": "https://libp2p.github.io/js-libp2p/functions/_libp2p_utils.tracked_map.trackedMap.html"
|
|
36
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@libp2p/utils",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "Package to aggregate shared logic and dependencies for the libp2p ecosystem",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
6
|
"homepage": "https://github.com/libp2p/js-libp2p/tree/main/packages/utils#readme",
|
|
@@ -76,14 +76,22 @@
|
|
|
76
76
|
"types": "./dist/src/multiaddr/is-private.d.ts",
|
|
77
77
|
"import": "./dist/src/multiaddr/is-private.js"
|
|
78
78
|
},
|
|
79
|
-
"./
|
|
80
|
-
"types": "./dist/src/
|
|
81
|
-
"import": "./dist/src/
|
|
79
|
+
"./queue": {
|
|
80
|
+
"types": "./dist/src/queue/index.d.ts",
|
|
81
|
+
"import": "./dist/src/queue/index.js"
|
|
82
|
+
},
|
|
83
|
+
"./peer-queue": {
|
|
84
|
+
"types": "./dist/src/peer-queue.d.ts",
|
|
85
|
+
"import": "./dist/src/peer-queue.js"
|
|
82
86
|
},
|
|
83
87
|
"./stream-to-ma-conn": {
|
|
84
88
|
"types": "./dist/src/stream-to-ma-conn.d.ts",
|
|
85
89
|
"import": "./dist/src/stream-to-ma-conn.js"
|
|
86
90
|
},
|
|
91
|
+
"./tracked-list": {
|
|
92
|
+
"types": "./dist/src/tracked-list.d.ts",
|
|
93
|
+
"import": "./dist/src/tracked-list.js"
|
|
94
|
+
},
|
|
87
95
|
"./tracked-map": {
|
|
88
96
|
"types": "./dist/src/tracked-map.d.ts",
|
|
89
97
|
"import": "./dist/src/tracked-map.js"
|
|
@@ -111,29 +119,28 @@
|
|
|
111
119
|
},
|
|
112
120
|
"dependencies": {
|
|
113
121
|
"@chainsafe/is-ip": "^2.0.2",
|
|
114
|
-
"@libp2p/interface": "^1.1.
|
|
115
|
-
"@libp2p/
|
|
122
|
+
"@libp2p/interface": "^1.1.1",
|
|
123
|
+
"@libp2p/logger": "^4.0.4",
|
|
116
124
|
"@multiformats/multiaddr": "^12.1.10",
|
|
117
125
|
"@multiformats/multiaddr-matcher": "^1.1.0",
|
|
118
126
|
"get-iterator": "^2.0.1",
|
|
119
127
|
"is-loopback-addr": "^2.0.1",
|
|
120
128
|
"it-pushable": "^3.2.2",
|
|
121
129
|
"it-stream-types": "^2.0.1",
|
|
122
|
-
"p-
|
|
130
|
+
"p-defer": "^4.0.0",
|
|
123
131
|
"private-ip": "^3.0.1",
|
|
132
|
+
"race-event": "^1.1.0",
|
|
124
133
|
"race-signal": "^1.0.1",
|
|
125
134
|
"uint8arraylist": "^2.4.3"
|
|
126
135
|
},
|
|
127
136
|
"devDependencies": {
|
|
128
|
-
"@libp2p/
|
|
129
|
-
"
|
|
130
|
-
"aegir": "^41.0.2",
|
|
137
|
+
"@libp2p/peer-id-factory": "^4.0.3",
|
|
138
|
+
"aegir": "^42.0.0",
|
|
131
139
|
"delay": "^6.0.0",
|
|
132
140
|
"it-all": "^3.0.3",
|
|
133
141
|
"it-drain": "^3.0.5",
|
|
134
142
|
"it-pair": "^2.0.6",
|
|
135
143
|
"it-pipe": "^3.0.1",
|
|
136
|
-
"p-defer": "^4.0.0",
|
|
137
144
|
"sinon": "^17.0.1",
|
|
138
145
|
"sinon-ts": "^2.0.0",
|
|
139
146
|
"uint8arrays": "^5.0.0"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
|
|
3
|
+
import { Queue, type QueueAddOptions } from './queue/index.js'
|
|
4
|
+
import type { Job } from './queue/job.js'
|
|
5
|
+
import type { PeerId } from '@libp2p/interface'
|
|
6
|
+
|
|
7
|
+
export interface PeerQueueOptions extends QueueAddOptions {
|
|
8
|
+
peerId: PeerId
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extends Queue to add support for querying queued jobs by peer id
|
|
13
|
+
*/
|
|
14
|
+
export class PeerQueue<JobReturnType = void> extends Queue<JobReturnType, PeerQueueOptions> {
|
|
15
|
+
has (peerId: PeerId): boolean {
|
|
16
|
+
return this.find(peerId) != null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
find (peerId: PeerId): Job<PeerQueueOptions, JobReturnType> | undefined {
|
|
20
|
+
return this.queue.find(job => {
|
|
21
|
+
return peerId.equals(job.options.peerId)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { AbortError, CodeError, TypedEventEmitter } from '@libp2p/interface'
|
|
2
|
+
import { pushable } from 'it-pushable'
|
|
3
|
+
import { raceEvent } from 'race-event'
|
|
4
|
+
import { Job } from './job.js'
|
|
5
|
+
import type { AbortOptions, Metrics } from '@libp2p/interface'
|
|
6
|
+
|
|
7
|
+
export interface QueueAddOptions extends AbortOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Priority of operation. Operations with greater priority will be scheduled first.
|
|
10
|
+
*
|
|
11
|
+
* @default 0
|
|
12
|
+
*/
|
|
13
|
+
priority?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface QueueInit {
|
|
17
|
+
/**
|
|
18
|
+
* Concurrency limit.
|
|
19
|
+
*
|
|
20
|
+
* Minimum: `1`.
|
|
21
|
+
*
|
|
22
|
+
* @default Infinity
|
|
23
|
+
*/
|
|
24
|
+
concurrency?: number
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The name of the metric for the queue length
|
|
28
|
+
*/
|
|
29
|
+
metricName?: string
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An implementation of the libp2p Metrics interface
|
|
33
|
+
*/
|
|
34
|
+
metrics?: Metrics
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type JobStatus = 'queued' | 'running' | 'errored' | 'complete'
|
|
38
|
+
|
|
39
|
+
export interface RunFunction<Options = AbortOptions, ReturnType = void> {
|
|
40
|
+
(opts?: Options): Promise<ReturnType>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface JobMatcher<JobOptions extends QueueAddOptions = QueueAddOptions> {
|
|
44
|
+
(options?: Partial<JobOptions>): boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface QueueEvents<JobReturnType> {
|
|
48
|
+
'active': CustomEvent
|
|
49
|
+
'idle': CustomEvent
|
|
50
|
+
'empty': CustomEvent
|
|
51
|
+
'add': CustomEvent
|
|
52
|
+
'next': CustomEvent
|
|
53
|
+
'completed': CustomEvent<JobReturnType>
|
|
54
|
+
'error': CustomEvent<Error>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Port of lower_bound from https://en.cppreference.com/w/cpp/algorithm/lower_bound
|
|
58
|
+
// Used to compute insertion index to keep queue sorted after insertion
|
|
59
|
+
function lowerBound<T> (array: readonly T[], value: T, comparator: (a: T, b: T) => number): number {
|
|
60
|
+
let first = 0
|
|
61
|
+
let count = array.length
|
|
62
|
+
|
|
63
|
+
while (count > 0) {
|
|
64
|
+
const step = Math.trunc(count / 2)
|
|
65
|
+
let it = first + step
|
|
66
|
+
|
|
67
|
+
if (comparator(array[it], value) <= 0) {
|
|
68
|
+
first = ++it
|
|
69
|
+
count -= step + 1
|
|
70
|
+
} else {
|
|
71
|
+
count = step
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return first
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Heavily influence by `p-queue` with the following differences:
|
|
80
|
+
*
|
|
81
|
+
* 1. Items remain at the head of the queue while they are running so `queue.size` includes `queue.pending` items - this is so interested parties can join the results of a queue item while it is running
|
|
82
|
+
* 2. The options for a job are stored separately to the job in order for them to be modified while they are still in the queue
|
|
83
|
+
*/
|
|
84
|
+
export class Queue<JobReturnType = unknown, JobOptions extends QueueAddOptions = QueueAddOptions> extends TypedEventEmitter<QueueEvents<JobReturnType>> {
|
|
85
|
+
public concurrency: number
|
|
86
|
+
public queue: Array<Job<JobOptions, JobReturnType>>
|
|
87
|
+
private pending: number
|
|
88
|
+
|
|
89
|
+
constructor (init: QueueInit = {}) {
|
|
90
|
+
super()
|
|
91
|
+
|
|
92
|
+
this.concurrency = init.concurrency ?? Number.POSITIVE_INFINITY
|
|
93
|
+
this.pending = 0
|
|
94
|
+
|
|
95
|
+
if (init.metricName != null) {
|
|
96
|
+
init.metrics?.registerMetricGroup(init.metricName, {
|
|
97
|
+
calculate: () => {
|
|
98
|
+
return {
|
|
99
|
+
size: this.queue.length,
|
|
100
|
+
running: this.pending,
|
|
101
|
+
queued: this.queue.length - this.pending
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.queue = []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private tryToStartAnother (): boolean {
|
|
111
|
+
if (this.size === 0) {
|
|
112
|
+
// do this in the microtask queue so all job recipients receive the
|
|
113
|
+
// result before the "empty" event fires
|
|
114
|
+
queueMicrotask(() => {
|
|
115
|
+
this.safeDispatchEvent('empty')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if (this.running === 0) {
|
|
119
|
+
// do this in the microtask queue so all job recipients receive the
|
|
120
|
+
// result before the "idle" event fires
|
|
121
|
+
queueMicrotask(() => {
|
|
122
|
+
this.safeDispatchEvent('idle')
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.pending < this.concurrency) {
|
|
130
|
+
let job: Job<JobOptions, JobReturnType> | undefined
|
|
131
|
+
|
|
132
|
+
for (const j of this.queue) {
|
|
133
|
+
if (j.status === 'queued') {
|
|
134
|
+
job = j
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (job == null) {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.safeDispatchEvent('active')
|
|
144
|
+
|
|
145
|
+
this.pending++
|
|
146
|
+
|
|
147
|
+
job.run()
|
|
148
|
+
.finally(() => {
|
|
149
|
+
// remove the job from the queue
|
|
150
|
+
for (let i = 0; i < this.queue.length; i++) {
|
|
151
|
+
if (this.queue[i] === job) {
|
|
152
|
+
this.queue.splice(i, 1)
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.pending--
|
|
158
|
+
this.tryToStartAnother()
|
|
159
|
+
this.safeDispatchEvent('next')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private enqueue (job: Job<JobOptions, JobReturnType>): void {
|
|
169
|
+
if (this.queue[this.size - 1]?.priority >= job.priority) {
|
|
170
|
+
this.queue.push(job)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const index = lowerBound(
|
|
175
|
+
this.queue, job,
|
|
176
|
+
(a: Readonly< Job<JobOptions, JobReturnType>>, b: Readonly< Job<JobOptions, JobReturnType>>) => b.priority - a.priority
|
|
177
|
+
)
|
|
178
|
+
this.queue.splice(index, 0, job)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Adds a sync or async task to the queue. Always returns a promise.
|
|
183
|
+
*/
|
|
184
|
+
async add (fn: RunFunction<JobOptions, JobReturnType>, options?: JobOptions): Promise<JobReturnType> {
|
|
185
|
+
options?.signal?.throwIfAborted()
|
|
186
|
+
|
|
187
|
+
const job = new Job<JobOptions, JobReturnType>(fn, options, options?.priority)
|
|
188
|
+
|
|
189
|
+
const p = job.join(options)
|
|
190
|
+
.then(result => {
|
|
191
|
+
this.safeDispatchEvent('completed', { detail: result })
|
|
192
|
+
|
|
193
|
+
return result
|
|
194
|
+
})
|
|
195
|
+
.catch(err => {
|
|
196
|
+
this.safeDispatchEvent('error', { detail: err })
|
|
197
|
+
|
|
198
|
+
throw err
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
this.enqueue(job)
|
|
202
|
+
this.safeDispatchEvent('add')
|
|
203
|
+
this.tryToStartAnother()
|
|
204
|
+
|
|
205
|
+
return p
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Clear the queue
|
|
210
|
+
*/
|
|
211
|
+
clear (): void {
|
|
212
|
+
this.queue.splice(0, this.queue.length)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Abort all jobs in the queue and clear it
|
|
217
|
+
*/
|
|
218
|
+
abort (): void {
|
|
219
|
+
this.queue.forEach(job => {
|
|
220
|
+
job.abort(new AbortError())
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
this.clear()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Can be called multiple times. Useful if you for example add additional items at a later time.
|
|
228
|
+
*
|
|
229
|
+
* @returns A promise that settles when the queue becomes empty.
|
|
230
|
+
*/
|
|
231
|
+
async onEmpty (options?: AbortOptions): Promise<void> {
|
|
232
|
+
// Instantly resolve if the queue is empty
|
|
233
|
+
if (this.size === 0) {
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await raceEvent(this, 'empty', options?.signal)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @returns A promise that settles when the queue size is less than the given
|
|
242
|
+
* limit: `queue.size < limit`.
|
|
243
|
+
*
|
|
244
|
+
* If you want to avoid having the queue grow beyond a certain size you can
|
|
245
|
+
* `await queue.onSizeLessThan()` before adding a new item.
|
|
246
|
+
*
|
|
247
|
+
* Note that this only limits the number of items waiting to start. There
|
|
248
|
+
* could still be up to `concurrency` jobs already running that this call does
|
|
249
|
+
* not include in its calculation.
|
|
250
|
+
*/
|
|
251
|
+
async onSizeLessThan (limit: number, options?: AbortOptions): Promise<void> {
|
|
252
|
+
// Instantly resolve if the queue is empty.
|
|
253
|
+
if (this.size < limit) {
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await raceEvent(this, 'next', options?.signal, {
|
|
258
|
+
filter: () => this.size < limit
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The difference with `.onEmpty` is that `.onIdle` guarantees that all work
|
|
264
|
+
* from the queue has finished. `.onEmpty` merely signals that the queue is
|
|
265
|
+
* empty, but it could mean that some promises haven't completed yet.
|
|
266
|
+
*
|
|
267
|
+
* @returns A promise that settles when the queue becomes empty, and all
|
|
268
|
+
* promises have completed; `queue.size === 0 && queue.pending === 0`.
|
|
269
|
+
*/
|
|
270
|
+
async onIdle (options?: AbortOptions): Promise<void> {
|
|
271
|
+
// Instantly resolve if none pending and if nothing else is queued
|
|
272
|
+
if (this.pending === 0 && this.size === 0) {
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await raceEvent(this, 'idle', options?.signal)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Size of the queue including running items
|
|
281
|
+
*/
|
|
282
|
+
get size (): number {
|
|
283
|
+
return this.queue.length
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The number of queued items waiting to run.
|
|
288
|
+
*/
|
|
289
|
+
get queued (): number {
|
|
290
|
+
return this.queue.length - this.pending
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The number of items currently running.
|
|
295
|
+
*/
|
|
296
|
+
get running (): number {
|
|
297
|
+
return this.pending
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Returns an async generator that makes it easy to iterate over the results
|
|
302
|
+
* of jobs added to the queue.
|
|
303
|
+
*
|
|
304
|
+
* The generator will end when the queue becomes idle, that is there are no
|
|
305
|
+
* jobs running and no jobs that have yet to run.
|
|
306
|
+
*
|
|
307
|
+
* If you need to keep the queue open indefinitely, consider using it-pushable
|
|
308
|
+
* instead.
|
|
309
|
+
*/
|
|
310
|
+
async * toGenerator (options?: AbortOptions): AsyncGenerator<JobReturnType, void, unknown> {
|
|
311
|
+
options?.signal?.throwIfAborted()
|
|
312
|
+
|
|
313
|
+
const stream = pushable<JobReturnType>({
|
|
314
|
+
objectMode: true
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const cleanup = (err?: Error): void => {
|
|
318
|
+
if (err != null) {
|
|
319
|
+
this.abort()
|
|
320
|
+
} else {
|
|
321
|
+
this.clear()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
stream.end(err)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const onQueueJobComplete = (evt: CustomEvent<JobReturnType>): void => {
|
|
328
|
+
if (evt.detail != null) {
|
|
329
|
+
stream.push(evt.detail)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const onQueueError = (evt: CustomEvent<Error>): void => {
|
|
334
|
+
cleanup(evt.detail)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const onQueueIdle = (): void => {
|
|
338
|
+
cleanup()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// clear the queue and throw if the query is aborted
|
|
342
|
+
const onSignalAbort = (): void => {
|
|
343
|
+
cleanup(new CodeError('Queue aborted', 'ERR_QUEUE_ABORTED'))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// add listeners
|
|
347
|
+
this.addEventListener('completed', onQueueJobComplete)
|
|
348
|
+
this.addEventListener('error', onQueueError)
|
|
349
|
+
this.addEventListener('idle', onQueueIdle)
|
|
350
|
+
options?.signal?.addEventListener('abort', onSignalAbort)
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
yield * stream
|
|
354
|
+
} finally {
|
|
355
|
+
// remove listeners
|
|
356
|
+
this.removeEventListener('completed', onQueueJobComplete)
|
|
357
|
+
this.removeEventListener('error', onQueueError)
|
|
358
|
+
this.removeEventListener('idle', onQueueIdle)
|
|
359
|
+
options?.signal?.removeEventListener('abort', onSignalAbort)
|
|
360
|
+
|
|
361
|
+
// empty the queue for when the user has broken out of a loop early
|
|
362
|
+
cleanup()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
package/src/queue/job.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { AbortError, setMaxListeners } from '@libp2p/interface'
|
|
2
|
+
import { raceSignal } from 'race-signal'
|
|
3
|
+
import { JobRecipient } from './recipient.js'
|
|
4
|
+
import type { JobStatus } from './index.js'
|
|
5
|
+
import type { AbortOptions } from '@libp2p/interface'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns a random string
|
|
9
|
+
*/
|
|
10
|
+
function randomId (): string {
|
|
11
|
+
return `${(parseInt(String(Math.random() * 1e9), 10)).toString()}${Date.now()}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface JobTimeline {
|
|
15
|
+
created: number
|
|
16
|
+
started?: number
|
|
17
|
+
finished?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class Job <JobOptions extends AbortOptions = AbortOptions, JobReturnType = unknown> {
|
|
21
|
+
public id: string
|
|
22
|
+
public fn: (options: JobOptions) => Promise<JobReturnType>
|
|
23
|
+
public options: JobOptions
|
|
24
|
+
public priority: number
|
|
25
|
+
public recipients: Array<JobRecipient<JobReturnType>>
|
|
26
|
+
public status: JobStatus
|
|
27
|
+
public readonly timeline: JobTimeline
|
|
28
|
+
private readonly controller: AbortController
|
|
29
|
+
|
|
30
|
+
constructor (fn: (options: JobOptions) => Promise<JobReturnType>, options: any, priority: number = 0) {
|
|
31
|
+
this.id = randomId()
|
|
32
|
+
this.status = 'queued'
|
|
33
|
+
this.fn = fn
|
|
34
|
+
this.priority = priority
|
|
35
|
+
this.options = options
|
|
36
|
+
this.recipients = []
|
|
37
|
+
this.timeline = {
|
|
38
|
+
created: Date.now()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.controller = new AbortController()
|
|
42
|
+
setMaxListeners(Infinity, this.controller.signal)
|
|
43
|
+
|
|
44
|
+
this.onAbort = this.onAbort.bind(this)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
abort (err: Error): void {
|
|
48
|
+
this.controller.abort(err)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
onAbort (): void {
|
|
52
|
+
const allAborted = this.recipients.reduce((acc, curr) => {
|
|
53
|
+
return acc && (curr.signal?.aborted === true)
|
|
54
|
+
}, true)
|
|
55
|
+
|
|
56
|
+
// if all recipients have aborted the job, actually abort the job
|
|
57
|
+
if (allAborted) {
|
|
58
|
+
this.controller.abort(new AbortError())
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async join (options: AbortOptions = {}): Promise<JobReturnType> {
|
|
63
|
+
const recipient = new JobRecipient<JobReturnType>((new Error('where')).stack, options.signal)
|
|
64
|
+
this.recipients.push(recipient)
|
|
65
|
+
|
|
66
|
+
options.signal?.addEventListener('abort', this.onAbort)
|
|
67
|
+
|
|
68
|
+
return recipient.deferred.promise
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async run (): Promise<void> {
|
|
72
|
+
this.status = 'running'
|
|
73
|
+
this.timeline.started = Date.now()
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
this.controller.signal.throwIfAborted()
|
|
77
|
+
|
|
78
|
+
const result = await raceSignal(this.fn({
|
|
79
|
+
...(this.options ?? {}),
|
|
80
|
+
signal: this.controller.signal
|
|
81
|
+
}), this.controller.signal)
|
|
82
|
+
|
|
83
|
+
this.recipients.forEach(recipient => {
|
|
84
|
+
recipient.deferred.resolve(result)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
this.status = 'complete'
|
|
88
|
+
} catch (err) {
|
|
89
|
+
this.recipients.forEach(recipient => {
|
|
90
|
+
recipient.deferred.reject(err)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
this.status = 'errored'
|
|
94
|
+
} finally {
|
|
95
|
+
this.timeline.finished = Date.now()
|
|
96
|
+
this.cleanup()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
cleanup (): void {
|
|
101
|
+
this.recipients.forEach(recipient => {
|
|
102
|
+
recipient.signal?.removeEventListener('abort', this.onAbort)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AbortError } from '@libp2p/interface'
|
|
2
|
+
import pDefer from 'p-defer'
|
|
3
|
+
import type { DeferredPromise } from 'p-defer'
|
|
4
|
+
|
|
5
|
+
export class JobRecipient<JobReturnType> {
|
|
6
|
+
public deferred: DeferredPromise<JobReturnType>
|
|
7
|
+
public signal?: AbortSignal
|
|
8
|
+
public where?: string
|
|
9
|
+
|
|
10
|
+
constructor (where?: string, signal?: AbortSignal) {
|
|
11
|
+
this.signal = signal
|
|
12
|
+
this.deferred = pDefer()
|
|
13
|
+
this.where = where
|
|
14
|
+
|
|
15
|
+
this.onAbort = this.onAbort.bind(this)
|
|
16
|
+
this.signal?.addEventListener('abort', this.onAbort)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
onAbort (): void {
|
|
20
|
+
this.deferred.reject(new AbortError())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
cleanup (): void {
|
|
24
|
+
this.signal?.removeEventListener('abort', this.onAbort)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Metrics } from '@libp2p/interface'
|
|
2
|
+
|
|
3
|
+
export interface CreateTrackedListInit {
|
|
4
|
+
/**
|
|
5
|
+
* The metric name to use
|
|
6
|
+
*/
|
|
7
|
+
name: string
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A metrics implementation
|
|
11
|
+
*/
|
|
12
|
+
metrics?: Metrics
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function trackedList <V> (config: CreateTrackedListInit): V[] {
|
|
16
|
+
const { name, metrics } = config
|
|
17
|
+
const list: V[] = []
|
|
18
|
+
|
|
19
|
+
metrics?.registerMetric(name, {
|
|
20
|
+
calculate: () => {
|
|
21
|
+
return list.length
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return list
|
|
26
|
+
}
|