codeweaver 2.3.1 → 3.0.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 +7 -38
- package/command.js +57 -51
- package/package.json +9 -19
- package/src/routers/orders/dto/order.dto.ts +6 -0
- package/src/routers/orders/index.router.ts +4 -4
- package/src/routers/orders/order.controller.ts +15 -17
- package/src/routers/products/dto/product.dto.ts +17 -9
- package/src/routers/products/index.router.ts +4 -4
- package/src/routers/products/product.controller.ts +21 -34
- package/src/routers/users/user.controller.ts +9 -12
- package/src/utilities/assignment.ts +48 -0
- package/src/utilities/conversion.ts +38 -31
- package/src/utilities/error-handling.ts +45 -30
- package/src/utilities/parallel/chanel.ts +142 -0
- package/src/utilities/parallel/parallel.ts +135 -0
- package/src/utilities/parallel/worker-pool.ts +99 -0
- package/src/utilities/assign.ts +0 -66
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ResponseError } from "./error-handling";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Strictly assign obj (type T1) to T2 using a Zod schema.
|
|
6
|
+
*
|
|
7
|
+
* - Extras in source are ignored.
|
|
8
|
+
* - Validates fields with the schema; on failure, throws with a descriptive message.
|
|
9
|
+
* - Returns an object typed as T2 (inferred from the schema).
|
|
10
|
+
*
|
|
11
|
+
* @param source - Source object of type T1
|
|
12
|
+
* @param destination - Destination object to be populated (typed as T2)
|
|
13
|
+
* @param schema - Zod schema describing the target type T2
|
|
14
|
+
* @returns T2 representing the destination after assignment
|
|
15
|
+
*/
|
|
16
|
+
export default async function assign<T1 extends object, T2 extends object>(
|
|
17
|
+
source: T1,
|
|
18
|
+
destination: T2,
|
|
19
|
+
schema?: z.ZodObject<any>
|
|
20
|
+
): Promise<T2> {
|
|
21
|
+
let keys = Object.keys(schema?.shape ?? destination);
|
|
22
|
+
|
|
23
|
+
// Iterate schema keys
|
|
24
|
+
await Promise.all(
|
|
25
|
+
keys.map(async (key) => {
|
|
26
|
+
if (source.hasOwnProperty(key)) {
|
|
27
|
+
(destination as any)[key] = (source as any)[key];
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (schema != null) {
|
|
33
|
+
// Validate using the schema on the subset (this will also coerce if the schema has transforms)
|
|
34
|
+
const parseResult = await schema.safeParseAsync(destination);
|
|
35
|
+
if (parseResult.success == false) {
|
|
36
|
+
// Build a descriptive error message from the first issue
|
|
37
|
+
const issue = parseResult.error.issues?.[0];
|
|
38
|
+
const path = issue?.path?.length ? issue.path.join(".") : "value";
|
|
39
|
+
const message = issue?.message ?? "Schema validation failed";
|
|
40
|
+
throw new ResponseError(
|
|
41
|
+
`Validation failed for "${path}": ${message}`,
|
|
42
|
+
500
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return destination;
|
|
48
|
+
}
|
|
@@ -109,50 +109,57 @@ export function stringToNumber(input: string): number {
|
|
|
109
109
|
* - Validates fields with the schema; on failure, throws with a descriptive message.
|
|
110
110
|
* - Returns an object typed as T2 (inferred from the schema).
|
|
111
111
|
*
|
|
112
|
-
* @param
|
|
112
|
+
* @param data - Source object of type T1
|
|
113
113
|
* @param schema - Zod schema describing the target type T2
|
|
114
114
|
* @returns T2 inferred from the provided schema
|
|
115
115
|
*/
|
|
116
|
-
export function convert<T1 extends object, T2 extends object>(
|
|
117
|
-
|
|
118
|
-
schema: z.ZodObject<any
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
export async function convert<T1 extends object, T2 extends object>(
|
|
117
|
+
data: T1,
|
|
118
|
+
schema: z.ZodObject<any>,
|
|
119
|
+
ignoreValidation: boolean = false
|
|
120
|
+
): Promise<T2> {
|
|
121
|
+
// Derive the runtime keys from the schema's shape
|
|
121
122
|
const shape = (schema as any)._def?.shape as ZodRawShape | undefined;
|
|
122
123
|
if (!shape) {
|
|
123
|
-
throw new ResponseError(
|
|
124
|
-
"convertStrictlyFromSchema: provided schema has no shape.",
|
|
125
|
-
500
|
|
126
|
-
);
|
|
124
|
+
throw new ResponseError("Provided schema has no shape.", 500);
|
|
127
125
|
}
|
|
128
126
|
|
|
129
127
|
const keysSchema = Object.keys(shape) as Array<keyof any>;
|
|
130
128
|
|
|
131
|
-
//
|
|
129
|
+
// Build a plain object to pass through Zod for validation
|
|
132
130
|
// Include only keys that exist on the schema (ignore extras in obj)
|
|
133
131
|
const candidate: any = {};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
|
|
133
|
+
// Iterate schema keys
|
|
134
|
+
await Promise.all(
|
|
135
|
+
keysSchema.map(async (key) => {
|
|
136
|
+
if ((data as any).hasOwnProperty(key)) {
|
|
137
|
+
candidate[key] = (data as any)[key];
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Validate against the schema
|
|
143
|
+
if (ignoreValidation) {
|
|
144
|
+
const result = await schema.safeParseAsync(candidate);
|
|
145
|
+
if (result.success == false) {
|
|
146
|
+
// Modern, non-format error reporting
|
|
147
|
+
const issues = result.error.issues.map((i) => ({
|
|
148
|
+
path: i.path, // where the issue occurred
|
|
149
|
+
message: i.message, // human-friendly message
|
|
150
|
+
code: i.code, // e.g., "too_small", "invalid_type"
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
// You can log issues or throw a structured error
|
|
154
|
+
throw new ResponseError(
|
|
155
|
+
`Validation failed: ${JSON.stringify(issues)}`,
|
|
156
|
+
500
|
|
157
|
+
);
|
|
137
158
|
}
|
|
138
|
-
}
|
|
139
159
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (!result.success) {
|
|
143
|
-
// Modern, non-format error reporting
|
|
144
|
-
const issues = result.error.issues.map((i) => ({
|
|
145
|
-
path: i.path, // where the issue occurred
|
|
146
|
-
message: i.message, // human-friendly message
|
|
147
|
-
code: i.code, // e.g., "too_small", "invalid_type"
|
|
148
|
-
}));
|
|
149
|
-
// You can log issues or throw a structured error
|
|
150
|
-
throw new ResponseError(
|
|
151
|
-
`convertStrictlyFromSchema: validation failed: ${JSON.stringify(issues)}`,
|
|
152
|
-
500
|
|
153
|
-
);
|
|
160
|
+
// Return the validated data typed as T2
|
|
161
|
+
return result.data as T2;
|
|
154
162
|
}
|
|
155
163
|
|
|
156
|
-
|
|
157
|
-
return result.data as T2;
|
|
164
|
+
return candidate as T2;
|
|
158
165
|
}
|
|
@@ -64,10 +64,10 @@ export function sendHttpError(res: Response, error: ResponseError): void {
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* A generic alias representing a tuple of [result, error].
|
|
67
|
-
* - result is either T or
|
|
68
|
-
* - error is either a ResponseError or
|
|
67
|
+
* - result is either T or undefined if an error occurred
|
|
68
|
+
* - error is either a ResponseError or undefined if the operation succeeded
|
|
69
69
|
*/
|
|
70
|
-
export type ReturnInfo<T> = [T |
|
|
70
|
+
export type ReturnInfo<T> = [T | undefined, ResponseError | undefined];
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* A Promise-wrapped version of ReturnInfo.
|
|
@@ -78,25 +78,25 @@ export type AsyncReturnInfo<T> = Promise<ReturnInfo<T>>;
|
|
|
78
78
|
* Executes a function and captures a potential error as a ReturnInfo tuple.
|
|
79
79
|
*
|
|
80
80
|
* Returns a two-element tuple: [value, error]
|
|
81
|
-
* - value: the function result if it succeeds;
|
|
82
|
-
* - error: the caught Error wrapped as a ResponseError (or the provided error) if the function throws;
|
|
81
|
+
* - value: the function result if it succeeds; undefined if an exception is thrown
|
|
82
|
+
* - error: the caught Error wrapped as a ResponseError (or the provided error) if the function throws; undefined if the function succeeds
|
|
83
83
|
*
|
|
84
84
|
* This utility helps avoid try/catch blocks at call sites by returning both the
|
|
85
85
|
* result and any error in a single value.
|
|
86
86
|
*
|
|
87
87
|
* @template T
|
|
88
88
|
* @param func - The function to execute
|
|
89
|
-
* @param error - The error object to return when an exception occurs (typically a ResponseError). If no error is provided,
|
|
90
|
-
* @returns ReturnInfo<T> A tuple: [value or
|
|
89
|
+
* @param error - The error object to return when an exception occurs (typically a ResponseError). If no error is provided, undefined is used.
|
|
90
|
+
* @returns ReturnInfo<T> A tuple: [value or undefined, error or undefined]
|
|
91
91
|
*/
|
|
92
92
|
export function invoke<T>(
|
|
93
93
|
func: () => T,
|
|
94
|
-
error: ResponseError |
|
|
94
|
+
error: ResponseError | undefined
|
|
95
95
|
): ReturnInfo<T> {
|
|
96
96
|
try {
|
|
97
|
-
return [func(),
|
|
97
|
+
return [func(), undefined];
|
|
98
98
|
} catch {
|
|
99
|
-
return [
|
|
99
|
+
return [undefined, error];
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -104,14 +104,14 @@ export function invoke<T>(
|
|
|
104
104
|
* Creates a successful result from a ReturnInfo tuple.
|
|
105
105
|
*
|
|
106
106
|
* Given a ReturnInfo<T> of the form [value, error], this returns the value
|
|
107
|
-
* when the operation succeeded, or
|
|
107
|
+
* when the operation succeeded, or undefined when there was an error.
|
|
108
108
|
*
|
|
109
109
|
* @template T
|
|
110
110
|
* @param input - The ReturnInfo tuple
|
|
111
|
-
* @returns The successful value of type T, or
|
|
111
|
+
* @returns The successful value of type T, or undefined if there was an error
|
|
112
112
|
*/
|
|
113
|
-
export function successfulResult<T>(
|
|
114
|
-
return
|
|
113
|
+
export function successfulResult<T>(result: ReturnInfo<T>): T | undefined {
|
|
114
|
+
return result[0];
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
@@ -122,24 +122,24 @@ export function successfulResult<T>(input: ReturnInfo<T>): T | null {
|
|
|
122
122
|
* If the error is already a ResponseError, it is returned as-is.
|
|
123
123
|
*
|
|
124
124
|
* @template T
|
|
125
|
-
* @param
|
|
126
|
-
* @returns The extracted or wrapped ResponseError, or
|
|
125
|
+
* @param result - The error to wrap, either as a ResponseError or as a ReturnInfo<T> where the error is at index 1
|
|
126
|
+
* @returns The extracted or wrapped ResponseError, or undefined if there is no error
|
|
127
127
|
*/
|
|
128
|
-
export function error<T>(
|
|
129
|
-
return
|
|
128
|
+
export function error<T>(result: ReturnInfo<T>): ResponseError | undefined {
|
|
129
|
+
return result[1];
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* Determines whether a ReturnInfo value represents a successful operation.
|
|
134
134
|
*
|
|
135
|
-
* A result is considered successful when there is no error (i.e., the error portion is
|
|
135
|
+
* A result is considered successful when there is no error (i.e., the error portion is undefined).
|
|
136
136
|
*
|
|
137
137
|
* @template T
|
|
138
|
-
* @param result - The ReturnInfo tuple [value |
|
|
138
|
+
* @param result - The ReturnInfo tuple [value | undefined, error | undefined]
|
|
139
139
|
* @returns true if there is no error; false otherwise
|
|
140
140
|
*/
|
|
141
141
|
export function isSuccessful<T>(result: ReturnInfo<T>): boolean {
|
|
142
|
-
return result[1] ===
|
|
142
|
+
return result[1] === undefined;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/**
|
|
@@ -148,11 +148,11 @@ export function isSuccessful<T>(result: ReturnInfo<T>): boolean {
|
|
|
148
148
|
* This is the logical negation of isSuccess for a given ReturnInfo.
|
|
149
149
|
*
|
|
150
150
|
* @template T
|
|
151
|
-
* @param result - The ReturnInfo tuple [value |
|
|
152
|
-
* @returns true if an error is present (i.e., error is not
|
|
151
|
+
* @param result - The ReturnInfo tuple [value | undefined, error | undefined]
|
|
152
|
+
* @returns true if an error is present (i.e., error is not undefined); false otherwise
|
|
153
153
|
*/
|
|
154
154
|
export function hasError<T>(result: ReturnInfo<T>): boolean {
|
|
155
|
-
return result[1] !==
|
|
155
|
+
return result[1] !== undefined;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/**
|
|
@@ -161,12 +161,12 @@ export function hasError<T>(result: ReturnInfo<T>): boolean {
|
|
|
161
161
|
* This is the logical negation of isSuccess for a given ReturnInfo.
|
|
162
162
|
*
|
|
163
163
|
* @template T
|
|
164
|
-
* @param result - The ReturnInfo tuple [value |
|
|
165
|
-
* @returns true if an error is present (i.e., error is not
|
|
164
|
+
* @param result - The ReturnInfo tuple [value | undefined, error | undefined]
|
|
165
|
+
* @returns true if an error is present (i.e., error is not undefined); false otherwise
|
|
166
166
|
*/
|
|
167
167
|
export function then<T>(
|
|
168
168
|
result: ReturnInfo<T>,
|
|
169
|
-
callback: (
|
|
169
|
+
callback: (value: T) => void
|
|
170
170
|
): void {
|
|
171
171
|
if (isSuccessful(result)) {
|
|
172
172
|
callback(successfulResult(result)!);
|
|
@@ -179,10 +179,10 @@ export function then<T>(
|
|
|
179
179
|
* This is the logical negation of isSuccess for a given ReturnInfo.
|
|
180
180
|
*
|
|
181
181
|
* @template T
|
|
182
|
-
* @param result - The ReturnInfo tuple [value |
|
|
183
|
-
* @returns true if an error is present (i.e., error is not
|
|
182
|
+
* @param result - The ReturnInfo tuple [value | undefined, error | undefined]
|
|
183
|
+
* @returns true if an error is present (i.e., error is not undefined); false otherwise
|
|
184
184
|
*/
|
|
185
|
-
export function
|
|
185
|
+
export function catchIfHasError<T>(
|
|
186
186
|
result: ReturnInfo<T>,
|
|
187
187
|
callback: (error: ResponseError) => void
|
|
188
188
|
): void {
|
|
@@ -190,3 +190,18 @@ export function catchError<T>(
|
|
|
190
190
|
callback(error(result)!);
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Indicates whether a ReturnInfo value represents an error.
|
|
196
|
+
*
|
|
197
|
+
* This is the logical negation of isSuccess for a given ReturnInfo.
|
|
198
|
+
*
|
|
199
|
+
* @template T
|
|
200
|
+
* @param result - The ReturnInfo tuple [value | undefined, error | undefined]
|
|
201
|
+
* @returns true if an error is present (i.e., error is not undefined); false otherwise
|
|
202
|
+
*/
|
|
203
|
+
export function throwIfHasError<T>(result: ReturnInfo<T>): void {
|
|
204
|
+
if (hasError(result)) {
|
|
205
|
+
throw error(result);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { ResponseError } from "../error-handling";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Event callback type for channel send event.
|
|
5
|
+
* @template T - Type of the value sent through the channel.
|
|
6
|
+
* @param {T} value - The value sent to the channel.
|
|
7
|
+
*/
|
|
8
|
+
export type EventCallback<T> = (value: T) => void;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Channel class inspired by Golang channels.
|
|
12
|
+
* Supports asynchronous send and receive operations,
|
|
13
|
+
* optional event callback on send, and closing semantics.
|
|
14
|
+
*
|
|
15
|
+
* @template T - Type of the values sent through the channel.
|
|
16
|
+
*/
|
|
17
|
+
export class Channel<T> {
|
|
18
|
+
private queue: T[] = [];
|
|
19
|
+
private receivers: ((value: T) => void)[] = [];
|
|
20
|
+
private closed = false;
|
|
21
|
+
private eventCb?: EventCallback<T>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new Channel.
|
|
25
|
+
* @param {EventCallback<T>} [eventCb] - Optional callback triggered after each send.
|
|
26
|
+
*/
|
|
27
|
+
public constructor(eventCb?: EventCallback<T>) {
|
|
28
|
+
this.eventCb = eventCb;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sends a value to the channel.
|
|
33
|
+
* If there are waiting receivers, delivers immediately.
|
|
34
|
+
* Otherwise, buffers the value.
|
|
35
|
+
* Throws if channel is closed.
|
|
36
|
+
* @param {T} value - Value to send.
|
|
37
|
+
* @returns {Promise<void>} Promise resolved when the send completes.
|
|
38
|
+
*/
|
|
39
|
+
public async send(value: T): Promise<void> {
|
|
40
|
+
if (this.closed) {
|
|
41
|
+
throw new ResponseError("Channel is closed", 500);
|
|
42
|
+
}
|
|
43
|
+
if (this.receivers.length > 0) {
|
|
44
|
+
const receiver = this.receivers.shift()!;
|
|
45
|
+
receiver(value);
|
|
46
|
+
} else {
|
|
47
|
+
this.queue.push(value);
|
|
48
|
+
}
|
|
49
|
+
if (this.eventCb) {
|
|
50
|
+
this.eventCb(value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Receives a value from the channel.
|
|
56
|
+
* If no buffered value, waits until one is sent.
|
|
57
|
+
* Throws if channel is closed and no buffered values remain.
|
|
58
|
+
* @returns {Promise<T>} Promise resolved with the next value.
|
|
59
|
+
*/
|
|
60
|
+
public async receive(): Promise<T> {
|
|
61
|
+
if (this.queue.length > 0) {
|
|
62
|
+
return this.queue.shift()!;
|
|
63
|
+
}
|
|
64
|
+
if (this.closed) {
|
|
65
|
+
throw new ResponseError("Channel is closed", 500);
|
|
66
|
+
}
|
|
67
|
+
return await new Promise<T>((resolve) => {
|
|
68
|
+
this.receivers.push(resolve);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Closes the channel.
|
|
74
|
+
* Further sends will throw.
|
|
75
|
+
* Pending receivers receive undefined.
|
|
76
|
+
*/
|
|
77
|
+
public close() {
|
|
78
|
+
this.closed = true;
|
|
79
|
+
while (this.receivers.length > 0) {
|
|
80
|
+
const receiver = this.receivers.shift()!;
|
|
81
|
+
receiver(undefined!); // Indicate closed channel to receivers
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Mutex class inspired by Golang sync.Mutex using Promise chaining.
|
|
88
|
+
* Supports lock, scoped locking with ulock, and tryLock with timeout.
|
|
89
|
+
*/
|
|
90
|
+
export class Mutex {
|
|
91
|
+
private mutex = Promise.resolve();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Locks the mutex asynchronously.
|
|
95
|
+
* Returns a release function to unlock.
|
|
96
|
+
* @returns {Promise<() => void>} Promise resolved with the unlock function.
|
|
97
|
+
*/
|
|
98
|
+
public async lock(): Promise<() => void> {
|
|
99
|
+
let release: () => void;
|
|
100
|
+
const lockPromise = new Promise<void>((res) => (release = res));
|
|
101
|
+
const oldMutex = this.mutex;
|
|
102
|
+
this.mutex = oldMutex.then(() => lockPromise);
|
|
103
|
+
await oldMutex;
|
|
104
|
+
return release!;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Executes a given task under mutex lock.
|
|
109
|
+
* Ensures the mutex is released even if task throws.
|
|
110
|
+
* @param {() => Promise<void> | void} task - Task to execute exclusively.
|
|
111
|
+
*/
|
|
112
|
+
public async unlock(task: () => Promise<void> | void): Promise<void> {
|
|
113
|
+
const release = await this.lock();
|
|
114
|
+
try {
|
|
115
|
+
await task();
|
|
116
|
+
} finally {
|
|
117
|
+
release();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Attempts to acquire the lock with a timeout.
|
|
123
|
+
* If timeout expires, returns null.
|
|
124
|
+
* Otherwise returns the unlock function.
|
|
125
|
+
* @param {number} timeoutMs - Timeout in milliseconds.
|
|
126
|
+
* @returns {Promise<(() => void) | null>} Unlock function or null on timeout.
|
|
127
|
+
*/
|
|
128
|
+
public async tryLock(timeoutMs: number): Promise<(() => void) | null> {
|
|
129
|
+
let timer: NodeJS.Timeout;
|
|
130
|
+
const timedOut = new Promise<null>((resolve) => {
|
|
131
|
+
timer = setTimeout(() => resolve(null), timeoutMs);
|
|
132
|
+
});
|
|
133
|
+
const lockPromise = this.lock();
|
|
134
|
+
|
|
135
|
+
const result = await Promise.race([lockPromise, timedOut]);
|
|
136
|
+
if (result === null) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
clearTimeout(timer!);
|
|
140
|
+
return result as () => void;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { ResponseError } from "../error-handling";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { WorkerPool } from "./worker-pool";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Executes multiple asynchronous or synchronous tasks in parallel and returns their results as an array.
|
|
7
|
+
*
|
|
8
|
+
* @template T The type of the result returned by each task.
|
|
9
|
+
* @param {(() => T)[]} tasks - An array of functions, where each function returns a value or a promise.
|
|
10
|
+
* @returns {Promise<T[]>} A promise that resolves to an array containing the resolved results of all tasks, preserving order.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const results = await parallel(
|
|
14
|
+
* () => fetchUser(1),
|
|
15
|
+
* () => fetchUser(2),
|
|
16
|
+
* () => fetchUser(3)
|
|
17
|
+
* );
|
|
18
|
+
* console.log(results); // [user1, user2, user3]
|
|
19
|
+
*/
|
|
20
|
+
export async function parallel<T>(...tasks: (() => T)[]): Promise<T[]> {
|
|
21
|
+
return await Promise.all(tasks.map(async (task) => task()));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Simple parallel mapper.
|
|
26
|
+
* Executes the provided async mapper function on each item in parallel
|
|
27
|
+
* and collects the results in the same order as the input.
|
|
28
|
+
*
|
|
29
|
+
* @template T - Type of input items
|
|
30
|
+
* @template U - Type of mapped results
|
|
31
|
+
* @param items - Array of items to process
|
|
32
|
+
* @param mapper - Async function that maps a T to a U (or Promise<U>)
|
|
33
|
+
* @returns {Promise<U[]>} - Resolves to an array of mapped results in input order
|
|
34
|
+
*/
|
|
35
|
+
export async function parallelMap<T, U>(
|
|
36
|
+
items: T[],
|
|
37
|
+
mapper: (item: T, index?: number, array?: T[]) => Promise<U> | U
|
|
38
|
+
): Promise<U[]> {
|
|
39
|
+
// Map each item to its mapped Promise and await all of them in parallel
|
|
40
|
+
return await Promise.all(
|
|
41
|
+
items.map(async (item, index, array) => await mapper(item, index, array))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Concurrency-limited parallel mapper.
|
|
47
|
+
* Processes items with a configurable maximum number of concurrent operations.
|
|
48
|
+
* If concurrency is Infinity or not finite, falls back to unbounded Promise.all.
|
|
49
|
+
*
|
|
50
|
+
* @template T - Type of input items
|
|
51
|
+
* @template U - Type of mapped results
|
|
52
|
+
* @param items - Array of items to process
|
|
53
|
+
* @param mapper - Async function that maps a T to a U (or Promise<U>)
|
|
54
|
+
* @param concurrencyLevel - number | Infinity - Max concurrent operations (default: Infinity)
|
|
55
|
+
* @returns {Promise<U[]>} - Resolves to an array of mapped results in input order
|
|
56
|
+
*/
|
|
57
|
+
export async function parallelMapWithConcurrencyLevel<T, U>(
|
|
58
|
+
items: T[],
|
|
59
|
+
mapper: (item: T, index?: number, array?: T[]) => Promise<U> | U,
|
|
60
|
+
concurrencyLevel: number = Infinity
|
|
61
|
+
): Promise<U[]> {
|
|
62
|
+
if (!Array.isArray(items)) {
|
|
63
|
+
throw new ResponseError("Items must be an array", 400);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (concurrencyLevel <= 0) {
|
|
67
|
+
throw new ResponseError("Concurrency must be greater than 0", 500);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// If concurrency is not finite, use the simple Promise.all approach
|
|
71
|
+
if (!isFinite(concurrencyLevel)) {
|
|
72
|
+
return await Promise.all(
|
|
73
|
+
items.map((item) => Promise.resolve(mapper(item)))
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const results: U[] = new Array(items.length);
|
|
78
|
+
let i = 0;
|
|
79
|
+
|
|
80
|
+
// Create a fixed number of worker promises that pull from the shared index
|
|
81
|
+
const workers = Array.from({ length: concurrencyLevel }, async () => {
|
|
82
|
+
while (i < items.length) {
|
|
83
|
+
const idx = i++;
|
|
84
|
+
const item = items[idx];
|
|
85
|
+
// Store the result in the corresponding position to preserve order
|
|
86
|
+
results[idx] = await mapper(item, idx, items);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await Promise.all(workers);
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parallel CPU-bound mapper using a fixed-size worker pool.
|
|
96
|
+
* - Distributes items to workers and collects results in input order.
|
|
97
|
+
* - Each worker runs a fixed performMapping function defined inside the worker.
|
|
98
|
+
*
|
|
99
|
+
* Important: The mapper logic inside the worker is fixed. If you need custom per-item logic,
|
|
100
|
+
* you should modify the worker to import your actual function or adapt to serialize logic.
|
|
101
|
+
*
|
|
102
|
+
* @template T - Input item type
|
|
103
|
+
* @template R - Result type
|
|
104
|
+
* @param items - Array of items to process
|
|
105
|
+
* @param options - Concurrency options (default uses number of CPU cores)
|
|
106
|
+
* @returns {Promise<R[]>} - Results in the same order as input
|
|
107
|
+
*/
|
|
108
|
+
export async function parallelCpuMap<T, R>(
|
|
109
|
+
items: T[],
|
|
110
|
+
mapperWorkerFilePath: string,
|
|
111
|
+
concurrencyLevel: number = require("os").cpus().length || 1
|
|
112
|
+
): Promise<R[]> {
|
|
113
|
+
if (!Array.isArray(items)) {
|
|
114
|
+
throw new TypeError("items must be an array");
|
|
115
|
+
}
|
|
116
|
+
if (concurrencyLevel <= 0) {
|
|
117
|
+
throw new Error("concurrency must be greater than 0");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Worker pool setup
|
|
121
|
+
const workerPath = path.resolve(__dirname, mapperWorkerFilePath);
|
|
122
|
+
|
|
123
|
+
// Instantiate a concrete pool
|
|
124
|
+
const workerPool = new WorkerPool<T, R>(workerPath, concurrencyLevel);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Dispatch all items and collect results in order
|
|
128
|
+
const results = await workerPool.mapAll(items);
|
|
129
|
+
await workerPool.close();
|
|
130
|
+
return results;
|
|
131
|
+
} catch (err) {
|
|
132
|
+
await workerPool.close();
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Worker } from "worker_threads";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import { ResponseError } from "../error-handling";
|
|
4
|
+
|
|
5
|
+
export type Task<T> = { id: number; payload: T };
|
|
6
|
+
export type Result<R> = { id: number; result?: R; error?: string };
|
|
7
|
+
|
|
8
|
+
export class WorkerPool<T, R> {
|
|
9
|
+
private workers: Worker[] = [];
|
|
10
|
+
private poolSize: number;
|
|
11
|
+
private nextId = 0;
|
|
12
|
+
|
|
13
|
+
// Map task id -> { resolve, reject }
|
|
14
|
+
private pending = new Map<
|
|
15
|
+
number,
|
|
16
|
+
{ resolve: (r: R) => void; reject: (e: any) => void }
|
|
17
|
+
>();
|
|
18
|
+
|
|
19
|
+
constructor(workerPath: string, poolSize?: number) {
|
|
20
|
+
const cores = os.cpus().length || 1;
|
|
21
|
+
this.poolSize = poolSize && poolSize > 0 ? poolSize : cores;
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < this.poolSize; i++) {
|
|
24
|
+
const w = new Worker(workerPath);
|
|
25
|
+
w.on("message", (msg: Result<R>) => this.handleResult(msg));
|
|
26
|
+
w.on("error", (err) => this.handleError(err, i));
|
|
27
|
+
w.on("exit", (code) => {
|
|
28
|
+
if (code !== 0) {
|
|
29
|
+
// Notify all pending promises about the exit
|
|
30
|
+
for (const [, p] of this.pending) {
|
|
31
|
+
p.reject(
|
|
32
|
+
new ResponseError(`Worker ${i} exited with code ${code}`, 500)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
this.pending.clear();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
this.workers.push(w);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private handleResult(msg: Result<R>) {
|
|
43
|
+
const { id, result, error } = msg;
|
|
44
|
+
const entry = this.pending.get(id);
|
|
45
|
+
if (!entry) return;
|
|
46
|
+
this.pending.delete(id);
|
|
47
|
+
if (error) {
|
|
48
|
+
entry.reject(new Error(error));
|
|
49
|
+
} else {
|
|
50
|
+
entry.resolve(result as R);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private handleError(err: any, workerIndex: number) {
|
|
55
|
+
// Propagate error to all pending tasks assigned to this worker, if tracked
|
|
56
|
+
// This simple version broadcasts the error to all pending tasks for safety.
|
|
57
|
+
for (const [id, entry] of this.pending) {
|
|
58
|
+
entry.reject(
|
|
59
|
+
new ResponseError(
|
|
60
|
+
`Worker ${workerIndex} error: ${err?.message ?? err}`,
|
|
61
|
+
500
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
this.pending.clear();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Run a single payload through a specific worker
|
|
69
|
+
run(workerIndex: number, payload: T): Promise<R> {
|
|
70
|
+
const id = this.nextId++;
|
|
71
|
+
const worker = this.workers[workerIndex];
|
|
72
|
+
return new Promise<R>((resolve, reject) => {
|
|
73
|
+
this.pending.set(id, { resolve, reject });
|
|
74
|
+
worker.postMessage({ id, payload });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Map all items using a round-robin distribution across workers
|
|
79
|
+
async mapAll(items: T[]): Promise<R[]> {
|
|
80
|
+
if (!Array.isArray(items))
|
|
81
|
+
throw new ResponseError("Items must be an array", 400);
|
|
82
|
+
const results: R[] = new Array(items.length);
|
|
83
|
+
|
|
84
|
+
const workerCount = this.workers.length;
|
|
85
|
+
const tasks: Promise<void>[] = items.map((payload, idx) => {
|
|
86
|
+
const workerIndex = idx % workerCount;
|
|
87
|
+
return this.run(workerIndex, payload).then((r) => {
|
|
88
|
+
results[idx] = r;
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await Promise.all(tasks);
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async close(): Promise<void> {
|
|
97
|
+
await Promise.all(this.workers.map((w) => w.terminate()));
|
|
98
|
+
}
|
|
99
|
+
}
|