Skip to content
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

Avoid infinite loops when reading context values at construct time #39

Merged
merged 2 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/strange-dolls-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ember-provide-consume-context": patch
---

Avoid infinite loops when reading context values at construct time
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ export class ProvideConsumeContextContainer {
// not the VM ones).
// The values are objects that map a string ID (provider ID) to the provider
// component instance.
contexts = new WeakMap<any, Contexts>();
// "parentContexts" contain references to contexts coming from "above", and
// are used to read values from (which allows a component to provide and consume the same key)
parentContexts = new WeakMap<any, Contexts>();
// "nextContexts" are context maps used to propagate context values down
// into the component tree, which includes the merged providers from the
// current component (if any)
nextContexts = new WeakMap<any, Contexts>();

// Global contexts are registered by test-support helpers to allow easily
// providing context values in tests.
Expand Down Expand Up @@ -135,52 +141,56 @@ export class ProvideConsumeContextContainer {
}

private registerProvider(provider: any) {
const providerContexts: Contexts = this.currentContexts();
const parentContexts: Contexts = this.currentContexts();
const mergedContexts: Contexts = { ...parentContexts };

const registeredContexts = provider[EMBER_PROVIDE_CONSUME_CONTEXT_KEY];

// If the provider has registered contexts, store references
// to them on the current contexts object
if (registeredContexts != null) {
Object.entries(
registeredContexts as Record<keyof ContextRegistry, string>,
).forEach(([contextKey, key]) => {
if (key in provider) {
providerContexts[contextKey] = {
mergedContexts[contextKey] = {
instance: provider,
key,
};
}
});
}

this.contexts.set(provider, providerContexts);
this.parentContexts.set(provider, parentContexts);
this.nextContexts.set(provider, mergedContexts);
}

private registerComponent(component: any) {
const currentContexts = this.currentContexts();

// If a current context reference or global contexts exist, register them to the component
if (Object.keys(currentContexts).length > 0) {
this.contexts.set(component, currentContexts);
}
this.parentContexts.set(component, currentContexts);
this.nextContexts.set(component, currentContexts);
}

currentContexts() {
const { current } = this;

const globalContexts = this.#globalContexts ?? {};

if (this.contexts.has(current) || Object.keys(globalContexts).length > 0) {
const context = this.contexts.get(current);
if (
this.nextContexts.has(current) ||
Object.keys(globalContexts).length > 0
) {
const context = this.nextContexts.get(current);
return { ...globalContexts, ...context };
}

return {};
}

contextsFor(component: any) {
if (this.contexts.has(component)) {
return this.contexts.get(component);
if (this.parentContexts.has(component)) {
return this.parentContexts.get(component);
}

// If a context for this component is not yet registered, but
Expand Down
113 changes: 113 additions & 0 deletions test-app/tests/integration/components/decorators-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -972,4 +972,117 @@ module('Integration | Decorators', function (hooks) {
assert.dom('#content-1').hasText('1');
assert.dom('#content-2').hasText('2');
});

test('provider that references its own context and reading context value at construct time', async function (assert) {
class TestProviderComponent extends Component<{
Element: HTMLDivElement;
Blocks: {
default: [];
};
}> {
@consume('my-test-context') parentContextValue!: number;

get nextContextValue() {
return (this.parentContextValue ?? 0) + 1;
}
}

setComponentTemplate(
// @ts-ignore
hbs`{{! @glint-ignore }}
<ContextProvider @key="my-test-context" @value={{this.nextContextValue}}>{{yield}}</ContextProvider>
`,
TestProviderComponent,
);

class TestConsumerComponent extends Component<{
Element: HTMLDivElement;
}> {
@consume('my-test-context') contextValue!: number;

readValue = this.contextValue;
}

setComponentTemplate(
// @ts-ignore
hbs`{{! @glint-ignore }}
<div id="content">{{this.readValue}}</div>
`,
TestConsumerComponent,
);

interface TestContext {
TestProviderComponent: typeof TestProviderComponent;
TestConsumerComponent: typeof TestConsumerComponent;
}
(this as unknown as TestContext).TestProviderComponent =
TestProviderComponent;
(this as unknown as TestContext).TestConsumerComponent =
TestConsumerComponent;

await render<TestContext>(hbs`
<this.TestProviderComponent>
<this.TestConsumerComponent />
</this.TestProviderComponent>
`);

assert.dom('#content').hasText('1');
});

test('ContextProvider that references its components context and reading context value at construct time', async function (assert) {
class TestProviderComponent extends Component<{
Element: HTMLDivElement;
Blocks: {
default: [];
};
}> {
@consume('my-test-context') parentContextValue!: number;

@provide('my-test-context')
get nextContextValue() {
return (this.parentContextValue ?? 0) + 1;
}
}

setComponentTemplate(
// @ts-ignore
hbs`{{! @glint-ignore }}
{{yield}}
`,
TestProviderComponent,
);

class TestConsumerComponent extends Component<{
Element: HTMLDivElement;
}> {
@consume('my-test-context') contextValue!: number;

readValue = this.contextValue;
}

setComponentTemplate(
// @ts-ignore
hbs`{{! @glint-ignore }}
<div id="content">{{this.readValue}}</div>
`,
TestConsumerComponent,
);

interface TestContext {
TestProviderComponent: typeof TestProviderComponent;
TestConsumerComponent: typeof TestConsumerComponent;
}
(this as unknown as TestContext).TestProviderComponent =
TestProviderComponent;
(this as unknown as TestContext).TestConsumerComponent =
TestConsumerComponent;

await render<TestContext>(hbs`
<this.TestProviderComponent>
<this.TestConsumerComponent />
</this.TestProviderComponent>
`);

assert.dom('#content').hasText('1');
});
});