diff --git a/packages/@lwc/engine/src/framework/__tests__/error-boundary.spec.ts b/packages/@lwc/engine/src/framework/__tests__/error-boundary.spec.ts index 98b40b6b96..2a0ed66887 100644 --- a/packages/@lwc/engine/src/framework/__tests__/error-boundary.spec.ts +++ b/packages/@lwc/engine/src/framework/__tests__/error-boundary.spec.ts @@ -979,7 +979,7 @@ describe('error boundary component', () => { } } registerDecorators(PreErrorChildContent, { - publicProps: { foo: { config: 1 } }, + publicProps: { foo: { config: 3 } }, }); const baseTmpl = compileTemplate( ` diff --git a/packages/@lwc/engine/src/framework/__tests__/html-element.spec.ts b/packages/@lwc/engine/src/framework/__tests__/html-element.spec.ts index 27f711c395..c2262678d2 100644 --- a/packages/@lwc/engine/src/framework/__tests__/html-element.spec.ts +++ b/packages/@lwc/engine/src/framework/__tests__/html-element.spec.ts @@ -494,10 +494,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { foo: {} }, - }); - - registerDecorators(MyComponent, { + publicProps: { foo: { config: 3 } }, track: { state: 1 }, }); @@ -550,7 +547,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { role: {} }, + publicProps: { role: { config: 3 } }, }); const element = createElement('prop-getter-aria-role', { is: MyComponent }); document.body.appendChild(element); @@ -588,7 +585,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { lang: {} }, + publicProps: { lang: { config: 3 } }, }); const element = createElement('prop-setter-lang', { is: MyComponent }); @@ -634,7 +631,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { lang: {} }, + publicProps: { lang: { config: 1 } }, }); const element = createElement('prop-getter-lang-imperative', { is: MyComponent }); @@ -708,7 +705,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { hidden: {} }, + publicProps: { hidden: { config: 3 } }, }); const element = createElement('prop-setter-hidden', { is: MyComponent }); @@ -753,7 +750,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { hidden: {} }, + publicProps: { hidden: { config: 1 } }, }); const element = createElement('prop-getter-hidden-imperative', { is: MyComponent }); @@ -831,7 +828,7 @@ describe('html-element', () => { } registerDecorators(MyComponent, { - publicProps: { dir: {} }, + publicProps: { dir: { config: 3 } }, }); const element = createElement('prop-setter-dir', { is: MyComponent }); @@ -877,7 +874,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { dir: {} }, + publicProps: { dir: { config: 1 } }, }); const element = createElement('prop-getter-dir-imperative', { is: MyComponent }); @@ -953,7 +950,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { id: {} }, + publicProps: { id: { config: 3 } }, }); const element = createElement('prop-setter-id', { is: MyComponent }); @@ -999,7 +996,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { id: {} }, + publicProps: { id: { config: 1 } }, }); const element = createElement('prop-getter-id-imperative', { is: MyComponent }); @@ -1077,7 +1074,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { accessKey: {} }, + publicProps: { accessKey: { config: 3 } }, }); const element = createElement('prop-setter-accessKey', { is: MyComponent }); @@ -1124,7 +1121,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { accessKey: {} }, + publicProps: { accessKey: { config: 1 } }, }); const element = createElement('prop-getter-accessKey-imperative', { is: MyComponent, @@ -1201,7 +1198,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { title: {} }, + publicProps: { title: { config: 3 } }, }); const element = createElement('prop-setter-title', { is: MyComponent }); (element.title = {}), expect(count).toBe(1); @@ -1244,7 +1241,7 @@ describe('html-element', () => { } } registerDecorators(MyComponent, { - publicProps: { title: {} }, + publicProps: { title: { config: 1 } }, }); const element = createElement('prop-getter-title-imperative', { is: MyComponent }); diff --git a/packages/@lwc/engine/src/framework/base-lightning-element.ts b/packages/@lwc/engine/src/framework/base-lightning-element.ts index e7758a088f..7d782ef2f4 100644 --- a/packages/@lwc/engine/src/framework/base-lightning-element.ts +++ b/packages/@lwc/engine/src/framework/base-lightning-element.ts @@ -143,7 +143,7 @@ export interface LightningElementConstructor { export declare var LightningElement: LightningElementConstructor; -export interface LightningElement { +export interface LightningElement extends EventTarget { // DOM - The good parts dispatchEvent(event: Event): boolean; addEventListener( diff --git a/packages/@lwc/engine/src/framework/component.ts b/packages/@lwc/engine/src/framework/component.ts index adc8b17bd7..e15c5133b0 100644 --- a/packages/@lwc/engine/src/framework/component.ts +++ b/packages/@lwc/engine/src/framework/component.ts @@ -13,16 +13,16 @@ import { invokeEventListener, } from './invoker'; import { isArray, isFunction, isUndefined, StringToLowerCase, isFalse } from '../shared/language'; -import { invokeServiceHook, Services } from './services'; import { VM, getComponentVM, UninitializedVM, scheduleRehydration } from './vm'; import { VNodes } from '../3rdparty/snabbdom/types'; import { tagNameGetter } from '../env/element'; import { Template } from './template'; import { ReactiveObserver } from '../libs/mutation-tracker'; -import { LightningElementConstructor } from './base-lightning-element'; +import { LightningElementConstructor, LightningElement } from './base-lightning-element'; +import { installWireAdapters } from './wiring'; export type ErrorCallback = (error: any, stack: string) => void; -export interface ComponentInterface { +export interface ComponentInterface extends LightningElement { // TODO: #1291 - complete the entire interface used by the engine setAttribute(attrName: string, value: any): void; } @@ -81,15 +81,9 @@ export function linkComponent(vm: VM) { if (process.env.NODE_ENV !== 'production') { assert.isTrue(vm && 'cmpRoot' in vm, `${vm} is not a vm.`); } - // wiring service - const { - def: { wire }, - } = vm; - if (wire) { - const { wiring } = Services; - if (wiring) { - invokeServiceHook(vm, wiring); - } + // initializing the wire decorator per instance only when really needed + if (vm.def.wire.length > 0) { + installWireAdapters(vm); } } diff --git a/packages/@lwc/engine/src/framework/context-provider.ts b/packages/@lwc/engine/src/framework/context-provider.ts new file mode 100644 index 0000000000..d133a799fb --- /dev/null +++ b/packages/@lwc/engine/src/framework/context-provider.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2018, 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, ArrayPush, forEach } from '../shared/language'; +import { guid } from './utils'; +import { WireAdapterConstructor, ContextValue, getAdapterToken, setAdapterToken } from './wiring'; + +type WireContextDisconnectCallback = () => void; +type WireContextInternalProtocolCallback = ( + newContext: ContextValue, + disconnectCallback: WireContextDisconnectCallback +) => void; +interface ContextConsumer { + provide(newContext: ContextValue): void; +} + +interface WireContextEvent extends CustomEvent { + detail: WireContextInternalProtocolCallback; +} + +// this is lwc internal implementation +export function createContextProvider(adapter: WireAdapterConstructor) { + let adapterContextToken = getAdapterToken(adapter); + if (!isUndefined(adapterContextToken)) { + throw new Error(`Adapter already have a context provider.`); + } + adapterContextToken = guid(); + setAdapterToken(adapter, adapterContextToken); + return (elm: EventTarget) => { + const connectQueue = []; + const disconnectQueue = []; + elm.addEventListener(adapterContextToken as string, (evt: WireContextEvent) => { + const { detail } = evt; + const consumer: ContextConsumer = { + provide(newContext) { + detail(newContext, disconnectCallback); + }, + }; + const disconnectCallback = () => { + forEach.call(disconnectQueue, callback => callback(consumer)); + }; + forEach.call(connectQueue, callback => callback(consumer)); + }); + return { + onConsumerConnected(callback: (consumer: ContextConsumer) => void) { + ArrayPush.call(connectQueue, callback); + }, + onConsumerDisconnected(callback: (consumer: ContextConsumer) => void) { + ArrayPush.call(disconnectQueue, callback); + }, + }; + }; +} diff --git a/packages/@lwc/engine/src/framework/decorators/__tests__/wire.spec.ts b/packages/@lwc/engine/src/framework/decorators/__tests__/wire.spec.ts index 9e6703508b..f4c0d45299 100644 --- a/packages/@lwc/engine/src/framework/decorators/__tests__/wire.spec.ts +++ b/packages/@lwc/engine/src/framework/decorators/__tests__/wire.spec.ts @@ -85,7 +85,7 @@ describe('wire.ts', () => { }); }); - it('should make properties of a wired object property reactive', () => { + it('should make wired properties as readonly', () => { let counter = 0; class MyComponent extends LightningElement { injectFooDotX(x) { @@ -108,10 +108,11 @@ describe('wire.ts', () => { const elm = createElement('x-foo', { is: MyComponent }); document.body.appendChild(elm); - elm.injectFooDotX(2); - + expect(() => { + elm.injectFooDotX(2); + }).toThrowError(); return Promise.resolve().then(() => { - expect(counter).toBe(2); + expect(counter).toBe(1); }); }); diff --git a/packages/@lwc/engine/src/framework/decorators/api.ts b/packages/@lwc/engine/src/framework/decorators/api.ts index ccf424a02a..1e84a8d8c8 100644 --- a/packages/@lwc/engine/src/framework/decorators/api.ts +++ b/packages/@lwc/engine/src/framework/decorators/api.ts @@ -6,63 +6,27 @@ */ import assert from '../../shared/assert'; import { isRendering, vmBeingRendered, isBeingConstructed } from '../invoker'; -import { isObject, toString, isFalse } from '../../shared/language'; +import { toString, isFalse } from '../../shared/language'; import { valueObserved, valueMutated } from '../../libs/mutation-tracker'; -import { ComponentInterface, ComponentConstructor } from '../component'; +import { ComponentInterface } from '../component'; import { getComponentVM } from '../vm'; -import { isUndefined, isFunction } from '../../shared/language'; -import { getDecoratorsRegisteredMeta } from './register'; +import { isFunction } from '../../shared/language'; /** * @api decorator to mark public fields and public methods in * LWC Components. This function implements the internals of this * decorator. */ -export default function api( - target: ComponentConstructor, - propName: PropertyKey, - descriptor: PropertyDescriptor | undefined -): PropertyDescriptor { +// TODO: how to make api a decoratorFunction type as well? +export default function api() { if (process.env.NODE_ENV !== 'production') { - if (arguments.length !== 3) { + if (arguments.length !== 0) { assert.fail(`@api decorator can only be used as a decorator function.`); } } - if (process.env.NODE_ENV !== 'production') { - assert.invariant( - !descriptor || (isFunction(descriptor.get) || isFunction(descriptor.set)), - `Invalid property ${toString( - propName - )} definition in ${target}, it cannot be a prototype definition if it is a public property. Instead use the constructor to define it.` - ); - if (isObject(descriptor) && isFunction(descriptor.set)) { - assert.isTrue( - isObject(descriptor) && isFunction(descriptor.get), - `Missing getter for property ${toString( - propName - )} decorated with @api in ${target}. You cannot have a setter without the corresponding getter.` - ); - } - } - const meta = getDecoratorsRegisteredMeta(target); - // initializing getters and setters for each public prop on the target prototype - if (isObject(descriptor) && (isFunction(descriptor.get) || isFunction(descriptor.set))) { - // if it is configured as an accessor it must have a descriptor - // @ts-ignore it must always be set before calling this method - meta.props[propName].config = isFunction(descriptor.set) ? 3 : 1; - return createPublicAccessorDescriptor(target, propName, descriptor); - } else { - // @ts-ignore it must always be set before calling this method - meta.props[propName].config = 0; - return createPublicPropertyDescriptor(target, propName, descriptor); - } } -function createPublicPropertyDescriptor( - proto: ComponentConstructor, - key: PropertyKey, - descriptor: PropertyDescriptor | undefined -): PropertyDescriptor { +export function createPublicPropertyDescriptor(key: string): PropertyDescriptor { return { get(this: ComponentInterface): any { const vm = getComponentVM(this); @@ -103,26 +67,17 @@ function createPublicPropertyDescriptor( valueMutated(this, key); } }, - enumerable: isUndefined(descriptor) ? true : descriptor.enumerable, + enumerable: true, + configurable: true, }; } -function createPublicAccessorDescriptor( - Ctor: ComponentConstructor, +export function createPublicAccessorDescriptor( key: PropertyKey, descriptor: PropertyDescriptor ): PropertyDescriptor { - const { get, set, enumerable } = descriptor; + const { get, set, enumerable, configurable } = descriptor; if (!isFunction(get)) { - if (process.env.NODE_ENV !== 'production') { - assert.fail( - `Invalid attempt to create public property descriptor ${toString( - key - )} in ${Ctor}. It is missing the getter declaration with @api get ${toString( - key - )}() {} syntax.` - ); - } throw new TypeError(); } return { @@ -155,5 +110,6 @@ function createPublicAccessorDescriptor( } }, enumerable, + configurable, }; } diff --git a/packages/@lwc/engine/src/framework/decorators/decorate.ts b/packages/@lwc/engine/src/framework/decorators/decorate.ts deleted file mode 100644 index 176d56e1c1..0000000000 --- a/packages/@lwc/engine/src/framework/decorators/decorate.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2018, 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 { - getOwnPropertyNames, - getOwnPropertyDescriptor, - defineProperty, - isFunction, - isUndefined, -} from '../../shared/language'; - -export type DecoratorFunction = ( - Ctor: any, - key: PropertyKey, - descriptor: PropertyDescriptor | undefined -) => PropertyDescriptor; -export type DecoratorMap = Record; - -/** - * EXPERIMENTAL: This function allows for the registration of "services" in - * LWC by exposing hooks into the component life-cycle. This API is subject - * to change or being removed. - */ -export default function decorate(Ctor: any, decorators: DecoratorMap): any { - // intentionally comparing decorators with null and undefined - if (!isFunction(Ctor) || decorators == null) { - throw new TypeError(); - } - const props = getOwnPropertyNames(decorators); - // intentionally allowing decoration of classes only for now - const target = Ctor.prototype; - for (let i = 0, len = props.length; i < len; i += 1) { - const propName = props[i]; - const decorator = decorators[propName]; - if (!isFunction(decorator)) { - throw new TypeError(); - } - const originalDescriptor = getOwnPropertyDescriptor(target, propName); - const descriptor = decorator(Ctor, propName, originalDescriptor); - if (!isUndefined(descriptor)) { - defineProperty(target, propName, descriptor); - } - } - return Ctor; // chaining -} diff --git a/packages/@lwc/engine/src/framework/decorators/register.ts b/packages/@lwc/engine/src/framework/decorators/register.ts index bc821db3da..3cfa170d57 100644 --- a/packages/@lwc/engine/src/framework/decorators/register.ts +++ b/packages/@lwc/engine/src/framework/decorators/register.ts @@ -6,167 +6,258 @@ */ import assert from '../../shared/assert'; import { - getOwnPropertyNames, - isFunction, isUndefined, - create, - assign, + forEach, + defineProperty, + getOwnPropertyDescriptor, + isFunction, + ArrayPush, + toString, + isFalse, } from '../../shared/language'; import { ComponentConstructor } from '../component'; -import wireDecorator from './wire'; -import trackDecorator from './track'; -import apiDecorator from './api'; -import { EmptyObject } from '../utils'; -import { getAttrNameFromPropName } from '../attributes'; -import decorate, { DecoratorMap } from './decorate'; +import { internalWireFieldDecorator } from './wire'; +import { internalTrackDecorator } from './track'; +import { createPublicPropertyDescriptor, createPublicAccessorDescriptor } from './api'; +import { createObservedFieldPropertyDescriptor } from '../observed-fields'; +import { + WireAdapterConstructor, + storeWiredMethodMeta, + storeWiredFieldMeta, + ConfigCallback, +} from '../wiring'; -export interface PropDef { - config: number; +// data produced by compiler +type WireCompilerMeta = Record; +type TrackCompilerMeta = Record; +type MethodCompilerMeta = string[]; +type PropCompilerMeta = Record; +enum PropType { + Field = 0, + Set = 1, + Get = 2, + GetSet = 3, +} +interface PropCompilerDef { + config: PropType; // 0 m type: string; // TODO: #1301 - make this an enum - attr: string; } -export interface WireDef { +interface WireCompilerDef { method?: number; - [key: string]: any; + adapter: WireAdapterConstructor; + configCallback: ConfigCallback; } -export interface PropsDef { - [key: string]: PropDef; +interface RegisterDecoratorMeta { + readonly publicMethods?: MethodCompilerMeta; + readonly publicProps?: PropCompilerMeta; + readonly track?: TrackCompilerMeta; + readonly wire?: WireCompilerMeta; + readonly fields?: string[]; } -export interface TrackDef { - [key: string]: 1; + +function validateObservedField(Ctor: ComponentConstructor, fieldName: string) { + if (process.env.NODE_ENV !== 'production') { + const descriptor = getOwnPropertyDescriptor(Ctor.prototype, fieldName); + if (!isUndefined(descriptor)) { + assert.fail(`Compiler Error: Invalid field ${fieldName} declaration.`); + } + } } -type PublicMethod = (...args: any[]) => any; -export interface MethodDef { - [key: string]: PublicMethod; + +function validateFieldDecoratedWithTrack(Ctor: ComponentConstructor, fieldName: string) { + if (process.env.NODE_ENV !== 'production') { + const descriptor = getOwnPropertyDescriptor(Ctor.prototype, fieldName); + if (!isUndefined(descriptor)) { + assert.fail(`Compiler Error: Invalid @track ${fieldName} declaration.`); + } + } } -export interface WireHash { - [key: string]: WireDef; + +function validateFieldDecoratedWithWire(Ctor: ComponentConstructor, fieldName: string) { + if (process.env.NODE_ENV !== 'production') { + const descriptor = getOwnPropertyDescriptor(Ctor.prototype, fieldName); + if (!isUndefined(descriptor)) { + assert.fail(`Compiler Error: Invalid @wire(...) ${fieldName} field declaration.`); + } + } } -export interface RegisterDecoratorMeta { - readonly publicMethods?: string[]; - readonly publicProps?: PropsDef; - readonly track?: TrackDef; - readonly wire?: WireHash; - readonly fields?: string[]; +function validateMethodDecoratedWithWire(Ctor: ComponentConstructor, methodName: string) { + if (process.env.NODE_ENV !== 'production') { + const descriptor = getOwnPropertyDescriptor(Ctor.prototype, methodName); + if ( + isUndefined(descriptor) || + !isFunction(descriptor.value) || + isFalse(descriptor.writable) + ) { + assert.fail(`Compiler Error: Invalid @wire(...) ${methodName} method declaration.`); + } + } } -export interface DecoratorMeta { - wire: WireHash | undefined; - track: TrackDef; - props: PropsDef; - methods: MethodDef; - fields?: string[]; +function validateFieldDecoratedWithApi(Ctor: ComponentConstructor, fieldName: string) { + if (process.env.NODE_ENV !== 'production') { + const descriptor = getOwnPropertyDescriptor(Ctor.prototype, fieldName); + if (!isUndefined(descriptor)) { + assert.fail(`Compiler Error: Invalid @api ${fieldName} field declaration.`); + } + } } -const signedDecoratorToMetaMap: Map = new Map(); +function validateAccessorDecoratedWithApi(Ctor: ComponentConstructor, fieldName: string) { + if (process.env.NODE_ENV !== 'production') { + const descriptor = getOwnPropertyDescriptor(Ctor.prototype, fieldName); + if (isUndefined(descriptor)) { + assert.fail(`Compiler Error: Invalid @api get ${fieldName} accessor declaration.`); + } else if (isFunction(descriptor.set)) { + assert.isTrue( + isFunction(descriptor.get), + `Compiler Error: Missing getter for property ${toString( + fieldName + )} decorated with @api in ${Ctor}. You cannot have a setter without the corresponding getter.` + ); + } else if (!isFunction(descriptor.get)) { + assert.fail(`Compiler Error: Missing @api get ${fieldName} accessor declaration.`); + } + } +} + +function validateMethodDecoratedWithApi(Ctor: ComponentConstructor, methodName: string) { + if (process.env.NODE_ENV !== 'production') { + const descriptor = getOwnPropertyDescriptor(Ctor.prototype, methodName); + if ( + isUndefined(descriptor) || + !isFunction(descriptor.value) || + isFalse(descriptor.writable) + ) { + assert.fail(`Compiler Error: Invalid @api ${methodName} method declaration.`); + } + } +} /** * INTERNAL: This function can only be invoked by compiled code. The compiler - * will prevent this function from being imported by userland code. + * will prevent this function from being imported by user-land code. */ export function registerDecorators( Ctor: ComponentConstructor, meta: RegisterDecoratorMeta ): ComponentConstructor { - const decoratorMap: DecoratorMap = create(null); - const props = getPublicPropertiesHash(Ctor, meta.publicProps); - const methods = getPublicMethodsHash(Ctor, meta.publicMethods); - const wire = getWireHash(Ctor, meta.wire); - const track = getTrackHash(Ctor, meta.track); - const fields = meta.fields; - signedDecoratorToMetaMap.set(Ctor, { - props, - methods, - wire, - track, - fields, - }); - for (const propName in props) { - decoratorMap[propName] = apiDecorator; + const proto = Ctor.prototype; + const { publicProps, publicMethods, wire, track, fields } = meta; + const apiMethods = []; + const apiFields = []; + const wiredMethods = []; + const wiredFields = []; + if (!isUndefined(publicProps)) { + for (const fieldName in publicProps) { + const propConfig = publicProps[fieldName]; + let descriptor: PropertyDescriptor | undefined; + if (propConfig.config > 0) { + // accessor declaration + if (process.env.NODE_ENV !== 'production') { + validateAccessorDecoratedWithApi(Ctor, fieldName); + } + descriptor = getOwnPropertyDescriptor(proto, fieldName); + descriptor = createPublicAccessorDescriptor( + fieldName, + descriptor as PropertyDescriptor + ); + } else { + // field declaration + if (process.env.NODE_ENV !== 'production') { + validateFieldDecoratedWithApi(Ctor, fieldName); + } + descriptor = createPublicPropertyDescriptor(fieldName); + } + ArrayPush.call(apiFields, fieldName); + defineProperty(proto, fieldName, descriptor); + } } - if (wire) { - for (const propName in wire) { - const wireDef: WireDef = wire[propName]; - if (wireDef.method) { - // for decorated methods we need to do nothing - continue; + if (!isUndefined(publicMethods)) { + forEach.call(publicMethods, methodName => { + if (process.env.NODE_ENV !== 'production') { + validateMethodDecoratedWithApi(Ctor, methodName); } - decoratorMap[propName] = wireDecorator(wireDef.adapter, wireDef.params); + ArrayPush.call(apiMethods, methodName); + }); + } + if (!isUndefined(wire)) { + for (const fieldOrMethodName in wire) { + const { adapter, method } = wire[fieldOrMethodName]; + const { configCallback } = wire[fieldOrMethodName]; + if (method === 1) { + if (process.env.NODE_ENV !== 'production') { + validateMethodDecoratedWithWire(Ctor, fieldOrMethodName); + } + ArrayPush.call(wiredMethods, fieldOrMethodName); + storeWiredMethodMeta( + Ctor, + fieldOrMethodName, + adapter, + proto[fieldOrMethodName] as (data: any) => void, + configCallback + ); + } else { + if (process.env.NODE_ENV !== 'production') { + validateFieldDecoratedWithWire(Ctor, fieldOrMethodName); + } + storeWiredFieldMeta(Ctor, fieldOrMethodName, adapter, configCallback); + ArrayPush.call(wiredFields, fieldOrMethodName); + defineProperty( + proto, + fieldOrMethodName, + internalWireFieldDecorator(fieldOrMethodName) + ); + } + } + } + if (!isUndefined(track)) { + for (const fieldName in track) { + if (process.env.NODE_ENV !== 'production') { + validateFieldDecoratedWithTrack(Ctor, fieldName); + } + defineProperty(proto, fieldName, internalTrackDecorator(fieldName)); } } - if (track) { - for (const propName in track) { - decoratorMap[propName] = trackDecorator; + if (!isUndefined(fields)) { + for (const fieldName in fields) { + if (process.env.NODE_ENV !== 'production') { + validateObservedField(Ctor, fieldName); + } + defineProperty(proto, fieldName, createObservedFieldPropertyDescriptor(fieldName)); } } - decorate(Ctor, decoratorMap); + setDecoratorsMeta(Ctor, { + apiMethods, + apiFields, + wiredMethods, + wiredFields, + }); return Ctor; } -export function getDecoratorsRegisteredMeta(Ctor: ComponentConstructor): DecoratorMeta | undefined { - return signedDecoratorToMetaMap.get(Ctor); -} - -function getTrackHash(target: ComponentConstructor, track: TrackDef | undefined): TrackDef { - if (isUndefined(track) || getOwnPropertyNames(track).length === 0) { - return EmptyObject; - } +const signedDecoratorToMetaMap: Map = new Map(); - // TODO: #1302 - check that anything in `track` is correctly defined in the prototype - return assign(create(null), track); +interface DecoratorMeta { + readonly apiMethods: string[]; + readonly apiFields: string[]; + readonly wiredMethods: string[]; + readonly wiredFields: string[]; } -function getWireHash( - target: ComponentConstructor, - wire: WireHash | undefined -): WireHash | undefined { - if (isUndefined(wire) || getOwnPropertyNames(wire).length === 0) { - return; - } - - // TODO: #1302 - check that anything in `wire` is correctly defined in the prototype - return assign(create(null), wire); +function setDecoratorsMeta(Ctor: ComponentConstructor, meta: DecoratorMeta) { + signedDecoratorToMetaMap.set(Ctor, meta); } -function getPublicPropertiesHash( - target: ComponentConstructor, - props: PropsDef | undefined -): PropsDef { - if (isUndefined(props) || getOwnPropertyNames(props).length === 0) { - return EmptyObject; - } - return getOwnPropertyNames(props).reduce((propsHash: PropsDef, propName: string): PropsDef => { - const attr = getAttrNameFromPropName(propName); - propsHash[propName] = assign( - { - config: 0, - type: 'any', - attr, - }, - props[propName] - ); - return propsHash; - }, create(null)); -} +const defaultMeta: DecoratorMeta = { + apiMethods: [], + apiFields: [], + wiredMethods: [], + wiredFields: [], +}; -function getPublicMethodsHash( - target: ComponentConstructor, - publicMethods: string[] | undefined -): MethodDef { - if (isUndefined(publicMethods) || publicMethods.length === 0) { - return EmptyObject; - } - return publicMethods.reduce((methodsHash: MethodDef, methodName: string): MethodDef => { - if (process.env.NODE_ENV !== 'production') { - assert.isTrue( - isFunction(target.prototype[methodName]), - `Component "${target.name}" should have a method \`${methodName}\` instead of ${ - target.prototype[methodName] - }.` - ); - } - methodsHash[methodName] = target.prototype[methodName]; - return methodsHash; - }, create(null)); +export function getDecoratorsMeta(Ctor: ComponentConstructor): DecoratorMeta { + const meta = signedDecoratorToMetaMap.get(Ctor); + return isUndefined(meta) ? defaultMeta : meta; } diff --git a/packages/@lwc/engine/src/framework/decorators/track.ts b/packages/@lwc/engine/src/framework/decorators/track.ts index 8da7f74759..65e7721053 100644 --- a/packages/@lwc/engine/src/framework/decorators/track.ts +++ b/packages/@lwc/engine/src/framework/decorators/track.ts @@ -5,24 +5,20 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import assert from '../../shared/assert'; -import { isUndefined, isFalse } from '../../shared/language'; +import { isFalse } from '../../shared/language'; import { isRendering, vmBeingRendered } from '../invoker'; import { valueObserved, valueMutated } from '../../libs/mutation-tracker'; import { getComponentVM } from '../vm'; import { reactiveMembrane } from '../membrane'; -import { ComponentConstructor, ComponentInterface } from '../component'; +import { ComponentInterface } from '../component'; /** - * @track decorator to mark fields as reactive in - * LWC Components. This function implements the internals of this - * decorator. + * @track decorator function to mark field value as reactive in + * LWC Components. This function can also be invoked directly + * with any value to obtain the trackable version of the value. */ -export default function track( - target: ComponentConstructor, - prop: PropertyKey, - descriptor: PropertyDescriptor | undefined -): PropertyDescriptor; -export default function track(target: any, prop?, descriptor?): any { +// TODO: how to make track a decoratorFunction type as well? +export default function track(target?: any): any { if (arguments.length === 1) { return reactiveMembrane.getProxy(target); } @@ -32,34 +28,10 @@ export default function track(target: any, prop?, descriptor?): any { `@track decorator can only be used with one argument to return a trackable object, or as a decorator function.` ); } - if (!isUndefined(descriptor)) { - const { get, set, configurable, writable } = descriptor; - assert.isTrue( - !get && !set, - `Compiler Error: A @track decorator can only be applied to a public field.` - ); - assert.isTrue( - configurable !== false, - `Compiler Error: A @track decorator can only be applied to a configurable property.` - ); - assert.isTrue( - writable !== false, - `Compiler Error: A @track decorator can only be applied to a writable property.` - ); - } } - return createTrackedPropertyDescriptor( - target, - prop, - isUndefined(descriptor) ? true : descriptor.enumerable === true - ); } -export function createTrackedPropertyDescriptor( - Ctor: any, - key: PropertyKey, - enumerable: boolean -): PropertyDescriptor { +export function internalTrackDecorator(key: string): PropertyDescriptor { return { get(this: ComponentInterface): any { const vm = getComponentVM(this); @@ -67,7 +39,7 @@ export function createTrackedPropertyDescriptor( assert.isTrue(vm && 'cmpRoot' in vm, `${vm} is not a vm.`); } valueObserved(this, key); - return vm.cmpTrack[key]; + return vm.cmpFields[key]; }, set(this: ComponentInterface, newValue: any) { const vm = getComponentVM(this); @@ -81,15 +53,15 @@ export function createTrackedPropertyDescriptor( ); } const reactiveOrAnyValue = reactiveMembrane.getProxy(newValue); - if (reactiveOrAnyValue !== vm.cmpTrack[key]) { - vm.cmpTrack[key] = reactiveOrAnyValue; + if (reactiveOrAnyValue !== vm.cmpFields[key]) { + vm.cmpFields[key] = reactiveOrAnyValue; if (isFalse(vm.isDirty)) { // perf optimization to skip this step if the track property is on a component that is already dirty valueMutated(this, key); } } }, - enumerable, + enumerable: true, configurable: true, }; } diff --git a/packages/@lwc/engine/src/framework/decorators/wire.ts b/packages/@lwc/engine/src/framework/decorators/wire.ts index 39c95450e7..7a281b29e6 100644 --- a/packages/@lwc/engine/src/framework/decorators/wire.ts +++ b/packages/@lwc/engine/src/framework/decorators/wire.ts @@ -4,54 +4,50 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { createTrackedPropertyDescriptor } from './track'; import assert from '../../shared/assert'; -import { isObject, isUndefined } from '../../shared/language'; -import { DecoratorFunction } from './decorate'; -import { ComponentConstructor } from '../component'; - -function wireDecorator( - target: ComponentConstructor, - prop: PropertyKey, - descriptor: PropertyDescriptor | undefined -): PropertyDescriptor | any { - if (process.env.NODE_ENV !== 'production') { - if (!isUndefined(descriptor)) { - const { get, set, configurable, writable } = descriptor; - assert.isTrue( - !get && !set, - `Compiler Error: A @wire decorator can only be applied to a public field.` - ); - assert.isTrue( - configurable !== false, - `Compiler Error: A @wire decorator can only be applied to a configurable property.` - ); - assert.isTrue( - writable !== false, - `Compiler Error: A @wire decorator can only be applied to a writable property.` - ); - } - } - return createTrackedPropertyDescriptor( - target, - prop, - isObject(descriptor) ? descriptor.enumerable === true : true - ); -} +import { ComponentInterface } from '../component'; +import { valueObserved } from '../../libs/mutation-tracker'; +import { getComponentVM } from '../vm'; +import { WireAdapterConstructor } from '../wiring'; /** * @wire decorator to wire fields and methods to a wire adapter in * LWC Components. This function implements the internals of this * decorator. */ -export default function wire(_adapter: any, _config: any): DecoratorFunction { - const len = arguments.length; - if (len > 0 && len < 3) { - return wireDecorator; - } else { - if (process.env.NODE_ENV !== 'production') { - assert.fail('@wire(adapter, config?) may only be used as a decorator.'); - } - throw new TypeError(); +export default function wire( + _adapter: WireAdapterConstructor, + _config?: Record +): PropertyDecorator | MethodDecorator { + if (process.env.NODE_ENV !== 'production') { + assert.fail('@wire(adapter, config?) may only be used as a decorator.'); } + throw new TypeError(); +} + +export function internalWireFieldDecorator(key: string): PropertyDescriptor { + return { + get(this: ComponentInterface): any { + const vm = getComponentVM(this); + if (process.env.NODE_ENV !== 'production') { + assert.isTrue(vm && 'cmpRoot' in vm, `${vm} is not a vm.`); + } + valueObserved(this, key); + return vm.cmpFields[key]; + }, + set(this: ComponentInterface, value: any) { + const vm = getComponentVM(this); + if (process.env.NODE_ENV !== 'production') { + assert.isTrue(vm && 'cmpRoot' in vm, `${vm} is not a vm.`); + } + /** + * intentionally ignoring the reactivity here since this is just + * letting the author to do the wrong thing, but it will keep our + * system to be backward compatible. + */ + vm.cmpFields[key] = value; + }, + enumerable: true, + configurable: true, + }; } diff --git a/packages/@lwc/engine/src/framework/def.ts b/packages/@lwc/engine/src/framework/def.ts index 5b94d4cad9..90d217cb9b 100644 --- a/packages/@lwc/engine/src/framework/def.ts +++ b/packages/@lwc/engine/src/framework/def.ts @@ -15,20 +15,16 @@ import assert from '../shared/assert'; import { - assign, freeze, - create, getOwnPropertyNames, getPrototypeOf, isNull, setPrototypeOf, - ArrayReduce, isUndefined, isFunction, - defineProperties, + ArrayConcat, } from '../shared/language'; import { getInternalField } from '../shared/fields'; -import { getAttrNameFromPropName } from './attributes'; import { resolveCircularModuleDependency, isCircularModuleDependency, @@ -40,19 +36,20 @@ import { ComponentMeta, getComponentRegisteredMeta, } from './component'; -import { createObservedFieldsDescriptorMap } from './observed-fields'; import { Template } from './template'; -export interface ComponentDef extends DecoratorMeta { +export interface ComponentDef { name: string; + props: string[]; + wire: string[]; template: Template; ctor: ComponentConstructor; bridge: HTMLElementConstructor; connectedCallback?: () => void; disconnectedCallback?: () => void; renderedCallback?: () => void; - render: () => Template; errorCallback?: ErrorCallback; + render: () => Template; } const CtorToDefMap: WeakMap = new WeakMap(); @@ -101,19 +98,8 @@ function createComponentDef( const { name } = meta; let { template } = meta; - const decoratorsMeta = getDecoratorsRegisteredMeta(Ctor); - let props: PropsDef = {}; - let methods: MethodDef = {}; - let wire: WireHash | undefined; - let track: TrackDef = {}; - let fields: string[] | undefined; - if (!isUndefined(decoratorsMeta)) { - props = decoratorsMeta.props; - methods = decoratorsMeta.methods; - wire = decoratorsMeta.wire; - track = decoratorsMeta.track; - fields = decoratorsMeta.fields; - } + const decoratorsMeta = getDecoratorsMeta(Ctor); + const { apiFields, apiMethods, wiredFields, wiredMethods } = decoratorsMeta; const proto = Ctor.prototype; let { @@ -127,43 +113,23 @@ function createComponentDef( const superDef: ComponentDef | null = (superProto as any) !== BaseLightningElement ? getComponentDef(superProto, subclassComponentName) - : null; + : lightingElementDef; const SuperBridge = isNull(superDef) ? BaseBridgeElement : superDef.bridge; - const bridge = HTMLBridgeElementFactory( - SuperBridge, - getOwnPropertyNames(props), - getOwnPropertyNames(methods) - ); - if (!isNull(superDef)) { - props = assign(create(null), superDef.props, props); - methods = assign(create(null), superDef.methods, methods); - wire = superDef.wire || wire ? assign(create(null), superDef.wire, wire) : undefined; - track = assign(create(null), superDef.track, track); - connectedCallback = connectedCallback || superDef.connectedCallback; - disconnectedCallback = disconnectedCallback || superDef.disconnectedCallback; - renderedCallback = renderedCallback || superDef.renderedCallback; - errorCallback = errorCallback || superDef.errorCallback; - render = render || superDef.render; - template = template || superDef.template; - } - props = assign(create(null), HTML_PROPS, props); - - if (!isUndefined(fields)) { - defineProperties(proto, createObservedFieldsDescriptorMap(fields)); - } - - if (isUndefined(template)) { - // default template - template = defaultEmptyTemplate; - } + const bridge = HTMLBridgeElementFactory(SuperBridge, apiFields, apiMethods); + const props = ArrayConcat.call(superDef.props, apiFields); + const wire = ArrayConcat.call(superDef.wire, wiredFields, wiredMethods); + connectedCallback = connectedCallback || superDef.connectedCallback; + disconnectedCallback = disconnectedCallback || superDef.disconnectedCallback; + renderedCallback = renderedCallback || superDef.renderedCallback; + errorCallback = errorCallback || superDef.errorCallback; + render = render || superDef.render; + template = template || superDef.template; const def: ComponentDef = { ctor: Ctor, name, wire, - track, props, - methods, bridge, template, connectedCallback, @@ -277,29 +243,15 @@ import { HTMLBridgeElementFactory, HTMLElementConstructor, } from './base-bridge-element'; -import { - getDecoratorsRegisteredMeta, - DecoratorMeta, - PropsDef, - WireHash, - MethodDef, - TrackDef, -} from './decorators/register'; +import { getDecoratorsMeta } from './decorators/register'; import { defaultEmptyTemplate } from './secure-template'; -// Typescript is inferring the wrong function type for this particular -// overloaded method: https://github.com/Microsoft/TypeScript/issues/27972 -// @ts-ignore type-mismatch -const HTML_PROPS: PropsDef = ArrayReduce.call( - getOwnPropertyNames(HTMLElementOriginalDescriptors), - (props: PropsDef, propName: string): PropsDef => { - const attrName = getAttrNameFromPropName(propName); - props[propName] = { - config: 3, - type: 'any', - attr: attrName, - }; - return props; - }, - create(null) -); +const lightingElementDef: ComponentDef = { + ctor: BaseLightningElement, + name: BaseLightningElement.name, + props: getOwnPropertyNames(HTMLElementOriginalDescriptors), + wire: [], + bridge: BaseBridgeElement, + template: defaultEmptyTemplate, + render: BaseLightningElement.prototype.render, +}; diff --git a/packages/@lwc/engine/src/framework/main.ts b/packages/@lwc/engine/src/framework/main.ts index 088894a70c..8e0fc5c7d0 100644 --- a/packages/@lwc/engine/src/framework/main.ts +++ b/packages/@lwc/engine/src/framework/main.ts @@ -22,7 +22,7 @@ export { isNodeFromTemplate } from './vm'; export { default as api } from './decorators/api'; export { default as track } from './decorators/track'; -export { default as readonly } from './decorators/readonly'; export { default as wire } from './decorators/wire'; -export { default as decorate } from './decorators/decorate'; + +export { readonly } from './readonly'; export { buildCustomElementConstructor } from './wc'; diff --git a/packages/@lwc/engine/src/framework/observed-fields.ts b/packages/@lwc/engine/src/framework/observed-fields.ts index 4ca7b4e9b4..2acd23b721 100644 --- a/packages/@lwc/engine/src/framework/observed-fields.ts +++ b/packages/@lwc/engine/src/framework/observed-fields.ts @@ -9,21 +9,9 @@ import { getComponentVM } from './vm'; import assert from '../shared/assert'; import { valueMutated, valueObserved } from '../libs/mutation-tracker'; import { isRendering, vmBeingRendered } from './invoker'; -import { isFalse, ArrayReduce } from '../shared/language'; +import { isFalse } from '../shared/language'; -export function createObservedFieldsDescriptorMap(fields: PropertyKey[]): PropertyDescriptorMap { - return ArrayReduce.call( - fields, - (acc: PropertyDescriptorMap, field) => { - acc[field] = createObservedFieldPropertyDescriptor(field); - - return acc; - }, - {} - ) as PropertyDescriptorMap; -} - -function createObservedFieldPropertyDescriptor(key: PropertyKey): PropertyDescriptor { +export function createObservedFieldPropertyDescriptor(key: string): PropertyDescriptor { return { get(this: ComponentInterface): any { const vm = getComponentVM(this); @@ -31,7 +19,7 @@ function createObservedFieldPropertyDescriptor(key: PropertyKey): PropertyDescri assert.isTrue(vm && 'cmpRoot' in vm, `${vm} is not a valid vm.`); } valueObserved(this, key); - return vm.cmpTrack[key]; + return vm.cmpFields[key]; }, set(this: ComponentInterface, newValue: any) { const vm = getComponentVM(this); @@ -45,8 +33,8 @@ function createObservedFieldPropertyDescriptor(key: PropertyKey): PropertyDescri ); } - if (newValue !== vm.cmpTrack[key]) { - vm.cmpTrack[key] = newValue; + if (newValue !== vm.cmpFields[key]) { + vm.cmpFields[key] = newValue; if (isFalse(vm.isDirty)) { valueMutated(this, key); } diff --git a/packages/@lwc/engine/src/framework/performance-timing.ts b/packages/@lwc/engine/src/framework/performance-timing.ts index b708931595..82e167c9d0 100644 --- a/packages/@lwc/engine/src/framework/performance-timing.ts +++ b/packages/@lwc/engine/src/framework/performance-timing.ts @@ -11,6 +11,7 @@ import { StringToLowerCase, isUndefined } from '../shared/language'; type MeasurementPhase = | 'constructor' + | 'wire' | 'render' | 'patch' | 'connectedCallback' diff --git a/packages/@lwc/engine/src/framework/decorators/readonly.ts b/packages/@lwc/engine/src/framework/readonly.ts similarity index 84% rename from packages/@lwc/engine/src/framework/decorators/readonly.ts rename to packages/@lwc/engine/src/framework/readonly.ts index 0407ba16be..c1a18fabb4 100644 --- a/packages/@lwc/engine/src/framework/decorators/readonly.ts +++ b/packages/@lwc/engine/src/framework/readonly.ts @@ -4,15 +4,15 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import assert from '../../shared/assert'; -import { reactiveMembrane } from '../membrane'; +import assert from '../shared/assert'; +import { reactiveMembrane } from './membrane'; /** * EXPERIMENTAL: This function allows you to create a reactive readonly * membrane around any object value. This API is subject to change or * being removed. */ -export default function readonly(obj: any): any { +export function readonly(obj: any): any { if (process.env.NODE_ENV !== 'production') { // TODO: #1292 - Remove the readonly decorator if (arguments.length !== 1) { diff --git a/packages/@lwc/engine/src/framework/services.ts b/packages/@lwc/engine/src/framework/services.ts index be538dc5f2..cf6d52b693 100644 --- a/packages/@lwc/engine/src/framework/services.ts +++ b/packages/@lwc/engine/src/framework/services.ts @@ -19,7 +19,6 @@ type ServiceCallback = ( context: Context ) => void; interface ServiceDef { - wiring?: ServiceCallback; locator?: ServiceCallback; connected?: ServiceCallback; disconnected?: ServiceCallback; @@ -27,20 +26,13 @@ interface ServiceDef { } export const Services: { - wiring?: ServiceCallback[]; locator?: ServiceCallback[]; connected?: ServiceCallback[]; disconnected?: ServiceCallback[]; rendered?: ServiceCallback[]; } = create(null); -const hooks: Array = [ - 'wiring', - 'locator', - 'rendered', - 'connected', - 'disconnected', -]; +const hooks: Array = ['locator', 'rendered', 'connected', 'disconnected']; /** * EXPERIMENTAL: This function allows for the registration of "services" diff --git a/packages/@lwc/engine/src/framework/utils.ts b/packages/@lwc/engine/src/framework/utils.ts index 45319c3ed0..bc4e7f073f 100644 --- a/packages/@lwc/engine/src/framework/utils.ts +++ b/packages/@lwc/engine/src/framework/utils.ts @@ -73,3 +73,13 @@ export function resolveCircularModuleDependency(fn: CircularModuleDependency): a } export const useSyntheticShadow = hasOwnProperty.call(Element.prototype, '$shadowToken$'); + +export function guid(): string { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); +} diff --git a/packages/@lwc/engine/src/framework/vm.ts b/packages/@lwc/engine/src/framework/vm.ts index 422a05d2a2..d91738a2d2 100644 --- a/packages/@lwc/engine/src/framework/vm.ts +++ b/packages/@lwc/engine/src/framework/vm.ts @@ -56,6 +56,7 @@ import { parentElementGetter, parentNodeGetter } from '../env/node'; import { updateDynamicChildren, updateStaticChildren } from '../3rdparty/snabbdom/snabbdom'; import { hasDynamicChildren } from './hooks'; import { ReactiveObserver } from '../libs/mutation-tracker'; +import { connectWireAdapters, disconnectWireAdapters } from './wiring'; export interface SlotSet { [key: string]: VNodes; @@ -86,9 +87,9 @@ export interface UninitializedVM { /** Adopted Children List */ aChildren: VNodes; velements: VCustomElement[]; - cmpProps: any; + cmpProps: Record; cmpSlots: SlotSet; - cmpTrack: any; + cmpFields: Record; callHook: ( cmp: ComponentInterface | undefined, fn: (...args: any[]) => any, @@ -230,7 +231,7 @@ export function createVM(elm: HTMLElement, Ctor: ComponentConstructor, options: data: EmptyObject, context: create(null), cmpProps: create(null), - cmpTrack: create(null), + cmpFields: create(null), cmpSlots: useSyntheticShadow ? create(null) : undefined, callHook, setHook, @@ -388,6 +389,13 @@ function runConnectedCallback(vm: VM) { if (connected) { invokeServiceHook(vm, connected); } + // TODO: eventually this should be done by node-reactions on the wire.ts directly + const { + def: { wire }, + } = vm; + if (wire.length > 0) { + connectWireAdapters(vm); + } const { connectedCallback } = vm.def; if (!isUndefined(connectedCallback)) { if (process.env.NODE_ENV !== 'production') { @@ -420,6 +428,13 @@ function runDisconnectedCallback(vm: VM) { if (disconnected) { invokeServiceHook(vm, disconnected); } + // TODO: eventually this should be done by node-reactions on the wire.ts directly + const { + def: { wire }, + } = vm; + if (wire.length > 0) { + disconnectWireAdapters(vm); + } const { disconnectedCallback } = vm.def; if (!isUndefined(disconnectedCallback)) { if (process.env.NODE_ENV !== 'production') { diff --git a/packages/@lwc/engine/src/framework/wc.ts b/packages/@lwc/engine/src/framework/wc.ts index beeacbb3a5..75de20da97 100644 --- a/packages/@lwc/engine/src/framework/wc.ts +++ b/packages/@lwc/engine/src/framework/wc.ts @@ -5,11 +5,11 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { ComponentConstructor } from './component'; -import { isUndefined, isObject, isNull, getOwnPropertyNames, ArrayMap } from '../shared/language'; +import { isUndefined, isObject, isNull, ArrayReduce, keys } from '../shared/language'; import { createVM, appendRootVM, removeRootVM, getCustomElementVM, CreateVMInit } from './vm'; import { EmptyObject } from './utils'; import { getComponentDef } from './def'; -import { getPropNameFromAttrName, isAttributeLocked } from './attributes'; +import { isAttributeLocked, getAttrNameFromPropName } from './attributes'; import { HTMLElementConstructor } from './base-bridge-element'; import { patchCustomElementWithRestrictions } from './restrictions'; @@ -30,6 +30,16 @@ export function buildCustomElementConstructor( options?: ShadowRootInit ): HTMLElementConstructor { const { props, bridge: BaseElement } = getComponentDef(Ctor); + // generating the hash table for attributes to avoid duplicate fields + // and facilitate validation and false positives in case of inheritance. + const attributeToPropMap = ArrayReduce.call( + props, + (reducer: Record, propName: string) => { + reducer[getAttrNameFromPropName(propName)] = propName; + return reducer; + }, + {} + ) as Record; const normalizedOptions: CreateVMInit = { mode: 'open', isRoot: true, @@ -62,8 +72,8 @@ export function buildCustomElementConstructor( // ignoring similar values for better perf return; } - const propName = getPropNameFromAttrName(attrName); - if (isUndefined(props[propName])) { + const propName = attributeToPropMap[attrName]; + if (isUndefined(propName)) { // ignoring unknown attributes return; } @@ -81,9 +91,6 @@ export function buildCustomElementConstructor( } // collecting all attribute names from all public props to apply // the reflection from attributes to props via attributeChangedCallback. - static observedAttributes = ArrayMap.call( - getOwnPropertyNames(props), - propName => props[propName].attr - ); + static observedAttributes = keys(attributeToPropMap); }; } diff --git a/packages/@lwc/engine/src/framework/wiring.ts b/packages/@lwc/engine/src/framework/wiring.ts new file mode 100644 index 0000000000..f726b8916a --- /dev/null +++ b/packages/@lwc/engine/src/framework/wiring.ts @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2018, 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 assert from '../shared/assert'; +import { isUndefined, create, ArrayPush } from '../shared/language'; +import { ComponentConstructor, ComponentInterface } from './component'; +import { valueMutated, ReactiveObserver } from '../libs/mutation-tracker'; +import { VM, runWithBoundaryProtection } from './vm'; +import { invokeComponentCallback } from './invoker'; +import { dispatchEvent } from '../env/dom'; + +const WireMetaMap: Map = new Map(); +function noop(): void {} + +function storeDef(Ctor: ComponentConstructor, key: string, def: WireDef) { + const record: Record = WireMetaMap.get(Ctor) || create(null); + record[key] = def; + WireMetaMap.set(Ctor, record); +} + +function createFieldDataCallback(vm: VM) { + const { component, cmpFields } = vm; + return (value: any) => { + // storing the value in the underlying storage + cmpFields[name] = value; + valueMutated(component, name); + }; +} + +function createMethodDataCallback(vm: VM, method: (data: any) => any) { + return (value: any) => { + // dispatching new value into the wired method + invokeComponentCallback(vm, method, [value]); + }; +} + +function createConfigWatcher( + vm: VM, + wireDef: WireDef, + callbackWhenConfigIsReady: (newConfig: ConfigValue) => void +) { + const { component } = vm; + const { configCallback } = wireDef; + let hasPendingConfig: boolean = false; + // creating the reactive observer for reactive params when needed + const ro = new ReactiveObserver(() => { + if (hasPendingConfig === false) { + hasPendingConfig = true; + // collect new config in the micro-task + Promise.resolve().then(() => { + hasPendingConfig = false; + // resetting current reactive params + ro.reset(); + // dispatching a new config due to a change in the configuration + callback(); + }); + } + }); + const callback = () => { + let config: ConfigValue; + ro.observe(() => (config = configCallback(component))); + // TODO: dev-mode validation of config based on the adapter.configSchema + // @ts-ignore it is assigned in the observe() callback + callbackWhenConfigIsReady(config); + }; + return callback; +} + +function createContextWatcher( + vm: VM, + wireDef: WireDef, + callbackWhenContextIsReady: (newContext: ContextValue) => void +) { + const { adapter } = wireDef; + const adapterContextToken = getAdapterToken(adapter); + if (isUndefined(adapterContextToken)) { + return; // no provider found, nothing to be done + } + const { + elm, + context: { wiredConnecting, wiredDisconnecting }, + } = vm; + // waiting for the component to be connected to formally request the context via the token + ArrayPush.call(wiredConnecting, () => { + // This event is responsible for connecting the host element with another + // element in the composed path that is providing contextual data. The provider + // must be listening for a special dom event with the name corresponding to the value of + // `adapterContextToken`, which will remain secret and internal to this file only to + // guarantee that the linkage can be forged. + const internalDomEvent = new CustomEvent(adapterContextToken, { + bubbles: true, + composed: true, + detail(newContext: ContextValue, disconnectCallback: () => void) { + // adds this callback into the disconnect bucket so it gets disconnected from parent + // the the element hosting the wire is disconnected + ArrayPush.call(wiredDisconnecting, disconnectCallback); + // TODO: dev-mode validation of config based on the adapter.contextSchema + callbackWhenContextIsReady(newContext); + }, + }); + dispatchEvent.call(elm, internalDomEvent); + }); +} + +function createConnector(vm: VM, wireDef: WireDef): WireAdapter { + const { method, adapter } = wireDef; + const dataCallback = isUndefined(method) + ? createFieldDataCallback(vm) + : createMethodDataCallback(vm, method); + let context: ContextValue | undefined; + let connector: WireAdapter; + runWithBoundaryProtection( + vm, + vm, + noop, + () => { + // job + connector = new adapter(dataCallback); + }, + noop + ); + const computeConfigAndUpdate = createConfigWatcher(vm, wireDef, (config: ConfigValue) => { + // every time the config is recomputed due to tracking, + // this callback will be invoked with the new computed config + runWithBoundaryProtection( + vm, + vm, + noop, + () => { + // job + connector.update(config, context); + }, + noop + ); + }); + // computing the initial config (no context at this point because the component is not connected) + computeConfigAndUpdate(); + // if the adapter needs contextualization, we need to watch for new context and push it alongside the config + if (!isUndefined(adapter.contextSchema)) { + createContextWatcher(vm, wireDef, (newContext: ContextValue) => { + // every time the context is pushed into this component, + // this callback will be invoked with the new computed context + if (context !== newContext) { + context = newContext; + // Note: when new context arrives, the config will be recomputed and pushed along side the new + // context, this is to preserve the identity characteristics, config should not have identity + // (ever), while context can have identity + computeConfigAndUpdate(); + } + }); + } + // @ts-ignore the boundary protection executes sync, connector is always defined + return connector; +} + +type DataCallback = (value: any) => void; +type ConfigValue = Record; + +interface WireAdapter { + update(config: ConfigValue, context?: ContextValue); + connect(); + disconnect(); +} + +type WireAdapterSchemaValue = 'optional' | 'required'; + +type WireHash = Record; + +interface WireDef { + method?: (data: any) => void; + adapter: WireAdapterConstructor; + configCallback: ConfigCallback; +} + +interface WireMethodDef extends WireDef { + method: (data: any) => void; +} + +interface WireFieldDef extends WireDef { + method?: undefined; +} + +const AdapterToTokenMap: Map = new Map(); + +export function getAdapterToken(adapter: WireAdapterConstructor): string | undefined { + return AdapterToTokenMap.get(adapter); +} + +export function setAdapterToken(adapter: WireAdapterConstructor, token: string) { + AdapterToTokenMap.set(adapter, token); +} + +export type ContextValue = Record; +export type ConfigCallback = (component: ComponentInterface) => ConfigValue; +export interface WireAdapterConstructor { + new (callback: DataCallback): WireAdapter; + configSchema?: Record; + contextSchema?: Record; +} + +export function storeWiredMethodMeta( + Ctor: ComponentConstructor, + methodName: string, + adapter: WireAdapterConstructor, + method: (data: any) => void, + configCallback: ConfigCallback +) { + // support for callable adapters + if ((adapter as any).adapter) { + adapter = (adapter as any).adapter; + } + const def: WireMethodDef = { + adapter, + method, + configCallback, + }; + storeDef(Ctor, methodName, def); +} + +export function storeWiredFieldMeta( + Ctor: ComponentConstructor, + fieldName: string, + adapter: WireAdapterConstructor, + configCallback: ConfigCallback +) { + // support for callable adapters + if ((adapter as any).adapter) { + adapter = (adapter as any).adapter; + } + const def: WireFieldDef = { + adapter, + configCallback, + }; + storeDef(Ctor, fieldName, def); +} + +export function installWireAdapters(vm: VM) { + const { + def: { ctor }, + } = vm; + const meta = WireMetaMap.get(ctor); + if (isUndefined(meta)) { + if (process.env.NODE_ENV !== 'production') { + assert.fail( + `Internal Error: wire adapters should only be installed in instances with at least one wire declaration.` + ); + } + } else { + const connect = []; + const disconnect = []; + for (const name in meta) { + const wireDef = meta[name]; + const connector = createConnector(vm, wireDef); + ArrayPush.call(connect, () => connector.connect()); + ArrayPush.call(disconnect, () => connector.disconnect()); + } + vm.context.wiredConnecting = connect; + vm.context.wiredDisconnecting = disconnect; + } +} + +export function connectWireAdapters(vm: VM) { + const { + context: { wiredConnecting }, + } = vm; + if (isUndefined(wiredConnecting)) { + if (process.env.NODE_ENV !== 'production') { + assert.fail( + `Internal Error: wire adapters must be installed in instances with at least one wire declaration.` + ); + } + } + for (let i = 0, len = wiredConnecting.length; i < len; i += 1) { + wiredConnecting[i](); + } +} + +export function disconnectWireAdapters(vm: VM) { + const { + context: { wiredDisconnecting }, + } = vm; + if (isUndefined(wiredDisconnecting)) { + if (process.env.NODE_ENV !== 'production') { + assert.fail( + `Internal Error: wire adapters must be installed in instances with at least one wire declaration.` + ); + } + } + runWithBoundaryProtection( + vm, + vm, + noop, + () => { + // job + for (let i = 0, len = wiredDisconnecting.length; i < len; i += 1) { + wiredDisconnecting[i](); + } + }, + noop + ); +} diff --git a/packages/@lwc/engine/src/shared/language.ts b/packages/@lwc/engine/src/shared/language.ts index 0ea4b29bc3..e3706c83e0 100644 --- a/packages/@lwc/engine/src/shared/language.ts +++ b/packages/@lwc/engine/src/shared/language.ts @@ -23,10 +23,12 @@ const { slice: ArraySlice, splice: ArraySplice, unshift: ArrayUnshift, + shift: ArrayShift, indexOf: ArrayIndexOf, push: ArrayPush, map: ArrayMap, join: ArrayJoin, + concat: ArrayConcat, forEach, reduce: ArrayReduce, } = Array.prototype; @@ -36,6 +38,7 @@ const { toLowerCase: StringToLowerCase, charCodeAt: StringCharCodeAt, slice: StringSlice, + split: StringSplit, } = String.prototype; export { @@ -43,6 +46,7 @@ export { StringReplace, StringCharCodeAt, StringSlice, + StringSplit, freeze, seal, keys, @@ -59,8 +63,10 @@ export { ArraySlice, ArraySplice, ArrayUnshift, + ArrayShift, ArrayMap, ArrayJoin, + ArrayConcat, isArray, ArrayIndexOf, ArrayPush, diff --git a/packages/@lwc/wire-service/src/__tests__/assert.spec.ts b/packages/@lwc/wire-service/src/__tests__/assert.spec.ts deleted file mode 100644 index 1e52c3a9ea..0000000000 --- a/packages/@lwc/wire-service/src/__tests__/assert.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2018, 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 assert from '../assert'; - -describe('assert', () => { - describe('isTrue()', () => { - it('should throw error that includes custom message', () => { - expect(() => assert.isTrue(false, 'foo bar')).toThrowError(/foo bar/); - }); - - it('should not throw error for true', () => { - expect(() => assert.isTrue(true, 'foo bar')).not.toThrow(); - }); - }); - describe('isFalse()', () => { - it('should throw error that includes custom message', () => { - expect(() => assert.isFalse(true, 'foo bar')).toThrowError(/foo bar/); - }); - - it('should not throw error for false', () => { - expect(() => assert.isFalse(false, 'foo bar')).not.toThrow(); - }); - }); -}); diff --git a/packages/@lwc/wire-service/src/assert.ts b/packages/@lwc/wire-service/src/assert.ts deleted file mode 100644 index 0399b00ac3..0000000000 --- a/packages/@lwc/wire-service/src/assert.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) 2018, 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 - */ - -export default { - isTrue(value: any, msg: string) { - if (!value) { - throw new Error(`Assert Violation: ${msg}`); - } - }, - isFalse(value: any, msg: string) { - if (value) { - throw new Error(`Assert Violation: ${msg}`); - } - }, -}; diff --git a/packages/@lwc/wire-service/src/constants.ts b/packages/@lwc/wire-service/src/constants.ts deleted file mode 100644 index 9ddbb8d3e5..0000000000 --- a/packages/@lwc/wire-service/src/constants.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2018, 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 - */ -// key in engine service context for wire service context -export const CONTEXT_ID = '@wire'; -// key in wire service context for updated listener metadata -export const CONTEXT_UPDATED = 'updated'; -// key in wire service context for connected listener metadata -export const CONTEXT_CONNECTED = 'connected'; -// key in wire service context for disconnected listener metadata -export const CONTEXT_DISCONNECTED = 'disconnected'; - -// wire event target life cycle connectedCallback hook event type -export const CONNECT = 'connect'; -// wire event target life cycle disconnectedCallback hook event type -export const DISCONNECT = 'disconnect'; -// wire event target life cycle config changed hook event type -export const CONFIG = 'config'; diff --git a/packages/@lwc/wire-service/src/engine.ts b/packages/@lwc/wire-service/src/engine.ts deleted file mode 100644 index 9116f69e9e..0000000000 --- a/packages/@lwc/wire-service/src/engine.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2018, 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 - */ -// subtypes from lwc -export interface WireDef { - params?: { - [key: string]: string; - }; - static?: { - [key: string]: any; - }; - adapter: any; - method?: 1; -} -export interface ElementDef { - // wire is optional on ElementDef but the lwc guarantees it before invoking wiring service hook - wire: { - [key: string]: WireDef; - }; -} diff --git a/packages/@lwc/wire-service/src/index.ts b/packages/@lwc/wire-service/src/index.ts index eb5aeab58a..88b7f4711a 100644 --- a/packages/@lwc/wire-service/src/index.ts +++ b/packages/@lwc/wire-service/src/index.ts @@ -4,166 +4,143 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ + /** - * The @wire service. - * - * Provides data binding between wire adapters and LWC components decorated with @wire. - * Register wire adapters with `register(adapterId: any, adapterFactory: WireAdapterFactory)`. + * Registers a wire adapter factory for Lightning Platform. + * @deprecated */ +export function register( + adapterId: any, + adapterEventTargetCallback: (eventTarget: WireEventTarget) => void +) { + if (!adapterId) { + new TypeError('adapter id must be truthy'); + } + if (typeof adapterEventTargetCallback !== 'function') { + new TypeError('adapter factory must be a callable'); + } + if ('adapter' in adapterId) { + new TypeError('adapter id is already associated to an adapter factory'); + } + adapterId.adapter = class extends WireAdapter { + constructor(dataCallback: dataCallback) { + super(dataCallback); + adapterEventTargetCallback(this.eventTarget); + } + }; +} -import assert from './assert'; -import { CONTEXT_ID, CONTEXT_CONNECTED, CONTEXT_DISCONNECTED, CONTEXT_UPDATED } from './constants'; -import { ElementDef } from './engine'; -import { - NoArgumentListener, - WireEventTargetListener, - Context, - WireContext, - WireEventTarget, -} from './wiring'; import { ValueChangedEvent } from './value-changed-event'; -import { LinkContextEvent } from './link-context-event'; -export interface WireEventTarget { - dispatchEvent(evt: ValueChangedEvent): boolean; - addEventListener(type: string, listener: WireEventTargetListener): void; - removeEventListener(type: string, listener: WireEventTargetListener): void; +const { forEach, splice: ArraySplice, indexOf: ArrayIndexOf } = Array.prototype; + +// wire event target life cycle connectedCallback hook event type +const CONNECT = 'connect'; +// wire event target life cycle disconnectedCallback hook event type +const DISCONNECT = 'disconnect'; +// wire event target life cycle config changed hook event type +const CONFIG = 'config'; + +type NoArgumentListener = () => void; +interface ConfigListenerArgument { + [key: string]: any; } +type ConfigListener = (config: ConfigListenerArgument) => void; -export type WireAdapterFactory = (eventTarget: WireEventTarget) => void; +type WireEventTargetListener = NoArgumentListener | ConfigListener; -// wire adapters: wire adapter id => adapter ctor -const adapterFactories: Map = new Map(); +export interface WireEventTarget { + addEventListener: (type: string, listener: WireEventTargetListener) => void; + removeEventListener: (type: string, listener: WireEventTargetListener) => void; + dispatchEvent: (evt: ValueChangedEvent) => boolean; +} -/** - * Invokes the specified callbacks. - * @param listeners functions to call - */ -function invokeListener(listeners: NoArgumentListener[]) { - for (let i = 0, len = listeners.length; i < len; ++i) { - listeners[i].call(undefined); +function removeListener(listeners: WireEventTargetListener[], toRemove: WireEventTargetListener) { + const idx = ArrayIndexOf.call(listeners, toRemove); + if (idx > -1) { + ArraySplice.call(listeners, idx, 1); } } -/** - * The wire service. - * - * This service is registered with the engine's service API. It connects service - * callbacks to wire adapter lifecycle events. - */ -const wireService = { - wiring: (cmp: EventTarget, data: object, def: ElementDef, context: Context) => { - const wireContext: WireContext = (context[CONTEXT_ID] = Object.create(null)); - wireContext[CONTEXT_CONNECTED] = []; - wireContext[CONTEXT_DISCONNECTED] = []; - wireContext[CONTEXT_UPDATED] = { listeners: {}, values: {} }; - - // engine guarantees invocation only if def.wire is defined - const wireStaticDef = def.wire; - const wireTargets = Object.keys(wireStaticDef); - for (let i = 0, len = wireTargets.length; i < len; i++) { - const wireTarget = wireTargets[i]; - const wireDef = wireStaticDef[wireTarget]; - const adapterFactory = adapterFactories.get(wireDef.adapter); - - if (process.env.NODE_ENV !== 'production') { - assert.isTrue( - wireDef.adapter, - `@wire on "${wireTarget}": adapter id must be truthy` - ); - assert.isTrue( - adapterFactory, - `@wire on "${wireTarget}": unknown adapter id: ${String(wireDef.adapter)}` - ); - - // enforce restrictions of reactive parameters - if (wireDef.params) { - Object.keys(wireDef.params).forEach(param => { - const prop = wireDef.params![param]; - const segments = prop.split('.'); - segments.forEach(segment => { - assert.isTrue( - segment.length > 0, - `@wire on "${wireTarget}": reactive parameters must not be empty` - ); - }); - assert.isTrue( - segments[0] !== wireTarget, - `@wire on "${wireTarget}": reactive parameter "${ - segments[0] - }" must not refer to self` - ); - // restriction for dot-notation reactive parameters - if (segments.length > 1) { - // @wire emits a stream of immutable values. an emit sets the target property; it does not mutate a previously emitted value. - // restricting dot-notation reactive parameters to reference other @wire targets makes trapping the 'head' of the parameter - // sufficient to observe the value change. - assert.isTrue( - wireTargets.includes(segments[0]) && - wireStaticDef[segments[0]].method !== 1, - `@wire on "${wireTarget}": dot-notation reactive parameter "${prop}" must refer to a @wire property` - ); - } - }); +type dataCallback = (value: any) => void; +export interface WireAdapterConstructor { + new (callback: dataCallback): WireAdapter; +} + +export class WireAdapter { + private callback: dataCallback; + + private connecting: NoArgumentListener[] = []; + private disconnecting: NoArgumentListener[] = []; + private configuring: ConfigListener[] = []; + + constructor(callback: dataCallback) { + this.callback = callback; + this.eventTarget = { + addEventListener: (type: string, listener: WireEventTargetListener): void => { + switch (type) { + case CONNECT: { + this.connecting.push(listener as NoArgumentListener); + break; + } + case DISCONNECT: { + this.disconnecting.push(listener as NoArgumentListener); + break; + } + case CONFIG: { + this.configuring.push(listener as ConfigListener); + break; + } + default: + throw new Error(`Invalid event type ${type}.`); } - } - - if (adapterFactory) { - const wireEventTarget = new WireEventTarget(cmp, def, context, wireDef, wireTarget); - adapterFactory({ - dispatchEvent: wireEventTarget.dispatchEvent.bind(wireEventTarget), - addEventListener: wireEventTarget.addEventListener.bind(wireEventTarget), - removeEventListener: wireEventTarget.removeEventListener.bind(wireEventTarget), - } as WireEventTarget); - } - } - }, - - connected: (cmp: EventTarget, data: object, def: ElementDef, context: Context) => { - let listeners: NoArgumentListener[]; - if (process.env.NODE_ENV !== 'production') { - assert.isTrue( - !def.wire || context[CONTEXT_ID], - 'wire service was not initialized prior to component creation: "connected" service hook invoked without necessary context' - ); - } - if (!def.wire || !(listeners = context[CONTEXT_ID][CONTEXT_CONNECTED])) { - return; - } - invokeListener(listeners); - }, - - disconnected: (cmp: EventTarget, data: object, def: ElementDef, context: Context) => { - let listeners: NoArgumentListener[]; - if (process.env.NODE_ENV !== 'production') { - assert.isTrue( - !def.wire || context[CONTEXT_ID], - 'wire service was not initialized prior to component creation: "disconnected" service hook invoked without necessary context' - ); - } - if (!def.wire || !(listeners = context[CONTEXT_ID][CONTEXT_DISCONNECTED])) { - return; - } - invokeListener(listeners); - }, -}; + }, + removeEventListener: (type: string, listener: WireEventTargetListener): void => { + switch (type) { + case CONNECT: { + removeListener(this.connecting, listener); + break; + } + case DISCONNECT: { + removeListener(this.disconnecting, listener); + break; + } + case CONFIG: { + removeListener(this.configuring, listener); + break; + } + default: + throw new Error(`Invalid event type ${type}.`); + } + }, + dispatchEvent: (evt: ValueChangedEvent): boolean => { + if (evt instanceof ValueChangedEvent) { + const value = evt.value; + this.callback(value); + } else { + throw new Error(`Invalid event type ${(evt as any).type}.`); + } + return false; // canceling signal since we don't want this to propagate + }, + }; + } -/** - * Registers the wire service. - */ -export function registerWireService(registerService: (object) => void) { - registerService(wireService); -} + protected eventTarget: WireEventTarget; -/** - * Registers a wire adapter. - */ -export function register(adapterId: any, adapterFactory: WireAdapterFactory) { - if (process.env.NODE_ENV !== 'production') { - assert.isTrue(adapterId, 'adapter id must be truthy'); - assert.isTrue(typeof adapterFactory === 'function', 'adapter factory must be a callable'); + update(config: Record) { + forEach.call(this.configuring, listener => { + listener.call(undefined, config); + }); + } + + connect() { + forEach.call(this.connecting, listener => listener.call(undefined)); + } + + disconnect() { + forEach.call(this.disconnecting, listener => listener.call(undefined)); } - adapterFactories.set(adapterId, adapterFactory); } -export { ValueChangedEvent, LinkContextEvent }; +// re-exporting event constructors +export { ValueChangedEvent }; diff --git a/packages/@lwc/wire-service/src/link-context-event.ts b/packages/@lwc/wire-service/src/link-context-event.ts deleted file mode 100644 index 3196513b08..0000000000 --- a/packages/@lwc/wire-service/src/link-context-event.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2018, 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 - */ - -const LinkContextEventType = 'LinkContextEvent'; - -/** - * Event fired by wire adapters to link to a context provider - */ -export class LinkContextEvent { - type: string; - uid: string; - callback: (...args: any[]) => void; - constructor(uid: string, callback: (...args: any[]) => void) { - this.type = LinkContextEventType; - this.uid = uid; - this.callback = callback; - } -} diff --git a/packages/@lwc/wire-service/src/property-trap.ts b/packages/@lwc/wire-service/src/property-trap.ts deleted file mode 100644 index b75d7fd8e6..0000000000 --- a/packages/@lwc/wire-service/src/property-trap.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (c) 2018, 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 - */ -/* - * Detects property changes by installing setter/getter overrides on the component - * instance. - * - * TODO - in 216 engine will expose an 'updated' callback for services that is invoked - * once after all property changes occur in the event loop. - */ - -import { ConfigListenerMetadata, ConfigContext, ReactiveParameter } from './wiring'; - -/** - * Invokes the provided change listeners with the resolved component properties. - * @param configListenerMetadatas List of config listener metadata (config listeners and their context) - * @param paramValues Values for all wire adapter config params - */ -function invokeConfigListeners( - configListenerMetadatas: Set, - paramValues: any -) { - configListenerMetadatas.forEach(metadata => { - const { listener, statics, reactives } = metadata; - - const reactiveValues = Object.create(null); - if (reactives) { - const keys = Object.keys(reactives); - for (let j = 0, jlen = keys.length; j < jlen; j++) { - const key = keys[j]; - const value = paramValues[reactives[key]]; - reactiveValues[key] = value; - } - } - - // TODO - consider read-only membrane to enforce invariant of immutable config - const config = Object.assign({}, statics, reactiveValues); - listener.call(undefined, config); - }); -} - -/** - * Marks a reactive parameter as having changed. - * @param cmp The component - * @param reactiveParameter Reactive parameter that has changed - * @param configContext The service context - */ -export function updated( - cmp: EventTarget, - reactiveParameter: ReactiveParameter, - configContext: ConfigContext -) { - if (!configContext.mutated) { - configContext.mutated = new Set(); - // collect all prop changes via a microtask - Promise.resolve().then(updatedFuture.bind(undefined, cmp, configContext)); - } - configContext.mutated.add(reactiveParameter); -} - -function updatedFuture(cmp: EventTarget, configContext: ConfigContext) { - const uniqueListeners = new Set(); - - // configContext.mutated must be set prior to invoking this function - const mutated = configContext.mutated as Set; - delete configContext.mutated; - - mutated.forEach(reactiveParameter => { - const value = getReactiveParameterValue(cmp, reactiveParameter); - if (configContext.values[reactiveParameter.reference] === value) { - return; - } - configContext.values[reactiveParameter.reference] = value; - - const listeners = configContext.listeners[reactiveParameter.head]; - for (let i = 0, len = listeners.length; i < len; i++) { - uniqueListeners.add(listeners[i]); - } - }); - - invokeConfigListeners(uniqueListeners, configContext.values); -} - -/** - * Gets the value of an @wire reactive parameter. - * @param cmp The component - * @param reactiveParameter The parameter to get - */ -export function getReactiveParameterValue( - cmp: EventTarget, - reactiveParameter: ReactiveParameter -): any { - let value: any = cmp[reactiveParameter.head]; - if (!reactiveParameter.tail) { - return value; - } - - const segments = reactiveParameter.tail; - for (let i = 0, len = segments.length; i < len && value != null; i++) { - const segment = segments[i]; - if (typeof value !== 'object' || !(segment in value)) { - return undefined; - } - value = value[segment]; - } - return value; -} - -/** - * Installs setter override to trap changes to a property, triggering the config listeners. - * @param cmp The component - * @param reactiveParameter Reactive parameter that defines the property to monitor - * @param configContext The service context - */ -export function installTrap( - cmp: EventTarget, - reactiveParameter: ReactiveParameter, - configContext: ConfigContext -) { - const callback = updated.bind(undefined, cmp, reactiveParameter, configContext); - const newDescriptor = getOverrideDescriptor(cmp, reactiveParameter.head, callback); - Object.defineProperty(cmp, reactiveParameter.head, newDescriptor); -} - -/** - * Finds the descriptor of the named property on the prototype chain - * @param target The target instance/constructor function - * @param propName Name of property to find - * @param protoSet Prototypes searched (to avoid circular prototype chains) - */ -export function findDescriptor( - target: any, - propName: PropertyKey, - protoSet?: any[] -): PropertyDescriptor | null { - protoSet = protoSet || []; - if (!target || protoSet.indexOf(target) > -1) { - return null; // null, undefined, or circular prototype definition - } - const descriptor = Object.getOwnPropertyDescriptor(target, propName); - if (descriptor) { - return descriptor; - } - const proto = Object.getPrototypeOf(target); - if (!proto) { - return null; - } - protoSet.push(target); - return findDescriptor(proto, propName, protoSet); -} - -/** - * Gets a property descriptor that monitors the provided property for changes - * @param cmp The component - * @param prop The name of the property to be monitored - * @param callback A function to invoke when the prop's value changes - * @return A property descriptor - */ -function getOverrideDescriptor(cmp: Object, prop: string, callback: () => void) { - const descriptor = findDescriptor(cmp, prop); - let enumerable; - let get; - let set; - // This does not cover the override of existing descriptors at the instance level - // and that's ok because eventually we will not need to do any of these :) - if (descriptor === null || (descriptor.get === undefined && descriptor.set === undefined)) { - let value = cmp[prop]; - enumerable = true; - get = function() { - return value; - }; - set = function(newValue) { - value = newValue; - callback(); - }; - } else { - const { set: originalSet, get: originalGet } = descriptor; - enumerable = descriptor.enumerable; - set = function(newValue) { - if (originalSet) { - originalSet.call(cmp, newValue); - } - callback(); - }; - get = function() { - return originalGet ? originalGet.call(cmp) : undefined; - }; - } - return { - set, - get, - enumerable, - configurable: true, - }; -} diff --git a/packages/@lwc/wire-service/src/wiring.ts b/packages/@lwc/wire-service/src/wiring.ts deleted file mode 100644 index c29a5934f5..0000000000 --- a/packages/@lwc/wire-service/src/wiring.ts +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright (c) 2018, 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 assert from './assert'; -import { - CONTEXT_ID, - CONTEXT_CONNECTED, - CONTEXT_DISCONNECTED, - CONTEXT_UPDATED, - CONNECT, - DISCONNECT, - CONFIG, -} from './constants'; -import { ElementDef, WireDef } from './engine'; -import { installTrap, updated } from './property-trap'; -import { ValueChangedEvent } from './value-changed-event'; -import { LinkContextEvent } from './link-context-event'; - -export type NoArgumentListener = () => void; -export interface ConfigListenerArgument { - [key: string]: any; -} -export type ConfigListener = (ConfigListenerArgument) => void; - -// a reactive parameter (WireDef.params.key) may be a dot-notation string to traverse into another @wire's target -export interface ReactiveParameter { - reference: string; // the complete parameter (aka original WireDef.params.key) - head: string; // head of the parameter - tail?: string[]; // remaining tail of the parameter, present if it's dot-notation -} - -export interface ConfigListenerMetadata { - listener: ConfigListener; - statics?: { - [key: string]: any; - }; - reactives?: { - [key: string]: string; - }; -} -export interface ConfigContext { - // map of reactive parameters to list of config listeners - // when a reactive parameter changes it's a O(1) lookup to the list of config listeners to notify - listeners: { - [key: string]: ConfigListenerMetadata[]; - }; - // map of param values - values: { - [key: string]: any; - }; - // mutated reactive parameters (debounced then cleared) - mutated?: Set; -} - -export interface WireContext { - [CONTEXT_CONNECTED]: NoArgumentListener[]; - [CONTEXT_DISCONNECTED]: NoArgumentListener[]; - [CONTEXT_UPDATED]: ConfigContext; -} - -export interface Context { - [CONTEXT_ID]: WireContext; -} - -export type WireEventTargetListener = NoArgumentListener | ConfigListener; - -function removeListener(listeners: WireEventTargetListener[], toRemove: WireEventTargetListener) { - const idx = listeners.indexOf(toRemove); - if (idx > -1) { - listeners.splice(idx, 1); - } -} - -function removeConfigListener( - configListenerMetadatas: ConfigListenerMetadata[], - toRemove: ConfigListener -) { - for (let i = 0, len = configListenerMetadatas.length; i < len; i++) { - if (configListenerMetadatas[i].listener === toRemove) { - configListenerMetadatas.splice(i, 1); - return; - } - } -} - -function buildReactiveParameter(reference: string): ReactiveParameter { - if (!reference.includes('.')) { - return { - reference, - head: reference, - }; - } - const segments = reference.split('.'); - return { - reference, - head: segments.shift() as string, - tail: segments, - }; -} - -export class WireEventTarget { - _cmp: EventTarget; - _def: ElementDef; - _context: Context; - _wireDef: WireDef; - _wireTarget: string; - - constructor( - cmp: EventTarget, - def: ElementDef, - context: Context, - wireDef: WireDef, - wireTarget: string - ) { - this._cmp = cmp; - this._def = def; - this._context = context; - this._wireDef = wireDef; - this._wireTarget = wireTarget; - } - - addEventListener(type: string, listener: WireEventTargetListener): void { - switch (type) { - case CONNECT: { - const connectedListeners = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; - if (process.env.NODE_ENV !== 'production') { - assert.isFalse( - connectedListeners.includes(listener as NoArgumentListener), - 'must not call addEventListener("connect") with the same listener' - ); - } - connectedListeners.push(listener as NoArgumentListener); - break; - } - - case DISCONNECT: { - const disconnectedListeners = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; - if (process.env.NODE_ENV !== 'production') { - assert.isFalse( - disconnectedListeners.includes(listener as NoArgumentListener), - 'must not call addEventListener("disconnect") with the same listener' - ); - } - disconnectedListeners.push(listener as NoArgumentListener); - break; - } - - case CONFIG: { - const reactives = this._wireDef.params; - const statics = this._wireDef.static; - let reactiveKeys: string[]; - - // no reactive parameters. fire config once with static parameters (if present). - if (!reactives || (reactiveKeys = Object.keys(reactives)).length === 0) { - const config = statics || Object.create(null); - listener.call(undefined, config); - return; - } - - const configListenerMetadata: ConfigListenerMetadata = { - listener, - statics, - reactives, - }; - - // setup listeners for all reactive parameters - const configContext = this._context[CONTEXT_ID][CONTEXT_UPDATED]; - reactiveKeys.forEach(key => { - const reactiveParameter = buildReactiveParameter(reactives[key]); - let configListenerMetadatas = configContext.listeners[reactiveParameter.head]; - if (!configListenerMetadatas) { - configListenerMetadatas = [configListenerMetadata]; - configContext.listeners[reactiveParameter.head] = configListenerMetadatas; - installTrap(this._cmp, reactiveParameter, configContext); - } else { - configListenerMetadatas.push(configListenerMetadata); - } - // enqueue to pickup default values - updated(this._cmp, reactiveParameter, configContext); - }); - - break; - } - - default: - throw new Error(`unsupported event type ${type}`); - } - } - - removeEventListener(type: string, listener: WireEventTargetListener): void { - switch (type) { - case CONNECT: { - const connectedListeners = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; - removeListener(connectedListeners, listener); - break; - } - - case DISCONNECT: { - const disconnectedListeners = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; - removeListener(disconnectedListeners, listener); - break; - } - - case CONFIG: { - const paramToConfigListenerMetadata = this._context[CONTEXT_ID][CONTEXT_UPDATED] - .listeners; - const reactives = this._wireDef.params; - if (reactives) { - Object.keys(reactives).forEach(key => { - const reactiveParameter = buildReactiveParameter(reactives[key]); - const configListenerMetadatas = - paramToConfigListenerMetadata[reactiveParameter.head]; - if (configListenerMetadatas) { - removeConfigListener(configListenerMetadatas, listener); - } - }); - } - break; - } - - default: - throw new Error(`unsupported event type ${type}`); - } - } - - dispatchEvent(evt: ValueChangedEvent | LinkContextEvent | Event): boolean { - if (evt instanceof ValueChangedEvent) { - const value = evt.value; - if (this._wireDef.method) { - this._cmp[this._wireTarget](value); - } else { - this._cmp[this._wireTarget] = value; - } - return false; // canceling signal since we don't want this to propagate - } else if (evt instanceof LinkContextEvent) { - const { uid, callback } = evt; - // This event is responsible for connecting the host element with another - // element in the composed path that is providing contextual data. The provider - // must be listening for a special dom event with the name corresponding to `uid`, - // which must remain secret, to guarantee that the linkage is only possible via - // the corresponding wire adapter. - const internalDomEvent = new CustomEvent(uid, { - bubbles: true, - composed: true, - // avoid leaking the callback function directly to prevent a side channel - // during the linking phase to the context provider. - detail(...args: any[]) { - callback(...args); - }, - }); - this._cmp.dispatchEvent(internalDomEvent); - return false; // canceling signal since we don't want this to propagate - } else if (evt.type === 'WireContextEvent') { - // TODO: issue #1357 - remove this branch - return this._cmp.dispatchEvent(evt); - } else { - throw new Error(`Invalid event ${evt}.`); - } - } -}