@siredvin/taskmaster-ts 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.
Files changed (3) hide show
  1. package/index.d.ts +469 -0
  2. package/index.lua +738 -0
  3. package/package.json +39 -0
package/index.d.ts ADDED
@@ -0,0 +1,469 @@
1
+ /**
2
+ * Taskmaster: A simple and highly flexible task runner/coroutine manager for ComputerCraft
3
+ *
4
+ * Supports adding/removing tasks, early exits for tasks, event white/blacklists, automatic
5
+ * terminal redirection, task pausing, promises, and more.
6
+ *
7
+ * @author JackMacWindows
8
+ * @license CC0 (Public Domain)
9
+ */
10
+
11
+ /** @noSelfInFile */
12
+
13
+ /**
14
+ * Represents a task in the event loop
15
+ */
16
+ export interface Task {
17
+ /** The event loop for the task */
18
+ readonly master: Taskmaster;
19
+
20
+ /**
21
+ * Pauses the task, preventing it from running.
22
+ * This will yield if the task calls this method on itself.
23
+ */
24
+ pause(): void;
25
+
26
+ /**
27
+ * Unpauses the task if it was previously paused by pause()
28
+ */
29
+ unpause(): void;
30
+
31
+ /**
32
+ * Removes the task from the run loop, as if it returned.
33
+ * This will yield if the task calls this method on itself.
34
+ */
35
+ remove(): void;
36
+
37
+ /**
38
+ * Sets the priority of the task. This determines the order tasks are run in.
39
+ * @param priority The priority of the task (0 is the default)
40
+ */
41
+ setPriority(priority: number): void;
42
+
43
+ /**
44
+ * Sets a blacklist for events to send to this task.
45
+ * @param list A list of events to not send to this task
46
+ */
47
+ setEventBlacklist(list: string[] | null): void;
48
+
49
+ /**
50
+ * Sets a whitelist for events to send to this task.
51
+ * @param list A list of events to send to this task (others are discarded)
52
+ */
53
+ setEventWhitelist(list: string[] | null): void;
54
+
55
+ /**
56
+ * Sets an error handler for a task.
57
+ * @param errh A function to call if the task throws an error
58
+ */
59
+ setErrorHandler(errh: ((err: any, task: Task) => void) | null): void;
60
+ }
61
+
62
+ /**
63
+ * Represents a Promise in the event loop
64
+ */
65
+ export interface Promise<T = any> {
66
+ /**
67
+ * Adds a function to call when the promise resolves.
68
+ * @param fn The function to call
69
+ * @param err A function to catch errors
70
+ * @returns The next promise in the chain
71
+ */
72
+ next<U = any>(
73
+ fn: (...args: any[]) => U | Promise<U>,
74
+ err?: (err: any) => void
75
+ ): Promise<U>;
76
+
77
+ /**
78
+ * Adds a function to call when the promise resolves (alias for next).
79
+ * @param fn The function to call
80
+ * @param err A function to catch errors
81
+ * @returns The next promise in the chain
82
+ */
83
+ Then<U = any>(
84
+ fn: (...args: any[]) => U | Promise<U>,
85
+ err?: (err: any) => void
86
+ ): Promise<U>;
87
+
88
+ /**
89
+ * Sets the error handler for the promise.
90
+ * @param fn The error handler to use
91
+ * @returns This promise
92
+ */
93
+ catch(fn: (err: any) => void): this;
94
+
95
+ /**
96
+ * Sets a function to call after the promise settles.
97
+ * @param fn The function to call
98
+ * @returns This promise
99
+ */
100
+ finally(fn: () => void): this;
101
+ }
102
+
103
+ /**
104
+ * HTTP response handle with promise methods
105
+ */
106
+ export interface HttpResponseHandle {
107
+ /**
108
+ * Returns a promise that resolves to the body of the response
109
+ */
110
+ text(): Promise<string>;
111
+
112
+ /**
113
+ * Returns a promise that resolves to the body unserialized as JSON
114
+ */
115
+ json<T = any>(): Promise<T>;
116
+
117
+ /**
118
+ * Returns a promise that resolves to the body unserialized as a Lua table
119
+ */
120
+ table<T = any>(): Promise<T>;
121
+
122
+ /** Closes the response handle */
123
+ close(): void;
124
+
125
+ /** Reads all content from the response */
126
+ readAll(): string;
127
+
128
+ /** Gets the response code */
129
+ getResponseCode(): number;
130
+
131
+ /** Gets response headers */
132
+ getResponseHeaders(): Record<string, string>;
133
+ }
134
+
135
+ /**
136
+ * Options for HTTP fetch requests
137
+ */
138
+ export interface HttpFetchOptions {
139
+ /** The URL to connect to */
140
+ url: string;
141
+ /** POST body to send */
142
+ body?: string;
143
+ /** HTTP headers to add to the request */
144
+ headers?: Record<string, string>;
145
+ /** HTTP method to use */
146
+ method?: string;
147
+ /** Whether to send in binary mode (deprecated as of CC:T 1.109.0) */
148
+ binary?: boolean;
149
+ /** Request timeout in seconds */
150
+ timeout?: number;
151
+ }
152
+
153
+ /**
154
+ * Promise constructor bound to a Taskmaster loop
155
+ */
156
+ export interface PromiseConstructor {
157
+ /**
158
+ * Creates a new Promise on the selected run loop.
159
+ * @param fn The main function for the promise
160
+ * @returns The new promise
161
+ */
162
+ new <T = any>(
163
+ fn: (
164
+ resolve: (...args: any[]) => void,
165
+ reject: (err: any) => void
166
+ ) => void
167
+ ): Promise<T>;
168
+
169
+ /**
170
+ * Creates a new Promise on the selected run loop (callable as function).
171
+ * @param fn The main function for the promise
172
+ * @returns The new promise
173
+ */
174
+ <T = any>(
175
+ fn: (
176
+ resolve: (...args: any[]) => void,
177
+ reject: (err: any) => void
178
+ ) => void
179
+ ): Promise<T>;
180
+
181
+ /**
182
+ * Creates a new Promise that resolves once all of the listed promises resolve.
183
+ * @param list The promises to wait for
184
+ * @returns The new promise
185
+ */
186
+ all<T = any>(list: Promise<T>[]): Promise<T>;
187
+
188
+ /**
189
+ * Creates a new Promise that resolves once any of the listed promises resolve,
190
+ * or rejects if all promises reject.
191
+ * @param list The promises to wait for
192
+ * @returns The new promise
193
+ */
194
+ any<T = any>(list: Promise<T>[]): Promise<T>;
195
+
196
+ /**
197
+ * Creates a new Promise that resolves once any of the listed promises resolve.
198
+ * @param list The promises to wait for
199
+ * @returns The new promise
200
+ */
201
+ race<T = any>(list: Promise<T>[]): Promise<T>;
202
+
203
+ /**
204
+ * Creates a new Promise that immediately resolves to a value.
205
+ * @param val The value to resolve to
206
+ * @returns The new promise
207
+ */
208
+ resolve<T = any>(val: T): Promise<T>;
209
+
210
+ /**
211
+ * Creates a new Promise that immediately rejects with an error.
212
+ * @param err The error to reject with
213
+ * @returns The new promise
214
+ */
215
+ reject<T = any>(err: any): Promise<T>;
216
+
217
+ /**
218
+ * Makes an HTTP request to a URL, and returns a Promise for the result.
219
+ * @param url The URL to connect to
220
+ * @param body POST body to send
221
+ * @param headers HTTP headers to add to the request
222
+ * @param binary Whether to send in binary mode
223
+ * @returns Promise that resolves with the response handle
224
+ */
225
+ fetch(
226
+ url: string,
227
+ body?: string,
228
+ headers?: Record<string, string>,
229
+ binary?: boolean
230
+ ): Promise<HttpResponseHandle>;
231
+
232
+ /**
233
+ * Makes an HTTP request with options, and returns a Promise for the result.
234
+ * @param options Request options
235
+ * @returns Promise that resolves with the response handle
236
+ */
237
+ fetch(options: HttpFetchOptions): Promise<HttpResponseHandle>;
238
+ }
239
+
240
+ /**
241
+ * A queue for pushing items between tasks
242
+ */
243
+ export interface Queue<T = any> {
244
+ /**
245
+ * Returns the length of the queue.
246
+ */
247
+ length(): number;
248
+
249
+ /**
250
+ * Returns whether the queue is empty.
251
+ */
252
+ empty(): boolean;
253
+
254
+ /**
255
+ * Returns the item in the front of the queue, or undefined if the queue is empty.
256
+ */
257
+ peek(): T | undefined;
258
+
259
+ /**
260
+ * Pushes an item to the end of the queue. This will safely awaken any tasks
261
+ * that are awaiting this queue.
262
+ * @param item The item to push
263
+ */
264
+ push(item: T): void;
265
+
266
+ /**
267
+ * Returns the next item in the queue and removes it. If the queue is empty,
268
+ * the task waits for an item to be pushed.
269
+ *
270
+ * This must be called by a task in the owning loop.
271
+ * @returns The item that was popped
272
+ */
273
+ pop(): T;
274
+ }
275
+
276
+ /**
277
+ * A multi queue that distributes items to all subscribed tasks
278
+ */
279
+ export interface MultiQueue<T = any> {
280
+ /**
281
+ * Initializes a queue for the current task, allowing items to be pushed for
282
+ * this task. This is called automatically by pop() as well.
283
+ *
284
+ * This must be called by a task in the owning loop.
285
+ */
286
+ init(): void;
287
+
288
+ /**
289
+ * Pushes an item to the end of all queues. This will safely awaken any tasks
290
+ * that are awaiting this queue.
291
+ * @param item The item to push
292
+ */
293
+ push(item: T): void;
294
+
295
+ /**
296
+ * Returns the next item in the queue and removes it. If the queue is empty,
297
+ * the task waits for an item to be pushed.
298
+ *
299
+ * This must be called by a task in the owning loop.
300
+ * @returns The item that was popped
301
+ */
302
+ pop(): T;
303
+ }
304
+
305
+ /**
306
+ * Main Taskmaster event loop
307
+ */
308
+ export interface Taskmaster {
309
+ /** Promise constructor bound to this loop */
310
+ readonly Promise: PromiseConstructor;
311
+
312
+ /**
313
+ * Adds a task to the loop.
314
+ * @param fn The main function to add, which receives the task as an argument
315
+ * @returns The created task
316
+ */
317
+ addTask(fn: () => void): Task;
318
+
319
+ /**
320
+ * Adds a task to the loop in builder style.
321
+ * @param fn The main function to add
322
+ * @returns This Taskmaster instance for chaining
323
+ */
324
+ task(fn: () => void): this;
325
+
326
+ /**
327
+ * Adds a function to the loop. This is just like a task, but allows extra arguments.
328
+ * @param fn The main function to add, which receives the arguments passed
329
+ * @param args Any arguments to pass to the function
330
+ * @returns The created task
331
+ */
332
+ addFunction<TArgs extends any[]>(
333
+ fn: (...args: TArgs) => void,
334
+ ...args: TArgs
335
+ ): Task;
336
+
337
+ /**
338
+ * Adds a function to the loop in builder style.
339
+ * @param fn The main function to add
340
+ * @param args Any arguments to pass to the function
341
+ * @returns This Taskmaster instance for chaining
342
+ */
343
+ func<TArgs extends any[]>(
344
+ fn: (...args: TArgs) => void,
345
+ ...args: TArgs
346
+ ): this;
347
+
348
+ /**
349
+ * Adds an event listener to the loop. This is a special task that calls a
350
+ * function whenever an event is triggered.
351
+ * @param name The name of the event to listen for
352
+ * @param fn The function to call for each event
353
+ * @returns The created task
354
+ */
355
+ addEventListener(
356
+ name: string,
357
+ fn: (event: string, ...args: any[]) => void
358
+ ): Task;
359
+
360
+ /**
361
+ * Adds an event listener to the loop in builder style.
362
+ * @param name The name of the event to listen for
363
+ * @param fn The function to call for each event
364
+ * @returns This Taskmaster instance for chaining
365
+ */
366
+ eventListener(
367
+ name: string,
368
+ fn: (event: string, ...args: any[]) => void
369
+ ): this;
370
+
371
+ /**
372
+ * Adds a task that triggers a function repeatedly after an interval.
373
+ * The function may modify or cancel the interval through a return value.
374
+ * @param timeout The initial interval to run the function after
375
+ * @param fn The function to call.
376
+ * - If returns a number, that number replaces the timeout
377
+ * - If returns a number <= 0, the timer is canceled
378
+ * - If returns null/undefined, the timeout remains the same
379
+ * @returns The created task
380
+ */
381
+ addTimer(timeout: number, fn: () => number | null | undefined | void): Task;
382
+
383
+ /**
384
+ * Adds a timer task in builder style.
385
+ * @param timeout The initial interval to run the function after
386
+ * @param fn The function to call
387
+ * @returns This Taskmaster instance for chaining
388
+ */
389
+ timer(timeout: number, fn: () => number | null | undefined | void): this;
390
+
391
+ /**
392
+ * Creates a new queue. A queue allows pushing items for other tasks to pop,
393
+ * functioning similarly to the CC event queue but without a size limit.
394
+ * @returns The new queue
395
+ */
396
+ createQueue<T = any>(): Queue<T>;
397
+
398
+ /**
399
+ * Creates a new multi queue. A multi queue is a combination of queues for each
400
+ * task that subscribes, with pushed items being distributed to all tasks.
401
+ * @returns The new multi queue
402
+ */
403
+ createMultiQueue<T = any>(): MultiQueue<T>;
404
+
405
+ /**
406
+ * Sets a blacklist for events to send to all tasks.
407
+ * Tasks can override this with their own blacklist.
408
+ * @param list A list of events to not send to any task
409
+ */
410
+ setEventBlacklist(list: string[] | null): void;
411
+
412
+ /**
413
+ * Sets a whitelist for events to send to all tasks.
414
+ * Tasks can override this with their own whitelist.
415
+ * @param list A list of events to send to all tasks (others are discarded)
416
+ */
417
+ setEventWhitelist(list: string[] | null): void;
418
+
419
+ /**
420
+ * Sets a function that is used to transform events. This function takes a task
421
+ * and event table, and may modify the event table to adjust the event for that task.
422
+ * @param fn A function to use to transform events
423
+ */
424
+ setEventTransformer(fn: ((task: Task, event: any[]) => void) | null): void;
425
+
426
+ /**
427
+ * Sets a function to call before yielding. This can be used to reset state such
428
+ * as terminal cursor position.
429
+ * @param fn The function to call
430
+ */
431
+ setPreYieldHook(fn: (() => void) | null): void;
432
+
433
+ /**
434
+ * Sets the maximum number of tasks that can run concurrently.
435
+ * @param num The maximum number of tasks to run (default unlimited)
436
+ */
437
+ setMaxConcurrent(num: number | null): void;
438
+
439
+ /**
440
+ * Runs the main loop, processing events and running each task.
441
+ * @param count The number of tasks that can exit before stopping the loop
442
+ */
443
+ run(count?: number): void;
444
+
445
+ /**
446
+ * Runs all tasks until a single task exits.
447
+ */
448
+ waitForAny(): void;
449
+
450
+ /**
451
+ * Runs all tasks until all tasks exit.
452
+ */
453
+ waitForAll(): void;
454
+
455
+ /**
456
+ * Stops the main loop if it is running.
457
+ * This will yield if called from a running task.
458
+ */
459
+ stop(): void;
460
+ }
461
+
462
+ /**
463
+ * Creates a new Taskmaster run loop.
464
+ * @param tasks Any tasks to add to the loop
465
+ * @returns The new Taskmaster instance
466
+ */
467
+ export declare function createTaskmaster(
468
+ ...tasks: Array<() => void>
469
+ ): Taskmaster;
package/index.lua ADDED
@@ -0,0 +1,738 @@
1
+ -- Taskmaster: A simple and highly flexible task runner/coroutine manager for ComputerCraft
2
+ -- Supports adding/removing tasks, early exits for tasks, event white/blacklists, automatic
3
+ -- terminal redirection, task pausing, promises, and more.
4
+ -- Made by JackMacWindows
5
+ -- Licensed under CC0 in the public domain
6
+
7
+ --[[
8
+ Examples:
9
+ - Run three functions in parallel, and wait for any to exit.
10
+ require("taskmaster")(
11
+ func1, func2, func3
12
+ ):waitForAny()
13
+
14
+ - Run three functions in parallel, and wait for all to exit.
15
+ require("taskmaster")(
16
+ func1, func2, func3
17
+ ):waitForAll()
18
+ - Builder-style creation of three event listeners for keyboard events.
19
+ require("taskmaster")()
20
+ :eventListener("key", function(ev, key) print("Key:", keys.getName(key)) end)
21
+ :eventListener("key_up", function(ev, key) print("Key up:", keys.getName(key)) end)
22
+ :eventListener("char", function(ev, char) print("Character:", char) end)
23
+ :run()
24
+ - Create a loop with two background tasks (which don't receive user interaction events) and one foreground task.
25
+ The foreground task may exit itself if a specific character is pressed.
26
+ local loop = require("taskmaster")()
27
+ loop:setEventBlacklist {"key", "key_up", "char", "paste", "mouse_click", "mouse_up", "mouse_scroll", "mouse_drag"}
28
+ loop:addTask(bgFunc)
29
+ loop:addTimer(2, pollingFunction)
30
+ local function fgFunc(task)
31
+ while true do
32
+ local event, p1 = os.pullEvent()
33
+ if event == "char" and p1 == "q" then
34
+ task:remove()
35
+ end
36
+ end
37
+ end
38
+ local task = loop:addTask(fgFunc)
39
+ task:setEventBlacklist {}
40
+ task:setPriority(10)
41
+ loop:run()
42
+
43
+ - Fetch a remote JSON resource in parallel using promises.
44
+ local loop = require("taskmaster")()
45
+ local function main()
46
+ loop.Promise.fetch("https://httpbin.org/headers")
47
+ :next(function(handle) return handle.json() end)
48
+ :next(function(data) print(data.headers["User-Agent"]) end)
49
+ :catch(printError)
50
+ end
51
+ loop:task(main):run()
52
+ ]]
53
+
54
+ local expect = require "cc.expect"
55
+
56
+ ---@class Task
57
+ ---@field master Taskmaster The event loop for the task
58
+ local Task = {}
59
+ local Task_mt = {__name = "Task", __index = Task}
60
+
61
+ --- Pauses the task, preventing it from running. This will yield if the task calls this method on itself.
62
+ function Task:pause()
63
+ self.paused = true
64
+ if self.master.currentTask == self then coroutine.yield() end
65
+ end
66
+
67
+ --- Unpauses the task if it was previously paused by @{Task.pause}.
68
+ function Task:unpause()
69
+ self.paused = false
70
+ end
71
+
72
+ --- Removes the task from the run loop, as if it returned. This will yield if the task calls this method on itself.
73
+ function Task:remove()
74
+ self.master.dead[#self.master.dead+1] = self
75
+ self.paused = true
76
+ if self.master.currentTask == self then coroutine.yield() end
77
+ end
78
+
79
+ --- Sets the priority of the task. This determines the order tasks are run in.
80
+ ---@param priority number The priority of the task (0 is the default)
81
+ function Task:setPriority(priority)
82
+ expect(1, priority, "number")
83
+ self.priority = priority
84
+ self.master.shouldSort = true
85
+ end
86
+
87
+ --- Sets a blacklist for events to send to this task.
88
+ ---@param list? string[] A list of events to not send to this task
89
+ function Task:setEventBlacklist(list)
90
+ if expect(1, list, "table", "nil") then
91
+ self.blacklist = {}
92
+ for _, v in ipairs(list) do self.blacklist[v] = true end
93
+ else self.blacklist = nil end
94
+ end
95
+
96
+ --- Sets a whitelist for events to send to this task.
97
+ ---@param list? string[] A list of events to send to this task (others are discarded)
98
+ function Task:setEventWhitelist(list)
99
+ if expect(1, list, "table", "nil") then
100
+ self.whitelist = {}
101
+ for _, v in ipairs(list) do self.whitelist[v] = true end
102
+ else self.whitelist = nil end
103
+ end
104
+
105
+ --- Sets an error handler for a task.
106
+ ---@param errh? fun(err: any, task: Task) A function to call if the task throws an error
107
+ function Task:setErrorHandler(errh)
108
+ self.errh = expect(1, errh, "function", "nil")
109
+ end
110
+
111
+ ---@class Promise
112
+ ---@field private task Task
113
+ ---@field private resolve fun(...: any)|nil
114
+ ---@field private reject fun(err: any)|nil
115
+ ---@field private final fun()|nil
116
+ local Promise = {}
117
+ local Promise_mt = {__name = "Promise", __index = Promise}
118
+
119
+ --- Creates a new Promise on the selected run loop.
120
+ ---@param loop Taskmaster The loop to create the promise on
121
+ ---@param fn fun(resolve: fun(...: any), reject: fun(err: any)) The main function for the promise
122
+ ---@return Promise promise The new promise
123
+ function Promise:new(loop, fn)
124
+ expect(1, loop, "table")
125
+ expect(2, fn, "function")
126
+ local obj = setmetatable({}, Promise_mt)
127
+ obj.task = loop:addTask(function()
128
+ local ok, err = pcall(fn,
129
+ function(...) if obj.resolve then return obj.resolve(...) end end,
130
+ function(err)
131
+ while obj do
132
+ if obj.reject then return obj.reject(err) end
133
+ obj = obj.next_promise
134
+ end
135
+ end
136
+ )
137
+ if not ok and obj.reject then obj.reject(err) end
138
+ end)
139
+ return obj
140
+ end
141
+
142
+ --- Creates a new Promise that resolves once all of the listed promises resolve.
143
+ ---@param loop Taskmaster The loop to create the promise on
144
+ ---@param list Promise[] The promises to wait for
145
+ ---@return Promise promise The new promise
146
+ function Promise:all(loop, list)
147
+ expect(1, loop, "table")
148
+ expect(2, list, "table")
149
+ return Promise:new(loop, function(resolve, reject)
150
+ local count = 0
151
+ for _, v in ipairs(list) do
152
+ v:next(function(...)
153
+ count = count + 1
154
+ if count == #list then resolve(...) end
155
+ end, reject)
156
+ end
157
+ end)
158
+ end
159
+
160
+ --- Creates a new Promise that resolves once any of the listed promises resolve, or rejects if all promises reject.
161
+ ---@param loop Taskmaster The loop to create the promise on
162
+ ---@param list Promise[] The promises to wait for
163
+ ---@return Promise promise The new promise
164
+ function Promise:any(loop, list)
165
+ expect(1, loop, "table")
166
+ expect(2, list, "table")
167
+ return Promise:new(loop, function(resolve, reject)
168
+ local count = 0
169
+ for _, v in ipairs(list) do
170
+ v:next(resolve, function(err)
171
+ count = count + 1
172
+ if count == #list then reject(err) end
173
+ end)
174
+ end
175
+ end)
176
+ end
177
+
178
+ --- Creates a new Promise that resolves once any of the listed promises resolve.
179
+ ---@param loop Taskmaster The loop to create the promise on
180
+ ---@param list Promise[] The promises to wait for
181
+ ---@return Promise promise The new promise
182
+ function Promise:race(loop, list)
183
+ expect(1, loop, "table")
184
+ expect(2, list, "table")
185
+ return Promise:new(loop, function(resolve, reject)
186
+ for _, v in ipairs(list) do v:next(resolve, reject) end
187
+ end)
188
+ end
189
+
190
+ --- Creates a new Promise that immediately resolves to a value.
191
+ ---@param loop Taskmaster The loop to create the promise on
192
+ ---@param val any The value to resolve to
193
+ ---@return Promise promise The new promise
194
+ function Promise:_resolve(loop, val)
195
+ expect(1, loop, "table")
196
+ local obj = setmetatable({}, Promise_mt)
197
+ obj.task = loop:addTask(function()
198
+ if obj.resolve then obj.resolve(val) end
199
+ end)
200
+ return obj
201
+ end
202
+
203
+ --- Creates a new Promise that immediately rejects with an error.
204
+ ---@param loop Taskmaster The loop to create the promise on
205
+ ---@param err any The value to resolve to
206
+ ---@return Promise promise The new promise
207
+ function Promise:_reject(loop, err)
208
+ expect(1, loop, "table")
209
+ local obj = setmetatable({}, Promise_mt)
210
+ obj.task = loop:addTask(function()
211
+ if obj.reject then obj.reject(err) end
212
+ end)
213
+ return obj
214
+ end
215
+
216
+ --- Adds a function to call when the promise resolves.
217
+ ---@param fn fun(...: any): Promise|nil The function to call
218
+ ---@param err? fun(err: any) A function to catch errors
219
+ ---@return Promise next The next promise in the chain
220
+ function Promise:next(fn, err)
221
+ expect(1, fn, "function")
222
+ expect(2, err, "function", "nil")
223
+ self.resolve = function(...)
224
+ self.resolve = nil
225
+ local res = fn(...)
226
+ if self.next_promise then
227
+ if type(res) == "table" and getmetatable(res) == Promise_mt then
228
+ for k, v in pairs(self.next_promise) do res[k] = v end
229
+ self.next_promise = res
230
+ else
231
+ self.next_promise.resolve(res)
232
+ end
233
+ end
234
+ if self.final then self.final() end
235
+ end
236
+ if err then self.reject = function(v) self.reject = nil err(v) if self.final then self.final() end end end
237
+ self.next_promise = setmetatable({}, Promise_mt)
238
+ return self.next_promise
239
+ end
240
+ Promise.Then = Promise.next
241
+
242
+ --- Sets the error handler for the promise.
243
+ ---@param fn fun(err: any) The error handler to use
244
+ ---@return Promise self
245
+ function Promise:catch(fn)
246
+ expect(1, fn, "function")
247
+ self.reject = function(err) self.reject = nil fn(err) if self.final then self.final() end end
248
+ return self
249
+ end
250
+
251
+ --- Sets a function to call after the promise settles.
252
+ ---@param fn fun() The function to call
253
+ ---@return Promise self
254
+ function Promise:finally(fn)
255
+ expect(1, fn, "function")
256
+ self.final = function() self.final = nil return fn() end
257
+ return self
258
+ end
259
+
260
+ ---@diagnostic disable: missing-return
261
+
262
+ ---@class PromiseConstructor
263
+ local PromiseConstructor = {}
264
+
265
+ --- Creates a new Promise on the selected run loop.
266
+ ---@param fn fun(resolve: fun(...: any), reject: fun(err: any)) The main function for the promise
267
+ ---@return Promise promise The new promise
268
+ function PromiseConstructor.new(fn) end
269
+
270
+ --- Creates a new Promise that resolves once all of the listed promises resolve.
271
+ ---@param list Promise[] The promises to wait for
272
+ ---@return Promise promise The new promise
273
+ function PromiseConstructor.all(list) end
274
+
275
+ --- Creates a new Promise that resolves once any of the listed promises resolve, or rejects if all promises reject.
276
+ ---@param list Promise[] The promises to wait for
277
+ ---@return Promise promise The new promise
278
+ function PromiseConstructor.any(list) end
279
+
280
+ --- Creates a new Promise that resolves once any of the listed promises resolve.
281
+ ---@param list Promise[] The promises to wait for
282
+ ---@return Promise promise The new promise
283
+ function PromiseConstructor.race(list) end
284
+
285
+ --- Creates a new Promise that immediately resolves to a value.
286
+ ---@param val any The value to resolve to
287
+ ---@return Promise promise The new promise
288
+ function PromiseConstructor.resolve(val) end
289
+
290
+ --- Creates a new Promise that immediately rejects with an error.
291
+ ---@param err any The value to resolve to
292
+ ---@return Promise promise The new promise
293
+ function PromiseConstructor.reject(err) end
294
+
295
+ --- Makes an HTTP request to a URL, and returns a Promise for the result.
296
+ --- The promise will resolve with the handle to the response, which will also
297
+ --- have the following methods:
298
+ --- - res.text(): Returns a promise that resolves to the body of the response.
299
+ --- - res.table(): Returns a promise that resolves to the body unserialized as a Lua table.
300
+ --- - res.json(): Returns a promise that resolves to the body unserialized as JSON.
301
+ ---@param url string The URL to connect to
302
+ ---@param body? string If specified, a POST body to send
303
+ ---@param headers? table<string, string> Any HTTP headers to add to the request
304
+ ---@param binary? boolean Whether to send in binary mode (deprecated as of CC:T 1.109.0)
305
+ ---@overload fun(options: {url: string, body?: string, headers?: string, method?: string, binary?: string, timeout?: number}): Promise
306
+ ---@return Promise promise The new promise
307
+ function PromiseConstructor.fetch(url, body, headers, binary) end
308
+
309
+ ---@class Queue
310
+ ---@field private loop Taskmaster
311
+ ---@field private head number
312
+ ---@field private tail number
313
+ ---@field private waiting table<Task, boolean>
314
+ ---@field private currentAwait any
315
+ ---@field private lastAwait any
316
+ local Queue = {}
317
+ local Queue_mt = {__name = "Queue", __index = Queue}
318
+
319
+ --- Returns the length of the queue.
320
+ ---@return number length The length of the queue
321
+ function Queue:length()
322
+ return self.tail - self.head
323
+ end
324
+ Queue_mt.__len = Queue.length
325
+
326
+ --- Returns whether the queue is empty.
327
+ ---@return boolean empty Whether the queue is empty
328
+ function Queue:empty()
329
+ return self.head == self.tail
330
+ end
331
+
332
+ --- Returns the item in the front of the queue, or nil if the queue is empty.
333
+ ---@return any|nil item The head of the queue
334
+ function Queue:peek()
335
+ return self[self.head]
336
+ end
337
+
338
+ --- Pushes an item to the end of the queue. This will safely awaken any tasks
339
+ --- that are awaiting this queue. Note that if multiple tasks are awaiting the
340
+ --- queue, only one of them will pop this item - if more items are pushed, the
341
+ --- other tasks may pop those, otherwise they will continue waiting.
342
+ ---@param item any The item to push
343
+ function Queue:push(item)
344
+ self[self.tail] = item
345
+ self.tail = self.tail + 1
346
+ if next(self.waiting) and self.currentAwait ~= self.lastAwait then
347
+ os.queueEvent("taskmaster_queue_" .. tostring(self))
348
+ self.lastAwait = self.currentAwait
349
+ end
350
+ end
351
+
352
+ --- Returns the next item in the queue and removes it. If the queue is empty,
353
+ --- the task waits for an item to be pushed. Note that if multiple tasks are
354
+ --- awaiting the queue, this may not always return the *next* item, as another
355
+ --- task may pop it before this task runs.
356
+ ---
357
+ --- This must be called by a task in the owning loop.
358
+ ---@return any item The item that was popped
359
+ function Queue:pop()
360
+ while self.head == self.tail do
361
+ if self.loop.currentTask == nil then error("This function must be called inside a running task", 2) end
362
+ self.waiting[self.loop.currentTask] = true
363
+ self.currentAwait = {}
364
+ os.pullEvent("taskmaster_queue_" .. tostring(self))
365
+ self.waiting[self.loop.currentTask] = nil
366
+ end
367
+ local retval = self[self.head]
368
+ self[self.head] = nil
369
+ self.head = self.head + 1
370
+ return retval
371
+ end
372
+
373
+ -- TODO: Multi queues should probably be able to share events. This'll break for
374
+ -- more than 256 tasks sharing a multiqueue.
375
+
376
+ ---@class MultiQueue
377
+ ---@field private loop Taskmaster
378
+ ---@field private queues table<Task, Queue>
379
+ local MultiQueue = {}
380
+ local MultiQueue_mt = {__name = "MultiQueue", __index = MultiQueue}
381
+
382
+ --- Initializes a queue for the current task, allowing items to be pushed for
383
+ --- this task. This is called automatically by `pop` as well.
384
+ ---
385
+ --- This must be called by a task in the owning loop.
386
+ function MultiQueue:init()
387
+ if not self.queues[self.loop.currentTask] then
388
+ self.queues[self.loop.currentTask] = self.loop:createQueue()
389
+ end
390
+ end
391
+
392
+ --- Pushes an item to the end of all queues. This will safely awaken any tasks
393
+ --- that are awaiting this queue.
394
+ ---@param item any The item to push
395
+ function MultiQueue:push(item)
396
+ for _, v in pairs(self.queues) do v:push(item) end
397
+ end
398
+
399
+ --- Returns the next item in the queue and removes it. If the queue is empty,
400
+ --- the task waits for an item to be pushed.
401
+ ---
402
+ --- This must be called by a task in the owning loop.
403
+ ---@return any item The item that was popped
404
+ function MultiQueue:pop()
405
+ local q = self.queues[self.loop.currentTask]
406
+ if not q then
407
+ q = self.loop:createQueue()
408
+ self.queues[self.loop.currentTask] = q
409
+ end
410
+ return q:pop()
411
+ end
412
+
413
+ ---@diagnostic enable: missing-return
414
+
415
+ ---@class Taskmaster
416
+ ---@field Promise PromiseConstructor
417
+ local Taskmaster = {}
418
+ local Taskmaster_mt = {__name = "Taskmaster", __index = Taskmaster}
419
+
420
+ --- Adds a task to the loop.
421
+ ---@param fn fun(Task) The main function to add, which receives the task as an argument
422
+ ---@return Task task The created task
423
+ function Taskmaster:addTask(fn)
424
+ expect(1, fn, "function")
425
+ local task = setmetatable({coro = coroutine.create(fn), master = self, priority = 0}, Task_mt)
426
+ self.new[#self.new+1] = task
427
+ self.shouldSort = true
428
+ return task
429
+ end
430
+
431
+ --- Adds a task to the loop in builder style.
432
+ ---@param fn fun(Task) The main function to add
433
+ ---@return Taskmaster self
434
+ function Taskmaster:task(fn) self:addTask(fn) return self end
435
+
436
+ --- Adds a function to the loop. This is just like a task, but allows extra arguments.
437
+ ---@param fn function The main function to add, which receives the arguments passed
438
+ ---@param ... any Any arguments to pass to the function
439
+ ---@return Task task The created task
440
+ function Taskmaster:addFunction(fn, ...)
441
+ expect(1, fn, "function")
442
+ local args = table.pack(...)
443
+ local task = setmetatable({coro = coroutine.create(function() return fn(table.unpack(args, 1, args.n)) end), master = self, priority = 0}, Task_mt)
444
+ self.new[#self.new+1] = task
445
+ self.shouldSort = true
446
+ return task
447
+ end
448
+
449
+ --- Adds a function to the loop in builder style.
450
+ ---@param fn function The main function to add
451
+ ---@param ... any Any arguments to pass to the function
452
+ ---@return Taskmaster self
453
+ function Taskmaster:func(fn, ...) self:addFunction(fn, ...) return self end
454
+
455
+ --- Adds an event listener to the loop. This is a special task that calls a function whenever an event is triggered.
456
+ ---@param name string The name of the event to listen for
457
+ ---@param fn fun(string, ...) The function to call for each event
458
+ ---@return Task task The created task
459
+ function Taskmaster:addEventListener(name, fn)
460
+ expect(1, name, "string")
461
+ expect(2, fn, "function")
462
+ local task = setmetatable({coro = coroutine.create(function() while true do fn(os.pullEvent(name)) end end), master = self, priority = 0}, Task_mt)
463
+ self.new[#self.new+1] = task
464
+ self.shouldSort = true
465
+ return task
466
+ end
467
+
468
+ --- Adds an event listener to the loop in builder style. This is a special task that calls a function whenever an event is triggered.
469
+ ---@param name string The name of the event to listen for
470
+ ---@param fn fun(string, ...) The function to call for each event
471
+ ---@return Taskmaster self
472
+ function Taskmaster:eventListener(name, fn) self:addEventListener(name, fn) return self end
473
+
474
+ --- Adds a task that triggers a function repeatedly after an interval. The function may modify or cancel the interval through a return value.
475
+ ---@param timeout number The initial interval to run the function after
476
+ ---@param fn fun():number|nil The function to call.
477
+ ---If this returns a number, that number replaces the timeout.
478
+ ---If this returns a number less than or equal to 0, the timer is canceled.
479
+ ---If this returns nil, the timeout remains the same.
480
+ ---@return Task task The created task
481
+ function Taskmaster:addTimer(timeout, fn)
482
+ expect(1, timeout, "number")
483
+ expect(2, fn, "function")
484
+ local task = setmetatable({coro = coroutine.create(function()
485
+ while true do
486
+ sleep(timeout)
487
+ timeout = fn() or timeout
488
+ if timeout <= 0 then return end
489
+ end
490
+ end), master = self, priority = 0}, Task_mt)
491
+ self.new[#self.new+1] = task
492
+ self.shouldSort = true
493
+ return task
494
+ end
495
+
496
+ --- Adds a task that triggers a function repeatedly after an interval in builder style. The function may modify or cancel the interval through a return value.
497
+ ---@param timeout number The initial interval to run the function after
498
+ ---@param fn fun():number|nil The function to call.
499
+ ---If this returns a number, that number replaces the timeout.
500
+ ---If this returns a number less than or equal to 0, the timer is canceled.
501
+ ---If this returns nil, the timeout remains the same.
502
+ ---@return Taskmaster self
503
+ function Taskmaster:timer(timeout, fn) self:addTimer(timeout, fn) return self end
504
+
505
+ --- Creates a new queue. A queue allows pushing items for other tasks to pop,
506
+ --- functioning similarly to the CC event queue but without a size limit.
507
+ ---
508
+ --- A queue may have items of any type pushed to its tail, from inside a task or
509
+ --- outside the loop. Tasks may pop those items from the queue, and they will
510
+ --- wait until the queue has an item to return it (if the queue has an item
511
+ --- already, it returns immediately without waiting). Smart logic is in place to
512
+ --- minimize the number of events required to awaken waiting tasks.
513
+ ---
514
+ --- Be aware there is a significant difference in behavior from the event queue
515
+ --- across multiple tasks: items in the queue are not duplicated across tasks,
516
+ --- so if multiple tasks are awaiting a new item, only one task will receive the
517
+ --- next item, and it will immediately be removed. The task which receives this
518
+ --- item is undefined. If multiple items are queued without yielding, they will
519
+ --- be distributed across tasks (again, which tasks and their order are
520
+ --- undefined). If you need a queue that distributes across awaiting tasks, use
521
+ --- a multi queue.
522
+ ---
523
+ --- This queue is bound to the Taskmaster loop. Items may be pushed from any
524
+ --- context (provided any awakening events don't get lost elsewhere), but
525
+ --- popping may only be done by a task running under this loop.
526
+ ---@return Queue queue The new queue
527
+ function Taskmaster:createQueue()
528
+ return setmetatable({loop = self, head = 1, tail = 1, waiting = setmetatable({}, {__mode = "k"})}, Queue_mt)
529
+ end
530
+
531
+ --- Creates a new multi queue. A multi queue is a combination of queues for each
532
+ --- task that subscribes, with pushed items being distributed to all tasks. It
533
+ --- functions similarly to a normal queue, but each task gets every item pushed.
534
+ ---@return MultiQueue queue The new multi queue.
535
+ function Taskmaster:createMultiQueue()
536
+ return setmetatable({loop = self, queues = setmetatable({}, {__mode = "k"})}, MultiQueue_mt)
537
+ end
538
+
539
+ --- Sets a blacklist for events to send to all tasks. Tasks can override this with their own blacklist.
540
+ ---@param list? string[] A list of events to not send to any task
541
+ function Taskmaster:setEventBlacklist(list)
542
+ if expect(1, list, "table", "nil") then
543
+ self.blacklist = {}
544
+ for _, v in ipairs(list) do self.blacklist[v] = true end
545
+ else self.blacklist = nil end
546
+ end
547
+
548
+ --- Sets a whitelist for events to send to all tasks. Tasks can override this with their own whitelist.
549
+ ---@param list? string[] A list of events to send to all tasks (others are discarded)
550
+ function Taskmaster:setEventWhitelist(list)
551
+ if expect(1, list, "table", "nil") then
552
+ self.whitelist = {}
553
+ for _, v in ipairs(list) do self.whitelist[v] = true end
554
+ else self.whitelist = nil end
555
+ end
556
+
557
+ --- Sets a function that is used to transform events. This function takes a task
558
+ --- and event table, and may modify the event table to adjust the event for that task.
559
+ ---@param fn fun(Task, table)|nil A function to use to transform events
560
+ function Taskmaster:setEventTransformer(fn)
561
+ expect(1, fn, "function", "nil")
562
+ self.transformer = fn
563
+ end
564
+
565
+ --- Sets a function to call before yielding. This can be used to reset state such
566
+ --- as terminal cursor position.
567
+ ---@param fn? fun() The function to call
568
+ function Taskmaster:setPreYieldHook(fn)
569
+ expect(1, fn, "function", "nil")
570
+ self.preYieldHook = fn
571
+ end
572
+
573
+ --- Sets the maximum number of tasks that can run concurrently.
574
+ ---@param num? number The maximum number of tasks to run (default unlimited)
575
+ function Taskmaster:setMaxConcurrent(num)
576
+ expect(1, num, "number", "nil")
577
+ self.maxConcurrent = num
578
+ end
579
+
580
+ --- Runs the main loop, processing events and running each task.
581
+ ---@param count? number The number of tasks that can exit before stopping the loop
582
+ function Taskmaster:run(count)
583
+ count = expect(1, count, "number", "nil") or math.huge
584
+ self.running = true
585
+ while self.running and (#self.tasks + #self.new) > 0 and count > 0 do
586
+ self.dead = {}
587
+ if not (self.maxConcurrent and #self.tasks >= self.maxConcurrent) then
588
+ local nextnew = {}
589
+ for i, task in ipairs(self.new) do
590
+ if self.maxConcurrent and #self.tasks >= self.maxConcurrent then
591
+ nextnew[#nextnew+1] = task
592
+ else
593
+ self.currentTask = task
594
+ local old = term.current()
595
+ local ok, filter = coroutine.resume(task.coro, task)
596
+ task.window = term.redirect(old)
597
+ if not ok then
598
+ self.currentTask = nil
599
+ self.running = false
600
+ self.new = {table.unpack(self.new, i + 1)}
601
+ return error(filter, 0)
602
+ end
603
+ task.filter = filter
604
+ if coroutine.status(task.coro) == "dead" then count = count - 1
605
+ else self.tasks[#self.tasks+1], self.shouldSort = task, true end
606
+ end
607
+ if not self.running or count <= 0 then break end
608
+ end
609
+ self.new = nextnew
610
+ end
611
+ if self.shouldSort then table.sort(self.tasks, function(a, b) return a.priority > b.priority end) self.shouldSort = false end
612
+ if self.running and #self.tasks > 0 and count > 0 then
613
+ if self.preYieldHook then self.preYieldHook() end
614
+ local _ev = table.pack(os.pullEventRaw())
615
+ for i, task in ipairs(self.tasks) do
616
+ local ev = _ev
617
+ if self.transformer then
618
+ ev = table.pack(table.unpack(_ev, 1, _ev.n))
619
+ self.transformer(task, ev)
620
+ end
621
+ local wl, bl = task.whitelist or self.whitelist, task.blacklist or self.blacklist
622
+ if not task.paused and
623
+ (task.filter == nil or task.filter == ev[1] or ev[1] == "terminate") and
624
+ (not bl or not bl[ev[1]]) and
625
+ (not wl or wl[ev[1]]) then
626
+ self.currentTask = task
627
+ local old = term.redirect(task.window)
628
+ local ok, filter = coroutine.resume(task.coro, table.unpack(ev, 1, ev.n))
629
+ task.window = term.redirect(old)
630
+ if not ok then
631
+ if task.errh then
632
+ task.errh(filter, task)
633
+ else
634
+ self.currentTask = nil
635
+ self.running = false
636
+ table.remove(self.tasks, i)
637
+ return error(filter, 0)
638
+ end
639
+ end
640
+ task.filter = filter
641
+ if coroutine.status(task.coro) == "dead" then self.dead[#self.dead+1] = task end
642
+ if not self.running or #self.dead >= count then break end
643
+ end
644
+ end
645
+ end
646
+ self.currentTask = nil
647
+ for _, task in ipairs(self.dead) do
648
+ for i, v in ipairs(self.tasks) do
649
+ if v == task then
650
+ table.remove(self.tasks, i)
651
+ count = count - 1
652
+ break
653
+ end
654
+ end
655
+ end
656
+ end
657
+ self.running = false
658
+ end
659
+
660
+ --- Runs all tasks until a single task exits.
661
+ function Taskmaster:waitForAny() return self:run(1) end
662
+ --- Runs all tasks until all tasks exit.
663
+ function Taskmaster:waitForAll() return self:run() end
664
+
665
+ --- Stops the main loop if it is running. This will yield if called from a running task.
666
+ function Taskmaster:stop()
667
+ self.running = false
668
+ if self.currentTask then coroutine.yield() end
669
+ end
670
+
671
+ Taskmaster_mt.__call = Taskmaster.run
672
+
673
+ local function fetch(loop, url, ...)
674
+ local ok, err = http.request(url, ...)
675
+ if not ok then return Promise:_reject(loop, err) end
676
+ return loop.Promise.new(function(resolve, reject)
677
+ while true do
678
+ local event, p1, p2, p3 = os.pullEvent()
679
+ if event == "http_success" and p1 == url then
680
+ p2.text = function()
681
+ return loop.Promise.new(function(_resolve, _reject)
682
+ local data = p2.readAll()
683
+ p2.close()
684
+ _resolve(data)
685
+ end)
686
+ end
687
+ p2.json = function()
688
+ return loop.Promise.new(function(_resolve, _reject)
689
+ local data = p2.readAll()
690
+ p2.close()
691
+ local d = textutils.unserializeJSON(data)
692
+ if d ~= nil then _resolve(d)
693
+ else _reject("Failed to parse JSON") end
694
+ end)
695
+ end
696
+ p2.table = function()
697
+ return loop.Promise.new(function(_resolve, _reject)
698
+ local data = p2.readAll()
699
+ p2.close()
700
+ local d = textutils.unserialize(data)
701
+ if d ~= nil then _resolve(d)
702
+ else _reject("Failed to parse Lua table") end
703
+ end)
704
+ end
705
+ return resolve(p2)
706
+ elseif event == "http_failure" and p1 == url then
707
+ if p3 then p3.close() end
708
+ return reject(p2)
709
+ end
710
+ end
711
+ end)
712
+ end
713
+
714
+ --- Creates a new Taskmaster run loop.
715
+ ---@param ... fun() Any tasks to add to the loop
716
+ ---@return Taskmaster loop The new Taskmaster
717
+ function createTaskmaster(...)
718
+ local loop = setmetatable({tasks = {}, dead = {}, new = {}}, Taskmaster_mt)
719
+ for i, v in ipairs{...} do
720
+ expect(i, v, "function")
721
+ loop:addTask(v)
722
+ end
723
+ loop.Promise = {
724
+ new = function(fn) return Promise:new(loop, fn) end,
725
+ all = function(list) return Promise:all(loop, list) end,
726
+ any = function(list) return Promise:any(loop, list) end,
727
+ race = function(list) return Promise:race(loop, list) end,
728
+ resolve = function(val) return Promise:_resolve(loop, val) end,
729
+ reject = function(err) return Promise:_reject(loop, err) end,
730
+ fetch = function(...) return fetch(loop, ...) end
731
+ }
732
+ setmetatable(loop.Promise, {__call = function(self, ...) return Promise:new(loop, ...) end})
733
+ return loop
734
+ end
735
+
736
+ return {
737
+ createTaskmaster = createTaskmaster
738
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@siredvin/taskmaster-ts",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript type definitions for taskmaster library",
5
+ "types": "./index.d.ts",
6
+ "files": [
7
+ "./index.d.ts",
8
+ "./index.lua"
9
+ ],
10
+ "main": "index",
11
+ "author": "SirEdvin",
12
+ "license": "MIT",
13
+ "scripts": {
14
+ "lint": "eslint . --ext .ts,.js",
15
+ "depcheck": "depcheck"
16
+ },
17
+ "nx": {
18
+ "targets": {
19
+ "lint": {
20
+ "executor": "@nx/linter:eslint",
21
+ "outputs": [
22
+ "{options.outputFile}"
23
+ ],
24
+ "options": {
25
+ "lintFilePatterns": [
26
+ "packages/taskmaster-ts/**/*.{ts,tsx,js,jsx}"
27
+ ]
28
+ }
29
+ },
30
+ "depcheck": {
31
+ "executor": "nx:run-commands",
32
+ "options": {
33
+ "command": "depcheck",
34
+ "cwd": "packages/taskmaster-ts"
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }