From d286bee70a24561b2759094c8c637af5b6fc44f1 Mon Sep 17 00:00:00 2001 From: Kevin Venkiteswaran Date: Wed, 14 Mar 2018 22:53:01 -0700 Subject: [PATCH 01/52] WIP --- packages/lwc-wire-service/src/assert.ts | 18 ++ packages/lwc-wire-service/src/index.ts | 107 +++++++++ packages/lwc-wire-service/src/shared-types.ts | 15 ++ packages/lwc-wire-service/src/wiring.ts | 222 ++++++++++++++++++ 4 files changed, 362 insertions(+) create mode 100644 packages/lwc-wire-service/src/assert.ts create mode 100644 packages/lwc-wire-service/src/index.ts create mode 100644 packages/lwc-wire-service/src/shared-types.ts create mode 100644 packages/lwc-wire-service/src/wiring.ts diff --git a/packages/lwc-wire-service/src/assert.ts b/packages/lwc-wire-service/src/assert.ts new file mode 100644 index 0000000000..451778d4a6 --- /dev/null +++ b/packages/lwc-wire-service/src/assert.ts @@ -0,0 +1,18 @@ + +export default { + invariant(value: any, msg: string) { + if (!value) { + throw new Error(`Invariant Violation: ${msg}`); + } + }, + 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/index.ts b/packages/lwc-wire-service/src/index.ts new file mode 100644 index 0000000000..3d3aa5484a --- /dev/null +++ b/packages/lwc-wire-service/src/index.ts @@ -0,0 +1,107 @@ +/** + * The @wire service. + * + * Provides data binding between wire adapters and LWC components decorated with @wire. + * Register wire adapters with `register(adapterId: any, adapterFactory: WireAdapterFactory)`. + */ + +import { Element } from 'engine'; +import assert from './assert'; +import { ElementDef } from './shared-types'; + +export interface WiredValue { + data?: any; + error?: any; +}; +export type TargetSetter = (WiredValue) => void; +export type UpdatedCallback = (object) => void; +export type NoArgumentCallback = () => void; +export type WireAdapterDefCallback = UpdatedCallback | NoArgumentCallback; +export interface WireAdapter { + updated?: UpdatedCallback; + connected?: NoArgumentCallback; + disconnected?: NoArgumentCallback; +}; +export type WireAdapterFactory = (targetSetter: TargetSetter) => WireAdapter; + +// lifecycle hooks of wire adapters +const HOOKS: Array = ['updated', 'connected', 'disconnected']; + +// wire adapters: wire adapter id => adapter ctor +const ADAPTERS: Map = new Map(); + +// key for engine service context store +const CONTEXT_ID: string = '@wire'; + +/** + * Invokes the specified callbacks with specified arguments. + */ +function invokeCallback(callbacks: WireAdapterDefCallback, arg: object|undefined) { + for (let i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(undefined, arg); + } +} + +/** + * The wire service. + * + * This is registered service with the engine's service API. It delegates + * lifecycle callbacks to relevant wire adapter lifecycle callbacks. + */ +const wireService = { + // TODO W-4072588 - support connected + disconnected (repeated) cycles + wiring: (cmp: Element, data: object, def: ElementDef, context: object) => { + // engine guarantees invocation only if def.wire is defined + const adapters = installWireAdapters(cmp, def); + + // collect all adapters' callbacks into arrays for future invocation + let contextData = Object.create(null); + for (let i = 0; i < HOOKS.length; i++) { + let hook = HOOKS[i]; + let callbacks: Array = []; + for (let j = 0; j < adapters.length; j++) { + let callback = adapters[j][hook]; + if (callback) { + callbacks.push(callback); + } + } + if (callbacks.length > 0) { + contextData[hook] = callbacks; + } + } + context[CONTEXT_ID] = contextData; + }, + + connected: (cmp: Element, data: object, def: ElementDef, context: object) => { + let callbacks; + if (!def.wire || (callbacks = context[CONTEXT_ID]['connected']) ) { + return; + } + invokeCallback(callbacks, undefined); + }, + + disconnected: (cmp: Element, data: object, def: ElementDef, context: object) => { + let callbacks; + if (!def.wire || (callbacks = context[CONTEXT_ID]['disconnected']) ) { + return; + } + invokeCallback(callbacks, undefined); + } +}; + + +/** + * Registers the wire service. + */ +export function registerWireService(engine: any) { + engine.register(wireService); +} + +/** + * Registers a wire adapter. + */ +export function register(adapterId: any, adapterFactory: WireAdapterFactory) { + assert.isTrue(adapterId, 'adapter id must be truthy'); + assert.isTrue(typeof adapterFactory === 'function', 'adapter factory must be a function'); + ADAPTERS.set(adapterId, adapterFactory); +}; diff --git a/packages/lwc-wire-service/src/shared-types.ts b/packages/lwc-wire-service/src/shared-types.ts new file mode 100644 index 0000000000..73181cf543 --- /dev/null +++ b/packages/lwc-wire-service/src/shared-types.ts @@ -0,0 +1,15 @@ +export interface WireDef { + params: { + [key: string]: string + }, + static: { + [key: string]: any + }, + type: string, + method?: 1 +} +export interface ElementDef { + wire: { // TODO - wire is optional but all wire service code assumes it's present + [key: string] : WireDef + }; +} diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts new file mode 100644 index 0000000000..a6e2c9aef5 --- /dev/null +++ b/packages/lwc-wire-service/src/wiring.ts @@ -0,0 +1,222 @@ +/** + * Wires LWC component to wire adapters. + */ + + +import { Element } from 'engine'; +import { ElementDef, WireDef } from './shared-types'; + +/** + * Gets a mapping of component prop to wire config dynamic params. In other words, + * the wire config's parameter set that updates whenever a prop changes. + * @param {*} wireDef The wire definition. + * @param {String} wireTarget Component property that is the target of the wire. + * @returns {Object} Map of prop name to wire config dynamic params. + */ +export function getPropToParams(wireDef: WireDef, wireTarget) { + const map = Object.create(null); + const { params } = wireDef; + if (params) { + Object.keys(params).forEach(param => { + const prop = params[param]; + + if (param in wireDef.static) { + throw new Error(`${wireTarget}'s @wire('${wireDef.type}') parameter ${param} specified multiple times.`); + } + + // attribute change handlers use hyphenated case + let set = map[prop]; + if (!set) { + set = map[prop] = []; + } + set.push(param); + }); + } + return map; +} + +/** + * Gets the wire adapter for a wire. + * @param {*} wireDef The wire definition + * @param {String} wireTarget Component property that is the target of the wire. + * @returns {Function} The wire adapter. + */ +export function getAdapter(wireDef: WireDef, wireTarget) { + let adapter; + // TODO: deprecate type once consumers have migrate to use function identifier for adapter id. + if (wireDef.type) { + adapter = ADAPTERS.get(wireDef.type); + if (!adapter) { + throw new Error(`Unknown wire adapter id '${wireDef.type}' in ${wireTarget}'s @wire('${wireDef.type}', ...)`); + } + } else if (wireDef.adapter) { + adapter = ADAPTERS.get(wireDef.adapter.name); + if (!adapter) { + throw new Error(`Unknown wire adapter id '${wireDef.adapter.name}' in ${wireTarget}'s @wire(${wireDef.adapter.name}, ...)`); + } + } + + return adapter; +} + +/** + * Gets the non-dynamic wire config. + * @param {*} wireDef The wire definition + * @returns {Object} The non-dynamic portions of the wire config. + */ +export function getStaticConfig(wireDef: WireDef): object { + const config = Object.create(null); + Object.assign(config, wireDef.static); + return config; +} + +/** + * Gets whether the wiring is to a method or property. + * @param {*} wireDef The wire definition + * @returns {Boolean} True if wired to a method, false otherwise. + */ +export function getIsMethod(wireDef: WireDef): boolean { + return wireDef.method === 1; +} + +/** + * Gets the config bags for all wires on a component. + * @param {*} def The component definition. + * @return {Object} Map of wire configs. + */ +export function getWireConfigs(def: ElementDef) { + const wires = def.wire; + const wireConfigs = Object.create(null); + Object.keys(wires).forEach(wireTarget => { + const wireDef = wires[wireTarget]; + const propToParams = getPropToParams(wireDef, wireTarget); + const adapter = getAdapter(wireDef, wireTarget); + const staticConfig = getStaticConfig(wireDef); + const isMethod = getIsMethod(wireDef); + wireConfigs[wireTarget] = { propToParams, adapter, staticConfig, isMethod }; + }); + return wireConfigs; +} + +/** + * Gets the WiredValue instances for the wire config bags. + * @param {*} wireConfigs The wire config bags. + * @param {*} cmp The component whose wiring is being created. + * @returns {Object} The WiredValue instances. + */ +export function getWiredValues(wireConfigs, cmp) { + const wiredValues = Object.create(null); + Object.keys(wireConfigs).forEach(wireTarget => { + const { adapter, staticConfig, isMethod } = wireConfigs[wireTarget]; + wiredValues[wireTarget] = new WiredValue(adapter, staticConfig, isMethod, cmp, wireTarget); + }); + return wiredValues; +} + +/** + * Gets the WiredValue handlers for property changes. + * @param {*} wireConfigs The wire config bags. + * @param {Object} wiredValues The WiredValue instances. + * @return {Object} Map of props to change handlers. + */ +export function getPropChangeHandlers(wireConfigs, wiredValues) { + const map = Object.create(null); + Object.keys(wireConfigs).forEach(wireTarget => { + const { propToParams } = wireConfigs[wireTarget]; + const wiredValue = wiredValues[wireTarget]; + + Object.keys(propToParams).forEach(prop => { + let set = map[prop]; + if (!set) { + set = map[prop] = []; + } + + const boundUpdate = propToParams[prop].map(param => { + return wiredValue.update.bind(wiredValue, param); + }); + + set.push(...boundUpdate); + }); + }); + return map; +} + +/** + * Installs property setters to listen for changes to property-based params. + * @param {Object} cmp The component. + * @param {Object} propsToListeners Map of prop names to change handler functions. + */ +export function installSetterOverrides(cmp, propsToListeners) { + const props = Object.keys(propsToListeners); + + // do not modify cmp if not required + if (props.length === 0) { + return; + } + + props.forEach(prop => { + const newDescriptor = getOverrideDescriptor(cmp, prop, propsToListeners[prop]); + Object.defineProperty(cmp, prop, newDescriptor); + }); +} + +/** + * Gets a property descriptor that monitors the provided property for changes. + * @param {Object} cmp The component. + * @param {String} prop The name of the property to be monitored. + * @param {Function[]} listeners List of functions to invoke when the prop's value changes. + * @return {Object} A property descriptor. + */ +export function getOverrideDescriptor(cmp, prop, listeners) { + const originalDescriptor = Object.getOwnPropertyDescriptor(cmp.constructor.prototype, prop); + + let newDescriptor; + if (originalDescriptor) { + newDescriptor = Object.assign({}, originalDescriptor, { + set(value) { + originalDescriptor.set.call(cmp, value); + // re-fetch the value to handle asymmetry between setter and getter values + listeners.forEach(f => f(cmp[prop])); + } + }); + } else { + const propSymbol = Symbol(prop); + newDescriptor = { + get() { + return cmp[propSymbol]; + }, + set(value) { + cmp[propSymbol] = value; + listeners.forEach(f => f(value)); + } + }; + // grab the existing value + cmp[propSymbol] = cmp[prop]; + } + return newDescriptor; +} + +/** + * Installs the WiredValue instances onto the component. + * @param {Object} wiredValues The WiredValue instances. + */ +export function installWiredValues(wiredValues) { + Object.keys(wiredValues).forEach(wiredTarget => { + const wiredValue = wiredValues[wiredTarget]; + wiredValue.install(); + }); +} + +/** + * Installs wiring for a component. + */ +export function installWiring(cmp:Element, def) { + const wireConfigs = getWireConfigs(def); + const wiredValues = getWiredValues(wireConfigs, cmp); + const propChangeHandlers = getPropChangeHandlers(wireConfigs, wiredValues); + installSetterOverrides(cmp, propChangeHandlers); + // TODO - handle when config values have only defaults and thus don't trigger setter overrides + installWiredValues(wiredValues); + + return wiredValues; +} From 8b1dfc2577f6e64a1ef427ca693da7a68af13902 Mon Sep 17 00:00:00 2001 From: Kevin Venkiteswaran Date: Thu, 15 Mar 2018 21:40:45 -0700 Subject: [PATCH 02/52] - Fix wire adapter callback names - Add todo-api with getTodo (imperative) and wire adapter - Update single-wire demo to use new getTodo - Update build steps to work with typescript - Kill old code Outstanding: - analyze def wires to know which adapters depend on which vars - ctr the adapters, setup their wireUpdated callback --- packages/lwc-wire-service/playground/app.js | 4 +- .../playground/x/single-wire/single-wire.js | 3 +- .../playground/x/todo-api/todo-api.js | 45 ++++ .../playground/x/todo-api/todo.js | 47 ++++ .../x/{wire-adapter => todo-api}/util.js | 0 .../todo-legacy-api.js} | 0 .../{wire-adapter => todo-legacy-api}/todo.js | 0 .../playground/x/todo-legacy-api/util.js | 63 +++++ .../rollup.config.es-and-cjs.js | 4 +- .../lwc-wire-service/rollup.config.umd.dev.js | 4 +- packages/lwc-wire-service/src/index.ts | 24 +- packages/lwc-wire-service/src/main.js | 45 ---- .../lwc-wire-service/src/wire-adapters.js | 33 --- packages/lwc-wire-service/src/wire-service.js | 243 ------------------ packages/lwc-wire-service/src/wired-value.js | 169 ------------ 15 files changed, 177 insertions(+), 507 deletions(-) create mode 100644 packages/lwc-wire-service/playground/x/todo-api/todo-api.js create mode 100644 packages/lwc-wire-service/playground/x/todo-api/todo.js rename packages/lwc-wire-service/playground/x/{wire-adapter => todo-api}/util.js (100%) rename packages/lwc-wire-service/playground/x/{wire-adapter/wire-adapter.js => todo-legacy-api/todo-legacy-api.js} (100%) rename packages/lwc-wire-service/playground/x/{wire-adapter => todo-legacy-api}/todo.js (100%) create mode 100644 packages/lwc-wire-service/playground/x/todo-legacy-api/util.js delete mode 100644 packages/lwc-wire-service/src/main.js delete mode 100644 packages/lwc-wire-service/src/wire-adapters.js delete mode 100644 packages/lwc-wire-service/src/wire-service.js delete mode 100644 packages/lwc-wire-service/src/wired-value.js diff --git a/packages/lwc-wire-service/playground/app.js b/packages/lwc-wire-service/playground/app.js index f5c1f7c9e5..4361316642 100644 --- a/packages/lwc-wire-service/playground/app.js +++ b/packages/lwc-wire-service/playground/app.js @@ -1,10 +1,10 @@ // bootstrapping process for App import { createElement, register } from 'engine'; +import { registerWireService } from 'wire-service'; import App from 'x-demo'; -import registerPlaygroundWireAdapters from 'x-wire-adapter'; -registerPlaygroundWireAdapters({register}); +registerWireService(register) const container = document.getElementById('main'); const element = createElement('x-demo', { is: App }); diff --git a/packages/lwc-wire-service/playground/x/single-wire/single-wire.js b/packages/lwc-wire-service/playground/x/single-wire/single-wire.js index d61dea6540..61a144460c 100644 --- a/packages/lwc-wire-service/playground/x/single-wire/single-wire.js +++ b/packages/lwc-wire-service/playground/x/single-wire/single-wire.js @@ -1,9 +1,10 @@ import { Element, api, wire } from 'engine'; +import { getTodo } from 'todo-api'; export default class SingleWire extends Element { @api todoId; - @wire('todo', { id: '$todoId' }) + @wire(getTodo, { id: '$todoId' }) todo; get error() { diff --git a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js new file mode 100644 index 0000000000..e03945826f --- /dev/null +++ b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js @@ -0,0 +1,45 @@ +/** + * Todo imperative APIs and wire adapters. + */ + +import register from 'wire-service'; +import getObservable from './todo'; + +// Component-importable imperative access. +export function getTodo(config) { + return new Promise((resolve, reject) => { + const observable = getObservable(config); + if (!observable) { + return reject(new Error('insufficient config')); + } + + observable.subscribe({ + next: value => resolve(value), + error: error => reject(error), + complete: resolve + }); + }); +} + +// Register the wire adapter for @wire(getTodo). +register(getTodo, function getTodoWireAdapter(targetSetter) { + let subscription; + let config; + return { + updatedCallback: (newConfig) => { + config = newConfig; + }, + + connectedCallback: () => { + // Subscribe to stream. + subscription = getObservable(config).subscribe({ + next: data => targetSetter({ data, error: undefined }), + error: error => targetSetter({ data: undefined, error }) + }); + }, + + disconnectedCallback: () => { + subscription.unsubscribe(); + } + }; +}); diff --git a/packages/lwc-wire-service/playground/x/todo-api/todo.js b/packages/lwc-wire-service/playground/x/todo-api/todo.js new file mode 100644 index 0000000000..c39ccb5dfe --- /dev/null +++ b/packages/lwc-wire-service/playground/x/todo-api/todo.js @@ -0,0 +1,47 @@ +/** + * @wire adapter for todo data. + */ +import { getSubject, getImmutableObservable } from './util'; + +function generateTodo(id, completed) { + return { + id, + title: 'task ' + id, + completed + }; +} + +// the data +const TODO = [ + generateTodo(0, true), + generateTodo(1, false), + // intentionally skip 2 + generateTodo(3, true), + generateTodo(4, true), + // intentionally skip 5 + generateTodo(6, false), + generateTodo(7, false) +].reduce((acc, value) => { + acc[value.id] = value; + return acc; +}, {}); + + +/** + * Services @wire('todo') requests. + * @param {Object} config Service config bag. + * @return {Observable} An observable for the recordUis. + */ +export default function getObservable(config) { + if (!('id' in config)) { + return undefined; + } + + const todo = TODO[config.id]; + if (!todo) { + const subject = getSubject(undefined, { message: 'Todo not found' }); + return subject.observable; + } + + return getImmutableObservable(todo); +} diff --git a/packages/lwc-wire-service/playground/x/wire-adapter/util.js b/packages/lwc-wire-service/playground/x/todo-api/util.js similarity index 100% rename from packages/lwc-wire-service/playground/x/wire-adapter/util.js rename to packages/lwc-wire-service/playground/x/todo-api/util.js diff --git a/packages/lwc-wire-service/playground/x/wire-adapter/wire-adapter.js b/packages/lwc-wire-service/playground/x/todo-legacy-api/todo-legacy-api.js similarity index 100% rename from packages/lwc-wire-service/playground/x/wire-adapter/wire-adapter.js rename to packages/lwc-wire-service/playground/x/todo-legacy-api/todo-legacy-api.js diff --git a/packages/lwc-wire-service/playground/x/wire-adapter/todo.js b/packages/lwc-wire-service/playground/x/todo-legacy-api/todo.js similarity index 100% rename from packages/lwc-wire-service/playground/x/wire-adapter/todo.js rename to packages/lwc-wire-service/playground/x/todo-legacy-api/todo.js diff --git a/packages/lwc-wire-service/playground/x/todo-legacy-api/util.js b/packages/lwc-wire-service/playground/x/todo-legacy-api/util.js new file mode 100644 index 0000000000..285be193dc --- /dev/null +++ b/packages/lwc-wire-service/playground/x/todo-legacy-api/util.js @@ -0,0 +1,63 @@ +// returns a read-only version via a proxy +export function getImmutable(obj) { + const handler = { + get: (target, key) => { + const value = target[key]; + if (value && typeof value === 'object') { + return getImmutable(value); + } + return value; + }, + set: () => { + return false; + }, + deleteProperty: () => { + return false; + } + }; + return new Proxy(obj, handler); +} + +// wraps a value in an observable that emits one immutable value +export function getImmutableObservable(value) { + return getSubject(getImmutable(value)).observable; +} + +// gets a subject with optional initial value and initial error +export function getSubject(initialValue, initialError) { + let observer; + + function next(value) { + observer.next(value); + } + + function error(err) { + observer.error(err); + } + + function complete() { + observer.complete(); + } + + const observable = { + subscribe: (obs) => { + observer = obs; + if (initialValue) { + next(initialValue); + } + if (initialError) { + error(initialError); + } + return { + unsubscribe: () => {} + }; + } + }; + + return { + next, + error, + complete, + observable + }; +} diff --git a/packages/lwc-wire-service/rollup.config.es-and-cjs.js b/packages/lwc-wire-service/rollup.config.es-and-cjs.js index 23c3c197d0..1f4e18c3ae 100644 --- a/packages/lwc-wire-service/rollup.config.es-and-cjs.js +++ b/packages/lwc-wire-service/rollup.config.es-and-cjs.js @@ -2,10 +2,11 @@ const path = require('path'); const rollupCompatPlugin = require('rollup-plugin-compat').default; +const typescript = require('rollup-plugin-typescript'); const { version } = require('./package.json'); const { generateTargetName } = require('./rollup.config.util'); -const entry = path.resolve(__dirname, 'src/main.js'); +const entry = path.resolve(__dirname, 'src/index.ts'); const commonJSDirectory = path.resolve(__dirname, 'dist/commonjs'); const modulesDirectory = path.resolve(__dirname, 'dist/modules'); @@ -17,6 +18,7 @@ function rollupConfig(config) { const isCompat = target === 'es5'; let plugins = [ + typescript({ target: target, typescript: require('typescript') }), isCompat && rollupCompatPlugin({ polyfills: false, disableProxyTransform: true }), ].filter(Boolean); diff --git a/packages/lwc-wire-service/rollup.config.umd.dev.js b/packages/lwc-wire-service/rollup.config.umd.dev.js index 38045b53b2..f0dd8495c5 100644 --- a/packages/lwc-wire-service/rollup.config.umd.dev.js +++ b/packages/lwc-wire-service/rollup.config.umd.dev.js @@ -2,11 +2,12 @@ const path = require('path'); const rollupReplacePlugin = require('rollup-plugin-replace'); +const typescript = require('rollup-plugin-typescript'); const rollupCompatPlugin = require('rollup-plugin-compat').default; const { version } = require('./package.json'); const { generateTargetName } = require('./rollup.config.util'); -const input = path.resolve(__dirname, 'src/main.js'); +const input = path.resolve(__dirname, 'src/index.ts'); const outputDir = path.resolve(__dirname, 'dist/umd'); const banner = (`/**\n * Copyright (C) 2017 salesforce.com, inc.\n */`); @@ -17,6 +18,7 @@ function rollupConfig(config) { const isCompat = target === 'es5'; const plugins = [ + typescript({ target: target, typescript: require('typescript') }), rollupReplacePlugin({ 'process.env.NODE_ENV': JSON.stringify('development') }), isCompat && rollupCompatPlugin({ polyfills: false, disableProxyTransform: true }), ].filter(Boolean); diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 3d3aa5484a..673cd329d9 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -16,16 +16,16 @@ export interface WiredValue { export type TargetSetter = (WiredValue) => void; export type UpdatedCallback = (object) => void; export type NoArgumentCallback = () => void; -export type WireAdapterDefCallback = UpdatedCallback | NoArgumentCallback; +export type WireAdapterCallback = UpdatedCallback | NoArgumentCallback; export interface WireAdapter { - updated?: UpdatedCallback; - connected?: NoArgumentCallback; - disconnected?: NoArgumentCallback; + updatedCallback?: UpdatedCallback; + connectedCallback?: NoArgumentCallback; + disconnectedCallback?: NoArgumentCallback; }; export type WireAdapterFactory = (targetSetter: TargetSetter) => WireAdapter; // lifecycle hooks of wire adapters -const HOOKS: Array = ['updated', 'connected', 'disconnected']; +const HOOKS: Array = ['updatedCallback', 'connectedCallback', 'disconnectedCallback']; // wire adapters: wire adapter id => adapter ctor const ADAPTERS: Map = new Map(); @@ -36,7 +36,7 @@ const CONTEXT_ID: string = '@wire'; /** * Invokes the specified callbacks with specified arguments. */ -function invokeCallback(callbacks: WireAdapterDefCallback, arg: object|undefined) { +function invokeCallback(callbacks: WireAdapterCallback[], arg: object|undefined) { for (let i = 0, len = callbacks.length; i < len; ++i) { callbacks[i].apply(undefined, arg); } @@ -58,7 +58,7 @@ const wireService = { let contextData = Object.create(null); for (let i = 0; i < HOOKS.length; i++) { let hook = HOOKS[i]; - let callbacks: Array = []; + let callbacks: Array = []; for (let j = 0; j < adapters.length; j++) { let callback = adapters[j][hook]; if (callback) { @@ -73,8 +73,8 @@ const wireService = { }, connected: (cmp: Element, data: object, def: ElementDef, context: object) => { - let callbacks; - if (!def.wire || (callbacks = context[CONTEXT_ID]['connected']) ) { + let callbacks : WireAdapterCallback[]; + if (!def.wire || !(callbacks = context[CONTEXT_ID]['connectedCallback'])) { return; } invokeCallback(callbacks, undefined); @@ -82,7 +82,7 @@ const wireService = { disconnected: (cmp: Element, data: object, def: ElementDef, context: object) => { let callbacks; - if (!def.wire || (callbacks = context[CONTEXT_ID]['disconnected']) ) { + if (!def.wire || (callbacks = context[CONTEXT_ID]['disconnectedCallback']) ) { return; } invokeCallback(callbacks, undefined); @@ -93,8 +93,8 @@ const wireService = { /** * Registers the wire service. */ -export function registerWireService(engine: any) { - engine.register(wireService); +export function registerWireService(register: Function) { + register(wireService); } /** diff --git a/packages/lwc-wire-service/src/main.js b/packages/lwc-wire-service/src/main.js deleted file mode 100644 index 9188b24304..0000000000 --- a/packages/lwc-wire-service/src/main.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * The @wire service. - * - * Provides data binding between wire adapters and LWC components decorated with @wire. - * This service is Salesforce-agnostic. The wire adapters must be provided to this module. - */ - -import { setWireAdapters, installWiring } from './wire-service'; - -/** - * The wire service. - */ -const wireService = { - // TODO W-4072588 - support connected + disconnected (repeated) cycles - wiring: (cmp, data, def, context) => { - // engine guarantees invocation only if def.wire is defined - const wiredValues = installWiring(cmp, def); - context['@wire'] = wiredValues; - }, - disconnected: (cmp, data, def, context) => { - if (!def.wire) { - return; - } - const wiredValues = context['@wire']; - if (!wiredValues) { - return; - } - Object.keys(wiredValues).forEach(wireTarget => { - const wiredValue = wiredValues[wireTarget]; - wiredValue.release(); - }); - } -}; - -/** - * Registers the wire service with the provided wire adapters. - * @param {Function} register Function to register an engine service. - * @param {...Function} adapterGenerators Wire adapter generators. Each function - * must return an object whose keys are adapter ids, values are the adapter - * function. - */ -export default function registerWireService(register, ...adapterGenerators) { - setWireAdapters(adapterGenerators); - register(wireService); -} diff --git a/packages/lwc-wire-service/src/wire-adapters.js b/packages/lwc-wire-service/src/wire-adapters.js deleted file mode 100644 index 0a8fa411c4..0000000000 --- a/packages/lwc-wire-service/src/wire-adapters.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * This module provides utilities for managing wire adapters. - */ - -/** - * Populates a target map from a source object. - * @param {Map} target The map to populate. - * @param {Object} source The source of key-values to populate. - */ -function populateMap(target, source) { - Object.keys(source).forEach(key => { - const value = source[key]; - if (typeof value !== 'function') { - throw new Error(`Invalid wire adapter: value must be a function, found ${typeof value}.`); - } else if (target.has(key)) { - throw new Error(`Duplicate wire adapter id ${key}'.`); - } - target.set(key, value); - }); -} - -/** - * Returns a map of wire adapter id to handler. - * @param {Array>} adapters The wire adapters. - * @return {Map} A map of wire adapter id to handler. - */ -export function buildWireAdapterMap(adapters) { - const map = new Map(); - adapters.forEach(a => { - populateMap(map, a()); - }); - return map; -} diff --git a/packages/lwc-wire-service/src/wire-service.js b/packages/lwc-wire-service/src/wire-service.js deleted file mode 100644 index 3141415aba..0000000000 --- a/packages/lwc-wire-service/src/wire-service.js +++ /dev/null @@ -1,243 +0,0 @@ -/** - * The @wire service. - * - * Provides data binding between services and LWC modules decorated with @wire. - * This service is Salesforce-agnostic. The data-providing services must be provided - * to this module. - */ - -import { WiredValue } from './wired-value'; -import { buildWireAdapterMap } from './wire-adapters'; - - -/** Map of wire adapter id to handler */ -let ADAPTERS; - - -/** - * Gets a mapping of component prop to wire config dynamic params. In other words, - * the wire config's parameter set that updates whenever a prop changes. - * @param {*} wireDef The wire definition. - * @param {String} wireTarget Component property that is the target of the wire. - * @returns {Object} Map of prop name to wire config dynamic params. - */ -export function getPropToParams(wireDef, wireTarget) { - const map = Object.create(null); - const { params } = wireDef; - if (params) { - Object.keys(params).forEach(param => { - const prop = params[param]; - - if (param in wireDef.static) { - throw new Error(`${wireTarget}'s @wire('${wireDef.type}') parameter ${param} specified multiple times.`); - } - - // attribute change handlers use hyphenated case - let set = map[prop]; - if (!set) { - set = map[prop] = []; - } - set.push(param); - }); - } - return map; -} - -/** - * Gets the wire adapter for a wire. - * @param {*} wireDef The wire definition - * @param {String} wireTarget Component property that is the target of the wire. - * @returns {Function} The wire adapter. - */ -export function getAdapter(wireDef, wireTarget) { - let adapter; - // TODO: deprecate type once consumers have migrate to use function identifier for adapter id. - if (wireDef.type) { - adapter = ADAPTERS.get(wireDef.type); - if (!adapter) { - throw new Error(`Unknown wire adapter id '${wireDef.type}' in ${wireTarget}'s @wire('${wireDef.type}', ...)`); - } - } else if (wireDef.adapter) { - adapter = ADAPTERS.get(wireDef.adapter.name); - if (!adapter) { - throw new Error(`Unknown wire adapter id '${wireDef.adapter.name}' in ${wireTarget}'s @wire(${wireDef.adapter.name}, ...)`); - } - } - - return adapter; -} - -/** - * Gets the non-dynamic wire config. - * @param {*} wireDef The wire definition - * @returns {Object} The non-dynamic portions of the wire config. - */ -export function getStaticConfig(wireDef) { - const config = Object.create(null); - Object.assign(config, wireDef.static); - return config; -} - -/** - * Gets whether the wiring is to a method or property. - * @param {*} wireDef The wire definition - * @returns {Boolean} True if wired to a method, false otherwise. - */ -export function getIsMethod(wireDef) { - return wireDef.method === 1; -} - -/** - * Gets the config bags for all wires on a component. - * @param {*} def The component definition. - * @return {Object} Map of wire configs. - */ -export function getWireConfigs(def) { - const wires = def.wire; - const wireConfigs = Object.create(null); - Object.keys(wires).forEach(wireTarget => { - const wireDef = wires[wireTarget]; - const propToParams = getPropToParams(wireDef, wireTarget); - const adapter = getAdapter(wireDef, wireTarget); - const staticConfig = getStaticConfig(wireDef); - const isMethod = getIsMethod(wireDef); - wireConfigs[wireTarget] = { propToParams, adapter, staticConfig, isMethod }; - }); - return wireConfigs; -} - -/** - * Gets the WiredValue instances for the wire config bags. - * @param {*} wireConfigs The wire config bags. - * @param {*} cmp The component whose wiring is being created. - * @returns {Object} The WiredValue instances. - */ -export function getWiredValues(wireConfigs, cmp) { - const wiredValues = Object.create(null); - Object.keys(wireConfigs).forEach(wireTarget => { - const { adapter, staticConfig, isMethod } = wireConfigs[wireTarget]; - wiredValues[wireTarget] = new WiredValue(adapter, staticConfig, isMethod, cmp, wireTarget); - }); - return wiredValues; -} - -/** - * Gets the WiredValue handlers for property changes. - * @param {*} wireConfigs The wire config bags. - * @param {Object} wiredValues The WiredValue instances. - * @return {Object} Map of props to change handlers. - */ -export function getPropChangeHandlers(wireConfigs, wiredValues) { - const map = Object.create(null); - Object.keys(wireConfigs).forEach(wireTarget => { - const { propToParams } = wireConfigs[wireTarget]; - const wiredValue = wiredValues[wireTarget]; - - Object.keys(propToParams).forEach(prop => { - let set = map[prop]; - if (!set) { - set = map[prop] = []; - } - - const boundUpdate = propToParams[prop].map(param => { - return wiredValue.update.bind(wiredValue, param); - }); - - set.push(...boundUpdate); - }); - }); - return map; -} - -/** - * Installs property setters to listen for changes to property-based params. - * @param {Object} cmp The component. - * @param {Object} propsToListeners Map of prop names to change handler functions. - */ -export function installSetterOverrides(cmp, propsToListeners) { - const props = Object.keys(propsToListeners); - - // do not modify cmp if not required - if (props.length === 0) { - return; - } - - props.forEach(prop => { - const newDescriptor = getOverrideDescriptor(cmp, prop, propsToListeners[prop]); - Object.defineProperty(cmp, prop, newDescriptor); - }); -} - -/** - * Gets a property descriptor that monitors the provided property for changes. - * @param {Object} cmp The component. - * @param {String} prop The name of the property to be monitored. - * @param {Function[]} listeners List of functions to invoke when the prop's value changes. - * @return {Object} A property descriptor. - */ -export function getOverrideDescriptor(cmp, prop, listeners) { - const originalDescriptor = Object.getOwnPropertyDescriptor(cmp.constructor.prototype, prop); - - let newDescriptor; - if (originalDescriptor) { - newDescriptor = Object.assign({}, originalDescriptor, { - set(value) { - originalDescriptor.set.call(cmp, value); - // re-fetch the value to handle asymmetry between setter and getter values - listeners.forEach(f => f(cmp[prop])); - } - }); - } else { - const propSymbol = Symbol(prop); - newDescriptor = { - get() { - return cmp[propSymbol]; - }, - set(value) { - cmp[propSymbol] = value; - listeners.forEach(f => f(value)); - } - }; - // grab the existing value - cmp[propSymbol] = cmp[prop]; - } - return newDescriptor; -} - -/** - * Installs the WiredValue instances onto the component. - * @param {Object} wiredValues The WiredValue instances. - */ -export function installWiredValues(wiredValues) { - Object.keys(wiredValues).forEach(wiredTarget => { - const wiredValue = wiredValues[wiredTarget]; - wiredValue.install(); - }); -} - -/** - * Installs wiring for a component. - * @param {*} cmp The component to wire. - * @param {*} def The component's definition. - * @returns {Object} The installed WiredValues. - */ -export function installWiring(cmp, def) { - const wireConfigs = getWireConfigs(def); - const wiredValues = getWiredValues(wireConfigs, cmp); - const propChangeHandlers = getPropChangeHandlers(wireConfigs, wiredValues); - installSetterOverrides(cmp, propChangeHandlers); - // TODO - handle when config values have only defaults and thus don't trigger setter overrides - installWiredValues(wiredValues); - - return wiredValues; -} - -/** - * Sets the wire adapters. - * @param {Function[]} adapterGenerators Wire adapter generators. Each function - * must return an object whose keys are adapter ids, values are the adapter - * function. - */ -export function setWireAdapters(adapterGenerators) { - ADAPTERS = buildWireAdapterMap(adapterGenerators); -} diff --git a/packages/lwc-wire-service/src/wired-value.js b/packages/lwc-wire-service/src/wired-value.js deleted file mode 100644 index 7e5030c6b8..0000000000 --- a/packages/lwc-wire-service/src/wired-value.js +++ /dev/null @@ -1,169 +0,0 @@ -/** Maximum number of wire adapter provides after observable complete */ -const MAX_PROVIDE_AFTER_COMPLETE = 1; - -/** - * A wired value. - */ -export class WiredValue { - /** - * Constructor - * @param {Function} adapter The adapter that provides the data. - * @param {Object} config Configuration for the adapter. - * @param {Boolean} isMethod True if wiring to a method, false otherwise. - * @param {*} cmp The component to which this value is wired. - * @param {String} propName Property on the component to which this value is wired. - */ - constructor(adapter, config, isMethod, cmp, propName) { - this.adapter = adapter; - this.config = config; - this.cmp = cmp; - this.propName = propName; - this.isMethod = isMethod; - - // subscription to wire adapter's observable - this.subscription = undefined; - - // count of wire adapter provides caused by receiving observable complete - this.completeHandled = 0; - - // debounce multiple param updates so adapter is invoked only once. - // use promise's microtask semantics. - this.providePromise = undefined; - } - - /** - * Updates a configuration value. - * @param {String} param Configuraton parameter. - * @param {Object} value New configuration value. - */ - update(param, value) { - // invariant: wired value doesn't change if params don't change - if (this.config[param] === value) { - return; - } - - // disconnect from previous observable - this.release(); - - this.config[param] = value; - this.provide(); - } - - /** - * Queues a request for the adapter to provide a new value. - */ - provide() { - if (!this.providePromise) { - this.providePromise = Promise.resolve().then(() => this._provide()); - } - } - - /** - * Installs the WiredValue onto the target component. - */ - install() { - if (!this.isMethod) { - this.cmp[this.propName] = { - data: undefined, - error: undefined - }; - } - this._provide(); - } - - /** - * Provides a new value from the adapter. - */ - _provide() { - this.providePromise = undefined; - - const observable = this.adapter(this.config); - // adapter returns falsey if config is insufficient - if (!observable) { - return; - } - - const observer = this.getObserver(); - this.subscription = observable.subscribe(observer); - } - - /** - * Handles observable's complete signal. - * - * After an existing observable emits complete, conditionally re-request the - * adapter to provide a new value. The conditions prevent a storm against the - * adapter by limiting loops of provide, subscribe, complete, provide, repeat. - */ - completeHandler() { - this.release(); - - this.completeHandled++; - if (this.completeHandled > MAX_PROVIDE_AFTER_COMPLETE) { - // TODO #15 - add telemetry so this occurrence is sent to the server - return; - } - - this.provide(); - } - - - /** - * Gets an observer for the adapter's observable. - * @returns {Object} observer. - */ - getObserver() { - if (!this.observer) { - if (this.isMethod) { - const wireMethod = this.cmp[this.propName]; - this.observer = { - next: value => { - // TODO: deprecate (error, data) args - if (wireMethod.length === 2) { - // eslint-disable-next-line no-console - console.warn('[DEPRECATE] @wire function no longer supports two arguments (error, data), please update your code to use ({error, data}) instead.'); - wireMethod.call(this.cmp, null, value); - } else { - wireMethod.call(this.cmp, { data: value, error: null }); - } - }, - error: err => { - // TODO: deprecate (error, data) args - if (wireMethod.length === 2) { - // eslint-disable-next-line no-console - console.warn('[DEPRECATE] @wire function no longer supports two arguments (error, data), please update your code to use ({error, data}) instead.'); - wireMethod.call(this.cmp, err, undefined); - } else { - wireMethod.call(this.cmp, { data: undefined, error: err }); - } - }, - complete: () => { - this.completeHandler(); - } - }; - } else { - this.observer = { - next: value => { - this.cmp[this.propName] = { 'data': value, 'error': undefined }; - }, - error: err => { - this.cmp[this.propName] = { 'data': undefined, 'error': err }; - }, - complete: () => { - this.completeHandler(); - } - }; - } - } - return this.observer; - } - - /** - * Release all resources. - */ - release() { - if (this.subscription) { - this.subscription.unsubscribe(); - this.subscription = undefined; - } - } -} From 9000634c6656c3439cf17303ad3a4eb59433f173 Mon Sep 17 00:00:00 2001 From: Kevin Venkiteswaran Date: Thu, 15 Mar 2018 23:40:57 -0700 Subject: [PATCH 03/52] - Add logic for prop change -> wire adapter updatedCallback - More structure and types --- packages/lwc-wire-service/src/index.ts | 161 ++++++++++++++---- packages/lwc-wire-service/src/shared-types.ts | 12 +- 2 files changed, 135 insertions(+), 38 deletions(-) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 673cd329d9..61e2068de0 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -24,29 +24,123 @@ export interface WireAdapter { }; export type WireAdapterFactory = (targetSetter: TargetSetter) => WireAdapter; -// lifecycle hooks of wire adapters -const HOOKS: Array = ['updatedCallback', 'connectedCallback', 'disconnectedCallback']; +export interface UpdatedCallbackConfig { + updatedCallback: UpdatedCallback; + statics: { + [key: string]: any; + }; + params: { + [key: string]: string; + }; +} +export interface ServiceUpdateContext { + callbacks: UpdatedCallbackConfig[]; + // union of callbacks.params values + paramValues: string[]; +} +export type ServiceContext = NoArgumentCallback[] | ServiceUpdateContext; -// wire adapters: wire adapter id => adapter ctor -const ADAPTERS: Map = new Map(); +// lifecycle hooks of wire adapters +// const HOOKS: Array = ['updatedCallback', 'connectedCallback', 'disconnectedCallback']; // key for engine service context store const CONTEXT_ID: string = '@wire'; +// wire adapters: wire adapter id => adapter ctor +const adapters: Map = new Map(); + /** - * Invokes the specified callbacks with specified arguments. + * Invokes the specified callbacks. */ -function invokeCallback(callbacks: WireAdapterCallback[], arg: object|undefined) { +function invokeCallback(callbacks: NoArgumentCallback[]) { for (let i = 0, len = callbacks.length; i < len; ++i) { - callbacks[i].apply(undefined, arg); + callbacks[i].call(undefined); + } +} + +/** + * Invokes the provided updated callbacks with the resolved component properties. + */ +function invokeUpdatedCallback(ucMetadatas: UpdatedCallbackConfig[], paramValues: any) { + for (let i = 0, len = ucMetadatas.length; i < len; ++i) { + const { updatedCallback, statics, params } = ucMetadatas[i]; + + const resolvedParams = Object.create(null); + const keys = Object.keys(params); + for (let j = 0, jlen = keys.length; j < jlen; j++) { + const key = keys[j]; + const value = paramValues[params[key]]; + resolvedParams[key] = value; + } + const config = Object.assign(Object.create(null), statics, resolvedParams); + updatedCallback.call(undefined, config); } } +/** + * Gets resolved values of the specified properties. + */ +function getPropertyValues(cmp: Element, properties: string[]) { + const resolvedValues = Object.create(null); + for (let i = 0, len = properties.length; i < len; ++i) { + const paramValue = properties[i]; + resolvedValues[paramValue] = cmp[paramValue]; + } + return resolvedValues; +} + +/** + * Build context payload. + */ +function buildContext(adapters: WireAdapter[], ) { + let context: Map = Object.create(null); + + const noArgCallbacks: Array = ['connectedCallback', 'disconnectedCallback']; + for (let i = 0; i < noArgCallbacks.length; i++) { + const noArgCallback = noArgCallbacks[i]; + // TODO - this is really Array + const callbacks: Array = []; + for (let j = 0; j < adapters.length; j++) { + let callback = adapters[j][noArgCallback]; + if (callback) { + callbacks.push(callback); + } + } + if (callbacks.length > 0) { + context[noArgCallback] = callbacks; + } + } + + const callbacks: Array = []; + const paramValues: string[] = []; + for (let j = 0; j < adapters.length; j++) { + let callback = adapters[j]['updatedCallback']; + if (callback) { + // TODO - extract statics and params from the wire def + callbacks.push({ + updatedCallback: callback, + statics: {}, + params: {} + }); + } + } + if (callbacks.length > 0) { + const ucContext: ServiceUpdateContext = { + callbacks, + paramValues + } + context['updatedCallback'] = ucContext; + } + + return context; + +} + /** * The wire service. * - * This is registered service with the engine's service API. It delegates - * lifecycle callbacks to relevant wire adapter lifecycle callbacks. + * This service is registered with the engine's service API. It connects service + * callbacks to wire adapter lifecycle callbacks. */ const wireService = { // TODO W-4072588 - support connected + disconnected (repeated) cycles @@ -54,42 +148,45 @@ const wireService = { // engine guarantees invocation only if def.wire is defined const adapters = installWireAdapters(cmp, def); - // collect all adapters' callbacks into arrays for future invocation - let contextData = Object.create(null); - for (let i = 0; i < HOOKS.length; i++) { - let hook = HOOKS[i]; - let callbacks: Array = []; - for (let j = 0; j < adapters.length; j++) { - let callback = adapters[j][hook]; - if (callback) { - callbacks.push(callback); - } - } - if (callbacks.length > 0) { - contextData[hook] = callbacks; - } + // cache context that optimizes runtime of service callbacks + context[CONTEXT_ID] = buildContext(adapters); + }, + + // TODO - in early 216, engine will expose an updated callback for services that + // is invoked whenever a tracked property is changed. wire service is structured to + // make this adoption trivial. + updated: (cmp: Element, data: object, def: ElementDef, context: object) => { + let ucMetadata : ServiceUpdateContext; + if (!def.wire || !(ucMetadata = context[CONTEXT_ID]['updated'])) { + return; } - context[CONTEXT_ID] = contextData; + // get new values for all dynamic props + const paramValues = getPropertyValues(cmp, ucMetadata.paramValues); + + // compare new to old dynamic prop values, updating old props with new values + // for each change, queue the impacted adapter(s) + + // process queue of impacted adapters + invokeUpdatedCallback(ucMetadata.callbacks, paramValues); }, connected: (cmp: Element, data: object, def: ElementDef, context: object) => { - let callbacks : WireAdapterCallback[]; - if (!def.wire || !(callbacks = context[CONTEXT_ID]['connectedCallback'])) { + let callbacks : NoArgumentCallback[]; + if (!def.wire || !(callbacks = context[CONTEXT_ID]['connected'])) { return; } - invokeCallback(callbacks, undefined); + invokeCallback(callbacks); }, disconnected: (cmp: Element, data: object, def: ElementDef, context: object) => { - let callbacks; - if (!def.wire || (callbacks = context[CONTEXT_ID]['disconnectedCallback']) ) { + let callbacks : NoArgumentCallback[]; + if (!def.wire || (callbacks = context[CONTEXT_ID]['disconnected']) ) { return; } - invokeCallback(callbacks, undefined); + invokeCallback(callbacks); } }; - /** * Registers the wire service. */ @@ -103,5 +200,5 @@ export function registerWireService(register: Function) { export function register(adapterId: any, adapterFactory: WireAdapterFactory) { assert.isTrue(adapterId, 'adapter id must be truthy'); assert.isTrue(typeof adapterFactory === 'function', 'adapter factory must be a function'); - ADAPTERS.set(adapterId, adapterFactory); + adapters.set(adapterId, adapterFactory); }; diff --git a/packages/lwc-wire-service/src/shared-types.ts b/packages/lwc-wire-service/src/shared-types.ts index 73181cf543..749a00f9a6 100644 --- a/packages/lwc-wire-service/src/shared-types.ts +++ b/packages/lwc-wire-service/src/shared-types.ts @@ -1,12 +1,12 @@ export interface WireDef { params: { - [key: string]: string - }, + [key: string]: string; + }; static: { - [key: string]: any - }, - type: string, - method?: 1 + [key: string]: any; + }; + type: string; + method?: 1; } export interface ElementDef { wire: { // TODO - wire is optional but all wire service code assumes it's present From f5ae6e9e1b6e8336baba1c3facc86a54bbc06dd5 Mon Sep 17 00:00:00 2001 From: Kevin Venkiteswaran Date: Fri, 16 Mar 2018 08:58:54 -0700 Subject: [PATCH 04/52] WIP - working on updated() callback hookup --- packages/lwc-wire-service/src/index.ts | 57 ++++++++++++------- packages/lwc-wire-service/src/shared-types.ts | 3 +- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 61e2068de0..db9e585d0d 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -47,7 +47,7 @@ export type ServiceContext = NoArgumentCallback[] | ServiceUpdateContext; const CONTEXT_ID: string = '@wire'; // wire adapters: wire adapter id => adapter ctor -const adapters: Map = new Map(); +const adapterFactories: Map = new Map(); /** * Invokes the specified callbacks. @@ -92,7 +92,7 @@ function getPropertyValues(cmp: Element, properties: string[]) { /** * Build context payload. */ -function buildContext(adapters: WireAdapter[], ) { +function buildContext(adapters: WireAdapter[], wiredefs: any) { let context: Map = Object.create(null); const noArgCallbacks: Array = ['connectedCallback', 'disconnectedCallback']; @@ -136,6 +136,24 @@ function buildContext(adapters: WireAdapter[], ) { } +// TODO - in early 216, engine will expose an `updated` callback for services that +// is invoked whenever a tracked property is changed. wire service is structured to +// make this adoption trivial. +function updated(context: object, cmp: Element, def: ElementDef) { + let ucMetadata : ServiceUpdateContext; + if (!def.wire || !(ucMetadata = context[CONTEXT_ID]['updated'])) { + return; + } + // get new values for all dynamic props + const paramValues = getPropertyValues(cmp, ucMetadata.paramValues); + + // compare new to old dynamic prop values, updating old props with new values + // for each change, queue the impacted adapter(s) + + // process queue of impacted adapters + invokeUpdatedCallback(ucMetadata.callbacks, paramValues); +} + /** * The wire service. * @@ -146,28 +164,25 @@ const wireService = { // TODO W-4072588 - support connected + disconnected (repeated) cycles wiring: (cmp: Element, data: object, def: ElementDef, context: object) => { // engine guarantees invocation only if def.wire is defined - const adapters = installWireAdapters(cmp, def); + const wiredefs = getWireDefs(cmp, def, updated.bind(context)); + const adapters: Array = []; - // cache context that optimizes runtime of service callbacks - context[CONTEXT_ID] = buildContext(adapters); - }, + for (let i = 0; i < wiredefs.length; i++) { + const wiredef = wiredefs[i]; + const id = wiredef.adapter || wiredef.type; + const wiredPropOrMethod = wiredef.target; - // TODO - in early 216, engine will expose an updated callback for services that - // is invoked whenever a tracked property is changed. wire service is structured to - // make this adoption trivial. - updated: (cmp: Element, data: object, def: ElementDef, context: object) => { - let ucMetadata : ServiceUpdateContext; - if (!def.wire || !(ucMetadata = context[CONTEXT_ID]['updated'])) { - return; - } - // get new values for all dynamic props - const paramValues = getPropertyValues(cmp, ucMetadata.paramValues); + const targetSetter: TargetSetter = wiredef.method ? + (value) => { cmp[wiredPropOrMethod](value); } : + (value) => { Object.assign(cmp[wiredPropOrMethod], value); }; - // compare new to old dynamic prop values, updating old props with new values - // for each change, queue the impacted adapter(s) + const adapterFactory = adapterFactories.get(id); + const adapter = adapterFactory(targetSetter); + adapters.push(adapter); + } - // process queue of impacted adapters - invokeUpdatedCallback(ucMetadata.callbacks, paramValues); + // cache context that optimizes runtime of service callbacks + context[CONTEXT_ID] = buildContext(adapters, wiredefs); }, connected: (cmp: Element, data: object, def: ElementDef, context: object) => { @@ -200,5 +215,5 @@ export function registerWireService(register: Function) { export function register(adapterId: any, adapterFactory: WireAdapterFactory) { assert.isTrue(adapterId, 'adapter id must be truthy'); assert.isTrue(typeof adapterFactory === 'function', 'adapter factory must be a function'); - adapters.set(adapterId, adapterFactory); + adapterFactories.set(adapterId, adapterFactory); }; diff --git a/packages/lwc-wire-service/src/shared-types.ts b/packages/lwc-wire-service/src/shared-types.ts index 749a00f9a6..f8de116a6b 100644 --- a/packages/lwc-wire-service/src/shared-types.ts +++ b/packages/lwc-wire-service/src/shared-types.ts @@ -5,7 +5,8 @@ export interface WireDef { static: { [key: string]: any; }; - type: string; + type?: string; + adapter?: any; method?: 1; } export interface ElementDef { From 7fee5b4467f24b03752d97549fca862cea07b5ab Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Fri, 16 Mar 2018 10:56:35 -0700 Subject: [PATCH 05/52] fix tslint errors --- packages/lwc-wire-service/package.json | 1 - packages/lwc-wire-service/src/index.ts | 69 ++++++++++--------- packages/lwc-wire-service/src/shared-types.ts | 2 +- packages/lwc-wire-service/src/wiring.ts | 9 +-- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/packages/lwc-wire-service/package.json b/packages/lwc-wire-service/package.json index beb69c8c62..3f10bad131 100644 --- a/packages/lwc-wire-service/package.json +++ b/packages/lwc-wire-service/package.json @@ -5,7 +5,6 @@ "main": "dist/commonjs/es2017/wire-service.js", "module": "dist/modules/es2017/wire-service.js", "scripts": { - "lint": "eslint src playground", "build": "concurrently \"yarn build:es-and-cjs\" \"yarn build:umd:dev\"", "build:umd:dev": "rollup -c rollup.config.umd.dev.js", "build:es-and-cjs": "rollup -c rollup.config.es-and-cjs.js", diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index db9e585d0d..5e1dc25e5b 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -12,7 +12,7 @@ import { ElementDef } from './shared-types'; export interface WiredValue { data?: any; error?: any; -}; +} export type TargetSetter = (WiredValue) => void; export type UpdatedCallback = (object) => void; export type NoArgumentCallback = () => void; @@ -21,7 +21,7 @@ export interface WireAdapter { updatedCallback?: UpdatedCallback; connectedCallback?: NoArgumentCallback; disconnectedCallback?: NoArgumentCallback; -}; +} export type WireAdapterFactory = (targetSetter: TargetSetter) => WireAdapter; export interface UpdatedCallbackConfig { @@ -93,43 +93,43 @@ function getPropertyValues(cmp: Element, properties: string[]) { * Build context payload. */ function buildContext(adapters: WireAdapter[], wiredefs: any) { - let context: Map = Object.create(null); + const context: Map = Object.create(null); - const noArgCallbacks: Array = ['connectedCallback', 'disconnectedCallback']; - for (let i = 0; i < noArgCallbacks.length; i++) { - const noArgCallback = noArgCallbacks[i]; - // TODO - this is really Array - const callbacks: Array = []; + const noArgCallbackKeys: Array = ['connectedCallback', 'disconnectedCallback']; + for (let i = 0; i < noArgCallbackKeys.length; i++) { + const noArgCallbackKey = noArgCallbackKeys[i]; + const wireNoArgCallbacks: WireAdapterCallback[] = []; for (let j = 0; j < adapters.length; j++) { - let callback = adapters[j][noArgCallback]; - if (callback) { - callbacks.push(callback); + const wireNoArgCallback = adapters[j][noArgCallbackKey]; + if (wireNoArgCallback) { + wireNoArgCallbacks.push(wireNoArgCallback); } } - if (callbacks.length > 0) { - context[noArgCallback] = callbacks; + if (wireNoArgCallbacks.length > 0) { + context[noArgCallbackKey] = wireNoArgCallbacks; } } - const callbacks: Array = []; + const updatedCallbackKey = 'updatedCallback'; + const wireUpdatedCallbacks: UpdatedCallbackConfig[] = []; const paramValues: string[] = []; for (let j = 0; j < adapters.length; j++) { - let callback = adapters[j]['updatedCallback']; - if (callback) { + const updatedCallback = adapters[j][updatedCallbackKey]; + if (updatedCallback) { // TODO - extract statics and params from the wire def - callbacks.push({ - updatedCallback: callback, + wireUpdatedCallbacks.push({ + updatedCallback, statics: {}, params: {} }); } } - if (callbacks.length > 0) { + if (wireUpdatedCallbacks.length > 0) { const ucContext: ServiceUpdateContext = { - callbacks, + callbacks: wireUpdatedCallbacks, paramValues - } - context['updatedCallback'] = ucContext; + }; + context[updatedCallbackKey] = ucContext; } return context; @@ -140,8 +140,8 @@ function buildContext(adapters: WireAdapter[], wiredefs: any) { // is invoked whenever a tracked property is changed. wire service is structured to // make this adoption trivial. function updated(context: object, cmp: Element, def: ElementDef) { - let ucMetadata : ServiceUpdateContext; - if (!def.wire || !(ucMetadata = context[CONTEXT_ID]['updated'])) { + let ucMetadata: ServiceUpdateContext; + if (!def.wire || !(ucMetadata = context[CONTEXT_ID].updated)) { return; } // get new values for all dynamic props @@ -165,7 +165,7 @@ const wireService = { wiring: (cmp: Element, data: object, def: ElementDef, context: object) => { // engine guarantees invocation only if def.wire is defined const wiredefs = getWireDefs(cmp, def, updated.bind(context)); - const adapters: Array = []; + const adapters: WireAdapter[] = []; for (let i = 0; i < wiredefs.length; i++) { const wiredef = wiredefs[i]; @@ -177,8 +177,9 @@ const wireService = { (value) => { Object.assign(cmp[wiredPropOrMethod], value); }; const adapterFactory = adapterFactories.get(id); - const adapter = adapterFactory(targetSetter); - adapters.push(adapter); + if (adapterFactory) { + adapters.push(adapterFactory(targetSetter)); + } } // cache context that optimizes runtime of service callbacks @@ -186,16 +187,16 @@ const wireService = { }, connected: (cmp: Element, data: object, def: ElementDef, context: object) => { - let callbacks : NoArgumentCallback[]; - if (!def.wire || !(callbacks = context[CONTEXT_ID]['connected'])) { + let callbacks: NoArgumentCallback[]; + if (!def.wire || !(callbacks = context[CONTEXT_ID].connected)) { return; } invokeCallback(callbacks); }, disconnected: (cmp: Element, data: object, def: ElementDef, context: object) => { - let callbacks : NoArgumentCallback[]; - if (!def.wire || (callbacks = context[CONTEXT_ID]['disconnected']) ) { + let callbacks: NoArgumentCallback[]; + if (!def.wire || !(callbacks = context[CONTEXT_ID].disconnected)) { return; } invokeCallback(callbacks); @@ -205,8 +206,8 @@ const wireService = { /** * Registers the wire service. */ -export function registerWireService(register: Function) { - register(wireService); +export function registerWireService(registerService: Function) { + registerService(wireService); } /** @@ -216,4 +217,4 @@ export function register(adapterId: any, adapterFactory: WireAdapterFactory) { assert.isTrue(adapterId, 'adapter id must be truthy'); assert.isTrue(typeof adapterFactory === 'function', 'adapter factory must be a function'); adapterFactories.set(adapterId, adapterFactory); -}; +} diff --git a/packages/lwc-wire-service/src/shared-types.ts b/packages/lwc-wire-service/src/shared-types.ts index f8de116a6b..78a6c7e9ec 100644 --- a/packages/lwc-wire-service/src/shared-types.ts +++ b/packages/lwc-wire-service/src/shared-types.ts @@ -11,6 +11,6 @@ export interface WireDef { } export interface ElementDef { wire: { // TODO - wire is optional but all wire service code assumes it's present - [key: string] : WireDef + [key: string]: WireDef }; } diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index a6e2c9aef5..b4248a2a48 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -1,8 +1,6 @@ /** * Wires LWC component to wire adapters. */ - - import { Element } from 'engine'; import { ElementDef, WireDef } from './shared-types'; @@ -174,7 +172,10 @@ export function getOverrideDescriptor(cmp, prop, listeners) { if (originalDescriptor) { newDescriptor = Object.assign({}, originalDescriptor, { set(value) { - originalDescriptor.set.call(cmp, value); + if (originalDescriptor.set) { + originalDescriptor.set.call(cmp, value); + } + // re-fetch the value to handle asymmetry between setter and getter values listeners.forEach(f => f(cmp[prop])); } @@ -210,7 +211,7 @@ export function installWiredValues(wiredValues) { /** * Installs wiring for a component. */ -export function installWiring(cmp:Element, def) { +export function installWiring(cmp: Element, def) { const wireConfigs = getWireConfigs(def); const wiredValues = getWiredValues(wireConfigs, cmp); const propChangeHandlers = getPropChangeHandlers(wireConfigs, wiredValues); From f6aea16a5e849d440a5afccba6ed76375d67c3b3 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Fri, 16 Mar 2018 15:19:19 -0700 Subject: [PATCH 06/52] wip: hook up updated --- packages/lwc-wire-service/src/index.ts | 94 +++++++++++++++++++++----- 1 file changed, 76 insertions(+), 18 deletions(-) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 5e1dc25e5b..e02bf70b26 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -7,7 +7,7 @@ import { Element } from 'engine'; import assert from './assert'; -import { ElementDef } from './shared-types'; +import { WireDef, ElementDef } from './shared-types'; export interface WiredValue { data?: any; @@ -92,7 +92,7 @@ function getPropertyValues(cmp: Element, properties: string[]) { /** * Build context payload. */ -function buildContext(adapters: WireAdapter[], wiredefs: any) { +function buildContext(adapters: WireAdapter[]) { const context: Map = Object.create(null); const noArgCallbackKeys: Array = ['connectedCallback', 'disconnectedCallback']; @@ -111,22 +111,21 @@ function buildContext(adapters: WireAdapter[], wiredefs: any) { } const updatedCallbackKey = 'updatedCallback'; - const wireUpdatedCallbacks: UpdatedCallbackConfig[] = []; + const updatedCallbackConfigs: UpdatedCallbackConfig[] = []; const paramValues: string[] = []; for (let j = 0; j < adapters.length; j++) { const updatedCallback = adapters[j][updatedCallbackKey]; if (updatedCallback) { - // TODO - extract statics and params from the wire def - wireUpdatedCallbacks.push({ + updatedCallbackConfigs.push({ updatedCallback, statics: {}, params: {} }); } } - if (wireUpdatedCallbacks.length > 0) { + if (updatedCallbackConfigs.length > 0) { const ucContext: ServiceUpdateContext = { - callbacks: wireUpdatedCallbacks, + callbacks: updatedCallbackConfigs, paramValues }; context[updatedCallbackKey] = ucContext; @@ -139,7 +138,7 @@ function buildContext(adapters: WireAdapter[], wiredefs: any) { // TODO - in early 216, engine will expose an `updated` callback for services that // is invoked whenever a tracked property is changed. wire service is structured to // make this adoption trivial. -function updated(context: object, cmp: Element, def: ElementDef) { +function updated(cmp: Element, data: object, def: ElementDef, context: object) { let ucMetadata: ServiceUpdateContext; if (!def.wire || !(ucMetadata = context[CONTEXT_ID].updated)) { return; @@ -154,6 +153,35 @@ function updated(context: object, cmp: Element, def: ElementDef) { invokeUpdatedCallback(ucMetadata.callbacks, paramValues); } +/** + * Gets a mapping of component prop to wire config dynamic params. In other words, + * the wire config's parameter set that updates whenever a prop changes. + * @param {*} wireDef The wire definition. + * @param {String} wireTarget Component property that is the target of the wire. + * @returns {Object} Map of prop name to wire config dynamic params. + */ +function getPropToParams(wireDef, wireTarget) { + const map = Object.create(null); + const { params } = wireDef; + if (params) { + Object.keys(params).forEach(param => { + const prop = params[param]; + + if (param in wireDef.static) { + throw new Error(`${wireTarget}'s @wire(${wireDef.adapter}) parameter ${param} specified multiple times.`); + } + + // attribute change handlers use hyphenated case + let set = map[prop]; + if (!set) { + set = map[prop] = []; + } + set.push(param); + }); + } + return map; +} + /** * The wire service. * @@ -164,26 +192,56 @@ const wireService = { // TODO W-4072588 - support connected + disconnected (repeated) cycles wiring: (cmp: Element, data: object, def: ElementDef, context: object) => { // engine guarantees invocation only if def.wire is defined - const wiredefs = getWireDefs(cmp, def, updated.bind(context)); + const wireStaticDef = def.wire; + const wireTargets = Object.keys(wireStaticDef); const adapters: WireAdapter[] = []; + for (let i = 0; i < wireTargets.length; i++) { + const wireTarget = wireTargets[i]; + const wireDef = wireStaticDef[wireTarget]; + const id = wireDef.adapter || wireDef.type; - for (let i = 0; i < wiredefs.length; i++) { - const wiredef = wiredefs[i]; - const id = wiredef.adapter || wiredef.type; - const wiredPropOrMethod = wiredef.target; - - const targetSetter: TargetSetter = wiredef.method ? - (value) => { cmp[wiredPropOrMethod](value); } : - (value) => { Object.assign(cmp[wiredPropOrMethod], value); }; + const targetSetter: TargetSetter = wireDef.method ? + (value) => { cmp[wireTarget](value); } : + (value) => { Object.assign(cmp[wireTarget], value); }; const adapterFactory = adapterFactories.get(id); if (adapterFactory) { adapters.push(adapterFactory(targetSetter)); } + + const propToParamsMap = getPropToParams(wireDef, wireTarget); + Object.keys(propToParamsMap).forEach(prop => { + const originalDescriptor = Object.getOwnPropertyDescriptor(cmp.constructor.prototype, prop); + let newDescriptor; + if (originalDescriptor) { + newDescriptor = Object.assign({}, originalDescriptor, { + set(value) { + if (originalDescriptor.set) { + originalDescriptor.set.call(cmp, value); + } + updated.bind(this, cmp, data, def, context); + } + }); + } else { + const propSymbol = Symbol(prop); + newDescriptor = { + get() { + return cmp[propSymbol]; + }, + set(value) { + cmp[propSymbol] = value; + updated.bind(this, cmp, data, def, context); + } + }; + // grab the existing value + cmp[propSymbol] = cmp[prop]; + } + Object.defineProperty(cmp, prop, newDescriptor); + }); } // cache context that optimizes runtime of service callbacks - context[CONTEXT_ID] = buildContext(adapters, wiredefs); + context[CONTEXT_ID] = buildContext(adapters); }, connected: (cmp: Element, data: object, def: ElementDef, context: object) => { From 4be7d460934e1d12ed69de28caf7810e6c583f4c Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 08:47:40 -0700 Subject: [PATCH 07/52] delete wiring.ts --- packages/lwc-wire-service/src/wiring.ts | 223 ------------------------ 1 file changed, 223 deletions(-) delete mode 100644 packages/lwc-wire-service/src/wiring.ts diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts deleted file mode 100644 index b4248a2a48..0000000000 --- a/packages/lwc-wire-service/src/wiring.ts +++ /dev/null @@ -1,223 +0,0 @@ -/** - * Wires LWC component to wire adapters. - */ -import { Element } from 'engine'; -import { ElementDef, WireDef } from './shared-types'; - -/** - * Gets a mapping of component prop to wire config dynamic params. In other words, - * the wire config's parameter set that updates whenever a prop changes. - * @param {*} wireDef The wire definition. - * @param {String} wireTarget Component property that is the target of the wire. - * @returns {Object} Map of prop name to wire config dynamic params. - */ -export function getPropToParams(wireDef: WireDef, wireTarget) { - const map = Object.create(null); - const { params } = wireDef; - if (params) { - Object.keys(params).forEach(param => { - const prop = params[param]; - - if (param in wireDef.static) { - throw new Error(`${wireTarget}'s @wire('${wireDef.type}') parameter ${param} specified multiple times.`); - } - - // attribute change handlers use hyphenated case - let set = map[prop]; - if (!set) { - set = map[prop] = []; - } - set.push(param); - }); - } - return map; -} - -/** - * Gets the wire adapter for a wire. - * @param {*} wireDef The wire definition - * @param {String} wireTarget Component property that is the target of the wire. - * @returns {Function} The wire adapter. - */ -export function getAdapter(wireDef: WireDef, wireTarget) { - let adapter; - // TODO: deprecate type once consumers have migrate to use function identifier for adapter id. - if (wireDef.type) { - adapter = ADAPTERS.get(wireDef.type); - if (!adapter) { - throw new Error(`Unknown wire adapter id '${wireDef.type}' in ${wireTarget}'s @wire('${wireDef.type}', ...)`); - } - } else if (wireDef.adapter) { - adapter = ADAPTERS.get(wireDef.adapter.name); - if (!adapter) { - throw new Error(`Unknown wire adapter id '${wireDef.adapter.name}' in ${wireTarget}'s @wire(${wireDef.adapter.name}, ...)`); - } - } - - return adapter; -} - -/** - * Gets the non-dynamic wire config. - * @param {*} wireDef The wire definition - * @returns {Object} The non-dynamic portions of the wire config. - */ -export function getStaticConfig(wireDef: WireDef): object { - const config = Object.create(null); - Object.assign(config, wireDef.static); - return config; -} - -/** - * Gets whether the wiring is to a method or property. - * @param {*} wireDef The wire definition - * @returns {Boolean} True if wired to a method, false otherwise. - */ -export function getIsMethod(wireDef: WireDef): boolean { - return wireDef.method === 1; -} - -/** - * Gets the config bags for all wires on a component. - * @param {*} def The component definition. - * @return {Object} Map of wire configs. - */ -export function getWireConfigs(def: ElementDef) { - const wires = def.wire; - const wireConfigs = Object.create(null); - Object.keys(wires).forEach(wireTarget => { - const wireDef = wires[wireTarget]; - const propToParams = getPropToParams(wireDef, wireTarget); - const adapter = getAdapter(wireDef, wireTarget); - const staticConfig = getStaticConfig(wireDef); - const isMethod = getIsMethod(wireDef); - wireConfigs[wireTarget] = { propToParams, adapter, staticConfig, isMethod }; - }); - return wireConfigs; -} - -/** - * Gets the WiredValue instances for the wire config bags. - * @param {*} wireConfigs The wire config bags. - * @param {*} cmp The component whose wiring is being created. - * @returns {Object} The WiredValue instances. - */ -export function getWiredValues(wireConfigs, cmp) { - const wiredValues = Object.create(null); - Object.keys(wireConfigs).forEach(wireTarget => { - const { adapter, staticConfig, isMethod } = wireConfigs[wireTarget]; - wiredValues[wireTarget] = new WiredValue(adapter, staticConfig, isMethod, cmp, wireTarget); - }); - return wiredValues; -} - -/** - * Gets the WiredValue handlers for property changes. - * @param {*} wireConfigs The wire config bags. - * @param {Object} wiredValues The WiredValue instances. - * @return {Object} Map of props to change handlers. - */ -export function getPropChangeHandlers(wireConfigs, wiredValues) { - const map = Object.create(null); - Object.keys(wireConfigs).forEach(wireTarget => { - const { propToParams } = wireConfigs[wireTarget]; - const wiredValue = wiredValues[wireTarget]; - - Object.keys(propToParams).forEach(prop => { - let set = map[prop]; - if (!set) { - set = map[prop] = []; - } - - const boundUpdate = propToParams[prop].map(param => { - return wiredValue.update.bind(wiredValue, param); - }); - - set.push(...boundUpdate); - }); - }); - return map; -} - -/** - * Installs property setters to listen for changes to property-based params. - * @param {Object} cmp The component. - * @param {Object} propsToListeners Map of prop names to change handler functions. - */ -export function installSetterOverrides(cmp, propsToListeners) { - const props = Object.keys(propsToListeners); - - // do not modify cmp if not required - if (props.length === 0) { - return; - } - - props.forEach(prop => { - const newDescriptor = getOverrideDescriptor(cmp, prop, propsToListeners[prop]); - Object.defineProperty(cmp, prop, newDescriptor); - }); -} - -/** - * Gets a property descriptor that monitors the provided property for changes. - * @param {Object} cmp The component. - * @param {String} prop The name of the property to be monitored. - * @param {Function[]} listeners List of functions to invoke when the prop's value changes. - * @return {Object} A property descriptor. - */ -export function getOverrideDescriptor(cmp, prop, listeners) { - const originalDescriptor = Object.getOwnPropertyDescriptor(cmp.constructor.prototype, prop); - - let newDescriptor; - if (originalDescriptor) { - newDescriptor = Object.assign({}, originalDescriptor, { - set(value) { - if (originalDescriptor.set) { - originalDescriptor.set.call(cmp, value); - } - - // re-fetch the value to handle asymmetry between setter and getter values - listeners.forEach(f => f(cmp[prop])); - } - }); - } else { - const propSymbol = Symbol(prop); - newDescriptor = { - get() { - return cmp[propSymbol]; - }, - set(value) { - cmp[propSymbol] = value; - listeners.forEach(f => f(value)); - } - }; - // grab the existing value - cmp[propSymbol] = cmp[prop]; - } - return newDescriptor; -} - -/** - * Installs the WiredValue instances onto the component. - * @param {Object} wiredValues The WiredValue instances. - */ -export function installWiredValues(wiredValues) { - Object.keys(wiredValues).forEach(wiredTarget => { - const wiredValue = wiredValues[wiredTarget]; - wiredValue.install(); - }); -} - -/** - * Installs wiring for a component. - */ -export function installWiring(cmp: Element, def) { - const wireConfigs = getWireConfigs(def); - const wiredValues = getWiredValues(wireConfigs, cmp); - const propChangeHandlers = getPropChangeHandlers(wireConfigs, wiredValues); - installSetterOverrides(cmp, propChangeHandlers); - // TODO - handle when config values have only defaults and thus don't trigger setter overrides - installWiredValues(wiredValues); - - return wiredValues; -} From 551fbd2869f3d47bb146fafc4940b8b85bf8ac42 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 09:34:14 -0700 Subject: [PATCH 08/52] only hook updated to bound props and clean up ServiceContext --- packages/lwc-wire-service/src/index.ts | 133 +++++++++---------------- 1 file changed, 49 insertions(+), 84 deletions(-) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index e02bf70b26..e3e227de83 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -24,21 +24,7 @@ export interface WireAdapter { } export type WireAdapterFactory = (targetSetter: TargetSetter) => WireAdapter; -export interface UpdatedCallbackConfig { - updatedCallback: UpdatedCallback; - statics: { - [key: string]: any; - }; - params: { - [key: string]: string; - }; -} -export interface ServiceUpdateContext { - callbacks: UpdatedCallbackConfig[]; - // union of callbacks.params values - paramValues: string[]; -} -export type ServiceContext = NoArgumentCallback[] | ServiceUpdateContext; +export type ServiceContext = WireAdapterCallback[]; // lifecycle hooks of wire adapters // const HOOKS: Array = ['updatedCallback', 'connectedCallback', 'disconnectedCallback']; @@ -111,28 +97,18 @@ function buildContext(adapters: WireAdapter[]) { } const updatedCallbackKey = 'updatedCallback'; - const updatedCallbackConfigs: UpdatedCallbackConfig[] = []; - const paramValues: string[] = []; + const wireUpdatedCallbacks: UpdatedCallback[] = []; for (let j = 0; j < adapters.length; j++) { const updatedCallback = adapters[j][updatedCallbackKey]; if (updatedCallback) { - updatedCallbackConfigs.push({ - updatedCallback, - statics: {}, - params: {} - }); + wireUpdatedCallbacks.push(updatedCallback); } } - if (updatedCallbackConfigs.length > 0) { - const ucContext: ServiceUpdateContext = { - callbacks: updatedCallbackConfigs, - paramValues - }; - context[updatedCallbackKey] = ucContext; + if (wireUpdatedCallbacks.length > 0) { + context[updatedCallbackKey] = wireUpdatedCallbacks; } return context; - } // TODO - in early 216, engine will expose an `updated` callback for services that @@ -153,33 +129,19 @@ function updated(cmp: Element, data: object, def: ElementDef, context: object) { invokeUpdatedCallback(ucMetadata.callbacks, paramValues); } -/** - * Gets a mapping of component prop to wire config dynamic params. In other words, - * the wire config's parameter set that updates whenever a prop changes. - * @param {*} wireDef The wire definition. - * @param {String} wireTarget Component property that is the target of the wire. - * @returns {Object} Map of prop name to wire config dynamic params. - */ -function getPropToParams(wireDef, wireTarget) { - const map = Object.create(null); - const { params } = wireDef; - if (params) { - Object.keys(params).forEach(param => { - const prop = params[param]; - - if (param in wireDef.static) { - throw new Error(`${wireTarget}'s @wire(${wireDef.adapter}) parameter ${param} specified multiple times.`); - } +function getPropsFromParams(wireDefs: WireDef[]) { + const props = new Set(); + wireDefs.forEach((wireDef) => { + const { params } = wireDef; + if (params) { + Object.keys(params).forEach(param => { + const prop = params[param]; + props.add(prop); + }); + } + }); - // attribute change handlers use hyphenated case - let set = map[prop]; - if (!set) { - set = map[prop] = []; - } - set.push(param); - }); - } - return map; + return props; } /** @@ -195,9 +157,11 @@ const wireService = { const wireStaticDef = def.wire; const wireTargets = Object.keys(wireStaticDef); const adapters: WireAdapter[] = []; + const wireDefs: WireDef[] = []; for (let i = 0; i < wireTargets.length; i++) { const wireTarget = wireTargets[i]; const wireDef = wireStaticDef[wireTarget]; + wireDefs.push(wireDef); const id = wireDef.adapter || wireDef.type; const targetSetter: TargetSetter = wireDef.method ? @@ -208,37 +172,38 @@ const wireService = { if (adapterFactory) { adapters.push(adapterFactory(targetSetter)); } + } - const propToParamsMap = getPropToParams(wireDef, wireTarget); - Object.keys(propToParamsMap).forEach(prop => { - const originalDescriptor = Object.getOwnPropertyDescriptor(cmp.constructor.prototype, prop); - let newDescriptor; - if (originalDescriptor) { - newDescriptor = Object.assign({}, originalDescriptor, { - set(value) { - if (originalDescriptor.set) { - originalDescriptor.set.call(cmp, value); - } - updated.bind(this, cmp, data, def, context); - } - }); - } else { - const propSymbol = Symbol(prop); - newDescriptor = { - get() { - return cmp[propSymbol]; - }, - set(value) { - cmp[propSymbol] = value; - updated.bind(this, cmp, data, def, context); + // only add updated to bound props + const props = getPropsFromParams(wireDefs); + props.forEach((prop) => { + const originalDescriptor = Object.getOwnPropertyDescriptor(cmp.constructor.prototype, prop); + let newDescriptor; + if (originalDescriptor) { + newDescriptor = Object.assign({}, originalDescriptor, { + set(value) { + if (originalDescriptor.set) { + originalDescriptor.set.call(cmp, value); } - }; - // grab the existing value - cmp[propSymbol] = cmp[prop]; - } - Object.defineProperty(cmp, prop, newDescriptor); - }); - } + updated.bind(this, cmp, data, def, context); + } + }); + } else { + const propSymbol = Symbol(prop); + newDescriptor = { + get() { + return cmp[propSymbol]; + }, + set(value) { + cmp[propSymbol] = value; + updated.bind(this, cmp, data, def, context); + } + }; + // grab the existing value + cmp[propSymbol] = cmp[prop]; + } + Object.defineProperty(cmp, prop, newDescriptor); + }); // cache context that optimizes runtime of service callbacks context[CONTEXT_ID] = buildContext(adapters); From 8ecb3034f50f6412e4cc33ae7d9693d433216773 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 10:21:05 -0700 Subject: [PATCH 09/52] now everything in ServiceUpdateContext make sense --- packages/lwc-wire-service/src/index.ts | 63 +++++++++++++++++--------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index e3e227de83..102d5f695b 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -24,7 +24,21 @@ export interface WireAdapter { } export type WireAdapterFactory = (targetSetter: TargetSetter) => WireAdapter; -export type ServiceContext = WireAdapterCallback[]; +export interface UpdatedCallbackConfig { + updatedCallback: UpdatedCallback; + statics: { + [key: string]: any; + }; + params: { + [key: string]: string; + }; +} +export interface ServiceUpdateContext { + callbacks: UpdatedCallbackConfig[]; + // union of callbacks.params values + paramValues: Set; +} +export type ServiceContext = NoArgumentCallback[] | ServiceUpdateContext; // lifecycle hooks of wire adapters // const HOOKS: Array = ['updatedCallback', 'connectedCallback', 'disconnectedCallback']; @@ -66,19 +80,20 @@ function invokeUpdatedCallback(ucMetadatas: UpdatedCallbackConfig[], paramValues /** * Gets resolved values of the specified properties. */ -function getPropertyValues(cmp: Element, properties: string[]) { +function getPropertyValues(cmp: Element, properties: Set) { const resolvedValues = Object.create(null); - for (let i = 0, len = properties.length; i < len; ++i) { - const paramValue = properties[i]; + properties.forEach((property) => { + const paramValue = property; resolvedValues[paramValue] = cmp[paramValue]; - } + }); + return resolvedValues; } /** * Build context payload. */ -function buildContext(adapters: WireAdapter[]) { +function buildServiceContext(adapters: WireAdapter[]) { const context: Map = Object.create(null); const noArgCallbackKeys: Array = ['connectedCallback', 'disconnectedCallback']; @@ -96,18 +111,6 @@ function buildContext(adapters: WireAdapter[]) { } } - const updatedCallbackKey = 'updatedCallback'; - const wireUpdatedCallbacks: UpdatedCallback[] = []; - for (let j = 0; j < adapters.length; j++) { - const updatedCallback = adapters[j][updatedCallbackKey]; - if (updatedCallback) { - wireUpdatedCallbacks.push(updatedCallback); - } - } - if (wireUpdatedCallbacks.length > 0) { - context[updatedCallbackKey] = wireUpdatedCallbacks; - } - return context; } @@ -119,6 +122,7 @@ function updated(cmp: Element, data: object, def: ElementDef, context: object) { if (!def.wire || !(ucMetadata = context[CONTEXT_ID].updated)) { return; } + // get new values for all dynamic props const paramValues = getPropertyValues(cmp, ucMetadata.paramValues); @@ -158,19 +162,29 @@ const wireService = { const wireTargets = Object.keys(wireStaticDef); const adapters: WireAdapter[] = []; const wireDefs: WireDef[] = []; + const updatedCallbackKey = 'updatedCallback'; + const updatedCallbackConfigs: UpdatedCallbackConfig[] = []; for (let i = 0; i < wireTargets.length; i++) { const wireTarget = wireTargets[i]; const wireDef = wireStaticDef[wireTarget]; wireDefs.push(wireDef); const id = wireDef.adapter || wireDef.type; - const targetSetter: TargetSetter = wireDef.method ? (value) => { cmp[wireTarget](value); } : (value) => { Object.assign(cmp[wireTarget], value); }; const adapterFactory = adapterFactories.get(id); if (adapterFactory) { - adapters.push(adapterFactory(targetSetter)); + const wireAdapter = adapterFactory(targetSetter); + adapters.push(wireAdapter); + const updatedCallback = wireAdapter[updatedCallbackKey]; + if (updatedCallback) { + updatedCallbackConfigs.push({ + updatedCallback, + statics: wireDef.static, + params: wireDef.params + }); + } } } @@ -206,7 +220,14 @@ const wireService = { }); // cache context that optimizes runtime of service callbacks - context[CONTEXT_ID] = buildContext(adapters); + context[CONTEXT_ID] = buildServiceContext(adapters); + if (updatedCallbackConfigs.length > 0) { + const ucContext: ServiceUpdateContext = { + callbacks: updatedCallbackConfigs, + paramValues: props + }; + context[CONTEXT_ID][updatedCallbackKey] = ucContext; + } }, connected: (cmp: Element, data: object, def: ElementDef, context: object) => { From 873a280c78b800b2a38398ed68892a2303ec81a6 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 11:10:46 -0700 Subject: [PATCH 10/52] fix playground setup --- packages/lwc-wire-service/playground/rollup.config.js | 2 +- .../playground/x/single-wire/single-wire.js | 2 +- .../playground/x/todo-api/todo-api.js | 2 +- packages/lwc-wire-service/src/index.ts | 11 +++++++---- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/lwc-wire-service/playground/rollup.config.js b/packages/lwc-wire-service/playground/rollup.config.js index eac66d21db..9dbc2d9dd0 100644 --- a/packages/lwc-wire-service/playground/rollup.config.js +++ b/packages/lwc-wire-service/playground/rollup.config.js @@ -11,7 +11,7 @@ function resolver() { resolveId: (id) => { // wire service itself (no namespace) if (id === 'wire-service') { - return path.resolve('./src/main.js'); + return require.resolve('lwc-wire-service').replace('commonjs', 'modules'); } else if (id === 'engine') { return require.resolve('lwc-engine').replace('commonjs', 'modules'); // test and wire namespace components diff --git a/packages/lwc-wire-service/playground/x/single-wire/single-wire.js b/packages/lwc-wire-service/playground/x/single-wire/single-wire.js index 61a144460c..bb7bb6ca94 100644 --- a/packages/lwc-wire-service/playground/x/single-wire/single-wire.js +++ b/packages/lwc-wire-service/playground/x/single-wire/single-wire.js @@ -1,5 +1,5 @@ import { Element, api, wire } from 'engine'; -import { getTodo } from 'todo-api'; +import { getTodo } from 'x-todo-api'; export default class SingleWire extends Element { @api todoId; diff --git a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js index e03945826f..a0e2a09881 100644 --- a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js +++ b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js @@ -2,7 +2,7 @@ * Todo imperative APIs and wire adapters. */ -import register from 'wire-service'; +import { register } from 'wire-service'; import getObservable from './todo'; // Component-importable imperative access. diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 102d5f695b..401f81ec1b 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -49,6 +49,8 @@ const CONTEXT_ID: string = '@wire'; // wire adapters: wire adapter id => adapter ctor const adapterFactories: Map = new Map(); +const UPDATED: string = 'updated'; + /** * Invokes the specified callbacks. */ @@ -119,7 +121,7 @@ function buildServiceContext(adapters: WireAdapter[]) { // make this adoption trivial. function updated(cmp: Element, data: object, def: ElementDef, context: object) { let ucMetadata: ServiceUpdateContext; - if (!def.wire || !(ucMetadata = context[CONTEXT_ID].updated)) { + if (!def.wire || !(ucMetadata = context[CONTEXT_ID][UPDATED])) { return; } @@ -128,6 +130,7 @@ function updated(cmp: Element, data: object, def: ElementDef, context: object) { // compare new to old dynamic prop values, updating old props with new values // for each change, queue the impacted adapter(s) + // TODO: do we really need this if updated is only hooked to bound props? // process queue of impacted adapters invokeUpdatedCallback(ucMetadata.callbacks, paramValues); @@ -199,7 +202,7 @@ const wireService = { if (originalDescriptor.set) { originalDescriptor.set.call(cmp, value); } - updated.bind(this, cmp, data, def, context); + updated.call(this, cmp, data, def, context); } }); } else { @@ -210,7 +213,7 @@ const wireService = { }, set(value) { cmp[propSymbol] = value; - updated.bind(this, cmp, data, def, context); + updated.call(this, cmp, data, def, context); } }; // grab the existing value @@ -226,7 +229,7 @@ const wireService = { callbacks: updatedCallbackConfigs, paramValues: props }; - context[CONTEXT_ID][updatedCallbackKey] = ucContext; + context[CONTEXT_ID][UPDATED] = ucContext; } }, From 05833f49afa2af9c83c4f9b27fc7876ca00b8985 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 11:44:17 -0700 Subject: [PATCH 11/52] fix lifecycle hooks --- .../playground/x/single-wire/single-wire.html | 7 +++++-- packages/lwc-wire-service/src/index.ts | 12 ++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/lwc-wire-service/playground/x/single-wire/single-wire.html b/packages/lwc-wire-service/playground/x/single-wire/single-wire.html index 30ab6fb390..fb56c50315 100644 --- a/packages/lwc-wire-service/playground/x/single-wire/single-wire.html +++ b/packages/lwc-wire-service/playground/x/single-wire/single-wire.html @@ -3,7 +3,10 @@ {error} diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 401f81ec1b..4d58816c3e 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -50,6 +50,8 @@ const CONTEXT_ID: string = '@wire'; const adapterFactories: Map = new Map(); const UPDATED: string = 'updated'; +const CONNECTED: string = 'connectedCallback'; +const DISCONNECTED: string = 'disconnectedCallback'; /** * Invokes the specified callbacks. @@ -172,6 +174,12 @@ const wireService = { const wireDef = wireStaticDef[wireTarget]; wireDefs.push(wireDef); const id = wireDef.adapter || wireDef.type; + + // initialize wired property + if (!wireDef.method) { + cmp[wireTarget] = {}; + } + const targetSetter: TargetSetter = wireDef.method ? (value) => { cmp[wireTarget](value); } : (value) => { Object.assign(cmp[wireTarget], value); }; @@ -235,7 +243,7 @@ const wireService = { connected: (cmp: Element, data: object, def: ElementDef, context: object) => { let callbacks: NoArgumentCallback[]; - if (!def.wire || !(callbacks = context[CONTEXT_ID].connected)) { + if (!def.wire || !(callbacks = context[CONTEXT_ID][CONNECTED])) { return; } invokeCallback(callbacks); @@ -243,7 +251,7 @@ const wireService = { disconnected: (cmp: Element, data: object, def: ElementDef, context: object) => { let callbacks: NoArgumentCallback[]; - if (!def.wire || !(callbacks = context[CONTEXT_ID].disconnected)) { + if (!def.wire || !(callbacks = context[CONTEXT_ID][DISCONNECTED])) { return; } invokeCallback(callbacks); From eef11f0ccdd82ff56fc75b65f0963f8cf59d3501 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 13:40:21 -0700 Subject: [PATCH 12/52] update todo-api --- packages/lwc-wire-service/playground/x/todo-api/todo-api.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js index a0e2a09881..f39c71be9c 100644 --- a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js +++ b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js @@ -28,6 +28,10 @@ register(getTodo, function getTodoWireAdapter(targetSetter) { return { updatedCallback: (newConfig) => { config = newConfig; + subscription = getObservable(config).subscribe({ + next: data => targetSetter({ data, error: undefined }), + error: error => targetSetter({ data: undefined, error }) + }); }, connectedCallback: () => { From 2a13a3672de66c28f564358a38bac30d6f1cb124 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 13:44:03 -0700 Subject: [PATCH 13/52] fix single-wire-method and multiple wires in playground --- .../playground/x/multiple-wires/multiple-wires.js | 5 +++-- .../playground/x/single-wire-method/single-wire-method.js | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/lwc-wire-service/playground/x/multiple-wires/multiple-wires.js b/packages/lwc-wire-service/playground/x/multiple-wires/multiple-wires.js index f9b191034e..2c30fdb0c8 100644 --- a/packages/lwc-wire-service/playground/x/multiple-wires/multiple-wires.js +++ b/packages/lwc-wire-service/playground/x/multiple-wires/multiple-wires.js @@ -1,4 +1,5 @@ import { Element, api, wire } from 'engine'; +import { getTodo } from 'x-todo-api'; export default class MultipleWires extends Element { idA; @@ -18,10 +19,10 @@ export default class MultipleWires extends Element { return this.idA; } - @wire('todo', { id: '$idA' }) + @wire(getTodo, { id: '$idA' }) todoA; - @wire('todo', { id: '$idB' }) + @wire(getTodo, { id: '$idB' }) todoB; get hasError() { diff --git a/packages/lwc-wire-service/playground/x/single-wire-method/single-wire-method.js b/packages/lwc-wire-service/playground/x/single-wire-method/single-wire-method.js index a5a38c7d3a..bd04c8737b 100644 --- a/packages/lwc-wire-service/playground/x/single-wire-method/single-wire-method.js +++ b/packages/lwc-wire-service/playground/x/single-wire-method/single-wire-method.js @@ -1,4 +1,5 @@ import { Element, api, wire, track } from 'engine'; +import { getTodo } from 'x-todo-api'; export default class SingleWireMethod extends Element { @api todoId; @@ -6,8 +7,8 @@ export default class SingleWireMethod extends Element { @track error = undefined; @track todo = undefined; - @wire('todo', { id: '$todoId' }) - function(error, data) { + @wire(getTodo, { id: '$todoId' }) + function({error, data}) { this.error = error; this.todo = data; } From d4279f8d5f17536612a1eb0c53d691b5a0f871bc Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 13:49:34 -0700 Subject: [PATCH 14/52] delete todo-legacy-api --- .../x/todo-legacy-api/todo-legacy-api.js | 18 ------ .../playground/x/todo-legacy-api/todo.js | 47 -------------- .../playground/x/todo-legacy-api/util.js | 63 ------------------- 3 files changed, 128 deletions(-) delete mode 100644 packages/lwc-wire-service/playground/x/todo-legacy-api/todo-legacy-api.js delete mode 100644 packages/lwc-wire-service/playground/x/todo-legacy-api/todo.js delete mode 100644 packages/lwc-wire-service/playground/x/todo-legacy-api/util.js diff --git a/packages/lwc-wire-service/playground/x/todo-legacy-api/todo-legacy-api.js b/packages/lwc-wire-service/playground/x/todo-legacy-api/todo-legacy-api.js deleted file mode 100644 index 9e4b1726fc..0000000000 --- a/packages/lwc-wire-service/playground/x/todo-legacy-api/todo-legacy-api.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Playground wire adapters. - */ - -import registerWireService from 'wire-service'; -import serviceTodo from './todo'; - -/** - * Registers the wire service with mocked Salesforce UI API data types. - * @param {Object} engine The module engine. - */ -export default function registerMockedWireService(engine) { - registerWireService(engine.register, () => { - return { - 'todo': serviceTodo - }; - }); -} diff --git a/packages/lwc-wire-service/playground/x/todo-legacy-api/todo.js b/packages/lwc-wire-service/playground/x/todo-legacy-api/todo.js deleted file mode 100644 index b7844f4148..0000000000 --- a/packages/lwc-wire-service/playground/x/todo-legacy-api/todo.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @wire adapter for todo data. - */ -import { getSubject, getImmutableObservable } from './util'; - -function generateTodo(id, completed) { - return { - id, - title: 'task ' + id, - completed - }; -} - -// the data -const TODO = [ - generateTodo(0, true), - generateTodo(1, false), - // intentionally skip 2 - generateTodo(3, true), - generateTodo(4, true), - // intentionally skip 5 - generateTodo(6, false), - generateTodo(7, false) -].reduce((acc, value) => { - acc[value.id] = value; - return acc; -}, {}); - - -/** - * Services @wire('todo') requests. - * @param {Object} config Service config bag. - * @return {Observable} An observable for the recordUis. - */ -export default function serviceTodo(config) { - if (!('id' in config)) { - return undefined; - } - - const todo = TODO[config.id]; - if (!todo) { - const subject = getSubject(undefined, { message: 'Todo not found' }); - return subject.observable; - } - - return getImmutableObservable(todo); -} diff --git a/packages/lwc-wire-service/playground/x/todo-legacy-api/util.js b/packages/lwc-wire-service/playground/x/todo-legacy-api/util.js deleted file mode 100644 index 285be193dc..0000000000 --- a/packages/lwc-wire-service/playground/x/todo-legacy-api/util.js +++ /dev/null @@ -1,63 +0,0 @@ -// returns a read-only version via a proxy -export function getImmutable(obj) { - const handler = { - get: (target, key) => { - const value = target[key]; - if (value && typeof value === 'object') { - return getImmutable(value); - } - return value; - }, - set: () => { - return false; - }, - deleteProperty: () => { - return false; - } - }; - return new Proxy(obj, handler); -} - -// wraps a value in an observable that emits one immutable value -export function getImmutableObservable(value) { - return getSubject(getImmutable(value)).observable; -} - -// gets a subject with optional initial value and initial error -export function getSubject(initialValue, initialError) { - let observer; - - function next(value) { - observer.next(value); - } - - function error(err) { - observer.error(err); - } - - function complete() { - observer.complete(); - } - - const observable = { - subscribe: (obs) => { - observer = obs; - if (initialValue) { - next(initialValue); - } - if (initialError) { - error(initialError); - } - return { - unsubscribe: () => {} - }; - } - }; - - return { - next, - error, - complete, - observable - }; -} From 303f6a2afd30377bbd48db28d3100aa555b6dc0f Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 13:59:28 -0700 Subject: [PATCH 15/52] delete modules as they should be under lwc-integration --- .../x/wire-adapter-mock/wire-adapter-mock.js | 54 ---------- .../__snapshots__/wired-method.spec.js.snap | 27 ----- .../__tests__/wired-method.spec.js | 101 ------------------ .../modules/x/wired-method/wired-method.html | 4 - .../modules/x/wired-method/wired-method.js | 16 --- .../__tests__/wired-nonapi-property.spec.js | 29 ----- .../wired-nonapi-property.html | 4 - .../wired-nonapi-property.js | 24 ----- .../__snapshots__/wired-property.spec.js.snap | 38 ------- .../__tests__/wired-property.spec.js | 97 ----------------- .../x/wired-property/wired-property.html | 4 - .../x/wired-property/wired-property.js | 16 --- 12 files changed, 414 deletions(-) delete mode 100644 packages/lwc-wire-service/src/modules/x/wire-adapter-mock/wire-adapter-mock.js delete mode 100644 packages/lwc-wire-service/src/modules/x/wired-method/__tests__/__snapshots__/wired-method.spec.js.snap delete mode 100644 packages/lwc-wire-service/src/modules/x/wired-method/__tests__/wired-method.spec.js delete mode 100644 packages/lwc-wire-service/src/modules/x/wired-method/wired-method.html delete mode 100644 packages/lwc-wire-service/src/modules/x/wired-method/wired-method.js delete mode 100644 packages/lwc-wire-service/src/modules/x/wired-nonapi-property/__tests__/wired-nonapi-property.spec.js delete mode 100644 packages/lwc-wire-service/src/modules/x/wired-nonapi-property/wired-nonapi-property.html delete mode 100644 packages/lwc-wire-service/src/modules/x/wired-nonapi-property/wired-nonapi-property.js delete mode 100644 packages/lwc-wire-service/src/modules/x/wired-property/__tests__/__snapshots__/wired-property.spec.js.snap delete mode 100644 packages/lwc-wire-service/src/modules/x/wired-property/__tests__/wired-property.spec.js delete mode 100644 packages/lwc-wire-service/src/modules/x/wired-property/wired-property.html delete mode 100644 packages/lwc-wire-service/src/modules/x/wired-property/wired-property.js diff --git a/packages/lwc-wire-service/src/modules/x/wire-adapter-mock/wire-adapter-mock.js b/packages/lwc-wire-service/src/modules/x/wire-adapter-mock/wire-adapter-mock.js deleted file mode 100644 index 49a77dcf1b..0000000000 --- a/packages/lwc-wire-service/src/modules/x/wire-adapter-mock/wire-adapter-mock.js +++ /dev/null @@ -1,54 +0,0 @@ -import { register } from 'engine'; -import registerWireService from 'wire-service'; - -function createMockWireService() { - let observer; - - function next(d) { - observer.next(d); - } - - function error(err) { - observer.error(err); - } - - function complete() { - observer.complete(); - } - - const observable = { - subscribe: (obs) => { - observer = obs; - } - }; - - const init = jest.fn().mockReturnValue(observable); - function lastInitializedArgs() { - return init.mock.calls[init.mock.calls.length - 1]; - } - - return { - init, - next, - error, - complete, - lastInitializedArgs - }; -} - -export function registerMockWireAdapters(...names) { - const serviceMocks = names.reduce((acc, key) => { - acc[key] = createMockWireService(); - return acc; - }, {}); - - registerWireService(register, () => { - return Object.getOwnPropertyNames(serviceMocks).reduce((acc, key) => { - acc[key] = serviceMocks[key].init; - - return acc; - }, {}); - }); - - return serviceMocks; -} diff --git a/packages/lwc-wire-service/src/modules/x/wired-method/__tests__/__snapshots__/wired-method.spec.js.snap b/packages/lwc-wire-service/src/modules/x/wired-method/__tests__/__snapshots__/wired-method.spec.js.snap deleted file mode 100644 index ab5fd1759f..0000000000 --- a/packages/lwc-wire-service/src/modules/x/wired-method/__tests__/__snapshots__/wired-method.spec.js.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`wired-method snapshots should display complete correctly 1`] = ` - - Name: -
- Error: -
-`; - -exports[`wired-method snapshots should display data correctly 1`] = ` - - Name: - name -
- Error: -
-`; - -exports[`wired-method snapshots should display error correctly 1`] = ` - - Name: -
- Error: - error message -
-`; diff --git a/packages/lwc-wire-service/src/modules/x/wired-method/__tests__/wired-method.spec.js b/packages/lwc-wire-service/src/modules/x/wired-method/__tests__/wired-method.spec.js deleted file mode 100644 index aa6a6ea937..0000000000 --- a/packages/lwc-wire-service/src/modules/x/wired-method/__tests__/wired-method.spec.js +++ /dev/null @@ -1,101 +0,0 @@ -import WiredMethod from 'x-wired-method'; -import { createElement } from 'engine'; -import { registerMockWireAdapters } from 'x-wire-adapter-mock'; - -describe('wired-method', () => { - const { - test: mockTestAdapter - } = registerMockWireAdapters('test'); - - afterEach(() => { - while (document.body.firstChild) { - document.body.removeChild(document.body.firstChild); - } - }); - - it('should have initialized wire service correctly', () => { - const elem = createElement('x-wired-method', { is: WiredMethod }); - document.body.appendChild(elem); - - expect(mockTestAdapter.init.mock.calls).toHaveLength(1); - expect(mockTestAdapter.lastInitializedArgs()).toEqual([{ - fields: ['Name'] - }]); - }); - - describe('snapshots', () => { - it('should display data correctly', () => { - const elem = createElement('x-wired-method', { is: WiredMethod }); - document.body.appendChild(elem); - mockTestAdapter.next({ - Name: 'name' - }); - - return Promise.resolve().then(() => { - expect(elem).toMatchSnapshot(); - }); - }); - - it('should display error correctly', () => { - const elem = createElement('x-wired-method', { is: WiredMethod }); - document.body.appendChild(elem); - mockTestAdapter.error('error message'); - - return Promise.resolve().then(() => { - expect(elem).toMatchSnapshot(); - }); - }); - - it('should display complete correctly', () => { - const elem = createElement('x-wired-method', { is: WiredMethod }); - document.body.appendChild(elem); - mockTestAdapter.complete(); - - return Promise.resolve().then(() => { - expect(elem).toMatchSnapshot(); - }); - }); - }); - - describe('component lifecycle hooks', () => { - it('should get data when created', () => { - const element = createElement('x-wired-method', { is: WiredMethod }); - mockTestAdapter.next({ - Name: 'name' - }); - - document.body.appendChild(element); - - expect(element.textContent.substring('Name: '.length, element.textContent.indexOf('Error'))).toBe('name'); - }); - - it('should stop receiving data when disconnected', () => { - const element = createElement('x-wired-method', { is: WiredMethod }); - document.body.appendChild(element); - mockTestAdapter.next({ - Name: 'name' - }); - document.body.removeChild(element); - mockTestAdapter.next({ - Name: 'new_name' - }); - - expect(element.textContent.substring('Name: '.length, element.textContent.indexOf('Error'))).toBe(''); - }); - - it('should receive data when reconnected', () => { - const element = createElement('x-wired-method', { is: WiredMethod }); - document.body.appendChild(element); - mockTestAdapter.next({ - Name: 'name' - }); - document.body.removeChild(element); - mockTestAdapter.next({ - Name: 'new_name' - }); - document.body.appendChild(element); - - expect(element.textContent.substring('Name: '.length, element.textContent.indexOf('Error'))).toBe('new_name'); - }); - }); -}); diff --git a/packages/lwc-wire-service/src/modules/x/wired-method/wired-method.html b/packages/lwc-wire-service/src/modules/x/wired-method/wired-method.html deleted file mode 100644 index f800939e97..0000000000 --- a/packages/lwc-wire-service/src/modules/x/wired-method/wired-method.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/packages/lwc-wire-service/src/modules/x/wired-method/wired-method.js b/packages/lwc-wire-service/src/modules/x/wired-method/wired-method.js deleted file mode 100644 index 27472daca0..0000000000 --- a/packages/lwc-wire-service/src/modules/x/wired-method/wired-method.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Element, api, track, wire } from 'engine'; - -export default class WiredMethod extends Element { - @api propName; - @track state = {}; - @wire('test', { propName: '$propName', fields: ['Name'] }) - wiredMethod({error, data}) { - if (error) { - this.state.error = error; - this.state.Name = undefined; - } else { - this.state.Name = data.Name; - this.state.error = undefined; - } - } -} diff --git a/packages/lwc-wire-service/src/modules/x/wired-nonapi-property/__tests__/wired-nonapi-property.spec.js b/packages/lwc-wire-service/src/modules/x/wired-nonapi-property/__tests__/wired-nonapi-property.spec.js deleted file mode 100644 index 2f20924394..0000000000 --- a/packages/lwc-wire-service/src/modules/x/wired-nonapi-property/__tests__/wired-nonapi-property.spec.js +++ /dev/null @@ -1,29 +0,0 @@ -import PrivateWiredNonapiProperty from 'x-wired-nonapi-property'; -import { createElement } from 'engine'; -import { registerMockWireAdapters } from 'x-wire-adapter-mock'; - -describe('wired-nonapi-property', () => { - const { - test: mockTestAdapter - } = registerMockWireAdapters('test'); - - afterEach(() => { - while (document.body.firstChild) { - document.body.removeChild(document.body.firstChild); - } - }); - - it('invokes wire adapter when non-public property changes', () => { - const element = createElement('x-wired-nonapi-property', { is: PrivateWiredNonapiProperty }); - - document.body.appendChild(element); - - return Promise.resolve().then(() => { - element.field = 'first'; - expect(mockTestAdapter.lastInitializedArgs()).toEqual([{field: 'first'}]); - }).then(() => { - element.field = 'second'; - expect(mockTestAdapter.lastInitializedArgs()).toEqual([{field: 'second'}]); - }); - }); -}); diff --git a/packages/lwc-wire-service/src/modules/x/wired-nonapi-property/wired-nonapi-property.html b/packages/lwc-wire-service/src/modules/x/wired-nonapi-property/wired-nonapi-property.html deleted file mode 100644 index 968894bd65..0000000000 --- a/packages/lwc-wire-service/src/modules/x/wired-nonapi-property/wired-nonapi-property.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/packages/lwc-wire-service/src/modules/x/wired-nonapi-property/wired-nonapi-property.js b/packages/lwc-wire-service/src/modules/x/wired-nonapi-property/wired-nonapi-property.js deleted file mode 100644 index 23883a8645..0000000000 --- a/packages/lwc-wire-service/src/modules/x/wired-nonapi-property/wired-nonapi-property.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Element, api, wire } from 'engine'; - -export default class PrivateWiredNonapiProperty extends Element { - // enable tests to trigger wire config changes driven by a non-public property - @api set field(value) { - this._field = value; - } - @api get field() { - return this._field; - } - - _field; - - @wire('test', { field: '$_field' }) - wiredField; - - get WiredName() { - return this.wiredField.data ? this.wiredField.data.Name : ''; - } - - get WiredError() { - return this.wiredField.error; - } -} diff --git a/packages/lwc-wire-service/src/modules/x/wired-property/__tests__/__snapshots__/wired-property.spec.js.snap b/packages/lwc-wire-service/src/modules/x/wired-property/__tests__/__snapshots__/wired-property.spec.js.snap deleted file mode 100644 index 25a4255dfd..0000000000 --- a/packages/lwc-wire-service/src/modules/x/wired-property/__tests__/__snapshots__/wired-property.spec.js.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`wired-property snapshots default snapshot 1`] = ` - - Name: - -
- Error: -
-`; - -exports[`wired-property snapshots should display complete correctly 1`] = ` - - Name: - -
- Error: -
-`; - -exports[`wired-property snapshots should display data correctly 1`] = ` - - Name: - name -
- Error: -
-`; - -exports[`wired-property snapshots should display error correctly 1`] = ` - - Name: - -
- Error: - error message -
-`; diff --git a/packages/lwc-wire-service/src/modules/x/wired-property/__tests__/wired-property.spec.js b/packages/lwc-wire-service/src/modules/x/wired-property/__tests__/wired-property.spec.js deleted file mode 100644 index 27866ebbab..0000000000 --- a/packages/lwc-wire-service/src/modules/x/wired-property/__tests__/wired-property.spec.js +++ /dev/null @@ -1,97 +0,0 @@ -import WiredProperty from 'x-wired-property'; -import { createElement } from 'engine'; -import { registerMockWireAdapters } from 'x-wire-adapter-mock'; - -describe('wired-property', () => { - const { - test: mockTestAdapter - } = registerMockWireAdapters('test'); - - afterEach(() => { - while (document.body.firstChild) { - document.body.removeChild(document.body.firstChild); - } - }); - - describe('snapshots', () => { - it('default snapshot', () => { - const element = createElement('x-wired-property', { is: WiredProperty }); - document.body.appendChild(element); - expect(element).toMatchSnapshot(); - }); - - it('should display data correctly', () => { - const element = createElement('x-wired-property', { is: WiredProperty }); - document.body.appendChild(element); - mockTestAdapter.next({ - Name: 'name' - }); - - return Promise.resolve().then(() => { - expect(element).toMatchSnapshot(); - }); - }); - - it('should display error correctly', () => { - const element = createElement('x-wired-property', { is: WiredProperty }); - document.body.appendChild(element); - mockTestAdapter.error('error message'); - - return Promise.resolve().then(() => { - expect(element).toMatchSnapshot(); - }); - }); - - it('should display complete correctly', () => { - const element = createElement('x-wired-property', { is: WiredProperty }); - document.body.appendChild(element); - mockTestAdapter.complete(); - - return Promise.resolve().then(() => { - expect(element).toMatchSnapshot(); - }); - }); - }); - - describe('component lifecycle hooks', () => { - it('should get data when created', () => { - const element = createElement('x-wired-property', { is: WiredProperty }); - mockTestAdapter.next({ - Name: 'name' - }); - - document.body.appendChild(element); - - expect(element.textContent.substring('Name: '.length, element.textContent.indexOf('Error'))).toBe('name'); - }); - - it('should stop receiving data when disconnected', () => { - const element = createElement('x-wired-property', { is: WiredProperty }); - document.body.appendChild(element); - mockTestAdapter.next({ - Name: 'name' - }); - document.body.removeChild(element); - mockTestAdapter.next({ - Name: 'new_name' - }); - - expect(element.textContent.substring('Name: '.length, element.textContent.indexOf('Error'))).toBe(''); - }); - - it('should receive data when reconnected', () => { - const element = createElement('x-wired-property', { is: WiredProperty }); - document.body.appendChild(element); - mockTestAdapter.next({ - Name: 'name' - }); - document.body.removeChild(element); - mockTestAdapter.next({ - Name: 'new_name' - }); - document.body.appendChild(element); - - expect(element.textContent.substring('Name: '.length, element.textContent.indexOf('Error'))).toBe('new_name'); - }); - }); -}); diff --git a/packages/lwc-wire-service/src/modules/x/wired-property/wired-property.html b/packages/lwc-wire-service/src/modules/x/wired-property/wired-property.html deleted file mode 100644 index 968894bd65..0000000000 --- a/packages/lwc-wire-service/src/modules/x/wired-property/wired-property.html +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/packages/lwc-wire-service/src/modules/x/wired-property/wired-property.js b/packages/lwc-wire-service/src/modules/x/wired-property/wired-property.js deleted file mode 100644 index 81c482f9d5..0000000000 --- a/packages/lwc-wire-service/src/modules/x/wired-property/wired-property.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Element, api, wire } from 'engine'; - -export default class WiredProperty extends Element { - @api propName; - - @wire('test', { propName: '$propName', fields: ['Name'] }) - wiredField; - - get WiredName() { - return this.wiredField.data ? this.wiredField.data.Name : ''; - } - - get WiredError() { - return this.wiredField.error; - } -} From df5356fd8d5e51043345b19c535d0a661a5d757f Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 14:01:35 -0700 Subject: [PATCH 16/52] delete old jest tests --- .../src/__tests__/main.spec.js | 32 --- .../src/__tests__/wire-adapters.spec.js | 34 --- .../src/__tests__/wire-service.spec.js | 230 ----------------- .../src/__tests__/wired-value.spec.js | 239 ------------------ 4 files changed, 535 deletions(-) delete mode 100644 packages/lwc-wire-service/src/__tests__/main.spec.js delete mode 100644 packages/lwc-wire-service/src/__tests__/wire-adapters.spec.js delete mode 100644 packages/lwc-wire-service/src/__tests__/wire-service.spec.js delete mode 100644 packages/lwc-wire-service/src/__tests__/wired-value.spec.js diff --git a/packages/lwc-wire-service/src/__tests__/main.spec.js b/packages/lwc-wire-service/src/__tests__/main.spec.js deleted file mode 100644 index 0cb7e1587f..0000000000 --- a/packages/lwc-wire-service/src/__tests__/main.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import registerWireService from "../main.js"; - -describe("main.js", () => { - describe("registerWireService()", () => { - const mockAdapters = () => { - return {}; - }; - - it("registers for wiring hook", () => { - const register = jest.fn(); - registerWireService(register, mockAdapters); - expect(register).toHaveBeenCalledWith(expect.objectContaining({ - wiring: expect.any(Function) - })); - }); - // TODO W-4072588 - support connected + disconnected (repeated) cycles - // it("registers for connected hook", () => { - // const register = jest.fn(); - // registerWireService(register, mockAdapters); - // expect(register).toHaveBeenCalledWith(expect.objectContaining({ - // connected: expect.any(Function) - // })); - // }); - it("registers for disconnected hook", () => { - const register = jest.fn(); - registerWireService(register, mockAdapters); - expect(register).toHaveBeenCalledWith(expect.objectContaining({ - disconnected: expect.any(Function) - })); - }); - }); -}); diff --git a/packages/lwc-wire-service/src/__tests__/wire-adapters.spec.js b/packages/lwc-wire-service/src/__tests__/wire-adapters.spec.js deleted file mode 100644 index 38207994d7..0000000000 --- a/packages/lwc-wire-service/src/__tests__/wire-adapters.spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import { buildWireAdapterMap } from "../wire-adapters.js"; - -describe("wire-adapters.js", () => { - describe("buildWireAdapterMap", () => { - it("throws on duplicate wire adapter ids", () => { - const adapter = () => { - return {"a": () => {}}; - }; - const adapterDup = () => { - return {"a": () => {}}; - }; - expect(() => { - buildWireAdapterMap([adapter, adapterDup]); - }).toThrow(); - }); - - it("throws on non-function adapter handler", () => { - const adapter = () => { - return {"a": 1}; - }; - expect(() => { - buildWireAdapterMap([adapter]); - }).toThrow(); - }); - - it("accepts function identifier as adapter id", () => { - const myFunc = () => {}; - const adapter = () => { - return { myFunc }; - }; - expect(buildWireAdapterMap([adapter]).size).toBe(1); - }); - }); -}); diff --git a/packages/lwc-wire-service/src/__tests__/wire-service.spec.js b/packages/lwc-wire-service/src/__tests__/wire-service.spec.js deleted file mode 100644 index c9bf25e1c7..0000000000 --- a/packages/lwc-wire-service/src/__tests__/wire-service.spec.js +++ /dev/null @@ -1,230 +0,0 @@ -import * as target from "../wire-service.js"; - -describe("wire-service.js", () => { - describe("getPropToParams()", () => { - it("throws when dynamic param is also static", () => { - const wireDef = { - params: { staticAndDynamic: "prop1" }, - static: { staticAndDynamic: "foo" } - }; - expect(() => { - target.getPropToParams(wireDef, "target"); - }).toThrow(); - }); - it("maps a single prop to a single param", () => { - const expected = { - prop1: ["param1"] - }; - const wireDef = { - params: { param1: "prop1" }, - static: {} - }; - const propToParams = target.getPropToParams(wireDef, "target"); - expect(propToParams).toMatchObject(expected); - }); - it("maps a single prop to multiple params", () => { - const expected = { - prop1: ["param1", "param2"] - }; - const wireDef = { - params: { param1: "prop1", param2: "prop1" }, - static: {} - }; - const propToParams = target.getPropToParams(wireDef, "target"); - expect(propToParams).toMatchObject(expected); - }); - it("maps multiple props to multiple params", () => { - const expected = { - prop1: ["param1", "param2"], - prop2: ["param3"] - }; - const wireDef = { - params: { param1: "prop1", param2: "prop1", param3: "prop2" }, - static: {} - }; - const propToParams = target.getPropToParams(wireDef, "target"); - expect(propToParams).toMatchObject(expected); - }); - }); - - describe("getAdapter()", () => { - const knownFunc = () => {}; - const adapters = () => { - return { known: () => { }, knownFunc }; - }; - target.setWireAdapters([adapters]); - - it("throws with an unknown adapter id", () => { - const wireDef = { type: "unknown" }; - expect(() => { - target.getAdapter(wireDef, "target"); - }).toThrowError("Unknown wire adapter id 'unknown' in target's @wire('unknown', ...)"); - }); - it("returns with a known adapter id", () => { - const wireDef = { type: "known" }; - target.getAdapter(wireDef, "target"); - }); - it("throws with an unknown function identifier adapter id", () => { - const unknownFunc = () => {}; - const wireDef = { adapter: unknownFunc }; - expect(() => { - target.getAdapter(wireDef, "target"); - }).toThrowError("Unknown wire adapter id 'unknownFunc' in target's @wire(unknownFunc, ...)"); - }); - it("returns with a known function identifier adapter id", () => { - const wireDef = { adapter: knownFunc }; - target.getAdapter(wireDef, "target"); - }); - }); - - describe("getStaticConfig()", () => { - it("maps a single prop to a single param", () => { - const expected = { - param1: "prop1", - param2: "prop2" - }; - const wireDef = { - params: { param3: "prop3" }, - static: expected - }; - const propToParams = target.getStaticConfig(wireDef); - expect(propToParams).toMatchObject(expected); - }); - }); - - describe("getPropChangeHandlers()", () => { - it("maps one prop to one param to one handler", () => { - const wireConfigs = { - target: { - propToParams: { "prop1": ["param1"] } - } - }; - const wiredValues = { - target: { update: () => { } } - }; - const handlers = target.getPropChangeHandlers(wireConfigs, wiredValues); - expect(handlers.prop1).toHaveLength(1); - }); - it("maps one prop to multiple params to multiple hanlders for one prop", () => { - const wireConfigs = { - target: { - propToParams: { "prop1": ["param1", "param2"] } - } - }; - const wiredValues = { - target: { update: () => { } } - }; - const handlers = target.getPropChangeHandlers(wireConfigs, wiredValues); - expect(handlers.prop1).toHaveLength(2); - }); - it("maps same prop from multiple wires to multiple handlers for one prop", () => { - const wireConfigs = { - target1: { - propToParams: { "prop1": ["param1"] } - }, - target2: { - propToParams: { "prop1": ["param1"] } - } - }; - const wiredValues = { - target1: { update: () => { } }, - target2: { update: () => { } } - }; - const handlers = target.getPropChangeHandlers(wireConfigs, wiredValues); - expect(handlers.prop1).toHaveLength(2); - }); - it("binds param name to WiredValue.update", () => { - const wireConfigs = { - target: { - propToParams: { "prop1": ["param1", "param2"] } - } - }; - let actual; - const update = (param) => { - actual = param; - }; - const wiredValues = { - target: { update } - }; - const handlers = target.getPropChangeHandlers(wireConfigs, wiredValues); - handlers.prop1[0]("newValue1"); - expect(actual).toBe("param1"); - handlers.prop1[1]("newValue2"); - expect(actual).toBe("param2"); - }); - }); - - describe("installSetterOverrides()", () => { - it("defaults to original value when setter installed", () => { - class Target { - prop1 = 'initial' - } - const cmp = new Target(); - target.installSetterOverrides(cmp, { prop1: [jest.fn()] }); - expect(cmp.prop1).toBe('initial'); - }); - it("updates original property when installed setter invoked", () => { - const expected = 'expected'; - class Target { - prop1; - } - const cmp = new Target(); - target.installSetterOverrides(cmp, { prop1: [jest.fn()] }); - cmp.prop1 = expected; - expect(cmp.prop1).toBe(expected); - }); - it("installs setter on cmp for property", () => { - class Target { - set prop1(value) { /* empty */ } - } - const original = Object.getOwnPropertyDescriptor(Target.prototype, "prop1"); - const cmp = new Target(); - target.installSetterOverrides(cmp, { prop1: "empty" }); - const descriptor = Object.getOwnPropertyDescriptor(cmp, "prop1"); - expect(descriptor.set).not.toBe(original.set); - }); - it("invokes original setter when installed setter invoked", () => { - const setter = jest.fn(); - const expected = 'expected'; - class Target { - set prop1(value) { - setter(value); - } - get prop1() { /* empty */ } - } - const cmp = new Target(); - target.installSetterOverrides(cmp, { prop1: [jest.fn()] }); - cmp.prop1 = expected; - expect(setter).toHaveBeenCalledTimes(1); - expect(setter).toHaveBeenCalledWith(expected); - }); - - it("invokes original getter once when installed setter invoked", () => { - const getter = jest.fn(); - class Target { - set prop1(value) { /* empty */ } - get prop1() { - getter(); - } - } - const cmp = new Target(); - target.installSetterOverrides(cmp, { prop1: [jest.fn()] }); - cmp.prop1 = ''; - expect(getter).toHaveBeenCalledTimes(1); - }); - it("uses getter return value, not setter argument value, as the new value", () => { - const expected = 1; - class Target { - set prop1(value) { /* empty */ } - get prop1() { - return expected; - } - } - const cmp = new Target(); - const handler = jest.fn(); - target.installSetterOverrides(cmp, { prop1: [handler] }); - cmp.prop1 = expected + 1; // a different value than the getter's return value - expect(handler).toHaveBeenCalledWith(expected); - }); - }); -}); diff --git a/packages/lwc-wire-service/src/__tests__/wired-value.spec.js b/packages/lwc-wire-service/src/__tests__/wired-value.spec.js deleted file mode 100644 index 2db7b87cf9..0000000000 --- a/packages/lwc-wire-service/src/__tests__/wired-value.spec.js +++ /dev/null @@ -1,239 +0,0 @@ -import { WiredValue } from "../wired-value.js"; - -describe("wired-value.js", () => { - it("data and error undefined by default", () => { - const wiredValue = new WiredValue(); - expect(wiredValue.data).toBe(undefined); - expect(wiredValue.error).toBe(undefined); - }); - - describe("update()", () => { - it("sets new value on config", () => { - const config = { "foo": "original value" }; - const wiredValue = new WiredValue(undefined, config); - wiredValue.provide = jest.fn(); - wiredValue.update("foo", "new value"); - expect(wiredValue.config.foo).toBe("new value"); - }); - it("calls provide() when new value set", () => { - const config = { "foo": "original value" }; - const wiredValue = new WiredValue(undefined, config); - wiredValue.provide = jest.fn(); - wiredValue.update("foo", "new value"); - expect(wiredValue.provide).toHaveBeenCalled(); - }); - it("does not call provide() when same value set", () => { - const config = { "foo": "original value" }; - const wiredValue = new WiredValue(undefined, config); - wiredValue.provide = jest.fn(); - wiredValue.update("foo", "original value"); - expect(wiredValue.provide).not.toHaveBeenCalled(); - }); - it("calls release() when new value set", () => { - const config = { "foo": "original value" }; - const wiredValue = new WiredValue(undefined, config); - wiredValue.release = jest.fn(); - wiredValue.provide = jest.fn(); - wiredValue.update("foo", "new value"); - expect(wiredValue.release).toHaveBeenCalled(); - }); - it("does not call release() when same value set", () => { - const config = { "foo": "original value" }; - const wiredValue = new WiredValue(undefined, config); - wiredValue.release = jest.fn(); - wiredValue.provide = jest.fn(); - wiredValue.update("foo", "original value"); - expect(wiredValue.release).not.toHaveBeenCalled(); - }); - }); - - describe("provide()", () => { - it("calls _provide()", done => { - const wiredValue = new WiredValue(); - wiredValue._provide = jest.fn(() => done()); - wiredValue.provide(); - }); - it("multiple invocations calls _provide() once", () => { - const wiredValue = new WiredValue(); - const mock = jest.fn(); - wiredValue._provide = mock; - wiredValue.provide(); - wiredValue.provide(); - return Promise.resolve().then(() => { - expect(mock).toHaveBeenCalledTimes(1); - }); - }); - }); - - describe("install()", () => { - it("wired field - assigns object with only data and error", () => { - const cmp = {}; - const propName = "target"; - const wiredValue = new WiredValue(jest.fn(), {}, false, cmp, propName); - wiredValue.install(); - expect(cmp[propName]).toEqual({ data: undefined, error: undefined }); - }); - it("wired method - does not assign self to component", () => { - const cmp = {}; - const propName = "target"; - const wiredMethod = jest.fn(); - cmp[propName] = wiredMethod; - const wiredValue = new WiredValue(jest.fn(), {}, true, cmp, propName); - wiredValue.install(); - expect(cmp[propName]).toBe(wiredMethod); - }); - }); - - describe("_provide()", () => { - it("calls adapter with config", () => { - const adapter = jest.fn(); - const config = "expected"; - const wiredValue = new WiredValue(adapter, config); - wiredValue.release = jest.fn(); - wiredValue._provide(); - expect(adapter).toHaveBeenCalledWith(config); - }); - it("calls subscribe on adapter's return", () => { - const mock = jest.fn(); - const adapter = () => { - return { - subscribe: mock - }; - }; - const config = {}; - const wiredValue = new WiredValue(adapter, config); - wiredValue._provide(); - expect(mock).toHaveBeenCalledTimes(1); - }); - it("sets subscription from adapter's observable.subscribe() return", () => { - const expected = "expected"; - const adapter = () => { - return { - subscribe: () => { - return expected; - } - }; - }; - const config = {}; - const wiredValue = new WiredValue(adapter, config); - wiredValue._provide(); - expect(wiredValue.subscription).toBe(expected); - }); - }); - - describe("getObserver()", () => { - it("wired field - next sets data, clears error", () => { - const expected = { value: "foo" }; - const cmp = {}; - const wiredValue = new WiredValue(jest.fn(), {}, false, cmp, "target"); - wiredValue.install(); - const observer = wiredValue.getObserver(); - observer.next(expected); - expect(cmp.target.data).toBe(expected); - expect(cmp.target.error).toBe(undefined); - }); - it("wired field - error sets error, clears data", () => { - const expected = new Error("error"); - const cmp = {}; - const wiredValue = new WiredValue(jest.fn(), {}, false, cmp, "target"); - wiredValue.install(); - const observer = wiredValue.getObserver(); - observer.error(expected); - expect(cmp.target.data).toBe(undefined); - expect(cmp.target.error).toBe(expected); - }); - it("wired field - complete invokes completeHandler, leaves data", () => { - const expected = { value: "foo" }; - const mock = jest.fn(); - const cmp = {}; - const wiredValue = new WiredValue(jest.fn(), {}, false, cmp, "target"); - wiredValue.install(); - const observer = wiredValue.getObserver(); - wiredValue.completeHandler = mock; - observer.next(expected); - observer.complete(); - expect(mock).toHaveBeenCalledTimes(1); - expect(cmp.target.data).toBe(expected); - expect(cmp.target.error).toBe(undefined); - }); - it("wired method - next invokes method with value, no error", () => { - const expected = { value: "foo" }; - const cmp = { - target: jest.fn() - }; - const wiredValue = new WiredValue(jest.fn(), {}, true, cmp, "target"); - wiredValue.install(); - const observer = wiredValue.getObserver(); - observer.next(expected); - expect(cmp.target.mock.calls).toHaveLength(1); - expect(cmp.target.mock.calls[0][0].error).toBe(null); - expect(cmp.target.mock.calls[0][0].data).toBe(expected); - }); - it("wired method - error invokes method with error, no value", () => { - const expected = {}; - const cmp = { - target: jest.fn() - }; - const wiredValue = new WiredValue(jest.fn(), {}, true, cmp, "target"); - wiredValue.install(); - const observer = wiredValue.getObserver(); - observer.error(expected); - expect(cmp.target.mock.calls).toHaveLength(1); - expect(cmp.target.mock.calls[0][0].error).toBe(expected); - expect(cmp.target.mock.calls[0][0].data).toBe(undefined); - }); - it("wired method - complete invokes completeHandler, does not invoke method", () => { - const expected = { value: "foo" }; - const mock = jest.fn(); - const cmp = { - target: jest.fn() - }; - const wiredValue = new WiredValue(jest.fn(), {}, true, cmp, "target"); - wiredValue.install(); - wiredValue.completeHandler = mock; - const observer = wiredValue.getObserver(); - observer.next(expected); - observer.complete(); - expect(mock).toHaveBeenCalledTimes(1); - expect(cmp.target.mock.calls).toHaveLength(1); - }); - }); - - describe("completeHandler()", () => { - it("calls release then provide()", () => { - const wiredValue = new WiredValue(); - const mock = jest.fn(); - wiredValue.provide = mock; - wiredValue.completeHandler(); - expect(mock).toHaveBeenCalledTimes(1); - }); - it("avoids provide storm by limiting provide() invocations", () => { - const wiredValue = new WiredValue(); - const mock = jest.fn(); - wiredValue.provide = mock; - wiredValue.completeHandler(); - wiredValue.completeHandler(); - expect(mock).toHaveBeenCalledTimes(1); - }); - }); - - describe("release()", () => { - it("calls unsubscribe()", () => { - const mock = jest.fn(); - const wiredValue = new WiredValue(); - wiredValue.subscription = {}; - wiredValue.subscription.unsubscribe = mock; - wiredValue.release(); - expect(mock).toHaveBeenCalledTimes(1); - }); - it("multiple invocations calls unsubscribe() once", () => { - const mock = jest.fn(); - const wiredValue = new WiredValue(); - wiredValue.subscription = {}; - wiredValue.subscription.unsubscribe = mock; - wiredValue.release(); - wiredValue.release(); - expect(mock).toHaveBeenCalledTimes(1); - }); - }); -}); From 1b50bfae52556e024afc0f36262e6fa17a9d36b2 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 15:14:13 -0700 Subject: [PATCH 17/52] fix wire lwc-integration tests --- .../wired-method/wired-method.js | 6 ++-- .../wired-prop/wired-prop.js | 4 +-- .../lwc-integration/src/shared/templates.js | 31 ++++++++++++++++--- packages/lwc-integration/src/shared/todo.js | 10 ++++-- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/lwc-integration/src/components/wired/test-wired-method-suite/wired-method/wired-method.js b/packages/lwc-integration/src/components/wired/test-wired-method-suite/wired-method/wired-method.js index c0f3a06110..2c986b6d73 100644 --- a/packages/lwc-integration/src/components/wired/test-wired-method-suite/wired-method/wired-method.js +++ b/packages/lwc-integration/src/components/wired/test-wired-method-suite/wired-method/wired-method.js @@ -1,12 +1,12 @@ import { Element, api, track, wire } from 'engine'; -import { serviceTodo } from 'todo'; +import { getTodo } from 'todo'; export default class WiredMethod extends Element { @api todoId; @track state = { error: undefined, todo: undefined }; - @wire(serviceTodo, { id: '$todoId' }) - function(error, data) { + @wire(getTodo, { id: '$todoId' }) + function({error, data}) { this.state = { error: error, todo: data }; } } diff --git a/packages/lwc-integration/src/components/wired/test-wired-prop-suite/wired-prop/wired-prop.js b/packages/lwc-integration/src/components/wired/test-wired-prop-suite/wired-prop/wired-prop.js index e75cbfce0d..73cc977ee8 100644 --- a/packages/lwc-integration/src/components/wired/test-wired-prop-suite/wired-prop/wired-prop.js +++ b/packages/lwc-integration/src/components/wired/test-wired-prop-suite/wired-prop/wired-prop.js @@ -1,9 +1,9 @@ import { Element, api, wire } from 'engine'; -import { serviceTodo } from 'todo'; +import { getTodo } from 'todo'; export default class WiredProp extends Element { @api todoId; - @wire(serviceTodo, { id: '$todoId' }) + @wire(getTodo, { id: '$todoId' }) todo; get error() { diff --git a/packages/lwc-integration/src/shared/templates.js b/packages/lwc-integration/src/shared/templates.js index 62bae19e99..340c91e4d8 100644 --- a/packages/lwc-integration/src/shared/templates.js +++ b/packages/lwc-integration/src/shared/templates.js @@ -9,14 +9,37 @@ exports.app = function (cmpName) { exports.todoApp = function (cmpName) { return ` - import { serviceTodo } from 'todo'; - import registerWireService from 'wire-service'; + import { registerWireService, register as registerAdapter } from 'wire-service'; import { createElement, register } from 'engine'; import Cmp from '${cmpName}'; + import { getTodo, getObservable } from 'todo'; - registerWireService(register, function () { + registerWireService(register); + + // Register the wire adapter for @wire(getTodo). + registerAdapter(getTodo, function getTodoWireAdapter(targetSetter) { + let subscription; + let config; return { - serviceTodo + updatedCallback: (newConfig) => { + config = newConfig; + subscription = getObservable(config).subscribe({ + next: data => targetSetter({ data, error: undefined }), + error: error => targetSetter({ data: undefined, error }) + }); + }, + + connectedCallback: () => { + // Subscribe to stream. + subscription = getObservable(config).subscribe({ + next: data => targetSetter({ data, error: undefined }), + error: error => targetSetter({ data: undefined, error }) + }); + }, + + disconnectedCallback: () => { + subscription.unsubscribe(); + } }; }); diff --git a/packages/lwc-integration/src/shared/todo.js b/packages/lwc-integration/src/shared/todo.js index 4f2fbbba78..613f1aae72 100644 --- a/packages/lwc-integration/src/shared/todo.js +++ b/packages/lwc-integration/src/shared/todo.js @@ -66,7 +66,7 @@ var TODO = [ }, {}); -function serviceTodo(config) { +function getObservable(config) { if (!('id' in config)) { return undefined; } @@ -80,6 +80,10 @@ function serviceTodo(config) { return getSubject(todo).observable; } - exports.serviceTodo = serviceTodo; +function getTodo(config) { + // not implemented +} + exports.getTodo = getTodo; + exports.getObservable = getObservable; Object.defineProperty(exports, '__esModule', { value: true }); -}))); \ No newline at end of file +}))); From 8a84e5556a1f1a4113c4c15a01c8fd1c26866fc4 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 15:29:44 -0700 Subject: [PATCH 18/52] optimize building context --- packages/lwc-wire-service/src/index.ts | 44 +++++++++++--------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 4d58816c3e..68e2ee132b 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -94,30 +94,6 @@ function getPropertyValues(cmp: Element, properties: Set) { return resolvedValues; } -/** - * Build context payload. - */ -function buildServiceContext(adapters: WireAdapter[]) { - const context: Map = Object.create(null); - - const noArgCallbackKeys: Array = ['connectedCallback', 'disconnectedCallback']; - for (let i = 0; i < noArgCallbackKeys.length; i++) { - const noArgCallbackKey = noArgCallbackKeys[i]; - const wireNoArgCallbacks: WireAdapterCallback[] = []; - for (let j = 0; j < adapters.length; j++) { - const wireNoArgCallback = adapters[j][noArgCallbackKey]; - if (wireNoArgCallback) { - wireNoArgCallbacks.push(wireNoArgCallback); - } - } - if (wireNoArgCallbacks.length > 0) { - context[noArgCallbackKey] = wireNoArgCallbacks; - } - } - - return context; -} - // TODO - in early 216, engine will expose an `updated` callback for services that // is invoked whenever a tracked property is changed. wire service is structured to // make this adoption trivial. @@ -169,6 +145,8 @@ const wireService = { const wireDefs: WireDef[] = []; const updatedCallbackKey = 'updatedCallback'; const updatedCallbackConfigs: UpdatedCallbackConfig[] = []; + const connectedNoArgCallbacks: NoArgumentCallback[] = []; + const disconnectedNoArgCallbacks: NoArgumentCallback[] = []; for (let i = 0; i < wireTargets.length; i++) { const wireTarget = wireTargets[i]; const wireDef = wireStaticDef[wireTarget]; @@ -188,6 +166,14 @@ const wireService = { if (adapterFactory) { const wireAdapter = adapterFactory(targetSetter); adapters.push(wireAdapter); + const connectedCallback = wireAdapter[CONNECTED]; + if (connectedCallback) { + connectedNoArgCallbacks.push(connectedCallback); + } + const disconnectedCallback = wireAdapter[DISCONNECTED]; + if (disconnectedCallback) { + disconnectedNoArgCallbacks.push(disconnectedCallback); + } const updatedCallback = wireAdapter[updatedCallbackKey]; if (updatedCallback) { updatedCallbackConfigs.push({ @@ -231,7 +217,15 @@ const wireService = { }); // cache context that optimizes runtime of service callbacks - context[CONTEXT_ID] = buildServiceContext(adapters); + context[CONTEXT_ID] = Object.create(null); + if (connectedNoArgCallbacks.length > 0) { + context[CONTEXT_ID][CONNECTED] = connectedNoArgCallbacks; + } + + if (disconnectedNoArgCallbacks.length > 0) { + context[CONTEXT_ID][DISCONNECTED] = disconnectedNoArgCallbacks; + } + if (updatedCallbackConfigs.length > 0) { const ucContext: ServiceUpdateContext = { callbacks: updatedCallbackConfigs, From c15dd48980b533911301e7884596ca6b337678dd Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 15:42:35 -0700 Subject: [PATCH 19/52] refactor to buildContext --- packages/lwc-wire-service/src/index.ts | 44 ++++++++++++++++---------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 68e2ee132b..7bef2f7e48 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -94,6 +94,33 @@ function getPropertyValues(cmp: Element, properties: Set) { return resolvedValues; } +function buildContext( + connectedNoArgCallbacks: NoArgumentCallback[], + disconnectedNoArgCallbacks: NoArgumentCallback[], + updatedCallbackConfigs: UpdatedCallbackConfig[], + props: Set +) { + // cache context that optimizes runtime of service callbacks + const wireContext = Object.create(null); + if (connectedNoArgCallbacks.length > 0) { + wireContext[CONNECTED] = connectedNoArgCallbacks; + } + + if (disconnectedNoArgCallbacks.length > 0) { + wireContext[DISCONNECTED] = disconnectedNoArgCallbacks; + } + + if (updatedCallbackConfigs.length > 0) { + const ucContext: ServiceUpdateContext = { + callbacks: updatedCallbackConfigs, + paramValues: props + }; + wireContext[UPDATED] = ucContext; + } + + return wireContext; +} + // TODO - in early 216, engine will expose an `updated` callback for services that // is invoked whenever a tracked property is changed. wire service is structured to // make this adoption trivial. @@ -217,22 +244,7 @@ const wireService = { }); // cache context that optimizes runtime of service callbacks - context[CONTEXT_ID] = Object.create(null); - if (connectedNoArgCallbacks.length > 0) { - context[CONTEXT_ID][CONNECTED] = connectedNoArgCallbacks; - } - - if (disconnectedNoArgCallbacks.length > 0) { - context[CONTEXT_ID][DISCONNECTED] = disconnectedNoArgCallbacks; - } - - if (updatedCallbackConfigs.length > 0) { - const ucContext: ServiceUpdateContext = { - callbacks: updatedCallbackConfigs, - paramValues: props - }; - context[CONTEXT_ID][UPDATED] = ucContext; - } + context[CONTEXT_ID] = buildContext(connectedNoArgCallbacks, disconnectedNoArgCallbacks, updatedCallbackConfigs, props); }, connected: (cmp: Element, data: object, def: ElementDef, context: object) => { From 18603ca07ff04e0c9159120bf949887e62ae286b Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 15:47:56 -0700 Subject: [PATCH 20/52] update type for buildContext --- packages/lwc-wire-service/src/index.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 7bef2f7e48..31b4fc3c17 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -40,9 +40,6 @@ export interface ServiceUpdateContext { } export type ServiceContext = NoArgumentCallback[] | ServiceUpdateContext; -// lifecycle hooks of wire adapters -// const HOOKS: Array = ['updatedCallback', 'connectedCallback', 'disconnectedCallback']; - // key for engine service context store const CONTEXT_ID: string = '@wire'; @@ -99,9 +96,9 @@ function buildContext( disconnectedNoArgCallbacks: NoArgumentCallback[], updatedCallbackConfigs: UpdatedCallbackConfig[], props: Set -) { +): Map { // cache context that optimizes runtime of service callbacks - const wireContext = Object.create(null); + const wireContext: Map = Object.create(null); if (connectedNoArgCallbacks.length > 0) { wireContext[CONNECTED] = connectedNoArgCallbacks; } From ae067638e45e5c8aa63798b649fe21716a031b1a Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 15:57:02 -0700 Subject: [PATCH 21/52] export unregister for non prod --- packages/lwc-wire-service/src/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 31b4fc3c17..e12d9de3be 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -276,3 +276,12 @@ export function register(adapterId: any, adapterFactory: WireAdapterFactory) { assert.isTrue(typeof adapterFactory === 'function', 'adapter factory must be a function'); adapterFactories.set(adapterId, adapterFactory); } + +/* + * Unregisters an adapter, only available for non prod (e.g. test util) + */ +export function unregister(adapterId: any) { + if (process.env.NODE_ENV !== 'production') { + adapterFactories.delete(adapterId); + } +} From 07a327ce94222d149ca2081d450eba414df817a6 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 19 Mar 2018 16:25:13 -0700 Subject: [PATCH 22/52] add unit test --- jest.config.js | 7 +---- packages/lwc-wire-service/jest.config.js | 22 -------------- .../src/__tests__/index.spec.ts | 29 +++++++++++++++++++ 3 files changed, 30 insertions(+), 28 deletions(-) delete mode 100644 packages/lwc-wire-service/jest.config.js create mode 100644 packages/lwc-wire-service/src/__tests__/index.spec.ts diff --git a/jest.config.js b/jest.config.js index b795af0803..f7ae879312 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,11 +8,6 @@ module.exports = { '/packages/*/**/__tests__/*.spec.(js|ts)' ], projects: [ - '', - '/packages/*/jest.config.js' - ], - testPathIgnorePatterns: [ - 'node_modules', - 'lwc-wire-service' + '' ] }; diff --git a/packages/lwc-wire-service/jest.config.js b/packages/lwc-wire-service/jest.config.js deleted file mode 100644 index 52236431c8..0000000000 --- a/packages/lwc-wire-service/jest.config.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - moduleFileExtensions: ['js', 'html'], - "moduleNameMapper": { - "^engine$": require.resolve('lwc-engine').replace('commonjs', 'modules'), - "^wire-service$": "/src/main.js", - "^x-(.+)$": "/src/modules/x/$1/$1", - ".css$": "/global-jest-stub.js", - }, - "transform": { - "^.+\\.(js|html)$": "lwc-jest-transformer" - }, - collectCoverageFrom: ['src/*.js', '!**/__tests__/**'], - coverageReporters: ['lcov', 'text', 'text-summary', 'html'], - coverageThreshold: { - global: { - branches: 78, - functions: 93, - lines: 95, - statements: 95 - } - } -}; diff --git a/packages/lwc-wire-service/src/__tests__/index.spec.ts b/packages/lwc-wire-service/src/__tests__/index.spec.ts new file mode 100644 index 0000000000..b680d9212d --- /dev/null +++ b/packages/lwc-wire-service/src/__tests__/index.spec.ts @@ -0,0 +1,29 @@ +import { + registerWireService, +} from '../index'; + +describe('wire service', () => { + describe('registers the service with engine', () => { + it('supports wiring hook', () => { + const mockEngineRegister = jest.fn(); + registerWireService(mockEngineRegister); + expect(mockEngineRegister).toHaveBeenCalledWith(expect.objectContaining({ + wiring: expect.any(Function) + })); + }); + it('supports connected hook', () => { + const mockEngineRegister = jest.fn(); + registerWireService(mockEngineRegister); + expect(mockEngineRegister).toHaveBeenCalledWith(expect.objectContaining({ + connected: expect.any(Function) + })); + }); + it('supports disconnected hook', () => { + const mockEngineRegister = jest.fn(); + registerWireService(mockEngineRegister); + expect(mockEngineRegister).toHaveBeenCalledWith(expect.objectContaining({ + disconnected: expect.any(Function) + })); + }); + }); +}); From 83f6e98ba2f4c0af2031cbc249370bc47ac56d4a Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Tue, 20 Mar 2018 10:22:24 -0700 Subject: [PATCH 23/52] refactor wiring internals --- .../src/__tests__/wiring.spec.ts | 80 ++++++++ packages/lwc-wire-service/src/constants.ts | 8 + packages/lwc-wire-service/src/index.ts | 168 +++-------------- packages/lwc-wire-service/src/shared-types.ts | 26 ++- packages/lwc-wire-service/src/wiring.ts | 177 ++++++++++++++++++ 5 files changed, 310 insertions(+), 149 deletions(-) create mode 100644 packages/lwc-wire-service/src/__tests__/wiring.spec.ts create mode 100644 packages/lwc-wire-service/src/constants.ts create mode 100644 packages/lwc-wire-service/src/wiring.ts diff --git a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts new file mode 100644 index 0000000000..a4b9a3182e --- /dev/null +++ b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts @@ -0,0 +1,80 @@ +import { CONNECTED, DISCONNECTED, UPDATED } from '../constants'; +import * as target from '../wiring'; + +describe('wiring internal', () => { + it('gets bound properties from wire definition', () => { + const wireDef = [ + { params: {key1: 'prop1', key2: 'prop2'} }, + { params: {key1: 'prop1', key2: 'prop3'} } + ]; + const actual = target.getPropsFromParams(wireDef); + expect(Array.from(actual)).toEqual(['prop1', 'prop2', 'prop3']); + }); + + describe('installs setter overrides', () => { + it("defaults to original value when setter installed", () => { + class Target { + prop1 = 'initial'; + } + const cmp = new Target(); + target.installSetterOverrides(cmp, 'prop1', jest.fn()); + expect(cmp.prop1).toBe('initial'); + }); + it("updates original property when installed setter invoked", () => { + const expected = 'expected'; + class Target { + prop1; + } + const cmp = new Target(); + target.installSetterOverrides(cmp, 'prop1', jest.fn()); + cmp.prop1 = expected; + expect(cmp.prop1).toBe(expected); + }); + it("installs setter on cmp for property", () => { + class Target { + set prop1(value) { /* empty */ } + } + const original = Object.getOwnPropertyDescriptor(Target.prototype, "prop1"); + const cmp = new Target(); + target.installSetterOverrides(cmp, 'prop1', jest.fn()); + const descriptor = Object.getOwnPropertyDescriptor(cmp, "prop1"); + if (descriptor && original) { + expect(descriptor.set).not.toBe(original.set); + } + }); + it("invokes original setter when installed setter invoked", () => { + const setter = jest.fn(); + const expected = 'expected'; + class Target { + set prop1(value) { + setter(value); + } + get prop1() { return ''; } + } + const cmp = new Target(); + target.installSetterOverrides(cmp, 'prop1', jest.fn()); + cmp.prop1 = expected; + expect(setter).toHaveBeenCalledTimes(1); + expect(setter).toHaveBeenCalledWith(expected); + }); + }); + + describe('builds wire service context', () => { + it('includes connected callback if any', () => { + expect(target.buildContext([jest.fn()], [], [], new Set())[CONNECTED]).toHaveLength(1); + }); + it('includes disconnected callback if any', () => { + expect(target.buildContext([], [jest.fn()], [], new Set())[DISCONNECTED]).toHaveLength(1); + }); + it('includes updated callback config if any', () => { + const updatedCallbackConfigs = [{ + updatedCallback: jest.fn() + }]; + const paramValues = new Set(['prop1']); + expect(target.buildContext([], [], updatedCallbackConfigs, paramValues)[UPDATED]).toEqual({ + callbacks: updatedCallbackConfigs, + paramValues + }); + }); + }); +}); diff --git a/packages/lwc-wire-service/src/constants.ts b/packages/lwc-wire-service/src/constants.ts new file mode 100644 index 0000000000..cd6b376ffa --- /dev/null +++ b/packages/lwc-wire-service/src/constants.ts @@ -0,0 +1,8 @@ +// key for engine service context store +export const CONTEXT_ID: string = '@wire'; +// key for wire service context updated context metadata +export const UPDATED: string = 'updated'; +// key for wire service context connected callbacks +export const CONNECTED: string = 'connectedCallback'; +// key for wire service context disconnected callbacks +export const DISCONNECTED: string = 'disconnectedCallback'; diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index e12d9de3be..78238126b5 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -7,15 +7,30 @@ import { Element } from 'engine'; import assert from './assert'; -import { WireDef, ElementDef } from './shared-types'; - +import { + WireDef, + ElementDef, + NoArgumentCallback, + UpdatedCallback, + UpdatedCallbackConfig +} from './shared-types'; +import { + CONTEXT_ID, + CONNECTED, + DISCONNECTED +} from './constants'; +import { + updated, + getPropsFromParams, + installSetterOverrides, + buildContext +} from './wiring'; export interface WiredValue { data?: any; error?: any; } export type TargetSetter = (WiredValue) => void; -export type UpdatedCallback = (object) => void; -export type NoArgumentCallback = () => void; + export type WireAdapterCallback = UpdatedCallback | NoArgumentCallback; export interface WireAdapter { updatedCallback?: UpdatedCallback; @@ -24,34 +39,12 @@ export interface WireAdapter { } export type WireAdapterFactory = (targetSetter: TargetSetter) => WireAdapter; -export interface UpdatedCallbackConfig { - updatedCallback: UpdatedCallback; - statics: { - [key: string]: any; - }; - params: { - [key: string]: string; - }; -} -export interface ServiceUpdateContext { - callbacks: UpdatedCallbackConfig[]; - // union of callbacks.params values - paramValues: Set; -} -export type ServiceContext = NoArgumentCallback[] | ServiceUpdateContext; - -// key for engine service context store -const CONTEXT_ID: string = '@wire'; - // wire adapters: wire adapter id => adapter ctor const adapterFactories: Map = new Map(); -const UPDATED: string = 'updated'; -const CONNECTED: string = 'connectedCallback'; -const DISCONNECTED: string = 'disconnectedCallback'; - /** * Invokes the specified callbacks. + * @param callbacks functions to call */ function invokeCallback(callbacks: NoArgumentCallback[]) { for (let i = 0, len = callbacks.length; i < len; ++i) { @@ -59,100 +52,6 @@ function invokeCallback(callbacks: NoArgumentCallback[]) { } } -/** - * Invokes the provided updated callbacks with the resolved component properties. - */ -function invokeUpdatedCallback(ucMetadatas: UpdatedCallbackConfig[], paramValues: any) { - for (let i = 0, len = ucMetadatas.length; i < len; ++i) { - const { updatedCallback, statics, params } = ucMetadatas[i]; - - const resolvedParams = Object.create(null); - const keys = Object.keys(params); - for (let j = 0, jlen = keys.length; j < jlen; j++) { - const key = keys[j]; - const value = paramValues[params[key]]; - resolvedParams[key] = value; - } - const config = Object.assign(Object.create(null), statics, resolvedParams); - updatedCallback.call(undefined, config); - } -} - -/** - * Gets resolved values of the specified properties. - */ -function getPropertyValues(cmp: Element, properties: Set) { - const resolvedValues = Object.create(null); - properties.forEach((property) => { - const paramValue = property; - resolvedValues[paramValue] = cmp[paramValue]; - }); - - return resolvedValues; -} - -function buildContext( - connectedNoArgCallbacks: NoArgumentCallback[], - disconnectedNoArgCallbacks: NoArgumentCallback[], - updatedCallbackConfigs: UpdatedCallbackConfig[], - props: Set -): Map { - // cache context that optimizes runtime of service callbacks - const wireContext: Map = Object.create(null); - if (connectedNoArgCallbacks.length > 0) { - wireContext[CONNECTED] = connectedNoArgCallbacks; - } - - if (disconnectedNoArgCallbacks.length > 0) { - wireContext[DISCONNECTED] = disconnectedNoArgCallbacks; - } - - if (updatedCallbackConfigs.length > 0) { - const ucContext: ServiceUpdateContext = { - callbacks: updatedCallbackConfigs, - paramValues: props - }; - wireContext[UPDATED] = ucContext; - } - - return wireContext; -} - -// TODO - in early 216, engine will expose an `updated` callback for services that -// is invoked whenever a tracked property is changed. wire service is structured to -// make this adoption trivial. -function updated(cmp: Element, data: object, def: ElementDef, context: object) { - let ucMetadata: ServiceUpdateContext; - if (!def.wire || !(ucMetadata = context[CONTEXT_ID][UPDATED])) { - return; - } - - // get new values for all dynamic props - const paramValues = getPropertyValues(cmp, ucMetadata.paramValues); - - // compare new to old dynamic prop values, updating old props with new values - // for each change, queue the impacted adapter(s) - // TODO: do we really need this if updated is only hooked to bound props? - - // process queue of impacted adapters - invokeUpdatedCallback(ucMetadata.callbacks, paramValues); -} - -function getPropsFromParams(wireDefs: WireDef[]) { - const props = new Set(); - wireDefs.forEach((wireDef) => { - const { params } = wireDef; - if (params) { - Object.keys(params).forEach(param => { - const prop = params[param]; - props.add(prop); - }); - } - }); - - return props; -} - /** * The wire service. * @@ -212,32 +111,7 @@ const wireService = { // only add updated to bound props const props = getPropsFromParams(wireDefs); props.forEach((prop) => { - const originalDescriptor = Object.getOwnPropertyDescriptor(cmp.constructor.prototype, prop); - let newDescriptor; - if (originalDescriptor) { - newDescriptor = Object.assign({}, originalDescriptor, { - set(value) { - if (originalDescriptor.set) { - originalDescriptor.set.call(cmp, value); - } - updated.call(this, cmp, data, def, context); - } - }); - } else { - const propSymbol = Symbol(prop); - newDescriptor = { - get() { - return cmp[propSymbol]; - }, - set(value) { - cmp[propSymbol] = value; - updated.call(this, cmp, data, def, context); - } - }; - // grab the existing value - cmp[propSymbol] = cmp[prop]; - } - Object.defineProperty(cmp, prop, newDescriptor); + installSetterOverrides(cmp, prop, updated.bind(undefined, cmp, data, def, context)); }); // cache context that optimizes runtime of service callbacks diff --git a/packages/lwc-wire-service/src/shared-types.ts b/packages/lwc-wire-service/src/shared-types.ts index 78a6c7e9ec..3c1cfe8be1 100644 --- a/packages/lwc-wire-service/src/shared-types.ts +++ b/packages/lwc-wire-service/src/shared-types.ts @@ -1,8 +1,8 @@ export interface WireDef { - params: { + params?: { [key: string]: string; }; - static: { + static?: { [key: string]: any; }; type?: string; @@ -14,3 +14,25 @@ export interface ElementDef { [key: string]: WireDef }; } + +export type NoArgumentCallback = () => void; + +export type UpdatedCallback = (object) => void; + +export interface UpdatedCallbackConfig { + updatedCallback: UpdatedCallback; + statics?: { + [key: string]: any; + }; + params?: { + [key: string]: string; + }; +} + +export interface ServiceUpdateContext { + callbacks: UpdatedCallbackConfig[]; + // union of callbacks.params values + paramValues: Set; +} + +export type ServiceContext = NoArgumentCallback[] | ServiceUpdateContext; diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts new file mode 100644 index 0000000000..a45c5b7e7b --- /dev/null +++ b/packages/lwc-wire-service/src/wiring.ts @@ -0,0 +1,177 @@ +import { Element } from 'engine'; +import { + WireDef, + ElementDef, + NoArgumentCallback, + UpdatedCallbackConfig, + ServiceUpdateContext, + ServiceContext +} from './shared-types'; +import { + CONTEXT_ID, + UPDATED, + CONNECTED, + DISCONNECTED +} from './constants'; + +/** + * Invokes the provided updated callbacks with the resolved component properties. + * @param ucMetadatas wire updated service context metadata + * @param paramValues values for all wire adapter config params + */ +function invokeUpdatedCallback(ucMetadatas: UpdatedCallbackConfig[], paramValues: any) { + for (let i = 0, len = ucMetadatas.length; i < len; ++i) { + const { updatedCallback, statics, params } = ucMetadatas[i]; + + const resolvedParams = Object.create(null); + if (params) { + const keys = Object.keys(params); + for (let j = 0, jlen = keys.length; j < jlen; j++) { + const key = keys[j]; + const value = paramValues[params[key]]; + resolvedParams[key] = value; + } + } + + const config = Object.assign(Object.create(null), statics, resolvedParams); + updatedCallback.call(undefined, config); + } +} + +/** + * Gets resolved values of the specified properties. + * @param cmp component to get property value from + * @param properties a set of bound property + */ +function getPropertyValues(cmp: Element, properties: Set) { + const resolvedValues = Object.create(null); + properties.forEach((property) => { + resolvedValues[property] = cmp[property]; + }); + + return resolvedValues; +} + +/** + * TODO - in early 216, engine will expose an `updated` callback for services that + * is invoked whenever a tracked property is changed. wire service is structured to + * make this adoption trivial. + */ +export function updated(cmp: Element, data: object, def: ElementDef, context: object) { + let ucMetadata: ServiceUpdateContext; + if (!def.wire || !(ucMetadata = context[CONTEXT_ID][UPDATED])) { + return; + } + + // get new values for all dynamic props + const paramValues = getPropertyValues(cmp, ucMetadata.paramValues); + + // compare new to old dynamic prop values, updating old props with new values + // for each change, queue the impacted adapter(s) + // TODO: do we really need this if updated is only hooked to bound props? + + // process queue of impacted adapters + invokeUpdatedCallback(ucMetadata.callbacks, paramValues); +} + +/** + * Gets bound properties from wire definitions + * @param wireDefs The wire definitions + * @returns Set of bound properties + */ +export function getPropsFromParams(wireDefs: WireDef[]) { + const props = new Set(); + wireDefs.forEach((wireDef) => { + const { params } = wireDef; + if (params) { + Object.keys(params).forEach(param => { + const prop = params[param]; + props.add(prop); + }); + } + }); + + return props; +} + +/** + * 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 + */ +export function installSetterOverrides(cmp: Object, prop: string, callback: Function) { + const newDescriptor = getOverrideDescriptor(cmp, prop, callback); + Object.defineProperty(cmp, prop, newDescriptor); +} + +/** + * 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 + */ +export function getOverrideDescriptor(cmp: Object, prop: string, callback: Function) { + const originalDescriptor = Object.getOwnPropertyDescriptor(cmp.constructor.prototype, prop); + let newDescriptor; + if (originalDescriptor) { + newDescriptor = Object.assign({}, originalDescriptor, { + set(value) { + if (originalDescriptor.set) { + originalDescriptor.set.call(cmp, value); + } + callback(); + } + }); + } else { + const propSymbol = Symbol(prop); + newDescriptor = { + get() { + return cmp[propSymbol]; + }, + set(value) { + cmp[propSymbol] = value; + callback(); + } + }; + // grab the existing value + cmp[propSymbol] = cmp[prop]; + } + return newDescriptor; +} + +/** + * Builds wire service context to optimize runtime lifecycle callbacks + * @param connectedNoArgCallbacks wire adapter connected callbacks + * @param disconnectedNoArgCallbacks wire adapter disconnected callbacks + * @param updatedCallbackConfigs wire service context metadata with wire adapter updated callbacks + * @param props bound properties + * @returns A wire service context + */ +export function buildContext( + connectedNoArgCallbacks: NoArgumentCallback[], + disconnectedNoArgCallbacks: NoArgumentCallback[], + updatedCallbackConfigs: UpdatedCallbackConfig[], + props: Set +): Map { + // cache context that optimizes runtime of service callbacks + const wireContext: Map = Object.create(null); + if (connectedNoArgCallbacks.length > 0) { + wireContext[CONNECTED] = connectedNoArgCallbacks; + } + + if (disconnectedNoArgCallbacks.length > 0) { + wireContext[DISCONNECTED] = disconnectedNoArgCallbacks; + } + + if (updatedCallbackConfigs.length > 0) { + const ucContext: ServiceUpdateContext = { + callbacks: updatedCallbackConfigs, + paramValues: props + }; + wireContext[UPDATED] = ucContext; + } + + return wireContext; +} From 112c99e52483224ac85ba3fe8ae864a8297a9aaf Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Tue, 20 Mar 2018 15:36:48 -0700 Subject: [PATCH 24/52] feat(wire-service): optimize serviceUpdateContext to have a prop to UpdatedCallbackConfigs map --- .../src/__tests__/wiring.spec.ts | 20 ++----- packages/lwc-wire-service/src/index.ts | 39 ++++++++----- packages/lwc-wire-service/src/shared-types.ts | 7 +-- packages/lwc-wire-service/src/wiring.ts | 56 +++---------------- 4 files changed, 38 insertions(+), 84 deletions(-) diff --git a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts index a4b9a3182e..31961e5a79 100644 --- a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts +++ b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts @@ -2,15 +2,6 @@ import { CONNECTED, DISCONNECTED, UPDATED } from '../constants'; import * as target from '../wiring'; describe('wiring internal', () => { - it('gets bound properties from wire definition', () => { - const wireDef = [ - { params: {key1: 'prop1', key2: 'prop2'} }, - { params: {key1: 'prop1', key2: 'prop3'} } - ]; - const actual = target.getPropsFromParams(wireDef); - expect(Array.from(actual)).toEqual(['prop1', 'prop2', 'prop3']); - }); - describe('installs setter overrides', () => { it("defaults to original value when setter installed", () => { class Target { @@ -61,20 +52,17 @@ describe('wiring internal', () => { describe('builds wire service context', () => { it('includes connected callback if any', () => { - expect(target.buildContext([jest.fn()], [], [], new Set())[CONNECTED]).toHaveLength(1); + expect(target.buildContext([jest.fn()], [], {})[CONNECTED]).toHaveLength(1); }); it('includes disconnected callback if any', () => { - expect(target.buildContext([], [jest.fn()], [], new Set())[DISCONNECTED]).toHaveLength(1); + expect(target.buildContext([], [jest.fn()], {})[DISCONNECTED]).toHaveLength(1); }); it('includes updated callback config if any', () => { const updatedCallbackConfigs = [{ updatedCallback: jest.fn() }]; - const paramValues = new Set(['prop1']); - expect(target.buildContext([], [], updatedCallbackConfigs, paramValues)[UPDATED]).toEqual({ - callbacks: updatedCallbackConfigs, - paramValues - }); + const serviceUpdateContext = { prop: updatedCallbackConfigs }; + expect(target.buildContext([], [], serviceUpdateContext)[UPDATED]).toEqual(serviceUpdateContext); }); }); }); diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 78238126b5..6e806d2615 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -8,11 +8,11 @@ import { Element } from 'engine'; import assert from './assert'; import { - WireDef, ElementDef, NoArgumentCallback, UpdatedCallback, - UpdatedCallbackConfig + UpdatedCallbackConfig, + ServiceUpdateContext } from './shared-types'; import { CONTEXT_ID, @@ -21,7 +21,6 @@ import { } from './constants'; import { updated, - getPropsFromParams, installSetterOverrides, buildContext } from './wiring'; @@ -65,17 +64,16 @@ const wireService = { const wireStaticDef = def.wire; const wireTargets = Object.keys(wireStaticDef); const adapters: WireAdapter[] = []; - const wireDefs: WireDef[] = []; + const updatedCallbackKey = 'updatedCallback'; - const updatedCallbackConfigs: UpdatedCallbackConfig[] = []; const connectedNoArgCallbacks: NoArgumentCallback[] = []; const disconnectedNoArgCallbacks: NoArgumentCallback[] = []; + const serviceUpdateContext: ServiceUpdateContext = Object.create(null); for (let i = 0; i < wireTargets.length; i++) { const wireTarget = wireTargets[i]; const wireDef = wireStaticDef[wireTarget]; - wireDefs.push(wireDef); - const id = wireDef.adapter || wireDef.type; - + const id = wireDef.adapter; + const params = wireDef.params; // initialize wired property if (!wireDef.method) { cmp[wireTarget] = {}; @@ -99,23 +97,36 @@ const wireService = { } const updatedCallback = wireAdapter[updatedCallbackKey]; if (updatedCallback) { - updatedCallbackConfigs.push({ + const updatedCallbackConfig: UpdatedCallbackConfig = { updatedCallback, statics: wireDef.static, params: wireDef.params - }); + }; + + if (params) { + Object.keys(params).forEach(param => { + const prop = params[param]; + let updatedCallbackConfigs = serviceUpdateContext[prop]; + if (!updatedCallbackConfigs) { + updatedCallbackConfigs = [updatedCallbackConfig]; + serviceUpdateContext[prop] = updatedCallbackConfigs; + } else { + updatedCallbackConfigs.push(updatedCallbackConfig); + } + }); + } } } } // only add updated to bound props - const props = getPropsFromParams(wireDefs); - props.forEach((prop) => { - installSetterOverrides(cmp, prop, updated.bind(undefined, cmp, data, def, context)); + Object.keys(serviceUpdateContext).forEach((prop) => { + // using data to notify which prop gets updated + installSetterOverrides(cmp, prop, updated.bind(undefined, cmp, prop, def, context)); }); // cache context that optimizes runtime of service callbacks - context[CONTEXT_ID] = buildContext(connectedNoArgCallbacks, disconnectedNoArgCallbacks, updatedCallbackConfigs, props); + context[CONTEXT_ID] = buildContext(connectedNoArgCallbacks, disconnectedNoArgCallbacks, serviceUpdateContext); }, connected: (cmp: Element, data: object, def: ElementDef, context: object) => { diff --git a/packages/lwc-wire-service/src/shared-types.ts b/packages/lwc-wire-service/src/shared-types.ts index 3c1cfe8be1..cc7a2b88ea 100644 --- a/packages/lwc-wire-service/src/shared-types.ts +++ b/packages/lwc-wire-service/src/shared-types.ts @@ -5,8 +5,7 @@ export interface WireDef { static?: { [key: string]: any; }; - type?: string; - adapter?: any; + adapter: any; method?: 1; } export interface ElementDef { @@ -30,9 +29,7 @@ export interface UpdatedCallbackConfig { } export interface ServiceUpdateContext { - callbacks: UpdatedCallbackConfig[]; - // union of callbacks.params values - paramValues: Set; + [prop: string]: UpdatedCallbackConfig[]; } export type ServiceContext = NoArgumentCallback[] | ServiceUpdateContext; diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index a45c5b7e7b..eef2ebbe22 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -38,20 +38,6 @@ function invokeUpdatedCallback(ucMetadatas: UpdatedCallbackConfig[], paramValues } } -/** - * Gets resolved values of the specified properties. - * @param cmp component to get property value from - * @param properties a set of bound property - */ -function getPropertyValues(cmp: Element, properties: Set) { - const resolvedValues = Object.create(null); - properties.forEach((property) => { - resolvedValues[property] = cmp[property]; - }); - - return resolvedValues; -} - /** * TODO - in early 216, engine will expose an `updated` callback for services that * is invoked whenever a tracked property is changed. wire service is structured to @@ -63,35 +49,12 @@ export function updated(cmp: Element, data: object, def: ElementDef, context: ob return; } - // get new values for all dynamic props - const paramValues = getPropertyValues(cmp, ucMetadata.paramValues); - - // compare new to old dynamic prop values, updating old props with new values - // for each change, queue the impacted adapter(s) - // TODO: do we really need this if updated is only hooked to bound props? + const updateProp = data.toString(); + const paramValue = Object.create(null); + paramValue[updateProp] = cmp[updateProp]; // process queue of impacted adapters - invokeUpdatedCallback(ucMetadata.callbacks, paramValues); -} - -/** - * Gets bound properties from wire definitions - * @param wireDefs The wire definitions - * @returns Set of bound properties - */ -export function getPropsFromParams(wireDefs: WireDef[]) { - const props = new Set(); - wireDefs.forEach((wireDef) => { - const { params } = wireDef; - if (params) { - Object.keys(params).forEach(param => { - const prop = params[param]; - props.add(prop); - }); - } - }); - - return props; + invokeUpdatedCallback(ucMetadata[updateProp], paramValue); } /** @@ -152,8 +115,7 @@ export function getOverrideDescriptor(cmp: Object, prop: string, callback: Funct export function buildContext( connectedNoArgCallbacks: NoArgumentCallback[], disconnectedNoArgCallbacks: NoArgumentCallback[], - updatedCallbackConfigs: UpdatedCallbackConfig[], - props: Set + serviceUpdateContext: ServiceUpdateContext ): Map { // cache context that optimizes runtime of service callbacks const wireContext: Map = Object.create(null); @@ -165,12 +127,8 @@ export function buildContext( wireContext[DISCONNECTED] = disconnectedNoArgCallbacks; } - if (updatedCallbackConfigs.length > 0) { - const ucContext: ServiceUpdateContext = { - callbacks: updatedCallbackConfigs, - paramValues: props - }; - wireContext[UPDATED] = ucContext; + if (serviceUpdateContext) { + wireContext[UPDATED] = serviceUpdateContext; } return wireContext; From fab305df1230fd5b041422ce86f509d9be4ee950 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Tue, 20 Mar 2018 15:44:57 -0700 Subject: [PATCH 25/52] fix(wire-service): lint error --- packages/lwc-wire-service/src/wiring.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index eef2ebbe22..2e09a07ba8 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -1,6 +1,5 @@ import { Element } from 'engine'; import { - WireDef, ElementDef, NoArgumentCallback, UpdatedCallbackConfig, From 986a70c8b80df946530b65c966192b7520feab5f Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Tue, 20 Mar 2018 17:19:15 -0700 Subject: [PATCH 26/52] build(wire-service): update file name to wire*.js and generate umd prod artifacts --- bin/generate-aura-dist.sh | 6 ++ packages/lwc-integration/scripts/build.js | 4 +- .../lwc-integration/src/shared/templates.js | 2 +- packages/lwc-wire-service/package.json | 3 +- .../rollup.config.umd.prod.js | 58 +++++++++++++++++++ .../lwc-wire-service/rollup.config.util.js | 2 +- 6 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 packages/lwc-wire-service/rollup.config.umd.prod.js diff --git a/bin/generate-aura-dist.sh b/bin/generate-aura-dist.sh index 7ec453a614..b1963fabd6 100755 --- a/bin/generate-aura-dist.sh +++ b/bin/generate-aura-dist.sh @@ -14,6 +14,12 @@ cp ./packages/lwc-engine/dist/umd/es5/engine.js $AURA_FILEPATH/aura-resources/s cp ./packages/lwc-engine/dist/umd/es5/engine_debug.js $AURA_FILEPATH/aura-resources/src/main/resources/aura/resources/engine/engine_compat_debug.js cp ./packages/lwc-engine/dist/umd/es5/engine.min.js $AURA_FILEPATH/aura-resources/src/main/resources/aura/resources/engine/engine_compat.min.js +# wire service +cp ./packages/lwc-wire-service/dist/umd/es2017/wire* $AURA_FILEPATH/aura-resources/src/main/resources/aura/resources/wire/ +cp ./packages/lwc-wire-service/dist/umd/es5/wire.js $AURA_FILEPATH/aura-resources/src/main/resources/aura/resources/wire/wire_compat.js +cp ./packages/lwc-wire-service/dist/umd/es5/wire_debug.js $AURA_FILEPATH/aura-resources/src/main/resources/aura/resources/wire/wire_compat_debug.js +cp ./packages/lwc-wire-service/dist/umd/es5/wire.min.js $AURA_FILEPATH/aura-resources/src/main/resources/aura/resources/engine/wire_compat.min.js + # compiler cp ./packages/lwc-compiler/dist/umd/compiler.js $AURA_FILEPATH/aura-modules/src/main/resources/modules/compiler.js diff --git a/packages/lwc-integration/scripts/build.js b/packages/lwc-integration/scripts/build.js index 1c66222ce4..9a689ff81f 100644 --- a/packages/lwc-integration/scripts/build.js +++ b/packages/lwc-integration/scripts/build.js @@ -13,7 +13,7 @@ const mode = process.env.MODE || 'compat'; const isCompat = /compat/.test(mode); const engineModeFile = path.join(require.resolve(`lwc-engine/dist/umd/${isCompat ? 'es5': 'es2017'}/engine.js`)); -const wireServicePath = path.join(require.resolve(`lwc-wire-service/dist/umd/${isCompat ? 'es5': 'es2017'}/wire-service.js`)); +const wireServicePath = path.join(require.resolve(`lwc-wire-service/dist/umd/${isCompat ? 'es5': 'es2017'}/wire.js`)); const todoPath = path.join(require.resolve('../src/shared/todo.js')); const testSufix = '.test.js'; @@ -117,7 +117,7 @@ fs.copySync(engineModeFile, path.join(testSharedOutput,'engine.js')); fs.writeFileSync(path.join(testSharedOutput,'downgrade.js'), compatPolyfills.loadDowngrade()); fs.writeFileSync(path.join(testSharedOutput,'polyfills.js'), compatPolyfills.loadPolyfills()); -fs.copySync(wireServicePath, path.join(testSharedOutput, 'wire-service.js')); +fs.copySync(wireServicePath, path.join(testSharedOutput, 'wire.js')); fs.copySync(todoPath, path.join(testSharedOutput, 'todo.js')); // -- Build component tests -----------------------------------------------------= diff --git a/packages/lwc-integration/src/shared/templates.js b/packages/lwc-integration/src/shared/templates.js index 340c91e4d8..bf46b4bfe5 100644 --- a/packages/lwc-integration/src/shared/templates.js +++ b/packages/lwc-integration/src/shared/templates.js @@ -80,7 +80,7 @@ exports.wireServiceHtml = function (cmpName, isCompat) { ${isCompat ? COMPAT : ''} - + diff --git a/packages/lwc-wire-service/package.json b/packages/lwc-wire-service/package.json index 3f10bad131..c42d2d9942 100644 --- a/packages/lwc-wire-service/package.json +++ b/packages/lwc-wire-service/package.json @@ -5,8 +5,9 @@ "main": "dist/commonjs/es2017/wire-service.js", "module": "dist/modules/es2017/wire-service.js", "scripts": { - "build": "concurrently \"yarn build:es-and-cjs\" \"yarn build:umd:dev\"", + "build": "concurrently \"yarn build:es-and-cjs\" \"yarn build:umd:dev\" \"yarn build:umd:prod\"", "build:umd:dev": "rollup -c rollup.config.umd.dev.js", + "build:umd:prod": "rollup -c rollup.config.umd.prod.js", "build:es-and-cjs": "rollup -c rollup.config.es-and-cjs.js", "build:playground": "rollup -c playground/rollup.config.js", "serve": "node playground/server.js", diff --git a/packages/lwc-wire-service/rollup.config.umd.prod.js b/packages/lwc-wire-service/rollup.config.umd.prod.js new file mode 100644 index 0000000000..132d1e3400 --- /dev/null +++ b/packages/lwc-wire-service/rollup.config.umd.prod.js @@ -0,0 +1,58 @@ +/* eslint-env node */ + +const path = require('path'); +const rollupReplacePlugin = require('rollup-plugin-replace'); +const typescript = require('rollup-plugin-typescript'); +const rollupCompatPlugin = require('rollup-plugin-compat').default; +const babelMinify = require('babel-minify'); +const { version } = require('./package.json'); +const { generateTargetName } = require('./rollup.config.util'); + +const input = path.resolve(__dirname, 'src/index.ts'); +const outputDir = path.resolve(__dirname, 'dist/umd'); + +const banner = (`/* proxy-compat-disable */`); +const footer = `/** version: ${version} */`; + +function inlineMinifyPlugin() { + return { + transformBundle(code) { + return babelMinify(code); + } + }; +} + +function rollupConfig(config) { + const { format, target, prod } = config; + const isCompat = target === 'es5'; + + const plugins = [ + typescript({ target: target, typescript: require('typescript') }), + rollupReplacePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }), + isCompat && rollupCompatPlugin({ polyfills: false, disableProxyTransform: true }), + prod && inlineMinifyPlugin({}) + ].filter(Boolean); + + + return { + input: input, + output: { + file: path.join(outputDir + `/${target}`, generateTargetName(config)), + name: "WireService", + format, + banner, + footer, + }, + plugins, + } +} + +module.exports = [ + // PROD + rollupConfig({ format: 'umd', prod: true, target: 'es5' }), + rollupConfig({ format: 'umd', prod: true, target: 'es2017' }), + + // PRODDEBUG mode + rollupConfig({ format: 'umd', proddebug: true, target: 'es2017' }), + rollupConfig({ format: 'umd', proddebug: true, target: 'es5' }) +] diff --git a/packages/lwc-wire-service/rollup.config.util.js b/packages/lwc-wire-service/rollup.config.util.js index eb849db417..c57244aa68 100644 --- a/packages/lwc-wire-service/rollup.config.util.js +++ b/packages/lwc-wire-service/rollup.config.util.js @@ -5,7 +5,7 @@ const PROD_SUFFIX = ".min"; function generateTargetName({ format, prod, target, proddebug }){ return [ - 'wire-service', + 'wire', proddebug ? DEBUG_SUFFIX : '', prod ? '.min' : '', '.js' From b517417d149bd1104e5ade0d8c4eb52e8dcde602 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Tue, 20 Mar 2018 17:39:07 -0700 Subject: [PATCH 27/52] chore(all): typo --- bin/generate-aura-dist.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/generate-aura-dist.sh b/bin/generate-aura-dist.sh index b1963fabd6..323b02eb97 100755 --- a/bin/generate-aura-dist.sh +++ b/bin/generate-aura-dist.sh @@ -18,7 +18,7 @@ cp ./packages/lwc-engine/dist/umd/es5/engine.min.js $AURA_FILEPATH/aura-resourc cp ./packages/lwc-wire-service/dist/umd/es2017/wire* $AURA_FILEPATH/aura-resources/src/main/resources/aura/resources/wire/ cp ./packages/lwc-wire-service/dist/umd/es5/wire.js $AURA_FILEPATH/aura-resources/src/main/resources/aura/resources/wire/wire_compat.js cp ./packages/lwc-wire-service/dist/umd/es5/wire_debug.js $AURA_FILEPATH/aura-resources/src/main/resources/aura/resources/wire/wire_compat_debug.js -cp ./packages/lwc-wire-service/dist/umd/es5/wire.min.js $AURA_FILEPATH/aura-resources/src/main/resources/aura/resources/engine/wire_compat.min.js +cp ./packages/lwc-wire-service/dist/umd/es5/wire.min.js $AURA_FILEPATH/aura-resources/src/main/resources/aura/resources/wire/wire_compat.min.js # compiler cp ./packages/lwc-compiler/dist/umd/compiler.js $AURA_FILEPATH/aura-modules/src/main/resources/modules/compiler.js From bba391548341addc4a89bd9cfb8b9d832568f02d Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Wed, 21 Mar 2018 15:32:21 -0700 Subject: [PATCH 28/52] feat(wire-service): address feedbacks --- packages/lwc-wire-service/package.json | 4 +-- .../playground/x/todo-api/todo-api.js | 11 ++++--- packages/lwc-wire-service/src/constants.ts | 2 ++ packages/lwc-wire-service/src/index.ts | 29 +++++++++---------- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/lwc-wire-service/package.json b/packages/lwc-wire-service/package.json index c42d2d9942..63e0630c16 100644 --- a/packages/lwc-wire-service/package.json +++ b/packages/lwc-wire-service/package.json @@ -2,8 +2,8 @@ "name": "lwc-wire-service", "version": "0.18.1", "description": "@wire service", - "main": "dist/commonjs/es2017/wire-service.js", - "module": "dist/modules/es2017/wire-service.js", + "main": "dist/commonjs/es2017/wire.js", + "module": "dist/modules/es2017/wire.js", "scripts": { "build": "concurrently \"yarn build:es-and-cjs\" \"yarn build:umd:dev\" \"yarn build:umd:prod\"", "build:umd:dev": "rollup -c rollup.config.umd.dev.js", diff --git a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js index f39c71be9c..14d94461fa 100644 --- a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js +++ b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js @@ -28,10 +28,13 @@ register(getTodo, function getTodoWireAdapter(targetSetter) { return { updatedCallback: (newConfig) => { config = newConfig; - subscription = getObservable(config).subscribe({ - next: data => targetSetter({ data, error: undefined }), - error: error => targetSetter({ data: undefined, error }) - }); + if (subscription) { + subscription.unsubscribe(); + subscription = getObservable(config).subscribe({ + next: data => targetSetter({ data, error: undefined }), + error: error => targetSetter({ data: undefined, error }) + }); + } }, connectedCallback: () => { diff --git a/packages/lwc-wire-service/src/constants.ts b/packages/lwc-wire-service/src/constants.ts index cd6b376ffa..bea810c98a 100644 --- a/packages/lwc-wire-service/src/constants.ts +++ b/packages/lwc-wire-service/src/constants.ts @@ -6,3 +6,5 @@ export const UPDATED: string = 'updated'; export const CONNECTED: string = 'connectedCallback'; // key for wire service context disconnected callbacks export const DISCONNECTED: string = 'disconnectedCallback'; +// key for wire adapter updated callbacks +export const UPDATEDCALLBACK = 'updatedCallback'; diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 6e806d2615..a216e4a701 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -17,26 +17,26 @@ import { import { CONTEXT_ID, CONNECTED, - DISCONNECTED + DISCONNECTED, + UPDATEDCALLBACK } from './constants'; import { updated, installSetterOverrides, buildContext } from './wiring'; -export interface WiredValue { - data?: any; - error?: any; -} -export type TargetSetter = (WiredValue) => void; +export type TargetSetter = (wiredValue: any) => void; +export interface EventTarget { + dispatchEvent(evt: Event): boolean; +} export type WireAdapterCallback = UpdatedCallback | NoArgumentCallback; export interface WireAdapter { updatedCallback?: UpdatedCallback; connectedCallback?: NoArgumentCallback; disconnectedCallback?: NoArgumentCallback; } -export type WireAdapterFactory = (targetSetter: TargetSetter) => WireAdapter; +export type WireAdapterFactory = (targetSetter: TargetSetter, eventTarget: EventTarget) => WireAdapter; // wire adapters: wire adapter id => adapter ctor const adapterFactories: Map = new Map(); @@ -65,7 +65,6 @@ const wireService = { const wireTargets = Object.keys(wireStaticDef); const adapters: WireAdapter[] = []; - const updatedCallbackKey = 'updatedCallback'; const connectedNoArgCallbacks: NoArgumentCallback[] = []; const disconnectedNoArgCallbacks: NoArgumentCallback[] = []; const serviceUpdateContext: ServiceUpdateContext = Object.create(null); @@ -74,18 +73,18 @@ const wireService = { const wireDef = wireStaticDef[wireTarget]; const id = wireDef.adapter; const params = wireDef.params; - // initialize wired property - if (!wireDef.method) { - cmp[wireTarget] = {}; - } const targetSetter: TargetSetter = wireDef.method ? (value) => { cmp[wireTarget](value); } : - (value) => { Object.assign(cmp[wireTarget], value); }; + (value) => { cmp[wireTarget] = value; }; + + const eventTarget: EventTarget = { + dispatchEvent: cmp.dispatchEvent.bind(cmp) + }; const adapterFactory = adapterFactories.get(id); if (adapterFactory) { - const wireAdapter = adapterFactory(targetSetter); + const wireAdapter = adapterFactory(targetSetter, eventTarget); adapters.push(wireAdapter); const connectedCallback = wireAdapter[CONNECTED]; if (connectedCallback) { @@ -95,7 +94,7 @@ const wireService = { if (disconnectedCallback) { disconnectedNoArgCallbacks.push(disconnectedCallback); } - const updatedCallback = wireAdapter[updatedCallbackKey]; + const updatedCallback = wireAdapter[UPDATEDCALLBACK]; if (updatedCallback) { const updatedCallbackConfig: UpdatedCallbackConfig = { updatedCallback, From 25c519afc1870e5844b339b832f4f09247246370 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Fri, 23 Mar 2018 14:54:01 -0700 Subject: [PATCH 29/52] feat(wire-service): update impl based on latest proposal to use wiredEventTarget --- .../playground/x/todo-api/todo-api.js | 42 ++-- .../src/__tests__/wiring.spec.ts | 17 -- packages/lwc-wire-service/src/constants.ts | 16 +- packages/lwc-wire-service/src/index.ts | 222 ++++++++++++------ packages/lwc-wire-service/src/shared-types.ts | 35 --- packages/lwc-wire-service/src/wiring.ts | 52 ++-- 6 files changed, 194 insertions(+), 190 deletions(-) delete mode 100644 packages/lwc-wire-service/src/shared-types.ts diff --git a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js index 14d94461fa..9581b3ff54 100644 --- a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js +++ b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js @@ -2,7 +2,7 @@ * Todo imperative APIs and wire adapters. */ -import { register } from 'wire-service'; +import { register, ValueChangedEvent } from 'wire-service'; import getObservable from './todo'; // Component-importable imperative access. @@ -22,31 +22,25 @@ export function getTodo(config) { } // Register the wire adapter for @wire(getTodo). -register(getTodo, function getTodoWireAdapter(targetSetter) { +register(getTodo, function getTodoWireAdapter(wiredEventTarget) { let subscription; let config; - return { - updatedCallback: (newConfig) => { - config = newConfig; - if (subscription) { - subscription.unsubscribe(); - subscription = getObservable(config).subscribe({ - next: data => targetSetter({ data, error: undefined }), - error: error => targetSetter({ data: undefined, error }) - }); - } - }, - - connectedCallback: () => { - // Subscribe to stream. - subscription = getObservable(config).subscribe({ - next: data => targetSetter({ data, error: undefined }), - error: error => targetSetter({ data: undefined, error }) - }); - }, - - disconnectedCallback: () => { + const observer = { + next: data => wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data, error: undefined })), + error: error => wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data: undefined, error })) + }; + wiredEventTarget.addEventListener('connect', () => { + // Subscribe to stream. + subscription = getObservable(config).subscribe(observer); + }); + wiredEventTarget.addEventListener('disconnect', () => { + subscription.unsubscribe(); + }); + wiredEventTarget.addEventListener('config', (newConfig) => { + config = newConfig; + if (subscription) { subscription.unsubscribe(); } - }; + subscription = getObservable(config).subscribe(observer); + }); }); diff --git a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts index 31961e5a79..419e62218c 100644 --- a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts +++ b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts @@ -1,4 +1,3 @@ -import { CONNECTED, DISCONNECTED, UPDATED } from '../constants'; import * as target from '../wiring'; describe('wiring internal', () => { @@ -49,20 +48,4 @@ describe('wiring internal', () => { expect(setter).toHaveBeenCalledWith(expected); }); }); - - describe('builds wire service context', () => { - it('includes connected callback if any', () => { - expect(target.buildContext([jest.fn()], [], {})[CONNECTED]).toHaveLength(1); - }); - it('includes disconnected callback if any', () => { - expect(target.buildContext([], [jest.fn()], {})[DISCONNECTED]).toHaveLength(1); - }); - it('includes updated callback config if any', () => { - const updatedCallbackConfigs = [{ - updatedCallback: jest.fn() - }]; - const serviceUpdateContext = { prop: updatedCallbackConfigs }; - expect(target.buildContext([], [], serviceUpdateContext)[UPDATED]).toEqual(serviceUpdateContext); - }); - }); }); diff --git a/packages/lwc-wire-service/src/constants.ts b/packages/lwc-wire-service/src/constants.ts index bea810c98a..21008a5397 100644 --- a/packages/lwc-wire-service/src/constants.ts +++ b/packages/lwc-wire-service/src/constants.ts @@ -1,10 +1,14 @@ // key for engine service context store -export const CONTEXT_ID: string = '@wire'; +export const CONTEXT_ID = '@wire'; // key for wire service context updated context metadata -export const UPDATED: string = 'updated'; +export const UPDATED = 'updated'; // key for wire service context connected callbacks -export const CONNECTED: string = 'connectedCallback'; +export const CONNECTEDCALLBACK = 'connectedCallback'; // key for wire service context disconnected callbacks -export const DISCONNECTED: string = 'disconnectedCallback'; -// key for wire adapter updated callbacks -export const UPDATEDCALLBACK = 'updatedCallback'; +export const DISCONNECTEDCALLBACK = 'disconnectedCallback'; +// 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/index.ts b/packages/lwc-wire-service/src/index.ts index a216e4a701..709857bc2e 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -5,38 +5,66 @@ * Register wire adapters with `register(adapterId: any, adapterFactory: WireAdapterFactory)`. */ -import { Element } from 'engine'; +import { Element, ComposableEvent } from 'engine'; import assert from './assert'; -import { - ElementDef, - NoArgumentCallback, - UpdatedCallback, - UpdatedCallbackConfig, - ServiceUpdateContext -} from './shared-types'; import { CONTEXT_ID, - CONNECTED, - DISCONNECTED, - UPDATEDCALLBACK + CONNECTEDCALLBACK, + DISCONNECTEDCALLBACK, + UPDATED, + CONNECT, + DISCONNECT, + CONFIG } from './constants'; import { updated, installSetterOverrides, - buildContext + removeCallback, + removeUpdatedCallbackConfigs } from './wiring'; -export type TargetSetter = (wiredValue: any) => void; -export interface EventTarget { - dispatchEvent(evt: Event): boolean; +export interface WireDef { + params?: { + [key: string]: string; + }; + static?: { + [key: string]: any; + }; + adapter: any; + method?: 1; +} +export interface ElementDef { + wire: { // TODO - wire is optional but all wire service code assumes it's present + [key: string]: WireDef + }; } -export type WireAdapterCallback = UpdatedCallback | NoArgumentCallback; -export interface WireAdapter { - updatedCallback?: UpdatedCallback; - connectedCallback?: NoArgumentCallback; - disconnectedCallback?: NoArgumentCallback; +export type NoArgumentCallback = () => void; +export type UpdatedCallback = (object) => void; +export interface UpdatedCallbackConfig { + updatedCallback: UpdatedCallback; + statics?: { + [key: string]: any; + }; + params?: { + [key: string]: string; + }; } -export type WireAdapterFactory = (targetSetter: TargetSetter, eventTarget: EventTarget) => WireAdapter; +export interface ServiceUpdateContext { + [prop: string]: UpdatedCallbackConfig[]; +} +export type ServiceContext = Set | ServiceUpdateContext; + +export type WireEventTargetCallback = NoArgumentCallback | UpdatedCallback; +export interface ValueChagnedEvent extends ComposableEvent { + value: any; +} +export interface WireEventTarget { + dispatchEvent(evt: ValueChagnedEvent): boolean; + addEventListener(type: string, callback: WireEventTargetCallback): void; + removeEventListener(type: string, callback: WireEventTargetCallback): void; +} + +export type WireAdapterFactory = (eventTarget: WireEventTarget) => void; // wire adapters: wire adapter id => adapter ctor const adapterFactories: Map = new Map(); @@ -60,77 +88,113 @@ function invokeCallback(callbacks: NoArgumentCallback[]) { const wireService = { // TODO W-4072588 - support connected + disconnected (repeated) cycles wiring: (cmp: Element, data: object, def: ElementDef, context: object) => { + const wireContext = context[CONTEXT_ID] = Object.create(null); + wireContext[CONNECTEDCALLBACK] = new Set(); + wireContext[DISCONNECTEDCALLBACK] = new Set(); + wireContext[UPDATED] = Object.create(null); + // engine guarantees invocation only if def.wire is defined const wireStaticDef = def.wire; const wireTargets = Object.keys(wireStaticDef); - const adapters: WireAdapter[] = []; - - const connectedNoArgCallbacks: NoArgumentCallback[] = []; - const disconnectedNoArgCallbacks: NoArgumentCallback[] = []; - const serviceUpdateContext: ServiceUpdateContext = Object.create(null); - for (let i = 0; i < wireTargets.length; i++) { + for (let i = 0, len = wireTargets.length; i < len; i++) { const wireTarget = wireTargets[i]; const wireDef = wireStaticDef[wireTarget]; const id = wireDef.adapter; const params = wireDef.params; - const targetSetter: TargetSetter = wireDef.method ? - (value) => { cmp[wireTarget](value); } : - (value) => { cmp[wireTarget] = value; }; - - const eventTarget: EventTarget = { - dispatchEvent: cmp.dispatchEvent.bind(cmp) + const wireEventTarget: WireEventTarget = { + addEventListener: (type, callback) => { + const connectedCallbacks = context[CONTEXT_ID][CONNECTEDCALLBACK]; + const disconnectedCallbacks = context[CONTEXT_ID][DISCONNECTEDCALLBACK]; + const serviceUpdateContext = context[CONTEXT_ID][UPDATED]; + switch (type) { + case CONNECT: + assert.isFalse(connectedCallbacks.has(callback), 'must not call addEventListener("connected") with the same callback'); + connectedCallbacks.add(callback); + break; + case DISCONNECT: + assert.isFalse(disconnectedCallbacks.has(callback), 'must not call addEventListener("disconnected") with the same callback'); + disconnectedCallbacks.add(callback); + break; + case CONFIG: + const updatedCallbackConfig: UpdatedCallbackConfig = { + updatedCallback: callback, + statics: wireDef.static, + params: wireDef.params + }; + + if (params) { + Object.keys(params).forEach(param => { + const prop = params[param]; + let updatedCallbackConfigs = serviceUpdateContext[prop]; + if (!updatedCallbackConfigs) { + updatedCallbackConfigs = [updatedCallbackConfig]; + serviceUpdateContext[prop] = updatedCallbackConfigs; + installSetterOverrides(cmp, prop, updated.bind(undefined, cmp, prop, def, context)); + } else { + updatedCallbackConfigs.push(updatedCallbackConfig); + } + }); + } + break; + case 'default': + throw new Error(`unsupported event type ${type}`); + } + }, + removeEventListener: (type, callback) => { + const connectedCallbacks = context[CONTEXT_ID][CONNECTEDCALLBACK]; + const disconnectedCallbacks = context[CONTEXT_ID][DISCONNECTEDCALLBACK]; + const serviceUpdateContext = context[CONTEXT_ID][UPDATED]; + switch (type) { + case CONNECT: + removeCallback(connectedCallbacks, callback); + break; + case DISCONNECT: + removeCallback(disconnectedCallbacks, callback); + break; + case CONFIG: + if (params) { + Object.keys(params).forEach(param => { + const prop = params[param]; + const updatedCallbackConfigs = serviceUpdateContext[prop]; + if (updatedCallbackConfigs) { + removeUpdatedCallbackConfigs(updatedCallbackConfigs, callback); + } + }); + } + break; + case 'default': + throw new Error(`unsupported event type ${type}`); + } + }, + dispatchEvent: (evt) => { + if (evt.type === "ValueChangedEvent") { + const value = evt.value; + if (wireDef.method) { + cmp[wireTarget](value); + } else { + cmp[wireTarget] = value; + } + return true; + } else { + // TODO: only allow ValueChangedEvent + // however, doing so would require adapter to implement machinery + // that fire the intended event as DOM event and wrap inside ValueChagnedEvent + return cmp.dispatchEvent(evt); + } + } }; const adapterFactory = adapterFactories.get(id); if (adapterFactory) { - const wireAdapter = adapterFactory(targetSetter, eventTarget); - adapters.push(wireAdapter); - const connectedCallback = wireAdapter[CONNECTED]; - if (connectedCallback) { - connectedNoArgCallbacks.push(connectedCallback); - } - const disconnectedCallback = wireAdapter[DISCONNECTED]; - if (disconnectedCallback) { - disconnectedNoArgCallbacks.push(disconnectedCallback); - } - const updatedCallback = wireAdapter[UPDATEDCALLBACK]; - if (updatedCallback) { - const updatedCallbackConfig: UpdatedCallbackConfig = { - updatedCallback, - statics: wireDef.static, - params: wireDef.params - }; - - if (params) { - Object.keys(params).forEach(param => { - const prop = params[param]; - let updatedCallbackConfigs = serviceUpdateContext[prop]; - if (!updatedCallbackConfigs) { - updatedCallbackConfigs = [updatedCallbackConfig]; - serviceUpdateContext[prop] = updatedCallbackConfigs; - } else { - updatedCallbackConfigs.push(updatedCallbackConfig); - } - }); - } - } + adapterFactory(wireEventTarget); } } - - // only add updated to bound props - Object.keys(serviceUpdateContext).forEach((prop) => { - // using data to notify which prop gets updated - installSetterOverrides(cmp, prop, updated.bind(undefined, cmp, prop, def, context)); - }); - - // cache context that optimizes runtime of service callbacks - context[CONTEXT_ID] = buildContext(connectedNoArgCallbacks, disconnectedNoArgCallbacks, serviceUpdateContext); }, connected: (cmp: Element, data: object, def: ElementDef, context: object) => { let callbacks: NoArgumentCallback[]; - if (!def.wire || !(callbacks = context[CONTEXT_ID][CONNECTED])) { + if (!def.wire || !(callbacks = context[CONTEXT_ID][CONNECTEDCALLBACK])) { return; } invokeCallback(callbacks); @@ -138,7 +202,7 @@ const wireService = { disconnected: (cmp: Element, data: object, def: ElementDef, context: object) => { let callbacks: NoArgumentCallback[]; - if (!def.wire || !(callbacks = context[CONTEXT_ID][DISCONNECTED])) { + if (!def.wire || !(callbacks = context[CONTEXT_ID][DISCONNECTEDCALLBACK])) { return; } invokeCallback(callbacks); @@ -169,3 +233,11 @@ export function unregister(adapterId: any) { adapterFactories.delete(adapterId); } } + +export class ValueChangedEvent extends Event { + value: any; + constructor(value) { + super("ValueChangedEvent"); + this.value = value; + } +} diff --git a/packages/lwc-wire-service/src/shared-types.ts b/packages/lwc-wire-service/src/shared-types.ts deleted file mode 100644 index cc7a2b88ea..0000000000 --- a/packages/lwc-wire-service/src/shared-types.ts +++ /dev/null @@ -1,35 +0,0 @@ -export interface WireDef { - params?: { - [key: string]: string; - }; - static?: { - [key: string]: any; - }; - adapter: any; - method?: 1; -} -export interface ElementDef { - wire: { // TODO - wire is optional but all wire service code assumes it's present - [key: string]: WireDef - }; -} - -export type NoArgumentCallback = () => void; - -export type UpdatedCallback = (object) => void; - -export interface UpdatedCallbackConfig { - updatedCallback: UpdatedCallback; - statics?: { - [key: string]: any; - }; - params?: { - [key: string]: string; - }; -} - -export interface ServiceUpdateContext { - [prop: string]: UpdatedCallbackConfig[]; -} - -export type ServiceContext = NoArgumentCallback[] | ServiceUpdateContext; diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index 2e09a07ba8..a80eab2520 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -1,16 +1,14 @@ import { Element } from 'engine'; import { ElementDef, - NoArgumentCallback, + WireEventTargetCallback, UpdatedCallbackConfig, ServiceUpdateContext, - ServiceContext -} from './shared-types'; + UpdatedCallback +} from './index'; import { CONTEXT_ID, - UPDATED, - CONNECTED, - DISCONNECTED + UPDATED } from './constants'; /** @@ -32,7 +30,7 @@ function invokeUpdatedCallback(ucMetadatas: UpdatedCallbackConfig[], paramValues } } - const config = Object.assign(Object.create(null), statics, resolvedParams); + const config = Object.assign({}, statics, resolvedParams); updatedCallback.call(undefined, config); } } @@ -49,7 +47,7 @@ export function updated(cmp: Element, data: object, def: ElementDef, context: ob } const updateProp = data.toString(); - const paramValue = Object.create(null); + const paramValue = {}; paramValue[updateProp] = cmp[updateProp]; // process queue of impacted adapters @@ -103,32 +101,20 @@ export function getOverrideDescriptor(cmp: Object, prop: string, callback: Funct return newDescriptor; } -/** - * Builds wire service context to optimize runtime lifecycle callbacks - * @param connectedNoArgCallbacks wire adapter connected callbacks - * @param disconnectedNoArgCallbacks wire adapter disconnected callbacks - * @param updatedCallbackConfigs wire service context metadata with wire adapter updated callbacks - * @param props bound properties - * @returns A wire service context - */ -export function buildContext( - connectedNoArgCallbacks: NoArgumentCallback[], - disconnectedNoArgCallbacks: NoArgumentCallback[], - serviceUpdateContext: ServiceUpdateContext -): Map { - // cache context that optimizes runtime of service callbacks - const wireContext: Map = Object.create(null); - if (connectedNoArgCallbacks.length > 0) { - wireContext[CONNECTED] = connectedNoArgCallbacks; - } - - if (disconnectedNoArgCallbacks.length > 0) { - wireContext[DISCONNECTED] = disconnectedNoArgCallbacks; +export function removeCallback(callbacks: WireEventTargetCallback[], toRemove: WireEventTargetCallback) { + for (let i = 0, l = callbacks.length; i < l; i++) { + if (callbacks[i] === toRemove) { + callbacks.splice(i, 1); + return; + } } +} - if (serviceUpdateContext) { - wireContext[UPDATED] = serviceUpdateContext; +export function removeUpdatedCallbackConfigs(updatedCallbackConfigs: UpdatedCallbackConfig[], toRemove: UpdatedCallback) { + for (let i = 0, l = updatedCallbackConfigs.length; i < l; i++) { + if (updatedCallbackConfigs[i].updatedCallback === toRemove) { + updatedCallbackConfigs.splice(i, 1); + return; + } } - - return wireContext; } From 2606fa7f1e802ef11ad816fe5ac4d4aca22e2b77 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Fri, 23 Mar 2018 15:16:38 -0700 Subject: [PATCH 30/52] fix(wire-service): update integration test --- .../lwc-integration/src/shared/templates.js | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/lwc-integration/src/shared/templates.js b/packages/lwc-integration/src/shared/templates.js index bf46b4bfe5..f1847a8567 100644 --- a/packages/lwc-integration/src/shared/templates.js +++ b/packages/lwc-integration/src/shared/templates.js @@ -9,7 +9,7 @@ exports.app = function (cmpName) { exports.todoApp = function (cmpName) { return ` - import { registerWireService, register as registerAdapter } from 'wire-service'; + import { registerWireService, register as registerAdapter, ValueChangedEvent } from 'wire-service'; import { createElement, register } from 'engine'; import Cmp from '${cmpName}'; import { getTodo, getObservable } from 'todo'; @@ -17,30 +17,27 @@ exports.todoApp = function (cmpName) { registerWireService(register); // Register the wire adapter for @wire(getTodo). - registerAdapter(getTodo, function getTodoWireAdapter(targetSetter) { + registerAdapter(getTodo, function getTodoWireAdapter(wiredEventTarget) { let subscription; let config; - return { - updatedCallback: (newConfig) => { - config = newConfig; - subscription = getObservable(config).subscribe({ - next: data => targetSetter({ data, error: undefined }), - error: error => targetSetter({ data: undefined, error }) - }); - }, - - connectedCallback: () => { - // Subscribe to stream. - subscription = getObservable(config).subscribe({ - next: data => targetSetter({ data, error: undefined }), - error: error => targetSetter({ data: undefined, error }) - }); - }, - - disconnectedCallback: () => { + const observer = { + next: data => wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data, error: undefined })), + error: error => wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data: undefined, error })) + }; + wiredEventTarget.addEventListener('connect', () => { + // Subscribe to stream. + subscription = getObservable(config).subscribe(observer); + }); + wiredEventTarget.addEventListener('disconnect', () => { + subscription.unsubscribe(); + }); + wiredEventTarget.addEventListener('config', (newConfig) => { + config = newConfig; + if (subscription) { subscription.unsubscribe(); } - }; + subscription = getObservable(config).subscribe(observer); + }); }); const element = createElement('${cmpName}', { is: Cmp }); From da522526c444355a9360d52ab27a55cd6d08de75 Mon Sep 17 00:00:00 2001 From: Caridy Date: Fri, 23 Mar 2018 20:23:33 -0400 Subject: [PATCH 31/52] refactor(wire): review feedback --- packages/lwc-wire-service/src/index.ts | 16 +++--- packages/lwc-wire-service/src/wiring.ts | 70 +++++++++++++++++-------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 709857bc2e..14e824cb99 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -168,19 +168,16 @@ const wireService = { } }, dispatchEvent: (evt) => { - if (evt.type === "ValueChangedEvent") { + if (evt instanceof ValueChangedEvent) { const value = evt.value; if (wireDef.method) { cmp[wireTarget](value); } else { cmp[wireTarget] = value; } - return true; + return false; // canceling signal since we don't want this to propagate } else { - // TODO: only allow ValueChangedEvent - // however, doing so would require adapter to implement machinery - // that fire the intended event as DOM event and wrap inside ValueChagnedEvent - return cmp.dispatchEvent(evt); + throw new Error(`Invalid event ${evt}.`); } } }; @@ -221,7 +218,7 @@ export function registerWireService(registerService: Function) { */ export function register(adapterId: any, adapterFactory: WireAdapterFactory) { assert.isTrue(adapterId, 'adapter id must be truthy'); - assert.isTrue(typeof adapterFactory === 'function', 'adapter factory must be a function'); + assert.isTrue(typeof adapterFactory === 'function', 'adapter factory must be a callable'); adapterFactories.set(adapterId, adapterFactory); } @@ -234,10 +231,11 @@ export function unregister(adapterId: any) { } } -export class ValueChangedEvent extends Event { +export class ValueChangedEvent { value: any; + type: string; constructor(value) { - super("ValueChangedEvent"); + this.type = 'ValueChangedEvent'; this.value = value; } } diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index a80eab2520..1d301f6c8a 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -65,6 +65,23 @@ export function installSetterOverrides(cmp: Object, prop: string, callback: Func Object.defineProperty(cmp, prop, newDescriptor); } +function findDescriptor(Ctor: any, propName: PropertyKey, protoSet?: any[]): PropertyDescriptor | null { + protoSet = protoSet || []; + if (!Ctor || protoSet.indexOf(Ctor) > -1) { + return null; // null, undefined, or circular prototype definition + } + const proto = Object.getPrototypeOf(Ctor); + if (!proto) { + return null; + } + const descriptor = Object.getOwnPropertyDescriptor(proto, propName); + if (descriptor) { + return descriptor; + } + protoSet.push(Ctor); + return findDescriptor(proto, propName, protoSet); +} + /** * Gets a property descriptor that monitors the provided property for changes * @param cmp The component @@ -73,32 +90,41 @@ export function installSetterOverrides(cmp: Object, prop: string, callback: Func * @return A property descriptor */ export function getOverrideDescriptor(cmp: Object, prop: string, callback: Function) { - const originalDescriptor = Object.getOwnPropertyDescriptor(cmp.constructor.prototype, prop); - let newDescriptor; - if (originalDescriptor) { - newDescriptor = Object.assign({}, originalDescriptor, { - set(value) { - if (originalDescriptor.set) { - originalDescriptor.set.call(cmp, value); - } - callback(); - } - }); + const descriptor = findDescriptor(cmp, prop); + let enumerable; + let get; + let set; + // TODO: 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 propSymbol = Symbol(prop); - newDescriptor = { - get() { - return cmp[propSymbol]; - }, - set(value) { - cmp[propSymbol] = value; - callback(); + 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; }; - // grab the existing value - cmp[propSymbol] = cmp[prop]; } - return newDescriptor; + return { + set, + get, + enumerable, + configurable: true, + }; } export function removeCallback(callbacks: WireEventTargetCallback[], toRemove: WireEventTargetCallback) { From baf5daae22b4efd59cfbf84484857cc2b4082b2a Mon Sep 17 00:00:00 2001 From: Kevin Venkiteswaran Date: Fri, 23 Mar 2018 18:46:46 -0700 Subject: [PATCH 32/52] fix(wire-service): incorporate PR feedback --- packages/lwc-integration/src/shared/todo.js | 127 +++++++++--------- packages/lwc-wire-service/playground/app.js | 2 +- .../playground/x/demo/demo.html | 2 + .../playground/x/todo-api/todo-api.js | 18 ++- .../playground/x/todo-api/todo.js | 9 +- packages/lwc-wire-service/src/index.ts | 10 +- 6 files changed, 90 insertions(+), 78 deletions(-) diff --git a/packages/lwc-integration/src/shared/todo.js b/packages/lwc-integration/src/shared/todo.js index 613f1aae72..53a999eac3 100644 --- a/packages/lwc-integration/src/shared/todo.js +++ b/packages/lwc-integration/src/shared/todo.js @@ -5,84 +5,83 @@ }(this, (function (exports) { 'use strict'; -function getSubject(initialValue, initialError) { - var observer; + function getSubject(initialValue, initialError) { + var observer; - function next(value) { - observer.next(value); - } + function next(value) { + observer.next(value); + } - function error(err) { - observer.error(err); - } + function error(err) { + observer.error(err); + } - function complete() { - observer.complete(); - } + function complete() { + observer.complete(); + } - var observable = { - subscribe: function(obs) { - observer = obs; - if (initialValue) { - next(initialValue); - } - if (initialError) { - error(initialError); + var observable = { + subscribe: function(obs) { + observer = obs; + if (initialValue) { + next(initialValue); + } + if (initialError) { + error(initialError); + } + return { + unsubscribe: function() {} + }; } - return { - unsubscribe: function() {} - }; - } - }; + }; - return { - next: next, - error: error, - complete: complete, - observable: observable - }; -} + return { + next: next, + error: error, + complete: complete, + observable: observable + }; + } -function generateTodo(id, completed) { - return { - id: id, - title: 'task ' + id, - completed: completed - }; -} + function generateTodo(id, completed) { + return { + id: id, + title: 'task ' + id, + completed: completed + }; + } -var TODO = [ - generateTodo(0, true), - generateTodo(1, false), - // intentionally skip 2 - generateTodo(3, true), - generateTodo(4, true), - // intentionally skip 5 - generateTodo(6, false), - generateTodo(7, false) -].reduce(function(acc, value) { - acc[value.id] = value; - return acc; -}, {}); + var TODO = [ + generateTodo(0, true), + generateTodo(1, false), + // intentionally skip 2 + generateTodo(3, true), + generateTodo(4, true), + // intentionally skip 5 + generateTodo(6, false), + generateTodo(7, false) + ].reduce(function(acc, value) { + acc[value.id] = value; + return acc; + }, {}); -function getObservable(config) { - if (!('id' in config)) { - return undefined; - } + function getObservable(config) { + if (!('id' in config)) { + return undefined; + } + + var todo = TODO[config.id]; + if (!todo) { + var subject = getSubject(undefined, { message: 'Todo not found' }); + return subject.observable; + } - var todo = TODO[config.id]; - if (!todo) { - var subject = getSubject(undefined, { message: 'Todo not found' }); - return subject.observable; + return getSubject(todo).observable; } - return getSubject(todo).observable; -} + const getTodo = Symbol('getTodo'); -function getTodo(config) { - // not implemented -} exports.getTodo = getTodo; exports.getObservable = getObservable; Object.defineProperty(exports, '__esModule', { value: true }); diff --git a/packages/lwc-wire-service/playground/app.js b/packages/lwc-wire-service/playground/app.js index 4361316642..af7c2087c6 100644 --- a/packages/lwc-wire-service/playground/app.js +++ b/packages/lwc-wire-service/playground/app.js @@ -4,7 +4,7 @@ import { registerWireService } from 'wire-service'; import App from 'x-demo'; -registerWireService(register) +registerWireService(register); const container = document.getElementById('main'); const element = createElement('x-demo', { is: App }); diff --git a/packages/lwc-wire-service/playground/x/demo/demo.html b/packages/lwc-wire-service/playground/x/demo/demo.html index 8ea09d5239..d89316080d 100644 --- a/packages/lwc-wire-service/playground/x/demo/demo.html +++ b/packages/lwc-wire-service/playground/x/demo/demo.html @@ -7,9 +7,11 @@

Demo of @wire

Component with single @wire bound to field


+ diff --git a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js index 9581b3ff54..dfc33eb6f6 100644 --- a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js +++ b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js @@ -25,22 +25,34 @@ export function getTodo(config) { register(getTodo, function getTodoWireAdapter(wiredEventTarget) { let subscription; let config; + const observer = { next: data => wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data, error: undefined })), error: error => wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data: undefined, error })) }; + wiredEventTarget.addEventListener('connect', () => { - // Subscribe to stream. - subscription = getObservable(config).subscribe(observer); + const observable = getObservable(config); + if (observable) { + subscription = observable.subscribe(observer); + return; + } }); + wiredEventTarget.addEventListener('disconnect', () => { subscription.unsubscribe(); }); + wiredEventTarget.addEventListener('config', (newConfig) => { config = newConfig; if (subscription) { subscription.unsubscribe(); + subscription = undefined; + } + const observable = getObservable(config); + if (observable) { + subscription = observable.subscribe(observer); + return; } - subscription = getObservable(config).subscribe(observer); }); }); diff --git a/packages/lwc-wire-service/playground/x/todo-api/todo.js b/packages/lwc-wire-service/playground/x/todo-api/todo.js index c39ccb5dfe..d49c8df583 100644 --- a/packages/lwc-wire-service/playground/x/todo-api/todo.js +++ b/packages/lwc-wire-service/playground/x/todo-api/todo.js @@ -26,12 +26,11 @@ const TODO = [ return acc; }, {}); - /** - * Services @wire('todo') requests. - * @param {Object} config Service config bag. - * @return {Observable} An observable for the recordUis. - */ + * Gets an observable for a todo. + * @param {Object} config Configuration. + * @return {Observable|undefined} An observable for the todo, or undefined if the configuration is insufficient. +*/ export default function getObservable(config) { if (!('id' in config)) { return undefined; diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 14e824cb99..d4990378d6 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -34,7 +34,8 @@ export interface WireDef { method?: 1; } export interface ElementDef { - wire: { // TODO - wire is optional but all wire service code assumes it's present + // wire is optional on ElementDef but the engine guarantees it before invoking wiring service hook + wire: { [key: string]: WireDef }; } @@ -55,11 +56,11 @@ export interface ServiceUpdateContext { export type ServiceContext = Set | ServiceUpdateContext; export type WireEventTargetCallback = NoArgumentCallback | UpdatedCallback; -export interface ValueChagnedEvent extends ComposableEvent { +export interface ValueChangedEvent extends ComposableEvent { value: any; } export interface WireEventTarget { - dispatchEvent(evt: ValueChagnedEvent): boolean; + dispatchEvent(evt: ValueChangedEvent): boolean; addEventListener(type: string, callback: WireEventTargetCallback): void; removeEventListener(type: string, callback: WireEventTargetCallback): void; } @@ -83,10 +84,9 @@ function invokeCallback(callbacks: NoArgumentCallback[]) { * The wire service. * * This service is registered with the engine's service API. It connects service - * callbacks to wire adapter lifecycle callbacks. + * callbacks to wire adapter lifecycle events. */ const wireService = { - // TODO W-4072588 - support connected + disconnected (repeated) cycles wiring: (cmp: Element, data: object, def: ElementDef, context: object) => { const wireContext = context[CONTEXT_ID] = Object.create(null); wireContext[CONNECTEDCALLBACK] = new Set(); From e249b237488a30a366ee17816a7b28bb3d42cedf Mon Sep 17 00:00:00 2001 From: Kevin Venkiteswaran Date: Fri, 23 Mar 2018 22:51:54 -0700 Subject: [PATCH 33/52] WIP - rename vars for new terminology --- packages/lwc-wire-service/src/constants.ts | 15 ++-- packages/lwc-wire-service/src/index.ts | 82 ++++++++++++---------- packages/lwc-wire-service/src/wiring.ts | 45 +++++++----- 3 files changed, 78 insertions(+), 64 deletions(-) diff --git a/packages/lwc-wire-service/src/constants.ts b/packages/lwc-wire-service/src/constants.ts index 21008a5397..800a6b243f 100644 --- a/packages/lwc-wire-service/src/constants.ts +++ b/packages/lwc-wire-service/src/constants.ts @@ -1,11 +1,12 @@ -// key for engine service context store +// key in engine service context for wire service context export const CONTEXT_ID = '@wire'; -// key for wire service context updated context metadata -export const UPDATED = 'updated'; -// key for wire service context connected callbacks -export const CONNECTEDCALLBACK = 'connectedCallback'; -// key for wire service context disconnected callbacks -export const DISCONNECTEDCALLBACK = 'disconnectedCallback'; +// 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 diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index d4990378d6..8caf3cd2e4 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -9,9 +9,9 @@ import { Element, ComposableEvent } from 'engine'; import assert from './assert'; import { CONTEXT_ID, - CONNECTEDCALLBACK, - DISCONNECTEDCALLBACK, - UPDATED, + CONTEXT_CONNECTED, + CONTEXT_DISCONNECTED, + CONTEXT_UPDATED, CONNECT, DISCONNECT, CONFIG @@ -20,7 +20,7 @@ import { updated, installSetterOverrides, removeCallback, - removeUpdatedCallbackConfigs + removeConfigListener } from './wiring'; export interface WireDef { @@ -39,10 +39,10 @@ export interface ElementDef { [key: string]: WireDef }; } -export type NoArgumentCallback = () => void; -export type UpdatedCallback = (object) => void; -export interface UpdatedCallbackConfig { - updatedCallback: UpdatedCallback; +export type NoArgumentListener = () => void; +export type ConfigListener = (object) => void; +export interface ConfigListenerMetadata { + callback: ConfigListener; statics?: { [key: string]: any; }; @@ -50,12 +50,13 @@ export interface UpdatedCallbackConfig { [key: string]: string; }; } -export interface ServiceUpdateContext { - [prop: string]: UpdatedCallbackConfig[]; +// map of param to list of config listeners +// when a param changes O(1) lookup to list of config listeners to notify +export interface ParamToConfigListenerMetadataMap { + [prop: string]: ConfigListenerMetadata[]; } -export type ServiceContext = Set | ServiceUpdateContext; -export type WireEventTargetCallback = NoArgumentCallback | UpdatedCallback; +export type WireEventTargetCallback = NoArgumentListener | ConfigListener; export interface ValueChangedEvent extends ComposableEvent { value: any; } @@ -74,7 +75,7 @@ const adapterFactories: Map = new Map { const wireContext = context[CONTEXT_ID] = Object.create(null); - wireContext[CONNECTEDCALLBACK] = new Set(); - wireContext[DISCONNECTEDCALLBACK] = new Set(); - wireContext[UPDATED] = Object.create(null); + wireContext[CONTEXT_CONNECTED] = new Set(); + wireContext[CONTEXT_DISCONNECTED] = new Set(); + wireContext[CONTEXT_UPDATED] = Object.create(null) as ParamToConfigListenerMetadataMap; // engine guarantees invocation only if def.wire is defined const wireStaticDef = def.wire; @@ -104,21 +105,21 @@ const wireService = { const wireEventTarget: WireEventTarget = { addEventListener: (type, callback) => { - const connectedCallbacks = context[CONTEXT_ID][CONNECTEDCALLBACK]; - const disconnectedCallbacks = context[CONTEXT_ID][DISCONNECTEDCALLBACK]; - const serviceUpdateContext = context[CONTEXT_ID][UPDATED]; switch (type) { case CONNECT: - assert.isFalse(connectedCallbacks.has(callback), 'must not call addEventListener("connected") with the same callback'); + const connectedCallbacks: Set = context[CONTEXT_ID][CONTEXT_CONNECTED]; + assert.isFalse(connectedCallbacks.has(callback), 'must not call addEventListener("connect") with the same callback'); connectedCallbacks.add(callback); break; case DISCONNECT: - assert.isFalse(disconnectedCallbacks.has(callback), 'must not call addEventListener("disconnected") with the same callback'); + const disconnectedCallbacks: Set = context[CONTEXT_ID][CONTEXT_DISCONNECTED]; + assert.isFalse(disconnectedCallbacks.has(callback), 'must not call addEventListener("disconnect") with the same callback'); disconnectedCallbacks.add(callback); break; case CONFIG: - const updatedCallbackConfig: UpdatedCallbackConfig = { - updatedCallback: callback, + const paramToConfigListenerMetadata: ParamToConfigListenerMetadataMap = context[CONTEXT_ID][CONTEXT_UPDATED]; + const configListenerMetadata: ConfigListenerMetadata = { + callback, statics: wireDef.static, params: wireDef.params }; @@ -126,13 +127,13 @@ const wireService = { if (params) { Object.keys(params).forEach(param => { const prop = params[param]; - let updatedCallbackConfigs = serviceUpdateContext[prop]; - if (!updatedCallbackConfigs) { - updatedCallbackConfigs = [updatedCallbackConfig]; - serviceUpdateContext[prop] = updatedCallbackConfigs; + let configListenerMetadatas = paramToConfigListenerMetadata[prop]; + if (!configListenerMetadatas) { + configListenerMetadatas = [configListenerMetadata]; + paramToConfigListenerMetadata[prop] = configListenerMetadatas; installSetterOverrides(cmp, prop, updated.bind(undefined, cmp, prop, def, context)); } else { - updatedCallbackConfigs.push(updatedCallbackConfig); + configListenerMetadatas.push(configListenerMetadata); } }); } @@ -142,23 +143,23 @@ const wireService = { } }, removeEventListener: (type, callback) => { - const connectedCallbacks = context[CONTEXT_ID][CONNECTEDCALLBACK]; - const disconnectedCallbacks = context[CONTEXT_ID][DISCONNECTEDCALLBACK]; - const serviceUpdateContext = context[CONTEXT_ID][UPDATED]; switch (type) { case CONNECT: + const connectedCallbacks = context[CONTEXT_ID][CONTEXT_CONNECTED]; removeCallback(connectedCallbacks, callback); break; case DISCONNECT: + const disconnectedCallbacks = context[CONTEXT_ID][CONTEXT_DISCONNECTED]; removeCallback(disconnectedCallbacks, callback); break; case CONFIG: + const paramToConfigListenerMetadata: ParamToConfigListenerMetadataMap = context[CONTEXT_ID][CONTEXT_UPDATED]; if (params) { Object.keys(params).forEach(param => { const prop = params[param]; - const updatedCallbackConfigs = serviceUpdateContext[prop]; + const updatedCallbackConfigs = paramToConfigListenerMetadata[prop]; if (updatedCallbackConfigs) { - removeUpdatedCallbackConfigs(updatedCallbackConfigs, callback); + removeConfigListener(updatedCallbackConfigs, callback); } }); } @@ -190,16 +191,16 @@ const wireService = { }, connected: (cmp: Element, data: object, def: ElementDef, context: object) => { - let callbacks: NoArgumentCallback[]; - if (!def.wire || !(callbacks = context[CONTEXT_ID][CONNECTEDCALLBACK])) { + let callbacks: NoArgumentListener[]; + if (!def.wire || !(callbacks = context[CONTEXT_ID][CONTEXT_CONNECTED])) { return; } invokeCallback(callbacks); }, disconnected: (cmp: Element, data: object, def: ElementDef, context: object) => { - let callbacks: NoArgumentCallback[]; - if (!def.wire || !(callbacks = context[CONTEXT_ID][DISCONNECTEDCALLBACK])) { + let callbacks: NoArgumentListener[]; + if (!def.wire || !(callbacks = context[CONTEXT_ID][CONTEXT_DISCONNECTED])) { return; } invokeCallback(callbacks); @@ -209,7 +210,7 @@ const wireService = { /** * Registers the wire service. */ -export function registerWireService(registerService: Function) { +export function registerWireService(registerService: (object) => void) { registerService(wireService); } @@ -222,7 +223,7 @@ export function register(adapterId: any, adapterFactory: WireAdapterFactory) { adapterFactories.set(adapterId, adapterFactory); } -/* +/** * Unregisters an adapter, only available for non prod (e.g. test util) */ export function unregister(adapterId: any) { @@ -231,6 +232,9 @@ export function unregister(adapterId: any) { } } +/** + * Event fired by wire adapters to emit a new value. + */ export class ValueChangedEvent { value: any; type: string; diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index 1d301f6c8a..7b19699469 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -2,23 +2,23 @@ import { Element } from 'engine'; import { ElementDef, WireEventTargetCallback, - UpdatedCallbackConfig, - ServiceUpdateContext, - UpdatedCallback + ConfigListener, + ParamToConfigListenerMetadataMap, + ConfigListenerMetadata } from './index'; import { CONTEXT_ID, - UPDATED + CONTEXT_UPDATED } from './constants'; /** - * Invokes the provided updated callbacks with the resolved component properties. - * @param ucMetadatas wire updated service context metadata + * 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 invokeUpdatedCallback(ucMetadatas: UpdatedCallbackConfig[], paramValues: any) { - for (let i = 0, len = ucMetadatas.length; i < len; ++i) { - const { updatedCallback, statics, params } = ucMetadatas[i]; +function invokeConfigListeners(configListenerMetadatas: ConfigListenerMetadata[], paramValues: any) { + for (let i = 0, len = configListenerMetadatas.length; i < len; ++i) { + const { callback, statics, params } = configListenerMetadatas[i]; const resolvedParams = Object.create(null); if (params) { @@ -30,8 +30,9 @@ function invokeUpdatedCallback(ucMetadatas: UpdatedCallbackConfig[], paramValues } } + // TODO - consider read-only membrane to enforce invariant of immutable config const config = Object.assign({}, statics, resolvedParams); - updatedCallback.call(undefined, config); + callback.call(undefined, config); } } @@ -41,8 +42,8 @@ function invokeUpdatedCallback(ucMetadatas: UpdatedCallbackConfig[], paramValues * make this adoption trivial. */ export function updated(cmp: Element, data: object, def: ElementDef, context: object) { - let ucMetadata: ServiceUpdateContext; - if (!def.wire || !(ucMetadata = context[CONTEXT_ID][UPDATED])) { + let paramToConfigListenerMetadatas: ParamToConfigListenerMetadataMap; + if (!def.wire || !(paramToConfigListenerMetadatas = context[CONTEXT_ID][CONTEXT_UPDATED])) { return; } @@ -50,8 +51,10 @@ export function updated(cmp: Element, data: object, def: ElementDef, context: ob const paramValue = {}; paramValue[updateProp] = cmp[updateProp]; + // TODO - must debounce multiple param changes so listeners are invoked only once + // process queue of impacted adapters - invokeUpdatedCallback(ucMetadata[updateProp], paramValue); + invokeConfigListeners(paramToConfigListenerMetadatas[updateProp], paramValue); } /** @@ -65,6 +68,12 @@ export function installSetterOverrides(cmp: Object, prop: string, callback: Func Object.defineProperty(cmp, prop, newDescriptor); } +/** + * Finds the descriptor of the named property on the prototype chain + * @param Ctor Constructor function + * @param propName Name of property to find + * @param protoSet Prototypes searched (to avoid circular prototype chains) + */ function findDescriptor(Ctor: any, propName: PropertyKey, protoSet?: any[]): PropertyDescriptor | null { protoSet = protoSet || []; if (!Ctor || protoSet.indexOf(Ctor) > -1) { @@ -89,7 +98,7 @@ function findDescriptor(Ctor: any, propName: PropertyKey, protoSet?: any[]): Pro * @param callback a function to invoke when the prop's value changes * @return A property descriptor */ -export function getOverrideDescriptor(cmp: Object, prop: string, callback: Function) { +export function getOverrideDescriptor(cmp: Object, prop: string, callback: () => void) { const descriptor = findDescriptor(cmp, prop); let enumerable; let get; @@ -136,10 +145,10 @@ export function removeCallback(callbacks: WireEventTargetCallback[], toRemove: W } } -export function removeUpdatedCallbackConfigs(updatedCallbackConfigs: UpdatedCallbackConfig[], toRemove: UpdatedCallback) { - for (let i = 0, l = updatedCallbackConfigs.length; i < l; i++) { - if (updatedCallbackConfigs[i].updatedCallback === toRemove) { - updatedCallbackConfigs.splice(i, 1); +export function removeConfigListener(configListenerMetadatas: ConfigListenerMetadata[], toRemove: ConfigListener) { + for (let i = 0, len = configListenerMetadatas.length; i < len; i++) { + if (configListenerMetadatas[i].callback === toRemove) { + configListenerMetadatas.splice(i, 1); return; } } From 458b307fcc95968447e30fe02673b020044546e1 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Sat, 24 Mar 2018 04:47:38 -0700 Subject: [PATCH 34/52] refactor(wire-service): create a class for wiredEventTarget --- .../playground/x/demo/demo.html | 4 +- packages/lwc-wire-service/src/index.ts | 99 ++------------ packages/lwc-wire-service/src/wiring.ts | 125 +++++++++++++++++- 3 files changed, 128 insertions(+), 100 deletions(-) diff --git a/packages/lwc-wire-service/playground/x/demo/demo.html b/packages/lwc-wire-service/playground/x/demo/demo.html index d89316080d..34c96601d2 100644 --- a/packages/lwc-wire-service/playground/x/demo/demo.html +++ b/packages/lwc-wire-service/playground/x/demo/demo.html @@ -7,11 +7,11 @@

Demo of @wire

Component with single @wire bound to field


- + diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 8caf3cd2e4..220a0fbc8d 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -11,16 +11,10 @@ import { CONTEXT_ID, CONTEXT_CONNECTED, CONTEXT_DISCONNECTED, - CONTEXT_UPDATED, - CONNECT, - DISCONNECT, - CONFIG + CONTEXT_UPDATED } from './constants'; import { - updated, - installSetterOverrides, - removeCallback, - removeConfigListener + WireEventTarget } from './wiring'; export interface WireDef { @@ -101,91 +95,14 @@ const wireService = { const wireTarget = wireTargets[i]; const wireDef = wireStaticDef[wireTarget]; const id = wireDef.adapter; - const params = wireDef.params; - - const wireEventTarget: WireEventTarget = { - addEventListener: (type, callback) => { - switch (type) { - case CONNECT: - const connectedCallbacks: Set = context[CONTEXT_ID][CONTEXT_CONNECTED]; - assert.isFalse(connectedCallbacks.has(callback), 'must not call addEventListener("connect") with the same callback'); - connectedCallbacks.add(callback); - break; - case DISCONNECT: - const disconnectedCallbacks: Set = context[CONTEXT_ID][CONTEXT_DISCONNECTED]; - assert.isFalse(disconnectedCallbacks.has(callback), 'must not call addEventListener("disconnect") with the same callback'); - disconnectedCallbacks.add(callback); - break; - case CONFIG: - const paramToConfigListenerMetadata: ParamToConfigListenerMetadataMap = context[CONTEXT_ID][CONTEXT_UPDATED]; - const configListenerMetadata: ConfigListenerMetadata = { - callback, - statics: wireDef.static, - params: wireDef.params - }; - - if (params) { - Object.keys(params).forEach(param => { - const prop = params[param]; - let configListenerMetadatas = paramToConfigListenerMetadata[prop]; - if (!configListenerMetadatas) { - configListenerMetadatas = [configListenerMetadata]; - paramToConfigListenerMetadata[prop] = configListenerMetadatas; - installSetterOverrides(cmp, prop, updated.bind(undefined, cmp, prop, def, context)); - } else { - configListenerMetadatas.push(configListenerMetadata); - } - }); - } - break; - case 'default': - throw new Error(`unsupported event type ${type}`); - } - }, - removeEventListener: (type, callback) => { - switch (type) { - case CONNECT: - const connectedCallbacks = context[CONTEXT_ID][CONTEXT_CONNECTED]; - removeCallback(connectedCallbacks, callback); - break; - case DISCONNECT: - const disconnectedCallbacks = context[CONTEXT_ID][CONTEXT_DISCONNECTED]; - removeCallback(disconnectedCallbacks, callback); - break; - case CONFIG: - const paramToConfigListenerMetadata: ParamToConfigListenerMetadataMap = context[CONTEXT_ID][CONTEXT_UPDATED]; - if (params) { - Object.keys(params).forEach(param => { - const prop = params[param]; - const updatedCallbackConfigs = paramToConfigListenerMetadata[prop]; - if (updatedCallbackConfigs) { - removeConfigListener(updatedCallbackConfigs, callback); - } - }); - } - break; - case 'default': - throw new Error(`unsupported event type ${type}`); - } - }, - dispatchEvent: (evt) => { - if (evt instanceof ValueChangedEvent) { - const value = evt.value; - if (wireDef.method) { - cmp[wireTarget](value); - } else { - cmp[wireTarget] = value; - } - return false; // canceling signal since we don't want this to propagate - } else { - throw new Error(`Invalid event ${evt}.`); - } - } - }; - + const wireEventTarget = new WireEventTarget(cmp, def, context, wireDef, wireTarget); const adapterFactory = adapterFactories.get(id); if (adapterFactory) { - adapterFactory(wireEventTarget); + adapterFactory({ + dispatchEvent: wireEventTarget.dispatchEvent.bind(wireEventTarget), + addEventListener: wireEventTarget.addEventListener.bind(wireEventTarget), + removeEventListener: wireEventTarget.removeEventListener.bind(wireEventTarget) + } as WireEventTarget); } } }, diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index 7b19699469..3f652b7909 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -1,14 +1,22 @@ import { Element } from 'engine'; +import assert from './assert'; import { ElementDef, - WireEventTargetCallback, ConfigListener, ParamToConfigListenerMetadataMap, - ConfigListenerMetadata + ConfigListenerMetadata, + WireEventTargetCallback, + ValueChangedEvent, + WireDef } from './index'; import { CONTEXT_ID, - CONTEXT_UPDATED + CONTEXT_CONNECTED, + CONTEXT_DISCONNECTED, + CONTEXT_UPDATED, + CONNECT, + DISCONNECT, + CONFIG } from './constants'; /** @@ -41,7 +49,7 @@ function invokeConfigListeners(configListenerMetadatas: ConfigListenerMetadata[] * is invoked whenever a tracked property is changed. wire service is structured to * make this adoption trivial. */ -export function updated(cmp: Element, data: object, def: ElementDef, context: object) { +function updated(cmp: Element, data: object, def: ElementDef, context: object) { let paramToConfigListenerMetadatas: ParamToConfigListenerMetadataMap; if (!def.wire || !(paramToConfigListenerMetadatas = context[CONTEXT_ID][CONTEXT_UPDATED])) { return; @@ -63,7 +71,7 @@ export function updated(cmp: Element, data: object, def: ElementDef, context: ob * @param prop The name of the property to be monitored * @param callback a function to invoke when the prop's value changes */ -export function installSetterOverrides(cmp: Object, prop: string, callback: Function) { +export function installSetterOverrides(cmp: Object, prop: string, callback: () => void) { const newDescriptor = getOverrideDescriptor(cmp, prop, callback); Object.defineProperty(cmp, prop, newDescriptor); } @@ -136,7 +144,7 @@ export function getOverrideDescriptor(cmp: Object, prop: string, callback: () => }; } -export function removeCallback(callbacks: WireEventTargetCallback[], toRemove: WireEventTargetCallback) { +function removeCallback(callbacks: WireEventTargetCallback[], toRemove: WireEventTargetCallback) { for (let i = 0, l = callbacks.length; i < l; i++) { if (callbacks[i] === toRemove) { callbacks.splice(i, 1); @@ -145,7 +153,7 @@ export function removeCallback(callbacks: WireEventTargetCallback[], toRemove: W } } -export function removeConfigListener(configListenerMetadatas: ConfigListenerMetadata[], toRemove: ConfigListener) { +function removeConfigListener(configListenerMetadatas: ConfigListenerMetadata[], toRemove: ConfigListener) { for (let i = 0, len = configListenerMetadatas.length; i < len; i++) { if (configListenerMetadatas[i].callback === toRemove) { configListenerMetadatas.splice(i, 1); @@ -153,3 +161,106 @@ export function removeConfigListener(configListenerMetadatas: ConfigListenerMeta } } } + +export class WireEventTarget { + _cmp: Element; + _def: ElementDef; + _context: object; + _wireDef: WireDef; + _wireTarget: string; + + constructor( + cmp: Element, + def: ElementDef, + context: object, + wireDef: WireDef, + wireTarget: string) { + this._cmp = cmp; + this._def = def; + this._context = context; + this._wireDef = wireDef; + this._wireTarget = wireTarget; + } + + addEventListener(type: string, callback: WireEventTargetCallback): void { + switch (type) { + case CONNECT: + const connectedCallbacks: Set = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; + assert.isFalse(connectedCallbacks.has(callback), 'must not call addEventListener("connect") with the same callback'); + connectedCallbacks.add(callback); + break; + case DISCONNECT: + const disconnectedCallbacks: Set = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; + assert.isFalse(disconnectedCallbacks.has(callback), 'must not call addEventListener("disconnect") with the same callback'); + disconnectedCallbacks.add(callback); + break; + case CONFIG: + const paramToConfigListenerMetadata: ParamToConfigListenerMetadataMap = this._context[CONTEXT_ID][CONTEXT_UPDATED]; + const { params } = this._wireDef; + const configListenerMetadata: ConfigListenerMetadata = { + callback, + statics: this._wireDef.static, + params + }; + + if (params) { + Object.keys(params).forEach(param => { + const prop = params[param]; + let configListenerMetadatas = paramToConfigListenerMetadata[prop]; + if (!configListenerMetadatas) { + configListenerMetadatas = [configListenerMetadata]; + paramToConfigListenerMetadata[prop] = configListenerMetadatas; + installSetterOverrides(this._cmp, prop, updated.bind(undefined, this._cmp, prop, this._def, this._context)); + } else { + configListenerMetadatas.push(configListenerMetadata); + } + }); + } + break; + case 'default': + throw new Error(`unsupported event type ${type}`); + } + } + + removeEventListener(type: string, callback: WireEventTargetCallback): void { + switch (type) { + case CONNECT: + const connectedCallbacks = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; + removeCallback(connectedCallbacks, callback); + break; + case DISCONNECT: + const disconnectedCallbacks = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; + removeCallback(disconnectedCallbacks, callback); + break; + case CONFIG: + const paramToConfigListenerMetadata: ParamToConfigListenerMetadataMap = this._context[CONTEXT_ID][CONTEXT_UPDATED]; + const { params } = this._wireDef; + if (params) { + Object.keys(params).forEach(param => { + const prop = params[param]; + const updatedCallbackConfigs = paramToConfigListenerMetadata[prop]; + if (updatedCallbackConfigs) { + removeConfigListener(updatedCallbackConfigs, callback); + } + }); + } + break; + case 'default': + throw new Error(`unsupported event type ${type}`); + } + } + + dispatchEvent(evt: ValueChangedEvent): 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 { + throw new Error(`Invalid event ${evt}.`); + } + } +} From d1e0f67fac8b82da3d235078e4a7780f863e1c76 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Sat, 24 Mar 2018 08:17:43 -0700 Subject: [PATCH 35/52] refactor(wire-service): get rid off circular dependency --- packages/lwc-wire-service/src/index.ts | 58 ++++--------------------- packages/lwc-wire-service/src/wiring.ts | 56 ++++++++++++++++++++---- 2 files changed, 55 insertions(+), 59 deletions(-) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 220a0fbc8d..79eef0a654 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -5,7 +5,7 @@ * Register wire adapters with `register(adapterId: any, adapterFactory: WireAdapterFactory)`. */ -import { Element, ComposableEvent } from 'engine'; +import { Element } from 'engine'; import assert from './assert'; import { CONTEXT_ID, @@ -14,46 +14,14 @@ import { CONTEXT_UPDATED } from './constants'; import { - WireEventTarget + ElementDef, + NoArgumentListener, + WireEventTargetCallback, + ParamToConfigListenerMetadataMap, + WireEventTarget, + ValueChangedEvent } from './wiring'; -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 engine guarantees it before invoking wiring service hook - wire: { - [key: string]: WireDef - }; -} -export type NoArgumentListener = () => void; -export type ConfigListener = (object) => void; -export interface ConfigListenerMetadata { - callback: ConfigListener; - statics?: { - [key: string]: any; - }; - params?: { - [key: string]: string; - }; -} -// map of param to list of config listeners -// when a param changes O(1) lookup to list of config listeners to notify -export interface ParamToConfigListenerMetadataMap { - [prop: string]: ConfigListenerMetadata[]; -} - -export type WireEventTargetCallback = NoArgumentListener | ConfigListener; -export interface ValueChangedEvent extends ComposableEvent { - value: any; -} export interface WireEventTarget { dispatchEvent(evt: ValueChangedEvent): boolean; addEventListener(type: string, callback: WireEventTargetCallback): void; @@ -149,14 +117,4 @@ export function unregister(adapterId: any) { } } -/** - * Event fired by wire adapters to emit a new value. - */ -export class ValueChangedEvent { - value: any; - type: string; - constructor(value) { - this.type = 'ValueChangedEvent'; - this.value = value; - } -} +export { ValueChangedEvent } from './wiring'; diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index 3f652b7909..3c7ad6d443 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -1,14 +1,5 @@ import { Element } from 'engine'; import assert from './assert'; -import { - ElementDef, - ConfigListener, - ParamToConfigListenerMetadataMap, - ConfigListenerMetadata, - WireEventTargetCallback, - ValueChangedEvent, - WireDef -} from './index'; import { CONTEXT_ID, CONTEXT_CONNECTED, @@ -18,6 +9,41 @@ import { DISCONNECT, CONFIG } from './constants'; +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 engine guarantees it before invoking wiring service hook + wire: { + [key: string]: WireDef + }; +} + +export type NoArgumentListener = () => void; +export type ConfigListener = (object) => void; +export interface ConfigListenerMetadata { + callback: ConfigListener; + statics?: { + [key: string]: any; + }; + params?: { + [key: string]: string; + }; +} +// map of param to list of config listeners +// when a param changes O(1) lookup to list of config listeners to notify +export interface ParamToConfigListenerMetadataMap { + [prop: string]: ConfigListenerMetadata[]; +} + +export type WireEventTargetCallback = NoArgumentListener | ConfigListener; /** * Invokes the provided change listeners with the resolved component properties. @@ -264,3 +290,15 @@ export class WireEventTarget { } } } + +/** + * Event fired by wire adapters to emit a new value. + */ +export class ValueChangedEvent { + value: any; + type: string; + constructor(value) { + this.type = 'ValueChangedEvent'; + this.value = value; + } +} From 218ac445874563df4ac54276b815e4ccb42b419e Mon Sep 17 00:00:00 2001 From: Kevin Venkiteswaran Date: Sat, 24 Mar 2018 16:49:13 -0700 Subject: [PATCH 36/52] refactor(wire-service): Fix type bug between set and array --- packages/lwc-wire-service/src/index.ts | 19 ++++++------- packages/lwc-wire-service/src/wiring.ts | 36 ++++++++++++++++--------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 79eef0a654..b79460b435 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -17,9 +17,10 @@ import { ElementDef, NoArgumentListener, WireEventTargetCallback, - ParamToConfigListenerMetadataMap, + Context, + WireContext, WireEventTarget, - ValueChangedEvent + ValueChangedEvent, } from './wiring'; export interface WireEventTarget { @@ -50,11 +51,11 @@ function invokeCallback(callbacks: NoArgumentListener[]) { * callbacks to wire adapter lifecycle events. */ const wireService = { - wiring: (cmp: Element, data: object, def: ElementDef, context: object) => { - const wireContext = context[CONTEXT_ID] = Object.create(null); - wireContext[CONTEXT_CONNECTED] = new Set(); - wireContext[CONTEXT_DISCONNECTED] = new Set(); - wireContext[CONTEXT_UPDATED] = Object.create(null) as ParamToConfigListenerMetadataMap; + wiring: (cmp: Element, data: object, def: ElementDef, context: Context) => { + const wireContext: WireContext = context[CONTEXT_ID] = Object.create(null); + wireContext[CONTEXT_CONNECTED] = []; + wireContext[CONTEXT_DISCONNECTED] = []; + wireContext[CONTEXT_UPDATED] = Object.create(null); // engine guarantees invocation only if def.wire is defined const wireStaticDef = def.wire; @@ -75,7 +76,7 @@ const wireService = { } }, - connected: (cmp: Element, data: object, def: ElementDef, context: object) => { + connected: (cmp: Element, data: object, def: ElementDef, context: Context) => { let callbacks: NoArgumentListener[]; if (!def.wire || !(callbacks = context[CONTEXT_ID][CONTEXT_CONNECTED])) { return; @@ -83,7 +84,7 @@ const wireService = { invokeCallback(callbacks); }, - disconnected: (cmp: Element, data: object, def: ElementDef, context: object) => { + disconnected: (cmp: Element, data: object, def: ElementDef, context: Context) => { let callbacks: NoArgumentListener[]; if (!def.wire || !(callbacks = context[CONTEXT_ID][CONTEXT_DISCONNECTED])) { return; diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index 3c7ad6d443..c0361434ae 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -43,6 +43,16 @@ export interface ParamToConfigListenerMetadataMap { [prop: string]: ConfigListenerMetadata[]; } +export interface WireContext { + [CONTEXT_CONNECTED]: NoArgumentListener[]; + [CONTEXT_DISCONNECTED]: NoArgumentListener[]; + [CONTEXT_UPDATED]: ParamToConfigListenerMetadataMap; +} + +export interface Context { + [CONTEXT_ID]: WireContext; +} + export type WireEventTargetCallback = NoArgumentListener | ConfigListener; /** @@ -191,14 +201,14 @@ function removeConfigListener(configListenerMetadatas: ConfigListenerMetadata[], export class WireEventTarget { _cmp: Element; _def: ElementDef; - _context: object; + _context: Context; _wireDef: WireDef; _wireTarget: string; constructor( cmp: Element, def: ElementDef, - context: object, + context: Context, wireDef: WireDef, wireTarget: string) { this._cmp = cmp; @@ -211,17 +221,17 @@ export class WireEventTarget { addEventListener(type: string, callback: WireEventTargetCallback): void { switch (type) { case CONNECT: - const connectedCallbacks: Set = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; - assert.isFalse(connectedCallbacks.has(callback), 'must not call addEventListener("connect") with the same callback'); - connectedCallbacks.add(callback); + const connectedCallbacks = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; + assert.isFalse(connectedCallbacks.includes(callback as NoArgumentListener), 'must not call addEventListener("connect") with the same callback'); + connectedCallbacks.push(callback as NoArgumentListener); break; case DISCONNECT: - const disconnectedCallbacks: Set = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; - assert.isFalse(disconnectedCallbacks.has(callback), 'must not call addEventListener("disconnect") with the same callback'); - disconnectedCallbacks.add(callback); + const disconnectedCallbacks = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; + assert.isFalse(disconnectedCallbacks.includes(callback as NoArgumentListener), 'must not call addEventListener("disconnect") with the same callback'); + disconnectedCallbacks.push(callback as NoArgumentListener); break; case CONFIG: - const paramToConfigListenerMetadata: ParamToConfigListenerMetadataMap = this._context[CONTEXT_ID][CONTEXT_UPDATED]; + const paramToConfigListenerMetadata = this._context[CONTEXT_ID][CONTEXT_UPDATED]; const { params } = this._wireDef; const configListenerMetadata: ConfigListenerMetadata = { callback, @@ -259,14 +269,14 @@ export class WireEventTarget { removeCallback(disconnectedCallbacks, callback); break; case CONFIG: - const paramToConfigListenerMetadata: ParamToConfigListenerMetadataMap = this._context[CONTEXT_ID][CONTEXT_UPDATED]; + const paramToConfigListenerMetadata = this._context[CONTEXT_ID][CONTEXT_UPDATED]; const { params } = this._wireDef; if (params) { Object.keys(params).forEach(param => { const prop = params[param]; - const updatedCallbackConfigs = paramToConfigListenerMetadata[prop]; - if (updatedCallbackConfigs) { - removeConfigListener(updatedCallbackConfigs, callback); + const configListenerMetadatas = paramToConfigListenerMetadata[prop]; + if (configListenerMetadatas) { + removeConfigListener(configListenerMetadatas, callback); } }); } From 36f16826a6782ce86a181a141f8f6bd1029a2b84 Mon Sep 17 00:00:00 2001 From: Kevin Venkiteswaran Date: Sat, 24 Mar 2018 17:11:00 -0700 Subject: [PATCH 37/52] refactor(wire-service): Extract engine-derived types into separate file --- packages/lwc-wire-service/src/engine.ts | 19 +++++++++++++++++++ packages/lwc-wire-service/src/index.ts | 6 ++++-- packages/lwc-wire-service/src/wiring.ts | 23 ++++++----------------- 3 files changed, 29 insertions(+), 19 deletions(-) create mode 100644 packages/lwc-wire-service/src/engine.ts diff --git a/packages/lwc-wire-service/src/engine.ts b/packages/lwc-wire-service/src/engine.ts new file mode 100644 index 0000000000..56c179e83a --- /dev/null +++ b/packages/lwc-wire-service/src/engine.ts @@ -0,0 +1,19 @@ +// subtypes from the engine +export { Element } from 'engine'; + +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 engine 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 b79460b435..c0935afb50 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -5,7 +5,6 @@ * Register wire adapters with `register(adapterId: any, adapterFactory: WireAdapterFactory)`. */ -import { Element } from 'engine'; import assert from './assert'; import { CONTEXT_ID, @@ -14,7 +13,10 @@ import { CONTEXT_UPDATED } from './constants'; import { - ElementDef, + Element, + ElementDef +} from './engine'; +import { NoArgumentListener, WireEventTargetCallback, Context, diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index c0361434ae..0564df81d3 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -1,4 +1,3 @@ -import { Element } from 'engine'; import assert from './assert'; import { CONTEXT_ID, @@ -9,22 +8,12 @@ import { DISCONNECT, CONFIG } from './constants'; -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 engine guarantees it before invoking wiring service hook - wire: { - [key: string]: WireDef - }; -} + +import { + Element, + ElementDef, + WireDef +} from './engine'; export type NoArgumentListener = () => void; export type ConfigListener = (object) => void; From 24878bb7f1aec6ce29a7d0028f9be1d901c38645 Mon Sep 17 00:00:00 2001 From: Kevin Venkiteswaran Date: Sat, 24 Mar 2018 19:20:16 -0700 Subject: [PATCH 38/52] fix(wire-service): Invoke wire adapter change handler once regardless of number of property changes --- .../playground/x/todo-api/todo-api.js | 2 + .../playground/x/todo-api/todo.js | 2 +- packages/lwc-wire-service/src/index.ts | 2 +- packages/lwc-wire-service/src/wiring.ts | 83 ++++++++++++++----- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js index dfc33eb6f6..b276bdb2b9 100644 --- a/packages/lwc-wire-service/playground/x/todo-api/todo-api.js +++ b/packages/lwc-wire-service/playground/x/todo-api/todo-api.js @@ -26,6 +26,8 @@ register(getTodo, function getTodoWireAdapter(wiredEventTarget) { let subscription; let config; + wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data: undefined, error: undefined })); + const observer = { next: data => wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data, error: undefined })), error: error => wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data: undefined, error })) diff --git a/packages/lwc-wire-service/playground/x/todo-api/todo.js b/packages/lwc-wire-service/playground/x/todo-api/todo.js index d49c8df583..3ce6eb72fc 100644 --- a/packages/lwc-wire-service/playground/x/todo-api/todo.js +++ b/packages/lwc-wire-service/playground/x/todo-api/todo.js @@ -32,7 +32,7 @@ const TODO = [ * @return {Observable|undefined} An observable for the todo, or undefined if the configuration is insufficient. */ export default function getObservable(config) { - if (!('id' in config)) { + if (!config || !('id' in config)) { return undefined; } diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index c0935afb50..6f81ba4429 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -57,7 +57,7 @@ const wireService = { const wireContext: WireContext = context[CONTEXT_ID] = Object.create(null); wireContext[CONTEXT_CONNECTED] = []; wireContext[CONTEXT_DISCONNECTED] = []; - wireContext[CONTEXT_UPDATED] = Object.create(null); + wireContext[CONTEXT_UPDATED] = { map: {}, values: {} }; // engine guarantees invocation only if def.wire is defined const wireStaticDef = def.wire; diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index 0564df81d3..3ddbab80cc 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -16,7 +16,10 @@ import { } from './engine'; export type NoArgumentListener = () => void; -export type ConfigListener = (object) => void; +export interface ConfigListenerArgument { + [key: string]: any; +} +export type ConfigListener = (ConfigListenerArgument) => void; export interface ConfigListenerMetadata { callback: ConfigListener; statics?: { @@ -26,16 +29,24 @@ export interface ConfigListenerMetadata { [key: string]: string; }; } -// map of param to list of config listeners -// when a param changes O(1) lookup to list of config listeners to notify -export interface ParamToConfigListenerMetadataMap { - [prop: string]: ConfigListenerMetadata[]; +export interface ConfigContext { + // map of param to list of config listeners + // when a param changes O(1) lookup to list of config listeners to notify + map: { + [prop: string]: ConfigListenerMetadata[]; + }; + // map of param values + values: { + [prop: string]: any + }; + // mutated props (debounced then cleared) + mutated?: Set; } export interface WireContext { [CONTEXT_CONNECTED]: NoArgumentListener[]; [CONTEXT_DISCONNECTED]: NoArgumentListener[]; - [CONTEXT_UPDATED]: ParamToConfigListenerMetadataMap; + [CONTEXT_UPDATED]: ConfigContext; } export interface Context { @@ -49,9 +60,9 @@ export type WireEventTargetCallback = NoArgumentListener | ConfigListener; * @param configListenerMetadatas list of config listener metadata (config listeners and their context) * @param paramValues values for all wire adapter config params */ -function invokeConfigListeners(configListenerMetadatas: ConfigListenerMetadata[], paramValues: any) { - for (let i = 0, len = configListenerMetadatas.length; i < len; ++i) { - const { callback, statics, params } = configListenerMetadatas[i]; +function invokeConfigListeners(configListenerMetadatas: Set, paramValues: any) { + for (const metadata of configListenerMetadatas) { + const { callback, statics, params } = metadata; const resolvedParams = Object.create(null); if (params) { @@ -74,20 +85,46 @@ function invokeConfigListeners(configListenerMetadatas: ConfigListenerMetadata[] * is invoked whenever a tracked property is changed. wire service is structured to * make this adoption trivial. */ -function updated(cmp: Element, data: object, def: ElementDef, context: object) { - let paramToConfigListenerMetadatas: ParamToConfigListenerMetadataMap; - if (!def.wire || !(paramToConfigListenerMetadatas = context[CONTEXT_ID][CONTEXT_UPDATED])) { - return; +function updatedFuture(cmp: Element, 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; + for (const prop of mutated) { + const value = cmp[prop]; + if (configContext.values[prop] === value) { + continue; + } + configContext.values[prop] = value; + const listeners = configContext.map[prop]; + for (let i = 0, len = listeners.length; i < len; i++) { + uniqueListeners.add(listeners[i]); + } } + invokeConfigListeners(uniqueListeners, configContext.values); +} - const updateProp = data.toString(); - const paramValue = {}; - paramValue[updateProp] = cmp[updateProp]; +function updated(cmp: Element, prop: string, def: ElementDef, context: Context) { + let configContext: ConfigContext; + if (!def.wire || !(configContext = context[CONTEXT_ID][CONTEXT_UPDATED])) { + return; + } - // TODO - must debounce multiple param changes so listeners are invoked only once + // TODO - don't think i can do this + // noop if value didn't change + const newValue = cmp[prop]; + if (configContext.values[prop] === newValue) { + return; + } - // process queue of impacted adapters - invokeConfigListeners(paramToConfigListenerMetadatas[updateProp], paramValue); + if (!configContext.mutated) { + configContext.mutated = new Set(); + // collect all prop changes via a microtask + // TODO 216 engine will provide a service callback for changed props + Promise.resolve().then(updatedFuture.bind(undefined, cmp, configContext)); + } + configContext.mutated.add(prop); } /** @@ -131,7 +168,7 @@ function findDescriptor(Ctor: any, propName: PropertyKey, protoSet?: any[]): Pro * @param callback a function to invoke when the prop's value changes * @return A property descriptor */ -export function getOverrideDescriptor(cmp: Object, prop: string, callback: () => void) { +function getOverrideDescriptor(cmp: Object, prop: string, callback: () => void) { const descriptor = findDescriptor(cmp, prop); let enumerable; let get; @@ -220,7 +257,7 @@ export class WireEventTarget { disconnectedCallbacks.push(callback as NoArgumentListener); break; case CONFIG: - const paramToConfigListenerMetadata = this._context[CONTEXT_ID][CONTEXT_UPDATED]; + const configContext = this._context[CONTEXT_ID][CONTEXT_UPDATED]; const { params } = this._wireDef; const configListenerMetadata: ConfigListenerMetadata = { callback, @@ -231,10 +268,10 @@ export class WireEventTarget { if (params) { Object.keys(params).forEach(param => { const prop = params[param]; - let configListenerMetadatas = paramToConfigListenerMetadata[prop]; + let configListenerMetadatas = configContext[prop]; if (!configListenerMetadatas) { configListenerMetadatas = [configListenerMetadata]; - paramToConfigListenerMetadata[prop] = configListenerMetadatas; + configContext.map[prop] = configListenerMetadatas; installSetterOverrides(this._cmp, prop, updated.bind(undefined, this._cmp, prop, this._def, this._context)); } else { configListenerMetadatas.push(configListenerMetadata); From 7cf9db49ea8abca7406806ee8cda6ff610db85b0 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Sat, 24 Mar 2018 19:34:49 -0700 Subject: [PATCH 39/52] refactor(wire-service): move updatedFuture after updated --- packages/lwc-wire-service/src/wiring.ts | 41 ++++++++++++------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index 3ddbab80cc..d85002665e 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -85,26 +85,6 @@ function invokeConfigListeners(configListenerMetadatas: Set(); - - // configContext.mutated must be set prior to invoking this function - const mutated = configContext.mutated as Set; - delete configContext.mutated; - for (const prop of mutated) { - const value = cmp[prop]; - if (configContext.values[prop] === value) { - continue; - } - configContext.values[prop] = value; - const listeners = configContext.map[prop]; - for (let i = 0, len = listeners.length; i < len; i++) { - uniqueListeners.add(listeners[i]); - } - } - invokeConfigListeners(uniqueListeners, configContext.values); -} - function updated(cmp: Element, prop: string, def: ElementDef, context: Context) { let configContext: ConfigContext; if (!def.wire || !(configContext = context[CONTEXT_ID][CONTEXT_UPDATED])) { @@ -121,12 +101,31 @@ function updated(cmp: Element, prop: string, def: ElementDef, context: Context) if (!configContext.mutated) { configContext.mutated = new Set(); // collect all prop changes via a microtask - // TODO 216 engine will provide a service callback for changed props Promise.resolve().then(updatedFuture.bind(undefined, cmp, configContext)); } configContext.mutated.add(prop); } +function updatedFuture(cmp: Element, 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; + for (const prop of mutated) { + const value = cmp[prop]; + if (configContext.values[prop] === value) { + continue; + } + configContext.values[prop] = value; + const listeners = configContext.map[prop]; + for (let i = 0, len = listeners.length; i < len; i++) { + uniqueListeners.add(listeners[i]); + } + } + invokeConfigListeners(uniqueListeners, configContext.values); +} + /** * Gets a property descriptor that monitors the provided property for changes * @param cmp The component From a26ed12c9a0b7d7e1ca6f40c38da42b31fdedcf2 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Sat, 24 Mar 2018 19:46:46 -0700 Subject: [PATCH 40/52] fix(wire-service): update integration test --- packages/lwc-integration/src/shared/templates.js | 15 ++++++++++++--- packages/lwc-integration/src/shared/todo.js | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/lwc-integration/src/shared/templates.js b/packages/lwc-integration/src/shared/templates.js index f1847a8567..9173b0788e 100644 --- a/packages/lwc-integration/src/shared/templates.js +++ b/packages/lwc-integration/src/shared/templates.js @@ -20,13 +20,17 @@ exports.todoApp = function (cmpName) { registerAdapter(getTodo, function getTodoWireAdapter(wiredEventTarget) { let subscription; let config; + wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data: undefined, error: undefined })); const observer = { next: data => wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data, error: undefined })), error: error => wiredEventTarget.dispatchEvent(new ValueChangedEvent({ data: undefined, error })) }; wiredEventTarget.addEventListener('connect', () => { - // Subscribe to stream. - subscription = getObservable(config).subscribe(observer); + const observable = getObservable(config); + if (observable) { + subscription = observable.subscribe(observer); + return; + } }); wiredEventTarget.addEventListener('disconnect', () => { subscription.unsubscribe(); @@ -35,8 +39,13 @@ exports.todoApp = function (cmpName) { config = newConfig; if (subscription) { subscription.unsubscribe(); + subscription = undefined; + } + const observable = getObservable(config); + if (observable) { + subscription = observable.subscribe(observer); + return; } - subscription = getObservable(config).subscribe(observer); }); }); diff --git a/packages/lwc-integration/src/shared/todo.js b/packages/lwc-integration/src/shared/todo.js index 53a999eac3..3314a0af4d 100644 --- a/packages/lwc-integration/src/shared/todo.js +++ b/packages/lwc-integration/src/shared/todo.js @@ -67,7 +67,7 @@ function getObservable(config) { - if (!('id' in config)) { + if (!config || !('id' in config)) { return undefined; } From b201fc14be2a5f225dcb0374e70752d0fc486bb6 Mon Sep 17 00:00:00 2001 From: Kevin Venkiteswaran Date: Sun, 25 Mar 2018 22:21:53 -0700 Subject: [PATCH 41/52] refactor(wire-service): Move prop change tracking to property-trap. Add test cases. --- .../src/__tests__/index.spec.ts | 14 +- .../src/__tests__/property-trap.spec.ts | 59 +++++ .../src/__tests__/wiring.spec.ts | 77 +++---- packages/lwc-wire-service/src/index.ts | 16 +- .../lwc-wire-service/src/property-trap.ts | 149 +++++++++++++ packages/lwc-wire-service/src/wiring.ts | 209 +++--------------- 6 files changed, 289 insertions(+), 235 deletions(-) create mode 100644 packages/lwc-wire-service/src/__tests__/property-trap.spec.ts create mode 100644 packages/lwc-wire-service/src/property-trap.ts diff --git a/packages/lwc-wire-service/src/__tests__/index.spec.ts b/packages/lwc-wire-service/src/__tests__/index.spec.ts index b680d9212d..fbf07b5490 100644 --- a/packages/lwc-wire-service/src/__tests__/index.spec.ts +++ b/packages/lwc-wire-service/src/__tests__/index.spec.ts @@ -4,21 +4,21 @@ import { describe('wire service', () => { describe('registers the service with engine', () => { - it('supports wiring hook', () => { + it('uses wiring hook', () => { const mockEngineRegister = jest.fn(); registerWireService(mockEngineRegister); expect(mockEngineRegister).toHaveBeenCalledWith(expect.objectContaining({ wiring: expect.any(Function) })); }); - it('supports connected hook', () => { + it('uses connected hook', () => { const mockEngineRegister = jest.fn(); registerWireService(mockEngineRegister); expect(mockEngineRegister).toHaveBeenCalledWith(expect.objectContaining({ connected: expect.any(Function) })); }); - it('supports disconnected hook', () => { + it('uses disconnected hook', () => { const mockEngineRegister = jest.fn(); registerWireService(mockEngineRegister); expect(mockEngineRegister).toHaveBeenCalledWith(expect.objectContaining({ @@ -27,3 +27,11 @@ describe('wire service', () => { }); }); }); + +describe('register', () => { + // TODO - reject non-function adapter id once we migrate all uses + it('accepts function as adapter id', () => { + }); + it('accepts string as adapter id', () => { + }); +}); diff --git a/packages/lwc-wire-service/src/__tests__/property-trap.spec.ts b/packages/lwc-wire-service/src/__tests__/property-trap.spec.ts new file mode 100644 index 0000000000..26ea13c22c --- /dev/null +++ b/packages/lwc-wire-service/src/__tests__/property-trap.spec.ts @@ -0,0 +1,59 @@ +import { + installTrap +} from '../property-trap'; +import { ConfigContext } from '../wiring'; + +describe('findDescriptor', () => { + it('detects circular prototype chains', () => { + }); + it('finds descriptor on super', () => { + }); +}); + +describe('installTrap', () => { + it('defaults to original value when setter installed', () => { + class Target { + prop1 = 'initial'; + } + const cmp = new Target(); + installTrap(cmp, 'prop1', {} as ConfigContext); + expect(cmp.prop1).toBe('initial'); + }); + it('updates original property when installed setter invoked', () => { + const expected = 'expected'; + class Target { + prop1; + } + const cmp = new Target(); + installTrap(cmp, 'prop1', {} as ConfigContext); + cmp.prop1 = expected; + expect(cmp.prop1).toBe(expected); + }); + it('installs setter on cmp for property', () => { + class Target { + set prop1(value) { /* empty */ } + } + const original = Object.getOwnPropertyDescriptor(Target.prototype, 'prop1'); + const cmp = new Target(); + installTrap(cmp, 'prop1', {} as ConfigContext); + const descriptor = Object.getOwnPropertyDescriptor(cmp, 'prop1'); + if (descriptor && original) { + expect(descriptor.set).not.toBe(original.set); + } + }); + it('invokes original setter when installed setter invoked', () => { + const setter = jest.fn(); + const expected = 'expected'; + class Target { + set prop1(value) { + setter(value); + } + get prop1() { return ''; } + } + const cmp = new Target(); + installTrap(cmp, 'prop1', {} as ConfigContext); + cmp.prop1 = expected; + expect(setter).toHaveBeenCalledTimes(1); + expect(setter).toHaveBeenCalledWith(expected); + }); +}); diff --git a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts index 419e62218c..822d0c4b78 100644 --- a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts +++ b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts @@ -1,51 +1,36 @@ import * as target from '../wiring'; -describe('wiring internal', () => { - describe('installs setter overrides', () => { - it("defaults to original value when setter installed", () => { - class Target { - prop1 = 'initial'; - } - const cmp = new Target(); - target.installSetterOverrides(cmp, 'prop1', jest.fn()); - expect(cmp.prop1).toBe('initial'); - }); - it("updates original property when installed setter invoked", () => { - const expected = 'expected'; - class Target { - prop1; - } - const cmp = new Target(); - target.installSetterOverrides(cmp, 'prop1', jest.fn()); - cmp.prop1 = expected; - expect(cmp.prop1).toBe(expected); - }); - it("installs setter on cmp for property", () => { - class Target { - set prop1(value) { /* empty */ } - } - const original = Object.getOwnPropertyDescriptor(Target.prototype, "prop1"); - const cmp = new Target(); - target.installSetterOverrides(cmp, 'prop1', jest.fn()); - const descriptor = Object.getOwnPropertyDescriptor(cmp, "prop1"); - if (descriptor && original) { - expect(descriptor.set).not.toBe(original.set); - } - }); - it("invokes original setter when installed setter invoked", () => { - const setter = jest.fn(); - const expected = 'expected'; - class Target { - set prop1(value) { - setter(value); - } - get prop1() { return ''; } - } - const cmp = new Target(); - target.installSetterOverrides(cmp, 'prop1', jest.fn()); - cmp.prop1 = expected; - expect(setter).toHaveBeenCalledTimes(1); - expect(setter).toHaveBeenCalledWith(expected); +describe('WireEventTarget', () => { + describe('connect event', () => { + it('addEventListener throws on duplicate listener', () => { }); }); + + describe('config event', () => { + it('addEventListener immediately fires when config is statics only', () => { + }); + it('multiple config listeners from one adapter creates only one trap per property', () => { + }); + it('multiple config listeners from multiple adapters creates only one trap per property', () => { + }); + it('invokes all registered listeners on prop change', () => { + }); + it('invokes config listener once when multiple props updated', () => { + }); + it('invokes config listener with getter value', () => { + // verify getter value is used which may differ from the setter's argument + }); + }); + + // TODO - do we need to do some cleanup at cmp "destroy"? what defines destroy? + + describe('dispatchEvent', () => { + it('ValueChangedEvent updates wired property', () => { + }); + it('ValueChangedEvent invokes wired method', () => { + }); + it('throws on non-ValueChangedEvent', () => { + }); + }); + }); diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 6f81ba4429..50b7958f85 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -57,7 +57,7 @@ const wireService = { const wireContext: WireContext = context[CONTEXT_ID] = Object.create(null); wireContext[CONTEXT_CONNECTED] = []; wireContext[CONTEXT_DISCONNECTED] = []; - wireContext[CONTEXT_UPDATED] = { map: {}, values: {} }; + wireContext[CONTEXT_UPDATED] = { listeners: {}, values: {} }; // engine guarantees invocation only if def.wire is defined const wireStaticDef = def.wire; @@ -65,10 +65,9 @@ const wireService = { for (let i = 0, len = wireTargets.length; i < len; i++) { const wireTarget = wireTargets[i]; const wireDef = wireStaticDef[wireTarget]; - const id = wireDef.adapter; - const wireEventTarget = new WireEventTarget(cmp, def, context, wireDef, wireTarget); - const adapterFactory = adapterFactories.get(id); + const adapterFactory = adapterFactories.get(wireDef.adapter); if (adapterFactory) { + const wireEventTarget = new WireEventTarget(cmp, def, context, wireDef, wireTarget); adapterFactory({ dispatchEvent: wireEventTarget.dispatchEvent.bind(wireEventTarget), addEventListener: wireEventTarget.addEventListener.bind(wireEventTarget), @@ -111,13 +110,4 @@ export function register(adapterId: any, adapterFactory: WireAdapterFactory) { adapterFactories.set(adapterId, adapterFactory); } -/** - * Unregisters an adapter, only available for non prod (e.g. test util) - */ -export function unregister(adapterId: any) { - if (process.env.NODE_ENV !== 'production') { - adapterFactories.delete(adapterId); - } -} - export { ValueChangedEvent } from './wiring'; diff --git a/packages/lwc-wire-service/src/property-trap.ts b/packages/lwc-wire-service/src/property-trap.ts new file mode 100644 index 0000000000..b9c65983b0 --- /dev/null +++ b/packages/lwc-wire-service/src/property-trap.ts @@ -0,0 +1,149 @@ +/* + * 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 { + Element, +} from './engine'; +import { + ConfigListenerMetadata, + ConfigContext, +} 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) { + for (const metadata of configListenerMetadatas) { + const { callback, statics, params } = metadata; + + const resolvedParams = Object.create(null); + if (params) { + const keys = Object.keys(params); + for (let j = 0, jlen = keys.length; j < jlen; j++) { + const key = keys[j]; + const value = paramValues[params[key]]; + resolvedParams[key] = value; + } + } + + // TODO - consider read-only membrane to enforce invariant of immutable config + const config = Object.assign({}, statics, resolvedParams); + callback.call(undefined, config); + } +} + +function updated(cmp: Element, prop: string, 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(prop); +} + +function updatedFuture(cmp: Element, 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; + for (const prop of mutated) { + const value = cmp[prop]; + if (configContext.values[prop] === value) { + continue; + } + configContext.values[prop] = value; + const listeners = configContext.listeners[prop]; + for (let i = 0, len = listeners.length; i < len; i++) { + uniqueListeners.add(listeners[i]); + } + } + invokeConfigListeners(uniqueListeners, configContext.values); +} + +/** + * Installs setter override to trap changes to a property, triggering the config listeners. + * @param cmp The component + * @param prop The name of the property to be monitored + * @param context The service context + */ +export function installTrap(cmp: Object, prop: string, configContext: ConfigContext) { + const callback = updated.bind(undefined, cmp, prop, configContext); + const newDescriptor = getOverrideDescriptor(cmp, prop, callback); + Object.defineProperty(cmp, prop, newDescriptor); +} + +/** + * Finds the descriptor of the named property on the prototype chain + * @param Ctor Constructor function + * @param propName Name of property to find + * @param protoSet Prototypes searched (to avoid circular prototype chains) + */ +function findDescriptor(Ctor: any, propName: PropertyKey, protoSet?: any[]): PropertyDescriptor | null { + protoSet = protoSet || []; + if (!Ctor || protoSet.indexOf(Ctor) > -1) { + return null; // null, undefined, or circular prototype definition + } + const proto = Object.getPrototypeOf(Ctor); + if (!proto) { + return null; + } + const descriptor = Object.getOwnPropertyDescriptor(proto, propName); + if (descriptor) { + return descriptor; + } + protoSet.push(Ctor); + 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; + // TODO: 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 index d85002665e..41009d94cf 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -8,12 +8,14 @@ import { DISCONNECT, CONFIG } from './constants'; - import { Element, ElementDef, WireDef } from './engine'; +import { + installTrap +} from './property-trap'; export type NoArgumentListener = () => void; export interface ConfigListenerArgument { @@ -32,7 +34,7 @@ export interface ConfigListenerMetadata { export interface ConfigContext { // map of param to list of config listeners // when a param changes O(1) lookup to list of config listeners to notify - map: { + listeners: { [prop: string]: ConfigListenerMetadata[]; }; // map of param values @@ -55,162 +57,10 @@ export interface Context { export type WireEventTargetCallback = NoArgumentListener | ConfigListener; -/** - * 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) { - for (const metadata of configListenerMetadatas) { - const { callback, statics, params } = metadata; - - const resolvedParams = Object.create(null); - if (params) { - const keys = Object.keys(params); - for (let j = 0, jlen = keys.length; j < jlen; j++) { - const key = keys[j]; - const value = paramValues[params[key]]; - resolvedParams[key] = value; - } - } - - // TODO - consider read-only membrane to enforce invariant of immutable config - const config = Object.assign({}, statics, resolvedParams); - callback.call(undefined, config); - } -} - -/** - * TODO - in early 216, engine will expose an `updated` callback for services that - * is invoked whenever a tracked property is changed. wire service is structured to - * make this adoption trivial. - */ -function updated(cmp: Element, prop: string, def: ElementDef, context: Context) { - let configContext: ConfigContext; - if (!def.wire || !(configContext = context[CONTEXT_ID][CONTEXT_UPDATED])) { - return; - } - - // TODO - don't think i can do this - // noop if value didn't change - const newValue = cmp[prop]; - if (configContext.values[prop] === newValue) { - return; - } - - 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(prop); -} - -function updatedFuture(cmp: Element, 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; - for (const prop of mutated) { - const value = cmp[prop]; - if (configContext.values[prop] === value) { - continue; - } - configContext.values[prop] = value; - const listeners = configContext.map[prop]; - for (let i = 0, len = listeners.length; i < len; i++) { - uniqueListeners.add(listeners[i]); - } - } - invokeConfigListeners(uniqueListeners, configContext.values); -} - -/** - * 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 - */ -export function installSetterOverrides(cmp: Object, prop: string, callback: () => void) { - const newDescriptor = getOverrideDescriptor(cmp, prop, callback); - Object.defineProperty(cmp, prop, newDescriptor); -} - -/** - * Finds the descriptor of the named property on the prototype chain - * @param Ctor Constructor function - * @param propName Name of property to find - * @param protoSet Prototypes searched (to avoid circular prototype chains) - */ -function findDescriptor(Ctor: any, propName: PropertyKey, protoSet?: any[]): PropertyDescriptor | null { - protoSet = protoSet || []; - if (!Ctor || protoSet.indexOf(Ctor) > -1) { - return null; // null, undefined, or circular prototype definition - } - const proto = Object.getPrototypeOf(Ctor); - if (!proto) { - return null; - } - const descriptor = Object.getOwnPropertyDescriptor(proto, propName); - if (descriptor) { - return descriptor; - } - protoSet.push(Ctor); - 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; - // TODO: 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, - }; -} - function removeCallback(callbacks: WireEventTargetCallback[], toRemove: WireEventTargetCallback) { - for (let i = 0, l = callbacks.length; i < l; i++) { - if (callbacks[i] === toRemove) { - callbacks.splice(i, 1); - return; - } + const idx = callbacks.indexOf(toRemove); + if (idx > -1) { + callbacks.splice(idx, 1); } } @@ -250,34 +100,44 @@ export class WireEventTarget { assert.isFalse(connectedCallbacks.includes(callback as NoArgumentListener), 'must not call addEventListener("connect") with the same callback'); connectedCallbacks.push(callback as NoArgumentListener); break; + case DISCONNECT: const disconnectedCallbacks = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; assert.isFalse(disconnectedCallbacks.includes(callback as NoArgumentListener), 'must not call addEventListener("disconnect") with the same callback'); disconnectedCallbacks.push(callback as NoArgumentListener); break; + case CONFIG: - const configContext = this._context[CONTEXT_ID][CONTEXT_UPDATED]; - const { params } = this._wireDef; + const params = this._wireDef.params; + const statics = this._wireDef.static; + + // no dynamic params, only static, so fire config once + if (!params) { + const config = statics || {}; + callback.call(undefined, config); + return; + } + const configListenerMetadata: ConfigListenerMetadata = { callback, - statics: this._wireDef.static, + statics, params }; - if (params) { - Object.keys(params).forEach(param => { - const prop = params[param]; - let configListenerMetadatas = configContext[prop]; - if (!configListenerMetadatas) { - configListenerMetadatas = [configListenerMetadata]; - configContext.map[prop] = configListenerMetadatas; - installSetterOverrides(this._cmp, prop, updated.bind(undefined, this._cmp, prop, this._def, this._context)); - } else { - configListenerMetadatas.push(configListenerMetadata); - } - }); - } + const configContext = this._context[CONTEXT_ID][CONTEXT_UPDATED]; + Object.keys(params).forEach(param => { + const prop = params[param]; + let configListenerMetadatas = configContext[prop]; + if (!configListenerMetadatas) { + configListenerMetadatas = [configListenerMetadata]; + configContext.listeners[prop] = configListenerMetadatas; + installTrap(this._cmp, prop, configContext); + } else { + configListenerMetadatas.push(configListenerMetadata); + } + }); break; + case 'default': throw new Error(`unsupported event type ${type}`); } @@ -289,10 +149,12 @@ export class WireEventTarget { const connectedCallbacks = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; removeCallback(connectedCallbacks, callback); break; + case DISCONNECT: const disconnectedCallbacks = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; removeCallback(disconnectedCallbacks, callback); break; + case CONFIG: const paramToConfigListenerMetadata = this._context[CONTEXT_ID][CONTEXT_UPDATED]; const { params } = this._wireDef; @@ -306,6 +168,7 @@ export class WireEventTarget { }); } break; + case 'default': throw new Error(`unsupported event type ${type}`); } From 81a30164b03bb8c9f2f6d916e1a7b8a1d2d02888 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 26 Mar 2018 10:14:55 -0700 Subject: [PATCH 42/52] refactor(wire-service): rename callback to listener --- packages/lwc-wire-service/src/index.ts | 26 ++++++------ .../lwc-wire-service/src/property-trap.ts | 4 +- packages/lwc-wire-service/src/wiring.ts | 42 +++++++++---------- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/lwc-wire-service/src/index.ts b/packages/lwc-wire-service/src/index.ts index 50b7958f85..a109c50f93 100644 --- a/packages/lwc-wire-service/src/index.ts +++ b/packages/lwc-wire-service/src/index.ts @@ -18,7 +18,7 @@ import { } from './engine'; import { NoArgumentListener, - WireEventTargetCallback, + WireEventTargetListener, Context, WireContext, WireEventTarget, @@ -27,8 +27,8 @@ import { export interface WireEventTarget { dispatchEvent(evt: ValueChangedEvent): boolean; - addEventListener(type: string, callback: WireEventTargetCallback): void; - removeEventListener(type: string, callback: WireEventTargetCallback): void; + addEventListener(type: string, listener: WireEventTargetListener): void; + removeEventListener(type: string, listener: WireEventTargetListener): void; } export type WireAdapterFactory = (eventTarget: WireEventTarget) => void; @@ -38,11 +38,11 @@ const adapterFactories: Map = new Map { - let callbacks: NoArgumentListener[]; - if (!def.wire || !(callbacks = context[CONTEXT_ID][CONTEXT_CONNECTED])) { + let listeners: NoArgumentListener[]; + if (!def.wire || !(listeners = context[CONTEXT_ID][CONTEXT_CONNECTED])) { return; } - invokeCallback(callbacks); + invokeListener(listeners); }, disconnected: (cmp: Element, data: object, def: ElementDef, context: Context) => { - let callbacks: NoArgumentListener[]; - if (!def.wire || !(callbacks = context[CONTEXT_ID][CONTEXT_DISCONNECTED])) { + let listeners: NoArgumentListener[]; + if (!def.wire || !(listeners = context[CONTEXT_ID][CONTEXT_DISCONNECTED])) { return; } - invokeCallback(callbacks); + invokeListener(listeners); } }; diff --git a/packages/lwc-wire-service/src/property-trap.ts b/packages/lwc-wire-service/src/property-trap.ts index b9c65983b0..bc8a2fc385 100644 --- a/packages/lwc-wire-service/src/property-trap.ts +++ b/packages/lwc-wire-service/src/property-trap.ts @@ -21,7 +21,7 @@ import { */ function invokeConfigListeners(configListenerMetadatas: Set, paramValues: any) { for (const metadata of configListenerMetadatas) { - const { callback, statics, params } = metadata; + const { listener, statics, params } = metadata; const resolvedParams = Object.create(null); if (params) { @@ -35,7 +35,7 @@ function invokeConfigListeners(configListenerMetadatas: Set void; export interface ConfigListenerMetadata { - callback: ConfigListener; + listener: ConfigListener; statics?: { [key: string]: any; }; @@ -55,18 +55,18 @@ export interface Context { [CONTEXT_ID]: WireContext; } -export type WireEventTargetCallback = NoArgumentListener | ConfigListener; +export type WireEventTargetListener = NoArgumentListener | ConfigListener; -function removeCallback(callbacks: WireEventTargetCallback[], toRemove: WireEventTargetCallback) { - const idx = callbacks.indexOf(toRemove); +function removeListener(listeners: WireEventTargetListener[], toRemove: WireEventTargetListener) { + const idx = listeners.indexOf(toRemove); if (idx > -1) { - callbacks.splice(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].callback === toRemove) { + if (configListenerMetadatas[i].listener === toRemove) { configListenerMetadatas.splice(i, 1); return; } @@ -93,18 +93,18 @@ export class WireEventTarget { this._wireTarget = wireTarget; } - addEventListener(type: string, callback: WireEventTargetCallback): void { + addEventListener(type: string, listener: WireEventTargetListener): void { switch (type) { case CONNECT: - const connectedCallbacks = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; - assert.isFalse(connectedCallbacks.includes(callback as NoArgumentListener), 'must not call addEventListener("connect") with the same callback'); - connectedCallbacks.push(callback as NoArgumentListener); + const connectedListeners = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; + 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 disconnectedCallbacks = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; - assert.isFalse(disconnectedCallbacks.includes(callback as NoArgumentListener), 'must not call addEventListener("disconnect") with the same callback'); - disconnectedCallbacks.push(callback as NoArgumentListener); + const disconnectedListeners = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; + assert.isFalse(disconnectedListeners.includes(listener as NoArgumentListener), 'must not call addEventListener("disconnect") with the same listener'); + disconnectedListeners.push(listener as NoArgumentListener); break; case CONFIG: @@ -114,12 +114,12 @@ export class WireEventTarget { // no dynamic params, only static, so fire config once if (!params) { const config = statics || {}; - callback.call(undefined, config); + listener.call(undefined, config); return; } const configListenerMetadata: ConfigListenerMetadata = { - callback, + listener, statics, params }; @@ -143,16 +143,16 @@ export class WireEventTarget { } } - removeEventListener(type: string, callback: WireEventTargetCallback): void { + removeEventListener(type: string, listener: WireEventTargetListener): void { switch (type) { case CONNECT: - const connectedCallbacks = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; - removeCallback(connectedCallbacks, callback); + const connectedListeners = this._context[CONTEXT_ID][CONTEXT_CONNECTED]; + removeListener(connectedListeners, listener); break; case DISCONNECT: - const disconnectedCallbacks = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; - removeCallback(disconnectedCallbacks, callback); + const disconnectedListeners = this._context[CONTEXT_ID][CONTEXT_DISCONNECTED]; + removeListener(disconnectedListeners, listener); break; case CONFIG: @@ -163,7 +163,7 @@ export class WireEventTarget { const prop = params[param]; const configListenerMetadatas = paramToConfigListenerMetadata[prop]; if (configListenerMetadatas) { - removeConfigListener(configListenerMetadatas, callback); + removeConfigListener(configListenerMetadatas, listener); } }); } From 0e5dbd1d6cf5bfb21853183836ed92c7c049c670 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 26 Mar 2018 11:05:41 -0700 Subject: [PATCH 43/52] feat(wire-service): check for WireContextEvent --- packages/lwc-wire-service/src/engine.ts | 3 +-- packages/lwc-wire-service/src/wiring.ts | 13 ++++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/lwc-wire-service/src/engine.ts b/packages/lwc-wire-service/src/engine.ts index 56c179e83a..742a8051fe 100644 --- a/packages/lwc-wire-service/src/engine.ts +++ b/packages/lwc-wire-service/src/engine.ts @@ -1,6 +1,5 @@ // subtypes from the engine -export { Element } from 'engine'; - +export { Element, ComposableEvent } from 'engine'; export interface WireDef { params?: { [key: string]: string; diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index 2ea863035f..ec6d86395d 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -11,7 +11,8 @@ import { import { Element, ElementDef, - WireDef + WireDef, + ComposableEvent } from './engine'; import { installTrap @@ -138,7 +139,7 @@ export class WireEventTarget { }); break; - case 'default': + default: throw new Error(`unsupported event type ${type}`); } } @@ -169,7 +170,7 @@ export class WireEventTarget { } break; - case 'default': + default: throw new Error(`unsupported event type ${type}`); } } @@ -183,6 +184,12 @@ export class WireEventTarget { this._cmp[this._wireTarget] = value; } return false; // canceling signal since we don't want this to propagate + } else if ((evt as ComposableEvent).type === 'WireContextEvent') { + // NOTE: kill this hack + // we should only allow ValueChangedEvent + // however, doing so would require adapter to implement machinery + // that fire the intended event as DOM event and wrap inside ValueChagnedEvent + return this._cmp.dispatchEvent(evt); } else { throw new Error(`Invalid event ${evt}.`); } From 6767eb13b4256b8565950217d8ea30d192f924cb Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 26 Mar 2018 14:06:43 -0700 Subject: [PATCH 44/52] feat(wire-service): add wiring jest tests --- .../src/__tests__/wiring.spec.ts | 172 ++++++++++++++++-- packages/lwc-wire-service/src/wiring.ts | 4 +- .../__tests__/index.spec.js | 1 - 3 files changed, 160 insertions(+), 17 deletions(-) diff --git a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts index 822d0c4b78..ef71d112ff 100644 --- a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts +++ b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts @@ -1,36 +1,180 @@ import * as target from '../wiring'; +import { + CONTEXT_ID, + CONTEXT_CONNECTED, + CONNECT, + CONTEXT_DISCONNECTED, + DISCONNECT, + CONTEXT_UPDATED, + CONFIG +} from '../constants'; +import { + Element, + ElementDef, + WireDef +} from '../engine'; +import * as dependency from '../property-trap'; describe('WireEventTarget', () => { - describe('connect event', () => { - it('addEventListener throws on duplicate listener', () => { + describe('addEventListener', () => { + describe('connect event', () => { + it('throws on duplicate listener', () => { + function dupeListener() { /**/ } + const mockContext = Object.create(null); + mockContext[CONTEXT_ID] = Object.create(null); + mockContext[CONTEXT_ID][CONTEXT_CONNECTED] = [dupeListener]; + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext, {} as WireDef, "test"); + expect(() => { wireEventTarget.addEventListener(CONNECT, dupeListener); }) + .toThrowError('must not call addEventListener("connect") with the same listener'); + }); + + it('adds listener to the queue', () => { + function listener() { /**/ } + const mockContext = Object.create(null); + mockContext[CONTEXT_ID] = Object.create(null); + mockContext[CONTEXT_ID][CONTEXT_CONNECTED] = []; + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext, {} as WireDef, "test"); + wireEventTarget.addEventListener(CONNECT, listener); + const actual = mockContext[CONTEXT_ID][CONTEXT_CONNECTED]; + expect(actual).toHaveLength(1); + expect(actual[0]).toBe(listener); + }); + }); + + describe('disconnect event', () => { + it('throws on duplicate listener', () => { + function dupeListener() { /**/ } + const mockContext = Object.create(null); + mockContext[CONTEXT_ID] = Object.create(null); + mockContext[CONTEXT_ID][CONTEXT_DISCONNECTED] = [dupeListener]; + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext, {} as WireDef, "test"); + expect(() => { wireEventTarget.addEventListener(DISCONNECT, dupeListener); }) + .toThrowError('must not call addEventListener("disconnect") with the same listener'); + }); + + it('adds listener to the queue', () => { + function listener() { /**/ } + const mockContext = Object.create(null); + mockContext[CONTEXT_ID] = Object.create(null); + mockContext[CONTEXT_ID][CONTEXT_DISCONNECTED] = []; + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext, {} as WireDef, "test"); + wireEventTarget.addEventListener(DISCONNECT, listener); + const actual = mockContext[CONTEXT_ID][CONTEXT_DISCONNECTED]; + expect(actual).toHaveLength(1); + expect(actual[0]).toBe(listener); + }); }); - }); - describe('config event', () => { - it('addEventListener immediately fires when config is statics only', () => { + describe('config event', () => { + it('immediately fires when config is statics only', () => { + let executed = false; + function listener(config) { + executed = true; + } + const mockWireDef = { + static: { + test: ["fixed", 'array'] + } + }; + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, {} as target.Context, mockWireDef as any, "test"); + wireEventTarget.addEventListener(CONFIG, listener); + expect(executed).toBeTruthy(); + }); + it('multiple listeners from one adapter creates only one trap per property', () => { + const wireContext = Object.create(null); + wireContext[CONTEXT_UPDATED] = { listeners: {}, values: {} }; + const mockContext = Object.create(null); + mockContext[CONTEXT_ID] = wireContext; + const mockWireDef = { + params: { + key: "prop" + } + }; + let invokeCounter = 0; + function mockInstallTrap() { + invokeCounter++; + } + const originalInstallTrap = dependency.installTrap; + (dependency as any).installTrap = mockInstallTrap; + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext, mockWireDef as any, "test"); + wireEventTarget.addEventListener(CONFIG, () => { /**/ }); + expect(invokeCounter).toBe(1); + const wireEventTarget1 = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext, mockWireDef as any, "test1"); + wireEventTarget1.addEventListener(CONFIG, () => { /**/ }); + expect(invokeCounter).toBe(1); + (dependency as any).installTrap = originalInstallTrap; + }); }); - it('multiple config listeners from one adapter creates only one trap per property', () => { + + it('throws when event type is not supported', () => { + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, {} as target.Context, {} as WireDef, "test"); + expect(() => { wireEventTarget.addEventListener('test', () => { /**/ }); }) + .toThrowError('unsupported event type test'); }); - it('multiple config listeners from multiple adapters creates only one trap per property', () => { + }); + + describe('removeEventListener', () => { + it('remove listener from the queue for connect event', () => { + function listener() { /**/ } + const mockContext = Object.create(null); + mockContext[CONTEXT_ID] = Object.create(null); + mockContext[CONTEXT_ID][CONTEXT_CONNECTED] = [listener]; + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext, {} as WireDef, "test"); + wireEventTarget.removeEventListener(CONNECT, listener); + expect(mockContext[CONTEXT_ID][CONTEXT_CONNECTED]).toHaveLength(0); }); - it('invokes all registered listeners on prop change', () => { + it('remove listener from the queue for disconnect event', () => { + function listener() { /**/ } + const mockContext = Object.create(null); + mockContext[CONTEXT_ID] = Object.create(null); + mockContext[CONTEXT_ID][CONTEXT_DISCONNECTED] = [listener]; + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext, {} as WireDef, "test"); + wireEventTarget.removeEventListener(DISCONNECT, listener); + expect(mockContext[CONTEXT_ID][CONTEXT_DISCONNECTED]).toHaveLength(0); }); - it('invokes config listener once when multiple props updated', () => { + it('remove listenerMetadata from the queue for config event', () => { + function listener() { /**/ } + const mockConfigListenerMetadata = { listener }; + const mockContext = Object.create(null); + mockContext[CONTEXT_ID] = Object.create(null); + mockContext[CONTEXT_ID][CONTEXT_UPDATED] = { listeners: { test: [mockConfigListenerMetadata] } }; + const mockWireDef = Object.create(null); + mockWireDef.params = {test: 'test'}; + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext, mockWireDef, "test"); + wireEventTarget.removeEventListener(CONFIG, listener); + expect(mockContext[CONTEXT_ID][CONTEXT_UPDATED].listeners.test).toHaveLength(0); }); - it('invokes config listener with getter value', () => { - // verify getter value is used which may differ from the setter's argument + it('throws when event type is not supported', () => { + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, {} as target.Context, {} as WireDef, "test"); + expect(() => { wireEventTarget.removeEventListener('test', () => { /**/ }); }) + .toThrowError('unsupported event type test'); }); }); - // TODO - do we need to do some cleanup at cmp "destroy"? what defines destroy? - describe('dispatchEvent', () => { it('ValueChangedEvent updates wired property', () => { + const mockCmp = { + test: undefined + }; + const wireEventTarget = new target.WireEventTarget(mockCmp as any, {} as ElementDef, {} as target.Context, {} as WireDef, "test"); + wireEventTarget.dispatchEvent(new target.ValueChangedEvent('value')); + expect(mockCmp.test).toBe('value'); }); it('ValueChangedEvent invokes wired method', () => { + let actual; + const mockCmp = { + test: (value) => { actual = value; } + }; + const wireEventTarget = new target.WireEventTarget(mockCmp as any, {} as ElementDef, {} as target.Context, { method: true } as any, "test"); + wireEventTarget.dispatchEvent(new target.ValueChangedEvent('value')); + expect(actual).toBe('value'); }); it('throws on non-ValueChangedEvent', () => { + const test = {}; + test.toString = () => 'test'; + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, {} as target.Context, {} as WireDef, "test"); + expect(() => { wireEventTarget.dispatchEvent(test as target.ValueChangedEvent); }) + .toThrowError('Invalid event test.'); }); }); - }); diff --git a/packages/lwc-wire-service/src/wiring.ts b/packages/lwc-wire-service/src/wiring.ts index ec6d86395d..2ab01a6e1e 100644 --- a/packages/lwc-wire-service/src/wiring.ts +++ b/packages/lwc-wire-service/src/wiring.ts @@ -128,7 +128,7 @@ export class WireEventTarget { const configContext = this._context[CONTEXT_ID][CONTEXT_UPDATED]; Object.keys(params).forEach(param => { const prop = params[param]; - let configListenerMetadatas = configContext[prop]; + let configListenerMetadatas = configContext.listeners[prop]; if (!configListenerMetadatas) { configListenerMetadatas = [configListenerMetadata]; configContext.listeners[prop] = configListenerMetadatas; @@ -157,7 +157,7 @@ export class WireEventTarget { break; case CONFIG: - const paramToConfigListenerMetadata = this._context[CONTEXT_ID][CONTEXT_UPDATED]; + const paramToConfigListenerMetadata = this._context[CONTEXT_ID][CONTEXT_UPDATED].listeners; const { params } = this._wireDef; if (params) { Object.keys(params).forEach(param => { diff --git a/packages/rollup-plugin-lwc-compiler/__tests__/index.spec.js b/packages/rollup-plugin-lwc-compiler/__tests__/index.spec.js index 26f2e409e5..c0091e669b 100644 --- a/packages/rollup-plugin-lwc-compiler/__tests__/index.spec.js +++ b/packages/rollup-plugin-lwc-compiler/__tests__/index.spec.js @@ -38,7 +38,6 @@ describe('rollup in compat mode', () => { const rollupOptions = { allowUnnamespaced: true, mode: 'compat' }; it(`simple app`, () => { - debugger; const entry = path.join(simpleAppDir, 'main.js'); return doRollup(entry, rollupOptions).then(({ code: actual }) => { const expected = fsExpected('expected_compat_config_simple_app'); From d7623ffc62146673586eb667fcbbb4d4850618f5 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 26 Mar 2018 14:16:55 -0700 Subject: [PATCH 45/52] refactor(wire-service): use jest mock fn for mockInstallTrap --- packages/lwc-wire-service/src/__tests__/wiring.spec.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts index ef71d112ff..2891bf706a 100644 --- a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts +++ b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts @@ -90,18 +90,15 @@ describe('WireEventTarget', () => { key: "prop" } }; - let invokeCounter = 0; - function mockInstallTrap() { - invokeCounter++; - } + const mockInstallTrap = jest.fn(); const originalInstallTrap = dependency.installTrap; (dependency as any).installTrap = mockInstallTrap; const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext, mockWireDef as any, "test"); wireEventTarget.addEventListener(CONFIG, () => { /**/ }); - expect(invokeCounter).toBe(1); + expect(mockInstallTrap).toHaveBeenCalled(); const wireEventTarget1 = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext, mockWireDef as any, "test1"); wireEventTarget1.addEventListener(CONFIG, () => { /**/ }); - expect(invokeCounter).toBe(1); + expect(mockInstallTrap).toHaveBeenCalledTimes(1); (dependency as any).installTrap = originalInstallTrap; }); }); From 4812f3034861533121c923f2fade164cefd7d5fb Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 26 Mar 2018 14:24:53 -0700 Subject: [PATCH 46/52] feat(wire-service): add jest test for register --- .../lwc-wire-service/src/__tests__/index.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/lwc-wire-service/src/__tests__/index.spec.ts b/packages/lwc-wire-service/src/__tests__/index.spec.ts index fbf07b5490..a1fd3273bb 100644 --- a/packages/lwc-wire-service/src/__tests__/index.spec.ts +++ b/packages/lwc-wire-service/src/__tests__/index.spec.ts @@ -1,5 +1,6 @@ import { registerWireService, + register } from '../index'; describe('wire service', () => { @@ -31,7 +32,20 @@ describe('wire service', () => { describe('register', () => { // TODO - reject non-function adapter id once we migrate all uses it('accepts function as adapter id', () => { + function getAdapter() { /**/ } + function getWireAdapter(wireEventTarget) { /**/ } + register(getAdapter, getWireAdapter); }); it('accepts string as adapter id', () => { + function getWireAdapter(wireEventTarget) { /**/ } + register('getAdapter', getWireAdapter); + }); + it('throws when adapter id is not truthy', () => { + function getWireAdapter(wireEventTarget) { /**/ } + expect(() => register(undefined, getWireAdapter)).toThrowError('adapter id must be truthy'); + }); + it('throws when adapter factory is not a function', () => { + function getAdapter() { /**/ } + expect(() => register(getAdapter, {} as any)).toThrowError('adapter factory must be a callable'); }); }); From 1ee07963b20efbbb7c6d632556f8b1177130b397 Mon Sep 17 00:00:00 2001 From: Kevin Venkiteswaran Date: Mon, 26 Mar 2018 14:46:17 -0700 Subject: [PATCH 47/52] test(wire-service): property-trap test cases --- .../src/__tests__/property-trap.spec.ts | 30 +++++++++++++++++-- .../lwc-wire-service/src/property-trap.ts | 18 +++++------ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/packages/lwc-wire-service/src/__tests__/property-trap.spec.ts b/packages/lwc-wire-service/src/__tests__/property-trap.spec.ts index 26ea13c22c..df97219ac3 100644 --- a/packages/lwc-wire-service/src/__tests__/property-trap.spec.ts +++ b/packages/lwc-wire-service/src/__tests__/property-trap.spec.ts @@ -1,13 +1,39 @@ import { - installTrap + installTrap, + findDescriptor } from '../property-trap'; import { ConfigContext } from '../wiring'; describe('findDescriptor', () => { it('detects circular prototype chains', () => { + function A(){}; + function B(){}; + B.prototype = Object.create(A.prototype); + A.prototype = Object.create(B.prototype); + const actual = findDescriptor(B, 'target'); + expect(actual).toBe(null); }); - it('finds descriptor on super', () => { + + it('finds descriptor on super with prototype setting', () => { + function A(){}; + A.prototype.target = 'target'; + function B(){}; + B.prototype = Object.create(A.prototype); + expect(findDescriptor(B, 'target')).toBe(null); + expect(findDescriptor(new B(), 'target')).not.toBe(null); }); + + it('finds descriptor on super with classes', () => { + class A { + target : any; + constructor() { + this.target = 'target'; + } + } + class B extends A {}; + expect(findDescriptor(B, 'target')).toBe(null); + expect(findDescriptor(new B(), 'target')).not.toBe(null); + }); }); describe('installTrap', () => { diff --git a/packages/lwc-wire-service/src/property-trap.ts b/packages/lwc-wire-service/src/property-trap.ts index bc8a2fc385..87a5fea5b4 100644 --- a/packages/lwc-wire-service/src/property-trap.ts +++ b/packages/lwc-wire-service/src/property-trap.ts @@ -82,24 +82,24 @@ export function installTrap(cmp: Object, prop: string, configContext: ConfigCont /** * Finds the descriptor of the named property on the prototype chain - * @param Ctor Constructor function + * @param target The target instance/constructor function * @param propName Name of property to find * @param protoSet Prototypes searched (to avoid circular prototype chains) */ -function findDescriptor(Ctor: any, propName: PropertyKey, protoSet?: any[]): PropertyDescriptor | null { +export function findDescriptor(target: any, propName: PropertyKey, protoSet?: any[]): PropertyDescriptor | null { protoSet = protoSet || []; - if (!Ctor || protoSet.indexOf(Ctor) > -1) { + if (!target || protoSet.indexOf(target) > -1) { return null; // null, undefined, or circular prototype definition } - const proto = Object.getPrototypeOf(Ctor); - if (!proto) { - return null; - } - const descriptor = Object.getOwnPropertyDescriptor(proto, propName); + const descriptor = Object.getOwnPropertyDescriptor(target, propName); if (descriptor) { return descriptor; } - protoSet.push(Ctor); + const proto = Object.getPrototypeOf(target); + if (!proto) { + return null; + } + protoSet.push(target); return findDescriptor(proto, propName, protoSet); } From a42f70adb331232b5a4df3bd8ca0211c9c572cf2 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 26 Mar 2018 15:05:36 -0700 Subject: [PATCH 48/52] refactor(wire-service): use jest.fn() instead of handroll function --- packages/lwc-wire-service/src/__tests__/wiring.spec.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts index 2891bf706a..5547da8d12 100644 --- a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts +++ b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts @@ -67,10 +67,7 @@ describe('WireEventTarget', () => { describe('config event', () => { it('immediately fires when config is statics only', () => { - let executed = false; - function listener(config) { - executed = true; - } + const listener = jest.fn(); const mockWireDef = { static: { test: ["fixed", 'array'] @@ -78,7 +75,7 @@ describe('WireEventTarget', () => { }; const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, {} as target.Context, mockWireDef as any, "test"); wireEventTarget.addEventListener(CONFIG, listener); - expect(executed).toBeTruthy(); + expect(listener).toHaveBeenCalledTimes(1); }); it('multiple listeners from one adapter creates only one trap per property', () => { const wireContext = Object.create(null); From d922d93a5a31f49ec4730fa3382a8143eca34cd7 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 26 Mar 2018 15:10:20 -0700 Subject: [PATCH 49/52] refactor(wire-service): use forEach instead of for...of for better compat perf --- packages/lwc-wire-service/src/property-trap.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lwc-wire-service/src/property-trap.ts b/packages/lwc-wire-service/src/property-trap.ts index bc8a2fc385..b7eed245ad 100644 --- a/packages/lwc-wire-service/src/property-trap.ts +++ b/packages/lwc-wire-service/src/property-trap.ts @@ -20,7 +20,7 @@ import { * @param paramValues values for all wire adapter config params */ function invokeConfigListeners(configListenerMetadatas: Set, paramValues: any) { - for (const metadata of configListenerMetadatas) { + configListenerMetadatas.forEach((metadata) => { const { listener, statics, params } = metadata; const resolvedParams = Object.create(null); @@ -36,7 +36,7 @@ function invokeConfigListeners(configListenerMetadatas: Set Date: Mon, 26 Mar 2018 15:17:25 -0700 Subject: [PATCH 50/52] feat(wire-service): add a test case to capture multiple components (context) --- .../src/__tests__/wiring.spec.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts index 5547da8d12..fa1e28069b 100644 --- a/packages/lwc-wire-service/src/__tests__/wiring.spec.ts +++ b/packages/lwc-wire-service/src/__tests__/wiring.spec.ts @@ -98,6 +98,34 @@ describe('WireEventTarget', () => { expect(mockInstallTrap).toHaveBeenCalledTimes(1); (dependency as any).installTrap = originalInstallTrap; }); + it('multiple components from one adapter create multiple traps', () => { + const wireContext1 = Object.create(null); + wireContext1[CONTEXT_UPDATED] = { listeners: {}, values: {} }; + const mockContext1 = Object.create(null); + mockContext1[CONTEXT_ID] = wireContext1; + + const wireContext2 = Object.create(null); + wireContext2[CONTEXT_UPDATED] = { listeners: {}, values: {} }; + const mockContext2 = Object.create(null); + mockContext2[CONTEXT_ID] = wireContext2; + + const mockWireDef = { + params: { + key: "prop" + } + }; + + const mockInstallTrap = jest.fn(); + const originalInstallTrap = dependency.installTrap; + (dependency as any).installTrap = mockInstallTrap; + const wireEventTarget = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext1, mockWireDef as any, "test"); + wireEventTarget.addEventListener(CONFIG, () => { /**/ }); + expect(mockInstallTrap).toHaveBeenCalled(); + const wireEventTarget1 = new target.WireEventTarget({} as Element, {} as ElementDef, mockContext2, mockWireDef as any, "test"); + wireEventTarget1.addEventListener(CONFIG, () => { /**/ }); + expect(mockInstallTrap).toHaveBeenCalledTimes(2); + (dependency as any).installTrap = originalInstallTrap; + }); }); it('throws when event type is not supported', () => { From abc8aca4f38bb341bb8ea59ff9496c7cfcad456c Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 26 Mar 2018 15:30:15 -0700 Subject: [PATCH 51/52] refactor(wire-service): fix lint errors --- .../src/__tests__/property-trap.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/lwc-wire-service/src/__tests__/property-trap.spec.ts b/packages/lwc-wire-service/src/__tests__/property-trap.spec.ts index df97219ac3..ee6754570f 100644 --- a/packages/lwc-wire-service/src/__tests__/property-trap.spec.ts +++ b/packages/lwc-wire-service/src/__tests__/property-trap.spec.ts @@ -6,8 +6,8 @@ import { ConfigContext } from '../wiring'; describe('findDescriptor', () => { it('detects circular prototype chains', () => { - function A(){}; - function B(){}; + function A() { /**/ } + function B() { /**/ } B.prototype = Object.create(A.prototype); A.prototype = Object.create(B.prototype); const actual = findDescriptor(B, 'target'); @@ -15,9 +15,9 @@ describe('findDescriptor', () => { }); it('finds descriptor on super with prototype setting', () => { - function A(){}; + function A() { /**/ } A.prototype.target = 'target'; - function B(){}; + function B() { /**/ } B.prototype = Object.create(A.prototype); expect(findDescriptor(B, 'target')).toBe(null); expect(findDescriptor(new B(), 'target')).not.toBe(null); @@ -25,12 +25,12 @@ describe('findDescriptor', () => { it('finds descriptor on super with classes', () => { class A { - target : any; + target: any; constructor() { this.target = 'target'; } } - class B extends A {}; + class B extends A {} expect(findDescriptor(B, 'target')).toBe(null); expect(findDescriptor(new B(), 'target')).not.toBe(null); }); From f85f8c92cb4395e76eb9fe48052d002f628a34c5 Mon Sep 17 00:00:00 2001 From: Vince Chen Date: Mon, 26 Mar 2018 15:53:49 -0700 Subject: [PATCH 52/52] build(wire-service): update module artifact name --- packages/lwc-wire-service/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lwc-wire-service/package.json b/packages/lwc-wire-service/package.json index 259064ef21..0f6a950388 100644 --- a/packages/lwc-wire-service/package.json +++ b/packages/lwc-wire-service/package.json @@ -34,7 +34,7 @@ }, "lwc": { "modules": { - "wire-service": "dist/modules/es2017/wire-service.js" + "wire-service": "dist/modules/es2017/wire.js" } } }