Skip to content

refactor: improve signals APIs, add effect and computed helpers#6

Merged
nberlette merged 7 commits intorefactor/componentsfrom
refactor/signals
Mar 11, 2026
Merged

refactor: improve signals APIs, add effect and computed helpers#6
nberlette merged 7 commits intorefactor/componentsfrom
refactor/signals

Conversation

@nberlette
Copy link
Copy Markdown
Owner

  • 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

Copilot AI review requested due to automatic review settings March 11, 2026 18:34
@nberlette nberlette added the feature New feature or request label Mar 11, 2026
@nberlette nberlette self-assigned this Mar 11, 2026
@nberlette nberlette merged commit ac71068 into refactor/components Mar 11, 2026
2 of 4 checks passed
@nberlette nberlette deleted the refactor/signals branch March 11, 2026 18:36
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines 25 to +29
incoming = 0;
throw error;
} finally {
activeSignals = undefined;
--incoming;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +83 to +85
export function computed<T>(computable: Computable<T>): Computed<T> {
return new Computed(computable);
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
export const $global: typeof globalThis = (() => {
try {
return (0, eval)("this");
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
import { $global } from "./primordials.ts";

export interface Performance {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 22 to 30
try {
func.call(thisArg);
} catch (error) {
incoming = 0;
throw error;
} finally {
activeSignals = undefined;
--incoming;
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +112
{
__proto__: impl,
get() {
if (impl && "writable" in impl) {
return impl.writable;
} else {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
@@ -0,0 +1 @@
export type Reshape<T> = Pick<T, keyof T>;
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
/** Thrown whenever someone tries to directly modify `Computed.value` */
export class ComputedReadOnlyError extends Error {
constructor() {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
// 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();
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (!dependant.dependencies) dependant.dispose();
if (dependant.dependencies.size === 0) dependant.dispose();

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +111
{
__proto__: impl,
get() {
if (impl && "writable" in impl) {
return impl.writable;
} else {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +118
export function effect(effectable: Effectable): Effect {
return new Effect(effectable);
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants