Skip to content

feat: computed cyclic node#63

Closed
Alexnortung wants to merge 5 commits into
sigrea:mainfrom
Oak-Digital:cyclic
Closed

feat: computed cyclic node#63
Alexnortung wants to merge 5 commits into
sigrea:mainfrom
Oak-Digital:cyclic

Conversation

@Alexnortung

@Alexnortung Alexnortung commented Jun 10, 2026

Copy link
Copy Markdown

Purpose

Part of #62

Summary

Adds a new CyclicComputed node and cyclicComputed function.

I am building a system where the user can configure a bunch of stuff, sometimes this can lead to cyclic dependencies, which alien-signals normally doesn't support.

The CyclicComputed node gets a stabilizer which is called as soon as the same signal is met in one of the dependencies.

I would like a review before moving on to the TODOs :)

TODO:

  • Add documentation
  • Make effect tests pass (I probably need some guidance on this one)
  • (optionally more tests)

Verification

  • pnpm -s cicheck

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

Copy link
Copy Markdown

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: c7e30e312e

ℹ️ 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".

// Run getter and only update if not part of the scc
try {
// REFACTOR: this currently causes the call to be recursive
const result = this.getter();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Evaluate dirty nodes through reactive bookkeeping

When this dirty path calls getter directly, it skips the bookkeeping that update()/normal Computed.get() perform: the active subscriber remains the outer effect/watch and this node's Dirty flag is never cleared. In an effect that reads computedCyclic(() => a.value < 10), the effect subscribes directly to a and reruns on every a write even when the computed result is unchanged, while standalone reads also recompute the getter/stabilizer every time because shouldUpdate(this) stays true.

Useful? React with 👍 / 👎.

Comment thread packages/core/computed-cyclic.ts
Comment on lines +77 to +78
const context = activeContext;
const hasCurrent = context?.stackNodes.has(this);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
const context = activeContext;
const hasCurrent = context?.stackNodes.has(this);
return !!activeContext?.stackNodes.has(this);

Comment on lines +88 to +93
const currentStackIndex = context.stack.length - 1;

if (context.sccTail === undefined || context.sccHead === undefined) {
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
const currentStackIndex = context.stack.length - 1;
if (context.sccTail === undefined || context.sccHead === undefined) {
return false;
}
if (context.sccTail === undefined || context.sccHead === undefined) {
return false;
}
const currentStackIndex = context.stack.length - 1;

Since you're not using currentStackIndex before the if-statements below

const context = activeContext!;

context.sccTail = context.stack.length - 1;
const prevOccuranceIndex = context.stack.indexOf(this);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
const prevOccuranceIndex = context.stack.indexOf(this);
const prevOccurrenceIndex = context.stack.indexOf(this);

Comment on lines +171 to +182
// if (this.update()) {
// const subs = this.subs;
// if (subs !== undefined) {
// shallowPropagate(subs);
// }
// }
// const subscriber = getActiveSubscriber();
// if (subscriber !== undefined) {
// link(this, subscriber, getCurrentCycle());
// }
// const value = this.currentValue;
// return value!;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remember to delete the comments and logs when you're done with the checklist :)

const previous = getActiveSubscriber();
setActiveSubscriber(this);
const prevContext = setNewActiveContext();
// biome-ignore lint/style/noNonNullAssertion: Non-null assertion because we just updated it in the line above

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It can still be undefined though

Comment on lines +208 to +210
const constex = activeContext!;
constex.stack.push(this);
constex.stackNodes.add(this);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
const constex = activeContext!;
constex.stack.push(this);
constex.stackNodes.add(this);
const context = activeContext!;
context.stack.push(this);
context.stackNodes.add(this);

@Alexnortung Alexnortung marked this pull request as draft June 13, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants