From 8cb5da6968807e8ed77b22d7e1d1c2b34e1fe95d Mon Sep 17 00:00:00 2001 From: Ryan Cebulko Date: Mon, 17 May 2021 13:33:18 -0400 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Enable=20passing=20type-ch?= =?UTF-8?q?ecking=20on=20src/context=20(#34387)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enable src-context type-check target * Fix type errors in src/context * export ContextPropDef * for...of in scan * clean up scheduler types * Update ContextPropDef in subscriber * update types in index * update types in index * update types in contextprops * update types and for...of in node * Drastically narrow types in values * Add DEP template type to contextpropdef * type-narrow subscriber * Clean up scheduler type decl * use templates in scan * Update index prop and setter types * Allow subscriber ID template type * Add deps template arg for contextprops * Add SID for return type * Assert non-null subscriber IDs * Code review tweak * Remove template type from static function * forEach -> for..of * Revert forEach changes to Maps --- build-system/tasks/check-types.js | 4 +- src/context/contextprops.js | 9 +- src/context/index.js | 22 ++-- src/context/node.js | 90 ++++++--------- src/context/prop.js | 14 ++- src/context/prop.type.js | 21 ++-- src/context/scan.js | 18 +-- src/context/scheduler.js | 10 +- src/context/shame.extern.js | 34 ++++++ src/context/subscriber.js | 33 +++--- src/context/values.js | 185 +++++++++++++++++------------- 11 files changed, 252 insertions(+), 188 deletions(-) create mode 100644 src/context/shame.extern.js diff --git a/build-system/tasks/check-types.js b/build-system/tasks/check-types.js index f354fb290bff..3496c9b26e4f 100644 --- a/build-system/tasks/check-types.js +++ b/build-system/tasks/check-types.js @@ -110,8 +110,8 @@ const TYPE_CHECK_TARGETS = { warningLevel: 'QUIET', }, 'src-context': { - srcGlobs: ['src/context/**/*.js'], - warningLevel: 'QUIET', + srcGlobs: ['src/context/**/*.js', ...CORE_SRCS_GLOBS], + externGlobs: ['src/context/**/*.extern.js', ...CORE_EXTERNS_GLOBS], }, 'src-core': { srcGlobs: CORE_SRCS_GLOBS, diff --git a/src/context/contextprops.js b/src/context/contextprops.js index 90789da92084..cca7204d9046 100644 --- a/src/context/contextprops.js +++ b/src/context/contextprops.js @@ -17,6 +17,9 @@ import {Loading, reducer as loadingReducer} from '../core/loading-instructions'; import {contextProp} from './prop'; +// typedef imports +import {ContextPropDef} from './prop.type'; + /** * Defines whether a DOM subtree can be currently seen by the user. A subtree * can be not renderable due `display: none`, or `hidden` attribute, unslotted @@ -25,7 +28,7 @@ import {contextProp} from './prop'; * * Default is `true`. * - * @const {!ContextProp} + * @const {!ContextPropDef} */ const CanRender = contextProp('CanRender', { defaultValue: true, @@ -42,7 +45,7 @@ const CanRender = contextProp('CanRender', { * * Default is `true`. * - * @const {!ContextProp} + * @const {!ContextPropDef} */ const CanPlay = contextProp('CanPlay', { defaultValue: true, @@ -59,7 +62,7 @@ const CanPlay = contextProp('CanPlay', { * * Default is "auto". * - * @const {!ContextProp} + * @const {!ContextPropDef} */ const LoadingProp = contextProp('Loading', { defaultValue: Loading.AUTO, diff --git a/src/context/index.js b/src/context/index.js index 4f1f1fa7281f..8353f40ab92c 100644 --- a/src/context/index.js +++ b/src/context/index.js @@ -15,10 +15,12 @@ */ import {ContextNode} from './node'; - export {contextProp} from './prop'; export {subscribe, unsubscribe} from './subscriber'; +// typedef imports +import {ContextPropDef} from './prop.type'; + /** * Direct slot assignment. Works the same way as shadow slots, but does not * require a shadow root. Automatically starts the discovery phase for the @@ -97,8 +99,8 @@ export function rediscoverChildren(node) { * All dependent properties are also recalculated. * * @param {!Node} node The target node. - * @param {!ContextProp} prop - * @param {*} setter + * @param {!ContextPropDef} prop + * @param {function(T)} setter * @param {T} value * @template T */ @@ -111,8 +113,9 @@ export function setProp(node, prop, setter, value) { * See `setProp()` for more info. * * @param {!Node} node The target node. - * @param {!ContextProp} prop - * @param {*} setter + * @param {!ContextPropDef} prop + * @param {function(T)} setter + * @template T */ export function removeProp(node, prop, setter) { ContextNode.get(node).values.remove(prop, setter); @@ -131,8 +134,8 @@ export function addGroup(node, name, match, weight = 0) { /** * @param {!Node} node * @param {string} groupName - * @param {!ContextProp} prop - * @param {*} setter + * @param {!ContextPropDef} prop + * @param {function(T)} setter * @param {T} value * @template T */ @@ -143,8 +146,9 @@ export function setGroupProp(node, groupName, prop, setter, value) { /** * @param {!Node} node * @param {string} groupName - * @param {!ContextProp} prop - * @param {*} setter + * @param {!ContextPropDef} prop + * @param {function(T)} setter + * @template T */ export function removeGroupProp(node, groupName, prop, setter) { ContextNode.get(node).group(groupName).values.remove(prop, setter); diff --git a/src/context/node.js b/src/context/node.js index 0f0b56535806..903cd5270129 100644 --- a/src/context/node.js +++ b/src/context/node.js @@ -15,11 +15,14 @@ */ import {Values} from './values'; -import {devAssert} from '../core/assert'; +import {devAssert, devAssertElement} from '../core/assert'; import {getMode} from '../mode'; import {pushIfNotExist, removeItem} from '../core/types/array'; import {throttleTail} from './scheduler'; +// typedef imports +import {ContextPropDef} from './prop.type'; + // Properties set on the DOM nodes to track the context state. const NODE_PROP = '__AMP_NODE'; const ASSIGNED_SLOT_PROP = '__AMP_ASSIGNED_SLOT'; @@ -50,6 +53,7 @@ let GroupDef; * are auto-discovered or prompted. * * @package + * @template SID subscriber ID type(s) */ export class ContextNode { /** @@ -105,16 +109,18 @@ export class ContextNode { return /** @type {!ContextNode} */ (n[NODE_PROP]); } const {nodeType} = n; - if (nodeType == DOCUMENT_NODE || nodeType == FRAGMENT_NODE) { + if ( // A context node is always created for a root. Due to this, a // non-root element is always at least attached to a root. This // allows for quick discovery and reattachment when new information // becomes available. - return ContextNode.get(n); - } - if (nodeType == ELEMENT_NODE && n.tagName.startsWith(AMP_PREFIX)) { + nodeType == DOCUMENT_NODE || + nodeType == FRAGMENT_NODE || // An AMP node will always have a context node backing it at some // point. + (nodeType == ELEMENT_NODE && + devAssertElement(n).tagName.startsWith(AMP_PREFIX)) + ) { return ContextNode.get(n); } } @@ -173,10 +179,7 @@ export class ContextNode { */ static rediscoverChildren(node) { const contextNode = /** @type {!ContextNode|undefined} */ (node[NODE_PROP]); - const children = contextNode && contextNode.children; - if (children) { - children.forEach(discoverContextNode); - } + contextNode?.children?.forEach(discoverContextNode); } /** @@ -233,7 +236,7 @@ export class ContextNode { /** @package {!Values} */ this.values = new Values(this); - /** @private {?Map<*, !./subscriber.Subscriber>} */ + /** @private {?Map} */ this.subscribers_ = null; /** @private {boolean} */ @@ -250,14 +253,9 @@ export class ContextNode { node.addEventListener('slotchange', (e) => { const slot = /** @type {!HTMLSlotElement} */ (e.target); // Rediscover newly assigned nodes. - const assignedNodes = slot.assignedNodes(); - assignedNodes.forEach(discoverContained); + slot.assignedNodes().forEach(discoverContained); // Rediscover unassigned nodes. - const closest = ContextNode.closest(slot); - const closestChildren = closest && closest.children; - if (closestChildren) { - closestChildren.forEach(discoverContextNode); - } + ContextNode.closest(slot)?.children?.forEach(discoverContextNode); }); } @@ -293,10 +291,9 @@ export class ContextNode { * @param {!ContextNode|!Node|null} parent */ setParent(parent) { - const parentContext = - parent && parent.nodeType - ? ContextNode.get(/** @type {!Node} */ (parent)) - : /** @type {?ContextNode} */ (parent); + const parentContext = parent?.nodeType + ? ContextNode.get(/** @type {!Node} */ (parent)) + : /** @type {?ContextNode} */ (parent); this.updateTree_(parentContext, /* parentOverridden */ parent != null); } @@ -308,7 +305,7 @@ export class ContextNode { */ setIsRoot(isRoot) { this.isRoot = isRoot; - const newRoot = isRoot ? this : this.parent ? this.parent.root : null; + const newRoot = isRoot ? this : this.parent?.root ?? null; this.updateRoot(newRoot); } @@ -327,17 +324,10 @@ export class ContextNode { this.values.rootUpdated(); // Make sure the tree changes have been reflected for subscribers. - const subscribers = this.subscribers_; - if (subscribers) { - subscribers.forEach((comp) => { - comp.rootUpdated(); - }); - } + this.subscribers_?.forEach((comp) => comp.rootUpdated()); // Propagate the root to the subtree. - if (this.children) { - this.children.forEach((child) => child.updateRoot(root)); - } + this.children?.forEach((child) => child.updateRoot(root)); } } @@ -353,9 +343,7 @@ export class ContextNode { const cn = new ContextNode(node, name); groups.set(name, {cn, match, weight}); cn.setParent(this); - if (children) { - children.forEach(discoverContextNode); - } + children?.forEach(discoverContextNode); return cn; } @@ -364,9 +352,7 @@ export class ContextNode { * @return {?ContextNode} */ group(name) { - const {groups} = this; - const group = groups && groups.get(name); - return (group && group.cn) || null; + return this.groups?.get(name)?.cn || null; } /** @@ -395,16 +381,16 @@ export class ContextNode { * yet exist, it will be created using the specified factory. The use * of factory is important to reduce bundling costs for context node. * - * @param {*} id - * @param {function(new:./subscriber.Subscriber, function(...?), !Array):void} constr + * @param {!SID} id + * @param {typeof ./subscriber.Subscriber} Ctor * @param {!Function} func - * @param {!Array} deps + * @param {!Array} deps */ - subscribe(id, constr, func, deps) { + subscribe(id, Ctor, func, deps) { const subscribers = this.subscribers_ || (this.subscribers_ = new Map()); let subscriber = subscribers.get(id); if (!subscriber) { - subscriber = new constr(this, func, deps); + subscriber = new Ctor(this, func, deps); subscribers.set(id, subscriber); } } @@ -412,11 +398,11 @@ export class ContextNode { /** * Removes the subscriber previously set with `subscribe`. * - * @param {*} id + * @param {!SID} id */ unsubscribe(id) { const subscribers = this.subscribers_; - const subscriber = subscribers && subscribers.get(id); + const subscriber = subscribers?.get(id); if (subscriber) { subscriber.dispose(); subscribers.delete(id); @@ -434,8 +420,7 @@ export class ContextNode { return; } const closestNode = ContextNode.closest(this.node, /* includeSelf */ false); - const parent = - (closestNode && closestNode.findGroup(this.node)) || closestNode; + const parent = closestNode?.findGroup(this.node) || closestNode; this.updateTree_(parent, /* parentOverridden */ false); } @@ -453,8 +438,8 @@ export class ContextNode { this.parent = parent; // Remove from the old parent. - if (oldParent && oldParent.children) { - removeItem(oldParent.children, this); + if (oldParent?.children) { + removeItem(devAssert(oldParent.children), this); } // Add to the new parent. @@ -466,8 +451,7 @@ export class ContextNode { // it's other children. // Since the new parent (`this`) is already known, this is a very // fast operation. - for (let i = 0; i < parentChildren.length; i++) { - const child = parentChildren[i]; + for (const child of parentChildren) { if (child != this && child.isDiscoverable()) { child.discover(); } @@ -478,7 +462,7 @@ export class ContextNode { } // Check the root. - this.updateRoot(parent ? parent.root : null); + this.updateRoot(parent?.root ?? null); } } @@ -498,11 +482,11 @@ function forEachContained(node, callback, includeSelf = true) { if (closest.node == node) { callback(closest); } else if (closest.children) { - closest.children.forEach((child) => { + for (const child of closest.children) { if (node.contains(child.node)) { callback(child); } - }); + } } } diff --git a/src/context/prop.js b/src/context/prop.js index 2a4f6d7600bd..ab5aad1a20c9 100644 --- a/src/context/prop.js +++ b/src/context/prop.js @@ -16,24 +16,28 @@ import {devAssert} from '../core/assert'; +// typedef imports +import {ContextPropDef} from './prop.type'; + const EMPTY_DEPS = []; /** - * Creates the `ContextProp` type. + * Creates the `ContextPropDef` type. * * @param {string} key * @param {{ * type: (!Object|undefined), - * deps: (!Array|undefined), + * deps: (!Array>|undefined), * recursive: (boolean|(function(!Array):boolean)|undefined), - * compute: ((function(!Node, !Array, ...*):(T|undefined))|undefined), + * compute: (function(!Node, !Array, ...DEP):(T|undefined)), * defaultValue: (T|undefined), * }=} opt_spec - * @return {!ContextProp} + * @return {!ContextPropDef} * @template T + * @template DEP */ export function contextProp(key, opt_spec) { - const prop = /** @type {!ContextProp} */ ({ + const prop = /** @type {!ContextPropDef} */ ({ key, // Default values. type: null, diff --git a/src/context/prop.type.js b/src/context/prop.type.js index 67d8f26afdfb..3d7eb3b8a953 100644 --- a/src/context/prop.type.js +++ b/src/context/prop.type.js @@ -14,15 +14,14 @@ * limitations under the License. */ -/** @externs */ - /** * A context property. * * @interface * @template T + * @template DEP */ -function ContextProp() {} +export function ContextPropDef() {} /** * A globally unique key. Extensions must use a fully qualified name such @@ -30,7 +29,7 @@ function ContextProp() {} * * @type {string} */ -ContextProp.prototype.key; +ContextPropDef.prototype.key; /** * An optional type object that can be used for a using system. E.g. @@ -38,14 +37,14 @@ ContextProp.prototype.key; * * @type {?Object} */ -ContextProp.prototype.type; +ContextPropDef.prototype.type; /** * An array of dependencies that are required for the `compute` callback. * - * @type {!Array} + * @type {!Array>} */ -ContextProp.prototype.deps; +ContextPropDef.prototype.deps; /** * Whether the value needs a recursive resolution of the parent value. The @@ -66,7 +65,7 @@ ContextProp.prototype.deps; * * @type {boolean|function(!Array):boolean} */ -ContextProp.prototype.recursive; +ContextPropDef.prototype.recursive; /** * Computes the property value. This callback is passed the following @@ -76,13 +75,13 @@ ContextProp.prototype.recursive; * 3. If it's a recursive property, the parent value. * 4. If `deps` are specified - the dep values. * - * @type {function(!Node, !Array, ...*):(T|undefined)} + * @type {function(!Node, !Array, ...DEP):(T|undefined)} */ -ContextProp.prototype.compute; +ContextPropDef.prototype.compute; /** * The default value of a recursive property. * * @type {T|undefined} */ -ContextProp.prototype.defaultValue; +ContextPropDef.prototype.defaultValue; diff --git a/src/context/scan.js b/src/context/scan.js index ccd57d044f75..0d5c16bd8163 100644 --- a/src/context/scan.js +++ b/src/context/scan.js @@ -19,10 +19,11 @@ * matches the predicate with an optional argument. * * @param {!./node.ContextNode} startNode - * @param {function(!./node.ContextNode, ?):boolean} predicate - * @param {?=} arg + * @param {function(!./node.ContextNode, T):boolean} predicate + * @param {T=} arg * @param {boolean=} includeSelf * @return {?./node.ContextNode} + * @template T */ export function findParent( startNode, @@ -45,11 +46,12 @@ export function findParent( * and the result value will be passed to the children callbacks. * * @param {!./node.ContextNode} startNode - * @param {function(!./node.ContextNode, ?, ?):*} callback - * @param {?=} arg - * @param {?=} state + * @param {function(!./node.ContextNode, T, S):S} callback + * @param {T=} arg + * @param {S=} state * @param {boolean=} includeSelf - * @return {?} + * @template T + * @template S */ export function deepScan( startNode, @@ -64,8 +66,8 @@ export function deepScan( deepScan(startNode, callback, arg, newState, false); } } else if (startNode.children) { - for (let i = 0; i < startNode.children.length; i++) { - deepScan(startNode.children[i], callback, arg, state, true); + for (const node of startNode.children) { + deepScan(node, callback, arg, state, true); } } } diff --git a/src/context/scheduler.js b/src/context/scheduler.js index f234c4c8fc2c..d369d7a35ae8 100644 --- a/src/context/scheduler.js +++ b/src/context/scheduler.js @@ -14,13 +14,15 @@ * limitations under the License. */ +/** @typedef {function(function())} */ +let SchedulerDef; /** * Creates a scheduling function that executes the callback based on the * scheduler, but only one task at a time. * * @param {function()} handler - * @param {?function(!Function)} defaultScheduler - * @return {function(function(!Function)=)} + * @param {?SchedulerDef} defaultScheduler + * @return {function(!SchedulerDef=)} */ export function throttleTail(handler, defaultScheduler = null) { let scheduled = false; @@ -28,9 +30,7 @@ export function throttleTail(handler, defaultScheduler = null) { scheduled = false; handler(); }; - /** - * @param {function(!Function)=} opt_scheduler - */ + /** @param {!SchedulerDef=} opt_scheduler */ const scheduleIfNotScheduled = (opt_scheduler) => { if (!scheduled) { scheduled = true; diff --git a/src/context/shame.extern.js b/src/context/shame.extern.js new file mode 100644 index 000000000000..3dd2326f3e66 --- /dev/null +++ b/src/context/shame.extern.js @@ -0,0 +1,34 @@ +/** + * Copyright 2021 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview The junk-drawer of externs that haven't yet been sorted well. + * Shame! Shame! Shame! Avoid adding to this. + * + * It's okay for some things to start off here, since moving them doesn't + * require any other file changes (unlike real code, which requires updating) + * imports throughout the repo). + * + * @externs + */ + +// This definition is exported by src/mode.js, but currently has dependencies on +// non-core modules that aren't yet type-checked. +// +// Planned destination: this should be removed when mode flags are make +// core-accessible +/** @type {function(!Window=):{localDev: boolean, test: boolean}} */ +let getMode$$module$src$mode; diff --git a/src/context/subscriber.js b/src/context/subscriber.js index 284d849616e1..fa8f4065ba81 100644 --- a/src/context/subscriber.js +++ b/src/context/subscriber.js @@ -19,6 +19,9 @@ import {arrayOrSingleItemToArray} from '../core/types/array'; import {throttleTail} from './scheduler'; import {tryCallback} from '../core/error'; +// typedef imports +import {ContextPropDef} from './prop.type'; + const EMPTY_ARRAY = []; const EMPTY_FUNC = () => {}; @@ -31,8 +34,9 @@ const EMPTY_FUNC = () => {}; * - A subscriber can optionally return a cleanup function. * * @param {!Node} node - * @param {!ContextProp|!Array} deps - * @param {function(...?)} callback + * @param {!ContextPropDef|!Array>} deps + * @param {function(...DEP)} callback + * @template DEP */ export function subscribe(node, deps, callback) { deps = arrayOrSingleItemToArray(deps); @@ -45,7 +49,8 @@ export function subscribe(node, deps, callback) { * Removes the subscriber prevoiously registered with `subscribe` API. * * @param {!Node} node - * @param {function(...?)} callback + * @param {function(...DEP)} callback + * @template DEP */ export function unsubscribe(node, callback) { const id = callback; @@ -58,32 +63,33 @@ export function unsubscribe(node, callback) { * internal state, and cleanup functions. * * @package + * @template DEP */ export class Subscriber { /** - * @param {!./node.ContextNode} contextNode - * @param {function(...?)} func - * @param {!Array} deps + * @param {!ContextNode} contextNode + * @param {function(...DEP)} func + * @param {!Array>} deps */ constructor(contextNode, func, deps) { - /** @package @const {!./node.ContextNode} */ + /** @package @const {!ContextNode} */ this.contextNode = contextNode; - /** @private @const {!Function} */ + /** @private @const {function(DEP)} */ this.func_ = func; - /** @private @const {!Array} */ + /** @private @const {!Array>} */ this.deps_ = deps; /** - * @private @const {!Array} + * @private @const {!Array} * * Start with a pre-allocated array filled with `undefined`. The filling * is important to ensure the correct `Array.every` execution. */ this.depValues_ = deps.length > 0 ? deps.map(EMPTY_FUNC) : EMPTY_ARRAY; - /** @private @const {!Array} */ + /** @private @const {!Array} */ this.depSubscribers_ = deps.length > 0 ? deps.map((unusedDep, index) => (value) => { @@ -207,9 +213,10 @@ function isDefined(v) { /** * Creates a subscriber. * - * @param {function(...?)} callback - * @param {!Array} deps + * @param {function(...DEP)} callback + * @param {!Array} deps * @return {?function()} + * @template DEP */ function callHandler(callback, deps) { switch (deps.length) { diff --git a/src/context/values.js b/src/context/values.js index 05cbb3b351c2..90879cd64053 100644 --- a/src/context/values.js +++ b/src/context/values.js @@ -20,12 +20,13 @@ import {pushIfNotExist, removeItem} from '../core/types/array'; import {rethrowAsync} from '../core/error'; import {throttleTail} from './scheduler'; +// typedef imports +import {ContextPropDef} from './prop.type'; + const EMPTY_ARRAY = []; const EMPTY_FUNC = () => {}; -/** - * @enum {number} - */ +/** @enum {number} */ const Pending = { NOT_PENDING: 0, PENDING: 1, @@ -37,35 +38,48 @@ const Pending = { * easily available as an array to pass them to the `recursive` and * `compute` callbacks without reallocation. * - * @typedef {{ - * values: !Array, - * setters: !Array, - * }} + * @interface + * @template T */ -let InputDef; +function InputDef() {} +/** @type {!Array} */ +InputDef.prototype.values; +/** @type {!Array} */ +InputDef.prototype.setters; /** * The structure for a property's computed values and subscribers. - * - * @typedef {{ - * prop: !ContextProp, - * subscribers: !Array, - * value: *, - * pending: !Pending, - * counter: number, - * depsValues: !Array, - * parentValue: *, - * parentContextNode: ?./node.ContextNode, - * ping: function(boolean), - * pingDep: !Array, - * pingParent: ?function(*), - * }} + * @interface + * @template T + * @template DEP */ -let UsedDef; +function UsedDef() {} +/** @type {!ContextPropDef} */ +UsedDef.prototype.prop; +/** @type {!Array} */ +UsedDef.prototype.subscribers; +/** @type {T} */ +UsedDef.prototype.value; +/** @type {!Pending} */ +UsedDef.prototype.pending; +/** @type {number} */ +UsedDef.prototype.counter; +/** @type {!Array} */ +UsedDef.prototype.depValues; +/** @type {!T} */ +UsedDef.prototype.parentValue; +/** @type {?./node.ContextNode} */ +UsedDef.prototype.parentContextNode; +/** @type {function(boolean)} */ +UsedDef.prototype.ping; +/** @type {!Array} */ +UsedDef.prototype.pingDep; +/** @type {?function(T)} */ +UsedDef.prototype.pingParent; /** * Propagates context property values in the context tree. The key APIs are - * `set()` and `subscribe()`. See `ContextProp` type for details on how + * `set()` and `subscribe()`. See `ContextPropDef` type for details on how * values are declared and propagated. */ export class Values { @@ -104,8 +118,8 @@ export class Values { * Once the input is set, the recalculation is rescheduled asynchronously. * All dependent properties are also recalculated. * - * @param {!ContextProp} prop - * @param {*} setter + * @param {!ContextPropDef} prop + * @param {function(T)} setter * @param {T} value * @template T */ @@ -141,7 +155,13 @@ export class Values { // deepscan can be avoided. this.ping(prop, false); if (isRecursive(prop)) { - deepScan(this.contextNode_, scan, prop, true, false); + deepScan( + this.contextNode_, + scan, + prop, + /*state=*/ true, + /*includeSelf=*/ false + ); } } } @@ -149,15 +169,16 @@ export class Values { /** * Unsets the input value for the specified property and setter. * See `set()` for more info. - * @param {!ContextProp} prop - * @param {*} setter + * @param {!ContextPropDef} prop + * @param {function(T)} setter + * @template T */ remove(prop, setter) { devAssert(setter); const {key} = prop; const inputsByKey = this.inputsByKey_; - const inputs = inputsByKey && inputsByKey.get(key); + const inputs = inputsByKey?.get(key); if (inputs) { const index = inputs.setters.indexOf(setter); if (index != -1) { @@ -174,12 +195,11 @@ export class Values { /** * Whether this node has inputs for the specified property. * - * @param {!ContextProp} prop + * @param {!ContextPropDef} prop * @return {boolean} */ has(prop) { - const inputsByKey = this.inputsByKey_; - return !!inputsByKey && inputsByKey.has(prop.key); + return !!this.inputsByKey_?.has(prop.key); } /** @@ -189,7 +209,7 @@ export class Values { * only called if a valid used value is available and only if this value * has changed since the last handler call. * - * @param {!ContextProp} prop + * @param {!ContextPropDef} prop * @param {function(T)} handler * @template T */ @@ -212,13 +232,12 @@ export class Values { * Unsubscribes a previously added handler. If there are no other subscribers * the property tracking is stopped and the used value is removed. * - * @param {!ContextProp} prop - * @param {function(?)} handler + * @param {!ContextPropDef} prop + * @param {function(T)} handler + * @template T */ unsubscribe(prop, handler) { - const {key} = prop; - const usedByKey = this.usedByKey_; - const used = usedByKey && usedByKey.get(key); + const used = this.usedByKey_?.get(prop.key); if (!used || !removeItem(used.subscribers, handler)) { // Not a subscriber. return; @@ -232,18 +251,13 @@ export class Values { * Schedules a recalculation of the specified property, but only if this * property is tracked by this node. * - * @param {!ContextProp} prop + * @param {!ContextPropDef} prop * @param {boolean} refreshParent Whether the parent node needs to be looked * up again. * @protected */ ping(prop, refreshParent) { - const {key} = prop; - const usedByKey = this.usedByKey_; - const used = usedByKey && usedByKey.get(key); - if (used) { - used.ping(refreshParent); - } + this.usedByKey_?.get(prop.key)?.ping(refreshParent); } /** @@ -258,7 +272,12 @@ export class Values { // a few specific props or even only specific nodes. E.g. when a single // intermediary parent is inserted between a parent and a child, the amount // of refreshes only depends on the inputs already set on this parent. - deepScan(this.contextNode_, scanAll, /* arg */ undefined, EMPTY_ARRAY); + deepScan( + this.contextNode_, + scanAll, + /*arg=*/ undefined, + /*state=*/ EMPTY_ARRAY + ); } } @@ -300,7 +319,7 @@ export class Values { * Scans are relatively common and this method exists (as opposed to be * inlined) only to avoid frequent function allocation. * - * @param {!ContextProp} prop + * @param {!ContextPropDef} prop * @return {boolean} * @protected Necessary for cross-binary access. */ @@ -334,8 +353,9 @@ export class Values { if (usedByKey) { usedByKey.forEach((used) => { const {prop} = used; + const {key} = prop; // Only ping unhandled props. - if ((newScheduled || scheduled).indexOf(prop.key) == -1) { + if ((newScheduled || scheduled).indexOf(key) == -1) { this.ping(prop, true); if (this.contextNode_.children && this.has(prop)) { @@ -344,7 +364,7 @@ export class Values { } // Stop the deepscan for this value. It will be propagated // by the responsible node. - newScheduled.push(prop.key); + newScheduled.push(key); } } }); @@ -363,9 +383,11 @@ export class Values { /** * Start the used value tracker if it hasn't started yet. * - * @param {!ContextProp} prop - * @return {!UsedDef} + * @param {!ContextPropDef} prop + * @return {!UsedDef} * @private + * @template T + * @template DEF */ startUsed_(prop) { const {key, deps} = prop; @@ -476,7 +498,7 @@ export class Values { used.counter++; if (used.counter > 5) { // A simple protection from infinte loops. - rethrowAsync(new Error('cyclical prop: ' + key)); + rethrowAsync(`cyclical prop: ${key}`); used.pending = Pending.NOT_PENDING; return; } @@ -502,7 +524,7 @@ export class Values { } catch (e) { // This is the narrowest catch to avoid unrelated values breaking each // other. The only exposure to the user-code are `recursive` and - // `compute` methods in the `ContextProp`. + // `compute` methods in the `ContextPropDef`. rethrowAsync(e); } @@ -515,9 +537,10 @@ export class Values { } /** - * @param {!UsedDef} used - * @param {*} value + * @param {!UsedDef} used + * @param {T} value * @private + * @template T */ maybeUpdated_(used, value) { const {prop, value: oldValue} = used; @@ -525,7 +548,7 @@ export class Values { const usedByKey = this.usedByKey_; if ( oldValue === value || - used !== (usedByKey && usedByKey.get(key)) || + used !== usedByKey?.get(key) || !this.isConnected_() ) { // Either the value didn't change, or no one needs this value anymore. @@ -536,18 +559,19 @@ export class Values { // Notify subscribers. const {subscribers} = used; - subscribers.forEach((handler) => { + for (const handler of subscribers) { handler(value); - }); + } } /** * The used value calculation algorithm. * - * @param {!UsedDef} used + * @param {!UsedDef} used * @param {boolean} refreshParent - * @return {*|undefined} The used value. + * @return {T|undefined} The used value. * @private + * @template T */ calc_(used, refreshParent) { devAssert(this.isConnected_()); @@ -555,9 +579,7 @@ export class Values { const {prop, depValues} = used; const {key, compute, defaultValue} = prop; - const inputsByKey = this.inputsByKey_; - const inputs = inputsByKey && inputsByKey.get(key); - const inputValues = inputs && inputs.values; + const inputValues = this.inputsByKey_?.get(key)?.values; // Calculate parent value. const recursive = calcRecursive(prop, inputValues); @@ -642,7 +664,7 @@ export class Values { * See `Values.scan()` method. * * @param {!./node.ContextNode} contextNode - * @param {!ContextProp} prop + * @param {!ContextPropDef} prop * @return {boolean} */ function scan(contextNode, prop) { @@ -653,7 +675,7 @@ function scan(contextNode, prop) { * See `Values.scanAll()` method. * * @param {!./node.ContextNode} contextNode - * @param {*} unusedArg + * @param {?} unusedArg * @param {!Array} state * @return {!Array} */ @@ -665,7 +687,7 @@ function scanAll(contextNode, unusedArg, state) { * See `Values.has()` method. * * @param {!./node.ContextNode} contextNode - * @param {!ContextProp} prop + * @param {!ContextPropDef} prop * @return {boolean} */ function hasInput(contextNode, prop) { @@ -675,7 +697,7 @@ function hasInput(contextNode, prop) { /** * Whether the property is recursive. * - * @param {!ContextProp} prop + * @param {!ContextPropDef} prop * @return {boolean} */ function isRecursive(prop) { @@ -687,9 +709,10 @@ function isRecursive(prop) { /** * Whether the parent value is required to calculate the used value. * - * @param {!ContextProp} prop - * @param {?Array} inputs + * @param {!ContextPropDef} prop + * @param {?Array} inputs * @return {boolean} + * @template T */ function calcRecursive(prop, inputs) { const {recursive, compute} = prop; @@ -708,11 +731,13 @@ function calcRecursive(prop, inputs) { /** * A substitute for `compute(...deps)`, but faster. * - * @param {function(!Node, !Array, ...*):*} compute See `ContextProp.compute()`. + * @param {function(!Node, !Array, ...DEP):T} compute See `ContextPropDef.compute()`. * @param {!Node} node - * @param {!Array} inputValues - * @param {!Array} deps - * @return {*} + * @param {!Array} inputValues + * @param {!Array} deps + * @return {T} + * @template T + * @template DEP */ function callCompute(compute, node, inputValues, deps) { switch (deps.length) { @@ -732,12 +757,14 @@ function callCompute(compute, node, inputValues, deps) { /** * A substitute for `compute(parentValue, ...deps)`, but faster. * - * @param {function(!Node, !Array, ...*):*} compute See `ContextProp.compute()`. + * @param {function(!Node, !Array, ...DEP):T} compute See `ContextPropDef.compute()`. * @param {!Node} node - * @param {!Array} inputValues - * @param {*} parentValue - * @param {!Array} deps - * @return {*} + * @param {!Array} inputValues + * @param {T} parentValue + * @param {!Array} deps + * @return {T} + * @template T + * @template DEP */ function callRecursiveCompute(compute, node, inputValues, parentValue, deps) { switch (deps.length) {