Skip to content

Commit

Permalink
fix(engine): karma tests for context providers
Browse files Browse the repository at this point in the history
  • Loading branch information
caridy committed Aug 29, 2019
1 parent 8b6c978 commit 2224bd2
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 163 deletions.
1 change: 1 addition & 0 deletions packages/@lwc/babel-plugin-component/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const LWC_API_WHITELIST = new Set([
'getComponentDef',
'getComponentConstructor',
'isComponentConstructor',
'createContextProvider',
'readonly',
'register',
'unwrap',
Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/engine/src/framework/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import '../polyfills/aria-properties/main';

// TODO: #1296 - Revisit these exports and figure out a better separation
export { createElement } from './upgrade';
export { createContextProvider } from './context-provider';
export { getComponentDef, isComponentConstructor, getComponentConstructor } from './def';
export { BaseLightningElement as LightningElement } from './base-lightning-element';
export { register } from './services';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { createElement } from 'lwc';
// import { registerWireService } from 'wire-service';
// registerWireService(register);
import { installCustomContext, getValueForIdentity } from 'x/advancedProvider';
import Consumer from 'x/advancedConsumer';
import { setValueForIdentity } from './x/advancedProvider/advancedProvider';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { createElement } from 'lwc';
// import { registerWireService } from 'wire-service';
// registerWireService(register);
import { installCustomContext, setCustomContext } from 'x/simpleProvider';
import Consumer from 'x/simpleConsumer';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { LightningElement, wire, api } from 'lwc';
import { Provider } from 'x/advancedProvider';
import { WireAdapter } from 'x/advancedProvider';

export default class ConsumerElement extends LightningElement {
@wire(Provider) context;
@wire(WireAdapter) context;

@api getIdentity() {
return this.context;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,38 @@
* This provide is sharing the same value with every child, and the
* identity of the consumer is not tracked.
*/

import { WireAdapter, ValueChangedEvent, LinkContextEvent } from 'wire-service';

const { addEventListener } = Document.prototype;
import { createContextProvider } from 'lwc';

const IdentityMetaMap = new WeakMap();
const UniqueEventName = `advanced_context_event_${guid()}`;
// const Provider = Symbol('SimpleContextProvider');
const ConsumerMetaMap = new WeakMap();

function guid() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
export class WireAdapter {
// no provider was found, in which case the default
// context should be set.
contextValue = null;

const adapterEventTargetCallback = eventTarget => {
let unsubscribeCallback;

function callback(data, unsubscribe) {
eventTarget.dispatchEvent(new ValueChangedEvent(data));
unsubscribeCallback = unsubscribe;
constructor(dataCallback) {
this._dataCallback = dataCallback;
// Note: you might also use a global identity in constructors
this._dataCallback(this.contextValue);
}

eventTarget.addEventListener('connect', () => {
const event = new LinkContextEvent(UniqueEventName, callback);
eventTarget.dispatchEvent(event);
if (unsubscribeCallback === undefined) {
// no provider was found, in which case the default
// context should be set.
const defaultContext = null;
// Note: you might decide to use a global identity instead
eventTarget.dispatchEvent(new ValueChangedEvent(defaultContext));
update(_config, context) {
if (!context || context.hasOwnProperty('value')) {
throw new Error(`Invalid context provided`);
}
});

eventTarget.addEventListener('disconnect', () => {
if (unsubscribeCallback !== undefined) {
unsubscribeCallback();
unsubscribeCallback = undefined; // resetting it to support reinsertion
}
});
};

const Provider = class extends WireAdapter {
constructor(dataCallback) {
super(dataCallback);
adapterEventTargetCallback(this.eventTarget);
this.contextValue = context.value;
this._dataCallback(this.contextValue);
}
connect() {
// noop
}
};
disconnect() {
// noop
}
static contextSchema = { value: 'required' /* could be 'optional' */ };
}

function createNewConsumerMeta(provider, callback) {
function createNewConsumerMeta(consumer) {
// identity must be an object that can't be proxified otherwise we
// loose the identity when tracking the value.
const identity = Object.freeze(_ => {
Expand All @@ -67,44 +48,46 @@ function createNewConsumerMeta(provider, callback) {
// this object is what we can get to via the weak map by using the identity as a key
const meta = {
identity,
callback,
provider,
consumer,
value,
};
// storing identity into the map
IdentityMetaMap.set(identity, meta);
ConsumerMetaMap.set(consumer, meta);
return meta;
}

function disconnectConsumer(eventTarget, consumerMeta) {
const meta = IdentityMetaMap.get(consumerMeta.identity);
if (meta !== undefined) {
// take care of disconnecting everything for this consumer
// ...
// then remove the identity from the map
IdentityMetaMap.delete(consumerMeta.identity);
} else {
throw new TypeError(`Invalid context operation in ${eventTarget}.`);
function decommissionConsumer(consumer) {
const meta = ConsumerMetaMap.get(consumer);
if (meta === undefined) {
// this should never happen unless you decommission consumers
// manually without waiting for the disconnect to occur.
throw new TypeError(`Invalid context operation.`);
}
// take care of disconnecting everything for this consumer
// ...
// then remove the identity and consumer from maps
IdentityMetaMap.delete(meta.identity);
ConsumerMetaMap.delete(consumer.identity);
}

function setupNewContextProvider(eventTarget) {
addEventListener.call(eventTarget, UniqueEventName, event => {
// this event must have a full stop when it is intercepted by a provider
event.stopImmediatePropagation();
// the new child provides a callback as a communication channel
const { detail: callback } = event;
// create consumer metadata as soon as it is connected
const consumerMeta = createNewConsumerMeta(eventTarget, callback);
// emit the identity value and provide disconnect callback
callback(consumerMeta.identity, () => disconnectConsumer(eventTarget, consumerMeta));
const contextualizer = createContextProvider(WireAdapter);

export function installCustomContext(target) {
// Note: the identity of the consumer is already bound to the target.
contextualizer(target, {
consumerConnectedCallback(consumer) {
// create consumer metadata as soon as it is connected
const consumerMeta = createNewConsumerMeta(target, consumer);
// emit the identity value
consumer.provide({ value: consumerMeta.identity });
},
consumerDisconnectedCallback(consumer) {
decommissionConsumer(consumer);
},
});
}

export function installCustomContext(elm) {
setupNewContextProvider(elm);
}

export function setValueForIdentity(identity, value) {
const meta = IdentityMetaMap.get(identity);
if (meta !== undefined) {
Expand All @@ -118,5 +101,3 @@ export function getValueForIdentity(identity) {
const meta = IdentityMetaMap.get(identity);
return meta.value;
}

export { Provider };
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LightningElement, wire } from 'lwc';
import { Provider } from 'x/simpleProvider';
import { WireAdapter } from 'x/simpleProvider';

export default class ConsumerElement extends LightningElement {
@wire(Provider) context;
@wire(WireAdapter) context;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,9 @@
* This provide is sharing the same value with every child, and the
* identity of the consumer is not tracked.
*/

// Per Context Component Instance, track the current context data
import { WireAdapter, ValueChangedEvent, LinkContextEvent } from 'wire-service';

const { addEventListener } = Document.prototype;
import { createContextProvider } from 'lwc';

const ContextValueMap = new WeakMap();
const UniqueEventName = `simple_context_event_${guid()}`;
// const Provider = Symbol('SimpleContextProvider');

function guid() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}

function getDefaultContext() {
return 'missing';
Expand All @@ -37,96 +25,69 @@ function createContextPayload(value) {
return value;
}

// register(Provider, eventTarget => {
const adapterEventTargetCallback = eventTarget => {
let unsubscribeCallback;

function callback(value, unsubscribe) {
eventTarget.dispatchEvent(new ValueChangedEvent(createContextPayload(value)));
unsubscribeCallback = unsubscribe;
export class WireAdapter {
contextValue = getDefaultContext();
constructor(dataCallback) {
this._dataCallback = dataCallback;
this._dataCallback(createContextPayload(this.contextValue));
}

eventTarget.addEventListener('connect', () => {
const event = new LinkContextEvent(UniqueEventName, callback);
eventTarget.dispatchEvent(event);
if (unsubscribeCallback === undefined) {
// no provider was found, in which case the default
// context should be set.
const defaultContext = getDefaultContext();
eventTarget.dispatchEvent(new ValueChangedEvent(createContextPayload(defaultContext)));
update(_config, context) {
if (!context || context.hasOwnProperty('value')) {
throw new Error(`Invalid context provided`);
}
});

eventTarget.addEventListener('disconnect', () => {
if (unsubscribeCallback !== undefined) {
unsubscribeCallback();
unsubscribeCallback = undefined; // resetting it to support reinsertion
}
});
};

const Provider = class extends WireAdapter {
constructor(dataCallback) {
super(dataCallback);
adapterEventTargetCallback(this.eventTarget);
this.contextValue = context.value;
this._dataCallback(createContextPayload(this.contextValue));
}
};
connect() {
// noop
}
disconnect() {
// noop
}
static configSchema = {};
static contextSchema = { value: 'required' /* could be 'optional' */ };
}

function getContextData(eventTarget) {
let contextData = ContextValueMap.get(eventTarget);
if (contextData === undefined) {
// collection of consumers' callbacks and default context value per provider instance
// collection of consumers and default context value per provider instance
contextData = {
listeners: [],
consumers: [],
value: getInitialContext(), // initial value for an installed provider
};
ContextValueMap.set(eventTarget, contextData);
}
return contextData;
}

function disconnectConsumer(eventTarget, contextData, callback) {
const i = contextData.listeners.indexOf(callback);
if (i >= 0) {
contextData.listeners.splice(i, 1);
} else {
throw new TypeError(`Invalid context operation in ${eventTarget}.`);
}
}

function setupNewContextProvider(eventTarget) {
let contextData; // lazy initialization
addEventListener.call(eventTarget, UniqueEventName, event => {
// this event must have a full stop when it is intercepted by a provider
event.stopImmediatePropagation();
// the new child provides a callback as a communication channel
const { detail: callback } = event;
// once the first consumer gets connected, then we create the contextData object
if (contextData === undefined) {
contextData = getContextData(eventTarget);
}
// registering the new callback
contextData.listeners.push(callback);
// emit the current value and provide disconnect callback
callback(contextData.value, () => disconnectConsumer(eventTarget, contextData, callback));
const contextualizer = createContextProvider(WireAdapter);

export function installCustomContext(target) {
contextualizer(target, {
consumerConnectedCallback(consumer) {
// once the first consumer gets connected, then we create the contextData object
const contextData = getContextData(target);
// registering the new consumer
contextData.consumers.push(consumer);
// push the current value
consumer.provide({ value: contextData.value });
},
consumerDisconnectedCallback(consumer) {
const contextData = getContextData(target);
const i = contextData.consumers.indexOf(consumer);
if (i >= 0) {
contextData.consumers.splice(i, 1);
} else {
throw new TypeError(`Invalid context operation in ${target}.`);
}
},
});
}

function emitNewContextValue(eventTarget, newValue) {
const contextData = getContextData(eventTarget);
export function setCustomContext(target, newValue) {
const contextData = getContextData(target);
// in this example, all consumers get the same context value
contextData.value = newValue;
contextData.listeners.forEach(callback =>
callback(newValue, () => disconnectConsumer(eventTarget, contextData, callback))
);
}

export function installCustomContext(node) {
setupNewContextProvider(node);
contextData.consumers.forEach(consumer => consumer.provide({ value: newValue }));
}

export function setCustomContext(node, newValue) {
emitNewContextValue(node, newValue);
}

export { Provider };

0 comments on commit 2224bd2

Please sign in to comment.