upwwwards 0.1.7-beta.1 → 0.1.8-beta.1

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.
@@ -8,13 +8,14 @@ declare namespace CallStack {
8
8
  type CALLBACK = (...args: any) => void;
9
9
  }
10
10
  /**
11
- * Callstack: an ordered list of synchronous callbacks that are invoked together, in insertion order,
12
- * with the same arguments (similar to a lightweight event emitter for a single event).
11
+ * Ordered list of synchronous callbacks invoked together, in insertion order, with the same arguments
12
+ * (a lightweight single-event emitter).
13
13
  *
14
- * Callbacks run either all at once with `loop`, or one at a time with `gen`, which returns a generator
15
- * that pauses after each callback (used by the frame scheduler `Thread` to spread work across frames).
16
- * Callbacks may be added or removed while the stack is being iterated: removals are applied immediately
17
- * without skipping the next callback, and additions only run from the next iteration.
14
+ * Run them all at once with {@link CallStack.loop}, or one-by-one with {@link CallStack.gen} (yields after each
15
+ * callback so a caller such as `Up.Thread` can spread work across frames).
16
+ * You may {@link CallStack.add}, {@link CallStack.remove}, or {@link CallStack.clear} while an iteration is running:
17
+ * removals apply immediately without skipping the next callback, additions run from the next iteration,
18
+ * and `clear` skips any callback that has not run yet.
18
19
  *
19
20
  * If a callback throws, the error is logged with `Logs.error` and the remaining callbacks of that
20
21
  * iteration are skipped; the error is not rethrown.
@@ -93,6 +94,12 @@ declare class CallStack<T extends CallStack.CALLBACK> {
93
94
  * ```
94
95
  */
95
96
  gen(...args: Parameters<T>): Thread.GENERATOR;
97
+ /**
98
+ * Removes every callback and resets iteration counters (`length` becomes `0`).
99
+ * Safe to call during {@link CallStack.loop} / {@link CallStack.gen}: callbacks that have not run yet are skipped.
100
+ * When no iteration is in progress, any outstanding generator created by {@link CallStack.gen} is closed.
101
+ */
102
+ clear(): void;
96
103
  }
97
104
 
