Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion packages/@lwc/engine-core/src/framework/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,5 @@ export { default as wire } from './decorators/wire';
export { readonly } from './readonly';

export { setFeatureFlag, setFeatureFlagForTest } from '@lwc/features';
export { setTrustedSignalSet } from '@lwc/shared';
export { setContextKeys, setTrustedSignalSet } from '@lwc/shared';
export type { Stylesheet, Stylesheets } from '@lwc/shared';
7 changes: 6 additions & 1 deletion packages/@lwc/engine-core/src/framework/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import type { WireContextSubscriptionPayload } from './wiring';
import type { WireContextSubscriptionCallback, WireContextSubscriptionPayload } from './wiring';

export type HostNode = any;
export type HostElement = any;
Expand Down Expand Up @@ -76,6 +76,11 @@ export interface RendererAPI {
) => E;
defineCustomElement: (tagName: string, isFormAssociated: boolean) => void;
ownerDocument(elm: E): Document;
registerContextProvider: (
element: E,
adapterContextToken: string,
onContextSubscription: WireContextSubscriptionCallback
) => void;
registerContextConsumer: (
element: E,
adapterContextToken: string,
Expand Down
108 changes: 107 additions & 1 deletion packages/@lwc/engine-core/src/framework/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import {
ArrayFilter,
ArrayPush,
ArraySlice,
ArrayUnshift,
assert,
create,
ContextEventName,
defineProperty,
getPrototypeOf,
getOwnPropertyNames,
getContextKeys,
isArray,
isFalse,
isFunction,
Expand All @@ -20,10 +24,11 @@ import {
isTrue,
isUndefined,
flattenStylesheets,
keys,
} from '@lwc/shared';

import { addErrorComponentStack } from '../shared/error';
import { logError, logWarnOnce } from '../shared/logger';
import { logError, logErrorOnce, logWarnOnce } from '../shared/logger';

import {
renderComponent,
Expand All @@ -49,6 +54,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 type { VNodes, VCustomElement, VNode, VBaseElement, VStaticPartElement } from './vnodes';
import type { ReactiveObserver } from './mutation-tracker';
import type {
Expand All @@ -60,6 +66,12 @@ import type { ComponentDef } from './def';
import type { Template } from './template';
import type { HostNode, HostElement, RendererAPI } from './renderer';
import type { Stylesheet, Stylesheets, APIVersion } from '@lwc/shared';
import type { Signal } from '@lwc/signals';
import type {
ContextProvidedCallback,
ContextRuntimeAdapter,
ContextVarieties,
} from './wiring/types';

type ShadowRootMode = 'open' | 'closed';

Expand Down Expand Up @@ -699,6 +711,10 @@ export function runConnectedCallback(vm: VM) {
if (hasWireAdapters(vm)) {
connectWireAdapters(vm);
}

// Setup context before connected callback is executed
setupContext(vm);

const { connectedCallback } = vm.def;
if (!isUndefined(connectedCallback)) {
logOperationStart(OperationId.ConnectedCallback, vm);
Expand Down Expand Up @@ -740,6 +756,95 @@ export function runConnectedCallback(vm: VM) {
}
}

function setupContext(vm: VM) {
const contextKeys = getContextKeys();

if (isUndefined(contextKeys)) {
return;
}

const { connectContext } = contextKeys;
const { component, renderer } = vm;
const enumerableKeys = keys(getPrototypeOf(component));
const contextfulFieldsOrProps = ArrayFilter.call(
enumerableKeys,
(propName) => (component as any)[propName]?.[connectContext]
);

if (contextfulFieldsOrProps.length === 0) {
return;
}

let isProvidingContext = false;
const providedContextVarieties: ContextVarieties = new Map();

const contextRuntimeAdapter: ContextRuntimeAdapter<LightningElement> = {
component,
provideContext<T extends object>(
contextVariety: T,
providedContextSignal: Signal<unknown>
): void {
if (!isProvidingContext) {
isProvidingContext = true;
renderer.registerContextProvider(component, ContextEventName, (payload) => {
return payload.setNewContext(providedContextVarieties);
});
}
if (providedContextVarieties.has(contextVariety)) {
logErrorOnce(
'Multiple contexts of the same variety were provided. Only the first context will be used.'
);
return;
}
providedContextVarieties.set(contextVariety, providedContextSignal);
},
consumeContext<T extends object>(
contextVariety: T,
contextProvidedCallback: ContextProvidedCallback
): void {
renderer.registerContextConsumer(component, ContextEventName, {
setNewContext: (contextVarieties: ContextVarieties) => {
if (contextVarieties.has(contextVariety)) {
contextProvidedCallback(contextVarieties.get(contextVariety));
return true;
}
// Return false as context has not been found/consumed
// and the consumer should continue traversing the context tree
return false;
},
});
},
};

// Calls the connectContext method on the component for each contextful field or property
for (const contextfulFieldsOrProp of contextfulFieldsOrProps) {
(component as any)[contextfulFieldsOrProp][connectContext](contextRuntimeAdapter);
}
}

function cleanupContext(vm: VM) {
const contextKeys = getContextKeys();

if (!contextKeys) {
return;
}

const { disconnectContext } = contextKeys;
const { component } = vm;
const enumerableKeys = keys(getPrototypeOf(component));
const contextfulFieldsOrProps = enumerableKeys.filter(
(propName) => (component as any)[propName]?.[disconnectContext]
);

if (contextfulFieldsOrProps.length === 0) {
return;
}

for (const contextfulField of contextfulFieldsOrProps) {
(component as any)[contextfulField][disconnectContext](component);
}
}

function hasWireAdapters(vm: VM): boolean {
return getOwnPropertyNames(vm.def.wire).length > 0;
}
Expand All @@ -748,6 +853,7 @@ function runDisconnectedCallback(vm: VM) {
if (process.env.NODE_ENV !== 'production') {
assert.isTrue(vm.state !== VMState.disconnected, `${vm} must be inserted.`);
}
cleanupContext(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
Expand Down
4 changes: 3 additions & 1 deletion packages/@lwc/engine-core/src/framework/wiring/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ export function createContextProviderWithRegister(
consumerDisconnectedCallback(consumer);
}
};
setDisconnectedCallback(disconnectCallback);
setDisconnectedCallback?.(disconnectCallback);

consumerConnectedCallback(consumer);
return true;
}
);
};
Expand Down Expand Up @@ -91,6 +92,7 @@ export function createContextWatcher(
// eslint-disable-next-line @lwc/lwc-internal/no-invalid-todo
// TODO: dev-mode validation of config based on the adapter.contextSchema
callbackWhenContextIsReady(newContext);
return true;
},
setDisconnectedCallback(disconnectCallback: () => void) {
// adds this callback into the disconnect bucket so it gets disconnected from parent
Expand Down
24 changes: 21 additions & 3 deletions packages/@lwc/engine-core/src/framework/wiring/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import type { Signal } from '@lwc/signals';
import type { LightningElement } from '../base-lightning-element';
import type { HostElement } from '../renderer';

Expand Down Expand Up @@ -61,11 +62,11 @@ export interface WireDebugInfo {

export type WireContextSubscriptionCallback = (
subscriptionPayload: WireContextSubscriptionPayload
) => void;
) => boolean;

export interface WireContextSubscriptionPayload {
setNewContext(newContext: ContextValue): void;
setDisconnectedCallback(disconnectCallback: () => void): void;
setNewContext(newContext: ContextValue): boolean;
setDisconnectedCallback?(disconnectCallback: () => void): void;
}

export interface ContextConsumer {
Expand All @@ -82,6 +83,23 @@ export type ContextProvider = (
options: ContextProviderOptions
) => void;

export type ContextProvidedCallback = (contextSignal?: Signal<unknown>) => void;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface ContextRuntimeAdapter<T extends object> {
component: object;
provideContext<T extends object>(
contextVariety: T,
providedContextSignal: Signal<unknown>
): void;
consumeContext<T extends object>(
contextVariety: T,
contextProvidedCallback: ContextProvidedCallback
): void;
}

export type ContextVarieties = Map<unknown, Signal<unknown>>;

export type RegisterContextProviderFn = (
element: HostElement,
adapterContextToken: string,
Expand Down
4 changes: 4 additions & 0 deletions packages/@lwc/engine-core/src/shared/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export function logError(message: string, vm?: VM) {
log('error', message, vm, false);
}

export function logErrorOnce(message: string, vm?: VM) {
log('error', message, vm, true);
}

export function logWarn(message: string, vm?: VM) {
log('warn', message, vm, false);
}
Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/engine-dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export {
isComponentConstructor,
parseFragment,
parseSVGFragment,
setContextKeys,
setTrustedSignalSet,
swapComponent,
swapStyle,
Expand Down
18 changes: 11 additions & 7 deletions packages/@lwc/engine-dom/src/renderer/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import type {

export class WireContextSubscriptionEvent extends CustomEvent<undefined> {
// These are initialized on the constructor via defineProperties.
public readonly setNewContext!: (newContext: WireContextValue) => void;
public readonly setDisconnectedCallback!: (disconnectCallback: () => void) => void;
public readonly setNewContext!: (newContext: WireContextValue) => boolean;
public readonly setDisconnectedCallback?: (disconnectCallback: () => void) => void;

constructor(
adapterToken: string,
Expand Down Expand Up @@ -56,11 +56,15 @@ export function registerContextProvider(
onContextSubscription: WireContextSubscriptionCallback
) {
addEventListener(elm, adapterContextToken, ((evt: WireContextSubscriptionEvent) => {
evt.stopImmediatePropagation();
const { setNewContext, setDisconnectedCallback } = evt;
onContextSubscription({
setNewContext,
setDisconnectedCallback,
});
// If context subscription is successful, stop event propagation
if (
onContextSubscription({
setNewContext,
setDisconnectedCallback,
})
) {
evt.stopImmediatePropagation();
}
}) as EventListener);
}
10 changes: 5 additions & 5 deletions packages/@lwc/engine-server/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function createContextProvider(adapter: WireAdapterConstructor) {
return createContextProviderWithRegister(adapter, registerContextProvider);
}

function registerContextProvider(
export function registerContextProvider(
elm: HostElement | LightningElement,
adapterContextToken: string,
onContextSubscription: WireContextSubscriptionCallback
Expand Down Expand Up @@ -57,10 +57,10 @@ export function registerContextConsumer(
const subscribeToProvider =
currentNode[HostContextProvidersKey].get(adapterContextToken);
if (!isUndefined(subscribeToProvider)) {
subscribeToProvider(subscriptionPayload);
// If we find a provider, we shouldn't continue traversing
// looking for another provider.
break;
// If context subscription is successful, stop traversing to locate a provider
if (subscribeToProvider(subscriptionPayload)) {
break;
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/@lwc/engine-server/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
startTrackingMutations,
stopTrackingMutations,
} from './utils/mutation-tracking';
import { registerContextConsumer } from './context';
import { registerContextConsumer, registerContextProvider } from './context';
import type { HostNode, HostElement, HostAttribute, HostChildNode } from './types';
import type { LifecycleCallback } from '@lwc/engine-core';

Expand Down Expand Up @@ -509,6 +509,7 @@ export const renderer = {
insertStylesheet,
assertInstanceOfHTMLElement,
ownerDocument,
registerContextProvider,
registerContextConsumer,
attachInternals,
defineCustomElement: getUpgradableElement,
Expand Down
Loading