fyflow-scheduler 0.1.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/AGENTS.md +577 -0
- package/LICENSE +21 -0
- package/README.md +415 -0
- package/dist/browser/index.js +2378 -0
- package/dist/node/index.js +2380 -0
- package/dist/types/core/FyflowScheduler.d.ts +279 -0
- package/dist/types/core/FyflowScheduler.d.ts.map +1 -0
- package/dist/types/core/inlineWrapper.d.ts +45 -0
- package/dist/types/core/inlineWrapper.d.ts.map +1 -0
- package/dist/types/core/threadWrapper.d.ts +59 -0
- package/dist/types/core/threadWrapper.d.ts.map +1 -0
- package/dist/types/core/workerInterface.d.ts +166 -0
- package/dist/types/core/workerInterface.d.ts.map +1 -0
- package/dist/types/core/workerManager.d.ts +141 -0
- package/dist/types/core/workerManager.d.ts.map +1 -0
- package/dist/types/core/workerWrapper.d.ts +2 -0
- package/dist/types/core/workerWrapper.d.ts.map +1 -0
- package/dist/types/core/workerWrapperUrl.bundled.d.ts +2 -0
- package/dist/types/core/workerWrapperUrl.bundled.d.ts.map +1 -0
- package/dist/types/core/workerWrapperUrl.d.ts +2 -0
- package/dist/types/core/workerWrapperUrl.d.ts.map +1 -0
- package/dist/types/groups/concurrentLimitGroup.d.ts +33 -0
- package/dist/types/groups/concurrentLimitGroup.d.ts.map +1 -0
- package/dist/types/groups/keyedRateLimitGroup.d.ts +98 -0
- package/dist/types/groups/keyedRateLimitGroup.d.ts.map +1 -0
- package/dist/types/groups/rateLimitGroup.d.ts +79 -0
- package/dist/types/groups/rateLimitGroup.d.ts.map +1 -0
- package/dist/types/groups/resourceGroup.d.ts +41 -0
- package/dist/types/groups/resourceGroup.d.ts.map +1 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# FyFlow Scheduler
|
|
2
|
+
|
|
3
|
+
Zero-dependency parallel task scheduler with resource management and cross-platform support.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/js/fyflow-scheduler)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
> **Note**: This project was mostly "vibe coded" with [Claude Code](https://claude.ai/code) - it was an experiment, if such a complex library can be built with sufficient code quality.
|
|
9
|
+
|
|
10
|
+
> **Using this library from an AI agent?** [AGENTS.md](AGENTS.md) is a complete
|
|
11
|
+
> single-file reference: exact signatures, defaults, events, gotchas and recipes.
|
|
12
|
+
> Every snippet in it is executed by the test suite.
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
FyFlow is a self-contained parallel task scheduler with resource management. Fully in-memory operation with zero dependencies. Extensible APIs allow enhancement for persistence, distributed execution, or multi-machine coordination.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
**Node.js and browser** - npm, which ships prebuilt bundles with the worker
|
|
21
|
+
bootstrap inlined, so no bundler configuration is needed:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install fyflow-scheduler
|
|
25
|
+
# or
|
|
26
|
+
yarn add fyflow-scheduler
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Deno** - JSR, which ships the TypeScript sources and loads the worker directly:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
deno add jsr:@fyflow/scheduler
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The two packages are not interchangeable. **The JSR package is Deno-only**: it
|
|
36
|
+
contains no Node-specific files, and importing it from Node or a browser throws
|
|
37
|
+
an error pointing you at npm. Node and browser support lives entirely in the npm
|
|
38
|
+
package.
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { FyflowScheduler, FyflowTask, WorkerManager, ConcurrentLimitGroup } from 'fyflow-scheduler';
|
|
44
|
+
|
|
45
|
+
// A pool must DECLARE the groups it uses - registering a group on the scheduler
|
|
46
|
+
// alone does not constrain anything
|
|
47
|
+
const workerPool = new WorkerManager('./worker.js', { maxThreads: 2, groups: ['cpu'] });
|
|
48
|
+
|
|
49
|
+
const scheduler = new FyflowScheduler(
|
|
50
|
+
{ MyWorker: workerPool },
|
|
51
|
+
{ cpu: new ConcurrentLimitGroup(4) }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const tasks = [
|
|
55
|
+
new FyflowTask({ id: 'task1', workerType: 'MyWorker', payload: { data: 'input' } }),
|
|
56
|
+
new FyflowTask({ id: 'task2', workerType: 'MyWorker', payload: { data: 'processed' } })
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// createPromise is required to get results back - addTask is fire-and-forget
|
|
60
|
+
const results = await Promise.all(scheduler.addTasks(tasks, { createPromise: true }));
|
|
61
|
+
|
|
62
|
+
await scheduler.shutdown(); // required, or live workers keep the process alive
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
See [Loading Workers](#loading-workers-differs-per-runtime) for how `./worker.js`
|
|
66
|
+
is resolved - it differs between Deno, Node and the browser.
|
|
67
|
+
|
|
68
|
+
## Key Features
|
|
69
|
+
|
|
70
|
+
- **Parallel Task Execution**: High-performance concurrent task processing
|
|
71
|
+
- **Resource Management**: Named concurrency and rate-limit groups applied per pool or per task
|
|
72
|
+
- **Dynamic Task Spawning**: Workers can create new tasks at runtime
|
|
73
|
+
- **Cross-Platform**: Node.js, Browser, and Deno support
|
|
74
|
+
- **Worker Pools**: Thread-based and inline execution modes
|
|
75
|
+
- **Progress Reporting**: Real-time task progress and event monitoring
|
|
76
|
+
- **Zero Dependencies**: Self-contained with no external requirements
|
|
77
|
+
- **In-Memory**: Fast coordination with extensible persistence APIs
|
|
78
|
+
|
|
79
|
+
## Basic Usage
|
|
80
|
+
|
|
81
|
+
### Creating Workers
|
|
82
|
+
|
|
83
|
+
A worker script must `export default` a class.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { BaseWorker } from 'fyflow-scheduler';
|
|
87
|
+
|
|
88
|
+
export default class MyWorker extends BaseWorker {
|
|
89
|
+
// The pool calls `new MyWorker(config, workerContext)` - forward both
|
|
90
|
+
constructor(config = {}, workerContext) {
|
|
91
|
+
super(config, workerContext);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// setup and teardown are abstract on BaseWorker: both required, may be empty
|
|
95
|
+
async setup() {}
|
|
96
|
+
async teardown() {}
|
|
97
|
+
|
|
98
|
+
async run(payload, context) {
|
|
99
|
+
context?.sendProgress(0.5, "Processing..."); // progress is 0-1
|
|
100
|
+
|
|
101
|
+
// Spawn additional tasks if needed
|
|
102
|
+
context?.spawnTask({
|
|
103
|
+
id: 'child-task',
|
|
104
|
+
workerType: 'MyWorker',
|
|
105
|
+
payload: { data: 'child-data' }
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return { result: 'completed', data: payload.data };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Loading Workers
|
|
114
|
+
|
|
115
|
+
Point the pool at your worker file's URL.
|
|
116
|
+
|
|
117
|
+
**npm (Node and browser)** - ship a compiled `.js` worker:
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
const workerUrl = new URL('./myWorker.js', import.meta.url).href;
|
|
121
|
+
const pool = new WorkerManager(workerUrl, { maxThreads: 4 });
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
No bundler plugin and no build step for the worker itself. Node cannot import
|
|
125
|
+
TypeScript, so a `.ts` worker fails with `Unknown file extension`; compile it as
|
|
126
|
+
part of your own build. In the browser, your bundler needs to emit the worker as
|
|
127
|
+
a separate asset, which most do automatically for
|
|
128
|
+
`new URL('./myWorker.js', import.meta.url)`.
|
|
129
|
+
|
|
130
|
+
**Deno (JSR)** - point at the TypeScript source, which Deno loads directly:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const workerUrl = new URL('./myWorker.ts', import.meta.url).href;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
> The `?worker-direct` suffix in this repository's examples and tests is a
|
|
137
|
+
> convention of its own esbuild config, used so the `.ts` examples run on both
|
|
138
|
+
> runtimes. It is not part of the published API and will not resolve in your
|
|
139
|
+
> code.
|
|
140
|
+
|
|
141
|
+
### Getting Results Back
|
|
142
|
+
|
|
143
|
+
`addTask` and `addTasks` are fire-and-forget by default and return nothing. Ask
|
|
144
|
+
for a promise explicitly:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const result = await scheduler.addTask(task, { createPromise: true });
|
|
148
|
+
|
|
149
|
+
const results = await Promise.all(
|
|
150
|
+
scheduler.addTasks(tasks, { createPromise: true })
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
await scheduler.shutdown(); // required, or workers keep the process alive
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Parallel Task Execution
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const tasks = [
|
|
160
|
+
new FyflowTask({ id: 'fetch', workerType: 'DataWorker', payload: { url: 'api/data' } }),
|
|
161
|
+
new FyflowTask({ id: 'validate', workerType: 'ValidationWorker', payload: { url: 'api/data2' } }),
|
|
162
|
+
new FyflowTask({ id: 'process', workerType: 'ProcessWorker', payload: { batch: 1 } }),
|
|
163
|
+
new FyflowTask({ id: 'save', workerType: 'SaveWorker', payload: { batch: 2 } })
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
// All tasks execute in parallel (subject to resource constraints)
|
|
167
|
+
tasks.forEach(task => scheduler.addTask(task));
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Resource Groups
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const cpuGroup = new ConcurrentLimitGroup(8); // Max 8 concurrent CPU tasks
|
|
174
|
+
const gpuGroup = new ConcurrentLimitGroup(2); // Max 2 concurrent GPU tasks
|
|
175
|
+
|
|
176
|
+
const scheduler = new FyflowScheduler(workerPools, {
|
|
177
|
+
cpu: cpuGroup,
|
|
178
|
+
gpu: gpuGroup
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Workers automatically use groups specified in WorkerManager
|
|
182
|
+
const gpuWorkerPool = new WorkerManager('./gpu-worker.js', {
|
|
183
|
+
maxThreads: 2,
|
|
184
|
+
groups: ['gpu'] // This pool uses GPU constraints
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Event Monitoring
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
scheduler.addEventListener('task.completed', (e) => {
|
|
192
|
+
console.log(`Task ${e.detail.id} completed:`, e.detail.result);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
scheduler.addEventListener('task.progress', (e) => {
|
|
196
|
+
// progress is 0-1
|
|
197
|
+
console.log(`Task ${e.detail.taskId}: ${(e.detail.progress * 100).toFixed(0)}%`);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
scheduler.addEventListener('scheduler.completed', (e) => {
|
|
201
|
+
console.log(`All tasks completed. Stats:`, e.detail);
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## API Reference
|
|
206
|
+
|
|
207
|
+
### Core Classes
|
|
208
|
+
|
|
209
|
+
**FyflowScheduler(workerPools, resourceGroups, options?)**
|
|
210
|
+
- `options.maxCompletedTasks`: cap how many terminal tasks stay in
|
|
211
|
+
`scheduler.tasks` (default: unlimited). Set it for long-lived schedulers -
|
|
212
|
+
completed tasks otherwise pin their payloads and results forever
|
|
213
|
+
- `options.periodicRetryIntervalMs`: retry interval for blocked tasks (default 50ms)
|
|
214
|
+
- `addTask(task, options?)`: Add a task. Fire-and-forget by default; pass
|
|
215
|
+
`{ createPromise: true }` to get a promise back
|
|
216
|
+
- `addTasks(tasks, options?)`: Batch version, optimised for bulk additions.
|
|
217
|
+
Throws on an unknown `workerType`, validating the batch before queueing any of it
|
|
218
|
+
- `stats`: Current execution statistics
|
|
219
|
+
- `getResourceMetrics()` / `getResourceStats()`: Per-group utilisation and lifetime counters
|
|
220
|
+
- `shutdown()`: Terminate worker pools and release all listeners
|
|
221
|
+
- `addEventListener(event, handler)`: Event monitoring
|
|
222
|
+
|
|
223
|
+
**FyflowTask(config)**
|
|
224
|
+
- `id`: Unique task identifier
|
|
225
|
+
- `workerType`: Worker pool to use
|
|
226
|
+
- `payload`: Data passed to worker
|
|
227
|
+
- `workerGroups`: Optional resource group names
|
|
228
|
+
- `optional`: Mark task as optional (failures don't block workflow)
|
|
229
|
+
- `limitKey`: Bucket for keyed resource groups (see `KeyedRateLimitGroup`)
|
|
230
|
+
- `retryPolicy`: `{ maxRetries, backoffMs }`
|
|
231
|
+
- `onCompletePromise()`: Promise for this task alone
|
|
232
|
+
- `onCompleteDescendants()`: Promise for this task plus everything it spawns
|
|
233
|
+
|
|
234
|
+
**WorkerManager(scriptUrl, options)**
|
|
235
|
+
- `maxThreads`: Maximum worker **instances** (not OS threads). With
|
|
236
|
+
`inline: false` each is a real worker thread; with `inline: true` they are
|
|
237
|
+
objects in the main process, so only `maxThreads × maxConcurrentTasks` matters
|
|
238
|
+
for throughput — the split controls how much per-instance state (connections,
|
|
239
|
+
caches) is duplicated
|
|
240
|
+
- `maxConcurrentTasks`: Tasks per instance
|
|
241
|
+
- `groups`: Resource group names
|
|
242
|
+
- `inline`: Use inline execution (default: false)
|
|
243
|
+
- `idleTimeout`: Ms before an idle worker is terminated (0 = never, default 5000)
|
|
244
|
+
|
|
245
|
+
**ConcurrentLimitGroup(limit, id?)**
|
|
246
|
+
- Resource constraint with a concurrent execution limit. Optimistic: may briefly
|
|
247
|
+
exceed the limit by up to `maxThreads × maxConcurrentTasks` under contention
|
|
248
|
+
|
|
249
|
+
**RateLimitGroup(windows, id?)**
|
|
250
|
+
- Time-window throttling, e.g. `[{ limit: 10, windowMs: 1000 }]`. Multiple
|
|
251
|
+
overlapping windows are enforced together
|
|
252
|
+
|
|
253
|
+
**KeyedRateLimitGroup(windows, options?)**
|
|
254
|
+
- The same windows applied **independently per key**, for when each endpoint,
|
|
255
|
+
tenant or account has its own quota
|
|
256
|
+
- `options.keyFrom` derives the key from a task, defaulting to `task.limitKey`
|
|
257
|
+
- A task with no derivable key throws at `addTask` rather than silently sharing
|
|
258
|
+
a bucket
|
|
259
|
+
- Blocked tasks queue per key, so a saturated key never delays another
|
|
260
|
+
- Idle keys are evicted after `options.idleKeyTtlMs` (default: twice the largest
|
|
261
|
+
window), so high-cardinality keys do not grow without bound
|
|
262
|
+
- `getKeyMetrics(key)` and `getActiveKeys()` give the per-bucket view
|
|
263
|
+
|
|
264
|
+
**Worker management** (on a `WorkerManager`)
|
|
265
|
+
- `getWorkerIds()`: ids of the workers that currently exist (created lazily)
|
|
266
|
+
- `getWorkerStatus(id)` / `getAllWorkerStatuses()`: `{ id, state, tasksCompleted,
|
|
267
|
+
errorCount, uptime, currentTasks, resourcesHeld, lastError? }`
|
|
268
|
+
- `restartWorker(id, newConfig?)` / `replaceWorker(...)`: terminate and rebuild a
|
|
269
|
+
worker, optionally with new config. The replacement gets a new id
|
|
270
|
+
- `updateWorkerConfig(id, config)`: merge config into a live worker
|
|
271
|
+
- `shutdown()`: terminate the pool
|
|
272
|
+
|
|
273
|
+
**Worker self-termination** — a worker can ask to be torn down, e.g. after
|
|
274
|
+
detecting a bad connection. In-flight tasks are requeued when the pool's
|
|
275
|
+
`requeueFailedTasks` allows it:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
this.workerContext?.terminateWithError(new Error('connection lost'), { canRestart: true });
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### When Workers Fail
|
|
282
|
+
|
|
283
|
+
The scheduler never discards queued work to make a broken pool look healthy:
|
|
284
|
+
|
|
285
|
+
- A worker that **dies or cannot be constructed**, with `requeueFailedTasks: true`
|
|
286
|
+
(the default), has its tasks requeued. The pool retries up to
|
|
287
|
+
`maxWorkerRestarts` times, then gives up. If it never manages to run a worker
|
|
288
|
+
those tasks wait, and their promises stay pending - a pool that cannot keep or
|
|
289
|
+
build a worker needs fixing, not its work thrown away.
|
|
290
|
+
- With `requeueFailedTasks: false` the tasks fail and their promises reject.
|
|
291
|
+
|
|
292
|
+
So watch the pool, not only the task. `worker.restart_limit_exceeded` means the
|
|
293
|
+
pool has given up rebuilding workers and needs intervention - it covers both a
|
|
294
|
+
worker that cannot be built and one that keeps dying.
|
|
295
|
+
|
|
296
|
+
### Events
|
|
297
|
+
|
|
298
|
+
On the **scheduler**: `task.running`, `task.completed`, `task.failed`,
|
|
299
|
+
`task.progress`, `task.user_action`, `task.spawn_request`, `task.spawn_failed`,
|
|
300
|
+
`scheduler.completed`.
|
|
301
|
+
|
|
302
|
+
On a **WorkerManager**: `task.started`, `task.completed`, `task.failed`,
|
|
303
|
+
`task.progress`, `task.spawn_request`, `task.requeue_required`, `worker.failed`,
|
|
304
|
+
`worker.self_terminated`, `worker.restart_limit_exceeded`, plus the worker
|
|
305
|
+
lifecycle events `worker.initialization.started|completed|failed`,
|
|
306
|
+
`worker.setup.started|completed` and `worker.teardown.started|completed|failed`.
|
|
307
|
+
|
|
308
|
+
Lifecycle events carry `{ workerId, workerType, timestamp }`, plus `duration` on
|
|
309
|
+
`*.completed` and `error` on `*.failed`. Inline and threaded pools emit the same
|
|
310
|
+
set. They fire on every worker creation and idle-timeout teardown, so keep those
|
|
311
|
+
listeners cheap.
|
|
312
|
+
|
|
313
|
+
`scheduler.completed` fires whenever the scheduler drains, so it can fire more
|
|
314
|
+
than once if tasks arrive in waves.
|
|
315
|
+
|
|
316
|
+
### Worker Interface
|
|
317
|
+
|
|
318
|
+
Workers extend `BaseWorker` and implement:
|
|
319
|
+
- `constructor(config)`: Setup worker configuration
|
|
320
|
+
- `async run(payload, context)`: Execute task logic
|
|
321
|
+
- `async setup()`: Initialize worker (required by `BaseWorker`; may be empty)
|
|
322
|
+
- `async teardown()`: Cleanup worker (required by `BaseWorker`; may be empty)
|
|
323
|
+
|
|
324
|
+
## Performance
|
|
325
|
+
|
|
326
|
+
Measured with `npm run benchmark:quick`:
|
|
327
|
+
|
|
328
|
+
- **Throughput (inline)**: tens of thousands of tasks/sec at 10K tasks, falling
|
|
329
|
+
at larger volumes as coordination grows relative to per-task work
|
|
330
|
+
- **Efficiency (threaded, CPU work)**: 96-98%. This is achieved average
|
|
331
|
+
concurrency over configured concurrency - "how much of the parallelism I asked
|
|
332
|
+
for did I get" - so steady-state coordination overhead is a few percent
|
|
333
|
+
- **Memory**: roughly 1MB per 1,000 in-flight tasks at 10K scale. Completed tasks
|
|
334
|
+
are retained by default; see `maxCompletedTasks`
|
|
335
|
+
|
|
336
|
+
**Worker startup** is measured separately (`npm run benchmark:startup`), because
|
|
337
|
+
every other scenario pre-warms its pool. It is the largest difference between the
|
|
338
|
+
two worker types, and between runtimes:
|
|
339
|
+
|
|
340
|
+
| | Deno | Node 22 |
|
|
341
|
+
|---|---:|---:|
|
|
342
|
+
| 1 worker thread | ~12 ms | ~54 ms |
|
|
343
|
+
| 8 inline instances (each) | ~85 µs | ~324 µs |
|
|
344
|
+
|
|
345
|
+
Threads cost roughly three orders of magnitude more to start than inline
|
|
346
|
+
instances, so a short-lived threaded pool can spend most of its life starting up.
|
|
347
|
+
|
|
348
|
+
Numbers vary substantially between runs and machines, and a scenario's result
|
|
349
|
+
depends on what ran before it in the suite. Re-run the suite rather than relying
|
|
350
|
+
on these figures, and compare full-suite runs only against other full-suite runs.
|
|
351
|
+
|
|
352
|
+
## Platform Support
|
|
353
|
+
|
|
354
|
+
| Platform | Package | Worker Threads | Inline Workers | Build Required |
|
|
355
|
+
|----------|---------|---------------|----------------|----------------|
|
|
356
|
+
| Node.js | npm `fyflow-scheduler` | ✅ | ✅ | Prebuilt in the package |
|
|
357
|
+
| Browser | npm `fyflow-scheduler` | ✅ | ✅ | Prebuilt in the package |
|
|
358
|
+
| Deno | JSR `@fyflow/scheduler` | ✅ | ✅ | None - sources are loaded directly |
|
|
359
|
+
|
|
360
|
+
Only four files differ between runtimes, and the split is resolved at build time
|
|
361
|
+
rather than at runtime: the worker bootstrap (`workerWrapper.ts` vs
|
|
362
|
+
`workerWrapper.node.ts`), the URL that locates it, and two feature checks in
|
|
363
|
+
`ThreadWrapper` for Node's `worker_threads` event emitter. Everything else -
|
|
364
|
+
the scheduler, worker manager and all resource groups - is platform-agnostic.
|
|
365
|
+
|
|
366
|
+
Node.js 22 or newer is required: the library and its tests rely on `CustomEvent`
|
|
367
|
+
being a global, which landed in Node 19.
|
|
368
|
+
|
|
369
|
+
## Development
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
# Build library
|
|
373
|
+
npm run build
|
|
374
|
+
|
|
375
|
+
# Run tests - core, error handling, spawning and the documentation examples
|
|
376
|
+
npm test # Node.js tests (requires Node >= 22)
|
|
377
|
+
npm run test:browser # Browser tests (Playwright)
|
|
378
|
+
npm run test:deno # Deno tests
|
|
379
|
+
npm run test:docs # Just the executable documentation examples
|
|
380
|
+
npm run test:performance # Contention scaling (not part of the default run)
|
|
381
|
+
|
|
382
|
+
# Type check library, tests, examples and benchmarks
|
|
383
|
+
deno task check
|
|
384
|
+
|
|
385
|
+
# Benchmarks
|
|
386
|
+
npm run benchmark # Full benchmark suite
|
|
387
|
+
npm run benchmark:quick # Quick performance test
|
|
388
|
+
npm run benchmark:startup # Worker startup cost, for comparing runtimes
|
|
389
|
+
npm run benchmark:baseline # The set behind benchmark-baseline-comprehensive.md
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Examples
|
|
393
|
+
|
|
394
|
+
See the `examples/` directory for comprehensive usage examples:
|
|
395
|
+
- `getting-started.ts`: Basic parallel task execution
|
|
396
|
+
- `enhanced-features.ts`: Progress reporting, dynamic task spawning and `onCompleteDescendants()`
|
|
397
|
+
- `worker-types.ts`: Thread vs inline worker comparison
|
|
398
|
+
- `performance-groups.ts`: Resource management, rate limits and constraints
|
|
399
|
+
- `monitor-resource-health.ts`: Drop-in resource health monitor helper
|
|
400
|
+
- `deno-only/minimal.ts`: The plain Deno form - a worker URL with no build step
|
|
401
|
+
and none of this repo's bundler convention
|
|
402
|
+
|
|
403
|
+
The examples above the `deno-only/` folder run on both Deno and Node, which is
|
|
404
|
+
why they carry the `?worker-direct` branch. Run them all with
|
|
405
|
+
`npm run build:dev && deno task examples`.
|
|
406
|
+
|
|
407
|
+
## License
|
|
408
|
+
|
|
409
|
+
MIT License.
|
|
410
|
+
|
|
411
|
+
## Links
|
|
412
|
+
|
|
413
|
+
- [GitHub Repository](https://github.com/fyflow/fyflow-scheduler)
|
|
414
|
+
- [NPM Package](https://www.npmjs.com/package/fyflow-scheduler)
|
|
415
|
+
- [Issue Tracker](https://github.com/fyflow/fyflow-scheduler/issues)
|