@autonomys/asynchronous 1.4.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/README.md +5 -0
- package/__test__/weightedRequestsConcurrencyController.spec.ts +94 -0
- package/jest.config.js +5 -0
- package/package.json +33 -0
- package/src/asyncGenerators/index.ts +116 -0
- package/src/concurrency/index.ts +1 -0
- package/src/concurrency/weightedRequestsConcurrencyController.ts +66 -0
- package/src/index.ts +2 -0
- package/tsconfig.json +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { weightedRequestConcurrencyController } from '../src/concurrency/weightedRequestsConcurrencyController'
|
|
2
|
+
|
|
3
|
+
const mockRequest = async (id: number) => {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
setTimeout(() => {
|
|
6
|
+
resolve(`Response from request ${id}`)
|
|
7
|
+
}, 100)
|
|
8
|
+
})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const mockTimestampedRequest = async () => {
|
|
12
|
+
const timestamp = Date.now()
|
|
13
|
+
return new Promise<number>((resolve) => {
|
|
14
|
+
setTimeout(() => {
|
|
15
|
+
resolve(timestamp)
|
|
16
|
+
}, 100)
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('MultirequestAsyncController', () => {
|
|
21
|
+
it('should handle multiple requests resolving in order', async () => {
|
|
22
|
+
const maxConcurrency = 2
|
|
23
|
+
const controller = weightedRequestConcurrencyController(maxConcurrency)
|
|
24
|
+
const requests = Array.from({ length: 5 }, (_, i) => () => mockRequest(i + 1))
|
|
25
|
+
|
|
26
|
+
const results = await Promise.all(requests.map((request) => controller(request, 1)))
|
|
27
|
+
|
|
28
|
+
expect(results).toEqual([
|
|
29
|
+
'Response from request 1',
|
|
30
|
+
'Response from request 2',
|
|
31
|
+
'Response from request 3',
|
|
32
|
+
'Response from request 4',
|
|
33
|
+
'Response from request 5',
|
|
34
|
+
])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should handle multiple requests applying without exceeding max concurrency', async () => {
|
|
38
|
+
let counter = 0
|
|
39
|
+
const maxConcurrency = 3
|
|
40
|
+
const requests = Array.from({ length: 5 }, () => async () => {
|
|
41
|
+
if (counter > maxConcurrency) throw new Error('Too many requests')
|
|
42
|
+
counter++
|
|
43
|
+
await mockRequest(counter)
|
|
44
|
+
counter--
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const controller = weightedRequestConcurrencyController(maxConcurrency)
|
|
48
|
+
await expect(
|
|
49
|
+
Promise.all(requests.map((request) => controller(request, 1))),
|
|
50
|
+
).resolves.not.toThrow()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should handle multiple requests applying without exceeding max concurrency (different weights)', async () => {
|
|
54
|
+
const maxConcurrency = 10
|
|
55
|
+
let counter = 0
|
|
56
|
+
const requests = Array.from({ length: 10 }, (_, i) => async () => {
|
|
57
|
+
if (counter > maxConcurrency) throw new Error('Too many requests')
|
|
58
|
+
counter += i
|
|
59
|
+
await mockRequest(counter)
|
|
60
|
+
counter -= i
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const controller = weightedRequestConcurrencyController(maxConcurrency)
|
|
64
|
+
await expect(
|
|
65
|
+
Promise.all(requests.map((request, i) => controller(request, i))),
|
|
66
|
+
).resolves.not.toThrow()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should handle light weight requests', async () => {
|
|
70
|
+
const maxConcurrency = 4
|
|
71
|
+
const weights = [3, 3, 3, 1]
|
|
72
|
+
const requests = weights.map(() => mockTimestampedRequest)
|
|
73
|
+
|
|
74
|
+
const controller = weightedRequestConcurrencyController(maxConcurrency)
|
|
75
|
+
const results = await Promise.all(requests.map((request, i) => controller(request, weights[i])))
|
|
76
|
+
|
|
77
|
+
expect(results[0]).toBeLessThanOrEqual(results[3])
|
|
78
|
+
expect(results[3]).toBeLessThanOrEqual(results[1])
|
|
79
|
+
expect(results[1]).toBeLessThanOrEqual(results[2])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should respect order of execution (using ensureOrder flag)', async () => {
|
|
83
|
+
const maxConcurrency = 4
|
|
84
|
+
const weights = [3, 3, 3, 1]
|
|
85
|
+
const requests = weights.map(() => mockTimestampedRequest)
|
|
86
|
+
|
|
87
|
+
const controller = weightedRequestConcurrencyController(maxConcurrency, true)
|
|
88
|
+
const results = await Promise.all(requests.map((request, i) => controller(request, weights[i])))
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < results.length - 1; i++) {
|
|
91
|
+
expect(results[i]).toBeLessThanOrEqual(results[i + 1])
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
})
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@autonomys/asynchronous",
|
|
3
|
+
"packageManager": "yarn@4.7.0",
|
|
4
|
+
"version": "1.4.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/autonomys/auto-sdk"
|
|
9
|
+
},
|
|
10
|
+
"author": {
|
|
11
|
+
"name": "Autonomys",
|
|
12
|
+
"url": "https://www.autonomys.xyz"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/autonomys/auto-sdk/issues"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "yarn tsc",
|
|
19
|
+
"test": "jest"
|
|
20
|
+
},
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/jest": "^29.5.12",
|
|
25
|
+
"eslint": "^9.21.0",
|
|
26
|
+
"interface-store": "^6.0.2",
|
|
27
|
+
"jest": "^29.7.0",
|
|
28
|
+
"prettier": "^3.2.5",
|
|
29
|
+
"ts-jest": "^29.1.4",
|
|
30
|
+
"typescript": "^5.4.5"
|
|
31
|
+
},
|
|
32
|
+
"gitHead": "53cf74430ba08ed3b8f38ccc2fcee7b373a7934c"
|
|
33
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { PassThrough, Readable } from 'stream'
|
|
2
|
+
|
|
3
|
+
export type AwaitIterable<T> = AsyncIterable<T> | Iterable<T>
|
|
4
|
+
|
|
5
|
+
export const asyncIterableMap = async <T, R>(
|
|
6
|
+
iterable: AwaitIterable<T>,
|
|
7
|
+
fn: (value: T) => Promise<R>,
|
|
8
|
+
): Promise<R[]> => {
|
|
9
|
+
const result = []
|
|
10
|
+
for await (const value of iterable) {
|
|
11
|
+
result.push(await fn(value))
|
|
12
|
+
}
|
|
13
|
+
return result
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const asyncIterableForEach = async <T>(
|
|
17
|
+
iterable: AwaitIterable<T>,
|
|
18
|
+
fn: (value: T[]) => Promise<void>,
|
|
19
|
+
concurrency: number,
|
|
20
|
+
): Promise<void> => {
|
|
21
|
+
let batch: T[] = []
|
|
22
|
+
for await (const value of iterable) {
|
|
23
|
+
batch.push(value)
|
|
24
|
+
if (batch.length === concurrency) {
|
|
25
|
+
await fn(batch)
|
|
26
|
+
batch = []
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (batch.length > 0) {
|
|
31
|
+
await fn(batch)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const asyncIterableToPromiseOfArray = async <T>(
|
|
36
|
+
iterable: AwaitIterable<T>,
|
|
37
|
+
): Promise<T[]> => {
|
|
38
|
+
const result = []
|
|
39
|
+
for await (const value of iterable) {
|
|
40
|
+
result.push(value)
|
|
41
|
+
}
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const bufferToAsyncIterable = (buffer: Buffer): AsyncIterable<Buffer> => {
|
|
46
|
+
return (async function* () {
|
|
47
|
+
yield buffer
|
|
48
|
+
})()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const asyncIterableToBuffer = async (iterable: AwaitIterable<Buffer>): Promise<Buffer> => {
|
|
52
|
+
let buffer = Buffer.alloc(0)
|
|
53
|
+
for await (const chunk of iterable) {
|
|
54
|
+
buffer = Buffer.concat([buffer, chunk])
|
|
55
|
+
}
|
|
56
|
+
return buffer
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const asyncByChunk = async function* (
|
|
60
|
+
iterable: AwaitIterable<Buffer>,
|
|
61
|
+
chunkSize: number,
|
|
62
|
+
ignoreLastChunk: boolean = false,
|
|
63
|
+
): AsyncIterable<Buffer> {
|
|
64
|
+
let accumulated = Buffer.alloc(0)
|
|
65
|
+
for await (const chunk of iterable) {
|
|
66
|
+
accumulated = Buffer.concat([accumulated, chunk])
|
|
67
|
+
while (accumulated.length >= chunkSize) {
|
|
68
|
+
yield accumulated.subarray(0, chunkSize)
|
|
69
|
+
accumulated = accumulated.subarray(chunkSize)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (accumulated.length > 0 && !ignoreLastChunk) {
|
|
74
|
+
yield accumulated
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function forkAsyncIterable(
|
|
79
|
+
asyncIterable: AwaitIterable<Uint8Array>,
|
|
80
|
+
): Promise<[Readable, Readable]> {
|
|
81
|
+
const passThrough1 = new PassThrough()
|
|
82
|
+
const passThrough2 = new PassThrough()
|
|
83
|
+
|
|
84
|
+
for await (const chunk of asyncIterable) {
|
|
85
|
+
passThrough1.write(chunk)
|
|
86
|
+
passThrough2.write(chunk)
|
|
87
|
+
}
|
|
88
|
+
passThrough1.end()
|
|
89
|
+
passThrough2.end()
|
|
90
|
+
|
|
91
|
+
return [passThrough1, passThrough2]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const asyncFromStream = async function* (
|
|
95
|
+
stream: ReadableStream<Uint8Array>,
|
|
96
|
+
): AsyncIterable<Buffer> {
|
|
97
|
+
const reader = stream.getReader()
|
|
98
|
+
let result = await reader.read()
|
|
99
|
+
while (!result.done) {
|
|
100
|
+
yield Buffer.from(result.value)
|
|
101
|
+
result = await reader.read()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const bufferToIterable = async function* (buffer: Buffer): AsyncIterable<Buffer> {
|
|
106
|
+
yield buffer
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const fileToIterable = async function* (
|
|
110
|
+
file: File | Blob,
|
|
111
|
+
chunkSize: number = 1024 * 1024,
|
|
112
|
+
): AsyncIterable<Buffer> {
|
|
113
|
+
for (let i = 0; i < file.size; i += chunkSize) {
|
|
114
|
+
yield Buffer.from(await file.slice(i, i + chunkSize).arrayBuffer())
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './weightedRequestsConcurrencyController.js'
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type Job<T> = () => Promise<T>
|
|
2
|
+
|
|
3
|
+
type ConcurrencyController = <T>(job: Job<T>, concurrency: number) => Promise<T>
|
|
4
|
+
|
|
5
|
+
interface ScheduleTask<T> {
|
|
6
|
+
job: Job<T>
|
|
7
|
+
concurrency: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const weightedRequestConcurrencyController = (
|
|
11
|
+
maxConcurrency: number,
|
|
12
|
+
ensureOrder: boolean = false,
|
|
13
|
+
): ConcurrencyController => {
|
|
14
|
+
if (maxConcurrency <= 0) {
|
|
15
|
+
throw new Error('Max concurrency must be greater than 0')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
const queue: ScheduleTask<any>[] = []
|
|
20
|
+
let active = 0
|
|
21
|
+
|
|
22
|
+
const enqueue = <T>(task: Job<T>, concurrency: number): Promise<T> => {
|
|
23
|
+
return new Promise<T>((resolve, reject) => {
|
|
24
|
+
queue.push({
|
|
25
|
+
job: () => task().then(resolve).catch(reject),
|
|
26
|
+
concurrency,
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const findExecutableTask = () => {
|
|
32
|
+
const index = queue.findIndex((e) => active + e.concurrency <= maxConcurrency)
|
|
33
|
+
const notMatched = index === -1
|
|
34
|
+
const notFirst = ensureOrder && index !== 0
|
|
35
|
+
if (notMatched || notFirst) {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return queue.splice(index, 1).at(0)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const manageJobFinalization = async (concurrency: number) => {
|
|
43
|
+
active -= concurrency
|
|
44
|
+
const executableTask = findExecutableTask()
|
|
45
|
+
if (executableTask) {
|
|
46
|
+
setImmediate(() => internalRunJob(executableTask.job, executableTask.concurrency))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const internalRunJob = async <T>(job: Job<T>, concurrency: number): Promise<T> => {
|
|
51
|
+
active += concurrency
|
|
52
|
+
return job().finally(() => manageJobFinalization(concurrency))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const runJob = async <T>(job: Job<T>, concurrency: number): Promise<T> => {
|
|
56
|
+
const exceededMaxConcurrency = active + concurrency > maxConcurrency
|
|
57
|
+
const shouldRespectOrder = ensureOrder && queue.length > 0
|
|
58
|
+
if (exceededMaxConcurrency || shouldRespectOrder) {
|
|
59
|
+
return enqueue(job, concurrency)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return internalRunJob(job, concurrency)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return runJob
|
|
66
|
+
}
|
package/src/index.ts
ADDED