'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const NOOP = () => { }; const IDENTITY = (value) => value; function wrap(value) { return value === undefined ? [] : Array.isArray(value) ? value : [value]; } function unwrap(arr) { return arr.length === 0 ? undefined : arr.length === 1 ? arr[0] : arr; } /** * Ensures a value is an array. * * This function does the same thing as wrap() above except it handles nulls * and iterables, so it is appropriate for wrapping user-provided element * children. */ function arrayify(value) { return value == null ? [] : Array.isArray(value) ? value : typeof value === "string" || typeof value[Symbol.iterator] !== "function" ? [value] : // TODO: inference broke in TypeScript 3.9. [...value]; } function isIteratorLike(value) { return value != null && typeof value.next === "function"; } function isPromiseLike(value) { return value != null && typeof value.then === "function"; } /*** * SPECIAL TAGS * * Crank provides a couple tags which have special meaning for the renderer. ***/ /** * A special tag for grouping multiple children within the same parent. * * All non-string iterables which appear in the element tree are implicitly * wrapped in a fragment element. * * This tag is just the empty string, and you can use the empty string in * createElement calls or transpiler options directly to avoid having to * reference this export. */ const Fragment = ""; // TODO: We assert the following symbol tags as any because TypeScript support // for symbol tags in JSX doesn’t exist yet. // https://github.com/microsoft/TypeScript/issues/38367 /** * A special tag for rendering into a new root node via a root prop. * * This tag is useful for creating element trees with multiple roots, for * things like modals or tooltips. * * Renderer.prototype.render() will implicitly wrap top-level element trees in * a Portal element. */ const Portal = Symbol.for("crank.Portal"); /** * A special tag which preserves whatever was previously rendered in the * element’s position. * * Copy elements are useful for when you want to prevent a subtree from * rerendering as a performance optimization. Copy elements can also be keyed, * in which case the previously rendered keyed element will be copied. */ const Copy = Symbol.for("crank.Copy"); /** * A special tag for injecting raw nodes or strings via a value prop. * * Renderer.prototype.raw() is called with the value prop. */ const Raw = Symbol.for("crank.Raw"); const ElementSymbol = Symbol.for("crank.Element"); /** * Elements are the basic building blocks of Crank applications. They are * JavaScript objects which are interpreted by special classes called renderers * to produce and manage stateful nodes. * * @template {Tag} [TTag=Tag] - The type of the tag of the element. * * @example * // specific element types * let div: Element<"div">; * let portal: Element; * let myEl: Element; * * // general element types * let host: Element; * let component: Element; * * Typically, you use a helper function like createElement to create elements * rather than instatiating this class directly. */ class Element { constructor(tag, props) { this.tag = tag; this.props = props; } get key() { return this.props.key; } get ref() { return this.props.ref; } get copy() { return !!this.props.copy; } } // See Element interface Element.prototype.$$typeof = ElementSymbol; function isElement(value) { return value != null && value.$$typeof === ElementSymbol; } const DEPRECATED_PROP_PREFIXES = ["crank-", "c-", "$"]; const DEPRECATED_SPECIAL_PROP_BASES = ["key", "ref", "static"]; const SPECIAL_PROPS = new Set(["children", "key", "ref", "copy"]); for (const propPrefix of DEPRECATED_PROP_PREFIXES) { for (const propBase of DEPRECATED_SPECIAL_PROP_BASES) { SPECIAL_PROPS.add(propPrefix + propBase); } } /** * Creates an element with the specified tag, props and children. * * This function is usually used as a transpilation target for JSX transpilers, * but it can also be called directly. It additionally extracts special props so * they aren’t accessible to renderer methods or components, and assigns the * children prop according to any additional arguments passed to the function. */ function createElement(tag, props, ...children) { if (props == null) { props = {}; } for (let i = 0; i < DEPRECATED_PROP_PREFIXES.length; i++) { const propPrefix = DEPRECATED_PROP_PREFIXES[i]; for (let j = 0; j < DEPRECATED_SPECIAL_PROP_BASES.length; j++) { const propBase = DEPRECATED_SPECIAL_PROP_BASES[j]; const deprecatedPropName = propPrefix + propBase; const targetPropBase = propBase === "static" ? "copy" : propBase; if (deprecatedPropName in props) { console.warn(`The \`${deprecatedPropName}\` prop is deprecated. Use \`${targetPropBase}\` instead.`); props[targetPropBase] = props[deprecatedPropName]; } } } if (children.length > 1) { props.children = children; } else if (children.length === 1) { props.children = children[0]; } return new Element(tag, props); } /** Clones a given element, shallowly copying the props object. */ function cloneElement(el) { if (!isElement(el)) { throw new TypeError("Cannot clone non-element"); } return new Element(el.tag, { ...el.props }); } function narrow(value) { if (typeof value === "boolean" || value == null) { return undefined; } else if (typeof value === "string" || isElement(value)) { return value; } else if (typeof value[Symbol.iterator] === "function") { return createElement(Fragment, null, value); } return value.toString(); } /** * Takes an array of element values and normalizes the output as an array of * nodes and strings. * * @returns Normalized array of nodes and/or strings. * * Normalize will flatten only one level of nested arrays, because it is * designed to be called once at each level of the tree. It will also * concatenate adjacent strings and remove all undefined values. */ function normalize(values) { const result = []; let buffer; for (let i = 0; i < values.length; i++) { const value = values[i]; if (!value) ; else if (typeof value === "string") { buffer = (buffer || "") + value; } else if (!Array.isArray(value)) { if (buffer) { result.push(buffer); buffer = undefined; } result.push(value); } else { // We could use recursion here but it’s just easier to do it inline. for (let j = 0; j < value.length; j++) { const value1 = value[j]; if (!value1) ; else if (typeof value1 === "string") { buffer = (buffer || "") + value1; } else { if (buffer) { result.push(buffer); buffer = undefined; } result.push(value1); } } } } if (buffer) { result.push(buffer); } return result; } /** * @internal * The internal nodes which are cached and diffed against new elements when * rendering element trees. */ class Retainer { constructor(el) { this.el = el; this.ctx = undefined; this.children = undefined; this.value = undefined; this.cachedChildValues = undefined; this.fallbackValue = undefined; this.inflightValue = undefined; this.onNextValues = undefined; } } /** * Finds the value of the element according to its type. * * @returns The value of the element. */ function getValue(ret) { if (typeof ret.fallbackValue !== "undefined") { return typeof ret.fallbackValue === "object" ? getValue(ret.fallbackValue) : ret.fallbackValue; } else if (ret.el.tag === Portal) { return; } else if (typeof ret.el.tag !== "function" && ret.el.tag !== Fragment) { return ret.value; } return unwrap(getChildValues(ret)); } /** * Walks an element’s children to find its child values. * * @returns A normalized array of nodes and strings. */ function getChildValues(ret) { if (ret.cachedChildValues) { return wrap(ret.cachedChildValues); } const values = []; const children = wrap(ret.children); for (let i = 0; i < children.length; i++) { const child = children[i]; if (child) { values.push(typeof child === "string" ? child : getValue(child)); } } const values1 = normalize(values); const tag = ret.el.tag; if (typeof tag === "function" || (tag !== Fragment && tag !== Raw)) { ret.cachedChildValues = unwrap(values1); } return values1; } const defaultRendererImpl = { create() { throw new Error("Not implemented"); }, hydrate() { throw new Error("Not implemented"); }, scope: IDENTITY, read: IDENTITY, text: IDENTITY, raw: IDENTITY, patch: NOOP, arrange: NOOP, dispose: NOOP, flush: NOOP, }; const _RendererImpl = Symbol.for("crank.RendererImpl"); /** * An abstract class which is subclassed to render to different target * environments. Subclasses will typically call super() with a custom * RendererImpl. This class is responsible for kicking off the rendering * process and caching previous trees by root. * * @template TNode - The type of the node for a rendering environment. * @template TScope - Data which is passed down the tree. * @template TRoot - The type of the root for a rendering environment. * @template TResult - The type of exposed values. */ class Renderer { constructor(impl) { this.cache = new WeakMap(); this[_RendererImpl] = { ...defaultRendererImpl, ...impl, }; } /** * Renders an element tree into a specific root. * * @param children - An element tree. You can render null with a previously * used root to delete the previously rendered element tree from the cache. * @param root - The node to be rendered into. The renderer will cache * element trees per root. * @param bridge - An optional context that will be the ancestor context of all * elements in the tree. Useful for connecting different renderers so that * events/provisions properly propagate. The context for a given root must be * the same or an error will be thrown. * * @returns The result of rendering the children, or a possible promise of * the result if the element tree renders asynchronously. */ render(children, root, bridge) { let ret; const ctx = bridge && bridge[_ContextImpl]; if (typeof root === "object" && root !== null) { ret = this.cache.get(root); } let oldProps; if (ret === undefined) { ret = new Retainer(createElement(Portal, { children, root })); ret.value = root; ret.ctx = ctx; if (typeof root === "object" && root !== null && children != null) { this.cache.set(root, ret); } } else if (ret.ctx !== ctx) { throw new Error("Context mismatch"); } else { oldProps = ret.el.props; ret.el = createElement(Portal, { children, root }); if (typeof root === "object" && root !== null && children == null) { this.cache.delete(root); } } const impl = this[_RendererImpl]; const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children, undefined); // We return the child values of the portal because portal elements // themselves have no readable value. if (isPromiseLike(childValues)) { return childValues.then((childValues) => commitRootRender(impl, root, ctx, ret, childValues, oldProps)); } return commitRootRender(impl, root, ctx, ret, childValues, oldProps); } hydrate(children, root, bridge) { const impl = this[_RendererImpl]; const ctx = bridge && bridge[_ContextImpl]; let ret; ret = this.cache.get(root); if (ret !== undefined) { // If there is a retainer for the root, hydration is not necessary. return this.render(children, root, bridge); } let oldProps; ret = new Retainer(createElement(Portal, { children, root })); ret.value = root; if (typeof root === "object" && root !== null && children != null) { this.cache.set(root, ret); } const hydrationData = impl.hydrate(Portal, root, {}); const childValues = diffChildren(impl, root, ret, ctx, impl.scope(undefined, Portal, ret.el.props), ret, children, hydrationData); // We return the child values of the portal because portal elements // themselves have no readable value. if (isPromiseLike(childValues)) { return childValues.then((childValues) => commitRootRender(impl, root, ctx, ret, childValues, oldProps)); } return commitRootRender(impl, root, ctx, ret, childValues, oldProps); } } /*** PRIVATE RENDERER FUNCTIONS ***/ function commitRootRender(renderer, root, ctx, ret, childValues, oldProps) { // element is a host or portal element if (root != null) { renderer.arrange(Portal, root, ret.el.props, childValues, oldProps, wrap(ret.cachedChildValues)); flush(renderer, root); } ret.cachedChildValues = unwrap(childValues); if (root == null) { unmount(renderer, ret, ctx, ret); } return renderer.read(ret.cachedChildValues); } function diffChildren(renderer, root, host, ctx, scope, parent, children, hydrationData) { const oldRetained = wrap(parent.children); const newRetained = []; const newChildren = arrayify(children); const values = []; let graveyard; let childrenByKey; let seenKeys; let isAsync = false; // When hydrating, sibling element trees must be rendered in order, because // we do not know how many DOM nodes an element will render. let hydrationBlock; let oi = 0; let oldLength = oldRetained.length; for (let ni = 0, newLength = newChildren.length; ni < newLength; ni++) { // length checks to prevent index out of bounds deoptimizations. let ret = oi >= oldLength ? undefined : oldRetained[oi]; let child = narrow(newChildren[ni]); { // aligning new children with old retainers let oldKey = typeof ret === "object" ? ret.el.key : undefined; let newKey = typeof child === "object" ? child.key : undefined; if (newKey !== undefined && seenKeys && seenKeys.has(newKey)) { console.error("Duplicate key", newKey); newKey = undefined; } if (oldKey === newKey) { if (childrenByKey !== undefined && newKey !== undefined) { childrenByKey.delete(newKey); } oi++; } else { childrenByKey = childrenByKey || createChildrenByKey(oldRetained, oi); if (newKey === undefined) { while (ret !== undefined && oldKey !== undefined) { oi++; ret = oldRetained[oi]; oldKey = typeof ret === "object" ? ret.el.key : undefined; } oi++; } else { ret = childrenByKey.get(newKey); if (ret !== undefined) { childrenByKey.delete(newKey); } (seenKeys = seenKeys || new Set()).add(newKey); } } } // Updating let value; if (typeof child === "object") { if (child.tag === Copy || (typeof ret === "object" && ret.el === child)) { value = getInflightValue(ret); } else { let oldProps; let copy = false; if (typeof ret === "object" && ret.el.tag === child.tag) { oldProps = ret.el.props; ret.el = child; if (child.copy) { value = getInflightValue(ret); copy = true; } } else { if (typeof ret === "object") { (graveyard = graveyard || []).push(ret); } const fallback = ret; ret = new Retainer(child); ret.fallbackValue = fallback; } if (copy) ; else if (child.tag === Raw) { value = hydrationBlock ? hydrationBlock.then(() => updateRaw(renderer, ret, scope, oldProps, hydrationData)) : updateRaw(renderer, ret, scope, oldProps, hydrationData); } else if (child.tag === Fragment) { value = hydrationBlock ? hydrationBlock.then(() => updateFragment(renderer, root, host, ctx, scope, ret, hydrationData)) : updateFragment(renderer, root, host, ctx, scope, ret, hydrationData); } else if (typeof child.tag === "function") { value = hydrationBlock ? hydrationBlock.then(() => updateComponent(renderer, root, host, ctx, scope, ret, oldProps, hydrationData)) : updateComponent(renderer, root, host, ctx, scope, ret, oldProps, hydrationData); } else { value = hydrationBlock ? hydrationBlock.then(() => updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData)) : updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData); } } if (isPromiseLike(value)) { isAsync = true; if (hydrationData !== undefined) { hydrationBlock = value; } } } else { // child is a string or undefined if (typeof ret === "object") { (graveyard = graveyard || []).push(ret); } if (typeof child === "string") { value = ret = renderer.text(child, scope, hydrationData); } else { ret = undefined; } } values[ni] = value; newRetained[ni] = ret; } // cleanup remaining retainers for (; oi < oldLength; oi++) { const ret = oldRetained[oi]; if (typeof ret === "object" && (typeof ret.el.key === "undefined" || !seenKeys || !seenKeys.has(ret.el.key))) { (graveyard = graveyard || []).push(ret); } } if (childrenByKey !== undefined && childrenByKey.size > 0) { (graveyard = graveyard || []).push(...childrenByKey.values()); } parent.children = unwrap(newRetained); if (isAsync) { let childValues1 = Promise.all(values).finally(() => { if (graveyard) { for (let i = 0; i < graveyard.length; i++) { unmount(renderer, host, ctx, graveyard[i]); } } }); let onChildValues; childValues1 = Promise.race([ childValues1, new Promise((resolve) => (onChildValues = resolve)), ]); if (parent.onNextValues) { parent.onNextValues(childValues1); } parent.onNextValues = onChildValues; return childValues1.then((childValues) => { parent.inflightValue = parent.fallbackValue = undefined; return normalize(childValues); }); } else { if (graveyard) { for (let i = 0; i < graveyard.length; i++) { unmount(renderer, host, ctx, graveyard[i]); } } if (parent.onNextValues) { parent.onNextValues(values); parent.onNextValues = undefined; } parent.inflightValue = parent.fallbackValue = undefined; // We can assert there are no promises in the array because isAsync is false return normalize(values); } } function createChildrenByKey(children, offset) { const childrenByKey = new Map(); for (let i = offset; i < children.length; i++) { const child = children[i]; if (typeof child === "object" && typeof child.el.key !== "undefined") { childrenByKey.set(child.el.key, child); } } return childrenByKey; } function getInflightValue(child) { if (typeof child !== "object") { return child; } const ctx = typeof child.el.tag === "function" ? child.ctx : undefined; if (ctx && ctx.f & IsUpdating && ctx.inflightValue) { return ctx.inflightValue; } else if (child.inflightValue) { return child.inflightValue; } return getValue(child); } function updateRaw(renderer, ret, scope, oldProps, hydrationData) { const props = ret.el.props; if (!oldProps || oldProps.value !== props.value) { ret.value = renderer.raw(props.value, scope, hydrationData); if (typeof ret.el.ref === "function") { ret.el.ref(ret.value); } } return ret.value; } function updateFragment(renderer, root, host, ctx, scope, ret, hydrationData) { const childValues = diffChildren(renderer, root, host, ctx, scope, ret, ret.el.props.children, hydrationData); if (isPromiseLike(childValues)) { ret.inflightValue = childValues.then((childValues) => unwrap(childValues)); return ret.inflightValue; } return unwrap(childValues); } function updateHost(renderer, root, ctx, scope, ret, oldProps, hydrationData) { const el = ret.el; const tag = el.tag; let hydrationValue; if (el.tag === Portal) { root = ret.value = el.props.root; } else { if (hydrationData !== undefined) { const value = hydrationData.children.shift(); hydrationValue = value; } } scope = renderer.scope(scope, tag, el.props); let childHydrationData; if (hydrationValue != null && typeof hydrationValue !== "string") { childHydrationData = renderer.hydrate(tag, hydrationValue, el.props); if (childHydrationData === undefined) { hydrationValue = undefined; } } const childValues = diffChildren(renderer, root, ret, ctx, scope, ret, ret.el.props.children, childHydrationData); if (isPromiseLike(childValues)) { ret.inflightValue = childValues.then((childValues) => commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue)); return ret.inflightValue; } return commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue); } function commitHost(renderer, scope, ret, childValues, oldProps, hydrationValue) { const tag = ret.el.tag; let value = ret.value; if (hydrationValue != null) { value = ret.value = hydrationValue; if (typeof ret.el.ref === "function") { ret.el.ref(value); } } let props = ret.el.props; let copied; if (tag !== Portal) { if (value == null) { // This assumes that renderer.create does not return nullish values. value = ret.value = renderer.create(tag, props, scope); if (typeof ret.el.ref === "function") { ret.el.ref(value); } } for (const propName in { ...oldProps, ...props }) { const propValue = props[propName]; if (propValue === Copy) { // TODO: The Copy tag doubles as a way to skip the patching of a prop. // Not sure about this feature. Should probably be removed. (copied = copied || new Set()).add(propName); } else if (!SPECIAL_PROPS.has(propName)) { renderer.patch(tag, value, propName, propValue, oldProps && oldProps[propName], scope); } } } if (copied) { props = { ...ret.el.props }; for (const name of copied) { props[name] = oldProps && oldProps[name]; } ret.el = new Element(tag, props); } renderer.arrange(tag, value, props, childValues, oldProps, wrap(ret.cachedChildValues)); ret.cachedChildValues = unwrap(childValues); if (tag === Portal) { flush(renderer, ret.value); return; } return value; } function flush(renderer, root, initiator) { renderer.flush(root); if (typeof root !== "object" || root === null) { return; } const flushMap = flushMaps.get(root); if (flushMap) { if (initiator) { const flushMap1 = new Map(); for (let [ctx, callbacks] of flushMap) { if (!ctxContains(initiator, ctx)) { flushMap.delete(ctx); flushMap1.set(ctx, callbacks); } } if (flushMap1.size) { flushMaps.set(root, flushMap1); } else { flushMaps.delete(root); } } else { flushMaps.delete(root); } for (const [ctx, callbacks] of flushMap) { const value = renderer.read(getValue(ctx.ret)); for (const callback of callbacks) { callback(value); } } } } function unmount(renderer, host, ctx, ret) { if (typeof ret.el.tag === "function") { ctx = ret.ctx; unmountComponent(ctx); } else if (ret.el.tag === Portal) { host = ret; renderer.arrange(Portal, host.value, host.el.props, [], host.el.props, wrap(host.cachedChildValues)); flush(renderer, host.value); } else if (ret.el.tag !== Fragment) { if (isEventTarget(ret.value)) { const records = getListenerRecords(ctx, host); for (let i = 0; i < records.length; i++) { const record = records[i]; ret.value.removeEventListener(record.type, record.callback, record.options); } } renderer.dispose(ret.el.tag, ret.value, ret.el.props); host = ret; } const children = wrap(ret.children); for (let i = 0; i < children.length; i++) { const child = children[i]; if (typeof child === "object") { unmount(renderer, host, ctx, child); } } } /*** CONTEXT FLAGS ***/ /** * A flag which is true when the component is initialized or updated by an * ancestor component or the root render call. * * Used to determine things like whether the nearest host ancestor needs to be * rearranged. */ const IsUpdating = 1 << 0; /** * A flag which is true when the component is synchronously executing. * * Used to guard against components triggering stack overflow or generator error. */ const IsSyncExecuting = 1 << 1; /** * A flag which is true when the component is in a for...of loop. */ const IsInForOfLoop = 1 << 2; /** * A flag which is true when the component is in a for await...of loop. */ const IsInForAwaitOfLoop = 1 << 3; /** * A flag which is true when the component starts the render loop but has not * yielded yet. * * Used to make sure that components yield at least once per loop. */ const NeedsToYield = 1 << 4; /** * A flag used by async generator components in conjunction with the * onAvailable callback to mark whether new props can be pulled via the context * async iterator. See the Symbol.asyncIterator method and the * resumeCtxIterator function. */ const PropsAvailable = 1 << 5; /** * A flag which is set when a component errors. * * This is mainly used to prevent some false positives in "component yields or * returns undefined" warnings. The reason we’re using this versus IsUnmounted * is a very troubling test (cascades sync generator parent and sync generator * child) where synchronous code causes a stack overflow error in a * non-deterministic way. Deeply disturbing stuff. */ const IsErrored = 1 << 6; /** * A flag which is set when the component is unmounted. Unmounted components * are no longer in the element tree and cannot refresh or rerender. */ const IsUnmounted = 1 << 7; /** * A flag which indicates that the component is a sync generator component. */ const IsSyncGen = 1 << 8; /** * A flag which indicates that the component is an async generator component. */ const IsAsyncGen = 1 << 9; /** * A flag which is set while schedule callbacks are called. */ const IsScheduling = 1 << 10; /** * A flag which is set when a schedule callback calls refresh. */ const IsSchedulingRefresh = 1 << 11; const provisionMaps = new WeakMap(); const scheduleMap = new WeakMap(); const cleanupMap = new WeakMap(); // keys are roots const flushMaps = new WeakMap(); /** * @internal * The internal class which holds context data. */ class ContextImpl { constructor(renderer, root, host, parent, scope, ret) { this.f = 0; this.owner = new Context(this); this.renderer = renderer; this.root = root; this.host = host; this.parent = parent; this.scope = scope; this.ret = ret; this.iterator = undefined; this.inflightBlock = undefined; this.inflightValue = undefined; this.enqueuedBlock = undefined; this.enqueuedValue = undefined; this.onProps = undefined; this.onPropsRequested = undefined; } } const _ContextImpl = Symbol.for("crank.ContextImpl"); /** * A class which is instantiated and passed to every component as its this * value. Contexts form a tree just like elements and all components in the * element tree are connected via contexts. Components can use this tree to * communicate data upwards via events and downwards via provisions. * * @template [T=*] - The expected shape of the props passed to the component, * or a component function. Used to strongly type the Context iterator methods. * @template [TResult=*] - The readable element value type. It is used in * places such as the return value of refresh and the argument passed to * schedule and cleanup callbacks. */ class Context { // TODO: If we could make the constructor function take a nicer value, it // would be useful for testing purposes. constructor(impl) { this[_ContextImpl] = impl; } /** * The current props of the associated element. */ get props() { return this[_ContextImpl].ret.el.props; } /** * The current value of the associated element. * * @deprecated */ get value() { return this[_ContextImpl].renderer.read(getValue(this[_ContextImpl].ret)); } *[Symbol.iterator]() { const ctx = this[_ContextImpl]; try { ctx.f |= IsInForOfLoop; while (!(ctx.f & IsUnmounted)) { if (ctx.f & NeedsToYield) { throw new Error("Context iterated twice without a yield"); } else { ctx.f |= NeedsToYield; } yield ctx.ret.el.props; } } finally { ctx.f &= ~IsInForOfLoop; } } async *[Symbol.asyncIterator]() { const ctx = this[_ContextImpl]; if (ctx.f & IsSyncGen) { throw new Error("Use for...of in sync generator components"); } try { ctx.f |= IsInForAwaitOfLoop; while (!(ctx.f & IsUnmounted)) { if (ctx.f & NeedsToYield) { throw new Error("Context iterated twice without a yield"); } else { ctx.f |= NeedsToYield; } if (ctx.f & PropsAvailable) { ctx.f &= ~PropsAvailable; yield ctx.ret.el.props; } else { const props = await new Promise((resolve) => (ctx.onProps = resolve)); if (ctx.f & IsUnmounted) { break; } yield props; } if (ctx.onPropsRequested) { ctx.onPropsRequested(); ctx.onPropsRequested = undefined; } } } finally { ctx.f &= ~IsInForAwaitOfLoop; if (ctx.onPropsRequested) { ctx.onPropsRequested(); ctx.onPropsRequested = undefined; } } } /** * Re-executes a component. * * @returns The rendered value of the component or a promise thereof if the * component or its children execute asynchronously. * * The refresh method works a little differently for async generator * components, in that it will resume the Context’s props async iterator * rather than resuming execution. This is because async generator components * are perpetually resumed independent of updates, and rely on the props * async iterator to suspend. */ refresh() { const ctx = this[_ContextImpl]; if (ctx.f & IsUnmounted) { console.error("Component is unmounted"); return ctx.renderer.read(undefined); } else if (ctx.f & IsSyncExecuting) { console.error("Component is already executing"); return ctx.renderer.read(getValue(ctx.ret)); } const value = enqueueComponentRun(ctx); if (isPromiseLike(value)) { return value.then((value) => ctx.renderer.read(value)); } return ctx.renderer.read(value); } /** * Registers a callback which fires when the component commits. Will only * fire once per callback and update. */ schedule(callback) { const ctx = this[_ContextImpl]; let callbacks = scheduleMap.get(ctx); if (!callbacks) { callbacks = new Set(); scheduleMap.set(ctx, callbacks); } callbacks.add(callback); } /** * Registers a callback which fires when the component’s children are * rendered into the root. Will only fire once per callback and render. */ flush(callback) { const ctx = this[_ContextImpl]; if (typeof ctx.root !== "object" || ctx.root === null) { return; } let flushMap = flushMaps.get(ctx.root); if (!flushMap) { flushMap = new Map(); flushMaps.set(ctx.root, flushMap); } let callbacks = flushMap.get(ctx); if (!callbacks) { callbacks = new Set(); flushMap.set(ctx, callbacks); } callbacks.add(callback); } /** * Registers a callback which fires when the component unmounts. Will only * fire once per callback. */ cleanup(callback) { const ctx = this[_ContextImpl]; if (ctx.f & IsUnmounted) { const value = ctx.renderer.read(getValue(ctx.ret)); callback(value); return; } let callbacks = cleanupMap.get(ctx); if (!callbacks) { callbacks = new Set(); cleanupMap.set(ctx, callbacks); } callbacks.add(callback); } consume(key) { for (let ctx = this[_ContextImpl].parent; ctx !== undefined; ctx = ctx.parent) { const provisions = provisionMaps.get(ctx); if (provisions && provisions.has(key)) { return provisions.get(key); } } } provide(key, value) { const ctx = this[_ContextImpl]; let provisions = provisionMaps.get(ctx); if (!provisions) { provisions = new Map(); provisionMaps.set(ctx, provisions); } provisions.set(key, value); } addEventListener(type, listener, options) { const ctx = this[_ContextImpl]; let listeners; if (!isListenerOrListenerObject(listener)) { return; } else { const listeners1 = listenersMap.get(ctx); if (listeners1) { listeners = listeners1; } else { listeners = []; listenersMap.set(ctx, listeners); } } options = normalizeListenerOptions(options); let callback; if (typeof listener === "object") { callback = () => listener.handleEvent.apply(listener, arguments); } else { callback = listener; } const record = { type, listener, callback, options }; if (options.once) { record.callback = function () { const i = listeners.indexOf(record); if (i !== -1) { listeners.splice(i, 1); } return callback.apply(this, arguments); }; } if (listeners.some((record1) => record.type === record1.type && record.listener === record1.listener && !record.options.capture === !record1.options.capture)) { return; } listeners.push(record); // TODO: is it possible to separate out the EventTarget delegation logic for (const value of getChildValues(ctx.ret)) { if (isEventTarget(value)) { value.addEventListener(record.type, record.callback, record.options); } } } removeEventListener(type, listener, options) { const ctx = this[_ContextImpl]; const listeners = listenersMap.get(ctx); if (listeners == null || !isListenerOrListenerObject(listener)) { return; } const options1 = normalizeListenerOptions(options); const i = listeners.findIndex((record) => record.type === type && record.listener === listener && !record.options.capture === !options1.capture); if (i === -1) { return; } const record = listeners[i]; listeners.splice(i, 1); // TODO: is it possible to separate out the EventTarget delegation logic for (const value of getChildValues(ctx.ret)) { if (isEventTarget(value)) { value.removeEventListener(record.type, record.callback, record.options); } } } dispatchEvent(ev) { const ctx = this[_ContextImpl]; const path = []; for (let parent = ctx.parent; parent !== undefined; parent = parent.parent) { path.push(parent); } // We patch the stopImmediatePropagation method because ev.cancelBubble // only informs us if stopPropagation was called and there are no // properties which inform us if stopImmediatePropagation was called. let immediateCancelBubble = false; const stopImmediatePropagation = ev.stopImmediatePropagation; setEventProperty(ev, "stopImmediatePropagation", () => { immediateCancelBubble = true; return stopImmediatePropagation.call(ev); }); setEventProperty(ev, "target", ctx.owner); // The only possible errors in this block are errors thrown by callbacks, // and dispatchEvent will only log these errors rather than throwing // them. Therefore, we place all code in a try block, log errors in the // catch block, and use an unsafe return statement in the finally block. // // Each early return within the try block returns true because while the // return value is overridden in the finally block, TypeScript // (justifiably) does not recognize the unsafe return statement. try { setEventProperty(ev, "eventPhase", CAPTURING_PHASE); for (let i = path.length - 1; i >= 0; i--) { const target = path[i]; const listeners = listenersMap.get(target); if (listeners) { setEventProperty(ev, "currentTarget", target.owner); for (const record of listeners) { if (record.type === ev.type && record.options.capture) { try { record.callback.call(target.owner, ev); } catch (err) { console.error(err); } if (immediateCancelBubble) { return true; } } } } if (ev.cancelBubble) { return true; } } { setEventProperty(ev, "eventPhase", AT_TARGET); setEventProperty(ev, "currentTarget", ctx.owner); // dispatchEvent calls the prop callback if it exists let propCallback = ctx.ret.el.props["on" + ev.type]; if (typeof propCallback === "function") { propCallback(ev); if (immediateCancelBubble || ev.cancelBubble) { return true; } } else { // Checks for camel-cased event props for (const propName in ctx.ret.el.props) { if (propName.toLowerCase() === "on" + ev.type.toLowerCase()) { propCallback = ctx.ret.el.props[propName]; if (typeof propCallback === "function") { propCallback(ev); if (immediateCancelBubble || ev.cancelBubble) { return true; } } } } } const listeners = listenersMap.get(ctx); if (listeners) { for (const record of listeners) { if (record.type === ev.type) { try { record.callback.call(ctx.owner, ev); } catch (err) { console.error(err); } if (immediateCancelBubble) { return true; } } } if (ev.cancelBubble) { return true; } } } if (ev.bubbles) { setEventProperty(ev, "eventPhase", BUBBLING_PHASE); for (let i = 0; i < path.length; i++) { const target = path[i]; const listeners = listenersMap.get(target); if (listeners) { setEventProperty(ev, "currentTarget", target.owner); for (const record of listeners) { if (record.type === ev.type && !record.options.capture) { try { record.callback.call(target.owner, ev); } catch (err) { console.error(err); } if (immediateCancelBubble) { return true; } } } } if (ev.cancelBubble) { return true; } } } } finally { setEventProperty(ev, "eventPhase", NONE); setEventProperty(ev, "currentTarget", null); // eslint-disable-next-line no-unsafe-finally return !ev.defaultPrevented; } } } /*** PRIVATE CONTEXT FUNCTIONS ***/ function ctxContains(parent, child) { for (let current = child; current !== undefined; current = current.parent) { if (current === parent) { return true; } } return false; } function updateComponent(renderer, root, host, parent, scope, ret, oldProps, hydrationData) { let ctx; if (oldProps) { ctx = ret.ctx; if (ctx.f & IsSyncExecuting) { console.error("Component is already executing"); return ret.cachedChildValues; } } else { ctx = ret.ctx = new ContextImpl(renderer, root, host, parent, scope, ret); } ctx.f |= IsUpdating; return enqueueComponentRun(ctx, hydrationData); } function updateComponentChildren(ctx, children, hydrationData) { if (ctx.f & IsUnmounted) { return; } else if (ctx.f & IsErrored) { // This branch is necessary for some race conditions where this function is // called after iterator.throw() in async generator components. return; } else if (children === undefined) { console.error("A component has returned or yielded undefined. If this was intentional, return or yield null instead."); } let childValues; try { // TODO: WAT // We set the isExecuting flag in case a child component dispatches an event // which bubbles to this component and causes a synchronous refresh(). ctx.f |= IsSyncExecuting; childValues = diffChildren(ctx.renderer, ctx.root, ctx.host, ctx, ctx.scope, ctx.ret, narrow(children), hydrationData); } finally { ctx.f &= ~IsSyncExecuting; } if (isPromiseLike(childValues)) { ctx.ret.inflightValue = childValues.then((childValues) => commitComponent(ctx, childValues)); return ctx.ret.inflightValue; } return commitComponent(ctx, childValues); } function commitComponent(ctx, values) { if (ctx.f & IsUnmounted) { return; } const listeners = listenersMap.get(ctx); if (listeners && listeners.length) { for (let i = 0; i < values.length; i++) { const value = values[i]; if (isEventTarget(value)) { for (let j = 0; j < listeners.length; j++) { const record = listeners[j]; value.addEventListener(record.type, record.callback, record.options); } } } } const oldValues = wrap(ctx.ret.cachedChildValues); let value = (ctx.ret.cachedChildValues = unwrap(values)); if (ctx.f & IsScheduling) { ctx.f |= IsSchedulingRefresh; } else if (!(ctx.f & IsUpdating)) { // If we’re not updating the component, which happens when components are // refreshed, or when async generator components iterate, we have to do a // little bit housekeeping when a component’s child values have changed. if (!arrayEqual(oldValues, values)) { const records = getListenerRecords(ctx.parent, ctx.host); if (records.length) { for (let i = 0; i < values.length; i++) { const value = values[i]; if (isEventTarget(value)) { for (let j = 0; j < records.length; j++) { const record = records[j]; value.addEventListener(record.type, record.callback, record.options); } } } } // rearranging the nearest ancestor host element const host = ctx.host; const oldHostValues = wrap(host.cachedChildValues); invalidate(ctx, host); const hostValues = getChildValues(host); ctx.renderer.arrange(host.el.tag, host.value, host.el.props, hostValues, // props and oldProps are the same because the host isn’t updated. host.el.props, oldHostValues); } flush(ctx.renderer, ctx.root, ctx); } const callbacks = scheduleMap.get(ctx); if (callbacks) { scheduleMap.delete(ctx); ctx.f |= IsScheduling; const value1 = ctx.renderer.read(value); for (const callback of callbacks) { callback(value1); } ctx.f &= ~IsScheduling; // Handles an edge case where refresh() is called during a schedule(). if (ctx.f & IsSchedulingRefresh) { ctx.f &= ~IsSchedulingRefresh; value = getValue(ctx.ret); } } ctx.f &= ~IsUpdating; return value; } function invalidate(ctx, host) { for (let parent = ctx.parent; parent !== undefined && parent.host === host; parent = parent.parent) { parent.ret.cachedChildValues = undefined; } host.cachedChildValues = undefined; } function arrayEqual(arr1, arr2) { if (arr1.length !== arr2.length) { return false; } for (let i = 0; i < arr1.length; i++) { const value1 = arr1[i]; const value2 = arr2[i]; if (value1 !== value2) { return false; } } return true; } /** Enqueues and executes the component associated with the context. */ function enqueueComponentRun(ctx, hydrationData) { if (ctx.f & IsAsyncGen && !(ctx.f & IsInForOfLoop)) { if (hydrationData !== undefined) { throw new Error("Hydration error"); } // This branch will run for non-initial renders of async generator // components when they are not in for...of loops. When in a for...of loop, // async generator components will behave normally. // // Async gen componennts can be in one of three states: // // 1. propsAvailable flag is true: "available" // // The component is suspended somewhere in the loop. When the component // reaches the bottom of the loop, it will run again with the next props. // // 2. onAvailable callback is defined: "suspended" // // The component has suspended at the bottom of the loop and is waiting // for new props. // // 3. neither 1 or 2: "Running" // // The component is suspended somewhere in the loop. When the component // reaches the bottom of the loop, it will suspend. // // Components will never be both available and suspended at // the same time. // // If the component is at the loop bottom, this means that the next value // produced by the component will have the most up to date props, so we can // simply return the current inflight value. Otherwise, we have to wait for // the bottom of the loop to be reached before returning the inflight // value. const isAtLoopbottom = ctx.f & IsInForAwaitOfLoop && !ctx.onProps; resumePropsAsyncIterator(ctx); if (isAtLoopbottom) { if (ctx.inflightBlock == null) { ctx.inflightBlock = new Promise((resolve) => (ctx.onPropsRequested = resolve)); } return ctx.inflightBlock.then(() => { ctx.inflightBlock = undefined; return ctx.inflightValue; }); } return ctx.inflightValue; } else if (!ctx.inflightBlock) { try { const [block, value] = runComponent(ctx, hydrationData); if (block) { ctx.inflightBlock = block // TODO: there is some fuckery going on here related to async // generator components resuming when they’re meant to be returned. .then((v) => v) .finally(() => advanceComponent(ctx)); // stepComponent will only return a block if the value is asynchronous ctx.inflightValue = value; } return value; } catch (err) { if (!(ctx.f & IsUpdating)) { if (!ctx.parent) { throw err; } return propagateError(ctx.parent, err); } throw err; } } else if (!ctx.enqueuedBlock) { if (hydrationData !== undefined) { throw new Error("Hydration error"); } // We need to assign enqueuedBlock and enqueuedValue synchronously, hence // the Promise constructor call here. let resolveEnqueuedBlock; ctx.enqueuedBlock = new Promise((resolve) => (resolveEnqueuedBlock = resolve)); ctx.enqueuedValue = ctx.inflightBlock.then(() => { try { const [block, value] = runComponent(ctx); if (block) { resolveEnqueuedBlock(block.finally(() => advanceComponent(ctx))); } return value; } catch (err) { if (!(ctx.f & IsUpdating)) { if (!ctx.parent) { throw err; } return propagateError(ctx.parent, err); } throw err; } }); } return ctx.enqueuedValue; } /** Called when the inflight block promise settles. */ function advanceComponent(ctx) { if (ctx.f & IsAsyncGen && !(ctx.f & IsInForOfLoop)) { return; } ctx.inflightBlock = ctx.enqueuedBlock; ctx.inflightValue = ctx.enqueuedValue; ctx.enqueuedBlock = undefined; ctx.enqueuedValue = undefined; } /** * This function is responsible for executing the component and handling all * the different component types. We cannot identify whether a component is a * generator or async without calling it and inspecting the return value. * * @returns {[block, value]} A tuple where * block - A possible promise which represents the duration during which the * component is blocked from updating. * value - A possible promise resolving to the rendered value of children. * * Each component type will block according to the type of the component. * - Sync function components never block and will transparently pass updates * to children. * - Async function components and async generator components block while * executing itself, but will not block for async children. * - Sync generator components block while any children are executing, because * they are expected to only resume when they’ve actually rendered. */ function runComponent(ctx, hydrationData) { const ret = ctx.ret; const initial = !ctx.iterator; if (initial) { resumePropsAsyncIterator(ctx); ctx.f |= IsSyncExecuting; clearEventListeners(ctx); let result; try { result = ret.el.tag.call(ctx.owner, ret.el.props, ctx.owner); } catch (err) { ctx.f |= IsErrored; throw err; } finally { ctx.f &= ~IsSyncExecuting; } if (isIteratorLike(result)) { ctx.iterator = result; } else if (isPromiseLike(result)) { // async function component const result1 = result instanceof Promise ? result : Promise.resolve(result); const value = result1.then((result) => updateComponentChildren(ctx, result, hydrationData), (err) => { ctx.f |= IsErrored; throw err; }); return [result1.catch(NOOP), value]; } else { // sync function component return [ undefined, updateComponentChildren(ctx, result, hydrationData), ]; } } else if (hydrationData !== undefined) { // hydration data should only be passed on the initial render throw new Error("Hydration error"); } let iteration; if (initial) { try { ctx.f |= IsSyncExecuting; iteration = ctx.iterator.next(); } catch (err) { ctx.f |= IsErrored; throw err; } finally { ctx.f &= ~IsSyncExecuting; } if (isPromiseLike(iteration)) { ctx.f |= IsAsyncGen; } else { ctx.f |= IsSyncGen; } } if (ctx.f & IsSyncGen) { // sync generator component if (!initial) { try { ctx.f |= IsSyncExecuting; iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret))); } catch (err) { ctx.f |= IsErrored; throw err; } finally { ctx.f &= ~IsSyncExecuting; } } if (isPromiseLike(iteration)) { throw new Error("Mixed generator component"); } if (ctx.f & IsInForOfLoop && !(ctx.f & NeedsToYield) && !(ctx.f & IsUnmounted)) { console.error("Component yielded more than once in for...of loop"); } ctx.f &= ~NeedsToYield; if (iteration.done) { ctx.f &= ~IsSyncGen; ctx.iterator = undefined; } let value; try { value = updateComponentChildren(ctx, // Children can be void so we eliminate that here iteration.value, hydrationData); if (isPromiseLike(value)) { value = value.catch((err) => handleChildError(ctx, err)); } } catch (err) { value = handleChildError(ctx, err); } const block = isPromiseLike(value) ? value.catch(NOOP) : undefined; return [block, value]; } else { if (ctx.f & IsInForOfLoop) { // Async generator component using for...of loops behave similar to sync // generator components. This allows for easier refactoring of sync to // async generator components. if (!initial) { try { ctx.f |= IsSyncExecuting; iteration = ctx.iterator.next(ctx.renderer.read(getValue(ret))); } catch (err) { ctx.f |= IsErrored; throw err; } finally { ctx.f &= ~IsSyncExecuting; } } if (!isPromiseLike(iteration)) { throw new Error("Mixed generator component"); } const block = iteration.catch(NOOP); const value = iteration.then((iteration) => { let value; if (!(ctx.f & IsInForOfLoop)) { runAsyncGenComponent(ctx, Promise.resolve(iteration), hydrationData); } else { if (!(ctx.f & NeedsToYield) && !(ctx.f & IsUnmounted)) { console.error("Component yielded more than once in for...of loop"); } } ctx.f &= ~NeedsToYield; try { value = updateComponentChildren(ctx, // Children can be void so we eliminate that here iteration.value, hydrationData); if (isPromiseLike(value)) { value = value.catch((err) => handleChildError(ctx, err)); } } catch (err) { value = handleChildError(ctx, err); } return value; }, (err) => { ctx.f |= IsErrored; throw err; }); return [block, value]; } else { runAsyncGenComponent(ctx, iteration, hydrationData, initial); return [ctx.inflightBlock, ctx.inflightValue]; } } } async function runAsyncGenComponent(ctx, iterationP, hydrationData, initial = false) { let done = false; try { while (!done) { if (ctx.f & IsInForOfLoop) { break; } // inflightValue must be set synchronously. let onValue; ctx.inflightValue = new Promise((resolve) => (onValue = resolve)); if (ctx.f & IsUpdating) { // We should not swallow unhandled promise rejections if the component is // updating independently. // TODO: Does this handle this.refresh() calls? ctx.inflightValue.catch(NOOP); } let iteration; try { iteration = await iterationP; } catch (err) { done = true; ctx.f |= IsErrored; onValue(Promise.reject(err)); break; } if (!(ctx.f & IsInForAwaitOfLoop)) { ctx.f &= ~PropsAvailable; } done = !!iteration.done; let value; try { if (!(ctx.f & NeedsToYield) && ctx.f & PropsAvailable && ctx.f & IsInForAwaitOfLoop && !initial && !done) { // We skip stale iterations in for await...of loops. value = ctx.ret.inflightValue || getValue(ctx.ret); } else { value = updateComponentChildren(ctx, iteration.value, hydrationData); hydrationData = undefined; if (isPromiseLike(value)) { value = value.catch((err) => handleChildError(ctx, err)); } } ctx.f &= ~NeedsToYield; } catch (err) { // Do we need to catch potential errors here in the case of unhandled // promise rejections? value = handleChildError(ctx, err); } finally { onValue(value); } let oldResult; if (ctx.ret.inflightValue) { // The value passed back into the generator as the argument to the next // method is a promise if an async generator component has async // children. Sync generator components only resume when their children // have fulfilled so the element’s inflight child values will never be // defined. oldResult = ctx.ret.inflightValue.then((value) => ctx.renderer.read(value)); oldResult.catch((err) => { if (ctx.f & IsUpdating) { return; } if (!ctx.parent) { throw err; } return propagateError(ctx.parent, err); }); } else { oldResult = ctx.renderer.read(getValue(ctx.ret)); } if (ctx.f & IsUnmounted) { if (ctx.f & IsInForAwaitOfLoop) { try { ctx.f |= IsSyncExecuting; iterationP = ctx.iterator.next(oldResult); } finally { ctx.f &= ~IsSyncExecuting; } } else { returnComponent(ctx); break; } } else if (!done && !(ctx.f & IsInForOfLoop)) { try { ctx.f |= IsSyncExecuting; iterationP = ctx.iterator.next(oldResult); } finally { ctx.f &= ~IsSyncExecuting; } } initial = false; } } finally { if (done) { ctx.f &= ~IsAsyncGen; ctx.iterator = undefined; } } } /** * Called to resume the props async iterator for async generator components. */ function resumePropsAsyncIterator(ctx) { if (ctx.onProps) { ctx.onProps(ctx.ret.el.props); ctx.onProps = undefined; ctx.f &= ~PropsAvailable; } else { ctx.f |= PropsAvailable; } } // TODO: async unmounting function unmountComponent(ctx) { if (ctx.f & IsUnmounted) { return; } clearEventListeners(ctx); const callbacks = cleanupMap.get(ctx); if (callbacks) { cleanupMap.delete(ctx); const value = ctx.renderer.read(getValue(ctx.ret)); for (const callback of callbacks) { callback(value); } } ctx.f |= IsUnmounted; if (ctx.iterator) { if (ctx.f & IsSyncGen) { let value; if (ctx.f & IsInForOfLoop) { value = enqueueComponentRun(ctx); } if (isPromiseLike(value)) { value.then(() => { if (ctx.f & IsInForOfLoop) { unmountComponent(ctx); } else { returnComponent(ctx); } }, (err) => { if (!ctx.parent) { throw err; } return propagateError(ctx.parent, err); }); } else { if (ctx.f & IsInForOfLoop) { unmountComponent(ctx); } else { returnComponent(ctx); } } } else if (ctx.f & IsAsyncGen) { if (ctx.f & IsInForOfLoop) { const value = enqueueComponentRun(ctx); value.then(() => { if (ctx.f & IsInForOfLoop) { unmountComponent(ctx); } else { returnComponent(ctx); } }, (err) => { if (!ctx.parent) { throw err; } return propagateError(ctx.parent, err); }); } else { // The logic for unmounting async generator components is in the // runAsyncGenComponent function. resumePropsAsyncIterator(ctx); } } } } function returnComponent(ctx) { resumePropsAsyncIterator(ctx); if (ctx.iterator && typeof ctx.iterator.return === "function") { try { ctx.f |= IsSyncExecuting; const iteration = ctx.iterator.return(); if (isPromiseLike(iteration)) { iteration.catch((err) => { if (!ctx.parent) { throw err; } return propagateError(ctx.parent, err); }); } } finally { ctx.f &= ~IsSyncExecuting; } } } /*** EVENT TARGET UTILITIES ***/ // EVENT PHASE CONSTANTS // https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase const NONE = 0; const CAPTURING_PHASE = 1; const AT_TARGET = 2; const BUBBLING_PHASE = 3; const listenersMap = new WeakMap(); function isListenerOrListenerObject(value) { return (typeof value === "function" || (value !== null && typeof value === "object" && typeof value.handleEvent === "function")); } function normalizeListenerOptions(options) { if (typeof options === "boolean") { return { capture: options }; } else if (options == null) { return {}; } return options; } function isEventTarget(value) { return (value != null && typeof value.addEventListener === "function" && typeof value.removeEventListener === "function" && typeof value.dispatchEvent === "function"); } function setEventProperty(ev, key, value) { Object.defineProperty(ev, key, { value, writable: false, configurable: true }); } // TODO: Maybe we can pass in the current context directly, rather than // starting from the parent? /** * A function to reconstruct an array of every listener given a context and a * host element. * * This function exploits the fact that contexts retain their nearest ancestor * host element. We can determine all the contexts which are directly listening * to an element by traversing up the context tree and checking that the host * element passed in matches the parent context’s host element. */ function getListenerRecords(ctx, ret) { let listeners = []; while (ctx !== undefined && ctx.host === ret) { const listeners1 = listenersMap.get(ctx); if (listeners1) { listeners = listeners.concat(listeners1); } ctx = ctx.parent; } return listeners; } function clearEventListeners(ctx) { const listeners = listenersMap.get(ctx); if (listeners && listeners.length) { for (const value of getChildValues(ctx.ret)) { if (isEventTarget(value)) { for (const record of listeners) { value.removeEventListener(record.type, record.callback, record.options); } } } listeners.length = 0; } } /*** ERROR HANDLING UTILITIES ***/ function handleChildError(ctx, err) { if (!ctx.iterator || typeof ctx.iterator.throw !== "function") { throw err; } resumePropsAsyncIterator(ctx); let iteration; try { ctx.f |= IsSyncExecuting; iteration = ctx.iterator.throw(err); } catch (err) { ctx.f |= IsErrored; throw err; } finally { ctx.f &= ~IsSyncExecuting; } if (isPromiseLike(iteration)) { return iteration.then((iteration) => { if (iteration.done) { ctx.f &= ~IsAsyncGen; ctx.iterator = undefined; } return updateComponentChildren(ctx, iteration.value); }, (err) => { ctx.f |= IsErrored; throw err; }); } if (iteration.done) { ctx.f &= ~IsSyncGen; ctx.f &= ~IsAsyncGen; ctx.iterator = undefined; } return updateComponentChildren(ctx, iteration.value); } function propagateError(ctx, err) { let result; try { result = handleChildError(ctx, err); } catch (err) { if (!ctx.parent) { throw err; } return propagateError(ctx.parent, err); } if (isPromiseLike(result)) { return result.catch((err) => { if (!ctx.parent) { throw err; } return propagateError(ctx.parent, err); }); } return result; } // Some JSX transpilation tools expect these functions to be defined on the // default export. Prefer named exports when importing directly. var crank = { createElement, Fragment }; exports.Context = Context; exports.Copy = Copy; exports.Element = Element; exports.Fragment = Fragment; exports.Portal = Portal; exports.Raw = Raw; exports.Renderer = Renderer; exports.cloneElement = cloneElement; exports.createElement = createElement; exports.default = crank; exports.isElement = isElement; //# sourceMappingURL=crank.cjs.map