import { Slot } from '@wry/context'; export { asyncFromGen, bind as bindContext, noContext, setTimeout } from '@wry/context'; function defaultDispose() { } var Cache = /** @class */ (function () { function Cache(max, dispose) { if (max === void 0) { max = Infinity; } if (dispose === void 0) { dispose = defaultDispose; } this.max = max; this.dispose = dispose; this.map = new Map(); this.newest = null; this.oldest = null; } Cache.prototype.has = function (key) { return this.map.has(key); }; Cache.prototype.get = function (key) { var entry = this.getEntry(key); return entry && entry.value; }; Cache.prototype.getEntry = function (key) { var entry = this.map.get(key); if (entry && entry !== this.newest) { var older = entry.older, newer = entry.newer; if (newer) { newer.older = older; } if (older) { older.newer = newer; } entry.older = this.newest; entry.older.newer = entry; entry.newer = null; this.newest = entry; if (entry === this.oldest) { this.oldest = newer; } } return entry; }; Cache.prototype.set = function (key, value) { var entry = this.getEntry(key); if (entry) { return entry.value = value; } entry = { key: key, value: value, newer: null, older: this.newest }; if (this.newest) { this.newest.newer = entry; } this.newest = entry; this.oldest = this.oldest || entry; this.map.set(key, entry); return entry.value; }; Cache.prototype.clean = function () { while (this.oldest && this.map.size > this.max) { this.delete(this.oldest.key); } }; Cache.prototype.delete = function (key) { var entry = this.map.get(key); if (entry) { if (entry === this.newest) { this.newest = entry.older; } if (entry === this.oldest) { this.oldest = entry.newer; } if (entry.newer) { entry.newer.older = entry.older; } if (entry.older) { entry.older.newer = entry.newer; } this.map.delete(key); this.dispose(entry.value, key); return true; } return false; }; return Cache; }()); var parentEntrySlot = new Slot(); var reusableEmptyArray = []; var emptySetPool = []; var POOL_TARGET_SIZE = 100; // Since this package might be used browsers, we should avoid using the // Node built-in assert module. function assert(condition, optionalMessage) { if (!condition) { throw new Error(optionalMessage || "assertion failure"); } } function valueIs(a, b) { var len = a.length; return ( // Unknown values are not equal to each other. len > 0 && // Both values must be ordinary (or both exceptional) to be equal. len === b.length && // The underlying value or exception must be the same. a[len - 1] === b[len - 1]); } function valueGet(value) { switch (value.length) { case 0: throw new Error("unknown value"); case 1: return value[0]; case 2: throw value[1]; } } function valueCopy(value) { return value.slice(0); } var Entry = /** @class */ (function () { function Entry(fn, args) { this.fn = fn; this.args = args; this.parents = new Set(); this.childValues = new Map(); // When this Entry has children that are dirty, this property becomes // a Set containing other Entry objects, borrowed from emptySetPool. // When the set becomes empty, it gets recycled back to emptySetPool. this.dirtyChildren = null; this.dirty = true; this.recomputing = false; this.value = []; ++Entry.count; } // This is the most important method of the Entry API, because it // determines whether the cached this.value can be returned immediately, // or must be recomputed. The overall performance of the caching system // depends on the truth of the following observations: (1) this.dirty is // usually false, (2) this.dirtyChildren is usually null/empty, and thus // (3) valueGet(this.value) is usually returned without recomputation. Entry.prototype.recompute = function () { assert(!this.recomputing, "already recomputing"); if (!rememberParent(this) && maybeReportOrphan(this)) { // The recipient of the entry.reportOrphan callback decided to dispose // of this orphan entry by calling entry.dispose(), so we don't need to // (and should not) proceed with the recomputation. return void 0; } return mightBeDirty(this) ? reallyRecompute(this) : valueGet(this.value); }; Entry.prototype.setDirty = function () { if (this.dirty) return; this.dirty = true; this.value.length = 0; reportDirty(this); // We can go ahead and unsubscribe here, since any further dirty // notifications we receive will be redundant, and unsubscribing may // free up some resources, e.g. file watchers. maybeUnsubscribe(this); }; Entry.prototype.dispose = function () { var _this = this; forgetChildren(this).forEach(maybeReportOrphan); maybeUnsubscribe(this); // Because this entry has been kicked out of the cache (in index.js), // we've lost the ability to find out if/when this entry becomes dirty, // whether that happens through a subscription, because of a direct call // to entry.setDirty(), or because one of its children becomes dirty. // Because of this loss of future information, we have to assume the // worst (that this entry might have become dirty very soon), so we must // immediately mark this entry's parents as dirty. Normally we could // just call entry.setDirty() rather than calling parent.setDirty() for // each parent, but that would leave this entry in parent.childValues // and parent.dirtyChildren, which would prevent the child from being // truly forgotten. this.parents.forEach(function (parent) { parent.setDirty(); forgetChild(parent, _this); }); }; Entry.count = 0; return Entry; }()); function rememberParent(child) { var parent = parentEntrySlot.getValue(); if (parent) { child.parents.add(parent); if (!parent.childValues.has(child)) { parent.childValues.set(child, []); } if (mightBeDirty(child)) { reportDirtyChild(parent, child); } else { reportCleanChild(parent, child); } return parent; } } function reallyRecompute(entry) { // Since this recomputation is likely to re-remember some of this // entry's children, we forget our children here but do not call // maybeReportOrphan until after the recomputation finishes. var originalChildren = forgetChildren(entry); // Set entry as the parent entry while calling recomputeNewValue(entry). parentEntrySlot.withValue(entry, recomputeNewValue, [entry]); if (maybeSubscribe(entry)) { // If we successfully recomputed entry.value and did not fail to // (re)subscribe, then this Entry is no longer explicitly dirty. setClean(entry); } // Now that we've had a chance to re-remember any children that were // involved in the recomputation, we can safely report any orphan // children that remain. originalChildren.forEach(maybeReportOrphan); return valueGet(entry.value); } function recomputeNewValue(entry) { entry.recomputing = true; // Set entry.value as unknown. entry.value.length = 0; try { // If entry.fn succeeds, entry.value will become a normal Value. entry.value[0] = entry.fn.apply(null, entry.args); } catch (e) { // If entry.fn throws, entry.value will become exceptional. entry.value[1] = e; } // Either way, this line is always reached. entry.recomputing = false; } function mightBeDirty(entry) { return entry.dirty || !!(entry.dirtyChildren && entry.dirtyChildren.size); } function setClean(entry) { entry.dirty = false; if (mightBeDirty(entry)) { // This Entry may still have dirty children, in which case we can't // let our parents know we're clean just yet. return; } reportClean(entry); } function reportDirty(child) { child.parents.forEach(function (parent) { return reportDirtyChild(parent, child); }); } function reportClean(child) { child.parents.forEach(function (parent) { return reportCleanChild(parent, child); }); } // Let a parent Entry know that one of its children may be dirty. function reportDirtyChild(parent, child) { // Must have called rememberParent(child) before calling // reportDirtyChild(parent, child). assert(parent.childValues.has(child)); assert(mightBeDirty(child)); if (!parent.dirtyChildren) { parent.dirtyChildren = emptySetPool.pop() || new Set; } else if (parent.dirtyChildren.has(child)) { // If we already know this child is dirty, then we must have already // informed our own parents that we are dirty, so we can terminate // the recursion early. return; } parent.dirtyChildren.add(child); reportDirty(parent); } // Let a parent Entry know that one of its children is no longer dirty. function reportCleanChild(parent, child) { // Must have called rememberChild(child) before calling // reportCleanChild(parent, child). assert(parent.childValues.has(child)); assert(!mightBeDirty(child)); var childValue = parent.childValues.get(child); if (childValue.length === 0) { parent.childValues.set(child, valueCopy(child.value)); } else if (!valueIs(childValue, child.value)) { parent.setDirty(); } removeDirtyChild(parent, child); if (mightBeDirty(parent)) { return; } reportClean(parent); } function removeDirtyChild(parent, child) { var dc = parent.dirtyChildren; if (dc) { dc.delete(child); if (dc.size === 0) { if (emptySetPool.length < POOL_TARGET_SIZE) { emptySetPool.push(dc); } parent.dirtyChildren = null; } } } // If the given entry has a reportOrphan method, and no remaining parents, // call entry.reportOrphan and return true iff it returns true. The // reportOrphan function should return true to indicate entry.dispose() // has been called, and the entry has been removed from any other caches // (see index.js for the only current example). function maybeReportOrphan(entry) { return entry.parents.size === 0 && typeof entry.reportOrphan === "function" && entry.reportOrphan() === true; } // Removes all children from this entry and returns an array of the // removed children. function forgetChildren(parent) { var children = reusableEmptyArray; if (parent.childValues.size > 0) { children = []; parent.childValues.forEach(function (_value, child) { forgetChild(parent, child); children.push(child); }); } // After we forget all our children, this.dirtyChildren must be empty // and therefore must have been reset to null. assert(parent.dirtyChildren === null); return children; } function forgetChild(parent, child) { child.parents.delete(parent); parent.childValues.delete(child); removeDirtyChild(parent, child); } function maybeSubscribe(entry) { if (typeof entry.subscribe === "function") { try { maybeUnsubscribe(entry); // Prevent double subscriptions. entry.unsubscribe = entry.subscribe.apply(null, entry.args); } catch (e) { // If this Entry has a subscribe function and it threw an exception // (or an unsubscribe function it previously returned now throws), // return false to indicate that we were not able to subscribe (or // unsubscribe), and this Entry should remain dirty. entry.setDirty(); return false; } } // Returning true indicates either that there was no entry.subscribe // function or that it succeeded. return true; } function maybeUnsubscribe(entry) { var unsubscribe = entry.unsubscribe; if (typeof unsubscribe === "function") { entry.unsubscribe = void 0; unsubscribe(); } } // A trie data structure that holds object keys weakly, yet can also hold // non-object keys, unlike the native `WeakMap`. var KeyTrie = /** @class */ (function () { function KeyTrie(weakness) { this.weakness = weakness; } KeyTrie.prototype.lookup = function () { var array = []; for (var _i = 0; _i < arguments.length; _i++) { array[_i] = arguments[_i]; } return this.lookupArray(array); }; KeyTrie.prototype.lookupArray = function (array) { var node = this; array.forEach(function (key) { return node = node.getChildTrie(key); }); return node.data || (node.data = Object.create(null)); }; KeyTrie.prototype.getChildTrie = function (key) { var map = this.weakness && isObjRef(key) ? this.weak || (this.weak = new WeakMap()) : this.strong || (this.strong = new Map()); var child = map.get(key); if (!child) map.set(key, child = new KeyTrie(this.weakness)); return child; }; return KeyTrie; }()); function isObjRef(value) { switch (typeof value) { case "object": if (value === null) break; // Fall through to return true... case "function": return true; } return false; } // The defaultMakeCacheKey function is remarkably powerful, because it gives // a unique object for any shallow-identical list of arguments. If you need // to implement a custom makeCacheKey function, you may find it helpful to // delegate the final work to defaultMakeCacheKey, which is why we export it // here. However, you may want to avoid defaultMakeCacheKey if your runtime // does not support WeakMap, or you have the ability to return a string key. // In those cases, just write your own custom makeCacheKey functions. var keyTrie = new KeyTrie(typeof WeakMap === "function"); function defaultMakeCacheKey() { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return keyTrie.lookupArray(args); } var caches = new Set(); function wrap(originalFunction, options) { if (options === void 0) { options = Object.create(null); } var cache = new Cache(options.max || Math.pow(2, 16), function (entry) { return entry.dispose(); }); var disposable = !!options.disposable; var makeCacheKey = options.makeCacheKey || defaultMakeCacheKey; function optimistic() { if (disposable && !parentEntrySlot.hasValue()) { // If there's no current parent computation, and this wrapped // function is disposable (meaning we don't care about entry.value, // just dependency tracking), then we can short-cut everything else // in this function, because entry.recompute() is going to recycle // the entry object without recomputing anything, anyway. return void 0; } var key = makeCacheKey.apply(null, arguments); if (key === void 0) { return originalFunction.apply(null, arguments); } var args = Array.prototype.slice.call(arguments); var entry = cache.get(key); if (entry) { entry.args = args; } else { entry = new Entry(originalFunction, args); cache.set(key, entry); entry.subscribe = options.subscribe; if (disposable) { entry.reportOrphan = function () { return cache.delete(key); }; } } var value = entry.recompute(); // Move this entry to the front of the least-recently used queue, // since we just finished computing its value. cache.set(key, entry); caches.add(cache); // Clean up any excess entries in the cache, but only if there is no // active parent entry, meaning we're not in the middle of a larger // computation that might be flummoxed by the cleaning. if (!parentEntrySlot.hasValue()) { caches.forEach(function (cache) { return cache.clean(); }); caches.clear(); } // If options.disposable is truthy, the caller of wrap is telling us // they don't care about the result of entry.recompute(), so we should // avoid returning the value, so it won't be accidentally used. return disposable ? void 0 : value; } optimistic.dirty = function () { var key = makeCacheKey.apply(null, arguments); var child = key !== void 0 && cache.get(key); if (child) { child.setDirty(); } }; return optimistic; } export { KeyTrie, defaultMakeCacheKey, wrap }; //# sourceMappingURL=bundle.esm.js.map