serial-task 1.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/LICENSE +21 -0
- package/README.md +355 -0
- package/dist/index.d.ts +184 -0
- package/dist/index.mjs +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 kasukabe tsumugi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Serial Task
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
5
|
+
> Put a list of functions in and get a composed task function. Similar to functional programming's compose (function composition), but with more fine-grained and precise control, and the generated task incurs almost no runtime overhead. ✨
|
|
6
|
+
|
|
7
|
+
**Note**: For async functions, use `createSerialTaskAsync` instead of `createSerialTask`. Both functions have the same API, but `createSerialTaskAsync` properly handles async/await and Promise-based functions.
|
|
8
|
+
|
|
9
|
+
For more awesome packages, check out [my homepage💛](https://baendlorel.github.io/?repoType=npm)
|
|
10
|
+
|
|
11
|
+
## 📦 Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install serial-task
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add serial-task
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 🎯 Quick Start
|
|
22
|
+
|
|
23
|
+
> Note: For async functions(tasks/resultWrapper/conditions), use `createSerialTaskAsync` instead.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { createSerialTask } from 'serial-task';
|
|
27
|
+
|
|
28
|
+
// Create a serial task with multiple functions
|
|
29
|
+
const mathTask = createSerialTask({
|
|
30
|
+
tasks: [
|
|
31
|
+
(x: number) => x + 1, // Step 1: add 1
|
|
32
|
+
(x: number) => x * 2, // Step 2: multiply by 2
|
|
33
|
+
(x: number) => x - 1, // Step 3: subtract 1
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const result = mathTask(5);
|
|
38
|
+
console.log(result.value); // 11 -> ((5 + 1) * 2) - 1 = 11
|
|
39
|
+
console.log(result.results); // [6, 12, 11]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 🔄 Execution Flow
|
|
43
|
+
|
|
44
|
+
The following diagram shows how functions are called in each iteration of the loop:
|
|
45
|
+
|
|
46
|
+
<div style="text-align: center">
|
|
47
|
+
<img src="https://raw.githubusercontent.com/baendlorel/serial-task/main/assets/flow.svg" alt="Execution Flow" width="360" />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
## 📖 API Reference
|
|
51
|
+
|
|
52
|
+
### createSerialTask(options) / createSerialTaskAsync(options)
|
|
53
|
+
|
|
54
|
+
Creates a sync/async serial task function.
|
|
55
|
+
|
|
56
|
+
#### Parameters
|
|
57
|
+
|
|
58
|
+
- **options**: `SerialTaskOptions<F>`
|
|
59
|
+
- **name?**: `string` - Name of the generated task function (default: `'kskbTask'`)
|
|
60
|
+
- **tasks**: `F[]` - Array of functions to be executed in order
|
|
61
|
+
- **breakCondition?**: `function` - Function that determines when to break the loop (default: `() => false`)
|
|
62
|
+
- **skipCondition?**: `function` - Function that determines when to skip a task (default: `() => false`)
|
|
63
|
+
- **resultWrapper?**: `function` - Function that transforms input between tasks, default(means the first task gets original args, subsequent tasks get the last return value):
|
|
64
|
+
```ts
|
|
65
|
+
(_task: Fn, index: number, _tasks: Fn[], args: unknown[], lastReturn: unknown) =>
|
|
66
|
+
index === 0 ? args : [lastReturn];
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Returns
|
|
70
|
+
|
|
71
|
+
A function that executes the tasks in order and returns a `TaskReturn<R>` object:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
interface TaskReturn<R> {
|
|
75
|
+
value: R; // Result of the last executed task
|
|
76
|
+
results: R[]; // All results (skipped tasks are undefined)
|
|
77
|
+
trivial: boolean; // True if tasks array was empty
|
|
78
|
+
breakAt: number; // Index where loop broke (-1 if not broken)
|
|
79
|
+
skipped: number[]; // Indices of skipped tasks
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 🎨 Usage Scenarios
|
|
84
|
+
|
|
85
|
+
### Scenario 1: Function Composition Pipeline
|
|
86
|
+
|
|
87
|
+
Perfect for data transformation pipelines where you need to apply multiple transformations in sequence:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { createSerialTask } from 'serial-task';
|
|
91
|
+
|
|
92
|
+
// Data processing pipeline
|
|
93
|
+
interface UserData {
|
|
94
|
+
name: string;
|
|
95
|
+
email: string;
|
|
96
|
+
age: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const processUser = createSerialTask({
|
|
100
|
+
name: 'userProcessor',
|
|
101
|
+
tasks: [
|
|
102
|
+
// Step 1: Validate input
|
|
103
|
+
(user: UserData) => {
|
|
104
|
+
if (!user.email.includes('@')) {
|
|
105
|
+
throw new Error('Invalid email');
|
|
106
|
+
}
|
|
107
|
+
return user;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// Step 2: Normalize data
|
|
111
|
+
(user: UserData) => ({
|
|
112
|
+
...user,
|
|
113
|
+
name: user.name.trim().toLowerCase(),
|
|
114
|
+
email: user.email.toLowerCase(),
|
|
115
|
+
}),
|
|
116
|
+
|
|
117
|
+
// Step 3: Add computed fields
|
|
118
|
+
(user: UserData) => ({
|
|
119
|
+
...user,
|
|
120
|
+
isAdult: user.age >= 18,
|
|
121
|
+
displayName: user.name.charAt(0).toUpperCase() + user.name.slice(1),
|
|
122
|
+
}),
|
|
123
|
+
|
|
124
|
+
// Step 4: Generate summary
|
|
125
|
+
(user: any) => ({
|
|
126
|
+
...user,
|
|
127
|
+
summary: `${user.displayName} (${user.email}) - ${user.isAdult ? 'Adult' : 'Minor'}`,
|
|
128
|
+
}),
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const result = processUser({
|
|
133
|
+
name: ' John Doe ',
|
|
134
|
+
email: 'JOHN@EXAMPLE.COM',
|
|
135
|
+
age: 25,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log(result.value);
|
|
139
|
+
// Output: {
|
|
140
|
+
// name: 'john doe',
|
|
141
|
+
// email: 'john@example.com',
|
|
142
|
+
// age: 25,
|
|
143
|
+
// isAdult: true,
|
|
144
|
+
// displayName: 'John doe',
|
|
145
|
+
// summary: 'John doe (john@example.com) - Adult'
|
|
146
|
+
// }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Scenario 2: Event Handler Chain with Conditional Logic
|
|
150
|
+
|
|
151
|
+
Great for building middleware-like handler chains with skip and break logic:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { createSerialTask } from 'serial-task';
|
|
155
|
+
|
|
156
|
+
interface Request {
|
|
157
|
+
path: string;
|
|
158
|
+
method: string;
|
|
159
|
+
headers: Record<string, string>;
|
|
160
|
+
body?: any;
|
|
161
|
+
metadata?: Record<string, any>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// HTTP request handler chain
|
|
165
|
+
const requestHandler = createSerialTask({
|
|
166
|
+
name: 'httpHandler',
|
|
167
|
+
tasks: [
|
|
168
|
+
// Handler 1: Authentication
|
|
169
|
+
(req: Request) => {
|
|
170
|
+
console.log('🔐 Authenticating request...');
|
|
171
|
+
return {
|
|
172
|
+
...req,
|
|
173
|
+
metadata: { ...req.metadata, authenticated: true, userId: 'user123' },
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// Handler 2: Rate limiting
|
|
178
|
+
(req: Request) => {
|
|
179
|
+
console.log('⏱️ Checking rate limits...');
|
|
180
|
+
return {
|
|
181
|
+
...req,
|
|
182
|
+
metadata: { ...req.metadata, rateLimited: false },
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Handler 3: Input validation
|
|
187
|
+
(req: Request) => {
|
|
188
|
+
console.log('✅ Validating input...');
|
|
189
|
+
if (req.method === 'POST' && !req.body) {
|
|
190
|
+
throw new Error('Body required for POST requests');
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
...req,
|
|
194
|
+
metadata: { ...req.metadata, validated: true },
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// Handler 4: Business logic
|
|
199
|
+
(req: Request) => {
|
|
200
|
+
console.log('🔄 Processing business logic...');
|
|
201
|
+
return {
|
|
202
|
+
...req,
|
|
203
|
+
metadata: { ...req.metadata, processed: true, result: 'success' },
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
// Handler 5: Response formatting
|
|
208
|
+
(req: Request) => {
|
|
209
|
+
console.log('📤 Formatting response...');
|
|
210
|
+
return {
|
|
211
|
+
...req,
|
|
212
|
+
metadata: { ...req.metadata, formatted: true },
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
|
|
217
|
+
// Skip rate limiting for admin users
|
|
218
|
+
skipCondition: (task, index, tasks, args, lastReturn) => {
|
|
219
|
+
if (index === 1) {
|
|
220
|
+
// rate limiting handler
|
|
221
|
+
const req = lastReturn as Request;
|
|
222
|
+
return req.metadata?.userId === 'admin';
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// Break early if user is not authenticated
|
|
228
|
+
breakCondition: (task, index, tasks, args, lastReturn) => {
|
|
229
|
+
if (index > 0) {
|
|
230
|
+
// after authentication
|
|
231
|
+
const req = lastReturn as Request;
|
|
232
|
+
return !req.metadata?.authenticated;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
// Pass the result to the next handler
|
|
238
|
+
resultWrapper: (task, index, tasks, args, lastReturn) => {
|
|
239
|
+
if (index === 0) {
|
|
240
|
+
return args; // First handler gets original args
|
|
241
|
+
}
|
|
242
|
+
return [...args, lastReturn]; // Subsequent handlers get the result from previous
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Example usage
|
|
247
|
+
const request: Request = {
|
|
248
|
+
path: '/api/users',
|
|
249
|
+
method: 'GET',
|
|
250
|
+
headers: { Authorization: 'Bearer token123' },
|
|
251
|
+
metadata: {},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const result = requestHandler(request);
|
|
255
|
+
|
|
256
|
+
console.log('Final result:', result.value);
|
|
257
|
+
console.log('Skipped handlers:', result.skipped); // e.g., [1] if rate limiting was skipped
|
|
258
|
+
console.log('Broke at:', result.breakAt); // -1 if completed successfully
|
|
259
|
+
|
|
260
|
+
// Example output:
|
|
261
|
+
// 🔐 Authenticating request...
|
|
262
|
+
// ⏱️ Checking rate limits...
|
|
263
|
+
// ✅ Validating input...
|
|
264
|
+
// 🔄 Processing business logic...
|
|
265
|
+
// 📤 Formatting response...
|
|
266
|
+
// Final result: { ... processed request with all metadata ... }
|
|
267
|
+
// Skipped handlers: []
|
|
268
|
+
// Broke at: -1
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## 🔧 Advanced Features
|
|
272
|
+
|
|
273
|
+
### Conditional Execution
|
|
274
|
+
|
|
275
|
+
Control the flow of your task execution with powerful conditions:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const conditionalTask = createSerialTask({
|
|
279
|
+
tasks: [taskA, taskB, taskC, taskD],
|
|
280
|
+
|
|
281
|
+
// Skip tasks based on conditions
|
|
282
|
+
skipCondition: (task, index, tasks, args, lastReturn) => {
|
|
283
|
+
// Skip taskB if input is negative
|
|
284
|
+
if (index === 1 && args[0] < 0) return true;
|
|
285
|
+
return false;
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// Break early if result exceeds threshold
|
|
289
|
+
breakCondition: (task, index, tasks, args, lastReturn) => {
|
|
290
|
+
return lastReturn > 100;
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Dynamic Task Arrays
|
|
296
|
+
|
|
297
|
+
Modify the task array during execution:
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
const dynamicTask = createSerialTask({
|
|
301
|
+
tasks: [initialTask],
|
|
302
|
+
resultWrapper: (task, index, taskArray, args, lastReturn) => {
|
|
303
|
+
// Add more tasks dynamically
|
|
304
|
+
if (index === 0 && someCondition) {
|
|
305
|
+
taskArray.push(additionalTask);
|
|
306
|
+
}
|
|
307
|
+
return index === 0 ? args : [lastReturn];
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## 🔄 Async Support
|
|
313
|
+
|
|
314
|
+
For async functions, use `createSerialTaskAsync`:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
import { createSerialTaskAsync } from 'serial-task';
|
|
318
|
+
|
|
319
|
+
const asyncTask = createSerialTaskAsync({
|
|
320
|
+
tasks: [
|
|
321
|
+
async (data) => await fetchUserData(data),
|
|
322
|
+
async (user) => await validateUser(user),
|
|
323
|
+
async (user) => await saveUser(user),
|
|
324
|
+
],
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const result = await asyncTask(inputData);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## 🎪 Error Handling
|
|
331
|
+
|
|
332
|
+
Tasks can throw errors, which will propagate up and stop execution:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
const taskWithErrors = createSerialTask({
|
|
336
|
+
tasks: [
|
|
337
|
+
(x) => x + 1,
|
|
338
|
+
(x) => {
|
|
339
|
+
if (x > 10) throw new Error('Value too large!');
|
|
340
|
+
return x * 2;
|
|
341
|
+
},
|
|
342
|
+
(x) => x - 1,
|
|
343
|
+
],
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const result = taskWithErrors(15);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error('Task failed:', error.message);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## 📄 License
|
|
354
|
+
|
|
355
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ## Usage
|
|
3
|
+
* Use this when you have async functions in tasks, conditions or result wrapper
|
|
4
|
+
*
|
|
5
|
+
* Creates an async serial task function that executes a series of functions in order
|
|
6
|
+
* - all given functions(`options.tasks`) will be called in order
|
|
7
|
+
* - generated task function will have the same length as the first task function
|
|
8
|
+
* - you can appoint generated task function's name by `options.name`
|
|
9
|
+
* - **Strongly Recommended**: all task functions have same input type and output type
|
|
10
|
+
* - returned function.length will be the same as the first task function's length
|
|
11
|
+
* @param opts Options for creating a serial task, details in `SerialTaskOptions`
|
|
12
|
+
* @returns a funtcion that executes the tasks in order, returns `TaskReturn<OriginalReturn>`
|
|
13
|
+
*
|
|
14
|
+
* ## About
|
|
15
|
+
* @package SerialTask
|
|
16
|
+
* @author Kasukabe Tsumugi <futami16237@gmail.com>
|
|
17
|
+
* @version 1.0.0 (Last Update: 2025.08.22 17:05:02.346)
|
|
18
|
+
* @license MIT
|
|
19
|
+
* @link https://github.com/baendlorel/serial-task
|
|
20
|
+
* @description Put a list of functions in and get a composed task function. Similar to functional programming's compose (function composition), but with more fine-grained and precise control, and the generated task incurs almost no runtime overhead. Supports both synchronous and asynchronous functions.
|
|
21
|
+
* @copyright Copyright (c) 2025 Kasukabe Tsumugi. All rights reserved.
|
|
22
|
+
*/
|
|
23
|
+
declare function createSerialTaskAsync<F extends Fn>(opts: SerialTaskOptions<F>): TaskifyAsync<F>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ## Usage
|
|
27
|
+
* **DO NOT Use** this when you have **async functions** in tasks, conditions or result wrapper
|
|
28
|
+
*
|
|
29
|
+
* Creates a serial task function that executes a series of functions in order.
|
|
30
|
+
* - all given functions(`options.tasks`) will be called in order
|
|
31
|
+
* - the returned value will be `options.resultWrapper`ed then passed to the next task
|
|
32
|
+
* - generated task function will have the same length as the first task function
|
|
33
|
+
* - you can appoint generated task function's name by `options.name`
|
|
34
|
+
* - **Strongly Recommended**: all task functions have same input type and output type
|
|
35
|
+
* - returned function.length will be the same as the first task function's length
|
|
36
|
+
* @param opts Options for creating a serial task, details in `SerialTaskOptions`
|
|
37
|
+
* @returns a funtcion that executes the tasks in order, returns `TaskReturn<OriginalReturn>`
|
|
38
|
+
*
|
|
39
|
+
* ## About
|
|
40
|
+
* @package SerialTask
|
|
41
|
+
* @author Kasukabe Tsumugi <futami16237@gmail.com>
|
|
42
|
+
* @version 1.0.0 (Last Update: 2025.08.22 17:05:02.346)
|
|
43
|
+
* @license MIT
|
|
44
|
+
* @link https://github.com/baendlorel/serial-task
|
|
45
|
+
* @description Put a list of functions in and get a composed task function. Similar to functional programming's compose (function composition), but with more fine-grained and precise control, and the generated task incurs almost no runtime overhead. Supports both synchronous and asynchronous functions.
|
|
46
|
+
* @copyright Copyright (c) 2025 Kasukabe Tsumugi. All rights reserved.
|
|
47
|
+
*/
|
|
48
|
+
declare function createSerialTask<F extends Fn>(opts: SerialTaskOptions<F>): Taskify<F>;
|
|
49
|
+
|
|
50
|
+
export { createSerialTask, createSerialTaskAsync };
|
|
51
|
+
|
|
52
|
+
// # from: src/global.d.ts
|
|
53
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
54
|
+
type Fn = (...args: any[]) => any;
|
|
55
|
+
|
|
56
|
+
interface TaskReturn<R = any> {
|
|
57
|
+
/**
|
|
58
|
+
* The result of the last task function
|
|
59
|
+
*/
|
|
60
|
+
value: R;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* All results of the tasks
|
|
64
|
+
* - same order as `tasks`
|
|
65
|
+
* - skipped tasks will be empty slot in this array
|
|
66
|
+
* - which means `index in results` is `false`
|
|
67
|
+
*/
|
|
68
|
+
results: R[];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Means `options.tasks.length` is `0`
|
|
72
|
+
*/
|
|
73
|
+
trivial: boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* If the task breaks, this will record the index of the task that caused the break.
|
|
77
|
+
* - `-1` means not broken
|
|
78
|
+
* - otherwise, the index of the task that caused the break
|
|
79
|
+
*/
|
|
80
|
+
breakAt: number;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Index of the skipped tasks
|
|
84
|
+
*/
|
|
85
|
+
skipped: number[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type TaskifyAsync<F extends Fn> = (...args: Parameters<F>) => Promise<TaskReturn<ReturnType<F>>>;
|
|
89
|
+
type Taskify<F extends Fn> = (...args: Parameters<F>) => TaskReturn<ReturnType<F>>;
|
|
90
|
+
|
|
91
|
+
interface SerialTaskOptions<F extends Fn> {
|
|
92
|
+
/**
|
|
93
|
+
* Name of the generated task function
|
|
94
|
+
* - default is `'kskbTask'`
|
|
95
|
+
*/
|
|
96
|
+
name?: string;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Functions to be executed in order. The one that iterates internally.
|
|
100
|
+
* - if you use `createSerialTaskAsync`, these functions will be called with `await`
|
|
101
|
+
* - **Strongly Recommended**: all task functions must have same input type and output type
|
|
102
|
+
* - creator will directly use this array, so you can modify it dynamically
|
|
103
|
+
* - will be executed from `0` to `length - 1`
|
|
104
|
+
*/
|
|
105
|
+
tasks: F[] | Fn[];
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns an array of arguments that will be passed to the next task
|
|
109
|
+
* - **MUST return an array of arguments!**
|
|
110
|
+
* - if you use `createSerialTaskAsync`, this function will be called with `await`
|
|
111
|
+
* - when calling the first task(no result before), the `lastReturn` will be set to `undefined`
|
|
112
|
+
* @default
|
|
113
|
+
* (_task: Fn, index: number, _tasks: Fn[], args: unknown[], lastReturn: unknown) => index === 0 ? args : [lastReturn]
|
|
114
|
+
* @param task current task function
|
|
115
|
+
* @param index index of current task
|
|
116
|
+
* @param tasks is `options.tasks`, since `for tasks.length` loop is used here, you can add new tasks dynamically
|
|
117
|
+
* @param args input value of the whole serial task
|
|
118
|
+
* @param lastReturn returned value of the last task function
|
|
119
|
+
* @returns **must return an array of arguments!**
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* // Internal implementation
|
|
123
|
+
* // first loop, index = 0
|
|
124
|
+
* // The calling order of each function is as follows:
|
|
125
|
+
* const inputValue = [...arguments]; // arguments of the created task function
|
|
126
|
+
* let returnValue = null
|
|
127
|
+
* for(...){
|
|
128
|
+
* const currentInput = resultWrapper(index, ...inputValue, returnValue);
|
|
129
|
+
* const toBreak = breakCondition(index, ...currentInput);
|
|
130
|
+
* if (toBreak) break;
|
|
131
|
+
* const toSkip = skipCondition(index, ...currentInput);
|
|
132
|
+
* if (toSkip) continue;
|
|
133
|
+
* returnValue = tasks[index](...currentInput);
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
resultWrapper?: (
|
|
138
|
+
task: F,
|
|
139
|
+
index: number,
|
|
140
|
+
tasks: F[],
|
|
141
|
+
args: Parameters<F>,
|
|
142
|
+
lastReturn: ReturnType<F>
|
|
143
|
+
) => unknown[];
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Break the loop and return the last result immediately when this function returns `true`
|
|
147
|
+
* - if you use `createSerialTaskAsync`, this function will be called with `await`
|
|
148
|
+
* - when calling the first task(no result before), the `lastReturn` will be `undefined`
|
|
149
|
+
* - default is `() => false`
|
|
150
|
+
* @param task current task function
|
|
151
|
+
* @param index index of current task
|
|
152
|
+
* @param tasks is `options.tasks`, since `for tasks.length` loop is used here, you can add new tasks dynamically
|
|
153
|
+
* @param args input value of the whole serial task
|
|
154
|
+
* @param lastReturn returned value of the last task function
|
|
155
|
+
*/
|
|
156
|
+
breakCondition?: (
|
|
157
|
+
task: F,
|
|
158
|
+
index: number,
|
|
159
|
+
tasks: F[],
|
|
160
|
+
args: Parameters<F>,
|
|
161
|
+
lastReturn: ReturnType<F>
|
|
162
|
+
) => boolean;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Give `true` to skip this task item
|
|
166
|
+
* - if you use `createSerialTaskAsync`, this function will be called with `await`
|
|
167
|
+
* - when calling the first task(no result before), the `lastReturn` will be `undefined`
|
|
168
|
+
* - default is `() => false`
|
|
169
|
+
* @param task current task function
|
|
170
|
+
* @param index index of current task
|
|
171
|
+
* @param tasks is `options.tasks`, since `for tasks.length` loop is used here, you can add new tasks dynamically
|
|
172
|
+
* @param args input value of the whole serial task
|
|
173
|
+
* @param lastReturn returned value of the last task function
|
|
174
|
+
*/
|
|
175
|
+
skipCondition?: (
|
|
176
|
+
task: F,
|
|
177
|
+
index: number,
|
|
178
|
+
tasks: F[],
|
|
179
|
+
args: Parameters<F>,
|
|
180
|
+
lastReturn: ReturnType<F>
|
|
181
|
+
) => boolean;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
type StrictSerialTaskOptions<F extends Fn> = Required<SerialTaskOptions<F>>;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const t=Reflect.defineProperty,e=Array.isArray,n=Promise.resolve.bind(Promise),r=Promise.reject.bind(Promise),o=t=>("object"==typeof t&&null!==t||"function"==typeof t)&&("then"in t&&"function"==typeof t.then),i=()=>!1,a=i,s=(t,e,n,r,o)=>0===e?r:[o];function u(t){if("object"!=typeof t||null===t)throw new TypeError("SerialTask: 'options' must be an object");const{name:n="kskbTask",tasks:r,breakCondition:o=i,skipCondition:u=a,resultWrapper:l=s}=t;if("string"!=typeof n)throw new TypeError("SerialTask: 'name' must be a string or omitted");if(!e(r)||r.some(t=>"function"!=typeof t))throw new TypeError("SerialTask: 'tasks' must be a function array");if("function"!=typeof o)throw new TypeError("SerialTask: 'breakCondition' must be a function or omitted");if("function"!=typeof u)throw new TypeError("SerialTask: 'skipCondition' must be a function or omitted");if("function"!=typeof l)throw new TypeError("SerialTask: 'resultWrapper' must be a function or omitted");return{name:n,tasks:r,breakCondition:o,skipCondition:u,resultWrapper:l}}function l(t,e,...i){try{const r=t.apply(e,i);return o(r)?r:n(r)}catch(t){return r(t)}}function c(t,e,i){try{const r=t.apply(e,i);return o(r)?r:n(r)}catch(t){return r(t)}}function f(e){const{name:n,tasks:r,breakCondition:o,skipCondition:i,resultWrapper:a}=u(e);if(0===r.length){const e=()=>({value:void 0,results:[],trivial:!0,breakAt:-1,skipped:[]});return t(e,"name",{value:n,configurable:!0}),e}const s=async function(...t){let e;const n=new Array(r.length);let s=-1;const u=[];for(let f=0;f<r.length;f++){const p=r[f],k=await l(a,null,p,f,r,t,e);if(await l(o,null,p,f,r,t,e)){s=f;break}await l(i,null,p,f,r,t,e)?u.push(f):(e=await c(p,null,k),n[f]=e)}return{value:e,results:n,trivial:!1,breakAt:s,skipped:u}};return t(s,"name",{value:n,configurable:!0}),t(s,"length",{value:r[0].length,configurable:!0}),s}function p(e){const{name:n,tasks:r,breakCondition:o,skipCondition:i,resultWrapper:a}=u(e);if(0===r.length){const e=()=>({value:void 0,results:[],trivial:!0,breakAt:-1,skipped:[]});return t(e,"name",{value:n,configurable:!0}),e}const s=function(...t){let e;const n=new Array(r.length);let s=-1;const u=[];for(let l=0;l<r.length;l++){const c=r[l],f=a(c,l,r,t,e);if(o(c,l,r,t,e)){s=l;break}i(c,l,r,t,e)?u.push(l):(e=c.apply(null,f),n[l]=e)}return{value:e,results:n,trivial:!1,breakAt:s,skipped:u}};return t(s,"name",{value:n,configurable:!0}),t(s,"length",{value:r[0].length,configurable:!0}),s}export{p as createSerialTask,f as createSerialTaskAsync};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "serial-task",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "Kasukabe Tsumugi",
|
|
6
|
+
"email": "futami16237@gmail.com"
|
|
7
|
+
},
|
|
8
|
+
"purpose": "npm",
|
|
9
|
+
"description": "Put a list of functions in and get a composed task function. Similar to functional programming's compose (function composition), but with more fine-grained and precise control, and the generated task incurs almost no runtime overhead. Supports both synchronous and asynchronous functions.",
|
|
10
|
+
"description_zh": "将一组函数放入并获取一个组合任务函数。类似于函数式编程的函数组合,但具有更细粒度和精确的控制,生成的任务几乎没有运行时判定的额外开销。支持同步和异步函数。",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"module": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.mjs",
|
|
17
|
+
"default": "./dist/index.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://github.com/baendlorel/serial-task#readme",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/baendlorel/serial-task"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"typescript",
|
|
29
|
+
"javascript"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "clear & vitest",
|
|
33
|
+
"lint": "oxlint .",
|
|
34
|
+
"cover": "clear & vitest --coverage",
|
|
35
|
+
"build": "node ./scripts/rollup.mjs"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@babel/plugin-proposal-decorators": "^7.28.0",
|
|
40
|
+
"@babel/preset-env": "^7.28.3",
|
|
41
|
+
"@rollup/plugin-alias": "^5.1.1",
|
|
42
|
+
"@rollup/plugin-babel": "^6.0.4",
|
|
43
|
+
"@rollup/plugin-commonjs": "^28.0.6",
|
|
44
|
+
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
45
|
+
"@rollup/plugin-replace": "^6.0.2",
|
|
46
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
47
|
+
"@rollup/plugin-typescript": "^12.1.4",
|
|
48
|
+
"@types/node": "^24.3.0",
|
|
49
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
50
|
+
"oxlint": "^1.12.0",
|
|
51
|
+
"prettier": "^3.6.2",
|
|
52
|
+
"rimraf": "^6.0.1",
|
|
53
|
+
"rollup": "^4.47.1",
|
|
54
|
+
"rollup-plugin-dts": "^6.2.3",
|
|
55
|
+
"rollup-plugin-dts-merger": "^1.2.3",
|
|
56
|
+
"tslib": "^2.8.1",
|
|
57
|
+
"typescript": "^5.9.2",
|
|
58
|
+
"vitest": "^3.2.4"
|
|
59
|
+
}
|
|
60
|
+
}
|