@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 ADDED
@@ -0,0 +1,5 @@
1
+ # @autonomys/asynchronous
2
+
3
+ ## Description
4
+
5
+ A toolbox library for managing asyncronous problems such as concurrency or async generator functions.
@@ -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
@@ -0,0 +1,5 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'node',
5
+ };
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
@@ -0,0 +1,2 @@
1
+ export * from './asyncGenerators'
2
+ export * from './concurrency'
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "module": "Node16",
7
+ "moduleResolution": "Node16",
8
+ "resolveJsonModule": true
9
+ },
10
+ "include": ["src"]
11
+ }