keytask-core 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Georg Schilin
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,409 @@
1
+ ![logo](https://raw.githubusercontent.com/voodoofugu/keytask/refs/heads/main/src/assets/banner-logo.png)
2
+
3
+ <h2></h2>
4
+
5
+ ### Table of contents
6
+
7
+ - [About](#about)
8
+ - [Installation](#installation)
9
+ - [API](#api)
10
+ - [Common patterns](#common-patterns)
11
+ - [License](#license)
12
+
13
+ <h2></h2>
14
+
15
+ ### About
16
+
17
+ `keytask` is a tiny TypeScript library for key-based control of timing-sensitive work.
18
+
19
+ It is designed for render-heavy UI, scroll libraries, animation systems and games: places where you need to schedule, replace, lock, loop and cancel work without scattering raw `setTimeout`, `requestAnimationFrame` and cleanup state across the codebase.
20
+
21
+ It is not an animation engine. It does not provide easing, tweens, timelines, physics or rendering. It only controls when work is allowed to run.
22
+
23
+ The core idea is simple - the same task key always describes the same unit of work.
24
+
25
+ <h2></h2>
26
+
27
+ ### Installation
28
+
29
+ ```bash
30
+ npm install keytask-core
31
+ ```
32
+
33
+ ```ts
34
+ import { setTask } from "keytask-core";
35
+ ```
36
+
37
+ > **✦ Note:**
38
+ >
39
+ > - Supports both **ESM** (`import`) and **CommonJS** (`require`) builds.
40
+ > - Supports one shared global manager and isolated local managers.
41
+ > - Written in TypeScript and ships declaration files.
42
+ > - No runtime dependencies.
43
+ > - RAF mode uses `requestAnimationFrame` / `cancelAnimationFrame` when available and falls back to `setTimeout(..., 16)` with a console warning when those APIs are missing.
44
+
45
+ <h2></h2>
46
+
47
+ ### API
48
+
49
+ <ul><div>
50
+
51
+ ###### **— GLOBAL —**
52
+
53
+ <details><summary><b><code>setTask</code></b>: <em>schedule latest one-shot work by timeout or RAF</em></summary><br /><ul><div>
54
+
55
+ <b>Usage:</b><br />
56
+
57
+ ```ts
58
+ setTask(callback, timer, key?);
59
+ ```
60
+
61
+ - `callback` - work to run once.
62
+ - `timer` - timeout in milliseconds or `"raf"`.
63
+ - `key` - optional string key, reusing the same key replaces the previous task state.
64
+
65
+ <br />
66
+
67
+ <b>Description:</b><em><br />
68
+ Schedules one task and returns its key.<br />
69
+ This is useful for "latest wins" work: scroll-end handlers, delayed cleanup, render batching, or any task where only the newest pending callback should survive.<br />
70
+ ✦ A single key can only reference one active task within the same manager instance.
71
+ </em><br />
72
+
73
+ <b>Return:</b><br />
74
+ Returns the passed key, or an automatically generated key when `key` is omitted.
75
+
76
+ ```ts
77
+ const key = setTask(callback, "raf");
78
+
79
+ cancelTask(key);
80
+ ```
81
+
82
+ <b>Example:</b>
83
+
84
+ ```ts
85
+ setTask(
86
+ () => {
87
+ doSomething();
88
+ },
89
+ "raf",
90
+ "my-key",
91
+ );
92
+ ```
93
+
94
+ </div></ul></details>
95
+
96
+ <h2></h2>
97
+
98
+ <details><summary><b><code>setLockTask</code></b>: <em>run now, then lock repeated calls</em></summary><br /><ul><div>
99
+
100
+ <b>Usage:</b><br />
101
+
102
+ ```ts
103
+ setLockTask(callback, timer, key?);
104
+ ```
105
+
106
+ - `callback` - work to run immediately.
107
+ - `timer` - lock duration in milliseconds or `"raf"` for a frame lock.
108
+ - `key` - optional string key, calls are ignored while the key already belongs to any task state.
109
+
110
+ <br />
111
+
112
+ <b>Description:</b><em><br />
113
+ Runs the callback immediately, then blocks repeated calls with the same key until the timeout ends or the next RAF unlocks it.<br />
114
+ This is useful for "first wins" work: frame locks, cooldowns, input guards, or preventing repeated calls from doing too much work in the same frame/window.<br />
115
+ ✦ A single key can only reference one active task within the same manager instance.
116
+ </em><br />
117
+
118
+ <b>Return:</b><br />
119
+ Returns the passed key, or an automatically generated key when `key` is omitted.
120
+
121
+ ```ts
122
+ const key = setLockTask(callback, "raf");
123
+
124
+ cancelTask(key);
125
+ ```
126
+
127
+ <b>Example:</b>
128
+
129
+ ```ts
130
+ setLockTask(
131
+ () => {
132
+ doSomething();
133
+ },
134
+ "raf",
135
+ "my-key",
136
+ );
137
+ ```
138
+
139
+ </div></ul></details>
140
+
141
+ <h2></h2>
142
+
143
+ <details><summary><b><code>setThrottleTask</code></b>: <em>run now, then keep the latest repeated call while throttled</em></summary><br /><ul><div>
144
+
145
+ <b>Usage:</b><br />
146
+
147
+ ```ts
148
+ setThrottleTask(callback, timer, key?);
149
+ ```
150
+
151
+ - `callback` - work to run immediately, or save as the latest trailing call while throttled.
152
+ - `timer` - throttle window in milliseconds or `"raf"` for a frame throttle.
153
+ - `key` - optional string key, repeated calls with the same key keep only the latest trailing callback.
154
+
155
+ <br />
156
+
157
+ <b>Description:</b><em><br />
158
+ Runs the first callback immediately. While the key is throttled, repeated calls do not run immediately. Instead, only the latest callback is saved and runs when the throttle window opens again.<br />
159
+ This is useful for "first now, latest later" work: pointer updates, resize updates, scroll sync, render requests or other cases where dropping the final update would be incorrect.<br />
160
+ ✦ A single key can only reference one active task within the same manager instance.
161
+ </em><br />
162
+
163
+ <b>Return:</b><br />
164
+ Returns the passed key, or an automatically generated key when `key` is omitted.
165
+
166
+ ```ts
167
+ const key = setThrottleTask(callback, "raf");
168
+
169
+ cancelTask(key);
170
+ ```
171
+
172
+ <b>Example:</b>
173
+
174
+ ```ts
175
+ setThrottleTask(
176
+ () => {
177
+ doSomething();
178
+ },
179
+ "raf",
180
+ "my-key",
181
+ );
182
+ ```
183
+
184
+ </div></ul></details>
185
+
186
+ <h2></h2>
187
+
188
+ <details><summary><b><code>setLoopTask</code></b>: <em>run repeated work by timeout or RAF</em></summary><br /><ul><div>
189
+
190
+ <b>Usage:</b><br />
191
+
192
+ ```ts
193
+ setLoopTask(callback, timer, key?);
194
+ ```
195
+
196
+ - `callback` - repeated work. Receives `tick`, `time`, and `delta`.
197
+ - `timer` - interval in milliseconds or `"raf"` for an animation-frame loop.
198
+ - `key` - optional string key, reusing the same key replaces the previous task state.
199
+
200
+ <br />
201
+
202
+ <b>Description:</b><em><br />
203
+ Starts repeated work and returns its key.<br />
204
+ The callback receives a 1-based tick count, current time and delta since the previous tick. Return <code>false</code> to stop the loop.<br />
205
+ ✦ A single key can only reference one active task within the same manager instance.
206
+ </em><br />
207
+
208
+ <b>Return:</b><br />
209
+ Returns the passed key, or an automatically generated key when `key` is omitted.
210
+
211
+ ```ts
212
+ const key = setLoopTask(callback, "raf");
213
+
214
+ cancelTask(key);
215
+ ```
216
+
217
+ <b>Example:</b>
218
+
219
+ ```ts
220
+ setLoopTask(
221
+ (tick, time, delta) => {
222
+ doSomething();
223
+
224
+ if (tick > 10) return false; // exit
225
+ },
226
+ "raf",
227
+ "my-key",
228
+ );
229
+ ```
230
+
231
+ </div></ul></details>
232
+
233
+ <h2></h2>
234
+
235
+ <details><summary><b><code>cancelTask</code></b>: <em>cancel tasks</em></summary><br /><ul><div>
236
+
237
+ <b>Usage:</b><br />
238
+
239
+ ```ts
240
+ cancelTask("key");
241
+ cancelTask(["key1", "key2"]);
242
+ cancelTask(); // clear all tasks
243
+ ```
244
+
245
+ <b>Description:</b><em><br />
246
+ Cancels one, several, or all tasks in the global manager.
247
+ </em><br />
248
+
249
+ </div></ul></details>
250
+
251
+ <h2></h2>
252
+
253
+ <details><summary><b><code>hasTask</code></b>: <em>check tasks</em></summary><br /><ul><div>
254
+
255
+ <b>Usage:</b><br />
256
+
257
+ ```ts
258
+ hasTask("key");
259
+ ```
260
+
261
+ <b>Description:</b><em><br />
262
+ Check pending tasks, active locks, active throttles or active loops.<br />
263
+ Returns <code>true</code> when the key belongs to any active task state in the global manager.
264
+ </em><br />
265
+
266
+ <b>Example:</b>
267
+
268
+ ```ts
269
+ if (hasTask("render")) {
270
+ cancelTask("render");
271
+ }
272
+ ```
273
+
274
+ </div></ul></details>
275
+
276
+ <h2></h2>
277
+
278
+ ###### **— LOCAL —**
279
+
280
+ <details><summary><b><code>createTaskManager</code></b>: <em>create isolated task state</em></summary><br /><ul><div>
281
+
282
+ <b>Usage:</b><br />
283
+
284
+ ```ts
285
+ const tasks = createTaskManager();
286
+ ```
287
+
288
+ The returned manager has the same task methods as the global API, plus `clear()`.
289
+
290
+ <b>Description:</b><em><br />
291
+ Creates a local manager with its own one-shot tasks, locks, throttles and loops.<br />
292
+ Use it for components, scenes, entities or modules that need isolated cleanup. Calling <code>clear()</code> cancels everything inside that local manager without touching the global manager.
293
+ </em>
294
+
295
+ <b>Example:</b>
296
+
297
+ ```ts
298
+ const sceneTasks = createTaskManager();
299
+
300
+ sceneTasks.setLoopTask(callback, "raf", "my-raf-key");
301
+ sceneTasks.setTask(callback, 1000, "my-timeout-key");
302
+
303
+ function destroyScene() {
304
+ sceneTasks.clear();
305
+ }
306
+ ```
307
+
308
+ </div></ul></details>
309
+
310
+ <h2></h2>
311
+
312
+ </div></ul>
313
+
314
+ <h2></h2>
315
+
316
+ ### Common patterns
317
+
318
+ <details><summary><b>Render batching</b>: <em>keep only the latest render request for the next frame</em></summary><br />
319
+
320
+ ```ts
321
+ setTask(
322
+ () => {
323
+ render();
324
+ },
325
+ "raf",
326
+ "render",
327
+ );
328
+ ```
329
+
330
+ </details>
331
+
332
+ <details><summary><b>Debounced work</b>: <em>reuse the same key so only the latest delayed callback runs</em></summary><br />
333
+
334
+ ```ts
335
+ setTask(
336
+ () => {
337
+ saveDraft();
338
+ },
339
+ 300,
340
+ "save-draft",
341
+ );
342
+ ```
343
+
344
+ </details>
345
+
346
+ <details><summary><b>Input cooldown</b>: <em>run once, then ignore repeated calls while locked</em></summary><br />
347
+
348
+ ```ts
349
+ setLockTask(
350
+ () => {
351
+ submitInput();
352
+ },
353
+ 150,
354
+ "input",
355
+ );
356
+ ```
357
+
358
+ </details>
359
+
360
+ <details><summary><b>Latest trailing update</b>: <em>run now, then preserve the latest update during the throttle window</em></summary><br />
361
+
362
+ ```ts
363
+ setThrottleTask(
364
+ () => {
365
+ syncPointerState();
366
+ },
367
+ "raf",
368
+ "pointer",
369
+ );
370
+ ```
371
+
372
+ </details>
373
+
374
+ <details><summary><b>Animation loop</b>: <em>run repeated work until the callback stops it</em></summary><br />
375
+
376
+ ```ts
377
+ setLoopTask(
378
+ (tick, time, delta) => {
379
+ update(delta);
380
+
381
+ if (tick > 10) return false;
382
+ },
383
+ "raf",
384
+ "loop",
385
+ );
386
+ ```
387
+
388
+ </details>
389
+
390
+ <details><summary><b>Local cleanup</b>: <em>isolate task state and clear it on teardown</em></summary><br />
391
+
392
+ ```ts
393
+ const tasks = createTaskManager();
394
+
395
+ tasks.setLoopTask(updateScene, "raf", "loop");
396
+ tasks.setTask(removeHint, 1000, "hint");
397
+
398
+ function destroy() {
399
+ tasks.clear();
400
+ }
401
+ ```
402
+
403
+ </details>
404
+
405
+ <h2></h2>
406
+
407
+ ### License
408
+
409
+ - [MIT](./LICENSE)
@@ -0,0 +1 @@
1
+ "use strict";const e=()=>"undefined"!=typeof performance&&performance.now?performance.now():Date.now();let t=0,a=!1;const n=()=>"key_"+t++,r=()=>"function"!=typeof globalThis.requestAnimationFrame||"function"!=typeof globalThis.cancelAnimationFrame?(a||(a=!0),{requestAnimationFrame:t=>globalThis.setTimeout(()=>t(e()),16),cancelAnimationFrame:e=>globalThis.clearTimeout(e)}):{requestAnimationFrame:e=>globalThis.requestAnimationFrame(e),cancelAnimationFrame:e=>globalThis.cancelAnimationFrame(e)};function l(){const t=[],a=new Map,l=new Map,i=new Map,s=new Set,o=new Map,c=new Map;let m=null,u=null;const d=()=>{null!==m&&(clearTimeout(m),m=null)},f=()=>{null!==u&&(r().cancelAnimationFrame(u),u=null)},k=()=>{if(d(),0===t.length)return;const a=t[0],n=Math.max(0,a.runAt-e());m=setTimeout(()=>{m=null;const a=e(),n=[];for(;t.length&&t[0].runAt<=a;){const e=t.shift();s.has(e.key)||n.push(e)}n.forEach(e=>{s.add(e.key);try{e.callback()}finally{s.delete(e.key)}}),k()},n)},h=e=>{const t=l.get(e);t&&(null!==t.timerId&&("raf"===t.timer?r().cancelAnimationFrame(t.timerId):clearTimeout(t.timerId)),l.delete(e))},T=(e,t)=>{const a=l.get(e);if(!a||s.has(e))return;a.timerId=null,a.tick+=1;const n=(a.tick,t-a.lastTime);let r;a.lastTime=t,s.add(e);try{r=a.callback(a.tick,t,n)}finally{s.delete(e)}!1!==r&&l.has(e)?y(a):l.delete(e)},y=t=>{if(!l.has(t.key))return;if("raf"===t.timer)return void(t.timerId=r().requestAnimationFrame(e=>T(t.key,e)));const a=Math.max(0,t.timer);t.timerId=setTimeout(()=>T(t.key,e()),a)},A=(e,t)=>{s.add(e);try{t()}finally{s.delete(e)}},p=e=>{const t=i.get(e);if(!t)return;t.timerId=null;const a=t.trailing;t.trailing=null,a?(A(e,a),i.has(e)&&g(t)):i.delete(e)},g=e=>{if(!i.has(e.key))return;if("raf"===e.timer)return void(e.timerId=r().requestAnimationFrame(()=>p(e.key)));const t=Math.max(0,e.timer);e.timerId=setTimeout(()=>p(e.key),t)},F=e=>{const t=i.get(e);t&&(null!==t.timerId&&("raf"===t.timer?r().cancelAnimationFrame(t.timerId):clearTimeout(t.timerId)),i.delete(e))},I=()=>{d(),f(),t.splice(0,t.length),a.clear(),s.clear(),o.forEach(e=>clearTimeout(e)),o.clear(),c.forEach(e=>r().cancelAnimationFrame(e)),c.clear(),Array.from(l.keys()).forEach(h),Array.from(i.keys()).forEach(F)},x=e=>s.has(e)||o.has(e)||c.has(e)||a.has(e)||l.has(e)||i.has(e)||t.some(t=>t.key===e),M=e=>{s.delete(e);const n=o.get(e);void 0!==n&&(clearTimeout(n),o.delete(e));const l=c.get(e);void 0!==l&&(r().cancelAnimationFrame(l),c.delete(e)),(e=>{a.delete(e),0===a.size&&f();const n=t.filter(t=>t.key!==e);t.splice(0,t.length,...n)})(e),h(e),F(e)};return{setTask:(l,i,o)=>{const c=(null!=o?o:n())+"";if(void 0!==o&&M(c),"raf"===i)return a.set(c,l),null===u&&(u=r().requestAnimationFrame(()=>{u=null;const e=Array.from(a.entries());a.clear(),e.forEach(([e,t])=>{if(!s.has(e)){s.add(e);try{t()}finally{s.delete(e)}}})})),c;const m=Math.max(0,i),d={key:c,runAt:e()+m,callback:l};return function(e,t){let a=0,n=e.length;for(;n>a;){const r=a+n>>1;e[r].runAt<t.runAt?a=r+1:n=r}e.splice(a,0,t)}(t,d),k(),c},setLockTask:(e,t,a)=>{const l=(null!=a?a:n())+"";if(x(l))return l;s.add(l);try{e()}finally{if("raf"===t){const e=r().requestAnimationFrame(()=>{s.delete(l),c.delete(l)});c.set(l,e)}else{const e=setTimeout(()=>{s.delete(l),o.delete(l)},Math.max(0,t));o.set(l,e)}}return l},setThrottleTask:(e,t,a)=>{const r=(null!=a?a:n())+"",l=i.get(r);if(l)return l.trailing=e,r;void 0!==a&&M(r);const s={key:r,timer:t,trailing:null,timerId:null};return i.set(r,s),A(r,e),i.has(r)&&g(s),r},setLoopTask:(t,a,r)=>{const i=(null!=r?r:n())+"";void 0!==r&&M(i);const s={key:i,callback:t,timer:a,tick:0,lastTime:e(),timerId:null};return l.set(i,s),y(s),i},cancelTask:e=>{void 0!==e?((Array.isArray(e)?e:[e]).forEach(e=>M(e+"")),k()):I()},hasTask:e=>x(e+""),clear:I}}const i=l(),s=i.setTask,o=i.setLockTask,c=i.setThrottleTask,m=i.setLoopTask,u=i.cancelTask,d=i.hasTask;exports.cancelTask=u,exports.createTaskManager=l,exports.hasTask=d,exports.setLockTask=o,exports.setLoopTask=m,exports.setTask=s,exports.setThrottleTask=c;
@@ -0,0 +1 @@
1
+ const e=()=>"undefined"!=typeof performance&&performance.now?performance.now():Date.now();let t=0,a=!1;const n=()=>"key_"+t++,r=()=>"function"!=typeof globalThis.requestAnimationFrame||"function"!=typeof globalThis.cancelAnimationFrame?(a||(a=!0),{requestAnimationFrame:t=>globalThis.setTimeout(()=>t(e()),16),cancelAnimationFrame:e=>globalThis.clearTimeout(e)}):{requestAnimationFrame:e=>globalThis.requestAnimationFrame(e),cancelAnimationFrame:e=>globalThis.cancelAnimationFrame(e)};function l(){const t=[],a=new Map,l=new Map,i=new Map,o=new Set,s=new Map,c=new Map;let m=null,u=null;const d=()=>{null!==m&&(clearTimeout(m),m=null)},f=()=>{null!==u&&(r().cancelAnimationFrame(u),u=null)},h=()=>{if(d(),0===t.length)return;const a=t[0],n=Math.max(0,a.runAt-e());m=setTimeout(()=>{m=null;const a=e(),n=[];for(;t.length&&t[0].runAt<=a;){const e=t.shift();o.has(e.key)||n.push(e)}n.forEach(e=>{o.add(e.key);try{e.callback()}finally{o.delete(e.key)}}),h()},n)},k=e=>{const t=l.get(e);t&&(null!==t.timerId&&("raf"===t.timer?r().cancelAnimationFrame(t.timerId):clearTimeout(t.timerId)),l.delete(e))},y=(e,t)=>{const a=l.get(e);if(!a||o.has(e))return;a.timerId=null,a.tick+=1;const n=(a.tick,t-a.lastTime);let r;a.lastTime=t,o.add(e);try{r=a.callback(a.tick,t,n)}finally{o.delete(e)}!1!==r&&l.has(e)?T(a):l.delete(e)},T=t=>{if(!l.has(t.key))return;if("raf"===t.timer)return void(t.timerId=r().requestAnimationFrame(e=>y(t.key,e)));const a=Math.max(0,t.timer);t.timerId=setTimeout(()=>y(t.key,e()),a)},A=(e,t)=>{o.add(e);try{t()}finally{o.delete(e)}},g=e=>{const t=i.get(e);if(!t)return;t.timerId=null;const a=t.trailing;t.trailing=null,a?(A(e,a),i.has(e)&&p(t)):i.delete(e)},p=e=>{if(!i.has(e.key))return;if("raf"===e.timer)return void(e.timerId=r().requestAnimationFrame(()=>g(e.key)));const t=Math.max(0,e.timer);e.timerId=setTimeout(()=>g(e.key),t)},F=e=>{const t=i.get(e);t&&(null!==t.timerId&&("raf"===t.timer?r().cancelAnimationFrame(t.timerId):clearTimeout(t.timerId)),i.delete(e))},I=()=>{d(),f(),t.splice(0,t.length),a.clear(),o.clear(),s.forEach(e=>clearTimeout(e)),s.clear(),c.forEach(e=>r().cancelAnimationFrame(e)),c.clear(),Array.from(l.keys()).forEach(k),Array.from(i.keys()).forEach(F)},b=e=>o.has(e)||s.has(e)||c.has(e)||a.has(e)||l.has(e)||i.has(e)||t.some(t=>t.key===e),M=e=>{o.delete(e);const n=s.get(e);void 0!==n&&(clearTimeout(n),s.delete(e));const l=c.get(e);void 0!==l&&(r().cancelAnimationFrame(l),c.delete(e)),(e=>{a.delete(e),0===a.size&&f();const n=t.filter(t=>t.key!==e);t.splice(0,t.length,...n)})(e),k(e),F(e)};return{setTask:(l,i,s)=>{const c=(null!=s?s:n())+"";if(void 0!==s&&M(c),"raf"===i)return a.set(c,l),null===u&&(u=r().requestAnimationFrame(()=>{u=null;const e=Array.from(a.entries());a.clear(),e.forEach(([e,t])=>{if(!o.has(e)){o.add(e);try{t()}finally{o.delete(e)}}})})),c;const m=Math.max(0,i),d={key:c,runAt:e()+m,callback:l};return function(e,t){let a=0,n=e.length;for(;n>a;){const r=a+n>>1;e[r].runAt<t.runAt?a=r+1:n=r}e.splice(a,0,t)}(t,d),h(),c},setLockTask:(e,t,a)=>{const l=(null!=a?a:n())+"";if(b(l))return l;o.add(l);try{e()}finally{if("raf"===t){const e=r().requestAnimationFrame(()=>{o.delete(l),c.delete(l)});c.set(l,e)}else{const e=setTimeout(()=>{o.delete(l),s.delete(l)},Math.max(0,t));s.set(l,e)}}return l},setThrottleTask:(e,t,a)=>{const r=(null!=a?a:n())+"",l=i.get(r);if(l)return l.trailing=e,r;void 0!==a&&M(r);const o={key:r,timer:t,trailing:null,timerId:null};return i.set(r,o),A(r,e),i.has(r)&&p(o),r},setLoopTask:(t,a,r)=>{const i=(null!=r?r:n())+"";void 0!==r&&M(i);const o={key:i,callback:t,timer:a,tick:0,lastTime:e(),timerId:null};return l.set(i,o),T(o),i},cancelTask:e=>{void 0!==e?((Array.isArray(e)?e:[e]).forEach(e=>M(e+"")),h()):I()},hasTask:e=>b(e+""),clear:I}}const i=l(),o=i.setTask,s=i.setLockTask,c=i.setThrottleTask,m=i.setLoopTask,u=i.cancelTask,d=i.hasTask;export{u as cancelTask,l as createTaskManager,d as hasTask,s as setLockTask,m as setLoopTask,o as setTask,c as setThrottleTask};
@@ -0,0 +1,2 @@
1
+ declare const getNow: () => number;
2
+ export default getNow;
@@ -0,0 +1,105 @@
1
+ import type { CancelTaskData, LoopTaskCallback, TaskCallback, TaskKey, TaskManager, TaskTimer } from "../types/types";
2
+ /**---
3
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
4
+ * ### ***createTaskManager***:
5
+ * create isolated task state.
6
+ * @description
7
+ * Use it for components, scenes, entities or modules that need independent cleanup.
8
+ * @example
9
+ * ```ts
10
+ * const tasks = createTaskManager();
11
+ *
12
+ * tasks.setLoopTask(callback, "raf", "my-raf-key");
13
+ * tasks.setTask(callback, 1000, "my-timeout-key");
14
+ *
15
+ * tasks.clear();
16
+ * ```
17
+ */
18
+ declare function createTaskManager(): TaskManager;
19
+ /**---
20
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
21
+ * ### ***setTask***:
22
+ * schedule latest one-shot work by timeout or RAF.
23
+ * @description
24
+ * If the same `key` is used again, the previous task state is replaced.
25
+ * @returns the passed key, or an automatically generated key when `key` is omitted.
26
+ * @example
27
+ * ```ts
28
+ * setTask(() => {
29
+ * doSomething();
30
+ * }, "raf", "my-key");
31
+ * ```
32
+ */
33
+ declare const setTask: (callback: TaskCallback, timer: TaskTimer, key?: TaskKey) => string;
34
+ /**---
35
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
36
+ * ### ***setLockTask***:
37
+ * run work immediately, then lock repeated calls by key.
38
+ * @description
39
+ * The first call wins. Calls with a key that already belongs to a task state are ignored until that key becomes free.
40
+ * @returns the passed key, or an automatically generated key when `key` is omitted.
41
+ * @example
42
+ * ```ts
43
+ * setLockTask(() => {
44
+ * doSomething();
45
+ * }, "raf", "my-key");
46
+ * ```
47
+ */
48
+ declare const setLockTask: (callback: TaskCallback, timer: TaskTimer, key?: TaskKey) => string;
49
+ /**---
50
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
51
+ * ### ***setThrottleTask***:
52
+ * run work immediately, then keep the latest repeated call while throttled.
53
+ * @description
54
+ * The first call runs immediately. Calls with the same `key` during the throttle window save only the latest callback, which runs when the window opens again.
55
+ * @returns the passed key, or an automatically generated key when `key` is omitted.
56
+ * @example
57
+ * ```ts
58
+ * setThrottleTask(() => {
59
+ * doSomething();
60
+ * }, "raf", "my-key");
61
+ * ```
62
+ */
63
+ declare const setThrottleTask: (callback: TaskCallback, timer: TaskTimer, key?: TaskKey) => string;
64
+ /**---
65
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
66
+ * ### ***setLoopTask***:
67
+ * run repeated work by timeout or RAF.
68
+ * @description
69
+ * If the same `key` is used again, the previous task state is cancelled and replaced. Return `false` from callback to stop the loop.
70
+ * @returns the passed key, or an automatically generated key when `key` is omitted.
71
+ * @example
72
+ * ```ts
73
+ * setLoopTask((tick, time, delta) => {
74
+ * doSomething();
75
+ *
76
+ * if (tick > 10) return false;
77
+ * }, "raf", "my-key");
78
+ * ```
79
+ */
80
+ declare const setLoopTask: (callback: LoopTaskCallback, timer: TaskTimer, key?: TaskKey) => string;
81
+ /**---
82
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
83
+ * ### ***cancelTask***:
84
+ * cancel one task, several tasks or all tasks in the global manager.
85
+ * @example
86
+ * ```ts
87
+ * cancelTask("my-key");
88
+ * cancelTask(["key1", "key2"]);
89
+ * cancelTask();
90
+ * ```
91
+ */
92
+ declare const cancelTask: (taskData?: CancelTaskData) => void;
93
+ /**---
94
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
95
+ * ### ***hasTask***:
96
+ * check whether a key belongs to any active task state.
97
+ * @example
98
+ * ```ts
99
+ * if (hasTask("my-key")) {
100
+ * cancelTask("my-key");
101
+ * }
102
+ * ```
103
+ */
104
+ declare const hasTask: (key: TaskKey) => boolean;
105
+ export { cancelTask, createTaskManager, hasTask, setLockTask, setLoopTask, setTask, setThrottleTask, };
@@ -0,0 +1,2 @@
1
+ export { cancelTask, createTaskManager, hasTask, setLockTask, setLoopTask, setTask, setThrottleTask, } from "./helpers/taskManager";
2
+ export type { CancelTaskData, LoopTaskCallback, TaskCallback, TaskKey, TaskManager, TaskTimer, } from "./types/types";
@@ -0,0 +1,195 @@
1
+ /**---
2
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
3
+ * ### ***TaskKey***:
4
+ * shared task key used by `setTask`, `setLockTask`, `setThrottleTask`, `setLoopTask`, `cancelTask` and `hasTask`.
5
+ * @description
6
+ * `TaskKey` is intentionally a string. One key can belong to only one active task state inside a manager. Readable keys make scheduling calls easier to scan, especially when `timer` can be a number.
7
+ * @example
8
+ * ```ts
9
+ * setTask(callback, "raf", "my-key");
10
+ * setThrottleTask(callback, 150, "my-key");
11
+ * ```
12
+ */
13
+ type TaskKey = string;
14
+ /**---
15
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
16
+ * ### ***TaskTimer***:
17
+ * scheduler used by task primitives.
18
+ * @description
19
+ * - `number`: *timeout delay in milliseconds*
20
+ * - `"raf"`: *run on `requestAnimationFrame`*
21
+ * @note
22
+ * `0` is a real `setTimeout(..., 0)` delay. Use `"raf"` explicitly for frame scheduling. RAF mode uses `requestAnimationFrame` / `cancelAnimationFrame` when available and falls back to `setTimeout(..., 16)` with a console warning when those APIs are missing.
23
+ * @example
24
+ * ```ts
25
+ * setTask(callback, 300, "my-key");
26
+ * setTask(callback, "raf", "my-key");
27
+ * ```
28
+ */
29
+ type TaskTimer = number | "raf";
30
+ /**---
31
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
32
+ * ### ***TaskCallback***:
33
+ * one-shot callback used by `setTask`, `setLockTask` and `setThrottleTask`.
34
+ * @example
35
+ * ```ts
36
+ * const callback: TaskCallback = () => {
37
+ * doSomething();
38
+ * };
39
+ * ```
40
+ */
41
+ type TaskCallback = () => void;
42
+ /**---
43
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
44
+ * ### ***LoopTaskCallback***:
45
+ * repeated callback used by `setLoopTask`.
46
+ * @param tick 1-based loop iteration count.
47
+ * @param time current time from `performance.now()` / RAF timestamp.
48
+ * @param delta time passed since the previous loop tick.
49
+ * @returns `false` to stop the loop.
50
+ * @example
51
+ * ```ts
52
+ * setLoopTask((tick, time, delta) => {
53
+ * doSomething();
54
+ *
55
+ * if (tick > 10) return false;
56
+ * }, "raf", "my-key");
57
+ * ```
58
+ */
59
+ type LoopTaskCallback = (tick: number, time: number, delta: number) => boolean | void;
60
+ type Task = {
61
+ key: string;
62
+ callback: TaskCallback;
63
+ runAt: number;
64
+ };
65
+ /**---
66
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
67
+ * ### ***CancelTaskData***:
68
+ * input accepted by `cancelTask`.
69
+ * @description
70
+ * - `TaskKey`: *cancel one task by key*
71
+ * - `TaskKey[]`: *cancel several tasks by key*
72
+ * - `undefined`: *cancel everything in the manager*
73
+ * @example
74
+ * ```ts
75
+ * cancelTask("my-key");
76
+ * cancelTask(["key1", "key2"]);
77
+ * cancelTask();
78
+ * ```
79
+ */
80
+ type CancelTaskData = TaskKey | TaskKey[];
81
+ /**---
82
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
83
+ * ### ***TaskManager***:
84
+ * isolated key-based task manager.
85
+ * @description
86
+ * Use `createTaskManager()` when a component, scene, entity or module needs its own task state and cleanup.
87
+ * @example
88
+ * ```ts
89
+ * const tasks = createTaskManager();
90
+ *
91
+ * tasks.setLoopTask(callback, "raf", "my-raf-key");
92
+ * tasks.setTask(callback, 1000, "my-timeout-key");
93
+ *
94
+ * tasks.clear();
95
+ * ```
96
+ */
97
+ type TaskManager = {
98
+ /**---
99
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
100
+ * ### ***setTask***:
101
+ * schedule latest one-shot work by timeout or RAF.
102
+ * @description
103
+ * If the same `key` is used again, the previous task state is replaced.
104
+ * @returns the passed key, or an automatically generated key when `key` is omitted.
105
+ * @example
106
+ * ```ts
107
+ * manager.setTask(() => {
108
+ * doSomething();
109
+ * }, "raf", "my-key");
110
+ * ```
111
+ */
112
+ setTask: (callback: TaskCallback, timer: TaskTimer, key?: TaskKey) => string;
113
+ /**---
114
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
115
+ * ### ***setLockTask***:
116
+ * run work immediately, then lock repeated calls by key.
117
+ * @description
118
+ * The first call wins. Calls with a key that already belongs to a task state are ignored until that key becomes free.
119
+ * @returns the passed key, or an automatically generated key when `key` is omitted.
120
+ * @example
121
+ * ```ts
122
+ * manager.setLockTask(() => {
123
+ * doSomething();
124
+ * }, "raf", "my-key");
125
+ * ```
126
+ */
127
+ setLockTask: (callback: TaskCallback, timer: TaskTimer, key?: TaskKey) => string;
128
+ /**---
129
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
130
+ * ### ***setThrottleTask***:
131
+ * run work immediately, then keep the latest repeated call while throttled.
132
+ * @description
133
+ * The first call runs immediately. Calls with the same `key` during the throttle window save only the latest callback, which runs when the window opens again.
134
+ * @returns the passed key, or an automatically generated key when `key` is omitted.
135
+ * @example
136
+ * ```ts
137
+ * manager.setThrottleTask(() => {
138
+ * doSomething();
139
+ * }, "raf", "my-key");
140
+ * ```
141
+ */
142
+ setThrottleTask: (callback: TaskCallback, timer: TaskTimer, key?: TaskKey) => string;
143
+ /**---
144
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
145
+ * ### ***setLoopTask***:
146
+ * run repeated work by timeout or RAF.
147
+ * @description
148
+ * If the same `key` is used again, the previous task state is cancelled and replaced. Return `false` from callback to stop the loop.
149
+ * @returns the passed key, or an automatically generated key when `key` is omitted.
150
+ * @example
151
+ * ```ts
152
+ * manager.setLoopTask((tick, time, delta) => {
153
+ * doSomething();
154
+ *
155
+ * if (tick > 10) return false;
156
+ * }, "raf", "my-key");
157
+ * ```
158
+ */
159
+ setLoopTask: (callback: LoopTaskCallback, timer: TaskTimer, key?: TaskKey) => string;
160
+ /**---
161
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
162
+ * ### ***cancelTask***:
163
+ * cancel one task, several tasks or all tasks in the manager.
164
+ * @example
165
+ * ```ts
166
+ * manager.cancelTask("my-key");
167
+ * manager.cancelTask(["key1", "key2"]);
168
+ * manager.cancelTask();
169
+ * ```
170
+ */
171
+ cancelTask: (taskData?: CancelTaskData) => void;
172
+ /**---
173
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
174
+ * ### ***hasTask***:
175
+ * check whether a key belongs to any active task state.
176
+ * @example
177
+ * ```ts
178
+ * if (manager.hasTask("my-key")) {
179
+ * manager.cancelTask("my-key");
180
+ * }
181
+ * ```
182
+ */
183
+ hasTask: (key: TaskKey) => boolean;
184
+ /**---
185
+ * ## ![logo](https://github.com/voodoofugu/keytask/raw/main/src/assets/keytask-logo.png)
186
+ * ### ***clear***:
187
+ * cancel every pending one-shot task, active lock, active throttle and active loop in this manager.
188
+ * @example
189
+ * ```ts
190
+ * tasks.clear();
191
+ * ```
192
+ */
193
+ clear: () => void;
194
+ };
195
+ export type { CancelTaskData, LoopTaskCallback, Task, TaskCallback, TaskKey, TaskManager, TaskTimer, };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "keytask-core",
3
+ "version": "1.0.0",
4
+ "description": "A tiny key-based task controller for timeout and RAF work.",
5
+ "author": "Georg Schilin",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/cjs/index.cjs",
9
+ "module": "dist/esm/index.js",
10
+ "types": "dist/types/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/types/index.d.ts",
14
+ "import": "./dist/esm/index.js",
15
+ "require": "./dist/cjs/index.cjs"
16
+ }
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/voodoofugu/keytask.git"
21
+ },
22
+ "homepage": "https://github.com/voodoofugu/keytask#readme",
23
+ "keywords": [
24
+ "keytask",
25
+ "task",
26
+ "keyed",
27
+ "timer",
28
+ "timeout",
29
+ "raf",
30
+ "animation",
31
+ "game-loop",
32
+ "throttle",
33
+ "scheduler"
34
+ ],
35
+ "sideEffects": false,
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "LICENSE"
40
+ ]
41
+ }