@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.
@@ -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
- "PeerJobQueue": "https://libp2p.github.io/js-libp2p/classes/_libp2p_utils.peer_job_queue.PeerJobQueue.html",
29
- "./peer-job-queue:PeerJobQueue": "https://libp2p.github.io/js-libp2p/classes/_libp2p_utils.peer_job_queue.PeerJobQueue.html",
30
- "PeerPriorityQueueOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.peer_job_queue.PeerPriorityQueueOptions.html",
31
- "./peer-job-queue:PeerPriorityQueueOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_utils.peer_job_queue.PeerPriorityQueueOptions.html",
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.1.1",
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
- "./peer-job-queue": {
80
- "types": "./dist/src/peer-job-queue.d.ts",
81
- "import": "./dist/src/peer-job-queue.js"
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.0",
115
- "@libp2p/peer-collections": "^5.1.2",
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-queue": "^8.0.0",
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/logger": "^4.0.3",
129
- "@libp2p/peer-id-factory": "^4.0.2",
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
+ }
@@ -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
+ }