@cplieger/reactive 2.1.2 → 2.2.0-dev.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.
package/README.md CHANGED
@@ -2,56 +2,20 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@cplieger/reactive)](https://www.npmjs.com/package/@cplieger/reactive) [![JSR](https://jsr.io/badges/@cplieger/reactive)](https://jsr.io/@cplieger/reactive) [![Mutation (TS)](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cplieger/reactive/badges/mutation-ts.json)](https://github.com/cplieger/reactive/issues?q=label%3Astryker-tracker)
4
4
 
5
- > Signals + collections + DOM-reconciliation micro-framework for TypeScript
5
+ reactive lets you build a framework-free TypeScript browser UI with signals, keyed list rendering and in-place DOM updates, in one package with no runtime dependencies.
6
6
 
7
- A standalone reactive app-skeleton:
7
+ It replaces the change listeners, list re-renders and element builder you would otherwise write between state and the page. It ships as ESM TypeScript source rather than compiled JavaScript, so it needs a bundler or runtime that compiles TypeScript. Releases follow semantic versioning, and it is licensed under Apache-2.0.
8
8
 
9
- - fine-grained signals with automatic dependency tracking and synchronous batched effects
10
- - a typed per-key reactive store
11
- - a dynamic per-id signal registry (`SignalMap`)
12
- - a reactive ordered keyed `Collection`
13
- - a two-tier list render binding (`bindList`)
14
- - a CSP-safe element factory (`el`)
15
- - keyed-list DOM reconciliation
16
- - structural tree-diffing
17
- - a typed event bus (`createBus`)
9
+ ## Why use it
18
10
 
19
- The store, `SignalMap`, and `Collection` are thin facades over a single signal engine: one reactive core, not several. Zero dependencies beyond the DOM API.
11
+ reactive is built for a small browser app in plain TypeScript that needs no component framework.
20
12
 
21
- > **What needs a DOM:** the render half, `el`, `bindList`, `reconcile`, `patch` and `trackHandler`. The engine and the state tiers do not: `signal`, `effect`, `computed`, `batch`, `untracked`, `createStore`, `SignalMap`, `createCollection` and `createBus`. Nothing touches the DOM at import time, so importing the package under Node, Deno, Bun or Workers succeeds and only the DOM functions throw when called.
13
+ - Signals, computed values and effects track their dependencies on their own. When a write returns, every effect that depends on it has already run.
14
+ - `createCollection` and `bindList` render a keyed list. Changing one item repaints only its row, and a reorder keeps the existing rows.
15
+ - `el` builds elements without HTML strings, so it works under a strict Content Security Policy. `patch` updates a subtree by reusing the elements on the page, so focus and scroll position survive a re-render.
16
+ - The signals, stores, collections and event bus need no DOM, so they also run under Node, Deno, Bun or Workers.
22
17
 
23
- Mirrors semantics from @preact/signals-core and solid-js reactivity.
24
-
25
- ## Flush model
26
-
27
- One phase, and this is the whole of it.
28
-
29
- **A write flushes.** When `sig.value = x` returns, every effect that depends on
30
- `sig` has already re-run. The setter notifies and drains before it returns, so
31
- there is no pending-effect state a caller can observe and no queue to drain. Read
32
- the DOM on the next statement and you are reading what the effects wrote. Same
33
- contract as [@preact/signals-core](https://github.com/preactjs/signals/blob/main/packages/core/README.md),
34
- this engine's baseline.
35
-
36
- **`batch(fn)` is the only deferral.** Inside a batch, writes coalesce and effects
37
- are held; they run once, synchronously, when the _outermost_ batch returns. A
38
- nested batch defers to its outermost, and a throwing body still flushes.
39
-
40
- **Deferral beyond that is yours.** To get effects on a later task, write on a
41
- later task:
42
-
43
- ```typescript
44
- queueMicrotask(() => {
45
- count.value = next; // effects run inside this microtask
46
- });
47
- ```
48
-
49
- There is deliberately **no flush barrier** — no `flushSync()`, no `nextTick()`.
50
- A barrier is the API a _deferred_ model owes its callers (Vue's `nextTick`,
51
- React's `flushSync`); an eager model never holds work, so it has nothing to
52
- report on and nothing to force. `flushSync()` existed through v1.x, could never
53
- find work pending, and refused to act when work was pending inside a batch; it
54
- was removed in v2.0.0. If you were calling it, delete the call — it was a no-op.
18
+ Consider [Solid](https://github.com/solidjs/solid) if you want a full UI framework, with components, JSX, Suspense for async data and server-side rendering.
55
19
 
56
20
  ## Install
57
21
 
@@ -63,132 +27,132 @@ npm i @cplieger/reactive
63
27
 
64
28
  ## Usage
65
29
 
30
+ A signal holds a value, a computed value derives from signals, and an effect re-runs when what it read changes:
31
+
66
32
  ```typescript
67
- import {
68
- signal,
69
- effect,
70
- batch,
71
- computed,
72
- untracked,
73
- subscribe,
74
- isSignal,
75
- isComputed,
76
- } from "@cplieger/reactive";
33
+ import { signal, computed, effect, batch } from "@cplieger/reactive";
77
34
 
78
35
  const count = signal(0);
79
36
  const doubled = computed(() => count.value * 2);
80
37
 
81
- effect(() => {
82
- console.log("count:", count.value, "doubled:", doubled.value);
83
- return undefined;
38
+ const dispose = effect(() => {
39
+ console.log(count.value, doubled.value);
40
+ return undefined; // an effect returns undefined or a cleanup function
84
41
  });
85
42
 
86
43
  batch(() => {
87
44
  count.value = 1;
88
- count.value = 2; // effect fires once with value 2, synchronously at batch end
45
+ count.value = 2;
46
+ }); // the effect ran once, with 2 and 4, before batch returned
47
+
48
+ dispose();
49
+ ```
50
+
51
+ A collection and `bindList` render a list. Updating one item repaints only its row:
52
+
53
+ ```typescript
54
+ import { bindList, createCollection, el } from "@cplieger/reactive";
55
+
56
+ interface Todo {
57
+ id: string;
58
+ title: string;
59
+ done: boolean;
60
+ }
61
+
62
+ const todos = createCollection<Todo>((todo) => todo.id);
63
+ const list = el("ul");
64
+ document.body.append(list);
65
+
66
+ bindList(list, todos, {
67
+ mount: () => el("li"),
68
+ update: (row, todo) => {
69
+ row.textContent = todo.done ? `${todo.title} (done)` : todo.title;
70
+ },
89
71
  });
90
72
 
91
- // Read without tracking
73
+ todos.setAll([
74
+ { id: "a", title: "Write docs", done: false },
75
+ { id: "b", title: "Ship", done: false },
76
+ ]);
77
+ todos.update("a", (todo) => ({ ...todo, done: true })); // only the first row repaints
78
+ ```
79
+
80
+ `patch` takes freshly built elements and applies them to a subtree, keeping the elements already on the page:
81
+
82
+ ```typescript
83
+ import { effect, el, patch, signal } from "@cplieger/reactive";
84
+
85
+ const name = signal("world");
86
+ const header = el("header");
87
+ document.body.append(header);
88
+
92
89
  effect(() => {
93
- const tracked = count.value;
94
- const notTracked = untracked(() => doubled.value);
95
- console.log(tracked, notTracked);
90
+ patch(header, el("h1", { className: "title" }, `Hello, ${name.value}`));
96
91
  return undefined;
97
92
  });
98
93
 
99
- // Subscribe utility
100
- const dispose = subscribe(count, (v) => console.log("count changed:", v));
101
- dispose();
102
-
103
- // Type guards
104
- isSignal(count); // true
105
- isComputed(doubled); // true
94
+ name.value = "reactive"; // the same <h1> stays on the page with its new text
106
95
  ```
107
96
 
108
97
  ## API
109
98
 
110
- ### Signals
111
-
112
- - `signal<T>(initial, options?): Signal<T>`: reactive value with `.value` getter/setter and `.peek()`. Options: `{ equals?: false | ((prev, next) => boolean) }`
113
- - `computed<T>(fn, options?): ReadonlySignal<T>`: lazy cached derived signal (glitch-free, equality-deduped). Options: `{ equals?: false | ((prev, next) => boolean) }`
114
- - `effect(fn: () => Cleanup): () => void`: auto-tracking side-effect with cleanup support. `fn` must return `undefined` or a cleanup function.
115
- - `batch(fn)`: coalesce signal writes; effects flush synchronously at end of outermost batch. The only deferral — see [Flush model](#flush-model)
116
- - `untracked<T>(fn): T`: run fn without tracking signal reads (like Preact `untracked()` / Solid `untrack()`)
117
- - `touch(...signals): void`: read signals for their tracking effect alone, discarding the values — the complement of `peek()` (`peek` reads without subscribing, `touch` subscribes without keeping). `undefined` entries are skipped, so a per-key lookup that may miss (`Collection.signalFor(id)`, `SignalMap.get(id)`) can be passed straight in. Use it where a scope must re-run on a signal whose value it has no use for: most often a two-tier read, where a computed or effect takes per-entity values through `signalFor` and so must depend on the structure signal (`Collection.ids`) separately to re-derive when the id set changes. Unlike `on()` it ADDS a dependency and leaves every other read in the scope tracked
118
- - `on(deps, fn, options?): () => U`: explicit dependency declaration helper (like Solid `on()`); pass into `effect()` or `computed()`. Its body runs untracked, so the declared deps are the scope's ONLY dependencies — reach for `touch()` instead when the rest of the scope should stay tracked. Passed to `effect()`, the body owes an explicit `return undefined` (or a cleanup function) like any effect callback, since a body with no return statement infers `() => void`, which the `Cleanup` contract rejects. With `{ defer: true }` the accessor returns `() => U | undefined`: the deferred first call yields `undefined`.
119
- - `subscribe(signal, cb): () => void`: subscribe to a signal, calling cb immediately and on every change. The callback runs untracked: signals read inside it do not become dependencies of the subscription
120
- - `isSignal(value): boolean`: type guard for signals created by `signal()`
121
- - `isComputed(value): boolean`: type guard for computed signals
122
- - `setEffectErrorHandler(handler): EffectErrorHandler`: set global error handler for effects; returns the previous handler
99
+ - Signals: `signal`, `computed`, `effect`, `batch`, `untracked`, `touch`, `on`, `subscribe`, `isSignal`, `isComputed` and `setEffectErrorHandler`.
100
+ - State: `createStore` for a fixed set of typed keys, `SignalMap` for keys known only at run time, and `createCollection` for an ordered list of keyed items.
101
+ - DOM: `el` builds elements, `reconcile` and `bindList` render keyed lists, and `patch` updates a subtree in place. `trackHandler` registers an `on*` handler on an element you built without `el`, so `patch` keeps it in step.
102
+ - Events: `createBus` for typed events that hold no value.
123
103
 
124
- ### DOM building & reconciliation
104
+ The full reference is on [JSR](https://jsr.io/@cplieger/reactive/doc). [Signals, stores and collections](docs/signals.md) and [Building and updating the DOM](docs/dom.md) describe how each one behaves.
125
105
 
126
- - `el(tag, attrs?, ...children)`: CSP-safe element factory (the build half; `reconcile`/`patch` are the commit half). `className` → class; `on*` → handler property + `trackHandler` (so `patch` reconciles handlers); boolean DOM props (`hidden`/`disabled`/`checked`/`selected`/…) and `value`/`colSpan`/`rowSpan`/`tabIndex`/`htmlFor` → properties; everything else (`data-*`, `aria-*`, `style`, `id`) → `setAttribute`. String children become text nodes (never parsed as HTML); null/undefined attrs and children are skipped.
127
- - `reconcile<T>(parent, items, spec)`: keyed-list DOM reconciliation with mount/update/onRemove lifecycle
128
- - `patch(parent, ...children)`: structural tree-diff, replacing a parent's children with reconciled new nodes. Correspondence between a child the parent has and a child you asked for is decided strongest-first: the same **node** (you handed back a node that is already a child of `parent` — it is placed at the index you passed it at, unmodified, so handing a parent its own children reordered is supported), then the same **key** (a `*-id` attribute, a specific entity id which takes precedence, or failing that `data-col`; duplicate-key siblings match in document order), then the same **position**. Any node that is not already a child of `parent` is a TEMPLATE: its shape may be copied into a reused child and the node itself never inserted. A reused element takes the new node's attributes, its tracked `on*` handlers, and the unreflected live properties the attribute pass cannot reach: `checked` (input), `selected` (option) and the `value` of an `input`/`textarea`. Those reflect to `defaultChecked`/`defaultSelected`/`defaultValue`, so without this a re-render could never turn a checkbox off or replace the text in a field. Syncing `value` overwrites what a user has typed and moves the caret to the end, which is correct only where a re-render cannot land mid-keystroke: a form re-rendered by a click is safe, one re-rendered by a timer or a server push is not, and that shape must drive the field from an effect instead (see [Unsupported by Design](#unsupported-by-design)).
129
- - `trackHandler(el, key)`: register an `on*` property for handler reconciliation during tree-diff
106
+ ## A write runs its effects before it returns
130
107
 
131
- ### Store
108
+ When `count.value = 1` returns, every effect that depends on `count` has already run, so the next line reads the DOM those effects wrote. There is no queue and no pending state to wait for.
132
109
 
133
- `createStore` and `SignalMap` are thin facades over the one signal engine. `createStore` lazily backs each key with a signal; `SignalMap` is a registry of signals keyed by a runtime string id.
110
+ `batch(fn)` is the only way to defer. Writes inside it coalesce, and the held effects run once, synchronously, when the outermost batch returns, even when the body throws. To run effects on a later task, make the write on a later task:
134
111
 
135
- - `createStore<M>(): Store<M>`: typed, fixed-key reactive store with `get`, `set`, `subscribe`, and `computed`. A member exists here only where it does something the engine's namesake does not, so `effect` and `batch` are **not** on the store (removed in v2.0.0) — import them from the package root; `store.batch(…)` read as "batch this store's writes" while batching the whole graph. `subscribe` notifies on change only (not immediately on subscribe), and its callback runs untracked. `computed(outputKey, fn)` is an eager effect, not a lazy engine computed: it writes `outputKey` on every dependency change. So `fn` runs whether or not anyone reads the key; a throwing `fn` routes to the effect error handler instead of being cached and rethrown at the read site; `set(outputKey, …)` still works between recomputes. A `computed` key whose fn reads its own output does not loop unbounded: a self-read that keeps yielding new values surfaces `Error("Cycle detected")` through the effect error handler, while a stable self-read settles. The guard re-arms per write: a key that is still cyclic reports again on the next write to it rather than reporting once and going quiet, and effects merely queued alongside the cycle (a `subscribe` on the same key, say) stay reactive.
136
- - `SignalMap<V>`: dynamic per-id signal registry: `get(id)`, `ensure(id, initial)`, `clear(id)`, `clearAll()`. For reactive state whose key set isn't known at the type level (per-message streaming text, per-row state, …); complements `createStore`'s fixed key set.
112
+ ```typescript
113
+ queueMicrotask(() => {
114
+ count.value = next; // effects run inside this microtask
115
+ });
116
+ ```
137
117
 
138
- ### Collections
118
+ This is the contract [@preact/signals-core](https://github.com/preactjs/signals/blob/main/packages/core/README.md) documents too. The package has no `flushSync()` or `nextTick()`, because it never holds work, so there is nothing to flush.
139
119
 
140
- A reactive ordered collection of keyed entities: the data half of the two-tier list pattern. A per-entity content update touches only that entity's subscribers; add/remove/reorder bumps the structure signal (`ids`). Built on `signal` + `SignalMap`.
120
+ ## Correctness guarantees
141
121
 
142
- - `createCollection<T>(keyOf: (item: T) => string): Collection<T>`: returns a collection with:
143
- - `setAll(items)`: replace everything (same-order replacement does not bump `ids`; duplicate keys deduplicate: first occurrence keeps the position, last value wins)
144
- - `upsert(item)`: add/replace one (appends new ids); `prepend(items)`: add to the front (scroll-up/load-older pagination)
145
- - `update(id, next | updater)` / `remove(id)` / `clear()`
146
- - `get(id)` / `has(id)` (untracked), `signalFor(id)` (reactive per-entity), `size`
147
- - `ids: ReadonlySignal<readonly string[]>`: structure tier (add/remove/reorder only)
148
- - `items()`: ordered reactive snapshot (tracks order + every entity)
122
+ - Effects downstream of a `computed` run only when its value changes, compared with `Object.is` unless you pass `equals`. A diamond-shaped dependency graph never shows a half-updated value.
123
+ - Reading a `computed` while it is computing throws `Error("Cycle detected")`.
124
+ - When a computed function throws, every read rethrows that error until a dependency changes.
125
+ - Setting `.value` on a `computed` throws `Error("Cannot set a computed signal")`.
126
+ - `batch()` runs the held effects synchronously at the end of the outermost batch, as @preact/signals-core and solid-js do.
149
127
 
150
- ### List rendering
128
+ ## Unsupported by design
151
129
 
152
- - `bindList<T>(parent, source, spec): () => void`: two-tier render binding. `source` is a `ListSource<T>` = `{ ids, signalFor }`; a `Collection` satisfies it directly, and a filtered/sorted/paginated view is just `{ ids: computed(...), signalFor: collection.signalFor }` (pagination is a sliced view, not a separate primitive). A structural effect tracks `source.ids` and reconciles the row list; each row's own effect tracks only its entity signal, so a per-entity change repaints just that row with no structural reconcile. An id whose `signalFor` is `undefined` (an inconsistent source) is skipped rather than rendered. `spec`: `{ mount(item, id) => HTMLElement; update?(el, item, id); onRemove?(el, id) }` (`update` runs at mount and on every later change). Returns a dispose that tears down the structural effect and every row effect.
130
+ The package leaves these out on purpose. [Non-goals](docs/non-goals.md) gives the reason for each.
153
131
 
154
- ### Event bus
132
+ - An ownership tree or `createRoot`, and automatic disposal of nested effects.
133
+ - Lazy activation and an `onMount` lifecycle.
134
+ - `Signal.subtle.Watcher` and notify-on-dirty.
135
+ - Introspection APIs, and explicit disposal of a computed.
136
+ - Server-side rendering and per-request isolation.
137
+ - Async signals and resources.
138
+ - Transactions, a custom scheduler, and a flush barrier such as `flushSync` or `nextTick`.
139
+ - Controlled form inputs. `patch` syncs form values, so if a view can re-render while a user types, drive that field from an effect instead.
140
+ - Writing a signal from inside a computed. The computed's own subscribers are not woken, so make that write in an effect.
155
141
 
156
- State lives in signals; discrete events go on a bus. `createBus` is the typed event primitive (not reactive state; no retained value).
142
+ ## Documentation
157
143
 
158
- - `createBus<EventMap>(options?): Bus<EventMap>`: `on`/`once`/`off`/`emit`/`clear`. Handlers are snapshot-cached (rebuilt only on mutation; a handler unsubscribed mid-emit still fires for that emit). Events whose payload type is `undefined` emit with no payload argument. A throwing handler is isolated via `options.onError` (default `console.error`).
144
+ - [Signals, stores and collections](docs/signals.md) covers the flush model, effects, stores, collections and the event bus, for a reader writing state code.
145
+ - [Building and updating the DOM](docs/dom.md) covers how `el`, `reconcile`, `bindList` and `patch` decide what to keep, for a reader rendering with them.
146
+ - [Non-goals](docs/non-goals.md) lists what the package leaves out and why.
159
147
 
160
- ## Correctness guarantees
148
+ ## Credits
161
149
 
162
- - **Glitch-freedom**: Computed signals use equality dedup (`Object.is`), so downstream effects only fire when the computed value actually changes. Diamond dependency graphs are handled correctly.
163
- - **Cycle detection**: Reading a computed that is currently being computed throws `Error("Cycle detected")`.
164
- - **Error caching**: If a computed's fn throws, the error is cached and rethrown on subsequent reads until dependencies change.
165
- - **Computed is read-only**: Setting `.value` on a computed throws `Error("Cannot set a computed signal")`.
166
- - **Synchronous batch**: `batch()` flushes effects synchronously at the end of the outermost batch (matching @preact/signals-core and solid-js). It is the only deferral the library has; a bare write flushes before it returns. See [Flush model](#flush-model).
167
-
168
- ## Unsupported by Design
169
-
170
- The following features are intentionally NOT implemented:
171
-
172
- | Feature | Reason |
173
- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
174
- | **Effect ownership tree / `createRoot`** | Consumers manage disposal explicitly. Adding ownership changes the mental model. |
175
- | **Nested effect auto-disposal** | Each effect is independent and returns its own dispose function. Compose with arrays or helper functions. |
176
- | **Lazy activation / `onMount` lifecycle** | Signals are always active. Resource management is the consumer's responsibility. |
177
- | **`Signal.subtle.Watcher` / notify-on-dirty** | This library IS the framework layer. The Watcher pattern is for frameworks that sit on top of a signals primitive. |
178
- | **Introspection APIs** | Dev-tools concern. Not needed for production. |
179
- | **Explicit computed disposal** | Computed signals are GC'd when unreferenced. No explicit teardown needed. |
180
- | **SSR / server-side isolation** | Client-side library. Server usage should instantiate fresh signal graphs per request. |
181
- | **Async signals / resources** | Out of scope. Use effects + manual signal writes for async data loading. |
182
- | **Transactions** | Framework-level concern. Not a signals primitive. |
183
- | **Custom scheduler / `setScheduler()`** | Batch is synchronous and a write flushes before it returns, so there is no queue to schedule. Deferral is the caller's: write on a later task. See [Flush model](#flush-model). |
184
- | **A flush barrier (`flushSync`, `nextTick`)** | A barrier is what a deferred model owes its callers; this model never holds work, so a barrier has nothing to report on. `flushSync()` shipped through v1.x and was a no-op from every position a caller could occupy; removed in v2.0.0. |
185
- | **Controlled form inputs** | `patch()` syncs `checked`, `selected` and `input`/`textarea` `value`, so the render owns them. It has no change routing or state ownership, so a surface re-rendered while a user types must own the field itself. |
186
- | **Waking subscribers of a self-writing computed** | A computed that writes a signal it reads is re-read correctly (it is marked stale so the next read re-runs it), but its own subscribers are not woken, so an effect depending on it does not re-run until something else notifies it. Writing a source from a computed body is outside the intended shape; do it in an effect. |
150
+ - The signal engine follows the design of [@preact/signals-core](https://github.com/preactjs/signals) in its dependency graph, its glitch-free computed refresh, its run-on-write effects and its untracked `subscribe` callback.
151
+ - The `on()` helper follows [Solid](https://github.com/solidjs/solid)'s `on()`, and `untracked()` is Solid's `untrack()` under the name Preact gives it.
187
152
 
188
153
  ## Contributing
189
154
 
190
- Issues and PRs are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the
191
- conventions and how to run the checks locally.
155
+ Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
192
156
 
193
157
  ## Disclaimer
194
158
 
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/reactive",
3
- "version": "2.1.2",
3
+ "version": "2.2.0-dev.1",
4
4
  "license": "Apache-2.0",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/reactive",
3
- "version": "2.1.2",
3
+ "version": "2.2.0-dev.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -21,17 +21,15 @@
21
21
  "@stryker-mutator/typescript-checker": "10.0.0",
22
22
  "@stryker-mutator/vitest-runner": "10.0.0",
23
23
  "@typescript/native": "npm:typescript@7.0.2",
24
- "@vitest/browser": "5.0.1",
25
- "@vitest/browser-playwright": "5.0.1",
26
- "@vitest/coverage-v8": "5.0.1",
27
- "eslint": "10.10.0",
28
- "fast-check": "4.10.1",
29
- "knip": "6.36.0",
24
+ "@vitest/browser-playwright": "5.0.3",
25
+ "eslint": "10.12.0",
26
+ "fast-check": "4.10.2",
27
+ "knip": "6.39.0",
30
28
  "playwright": "1.63.0",
31
- "prettier": "3.9.7",
29
+ "prettier": "3.9.9",
32
30
  "typescript": "npm:@typescript/typescript6@6.0.2",
33
- "typescript-eslint": "8.70.0",
34
- "vitest": "5.0.1"
31
+ "typescript-eslint": "8.71.0",
32
+ "vitest": "5.0.3"
35
33
  },
36
34
  "overrides": {
37
35
  "qs": "^6.15.2"
package/src/signal.ts CHANGED
@@ -682,9 +682,8 @@ export function untracked<T>(fn: () => T): T {
682
682
  * This exists because the two-tier shape `createCollection` and `bindList` are
683
683
  * built on produces it: a scope reads per-entity values through `signalFor`,
684
684
  * so it must depend on the structure signal separately to re-run when the id
685
- * set changes, and that dependency has no value the body wants. Spelling it as
686
- * a discarded expression left the intent to a comment, and `undefined` entries
687
- * are accepted so a per-key lookup that may miss (`signalFor(id)`,
685
+ * set changes, and that dependency has no value the body wants. `undefined`
686
+ * entries are accepted so a per-key lookup that may miss (`signalFor(id)`,
688
687
  * `SignalMap.get(id)`) can be passed straight in.
689
688
  *
690
689
  * Not a substitute for `on()`, which declares an exhaustive dependency list
@@ -698,8 +697,7 @@ export function touch(
698
697
  }
699
698
  }
700
699
 
701
- // Named so the read is a call rather than a discarded expression: the value is
702
- // deliberately unused, and returning it is what keeps that legible.
700
+ // deadset:ignore DS1803 -- returning the value keeps the tracking read from being a discarded expression; typescript-eslint rejects void on a property read
703
701
  function readForDependency(sig: Signal<unknown> | ReadonlySignal<unknown> | undefined): unknown {
704
702
  return sig?.value;
705
703
  }