refactor: improve signals APIs, add effect and computed helpers#6
refactor: improve signals APIs, add effect and computed helpers#6nberlette merged 7 commits intorefactor/componentsfrom
effect and computed helpers#6Conversation
nberlette
commented
Mar 11, 2026
- refactor(signal): improve errors in signal submodules
- feat(utils): add internal performance module for mocking
- feat(internal): add internal types and primordials modules
- chore: fmt
- refactor(signals): clean various little things up
- Document flusher usage
- refactor(signals): clean up formatting, use primordials more consistently
- [x] add a dedicated ./signals/errors submodule - [x] move error subclasses out of signal.ts and computed.ts - [x] improve some other minor things
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 176b64f573
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| incoming = 0; | ||
| throw error; | ||
| } finally { | ||
| activeSignals = undefined; | ||
| --incoming; |
There was a problem hiding this comment.
Keep incoming counter non-negative on tracking errors
If func.call(thisArg) throws, the catch block sets incoming = 0 but the finally block still executes --incoming, which leaves the counter at -1. Subsequent calls then block forever in while (incoming) await Promise.resolve(); because -1 is truthy, so one tracking-time exception can deadlock all future dependency tracking (Computed, Effect, etc.) in the same process.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR refactors the signals subsystem and related utilities to provide cleaner APIs (effect, computed, isSignal), centralize error types, and standardize usage of “primordials”/global utilities across runtime environments.
Changes:
- Introduces new internal utility modules (
primordials,performance,types) and applies them in stdio wrappers. - Refactors signals internals (dependency tracking, errors, type helpers) and adds convenience helpers (
effect(),computed(),isSignal()). - Improves documentation/formatting across signals modules (lazy computed/effect, flusher, reactivity).
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/types.ts | Adds Reshape<T> helper type for reuse. |
| src/utils/primordials.ts | Adds $global and “SafeObject” wrappers for Object primordials. |
| src/utils/performance.ts | Adds a mockable performance.now() abstraction. |
| src/stdio/stdout.ts | Switches to primordials-backed ObjectAssign/ObjectDefineProperty. |
| src/stdio/stdin.ts | Uses shared Reshape type + primordials-backed Object helpers; doc tweak. |
| src/stdio/stderr.ts | Switches to primordials-backed ObjectAssign/ObjectDefineProperty. |
| src/signals/types.ts | Adds Dependish, Disposable, AsyncDisposable types and minor formatting. |
| src/signals/signal.ts | Centralizes error import, tightens options typing, adds isSignal/SignalLike, refactors internals. |
| src/signals/reactivity.ts | Minor refactor/formatting of propagation conditions. |
| src/signals/mod.ts | Re-exports new errors.ts. |
| src/signals/lazy_effect.ts | Uses new performance util; exports options type; doc/cleanup. |
| src/signals/lazy_computed.ts | Uses new performance util; exports options type; doc/cleanup. |
| src/signals/flusher.ts | Documentation improvements and minor loop simplification. |
| src/signals/errors.ts | New centralized error types for signals modules. |
| src/signals/effect.ts | Improves error throwing, pause/resume semantics, adds effect() helper. |
| src/signals/dependency_tracking.ts | Renames internals to track/optimize and re-exports old names. |
| src/signals/computed.ts | Moves error to errors.ts, minor cleanup, adds computed() helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| export function computed<T>(computable: Computable<T>): Computed<T> { | ||
| return new Computed(computable); | ||
| } |
There was a problem hiding this comment.
New public helper computed() is added here but existing signal tests only cover constructing Computed directly. Add a small test that imports and uses computed() to ensure it’s exported correctly and behaves the same (read-only behavior + dependency tracking).
| export const $global: typeof globalThis = (() => { | ||
| try { | ||
| return (0, eval)("this"); |
There was a problem hiding this comment.
This new file is missing the standard license header comment used across the repo (many existing src/**/*.ts files start with a copyright + MIT license line). Please add the header so it passes the repository’s license-header checks.
| import { $global } from "./primordials.ts"; | ||
|
|
||
| export interface Performance { |
There was a problem hiding this comment.
This new file is missing the standard license header comment used across the repo (many existing src/**/*.ts files start with a copyright + MIT license line). Please add the header so it passes the repository’s license-header checks.
| try { | ||
| func.call(thisArg); | ||
| } catch (error) { | ||
| incoming = 0; | ||
| throw error; | ||
| } finally { | ||
| activeSignals = undefined; | ||
| --incoming; | ||
| } |
There was a problem hiding this comment.
In track(), the catch sets incoming = 0 but the finally block always decrements incoming, so an exception will leave incoming negative (e.g. -1). This can break the while (incoming) gating and dependency tracking state. Remove the incoming = 0 assignment (or move it into finally without also decrementing) so incoming is always restored to its previous value.
| { | ||
| __proto__: impl, | ||
| get() { | ||
| if (impl && "writable" in impl) { | ||
| return impl.writable; | ||
| } else { |
There was a problem hiding this comment.
The property descriptor passed to ObjectDefineProperty sets __proto__ to impl, which changes the descriptor object's prototype to a user-provided object. This is unnecessary for defining an accessor and can introduce surprising prototype-chain lookups / security footguns. Drop __proto__ entirely (or set it to null if you want a dictionary object).
| @@ -0,0 +1 @@ | |||
| export type Reshape<T> = Pick<T, keyof T>; | |||
There was a problem hiding this comment.
This new file is missing the standard license header comment used across the repo (e.g. // Copyright 2023 Im-Beast. MIT license. at the top of other src/utils/*.ts files). Please add the header so it passes the repository’s license-header checks.
| /** Thrown whenever someone tries to directly modify `Computed.value` */ | ||
| export class ComputedReadOnlyError extends Error { | ||
| constructor() { |
There was a problem hiding this comment.
This new file is missing the standard license header comment used across the repo (many existing src/signals/*.ts files start with a copyright + MIT license line). Please add the header so it passes the repository’s license-header checks.
| // If dependant has no more dependencies then | ||
| // it means that it should be replaced with constant value, | ||
| // because nothing can update its value anymore | ||
| if (!dependant.dependencies) dependant.dispose(); |
There was a problem hiding this comment.
dependant.dependencies is always a Set per the Dependant interface, so if (!dependant.dependencies) will never be true. If the intent is to dispose dependants that no longer depend on anything, this should check dependant.dependencies.size === 0 after deleting this signal.
| if (!dependant.dependencies) dependant.dispose(); | |
| if (dependant.dependencies.size === 0) dependant.dispose(); |
| { | ||
| __proto__: impl, | ||
| get() { | ||
| if (impl && "writable" in impl) { | ||
| return impl.writable; | ||
| } else { |
There was a problem hiding this comment.
The property descriptor passed to ObjectDefineProperty sets __proto__ to impl, which changes the descriptor object's prototype to a user-provided object. This is unnecessary for defining an accessor and can introduce surprising prototype-chain lookups / security footguns. Drop __proto__ entirely (or set it to null if you want a dictionary object).
| export function effect(effectable: Effectable): Effect { | ||
| return new Effect(effectable); | ||
| } |
There was a problem hiding this comment.
New public helper effect() is added here but existing signal tests only cover constructing Effect directly. Add a small test that imports and uses effect() to ensure it’s exported correctly and behaves the same (dependency tracking, pause/resume/dispose).