98
105
  declare namespace Arrays {
@@ -294,16 +301,6 @@ declare const
294
301
  * Key of the method returning the active cache (browser map or request-scoped server map).
295
302
  */
296
303
  getCache: unique symbol;
297
- declare const /**
298
- * Key of the modifier refreshing viewport metrics.
299
- * @internal
300
- */
301
- updateScreenVars: unique symbol;
302
- declare const /**
303
- * Key of the modifier refreshing scroll metrics.
304
- * @internal
305
- */
306
- updateScrollVars: unique symbol;
307
304
  declare namespace App {
308
305
  /**
309
306
  * Operating system / device family detected from `navigator.userAgent`.
@@ -332,8 +329,9 @@ declare namespace App {
332
329
  /**
333
330
  * Application runtime: device and browser detection, viewport and scroll metrics, an application cache,
334
331
  * and a wait queue that delays the `LOAD` event (see {@link Event}).
335
- * @note Metrics are client-only. They are read once by `Up.set`, then refreshed by Thread modifiers before
336
- * `resize` (flag `8`) and `scroll` (flag `32`) tasks run.
332
+ * @note Metrics are client-only. They are read once by `Up.set`, then refreshed by Thread modifiers on
333
+ * `resize` (flag `8`) and `scroll` (flag `32`) when those flags are armed ({@link Thread.sub} / {@link Thread.arm}).
334
+ * With the Scroll plugin, Lenis arms the scroll flag after writing `window.scrollY`.
337
335
  */
338
336
  declare abstract class App {
339
337
  #private;
@@ -489,15 +487,16 @@ declare abstract class App {
489
487
  */
490
488
  static remove(key: App.CACHE_KEY): boolean;
491
489
  /**
492
- * Thread modifier refreshing viewport metrics (`w`, `cw`, `sw`, `lw`, `h`, `ch`, `sh`, `lh`).
493
- * @internal
490
+ * Refreshes viewport metrics (`w`, `cw`, `sw`, `lw`, `h`, `ch`, `sh`, `lh`).
491
+ * Registered as a Thread modifier on {@link Thread.RESIZE_FLAG}; call it yourself only if you need an immediate read outside that phase.
494
492
  */
495
- static [updateScreenVars](): void;
493
+ static updateScreenVars(): void;
496
494
  /**
497
- * Thread modifier refreshing scroll metrics (`y`, `dY`, `downwards`).
498
- * @internal
495
+ * Refreshes scroll metrics (`y`, `dY`, `downwards`) from `window.scrollY`.
496
+ * Registered as a Thread modifier on {@link Thread.RESIZE_FLAG} and {@link Thread.SCROLL_FLAG}; no-op when `y` is unchanged.
497
+ * Prefer letting Thread arm those flags (or the Scroll plugin) rather than calling this every frame by hand.
499
498
  */
500
- static [updateScrollVars](): void;
499
+ static updateScrollVars(): void;
501
500
  /**
502
501
  * Clears the application cache and pending wait queue. Called from `Up.destroy` in the browser.
503
502
  * @internal
@@ -518,15 +517,15 @@ declare namespace Thread {
518
517
  * Thread scheduler configuration, declared in `up.thread` of the Vite initializer.
519
518
  */
520
519
  type CONFIG = {
521
- /**
522
- * Soft time budget in ms per frame for flow (one-shot) tasks. Must be greater than `0`. Default: `8`.
523
- */
524
- flowBudget?: number;
525
520
  /**
526
521
  * Soft time budget in ms per frame for forever tasks. Must be greater than `0`. Default: `4`.
527
522
  * Forever tasks always run every frame; the budget only limits how long their generators may run.
528
523
  */
529
524
  foreverBudget?: number;
525
+ /**
526
+ * Soft time budget in ms per frame for flow (one-shot) tasks. Must be greater than `0`. Default: `8`.
527
+ */
528
+ flowBudget?: number;
530
529
  };
531
530
  /**
532
531
  * Flag of `EARTHQUAKE` event tasks (`2`).
@@ -553,10 +552,20 @@ declare namespace Thread {
553
552
  */
554
553
  type SYNTHRA_FLAG = 2_048;
555
554
  /**
556
- * Priority flag of a task or modifier: a power of 2 from `1` to `4096`.
557
- * Within a frame, lower flags run first; tasks sharing a flag run in insertion order.
555
+ * Default flag of {@link Thread.sub} when `flag` is omitted (`8192`).
556
+ * Runs before {@link Thread.MAX_FLAG} (end-of-frame work such as Anime `updateRoot`).
557
+ */
558
+ type DEFAULT_FLAG = 8_192;
559
+ /**
560
+ * Highest allowed priority flag (`32768`). Reserved for end-of-frame work (e.g. Anime advances GSAP here).
561
+ * Not the default of {@link Thread.sub} — see {@link Thread.DEFAULT_FLAG}.
558
562
  */
559
- type FLAG = 1 | EARTHQUAKE_FLAG | 4 | RESIZE_FLAG | 16 | SCROLL_FLAG | 64 | EVENT_FLAG | 256 | OBSERVER_FLAG | 1_024 | SYNTHRA_FLAG | 4_096;
563
+ type MAX_FLAG = 32_768;
564
+ /**
565
+ * Priority flag of a task or modifier: a power of 2 from `1` to {@link Thread.MAX_FLAG} (`32768`).
566
+ * Within a frame, lower flags run first; for each flag, forever tasks run before flow tasks.
567
+ */
568
+ type FLAG = 1 | EARTHQUAKE_FLAG | 4 | RESIZE_FLAG | 16 | SCROLL_FLAG | 64 | EVENT_FLAG | 256 | OBSERVER_FLAG | 1_024 | SYNTHRA_FLAG | 4_096 | DEFAULT_FLAG | 16_384 | MAX_FLAG;
560
569
  /**
561
570
  * Modifier callback. It is called without arguments and its return value is ignored.
562
571
  */
@@ -602,10 +611,12 @@ declare namespace Thread {
602
611
  ];
603
612
  }
604
613
  /**
605
- * Frame scheduler of the runtime. Each frame (`requestAnimationFrame`, or an external ticker such as GSAP) runs, in order:
606
- * modifiers, then flow tasks (run once, then removed), then forever tasks (run every frame).
607
- * Work is grouped by priority flags (see {@link Thread.FLAG}) and limited by a soft time budget ({@link Thread.CONFIG});
608
- * flow tasks that do not fit are postponed to the next frame, and generator tasks are resumed across frames.
614
+ * Frame scheduler of the runtime (`Up.Thread`). Each frame (`requestAnimationFrame`, or an external ticker such as GSAP)
615
+ * runs, in order: **drivers** → **modifiers** → **tasks by ascending flag** (forever then flow for each flag).
616
+ * Drivers (see {@link Thread.addDriver}) write frame inputs early (e.g. Lenis `raf`). Modifiers refresh shared data
617
+ * before tasks; arm them with {@link Thread.sub} or {@link Thread.arm}. Soft budgets ({@link Thread.CONFIG}) apply
618
+ * separately to forever and flow. Prefer `Up.Event.sub(['TICK', …])` for simple per-frame work; use Thread for
619
+ * budgets, priorities, drivers, or end-of-frame forever tasks on {@link Thread.MAX_FLAG}.
609
620
  * @note Client-only: the frame loop starts with `Up.set`. The Thread is paused while the page is hidden.
610
621
  */
611
622
  declare abstract class Thread {
@@ -632,10 +643,15 @@ declare abstract class Thread {
632
643
  */
633
644
  static get App(): typeof App;
634
645
  /**
635
- * Highest allowed flag, also the default flag of {@link Thread.sub}.
636
- * @returns `4096`.
646
+ * Default priority flag of {@link Thread.sub} when `flag` is omitted (`8192`).
647
+ * @returns `8192`.
637
648
  */
638
- static get MAX_FLAG(): Thread.FLAG;
649
+ static get DEFAULT_FLAG(): Thread.DEFAULT_FLAG;
650
+ /**
651
+ * Highest allowed priority flag (`32768`). Use for end-of-frame forever work (e.g. Anime `gsap.updateRoot`).
652
+ * @returns `32768`.
653
+ */
654
+ static get MAX_FLAG(): Thread.MAX_FLAG;
639
655
  /**
640
656
  * Timestamp (ms) of the last processed frame, in the clock of the frame driver:
641
657
  * `requestAnimationFrame` time by default, or the external ticker time when one is attached (e.g. GSAP ticker time × `1000`).
@@ -662,16 +678,32 @@ declare abstract class Thread {
662
678
  */
663
679
  static [set](): void;
664
680
  /**
665
- * Registers a modifier: a lightweight hook that refreshes shared data before tasks use it.
666
- * After {@link Thread.sub} is called with the same `flag`, the modifier runs once at the start of the next processed frame,
667
- * before any task (at most once per frame). Modifiers run by ascending flag, then in registration order.
668
- * Registering the same callback twice on the same flag logs a warning and does nothing.
681
+ * Registers a **driver**: a sync callback run at the **start** of every processed frame, before modifiers and tasks.
682
+ * Use it for external clocks that write frame inputs (e.g. Scroll registers Lenis `raf` here with `autoRaf: false`).
683
+ * Callbacks run in registration order with no arguments; prefer sync functions. The same reference can be
684
+ * registered only once (a duplicate logs a warning and is ignored).
685
+ * @param callback Function called at the start of the frame (reads {@link Thread.timestamp} if needed).
686
+ * @example
687
+ * ```ts
688
+ * const step = (): void => { lenis.raf(Up.Thread.timestamp) }
689
+ *
690
+ * Up.Thread.addDriver(step)
691
+ * // later
692
+ * Up.Thread.removeDriver(step)
693
+ * ```
694
+ */
695
+ static addDriver(callback: Thread.CALLBACK): void;
696
+ /**
697
+ * Registers a modifier: a lightweight hook that refreshes shared data before tasks of the same `flag`.
698
+ * After {@link Thread.sub} or {@link Thread.arm} with that flag, the modifier runs once in the modifiers phase
699
+ * of the next processed frame (at most once per frame), before forever/flow tasks. Modifiers run by ascending
700
+ * flag, then in registration order. Registering the same callback twice on the same flag logs a warning.
669
701
  * @note Keep modifiers cheap: they may run on many frames.
670
702
  * @note The `App` metric modifiers (on {@link Thread.RESIZE_FLAG} and {@link Thread.SCROLL_FLAG}) are registered first,
671
703
  * so they run before user modifiers of the same flag.
672
704
  * @param callback Function called without arguments.
673
- * @param flag Priority flag, a power of 2 from `1` to `4096`.
674
- * @throws If `flag` is not a power of 2 between `1` and `4096`.
705
+ * @param flag Priority flag, a power of 2 from `1` to {@link Thread.MAX_FLAG}.
706
+ * @throws If `flag` is not a power of 2 between `1` and {@link Thread.MAX_FLAG}.
675
707
  * @example
676
708
  * ```ts
677
709
  * let ratio = 1
@@ -680,15 +712,20 @@ declare abstract class Thread {
680
712
  * ```
681
713
  */
682
714
  static addModifier(callback: Thread.CALLBACK, flag: Thread.FLAG): void;
715
+ /**
716
+ * Unregisters a driver added with {@link Thread.addDriver}. Does nothing if it is not registered.
717
+ * @param callback The same function reference passed to `addDriver`.
718
+ */
719
+ static removeDriver(callback: Thread.CALLBACK): void;
683
720
  /**
684
721
  * Removes a modifier registered with {@link Thread.addModifier}. Does nothing if it is not registered on `flag`.
685
722
  * @param callback The same function reference passed to `addModifier`.
686
723
  * @param flag The flag used when adding the modifier.
687
- * @throws If `flag` is not a power of 2 between `1` and `4096`.
724
+ * @throws If `flag` is not a power of 2 between `1` and {@link Thread.MAX_FLAG}.
688
725
  */
689
726
  static removeModifier(callback: Thread.CALLBACK, flag: Thread.FLAG): void;
690
727
  /**
691
- * Clears all modifiers and tasks and stops the internal frame loop. Called from `Up.destroy` in the browser.
728
+ * Clears all modifiers, tasks, and drivers, and stops the internal frame loop. Called from `Up.destroy` in the browser.
692
729
  * @internal
693
730
  */
694
731
  static [destroy](): void;
@@ -722,9 +759,9 @@ declare abstract class Thread {
722
759
  * (the task stays scheduled until the promise settles).
723
760
  * @template T Task type, used to type `args`.
724
761
  * @param task The task function, identified by reference.
725
- * @param flag Priority flag, a power of 2 from `1` to `4096`. Default: {@link Thread.MAX_FLAG} (`4096`).
762
+ * @param flag Priority flag, a power of 2 from `1` to {@link Thread.MAX_FLAG}. Default: {@link Thread.DEFAULT_FLAG} (`8192`).
726
763
  * @param args Arguments passed to the task when it runs (the latest call wins).
727
- * @throws If `flag` is not a power of 2 between `1` and `4096`.
764
+ * @throws If `flag` is not a power of 2 between `1` and {@link Thread.MAX_FLAG} (`32768`).
728
765
  * @example
729
766
  * ```ts
730
767
  * function* build(items: HTMLElement[]): Generator<void, void, void>
@@ -747,6 +784,18 @@ declare abstract class Thread {
747
784
  * @param task The same function reference passed to {@link Thread.sub}.
748
785
  */
749
786
  static unsub(task: Thread.TASK): void;
787
+ /**
788
+ * Arms modifiers of `flag` for the next modifiers phase (same frame if called from a driver or before modifiers run).
789
+ * No-op when no modifiers are registered on `flag`. {@link Thread.sub} calls this automatically; call it yourself
790
+ * when an external writer updates shared state without scheduling a task (e.g. Scroll arms {@link Thread.SCROLL_FLAG}
791
+ * after Lenis writes `window.scrollY`, so `App.updateScrollVars` and ScrollTrigger modifiers run).
792
+ * @param flag Priority flag whose modifiers should run.
793
+ * @example
794
+ * ```ts
795
+ * lenis.on('scroll', () => Up.Thread.arm(32))
796
+ * ```
797
+ */
798
+ static arm(flag: Thread.FLAG): void;
750
799
  /**
751
800
  * Returns a promise that resolves when a scheduled task completes its next (or current) run, or when it is unsubscribed.
752
801
  * @param task The same function reference passed to {@link Thread.sub}.
@@ -781,4 +830,4 @@ declare abstract class Thread {
781
830
  static unmarkForever(task: Thread.TASK): void;
782
831
  }
783
832
 
784
- export { Arrays as A, CallStack as C, Thread as T, App as a, type AppCache as b, updateScrollVars as c, updateScreenVars as u };
833
+ export { Arrays as A, CallStack as C, Thread as T, App as a, type AppCache as b };
@@ -1,16 +1,19 @@
1
1
  import { INHERITANCE, STATUS, construct, set, destroy, apply, start, stop } from "../../../symbols/index.js";
2
+ import Arrays from "../../../scripts/Arrays.js";
2
3
  import Logs from "../../../scripts/Logs.js";
3
- import App, { updateScreenVars, updateScrollVars } from "./[3]/App.js";
4
+ import App from "./[3]/App.js";
4
5
  const FOREVER = /* @__PURE__ */ Symbol("FOREVER");
5
6
  class Thread {
6
7
  // #PROPS
7
8
  // ##STATICS
8
- static #MAX_FLAG = 4096;
9
+ static #DEFAULT_FLAG = 8192;
10
+ static #MAX_FLAG = 32768;
9
11
  static #MODIFIERS_MAP = /* @__PURE__ */ new Map();
10
- static #FLOW_MAP = /* @__PURE__ */ new Map();
11
12
  static #FOREVER_MAP = /* @__PURE__ */ new Map();
12
- static #FLOW_LISTED = /* @__PURE__ */ new Map();
13
+ static #FLOW_MAP = /* @__PURE__ */ new Map();
13
14
  static #FOREVER_LISTED = /* @__PURE__ */ new Map();
15
+ static #FLOW_LISTED = /* @__PURE__ */ new Map();
16
+ static #DRIVERS_CALLSTACK = new Arrays.CallStack();
14
17
  /**
15
18
  * Shared HRT stack after hydrating `App` (`[Adapter, App]`); later modules append `Thread`, `Event`.
16
19
  * Same array as `App[INHERITANCE]` — does not include `Thread` itself.
@@ -26,8 +29,8 @@ class Thread {
26
29
  static #flowBudget = 8;
27
30
  static #foreverBudget = 4;
28
31
  static #modifiersBitMask = 0;
29
- static #flowBitMask = 0;
30
32
  static #foreverBitMask = 0;
33
+ static #flowBitMask = 0;
31
34
  static #i = -1;
32
35
  static #max = -1;
33
36
  static #stack = null;
@@ -54,8 +57,15 @@ class Thread {
54
57
  return App;
55
58
  }
56
59
  /**
57
- * Highest allowed flag, also the default flag of {@link Thread.sub}.
58
- * @returns `4096`.
60
+ * Default priority flag of {@link Thread.sub} when `flag` is omitted (`8192`).
61
+ * @returns `8192`.
62
+ */
63
+ static get DEFAULT_FLAG() {
64
+ return Thread.#DEFAULT_FLAG;
65
+ }
66
+ /**
67
+ * Highest allowed priority flag (`32768`). Use for end-of-frame forever work (e.g. Anime `gsap.updateRoot`).
68
+ * @returns `32768`.
59
69
  */
60
70
  static get MAX_FLAG() {
61
71
  return Thread.#MAX_FLAG;
@@ -91,9 +101,9 @@ class Thread {
91
101
  if (config.flowBudget) config.flowBudget > 0 ? Thread.#flowBudget = config.flowBudget : Logs.warn("THREAD", "THE FLOW BUDGET MUST BE GREATER THAN 0.");
92
102
  if (config.foreverBudget) config.foreverBudget > 0 ? Thread.#foreverBudget = config.foreverBudget : Logs.warn("THREAD", "THE FOREVER BUDGET MUST BE GREATER THAN 0.");
93
103
  }
94
- Thread.addModifier(Thread.App[updateScreenVars], 8);
95
- Thread.addModifier(Thread.App[updateScrollVars], 8);
96
- Thread.addModifier(Thread.App[updateScrollVars], 32);
104
+ Thread.addModifier(Thread.App.updateScreenVars, 8);
105
+ Thread.addModifier(Thread.App.updateScrollVars, 8);
106
+ Thread.addModifier(Thread.App.updateScrollVars, 32);
97
107
  }
98
108
  // ##SET
99
109
  /**
@@ -113,10 +123,10 @@ class Thread {
113
123
  }
114
124
  // ##GET
115
125
  /**
116
- * Returns `flag` after validation, or `MAX_FLAG` when omitted.
126
+ * Returns `flag` after validation, or `DEFAULT_FLAG` when omitted.
117
127
  */
118
128
  static #getFlagFrom(flag) {
119
- if (flag == null) return Thread.#MAX_FLAG;
129
+ if (flag == null) return Thread.#DEFAULT_FLAG;
120
130
  Thread.#checkFlag(flag);
121
131
  return flag;
122
132
  }
@@ -144,16 +154,35 @@ class Thread {
144
154
  map.get(flag)?.push(wrapper) ?? map.set(flag, [wrapper]);
145
155
  }
146
156
  /**
147
- * Registers a modifier: a lightweight hook that refreshes shared data before tasks use it.
148
- * After {@link Thread.sub} is called with the same `flag`, the modifier runs once at the start of the next processed frame,
149
- * before any task (at most once per frame). Modifiers run by ascending flag, then in registration order.
150
- * Registering the same callback twice on the same flag logs a warning and does nothing.
157
+ * Registers a **driver**: a sync callback run at the **start** of every processed frame, before modifiers and tasks.
158
+ * Use it for external clocks that write frame inputs (e.g. Scroll registers Lenis `raf` here with `autoRaf: false`).
159
+ * Callbacks run in registration order with no arguments; prefer sync functions. The same reference can be
160
+ * registered only once (a duplicate logs a warning and is ignored).
161
+ * @param callback Function called at the start of the frame (reads {@link Thread.timestamp} if needed).
162
+ * @example
163
+ * ```ts
164
+ * const step = (): void => { lenis.raf(Up.Thread.timestamp) }
165
+ *
166
+ * Up.Thread.addDriver(step)
167
+ * // later
168
+ * Up.Thread.removeDriver(step)
169
+ * ```
170
+ */
171
+ static addDriver(callback) {
172
+ if (Thread.#DRIVERS_CALLSTACK.has(callback)) return Logs.warn("THREAD", "THE DRIVER CALLBACK IS ALREADY REGISTERED.");
173
+ Thread.#DRIVERS_CALLSTACK.add(callback);
174
+ }
175
+ /**
176
+ * Registers a modifier: a lightweight hook that refreshes shared data before tasks of the same `flag`.
177
+ * After {@link Thread.sub} or {@link Thread.arm} with that flag, the modifier runs once in the modifiers phase
178
+ * of the next processed frame (at most once per frame), before forever/flow tasks. Modifiers run by ascending
179
+ * flag, then in registration order. Registering the same callback twice on the same flag logs a warning.
151
180
  * @note Keep modifiers cheap: they may run on many frames.
152
181
  * @note The `App` metric modifiers (on {@link Thread.RESIZE_FLAG} and {@link Thread.SCROLL_FLAG}) are registered first,
153
182
  * so they run before user modifiers of the same flag.
154
183
  * @param callback Function called without arguments.
155
- * @param flag Priority flag, a power of 2 from `1` to `4096`.
156
- * @throws If `flag` is not a power of 2 between `1` and `4096`.
184
+ * @param flag Priority flag, a power of 2 from `1` to {@link Thread.MAX_FLAG}.
185
+ * @throws If `flag` is not a power of 2 between `1` and {@link Thread.MAX_FLAG}.
157
186
  * @example
158
187
  * ```ts
159
188
  * let ratio = 1
@@ -202,11 +231,18 @@ class Thread {
202
231
  STACK.splice(INDEX, 1);
203
232
  Thread.#updateCurrentAfterRemoving(STACK, INDEX);
204
233
  }
234
+ /**
235
+ * Unregisters a driver added with {@link Thread.addDriver}. Does nothing if it is not registered.
236
+ * @param callback The same function reference passed to `addDriver`.
237
+ */
238
+ static removeDriver(callback) {
239
+ Thread.#DRIVERS_CALLSTACK.remove(callback);
240
+ }
205
241
  /**
206
242
  * Removes a modifier registered with {@link Thread.addModifier}. Does nothing if it is not registered on `flag`.
207
243
  * @param callback The same function reference passed to `addModifier`.
208
244
  * @param flag The flag used when adding the modifier.
209
- * @throws If `flag` is not a power of 2 between `1` and `4096`.
245
+ * @throws If `flag` is not a power of 2 between `1` and {@link Thread.MAX_FLAG}.
210
246
  */
211
247
  static removeModifier(callback, flag) {
212
248
  Thread.#checkFlag(flag);
@@ -220,7 +256,7 @@ class Thread {
220
256
  }
221
257
  // ##UPDATES
222
258
  /**
223
- * One scheduler frame: refresh `#delta` / `#timestamp`, then run modifiers → flow → forever.
259
+ * One scheduler frame: refresh `#delta` / `#timestamp`, then drivers → modifiers → tasks (forever then flow per flag).
224
260
  * - `STATUS === -1` (stopped): no-op (ticker may still invoke this).
225
261
  * - `STATUS === 0` (pending): adopt `timestamp`, set `#delta = 0`, promote to started, then return (no work this frame).
226
262
  * - `STATUS === 1` (started): normal tick.
@@ -239,9 +275,9 @@ class Thread {
239
275
  }
240
276
  Thread.#delta = (timestamp - Thread.#timestamp) / 1e3;
241
277
  Thread.#timestamp = timestamp;
278
+ Thread.#DRIVERS_CALLSTACK.loop();
242
279
  Thread.#execModifiers();
243
- Thread.#execFlowTasks();
244
- Thread.#execForeverTasks();
280
+ Thread.#execTasks();
245
281
  Thread.#stack = null;
246
282
  }
247
283
  /**
@@ -254,7 +290,7 @@ class Thread {
254
290
  }
255
291
  // ##DESTROY
256
292
  /**
257
- * Clears every modifier and task.
293
+ * Clears every modifier, task, and driver callback.
258
294
  */
259
295
  static #destroyVars() {
260
296
  Thread.#modifiersBitMask = Thread.#flowBitMask = Thread.#foreverBitMask = 0;
@@ -263,6 +299,7 @@ class Thread {
263
299
  if (Thread.#FOREVER_MAP.size) Thread.#FOREVER_MAP.clear();
264
300
  if (Thread.#FLOW_LISTED.size) Thread.#FLOW_LISTED.clear();
265
301
  if (Thread.#FOREVER_LISTED.size) Thread.#FOREVER_LISTED.clear();
302
+ Thread.#DRIVERS_CALLSTACK.clear();
266
303
  }
267
304
  /**
268
305
  * Stops the internal `requestAnimationFrame` loop.
@@ -274,7 +311,7 @@ class Thread {
274
311
  Thread.#tickID = -1;
275
312
  }
276
313
  /**
277
- * Clears all modifiers and tasks and stops the internal frame loop. Called from `Up.destroy` in the browser.
314
+ * Clears all modifiers, tasks, and drivers, and stops the internal frame loop. Called from `Up.destroy` in the browser.
278
315
  * @internal
279
316
  */
280
317
  static [destroy]() {
@@ -350,88 +387,79 @@ class Thread {
350
387
  Thread.#resolve(wrapper);
351
388
  return 1;
352
389
  }
353
- /**
354
- * Runs scheduled flow tasks by flag within the flow budget, keeping unfinished ones for the next frame.
355
- */
356
- static #execFlowTasks() {
357
- if (!Thread.#flowBitMask) return;
390
+ static #execTasks() {
391
+ let bitMask = Thread.#foreverBitMask | Thread.#flowBitMask;
392
+ if (!bitMask) return;
358
393
  const N1 = performance.now();
359
- let bitMask = Thread.#flowBitMask, n = 0, wrapper;
394
+ let foreverUsage = 0, foreverBitMask = Thread.#foreverBitMask, flowBitMask = Thread.#flowBitMask, n = 0, wrapper;
360
395
  Thread.#flowBitMask = 0;
361
396
  do {
362
397
  const FLAG = bitMask & -bitMask;
363
398
  bitMask ^= FLAG;
364
- Thread.#stack = Thread.#FLOW_MAP.get(FLAG);
365
- Thread.#max = Thread.#stack.length;
366
- if (!Thread.#max) continue;
367
- Thread.#i = 0;
368
- const N2 = n = performance.now(), BUDGET = (Thread.#flowBudget - N2 + N1) / (Thread.#getSize(bitMask) + 1);
369
- TASK_LOOP: do {
370
- const R = Thread.#execTask(wrapper = Thread.#stack[Thread.#i], BUDGET, N2, n);
371
- wrapper = Thread.#FLOW_LISTED.get(wrapper[0]);
372
- if (wrapper && wrapper[1] === FLAG) switch (R) {
373
- case 0:
374
- break TASK_LOOP;
375
- case 1: {
376
- if (wrapper[4]) {
377
- wrapper[4] = false;
378
- Thread.#stack[Thread.#i] = [Thread.App.void, FLAG, []];
379
- Thread.#stack.push(wrapper);
380
- } else Thread.#FLOW_LISTED.delete(wrapper[0]);
381
- continue TASK_LOOP;
382
- }
383
- case 2: {
384
- const TASK = wrapper[0];
385
- wrapper[5].finally(() => {
386
- wrapper = Thread.#FLOW_LISTED.get(TASK);
387
- if (!wrapper) return;
399
+ if (foreverBitMask & FLAG) {
400
+ foreverBitMask ^= FLAG;
401
+ Thread.#stack = Thread.#FOREVER_MAP.get(FLAG);
402
+ Thread.#max = Thread.#stack.length;
403
+ if (Thread.#max) {
404
+ Thread.#i = 0;
405
+ const N2 = performance.now(), BUDGET = (Thread.#foreverBudget - N2 + N1) / (Thread.#getSize(foreverBitMask) + 1);
406
+ do {
407
+ Thread.#execTask(Thread.#stack[Thread.#i], BUDGET, N2);
408
+ } while (++Thread.#i < Thread.#max);
409
+ foreverUsage += performance.now() - N2;
410
+ } else Thread.#foreverBitMask ^= FLAG;
411
+ }
412
+ if (flowBitMask & FLAG) {
413
+ flowBitMask ^= FLAG;
414
+ Thread.#stack = Thread.#FLOW_MAP.get(FLAG);
415
+ Thread.#max = Thread.#stack.length;
416
+ if (!Thread.#max) continue;
417
+ Thread.#i = 0;
418
+ const N2 = n = performance.now(), BUDGET = (Thread.#flowBudget - N2 + N1 + foreverUsage) / (Thread.#getSize(flowBitMask) + 1);
419
+ FLOW_TASK_LOOP: do {
420
+ const R = Thread.#execTask(wrapper = Thread.#stack[Thread.#i], BUDGET, N2, n);
421
+ wrapper = Thread.#FLOW_LISTED.get(wrapper[0]);
422
+ if (wrapper && wrapper[1] === FLAG) switch (R) {
423
+ case 0:
424
+ break FLOW_TASK_LOOP;
425
+ case 1: {
388
426
  if (wrapper[4]) {
389
427
  wrapper[4] = false;
390
- const STACK = Thread.#FLOW_MAP.get(wrapper[1]);
391
- if (!STACK) Thread.#FLOW_MAP.set(wrapper[1], [wrapper]);
392
- else if (!STACK.includes(wrapper)) STACK.push(wrapper);
393
- Thread.#flowBitMask |= wrapper[1];
428
+ Thread.#stack[Thread.#i] = [Thread.App.void, FLAG, []];
429
+ Thread.#stack.push(wrapper);
394
430
  } else Thread.#FLOW_LISTED.delete(wrapper[0]);
395
- });
431
+ continue FLOW_TASK_LOOP;
432
+ }
433
+ case 2: {
434
+ const TASK = wrapper[0];
435
+ wrapper[5].finally(() => {
436
+ wrapper = Thread.#FLOW_LISTED.get(TASK);
437
+ if (!wrapper) return;
438
+ if (wrapper[4]) {
439
+ wrapper[4] = false;
440
+ const STACK = Thread.#FLOW_MAP.get(wrapper[1]);
441
+ if (!STACK) Thread.#FLOW_MAP.set(wrapper[1], [wrapper]);
442
+ else if (!STACK.includes(wrapper)) STACK.push(wrapper);
443
+ Thread.#flowBitMask |= wrapper[1];
444
+ } else Thread.#FLOW_LISTED.delete(wrapper[0]);
445
+ });
446
+ }
447
+ default:
448
+ continue FLOW_TASK_LOOP;
396
449
  }
397
- default:
398
- continue TASK_LOOP;
399
- }
400
- else if (!R) {
401
- ++Thread.#i;
402
- break TASK_LOOP;
403
- }
404
- } while (++Thread.#i < Thread.#max && (n = performance.now()) - N2 < BUDGET);
405
- Thread.#FLOW_MAP.set(FLAG, Thread.#stack = Thread.#stack.splice(Thread.#i));
406
- if (Thread.#stack.length) Thread.#flowBitMask |= FLAG;
407
- } while (bitMask);
408
- }
409
- /**
410
- * Runs every forever task by flag, sharing the forever budget between flags.
411
- */
412
- static #execForeverTasks() {
413
- if (!Thread.#foreverBitMask) return;
414
- const N1 = performance.now();
415
- let bitMask = Thread.#foreverBitMask;
416
- do {
417
- const FLAG = bitMask & -bitMask;
418
- bitMask ^= FLAG;
419
- Thread.#stack = Thread.#FOREVER_MAP.get(FLAG);
420
- Thread.#max = Thread.#stack.length;
421
- if (!Thread.#max) {
422
- Thread.#foreverBitMask ^= FLAG;
423
- continue;
450
+ else if (!R) {
451
+ ++Thread.#i;
452
+ break FLOW_TASK_LOOP;
453
+ }
454
+ } while (++Thread.#i < Thread.#max && (n = performance.now()) - N2 < BUDGET);
455
+ Thread.#FLOW_MAP.set(FLAG, Thread.#stack = Thread.#stack.splice(Thread.#i));
456
+ if (Thread.#stack.length) Thread.#flowBitMask |= FLAG;
424
457
  }
425
- Thread.#i = 0;
426
- const N2 = performance.now(), BUDGET = (Thread.#foreverBudget - N2 + N1) / (Thread.#getSize(bitMask) + 1);
427
- do {
428
- Thread.#execTask(Thread.#stack[Thread.#i], BUDGET, N2);
429
- } while (++Thread.#i < Thread.#max);
430
458
  } while (bitMask);
431
459
  }
432
460
  // ##UTILS
433
461
  /**
434
- * Throws if `flag` is not a power of 2 between `1` and `MAX_FLAG`.
462
+ * Throws if `flag` is not a power of 2 between `1` and {@link Thread.MAX_FLAG}.
435
463
  */
436
464
  static #checkFlag(flag) {
437
465
  if (flag < 1 || flag > Thread.#MAX_FLAG || (flag & flag - 1) !== 0) Logs.throw("THREAD", `THE FLAG MUST BE A POWER OF 2 AND LESS THAN OR EQUAL TO ${Thread.#MAX_FLAG}.`);
@@ -491,9 +519,9 @@ class Thread {
491
519
  * (the task stays scheduled until the promise settles).
492
520
  * @template T Task type, used to type `args`.
493
521
  * @param task The task function, identified by reference.
494
- * @param flag Priority flag, a power of 2 from `1` to `4096`. Default: {@link Thread.MAX_FLAG} (`4096`).
522
+ * @param flag Priority flag, a power of 2 from `1` to {@link Thread.MAX_FLAG}. Default: {@link Thread.DEFAULT_FLAG} (`8192`).
495
523
  * @param args Arguments passed to the task when it runs (the latest call wins).
496
- * @throws If `flag` is not a power of 2 between `1` and `4096`.
524
+ * @throws If `flag` is not a power of 2 between `1` and {@link Thread.MAX_FLAG} (`32768`).
497
525
  * @example
498
526
  * ```ts
499
527
  * function* build(items: HTMLElement[]): Generator<void, void, void>
@@ -518,7 +546,7 @@ class Thread {
518
546
  Thread.#add(Thread.#FLOW_MAP, Thread.#FLOW_LISTED, task, FLAG, args);
519
547
  Thread.#flowBitMask |= FLAG;
520
548
  }
521
- if (Thread.#MODIFIERS_MAP.has(FLAG)) Thread.#modifiersBitMask |= FLAG;
549
+ Thread.arm(FLAG);
522
550
  }
523
551
  /**
524
552
  * Removes a scheduled task. A pending {@link Thread.wait} promise is resolved and a running generator is closed.
@@ -528,6 +556,20 @@ class Thread {
528
556
  static unsub(task) {
529
557
  task[FOREVER] ? Thread.#remove(Thread.#FOREVER_MAP, Thread.#FOREVER_LISTED, task) : Thread.#remove(Thread.#FLOW_MAP, Thread.#FLOW_LISTED, task);
530
558
  }
559
+ /**
560
+ * Arms modifiers of `flag` for the next modifiers phase (same frame if called from a driver or before modifiers run).
561
+ * No-op when no modifiers are registered on `flag`. {@link Thread.sub} calls this automatically; call it yourself
562
+ * when an external writer updates shared state without scheduling a task (e.g. Scroll arms {@link Thread.SCROLL_FLAG}
563
+ * after Lenis writes `window.scrollY`, so `App.updateScrollVars` and ScrollTrigger modifiers run).
564
+ * @param flag Priority flag whose modifiers should run.
565
+ * @example
566
+ * ```ts
567
+ * lenis.on('scroll', () => Up.Thread.arm(32))
568
+ * ```
569
+ */
570
+ static arm(flag) {
571
+ if (Thread.#MODIFIERS_MAP.has(flag)) Thread.#modifiersBitMask |= flag;
572
+ }
531
573
  /**
532
574
  * Returns a promise that resolves when a scheduled task completes its next (or current) run, or when it is unsubscribed.
533
575
  * @param task The same function reference passed to {@link Thread.sub}.