elements-kit 0.0.2 → 0.0.3

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.
@@ -0,0 +1,966 @@
1
+ //#region src/signals/system.ts
2
+ /**
3
+ * Bitmask flags that encode the current state of a {@link ReactiveNode}.
4
+ *
5
+ * | Flag | Meaning |
6
+ * |-----------------|---------|
7
+ * | `None` | Clean, not tracking. |
8
+ * | `Mutable` | Node can propagate value changes downstream (signal / computed). |
9
+ * | `Watching` | Node is actively watching for changes and should be notified (effect). |
10
+ * | `RecursedCheck` | Node is mid-execution; used to detect self-referential cycles. |
11
+ * | `Recursed` | Node was found to be recursed during propagation. |
12
+ * | `Dirty` | Node's value is known to be stale; must recompute before reading. |
13
+ * | `Pending` | A dep *might* be dirty; check before deciding whether to recompute. |
14
+ */
15
+ let ReactiveFlags = /* @__PURE__ */ function(ReactiveFlags) {
16
+ ReactiveFlags[ReactiveFlags["None"] = 0] = "None";
17
+ ReactiveFlags[ReactiveFlags["Mutable"] = 1] = "Mutable";
18
+ ReactiveFlags[ReactiveFlags["Watching"] = 2] = "Watching";
19
+ ReactiveFlags[ReactiveFlags["RecursedCheck"] = 4] = "RecursedCheck";
20
+ ReactiveFlags[ReactiveFlags["Recursed"] = 8] = "Recursed";
21
+ ReactiveFlags[ReactiveFlags["Dirty"] = 16] = "Dirty";
22
+ ReactiveFlags[ReactiveFlags["Pending"] = 32] = "Pending";
23
+ return ReactiveFlags;
24
+ }({});
25
+ /**
26
+ * Creates and returns the five core graph-manipulation functions that together
27
+ * implement push-pull reactive dependency tracking.
28
+ *
29
+ * The caller supplies three callbacks that customise high-level behaviour while
30
+ * the returned functions handle all graph bookkeeping:
31
+ *
32
+ * @param update - Called when a Mutable+Dirty node needs to recompute.
33
+ * Return `true` if the computed value changed (triggers
34
+ * downstream dirty propagation).
35
+ * @param notify - Called when a Watching node should be scheduled for
36
+ * re-execution (i.e. a dep became dirty).
37
+ * @param unwatched - Called when a dep node's last subscriber is removed.
38
+ * The caller may choose to stop the node or keep it alive.
39
+ *
40
+ * @returns `{ link, unlink, propagate, checkDirty, shallowPropagate }`
41
+ */
42
+ function createReactiveSystem({ update, notify, unwatched }) {
43
+ return {
44
+ link,
45
+ unlink,
46
+ propagate,
47
+ checkDirty,
48
+ shallowPropagate
49
+ };
50
+ /**
51
+ * Records that `sub` now depends on `dep` at the given `version`.
52
+ *
53
+ * Duplicate links (same dep/sub pair within the same tracking cycle) are
54
+ * detected and skipped in O(1) by checking the tail of both chains before
55
+ * allocating a new `Link` object.
56
+ *
57
+ * @param dep - The upstream node being read.
58
+ * @param sub - The downstream node doing the reading.
59
+ * @param version - Current logical clock value; used for stale-link detection.
60
+ */
61
+ function link(dep, sub, version) {
62
+ const prevDep = sub.depsTail;
63
+ if (prevDep !== void 0 && prevDep.dep === dep) return;
64
+ const nextDep = prevDep !== void 0 ? prevDep.nextDep : sub.deps;
65
+ if (nextDep !== void 0 && nextDep.dep === dep) {
66
+ nextDep.version = version;
67
+ sub.depsTail = nextDep;
68
+ return;
69
+ }
70
+ const prevSub = dep.subsTail;
71
+ if (prevSub !== void 0 && prevSub.version === version && prevSub.sub === sub) return;
72
+ const newLink = sub.depsTail = dep.subsTail = {
73
+ version,
74
+ dep,
75
+ sub,
76
+ prevDep,
77
+ nextDep,
78
+ prevSub,
79
+ nextSub: void 0
80
+ };
81
+ if (nextDep !== void 0) nextDep.prevDep = newLink;
82
+ if (prevDep !== void 0) prevDep.nextDep = newLink;
83
+ else sub.deps = newLink;
84
+ if (prevSub !== void 0) prevSub.nextSub = newLink;
85
+ else dep.subs = newLink;
86
+ }
87
+ /**
88
+ * Removes a link from both the dep-chain and the sub-chain.
89
+ *
90
+ * If removing the link leaves the dep with **no remaining subscribers**,
91
+ * {@link unwatched} is called on the dep so the caller can decide whether to
92
+ * stop tracking it.
93
+ *
94
+ * @param link - The edge to remove.
95
+ * @param sub - The subscriber owning the dep-chain (defaults to `link.sub`).
96
+ * @returns The next link in the subscriber's dep-chain, or `undefined`.
97
+ */
98
+ function unlink(link, sub = link.sub) {
99
+ const dep = link.dep;
100
+ const prevDep = link.prevDep;
101
+ const nextDep = link.nextDep;
102
+ const nextSub = link.nextSub;
103
+ const prevSub = link.prevSub;
104
+ if (nextDep !== void 0) nextDep.prevDep = prevDep;
105
+ else sub.depsTail = prevDep;
106
+ if (prevDep !== void 0) prevDep.nextDep = nextDep;
107
+ else sub.deps = nextDep;
108
+ if (nextSub !== void 0) nextSub.prevSub = prevSub;
109
+ else dep.subsTail = prevSub;
110
+ if (prevSub !== void 0) prevSub.nextSub = nextSub;
111
+ else if ((dep.subs = nextSub) === void 0) unwatched(dep);
112
+ return nextDep;
113
+ }
114
+ /**
115
+ * Performs a **deep, push-based** propagation starting from `link`.
116
+ *
117
+ * Traverses the subscriber graph breadth-first (using an explicit stack to
118
+ * avoid call-stack overflows on deep graphs), marking each reachable node:
119
+ *
120
+ * - Watching nodes (effects) are passed to {@link notify}.
121
+ * - Mutable nodes (computeds) are traversed further so their subscribers are
122
+ * also marked.
123
+ * - Already-dirty or recursed nodes are handled conservatively to avoid
124
+ * duplicate notifications.
125
+ *
126
+ * @param link - The first subscriber link from which propagation begins.
127
+ */
128
+ function propagate(link) {
129
+ let next = link.nextSub;
130
+ let stack;
131
+ top: do {
132
+ const sub = link.sub;
133
+ let flags = sub.flags;
134
+ if (!(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed | ReactiveFlags.Dirty | ReactiveFlags.Pending))) sub.flags = flags | ReactiveFlags.Pending;
135
+ else if (!(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed))) flags = ReactiveFlags.None;
136
+ else if (!(flags & ReactiveFlags.RecursedCheck)) sub.flags = flags & ~ReactiveFlags.Recursed | ReactiveFlags.Pending;
137
+ else if (!(flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) && isValidLink(link, sub)) {
138
+ sub.flags = flags | (ReactiveFlags.Recursed | ReactiveFlags.Pending);
139
+ flags &= ReactiveFlags.Mutable;
140
+ } else flags = ReactiveFlags.None;
141
+ if (flags & ReactiveFlags.Watching) notify(sub);
142
+ if (flags & ReactiveFlags.Mutable) {
143
+ const subSubs = sub.subs;
144
+ if (subSubs !== void 0) {
145
+ const nextSub = (link = subSubs).nextSub;
146
+ if (nextSub !== void 0) {
147
+ stack = {
148
+ value: next,
149
+ prev: stack
150
+ };
151
+ next = nextSub;
152
+ }
153
+ continue;
154
+ }
155
+ }
156
+ if ((link = next) !== void 0) {
157
+ next = link.nextSub;
158
+ continue;
159
+ }
160
+ while (stack !== void 0) {
161
+ link = stack.value;
162
+ stack = stack.prev;
163
+ if (link !== void 0) {
164
+ next = link.nextSub;
165
+ continue top;
166
+ }
167
+ }
168
+ break;
169
+ } while (true);
170
+ }
171
+ /**
172
+ * **Pull-based** dirty check: walks up the dep graph from `sub` to determine
173
+ * whether any ancestor signal has actually changed.
174
+ *
175
+ * This is the "lazy" half of the push-pull model. After `propagate` marks a
176
+ * computed as `Pending`, `checkDirty` is called before the computed is read
177
+ * to decide whether a full recompute is warranted. It short-circuits as soon
178
+ * as it confirms the node is dirty (or clean).
179
+ *
180
+ * @param link - First dep link of the node being checked.
181
+ * @param sub - The node whose dirtiness is in question.
182
+ * @returns `true` if the node must recompute, `false` if it is still clean.
183
+ */
184
+ function checkDirty(link, sub) {
185
+ let stack;
186
+ let checkDepth = 0;
187
+ let dirty = false;
188
+ top: do {
189
+ const dep = link.dep;
190
+ const flags = dep.flags;
191
+ if (sub.flags & ReactiveFlags.Dirty) dirty = true;
192
+ else if ((flags & (ReactiveFlags.Mutable | ReactiveFlags.Dirty)) === (ReactiveFlags.Mutable | ReactiveFlags.Dirty)) {
193
+ if (update(dep)) {
194
+ const subs = dep.subs;
195
+ if (subs.nextSub !== void 0) shallowPropagate(subs);
196
+ dirty = true;
197
+ }
198
+ } else if ((flags & (ReactiveFlags.Mutable | ReactiveFlags.Pending)) === (ReactiveFlags.Mutable | ReactiveFlags.Pending)) {
199
+ if (link.nextSub !== void 0 || link.prevSub !== void 0) stack = {
200
+ value: link,
201
+ prev: stack
202
+ };
203
+ link = dep.deps;
204
+ sub = dep;
205
+ ++checkDepth;
206
+ continue;
207
+ }
208
+ if (!dirty) {
209
+ const nextDep = link.nextDep;
210
+ if (nextDep !== void 0) {
211
+ link = nextDep;
212
+ continue;
213
+ }
214
+ }
215
+ while (checkDepth--) {
216
+ const firstSub = sub.subs;
217
+ const hasMultipleSubs = firstSub.nextSub !== void 0;
218
+ if (hasMultipleSubs) {
219
+ link = stack.value;
220
+ stack = stack.prev;
221
+ } else link = firstSub;
222
+ if (dirty) {
223
+ if (update(sub)) {
224
+ if (hasMultipleSubs) shallowPropagate(firstSub);
225
+ sub = link.sub;
226
+ continue;
227
+ }
228
+ dirty = false;
229
+ } else sub.flags &= ~ReactiveFlags.Pending;
230
+ sub = link.sub;
231
+ const nextDep = link.nextDep;
232
+ if (nextDep !== void 0) {
233
+ link = nextDep;
234
+ continue top;
235
+ }
236
+ }
237
+ return dirty;
238
+ } while (true);
239
+ }
240
+ /**
241
+ * Marks all direct subscribers of `link` as `Dirty` (if they were only
242
+ * `Pending`) and notifies any Watching subscribers.
243
+ *
244
+ * Used after a computed node is found to have changed during `checkDirty`, to
245
+ * eagerly mark its immediate subscribers without recursing into the full graph.
246
+ *
247
+ * @param link - The first subscriber link of the changed computed.
248
+ */
249
+ function shallowPropagate(link) {
250
+ do {
251
+ const sub = link.sub;
252
+ const flags = sub.flags;
253
+ if ((flags & (ReactiveFlags.Pending | ReactiveFlags.Dirty)) === ReactiveFlags.Pending) {
254
+ sub.flags = flags | ReactiveFlags.Dirty;
255
+ if ((flags & (ReactiveFlags.Watching | ReactiveFlags.RecursedCheck)) === ReactiveFlags.Watching) notify(sub);
256
+ }
257
+ } while ((link = link.nextSub) !== void 0);
258
+ }
259
+ /**
260
+ * Returns `true` if `checkLink` is still present in `sub`'s current dep-chain.
261
+ *
262
+ * Used during propagation to verify that a link is valid for a recursed node
263
+ * before upgrading its flags to `Recursed | Pending`.
264
+ *
265
+ * @internal
266
+ */
267
+ function isValidLink(checkLink, sub) {
268
+ let link = sub.depsTail;
269
+ while (link !== void 0) {
270
+ if (link === checkLink) return true;
271
+ link = link.prevDep;
272
+ }
273
+ return false;
274
+ }
275
+ }
276
+ //#endregion
277
+ //#region src/signals/lib.ts
278
+ /**
279
+ * @module signals
280
+ *
281
+ * High-level reactive primitives built on top of the low-level graph engine in
282
+ * `./system`.
283
+ *
284
+ * The API surface intentionally mirrors alien-signals but extends it with
285
+ * first-class **cleanup support** via {@link onCleanup}.
286
+ *
287
+ * ### Primitives
288
+ * | Export | Role |
289
+ * |--------|------|
290
+ * | {@link signal} | Mutable reactive value. |
291
+ * | {@link computed} | Derived, lazily-evaluated value. |
292
+ * | {@link effect} | Side-effect that re-runs when its deps change. |
293
+ * | {@link effectScope} | Ownership scope that groups and disposes nested effects together. |
294
+ * | {@link onCleanup} | Register a teardown callback inside the currently running effect. |
295
+ * | {@link batch} | Defer flush until all synchronous mutations are done. |
296
+ * | {@link untracked} | Read signals without creating dependency links. |
297
+ * | {@link trigger} | Manually re-trigger subscribers of signals read inside `fn`. |
298
+ *
299
+ * ### Cleanup model
300
+ * `onCleanup(fn)` registers a callback that fires:
301
+ * 1. **Before each re-run** – so resources set up in the previous run are torn
302
+ * down before the effect body executes again.
303
+ * 2. **On disposal** – whether the effect is stopped explicitly (calling the
304
+ * handle returned by `effect()`) or implicitly (an owning `effectScope` is
305
+ * disposed, cascading through all nested effects).
306
+ *
307
+ * Because `onCleanup` reads `activeSub` (the same module-level variable that
308
+ * signals use for dependency tracking), it works correctly when called from
309
+ * deeply nested helper functions during effect execution – no prop-drilling
310
+ * required.
311
+ */
312
+ /** Monotonically increasing counter; incremented on each tracking run to stamp links. */
313
+ let cycle = 0;
314
+ /** Depth counter for nested `batch` calls; flush is deferred while > 0. */
315
+ let batchDepth = 0;
316
+ /** Read cursor into the `queued` array during flush. */
317
+ let notifyIndex = 0;
318
+ /** Write cursor into the `queued` array (logical length of the queue). */
319
+ let queuedLength = 0;
320
+ /**
321
+ * The currently executing subscriber (effect, computed, or effectScope).
322
+ * Signals read while `activeSub !== undefined` automatically register a dep
323
+ * link back to this node.
324
+ */
325
+ let activeSub;
326
+ /** Ring-buffer of effects waiting to be flushed. */
327
+ const queued = [];
328
+ const { link, unlink, propagate, checkDirty, shallowPropagate } = createReactiveSystem({
329
+ update(node) {
330
+ if (node.depsTail !== void 0) return updateComputed(node);
331
+ else return updateSignal(node);
332
+ },
333
+ notify(effect) {
334
+ let insertIndex = queuedLength;
335
+ let firstInsertedIndex = insertIndex;
336
+ do {
337
+ queued[insertIndex++] = effect;
338
+ effect.flags &= ~ReactiveFlags.Watching;
339
+ effect = effect.subs?.sub;
340
+ if (effect === void 0 || !(effect.flags & ReactiveFlags.Watching)) break;
341
+ } while (true);
342
+ queuedLength = insertIndex;
343
+ while (firstInsertedIndex < --insertIndex) {
344
+ const left = queued[firstInsertedIndex];
345
+ queued[firstInsertedIndex++] = queued[insertIndex];
346
+ queued[insertIndex] = left;
347
+ }
348
+ },
349
+ unwatched(node) {
350
+ if (!(node.flags & ReactiveFlags.Mutable)) effectScopeOper.call(node);
351
+ else if (node.depsTail !== void 0) {
352
+ node.depsTail = void 0;
353
+ node.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty;
354
+ purgeDeps(node);
355
+ }
356
+ }
357
+ });
358
+ /**
359
+ * Replaces the active subscriber with `sub` and returns the previous value.
360
+ *
361
+ * Always restore the previous value in a `finally` block:
362
+ *
363
+ * ```ts
364
+ * const prev = setActiveSub(myNode);
365
+ * try { ... } finally { setActiveSub(prev); }
366
+ * ```
367
+ */
368
+ function setActiveSub(sub) {
369
+ const prevSub = activeSub;
370
+ activeSub = sub;
371
+ return prevSub;
372
+ }
373
+ /**
374
+ * Increments the batch depth, deferring effect flush until `endBatch` is
375
+ * called a matching number of times.
376
+ *
377
+ * Prefer {@link batch} over calling `startBatch` / `endBatch` directly.
378
+ */
379
+ function startBatch() {
380
+ ++batchDepth;
381
+ }
382
+ /**
383
+ * Decrements the batch depth and flushes the effect queue when the depth
384
+ * reaches zero.
385
+ *
386
+ * Prefer {@link batch} over calling `startBatch` / `endBatch` directly.
387
+ */
388
+ function endBatch() {
389
+ if (!--batchDepth) flush();
390
+ }
391
+ /**
392
+ * Returns `true` if `fn` is a signal handle created by {@link signal}.
393
+ *
394
+ * Relies on `Function.name` matching the internal `signalOper` function name.
395
+ */
396
+ function isSignal(fn) {
397
+ return fn.name === "bound " + signalOper.name;
398
+ }
399
+ /**
400
+ * Returns `true` if `fn` is a computed handle created by {@link computed}.
401
+ *
402
+ * Relies on `Function.name` matching the internal `computedOper` function name.
403
+ */
404
+ function isComputed(fn) {
405
+ return fn.name === "bound " + computedOper.name;
406
+ }
407
+ /**
408
+ * Returns `true` if `fn` is an effect cleanup handle created by {@link effect}.
409
+ *
410
+ * Relies on `Function.name` matching the internal `effectOper` function name.
411
+ */
412
+ function isEffect(fn) {
413
+ return fn.name === "bound " + effectOper.name;
414
+ }
415
+ /**
416
+ * Returns `true` if `fn` is an effectScope cleanup handle created by
417
+ * {@link effectScope}.
418
+ *
419
+ * Relies on `Function.name` matching the internal `effectScopeOper` function name.
420
+ */
421
+ function isEffectScope(fn) {
422
+ return fn.name === "bound " + effectScopeOper.name;
423
+ }
424
+ function signal(initialValue) {
425
+ return signalOper.bind({
426
+ currentValue: initialValue,
427
+ pendingValue: initialValue,
428
+ subs: void 0,
429
+ subsTail: void 0,
430
+ flags: ReactiveFlags.Mutable
431
+ });
432
+ }
433
+ /**
434
+ * Creates a lazily-evaluated computed value.
435
+ *
436
+ * The `getter` is only called when the computed value is read **and** one of
437
+ * its dependencies has changed since the last evaluation. If nothing has
438
+ * changed the cached `value` is returned without re-running `getter`.
439
+ *
440
+ * Computed values are read-only; they cannot be set directly.
441
+ *
442
+ * @param getter - Pure function deriving a value from other reactive sources.
443
+ * Receives the previous value as an optional optimisation hint.
444
+ *
445
+ * @example
446
+ * ```ts
447
+ * const a = signal(1);
448
+ * const b = signal(2);
449
+ * const sum = computed(() => a() + b());
450
+ *
451
+ * sum(); // → 3
452
+ * a(10);
453
+ * sum(); // → 12 (re-evaluated lazily)
454
+ * ```
455
+ */
456
+ function computed(getter) {
457
+ return computedOper.bind({
458
+ value: void 0,
459
+ subs: void 0,
460
+ subsTail: void 0,
461
+ deps: void 0,
462
+ depsTail: void 0,
463
+ flags: ReactiveFlags.None,
464
+ getter
465
+ });
466
+ }
467
+ /**
468
+ * Creates a reactive side-effect that runs immediately and re-runs whenever
469
+ * any signal or computed it read during its last execution changes.
470
+ *
471
+ * Use {@link onCleanup} inside `fn` to register teardown logic that runs
472
+ * before each re-execution and on final disposal.
473
+ *
474
+ * If `effect` is called inside an `effectScope` or another `effect`, the
475
+ * new effect is automatically owned by the outer scope and will be disposed
476
+ * when the scope is disposed.
477
+ *
478
+ * @param fn - The side-effect body. Reactive reads inside this function
479
+ * establish dependency links.
480
+ * @returns A disposal function. Call it to stop the effect and run any
481
+ * registered cleanup.
482
+ *
483
+ * @example
484
+ * ```ts
485
+ * const url = signal('/api/data');
486
+ *
487
+ * const stop = effect(() => {
488
+ * const controller = new AbortController();
489
+ * fetch(url(), { signal: controller.signal });
490
+ * onCleanup(() => controller.abort());
491
+ * });
492
+ *
493
+ * url('/api/other'); // previous fetch is aborted, new one starts
494
+ * stop(); // final cleanup: abort the last fetch
495
+ * ```
496
+ */
497
+ function effect(fn) {
498
+ const e = {
499
+ fn,
500
+ subs: void 0,
501
+ subsTail: void 0,
502
+ deps: void 0,
503
+ depsTail: void 0,
504
+ flags: ReactiveFlags.Watching | ReactiveFlags.RecursedCheck
505
+ };
506
+ const prevSub = setActiveSub(e);
507
+ if (prevSub !== void 0) link(e, prevSub, 0);
508
+ try {
509
+ e.fn();
510
+ } finally {
511
+ activeSub = prevSub;
512
+ e.flags &= ~ReactiveFlags.RecursedCheck;
513
+ }
514
+ return effectOper.bind(e);
515
+ }
516
+ /**
517
+ * Creates an ownership scope that groups reactive effects so they can all be
518
+ * disposed at once.
519
+ *
520
+ * Effects and nested scopes created inside `fn` are linked to this scope.
521
+ * When the returned disposal function is called, all owned effects are stopped
522
+ * in cascade – triggering their registered {@link onCleanup} callbacks – and
523
+ * the scope itself is removed from any parent scope that owns it.
524
+ *
525
+ * @param fn - Synchronous setup function. Create effects and nested scopes
526
+ * here.
527
+ * @returns A disposal function that tears down all owned effects and the scope
528
+ * itself.
529
+ *
530
+ * @example
531
+ * ```ts
532
+ * const stopAll = effectScope(() => {
533
+ * effect(() => console.log('a:', a()));
534
+ * effect(() => console.log('b:', b()));
535
+ * });
536
+ *
537
+ * stopAll(); // both effects stopped simultaneously
538
+ * ```
539
+ */
540
+ function effectScope(fn) {
541
+ const e = {
542
+ deps: void 0,
543
+ depsTail: void 0,
544
+ subs: void 0,
545
+ subsTail: void 0,
546
+ flags: ReactiveFlags.None
547
+ };
548
+ const prevSub = setActiveSub(e);
549
+ if (prevSub !== void 0) link(e, prevSub, 0);
550
+ try {
551
+ fn();
552
+ } finally {
553
+ activeSub = prevSub;
554
+ }
555
+ return effectScopeOper.bind(e);
556
+ }
557
+ /**
558
+ * Registers a cleanup callback for the currently executing effect or scope.
559
+ *
560
+ * The callback will be called:
561
+ * 1. **Before the next re-run** of the enclosing effect (so resources from
562
+ * the previous run are released before the new run sets them up again).
563
+ * 2. **On final disposal** of the effect, whether triggered explicitly by
564
+ * calling the effect's cleanup handle or implicitly by an owning
565
+ * `effectScope` being disposed.
566
+ *
567
+ * Calling `onCleanup` outside of a tracking context (no active effect) is a
568
+ * no-op; it does **not** throw.
569
+ *
570
+ * Only one cleanup function per effect run is supported. Calling `onCleanup`
571
+ * multiple times within the same run overwrites the previous registration.
572
+ *
573
+ * @param fn - The teardown callback.
574
+ *
575
+ * @example
576
+ * ```ts
577
+ * effect(() => {
578
+ * const id = setInterval(() => tick(), 1000);
579
+ * onCleanup(() => clearInterval(id));
580
+ * });
581
+ * ```
582
+ *
583
+ * @example Composable helper – no prop-drilling needed:
584
+ * ```ts
585
+ * function useEventListener(target: EventTarget, type: string, handler: EventListener) {
586
+ * target.addEventListener(type, handler);
587
+ * onCleanup(() => target.removeEventListener(type, handler));
588
+ * }
589
+ *
590
+ * effect(() => {
591
+ * useEventListener(window, 'resize', onResize);
592
+ * });
593
+ * ```
594
+ */
595
+ function onCleanup(fn) {
596
+ if (activeSub !== void 0) activeSub.onCleanup = fn;
597
+ }
598
+ /**
599
+ * Runs `fn` as a single atomic update: all signal writes inside `fn` are
600
+ * collected and effects are flushed only once after `fn` returns, rather than
601
+ * after each individual write.
602
+ *
603
+ * Batches can be nested; the flush only occurs when the outermost batch
604
+ * completes.
605
+ *
606
+ * @example
607
+ * ```ts
608
+ * batch(() => {
609
+ * x(1);
610
+ * y(2);
611
+ * z(3);
612
+ * }); // effects that depend on x, y, or z run once here
613
+ * ```
614
+ */
615
+ function batch(fn) {
616
+ startBatch();
617
+ try {
618
+ fn();
619
+ } finally {
620
+ endBatch();
621
+ }
622
+ }
623
+ /**
624
+ * Executes `fn` in a non-tracking context: any signals read inside `fn` do
625
+ * **not** create dependency links on the currently active subscriber.
626
+ *
627
+ * Useful when you need to read a signal's current value without subscribing to
628
+ * future changes.
629
+ *
630
+ * @returns The value returned by `fn`.
631
+ *
632
+ * @example
633
+ * ```ts
634
+ * const logCount = effect(() => {
635
+ * console.log('triggered by a:', a());
636
+ * // read b without subscribing – effect won't re-run when b changes
637
+ * console.log('current b:', untracked(() => b()));
638
+ * });
639
+ * ```
640
+ */
641
+ function untracked(fn) {
642
+ const prev = setActiveSub(void 0);
643
+ try {
644
+ return fn();
645
+ } finally {
646
+ setActiveSub(prev);
647
+ }
648
+ }
649
+ /**
650
+ * Manually triggers all subscribers of every signal read inside `fn`.
651
+ *
652
+ * Unlike writing to a signal, `trigger` does not change the signal's value; it
653
+ * only forces downstream effects and computeds to re-evaluate.
654
+ *
655
+ * @param fn - Function whose reactive reads identify the signals to trigger.
656
+ *
657
+ * @example
658
+ * ```ts
659
+ * const items = signal([1, 2, 3]);
660
+ *
661
+ * // Mutate in place (referential equality won't detect the change):
662
+ * items().push(4);
663
+ * trigger(() => items()); // manually notify subscribers
664
+ * ```
665
+ */
666
+ function trigger(fn) {
667
+ const sub = {
668
+ deps: void 0,
669
+ depsTail: void 0,
670
+ flags: ReactiveFlags.Watching
671
+ };
672
+ const prevSub = setActiveSub(sub);
673
+ try {
674
+ fn();
675
+ } finally {
676
+ activeSub = prevSub;
677
+ let link = sub.deps;
678
+ while (link !== void 0) {
679
+ const dep = link.dep;
680
+ link = unlink(link, sub);
681
+ const subs = dep.subs;
682
+ if (subs !== void 0) {
683
+ sub.flags = ReactiveFlags.None;
684
+ propagate(subs);
685
+ shallowPropagate(subs);
686
+ }
687
+ }
688
+ if (!batchDepth) flush();
689
+ }
690
+ }
691
+ /**
692
+ * Recomputes a computed node and returns whether its value changed.
693
+ * Called by the graph engine's `update` callback and by `computedOper` itself.
694
+ * @internal
695
+ */
696
+ function updateComputed(c) {
697
+ ++cycle;
698
+ c.depsTail = void 0;
699
+ c.flags = ReactiveFlags.Mutable | ReactiveFlags.RecursedCheck;
700
+ const prevSub = setActiveSub(c);
701
+ try {
702
+ const oldValue = c.value;
703
+ return oldValue !== (c.value = c.getter(oldValue));
704
+ } finally {
705
+ activeSub = prevSub;
706
+ c.flags &= ~ReactiveFlags.RecursedCheck;
707
+ purgeDeps(c);
708
+ }
709
+ }
710
+ /**
711
+ * Commits a signal's pending value to its current value.
712
+ * Returns `true` if the value actually changed.
713
+ * @internal
714
+ */
715
+ function updateSignal(s) {
716
+ s.flags = ReactiveFlags.Mutable;
717
+ return s.currentValue !== (s.currentValue = s.pendingValue);
718
+ }
719
+ /**
720
+ * Executes an effect node if it is dirty or pending-dirty.
721
+ *
722
+ * Before re-running the effect body, any cleanup registered during the
723
+ * previous run is called and cleared.
724
+ *
725
+ * @internal
726
+ */
727
+ function run(e) {
728
+ const flags = e.flags;
729
+ if (flags & ReactiveFlags.Dirty || flags & ReactiveFlags.Pending && checkDirty(e.deps, e)) {
730
+ ++cycle;
731
+ if (e.onCleanup !== void 0) {
732
+ const cleanup = e.onCleanup;
733
+ e.onCleanup = void 0;
734
+ cleanup();
735
+ }
736
+ e.depsTail = void 0;
737
+ e.flags = ReactiveFlags.Watching | ReactiveFlags.RecursedCheck;
738
+ const prevSub = setActiveSub(e);
739
+ try {
740
+ e.fn();
741
+ } finally {
742
+ activeSub = prevSub;
743
+ e.flags &= ~ReactiveFlags.RecursedCheck;
744
+ purgeDeps(e);
745
+ }
746
+ } else e.flags = ReactiveFlags.Watching;
747
+ }
748
+ /**
749
+ * Drains the queued-effects array, running each effect in order.
750
+ *
751
+ * If an effect throws, remaining effects are marked `Recursed | Watching` (so
752
+ * they will retry next time) and the scheduler resets cleanly.
753
+ *
754
+ * @internal
755
+ */
756
+ function flush() {
757
+ try {
758
+ while (notifyIndex < queuedLength) {
759
+ const effect = queued[notifyIndex];
760
+ queued[notifyIndex++] = void 0;
761
+ run(effect);
762
+ }
763
+ } finally {
764
+ while (notifyIndex < queuedLength) {
765
+ const effect = queued[notifyIndex];
766
+ queued[notifyIndex++] = void 0;
767
+ effect.flags |= ReactiveFlags.Watching | ReactiveFlags.Recursed;
768
+ }
769
+ notifyIndex = 0;
770
+ queuedLength = 0;
771
+ }
772
+ }
773
+ /**
774
+ * The bound operation function for computed nodes.
775
+ *
776
+ * On read:
777
+ * 1. Checks whether the node is dirty (or pending-dirty via `checkDirty`).
778
+ * 2. Recomputes via `updateComputed` if needed, propagating to subscribers if
779
+ * the value changed.
780
+ * 3. Links `this` to the current `activeSub` so future writes propagate here.
781
+ *
782
+ * @internal
783
+ */
784
+ function computedOper() {
785
+ const flags = this.flags;
786
+ if (flags & ReactiveFlags.Dirty || flags & ReactiveFlags.Pending && (checkDirty(this.deps, this) || (this.flags = flags & ~ReactiveFlags.Pending, false))) {
787
+ if (updateComputed(this)) {
788
+ const subs = this.subs;
789
+ if (subs !== void 0) shallowPropagate(subs);
790
+ }
791
+ } else if (!flags) {
792
+ this.flags = ReactiveFlags.Mutable | ReactiveFlags.RecursedCheck;
793
+ const prevSub = setActiveSub(this);
794
+ try {
795
+ this.value = this.getter();
796
+ } finally {
797
+ activeSub = prevSub;
798
+ this.flags &= ~ReactiveFlags.RecursedCheck;
799
+ }
800
+ }
801
+ const sub = activeSub;
802
+ if (sub !== void 0) link(this, sub, cycle);
803
+ return this.value;
804
+ }
805
+ /**
806
+ * The bound operation function for signal nodes.
807
+ *
808
+ * - **Read** (no arguments): registers a dep link and returns `currentValue`.
809
+ * If the signal is dirty (pending write flushed by batch), commits the
810
+ * pending value first.
811
+ * - **Write** (one argument): stages the new value as `pendingValue` and, if
812
+ * it differs from the current pending value, propagates dirtiness and
813
+ * schedules a flush.
814
+ *
815
+ * @internal
816
+ */
817
+ function signalOper(...value) {
818
+ if (value.length) {
819
+ if (this.pendingValue !== (this.pendingValue = value[0])) {
820
+ this.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty;
821
+ const subs = this.subs;
822
+ if (subs !== void 0) {
823
+ propagate(subs);
824
+ if (!batchDepth) flush();
825
+ }
826
+ }
827
+ } else {
828
+ if (this.flags & ReactiveFlags.Dirty) {
829
+ if (updateSignal(this)) {
830
+ const subs = this.subs;
831
+ if (subs !== void 0) shallowPropagate(subs);
832
+ }
833
+ }
834
+ let sub = activeSub;
835
+ while (sub !== void 0) {
836
+ if (sub.flags & (ReactiveFlags.Mutable | ReactiveFlags.Watching)) {
837
+ link(this, sub, cycle);
838
+ break;
839
+ }
840
+ sub = sub.subs?.sub;
841
+ }
842
+ return this.currentValue;
843
+ }
844
+ }
845
+ /**
846
+ * The bound disposal function for effect nodes.
847
+ *
848
+ * Calls the cleanup registered by the last run of the effect body (if any),
849
+ * then delegates to `effectScopeOper` to release all dep links and unlink
850
+ * the effect from any parent scope.
851
+ *
852
+ * @internal
853
+ */
854
+ function effectOper() {
855
+ if (this.onCleanup !== void 0) {
856
+ const cleanup = this.onCleanup;
857
+ this.onCleanup = void 0;
858
+ cleanup();
859
+ }
860
+ effectScopeOper.call(this);
861
+ }
862
+ /**
863
+ * The shared disposal implementation for both effect nodes and effectScope
864
+ * nodes.
865
+ *
866
+ * For effect nodes this is also called indirectly via the `unwatched` callback
867
+ * when the owning scope is disposed, triggering cleanup cascades through the
868
+ * entire ownership tree.
869
+ *
870
+ * Steps:
871
+ * 1. Call and clear any registered `onCleanup` (handles cascade disposal where
872
+ * `effectOper` is not called directly).
873
+ * 2. Reset `depsTail` and `flags` so the node is inert.
874
+ * 3. Purge all dep links (which may recursively trigger `unwatched` on
875
+ * nested effects).
876
+ * 4. Unlink from any parent scope's sub-chain.
877
+ *
878
+ * @internal
879
+ */
880
+ function effectScopeOper() {
881
+ const cleanup = this.onCleanup;
882
+ if (cleanup !== void 0) {
883
+ this.onCleanup = void 0;
884
+ cleanup();
885
+ }
886
+ this.depsTail = void 0;
887
+ this.flags = ReactiveFlags.None;
888
+ purgeDeps(this);
889
+ const sub = this.subs;
890
+ if (sub !== void 0) unlink(sub);
891
+ }
892
+ /**
893
+ * Removes all dep links from `sub` that were not refreshed during the most
894
+ * recent tracking run (i.e. stale links appended after `depsTail`).
895
+ *
896
+ * Called at the end of each tracking run (`updateComputed`, `run`) to prune
897
+ * dependencies that the node no longer reads.
898
+ *
899
+ * @internal
900
+ */
901
+ function purgeDeps(sub) {
902
+ const depsTail = sub.depsTail;
903
+ let dep = depsTail !== void 0 ? depsTail.nextDep : sub.deps;
904
+ while (dep !== void 0) dep = unlink(dep, sub);
905
+ }
906
+ //#endregion
907
+ //#region src/polyfill.ts
908
+ if (!Symbol.dispose) Object.defineProperty(Symbol, "dispose", { value: Symbol("dispose") });
909
+ //#endregion
910
+ //#region src/signals/index.ts
911
+ function isReactive(value) {
912
+ return isSignal(value) || isComputed(value);
913
+ }
914
+ /**
915
+ * A decorator that makes a class field reactive by automatically wrapping its value in a signal.
916
+ *
917
+ * The field behaves like a normal property (get/set) but reactivity is tracked under the hood.
918
+ * Any reads will subscribe to the signal and any writes will trigger updates.
919
+ *
920
+ * @example
921
+ * ```ts
922
+ * class Counter {
923
+ * @reactive()
924
+ * count: number = 0;
925
+ * }
926
+ *
927
+ * const counter = new Counter();
928
+ * counter.count++; // Triggers reactivity
929
+ * console.log(counter.count); // Subscribes to changes
930
+ * ```
931
+ *
932
+ * @remarks
933
+ * Equivalent to manually creating a private signal and getter/setter:
934
+ * ```ts
935
+ * class Counter {
936
+ * #count = signal(0);
937
+ * get count() { return this.#count(); }
938
+ * set count(value) { this.#count(value); }
939
+ * }
940
+ * ```
941
+ */
942
+ function reactive(source) {
943
+ const signalStore = /* @__PURE__ */ new WeakMap();
944
+ return (_target, context) => {
945
+ context.addInitializer(function() {
946
+ const sig = signalStore.get(this);
947
+ const writable = !isComputed(sig);
948
+ Object.defineProperty(this, context.name, {
949
+ get() {
950
+ return sig();
951
+ },
952
+ ...writable && { set(value) {
953
+ sig(value);
954
+ } },
955
+ enumerable: true,
956
+ configurable: true
957
+ });
958
+ });
959
+ return function(initialValue) {
960
+ signalStore.set(this, source ? source(this) : signal(initialValue));
961
+ return initialValue;
962
+ };
963
+ };
964
+ }
965
+ //#endregion
966
+ export { effect as a, isEffect as c, onCleanup as d, signal as f, computed as i, isEffectScope as l, untracked as m, reactive as n, effectScope as o, trigger as p, batch as r, isComputed as s, isReactive as t, isSignal as u };