From b9f88348900717c0200d55dac7e2e57f03f0e85c Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:09:02 -0500 Subject: [PATCH 01/23] Motivation and Summary --- ...0-public-api-for-contextual-exploration.md | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 text/0000-public-api-for-contextual-exploration.md diff --git a/text/0000-public-api-for-contextual-exploration.md b/text/0000-public-api-for-contextual-exploration.md new file mode 100644 index 0000000000..e4f358f360 --- /dev/null +++ b/text/0000-public-api-for-contextual-exploration.md @@ -0,0 +1,98 @@ +--- +stage: accepted +start-date: # In format YYYY-MM-DDT00:00:00.000Z +release-date: # In format YYYY-MM-DDT00:00:00.000Z +release-versions: +teams: # delete teams that aren't relevant + - cli + - data + - framework + - learning + - steering + - typescript +prs: + accepted: # Fill this in with the URL for the Proposal RFC PR +project-link: +suite: +--- + + + + + +# Public API for render-tree-based contextual exploration + +## Summary + +This RFC proposes a public API that enables safe experimentation with render-tree-based contextual data access patterns. The API provides scope-based synchronous access for all invokables (components, helpers, modifiers) across apps, engines, and addons. + +## Motivation + +The Ember community has long desired a Context API to share state across the render tree without prop drilling, but developers currently must resort to [abusing private Glimmer APIs](https://github.com/customerio/ember-provide-consume-context/blob/def6d34f639d56ebec1c7c8c888f86ec524b8688/ember-provide-consume-context/src/-private/override-glimmer-runtime-classes.ts#L1), creating upgrade risks and maintenance burdens. This RFC provides a public API that enables safe experimentation with Context patterns for use cases like theme systems, authentication state, and feature flags. The expected outcome is to establish a stable foundation for community exploration of contextual data access patterns while gathering real-world feedback to inform future official Context API design. + +Additionally, this also enables exploration of providing _owner_ access to plain function helpers, modifiers, (all invokables). + +## Detailed design + +> This is the bulk of the RFC. + +> Explain the design in enough detail for somebody +familiar with the framework to understand, and for somebody familiar with the +implementation to implement. This should get into specifics and corner-cases, +and include examples of how the feature is used. Any new terminology should be +defined here. + +> Please keep in mind any implications within the Ember ecosystem, such as: +> - Lint rules (ember-template-lint, eslint-plugin-ember) that should be added, modified or removed +> - Features that are replaced or made obsolete by this feature and should eventually be deprecated +> - Ember Inspector and debuggability +> - Server-side Rendering +> - Ember Engines +> - The Addon Ecosystem +> - IDE Support +> - Blueprints that should be added or modified + +## How we teach this + +> What names and terminology work best for these concepts and why? How is this +idea best presented? As a continuation of existing Ember patterns, or as a +wholly new one? + +> Would the acceptance of this proposal mean the Ember guides must be +re-organized or altered? Does it change how Ember is taught to new users +at any level? + +> How should this feature be introduced and taught to existing Ember +users? + +> Keep in mind the variety of learning materials: API docs, guides, blog posts, tutorials, etc. + +## Drawbacks + +> Why should we *not* do this? Please consider the impact on teaching Ember, +on the integration of this feature with other existing and planned features, +on the impact of the API churn on existing apps, etc. + +> There are tradeoffs to choosing any path, please attempt to identify them here. + +## Alternatives + +- [ember-provide-consume-context](https://github.com/customerio/ember-provide-consume-context) +- [passing a third argument to component constructors that is the VM Stack](https://github.com/rtablada/ember-context-experiment/blob/main/app/components/UserName.gjs) + +## Unresolved questions + +> Optional, but suggested for first drafts. What parts of the design are still +TBD? From 6feee9ead215ce3b522c5a05494656d52c08ce81 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:35:03 -0500 Subject: [PATCH 02/23] Some how we teach this via example --- ...0-public-api-for-contextual-exploration.md | 353 ++++++++++++++++-- 1 file changed, 321 insertions(+), 32 deletions(-) diff --git a/text/0000-public-api-for-contextual-exploration.md b/text/0000-public-api-for-contextual-exploration.md index e4f358f360..4c73994618 100644 --- a/text/0000-public-api-for-contextual-exploration.md +++ b/text/0000-public-api-for-contextual-exploration.md @@ -46,46 +46,336 @@ Additionally, this also enables exploration of providing _owner_ access to plain ## Detailed design -> This is the bulk of the RFC. - -> Explain the design in enough detail for somebody -familiar with the framework to understand, and for somebody familiar with the -implementation to implement. This should get into specifics and corner-cases, -and include examples of how the feature is used. Any new terminology should be -defined here. - -> Please keep in mind any implications within the Ember ecosystem, such as: -> - Lint rules (ember-template-lint, eslint-plugin-ember) that should be added, modified or removed -> - Features that are replaced or made obsolete by this feature and should eventually be deprecated -> - Ember Inspector and debuggability -> - Server-side Rendering -> - Ember Engines -> - The Addon Ecosystem -> - IDE Support -> - Blueprints that should be added or modified +> [!NOTE] +> This RFC does not intend to tie is to any implementation details in our renderer, as experimentation in this area should be possible without changing existing behavior for pre-exitsing apps, and such renderer changes would not change the behavior of what this RFC proposes. + +### The Public API + +A few examples of usage, once implemented, + +
Accessing the owner in a plain function + +```gjs +import { getScope } from '@ember/renderer'; +import { getOwner } from '@ember/owner'; + +function currentRoute() { + const scope = getScope(); + const owner = getOwner(scope); + + return owner.lookup('service:router').currentRouteName; +} + + +``` + +
+ +
Accessing the owner in a modifier + +```gjs +import { getScope } from '@ember/renderer'; +import { modifier } from 'ember-modifier'; + +const dimensions = modifier((element) => { + const scope = getScope(); + const owner = getOwner(scope); + const resize = owner.lookup('service:resize'); + const callback => (entry) => { + element.textContent = JSON.stringify(entry); + }; + + resize.observe(element, callback); + + return () => { + resize.unobserve(element, callback) + } +}) + +``` + +
+ +
Accessing via component getter + +```gjs +import Component from '@glimmer/component'; +import { getScope } from '@ember/renderer'; +import { getOwner } from '@ember/owner'; + +export default class Demo extends Component { + // equiv of @service router; + get router() { + return getOwner(getScope()).lookup('service:router'); + } + + +} +``` + +
+ +
Writing to scope: A component adding to the scope for the component's descendants + +```gjs +import { addToScope, getScope } from '@ember/renderer'; +import Component from '@glimmer/component'; + +class MyCustomState { + /* ... */ + foo = 2; +} + +class Demo extends Component { + constructor(owner, args) { + super(owner, args); + + let state = new MyCustomState(owner); + addToScope(state); + } + + +} + +function accessIt() { + let scope = getScope(); + let state = scope.entries.find(x => x instanceof MyCustomState); + + return state.foo; +} + +// And then accessing: + +``` + +
+ +### Interface + +```ts +import type Owner from '@ember/owner'; + +function getScope(): Scope | undefined; + +interface Scope { + /** + * The owner can change based on rendering context, + * renderComponent usage, engine usage, etc. + * + * there are two ways this could be implemented: + * - the framework could add the owner into 'entries' whenever it would change + * - since the renderer _always_ knows the current owner, we reference that + */ + get [OwnerSymbol](): Owner; + + /** + * Each rendering layer will push into this array, + * we can detect usage of `addToScope`, and pop when we exit rendering the thing that called `addToScope`. + * + * This array should contain references *only* (but we can't enforce that). + * For each invokable, we'll associate the scope with the invokable during its creation. + * + * This means that modifications to the scope *cannot* occur during updates. + */ + entries: unknown[]; +} +``` + + + +### High-level "how it works" + +Simular to the exiting debug-render-tree, we'd keep track of a set of "scope" objects throughout the render hierarchy. + +And similar to how auto-tracking works, we'd enable access to the scope via: +```js +// psuedocode + +// Evaluating an "Invokable" ( a curly expression, component, etc ) +globalThis.scope = currentScope(); // implementation depends on renderer strategy +invoke() +delete globalThis.scope; + +// in @ember/renderer +export function getScope() { + return globalThis.scope; +} +``` + +> [!NOTE] +> `globalThis.scope` is not literally being suggested here -- this variable should be private, and un findable by means other than the exported `getScope` function. + +### How to add to the scope + +### Limitations + +Because this is a synchronous API, `getScope()` will be undefined after any `await`, and should not be used after an `await` as a result. A new rule to `eslint-plugin-ember` can help out here. + ## How we teach this -> What names and terminology work best for these concepts and why? How is this -idea best presented? As a continuation of existing Ember patterns, or as a -wholly new one? +This is a low-level API, and the API Docs generated from the JSDoc for these newly expored functions should have some examples. + +It could also be worth demonstrating in the guides how to implement "Context" via these APIs -- for example: + +```ts +// hypothetical ember-prototype-context library +import { addToScope, getScope } from '@ember/renderer'; +import Component from '@glimmer/component'; + +export class Provide extends Component { + constructor(owner, args) { + assert(`Must pass @context`, args.context); + assert(`Must pass a key`, args.key) + + provide([args.key, args.context]); + } +} + +export function provide(...args) { + if (args.length === 3) { + /** + * TC39's Stage 1 Decorator + */ + return provideTC39Stage1Decorator(...args); // implementation omitted for brevity + } + + if (args.length === 2 && 'kind' in args[1]) { + if (args[1].kind === 'field') { + /** + * TC39 "Field" Decorator + * + * @provide x = new State(); + */ + return (initialValue) => provide(initialValue.constructor, initialValue); + } + + throw new Error(`Unsupported decorator usage for kind: ${args[1].kind}`); + } + + /** + * provide(key, instance or state); + */ + addToScope([key, context]); + + return context; +} + +export function consume(key: ClassType): ContextType { + let scope = getScope(); + + // search up until we find our key + for (let i = 0; i < scope.entries.length; i++) { + let entry = scope.entries[i]; + + if (!Array.isArray(entry)) continue; + if (entry[0] === key) return entry[1]; + } +} -> Would the acceptance of this proposal mean the Ember guides must be -re-organized or altered? Does it change how Ember is taught to new users -at any level? +export class Consume extends Component { + get context() { + assert(`Must pass a key`, this.args.key); + + return consume(this.args.key); + } + + +} +``` + +This supports both class providing and consuming as well as template-based providing and consuming. + +```gjs +import { Consume, Provide, consume, provide } from 'hypothetical ember-prototype-context library'; + +let id = 0; +class StateA { foo = 'a'; id = id++; } +class StateB { foo = 'b' } + +const newA = () => new StateA(); +const newB = () => new StateB(); + +class CustomProvide extends Component { + @provide a = new StateA(); +} + +const CustomConsume = ; + + +``` -> How should this feature be introduced and taught to existing Ember -users? -> Keep in mind the variety of learning materials: API docs, guides, blog posts, tutorials, etc. ## Drawbacks -> Why should we *not* do this? Please consider the impact on teaching Ember, -on the integration of this feature with other existing and planned features, -on the impact of the API churn on existing apps, etc. +We already track a "stack" in our current renderer, but depending on how this is implemented, it could accidentally double the memory usage of that stack -- however most "scopes" will be empty / reused between render nodes, as what is in the scope is not expected to change that often. + +Depending on implementation, there _could_ be a userland-induced memory leak, so we'll want to explore how to ensure the scope entries don't endlessly grow over time (we already manage this with the current renderer's stack (see second alternative below)). -> There are tradeoffs to choosing any path, please attempt to identify them here. +Adding more APIs is always more for people to learn, but many frontend frameworks already have similar features, so developers should have an easy time picking this up if they wish (or the future features enabled by this API). ## Alternatives @@ -94,5 +384,4 @@ on the impact of the API churn on existing apps, etc. ## Unresolved questions -> Optional, but suggested for first drafts. What parts of the design are still -TBD? +n/a (so far) \ No newline at end of file From 428047dc0533008b0d6093b71d84a0f5dbb2533f Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:39:49 -0500 Subject: [PATCH 03/23] More examples --- ...0-public-api-for-contextual-exploration.md | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/text/0000-public-api-for-contextual-exploration.md b/text/0000-public-api-for-contextual-exploration.md index 4c73994618..00b446c42c 100644 --- a/text/0000-public-api-for-contextual-exploration.md +++ b/text/0000-public-api-for-contextual-exploration.md @@ -228,7 +228,44 @@ Because this is a synchronous API, `getScope()` will be undefined after any `awa This is a low-level API, and the API Docs generated from the JSDoc for these newly expored functions should have some examples. -It could also be worth demonstrating in the guides how to implement "Context" via these APIs -- for example: +It could also be worth demonstrating in the guides how to implement some use cases. + +### Example service access without injection + +```gjs +import { getScope } from '@ember/renderer'; +import { getOwner } from '@ember/owner'; + +// in a hypothetical library +export function service(name) { + let scope = getScope(); + let owner = getOwner(scope); + + return owner.lookup(`service:${name}`); +} + +/** + * We no longer need class-based helpers to access services + */ +export function currentRouteName() { + return service('router').currentRouteName; +} +``` + +Usage: +```gjs + +``` + +Both of these usages would be reactive as teh router changes routes. + +### Example Context Implementation and Usage ```ts // hypothetical ember-prototype-context library @@ -368,7 +405,6 @@ const CustomConsume = ``` -Both of these usages would be reactive as teh router changes routes. +Both of these usages would be reactive as the router changes routes. ### Example Context Implementation and Usage From f7526637668c8583076720f1d2fef3ff6461f5e9 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:00:21 -0500 Subject: [PATCH 11/23] Be clear about difference between Alternatives and Prior Art --- ...api-for-render-tree-based-scope-exploration.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/text/1154-public-api-for-render-tree-based-scope-exploration.md b/text/1154-public-api-for-render-tree-based-scope-exploration.md index 221e9fe079..36df7331a9 100644 --- a/text/1154-public-api-for-render-tree-based-scope-exploration.md +++ b/text/1154-public-api-for-render-tree-based-scope-exploration.md @@ -437,8 +437,21 @@ Adding more APIs is always more for people to learn, but many frontend framework ## Alternatives +None. + +### Prior Art + +Context Explorations + - [ember-provide-consume-context](https://github.com/customerio/ember-provide-consume-context) -- [passing a third argument to component constructors that is the VM Stack](https://github.com/rtablada/ember-context-experiment/blob/main/app/components/UserName.gjs) +- [experiment passing the component tree to component managers](https://github.com/rtablada/ember-context-experiment/blob/main/app/components/UserName.gjs) + - for this RFC, we don't want to change how component managers work, because this scope feature should work for _all_ invokables and _all_ `{{}}` regions + + +Service-like things with non-string keys: + +- [createStore](https://ember-primitives.pages.dev/6-utils/createStore.md) +- [ember-polaris-service](https://github.com/chancancode/ember-polaris-service) ## Unresolved questions From 7491fad88e547ad16502cb7f67ac231082742b8a Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:06:38 -0500 Subject: [PATCH 12/23] Some clarifications --- ...for-render-tree-based-scope-exploration.md | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/text/1154-public-api-for-render-tree-based-scope-exploration.md b/text/1154-public-api-for-render-tree-based-scope-exploration.md index 36df7331a9..8895873ae8 100644 --- a/text/1154-public-api-for-render-tree-based-scope-exploration.md +++ b/text/1154-public-api-for-render-tree-based-scope-exploration.md @@ -184,15 +184,11 @@ interface Scope { get [OwnerSymbol](): Owner; /** - * Each rendering layer will push into this array, - * we can detect usage of `addToScope`, and pop when we exit rendering the thing that called `addToScope`. + * For iterating up the render tree. * - * This array should contain references *only* (but we can't enforce that). - * For each invokable, we'll associate the scope with the invokable during its creation. - * - * This means that modifications to the scope *cannot* occur during updates. + * This the data within should contain references *only* (but we can't enforce that). */ - entries: unknown[]; + entries: Iterator; } export function addToScope(x: unknown); @@ -207,6 +203,7 @@ Similar to the existing debug-render-tree, we'd keep track of a set of "scope" o And similar to how auto-tracking works, we'd enable access to the scope via: ```js // psuedocode +// for demonstration only, not actual code // Evaluating an "Invokable" ( a curly expression, component, etc ) globalThis.scope = currentScope(); // implementation may depend on renderer strategy @@ -222,24 +219,37 @@ export function addToScope(x) { let scope = getScope(); assert('cannot add to scope when there is no current scope', scope); - // It will probably be better to make this readonly from the outside and have some other way of - // manipulating the entries - scope.entries.push(x); + + // Implementation ommitted for brevity + setScopeData(scope, x); } ``` > [!NOTE] > `globalThis.scope` is not literally being suggested here -- this variable should be private, and un findable by means other than the exported `getScope` function. + +In renderer code, we already know the render tree, but none of it is exposed to users (it is all private API, and the debugRenderTree uses this for inspector support). Each rendering node will have the opportunity to set some userland metadata via `addToScope`. This userland metadata is privately stored on the rendering node, and can only be accessed via `getScope`. + +For crawling up the userland metadata of the render tree, you'd iterate over the [entries Iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator/Iterator) looking for whatever you need. + +### What's in `scope.entries`? + +- At a minimum, the `scope.entries` will contain a _root metadata_, the root of the application (containing the `owner`). +- Iterating `scope.entries` will always have _one_ iteration, unless `addToScope` is called during rendering. +- `scope.entries` is lazy, in that when rendering, we don't eagerly calculate what can be found within, nor while iterating (unless iteration completes, and hits the _root metadata_) +- For each render node, the metadata is undefined until set, so that iteration can skip over empty metadatas + + ### Inspector -todo +Because the inspector uses the debugRenderTree, and the debugRenderTree has access to everything that can have a scope -- the inspector can display the scope in the object inspector. ### Testing -todo +Testing with scope is the same as using in the application or library code. -### How to add to the scope +If you want to use a separate value or implementation of something higher up in the render tree, the test rendered code should be wrapped wiht a test implementation which sets the test-specific scope value. ### Limitations @@ -429,9 +439,7 @@ const CustomConsume =