-
-
Notifications
You must be signed in to change notification settings - Fork 407
lowLevel.subtle.sync #1136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
NullVoxPopuli
wants to merge
7
commits into
main
Choose a base branch
from
nvp/lowLevel.subtle.watch
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
lowLevel.subtle.sync #1136
Changes from 5 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
ac0e6ec
draft
NullVoxPopuli 05c6621
oh my
NullVoxPopuli 8d740ba
Rename file, update meta
NullVoxPopuli b07dfcd
Add ember-concurrency example
NullVoxPopuli 621ce49
Demo
NullVoxPopuli 3b3ee38
Some q/a
NullVoxPopuli 745aba9
Rename file
NullVoxPopuli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| --- | ||
| stage: accepted | ||
| start-date: 2025-08-15T00:00:00.000Z | ||
| release-date: # In format YYYY-MM-DDT00:00:00.000Z | ||
| release-versions: | ||
| teams: | ||
| - framework | ||
| prs: | ||
| accepted: https://github.com/emberjs/rfcs/pull/1136 | ||
| project-link: | ||
| suite: | ||
| --- | ||
|
|
||
| <!--- | ||
| Directions for above: | ||
|
|
||
| stage: Leave as is | ||
| start-date: Fill in with today's date, 2032-12-01T00:00:00.000Z | ||
| release-date: Leave as is | ||
| release-versions: Leave as is | ||
| teams: Include only the [team(s)](README.md#relevant-teams) for which this RFC applies | ||
| prs: | ||
| accepted: Fill this in with the URL for the Proposal RFC PR | ||
| project-link: Leave as is | ||
| suite: Leave as is | ||
| --> | ||
|
|
||
| <!-- Replace "RFC title" with the title of your RFC --> | ||
|
|
||
| # lowLevel.subtle.watch | ||
|
|
||
| ## Summary | ||
|
|
||
| Introduce a new low-level API, `lowLevel.subtle.watch`, available from `@ember/renderer`, which allows users to register a callback that runs when tracked data changes. This API is designed for advanced use cases and is not intended for general application reactivity. | ||
|
|
||
| It is not a replacement for computed properties, autotracking, or other high-level reactivity features. | ||
|
|
||
| > [!CAUTION] | ||
| > This is not a tool for general use. | ||
|
|
||
| [tc39-signals]: https://github.com/tc39/proposal-signals | ||
|
|
||
| ## Motivation | ||
|
|
||
| Some advanced scenarios require observing changes to tracked data without triggering a re-render or scheduling a revalidation. The `lowLevel.subtle.watch` API provides a mechanism for users to hook into tracked data changes at a low level, similar to [TC39's signals + watchers proposal][tc39-signals]: | ||
|
|
||
| Use cases include: | ||
| - synchronizing external state whithout the need to piggy-back off DOM-rendering | ||
| - ember-concurrency's `waitFor` witch would not need to rely on observers (as today) or polling | ||
| - Building alternate renderers (instead of rendering to DOM, render to `<canvas>`, or the Terminal) | ||
|
|
||
| > [!CAUTION] | ||
| > This API is not intended for application logic. | ||
|
|
||
| ## Detailed design | ||
|
|
||
| ### API Signature | ||
|
|
||
| ```ts | ||
| type Unwatch = () => void; | ||
| function watch(callback: () => void): Unwatch; | ||
| ``` | ||
|
|
||
| The API is available as `lowLevel.subtle.watch` from `@ember/renderer`. | ||
|
|
||
| ### Lifecycle and Semantics | ||
|
|
||
| - The callback runs during the transaction in `_renderRoot`, piggybacking on infrastructure that prevents sets to tracked data during render | ||
| - This is not immediately after tracked data changes, nor during `scheduleRevalidate` (which is called whenever `dirtyTag` is called) | ||
| - Callbacks are registered and run after tracked data changes, but before the next render completes | ||
| - Multiple callbacks may be registered; all will run in the same serially in relation to each other (if the same tracked data changes) | ||
| - Callbacks must not mutate tracked data. Attempting to do so will throw an error -- the backtracking re-render protection message. | ||
|
|
||
| ### Safeguards | ||
|
|
||
| - If a callback attempts to set tracked data, an error is thrown to prevent feedback loops and maintain render integrity | ||
| - Callbacks are run in a controlled environment, leveraging Ember's transaction system to avoid side effects | ||
| - This API is intended for low-level integrations and debugging tools, not for general application logic | ||
|
|
||
| ### Comparison to TC39 Signals/Watchers | ||
|
|
||
| - TC39's `watch` proposal allows observing changes to signals in JavaScript | ||
| - Ember's `lowLevel.subtle.watch` is similar in spirit but scoped to tracked properties and the rendering lifecycle | ||
| - This API does not provide direct access to the changed value or path; it is a notification mechanism only | ||
| - Unlike TC39 watchers, this API is tied to Ember's render transaction system | ||
| - if/when [TC39 Signals][tc39-signals] are implemented, the implementation of this behavior can be swapped out for the native implementation (as would be the case for all of Ember's reactivity) | ||
| - `watch` will return a method to `unwatch` which can be called at any time and will remove the callback from the in-memory list of callbacks to keep track of. | ||
|
|
||
| ### Example | ||
|
|
||
| ```gjs | ||
| import { lowLevel } from '@ember/renderer'; | ||
| import { cell } from '@ember/reactive'; | ||
|
|
||
| const count = cell(0); | ||
| const increment = () => count.current++; | ||
|
|
||
| lowLevel.subtle.watch(() => { | ||
| // This callback runs when tracked data changes | ||
| console.log('Tracked data changed! :: ', count.current); | ||
|
|
||
| // Forbidden operations: setting tracked data | ||
| // count.current = 'new value'; // Will throw! | ||
| }); | ||
|
|
||
| <template> | ||
| <button onclick={{increment}}>++</button> | ||
| </template> | ||
| ``` | ||
|
|
||
| ### `waitFor` implementation for `ember-concurrency` | ||
|
|
||
| ```js | ||
| import { lowLevel } from '@ember/renderer'; | ||
| import { registerDestructor, unregisterDestructor } from '@ember/destroyable'; | ||
|
|
||
| function waitFor(context, callback, timeout = 10_000) { | ||
| let pass; | ||
| let fail; | ||
| let promise = new Promise((resolve, reject) => { | ||
| pass = resolve; | ||
| fail = reject; | ||
| }); | ||
| let timer = setTimeout(() => fail(`Timed out waiting ${timeout}ms!`), timeout); | ||
| let unwatch = lowLevel.subtle.watch(() => { | ||
| if (callback()) { | ||
| clearTimeout(timer); | ||
| pass(); | ||
| unwatch(); | ||
| unregisterDestructor(context, unwatch); | ||
| } | ||
| }); | ||
|
|
||
| registerDestructor(context, unwatch); | ||
| } | ||
| ``` | ||
|
|
||
| usage: | ||
| ```js | ||
| import { task, waitFor } from 'ember-concurrency'; | ||
|
|
||
| export class Demo extends Component { | ||
| @tracked foo = 0; | ||
|
|
||
| myTask = task(async () => { | ||
| console.log("Waiting for `foo` to become 5"); | ||
|
|
||
| await waitFor(this, () => this.foo === 5); | ||
|
|
||
| console.log("`foo` is 5!"); | ||
| }); | ||
| } | ||
| ``` | ||
|
|
||
| ## How we teach this | ||
|
|
||
| Until we prove that this isn't problematic, we should not provide documentation other than basic API docs on the export. | ||
|
|
||
| We do want to encourage intrepid developers to explore implementation of other renderers using this utility. | ||
|
|
||
| ### Terminology | ||
|
|
||
| - "Subtle" indicates low-level, non-intrusive observation | ||
| - "Watch" aligns with TC39 Signals terminology and developer expectations | ||
| - "lowLevel" namespace clearly indicates advanced/internal usage | ||
|
|
||
| ## Drawbacks | ||
|
|
||
| - Exposes internals that may be misused for application logic | ||
| - May increase complexity for debugging and maintenance | ||
| - Lack of unsubscribe mechanism may lead to memory leaks if misused | ||
| - May encourage patterns that bypass Ember's intended reactivity model | ||
| - Could be confused with higher-level reactivity APIs despite "subtle" naming | ||
|
|
||
| ## Alternatives | ||
|
|
||
| n/a (for now / to start with) | ||
|
|
||
| ## Unresolved questions | ||
|
|
||
| n/a | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.