-
Notifications
You must be signed in to change notification settings - Fork 439
feat: CSR/SSRv1 context #5356
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
feat: CSR/SSRv1 context #5356
Changes from 17 commits
8097059
60040a9
f9a9acb
b394e9e
d395543
3267884
b95f94f
f857d10
4d7c32b
1181705
bfbb5ca
c5b29e4
8edc472
043eb29
24dcf50
094aeb8
5c4b25f
eed8082
4ae7f1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| /* | ||
| * Copyright (c) 2025, salesforce.com, inc. | ||
| * All rights reserved. | ||
| * SPDX-License-Identifier: MIT | ||
| * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT | ||
| */ | ||
| import { | ||
| isUndefined, | ||
| getPrototypeOf, | ||
| keys, | ||
| getContextKeys, | ||
| ArrayFilter, | ||
| ContextEventName, | ||
| isTrustedContext, | ||
| type ContextProvidedCallback, | ||
| type ContextConnector as IContextConnector, | ||
| } from '@lwc/shared'; | ||
| import { type VM } from '../vm'; | ||
| import { logWarnOnce } from '../../shared/logger'; | ||
| import type { Signal } from '@lwc/signals'; | ||
| import type { RendererAPI } from '../renderer'; | ||
| import type { ShouldContinueBubbling } from '../wiring/types'; | ||
|
|
||
| type ContextVarieties = Map<unknown, Signal<unknown>>; | ||
jhefferman-sfdc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| class ContextConnector<C extends object> implements IContextConnector<C> { | ||
| component: C; | ||
| #renderer: RendererAPI; | ||
| #providedContextVarieties: ContextVarieties; | ||
| #elm: HTMLElement; | ||
|
|
||
| constructor(vm: VM, component: C, providedContextVarieties: ContextVarieties) { | ||
| this.component = component; | ||
| this.#renderer = vm.renderer; | ||
| this.#elm = vm.elm; | ||
| this.#providedContextVarieties = providedContextVarieties; | ||
| } | ||
|
|
||
| provideContext<V extends object>( | ||
| contextVariety: V, | ||
| providedContextSignal: Signal<unknown> | ||
| ): void { | ||
| // registerContextProvider is called one time when the component is first provided context. | ||
|
||
| // The component is then listening for consumers to consume the provided context. | ||
| if (this.#providedContextVarieties.size === 0) { | ||
| this.#renderer.registerContextProvider( | ||
| this.#elm, | ||
| ContextEventName, | ||
| (payload): ShouldContinueBubbling => { | ||
| // This callback is invoked when the provided context is consumed somewhere down | ||
| // in the component's subtree. | ||
| return payload.setNewContext(this.#providedContextVarieties); | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| if (this.#providedContextVarieties.has(contextVariety)) { | ||
| logWarnOnce( | ||
divmain marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 'Multiple contexts of the same variety were provided. Only the first context will be used.' | ||
| ); | ||
| return; | ||
| } | ||
| this.#providedContextVarieties.set(contextVariety, providedContextSignal); | ||
| } | ||
|
|
||
| consumeContext<V extends object>( | ||
| contextVariety: V, | ||
| contextProvidedCallback: ContextProvidedCallback | ||
| ): void { | ||
| this.#renderer.registerContextConsumer(this.#elm, ContextEventName, { | ||
| setNewContext: (providerContextVarieties: ContextVarieties): ShouldContinueBubbling => { | ||
| // If the provider has the specified context variety, then it is consumed | ||
| // and true is returned to stop bubbling. | ||
| if (providerContextVarieties.has(contextVariety)) { | ||
| contextProvidedCallback(providerContextVarieties.get(contextVariety)); | ||
| return true; | ||
| } | ||
| // Return false as context has not been found/consumed | ||
| // and the consumer should continue traversing the context tree | ||
| return false; | ||
| }, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| export function connectContext(vm: VM) { | ||
| const contextKeys = getContextKeys(); | ||
|
|
||
| if (isUndefined(contextKeys)) { | ||
| return; | ||
| } | ||
|
|
||
| const { connectContext } = contextKeys; | ||
| const { component } = vm; | ||
|
|
||
| const enumerableKeys = keys(getPrototypeOf(component)); | ||
| const contextfulKeys = ArrayFilter.call(enumerableKeys, (enumerableKey) => | ||
| isTrustedContext((component as any)[enumerableKey]) | ||
| ); | ||
|
|
||
| if (contextfulKeys.length === 0) { | ||
| return; | ||
| } | ||
|
|
||
| const providedContextVarieties: ContextVarieties = new Map(); | ||
|
|
||
| try { | ||
divmain marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| for (let i = 0; i < contextfulKeys.length; i++) { | ||
| (component as any)[contextfulKeys[i]][connectContext]( | ||
| new ContextConnector(vm, component, providedContextVarieties) | ||
| ); | ||
| } | ||
| } catch (err: any) { | ||
| logWarnOnce( | ||
| `Attempted to connect to trusted context but received the following error: ${ | ||
| err.message | ||
| }` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| export function disconnectContext(vm: VM) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The exposed functions look good. Let's just add docs, and I think they're finished.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are not exposed via engine-server/dom - IIUC:
|
||
| const contextKeys = getContextKeys(); | ||
|
|
||
| if (!contextKeys) { | ||
| return; | ||
| } | ||
|
|
||
| const { disconnectContext } = contextKeys; | ||
| const { component } = vm; | ||
|
|
||
| const enumerableKeys = keys(getPrototypeOf(component)); | ||
| const contextfulKeys = ArrayFilter.call(enumerableKeys, (enumerableKey) => | ||
| isTrustedContext((component as any)[enumerableKey]) | ||
| ); | ||
|
|
||
| if (contextfulKeys.length === 0) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| for (let i = 0; i < contextfulKeys.length; i++) { | ||
| (component as any)[contextfulKeys[i]][disconnectContext](component); | ||
| } | ||
| } catch (err: any) { | ||
| logWarnOnce( | ||
| `Attempted to disconnect from trusted context but received the following error: ${ | ||
| err.message | ||
| }` | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,6 +49,7 @@ import { flushMutationLogsForVM, getAndFlushMutationLogs } from './mutation-logg | |
| import { connectWireAdapters, disconnectWireAdapters, installWireAdapters } from './wiring'; | ||
| import { VNodeType, isVFragment } from './vnodes'; | ||
| import { isReportingEnabled, report, ReportingEventId } from './reporting'; | ||
| import { connectContext, disconnectContext } from './modules/context'; | ||
| import type { VNodes, VCustomElement, VNode, VBaseElement, VStaticPartElement } from './vnodes'; | ||
| import type { ReactiveObserver } from './mutation-tracker'; | ||
| import type { | ||
|
|
@@ -702,6 +703,12 @@ export function runConnectedCallback(vm: VM) { | |
| if (hasWireAdapters(vm)) { | ||
| connectWireAdapters(vm); | ||
| } | ||
|
|
||
| if (lwcRuntimeFlags.ENABLE_EXPERIMENTAL_SIGNALS) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @divmain I protected the new context implementation with the same
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-using the flag makes sense to me. They're closely entangled, as you say. |
||
| // Setup context before connected callback is executed | ||
| connectContext(vm); | ||
| } | ||
|
|
||
| const { connectedCallback } = vm.def; | ||
| if (!isUndefined(connectedCallback)) { | ||
| logOperationStart(OperationId.ConnectedCallback, vm); | ||
|
|
@@ -751,6 +758,11 @@ function runDisconnectedCallback(vm: VM) { | |
| if (process.env.NODE_ENV !== 'production') { | ||
| assert.isTrue(vm.state !== VMState.disconnected, `${vm} must be inserted.`); | ||
| } | ||
|
|
||
| if (lwcRuntimeFlags.ENABLE_EXPERIMENTAL_SIGNALS) { | ||
| disconnectContext(vm); | ||
| } | ||
|
|
||
| if (isFalse(vm.isDirty)) { | ||
| // this guarantees that if the component is reused/reinserted, | ||
| // it will be re-rendered because we are disconnecting the reactivity | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.