inital upload

This commit is contained in:
jackbeeby
2025-05-15 13:35:49 +10:00
commit 8c53ff1000
9092 changed files with 1833300 additions and 0 deletions

16
node_modules/optimism/src/context.ts generated vendored Normal file
View File

@@ -0,0 +1,16 @@
import { Slot } from "@wry/context";
import { AnyEntry } from "./entry.js";
export const parentEntrySlot = new Slot<AnyEntry | undefined>();
export function nonReactive<R>(fn: () => R): R {
return parentEntrySlot.withValue(void 0, fn);
}
export { Slot }
export {
bind as bindContext,
noContext,
setTimeout,
asyncFromGen,
} from "@wry/context";

68
node_modules/optimism/src/dep.ts generated vendored Normal file
View File

@@ -0,0 +1,68 @@
import { AnyEntry } from "./entry.js";
import { OptimisticWrapOptions } from "./index.js";
import { parentEntrySlot } from "./context.js";
import {
hasOwnProperty,
Unsubscribable,
maybeUnsubscribe,
arrayFromSet,
} from "./helpers.js";
type EntryMethodName = keyof typeof EntryMethods;
const EntryMethods = {
setDirty: true, // Mark parent Entry as needing to be recomputed (default)
dispose: true, // Detach parent Entry from parents and children, but leave in LRU cache
forget: true, // Fully remove parent Entry from LRU cache and computation graph
};
export type OptimisticDependencyFunction<TKey> =
((key: TKey) => void) & {
dirty: (key: TKey, entryMethodName?: EntryMethodName) => void;
};
export type Dep<TKey> = Set<AnyEntry> & {
subscribe: OptimisticWrapOptions<[TKey]>["subscribe"];
} & Unsubscribable;
export function dep<TKey>(options?: {
subscribe: Dep<TKey>["subscribe"];
}) {
const depsByKey = new Map<TKey, Dep<TKey>>();
const subscribe = options && options.subscribe;
function depend(key: TKey) {
const parent = parentEntrySlot.getValue();
if (parent) {
let dep = depsByKey.get(key);
if (!dep) {
depsByKey.set(key, dep = new Set as Dep<TKey>);
}
parent.dependOn(dep);
if (typeof subscribe === "function") {
maybeUnsubscribe(dep);
dep.unsubscribe = subscribe(key);
}
}
}
depend.dirty = function dirty(
key: TKey,
entryMethodName?: EntryMethodName,
) {
const dep = depsByKey.get(key);
if (dep) {
const m: EntryMethodName = (
entryMethodName &&
hasOwnProperty.call(EntryMethods, entryMethodName)
) ? entryMethodName : "setDirty";
// We have to use arrayFromSet(dep).forEach instead of dep.forEach,
// because modifying a Set while iterating over it can cause elements in
// the Set to be removed from the Set before they've been iterated over.
arrayFromSet(dep).forEach(entry => entry[m]());
depsByKey.delete(key);
maybeUnsubscribe(dep);
}
};
return depend as OptimisticDependencyFunction<TKey>;
}

372
node_modules/optimism/src/entry.ts generated vendored Normal file
View File

@@ -0,0 +1,372 @@
import { parentEntrySlot } from "./context.js";
import { OptimisticWrapOptions } from "./index.js";
import { Dep } from "./dep.js";
import { maybeUnsubscribe, arrayFromSet, Unsubscribable } from "./helpers.js";
const emptySetPool: Set<any>[] = [];
const POOL_TARGET_SIZE = 100;
// Since this package might be used browsers, we should avoid using the
// Node built-in assert module.
function assert(condition: any, optionalMessage?: string) {
if (! condition) {
throw new Error(optionalMessage || "assertion failure");
}
}
// Since exceptions are cached just like normal values, we need an efficient
// way of representing unknown, ordinary, and exceptional values.
type Value<T> =
| [] // unknown
| [T] // known value
| [void, any]; // known exception
function valueIs(a: Value<any>, b: Value<any>) {
const 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<T>(value: Value<T>): T {
switch (value.length) {
case 0: throw new Error("unknown value");
case 1: return value[0];
case 2: throw value[1];
}
}
function valueCopy<T>(value: Value<T>): Value<T> {
return value.slice(0) as Value<T>;
}
export type AnyEntry = Entry<any, any>;
export class Entry<TArgs extends any[], TValue> {
public static count = 0;
public normalizeResult: OptimisticWrapOptions<TArgs, any, any, TValue>["normalizeResult"];
public subscribe: OptimisticWrapOptions<TArgs>["subscribe"];
public unsubscribe: Unsubscribable["unsubscribe"];
public readonly parents = new Set<AnyEntry>();
public readonly childValues = new Map<AnyEntry, Value<any>>();
// 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.
public dirtyChildren: Set<AnyEntry> | null = null;
public dirty = true;
public recomputing = false;
public readonly value: Value<TValue> = [];
constructor(
public readonly fn: (...args: TArgs) => TValue,
) {
++Entry.count;
}
public peek(): TValue | undefined {
if (this.value.length === 1 && !mightBeDirty(this)) {
rememberParent(this);
return this.value[0];
}
}
// 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.
public recompute(args: TArgs): TValue {
assert(! this.recomputing, "already recomputing");
rememberParent(this);
return mightBeDirty(this)
? reallyRecompute(this, args)
: valueGet(this.value);
}
public setDirty() {
if (this.dirty) return;
this.dirty = true;
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);
}
public dispose() {
this.setDirty();
// Sever any dependency relationships with our own children, so those
// children don't retain this parent Entry in their child.parents sets,
// thereby preventing it from being fully garbage collected.
forgetChildren(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.
eachParent(this, (parent, child) => {
parent.setDirty();
forgetChild(parent, this);
});
}
public forget() {
// The code that creates Entry objects in index.ts will replace this method
// with one that actually removes the Entry from the cache, which will also
// trigger the entry.dispose method.
this.dispose();
}
private deps: Set<Dep<any>> | null = null;
public dependOn(dep: Dep<any>) {
dep.add(this);
if (! this.deps) {
this.deps = emptySetPool.pop() || new Set<Set<AnyEntry>>();
}
this.deps.add(dep);
}
public forgetDeps() {
if (this.deps) {
arrayFromSet(this.deps).forEach(dep => dep.delete(this));
this.deps.clear();
emptySetPool.push(this.deps);
this.deps = null;
}
}
}
function rememberParent(child: AnyEntry) {
const 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: AnyEntry, args: any[]) {
forgetChildren(entry);
// Set entry as the parent entry while calling recomputeNewValue(entry).
parentEntrySlot.withValue(entry, recomputeNewValue, [entry, args]);
if (maybeSubscribe(entry, args)) {
// If we successfully recomputed entry.value and did not fail to
// (re)subscribe, then this Entry is no longer explicitly dirty.
setClean(entry);
}
return valueGet(entry.value);
}
function recomputeNewValue(entry: AnyEntry, args: any[]) {
entry.recomputing = true;
const { normalizeResult } = entry;
let oldValueCopy: Value<any> | undefined;
if (normalizeResult && entry.value.length === 1) {
oldValueCopy = valueCopy(entry.value);
}
// Make entry.value an empty array, representing an unknown value.
entry.value.length = 0;
try {
// If entry.fn succeeds, entry.value will become a normal Value.
entry.value[0] = entry.fn.apply(null, args);
// If we have a viable oldValueCopy to compare with the (successfully
// recomputed) new entry.value, and they are not already === identical, give
// normalizeResult a chance to pick/choose/reuse parts of oldValueCopy[0]
// and/or entry.value[0] to determine the final cached entry.value.
if (normalizeResult && oldValueCopy && !valueIs(oldValueCopy, entry.value)) {
try {
entry.value[0] = normalizeResult(entry.value[0], oldValueCopy[0]);
} catch {
// If normalizeResult throws, just use the newer value, rather than
// saving the exception as entry.value[1].
}
}
} catch (e) {
// If entry.fn throws, entry.value will hold that exception.
entry.value[1] = e;
}
// Either way, this line is always reached.
entry.recomputing = false;
}
function mightBeDirty(entry: AnyEntry) {
return entry.dirty || !!(entry.dirtyChildren && entry.dirtyChildren.size);
}
function setClean(entry: AnyEntry) {
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: AnyEntry) {
eachParent(child, reportDirtyChild);
}
function reportClean(child: AnyEntry) {
eachParent(child, reportCleanChild);
}
function eachParent(
child: AnyEntry,
callback: (parent: AnyEntry, child: AnyEntry) => any,
) {
const parentCount = child.parents.size;
if (parentCount) {
const parents = arrayFromSet(child.parents);
for (let i = 0; i < parentCount; ++i) {
callback(parents[i], child);
}
}
}
// Let a parent Entry know that one of its children may be dirty.
function reportDirtyChild(parent: AnyEntry, child: AnyEntry) {
// Must have called rememberParent(child) before calling
// reportDirtyChild(parent, child).
assert(parent.childValues.has(child));
assert(mightBeDirty(child));
const parentWasClean = !mightBeDirty(parent);
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);
// If parent was clean before, it just became (possibly) dirty (according to
// mightBeDirty), since we just added child to parent.dirtyChildren.
if (parentWasClean) {
reportDirty(parent);
}
}
// Let a parent Entry know that one of its children is no longer dirty.
function reportCleanChild(parent: AnyEntry, child: AnyEntry) {
// Must have called rememberChild(child) before calling
// reportCleanChild(parent, child).
assert(parent.childValues.has(child));
assert(! mightBeDirty(child));
const 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: AnyEntry, child: AnyEntry) {
const dc = parent.dirtyChildren;
if (dc) {
dc.delete(child);
if (dc.size === 0) {
if (emptySetPool.length < POOL_TARGET_SIZE) {
emptySetPool.push(dc);
}
parent.dirtyChildren = null;
}
}
}
// Removes all children from this entry and returns an array of the
// removed children.
function forgetChildren(parent: AnyEntry) {
if (parent.childValues.size > 0) {
parent.childValues.forEach((_value, child) => {
forgetChild(parent, child);
});
}
// Remove this parent Entry from any sets to which it was added by the
// addToSet method.
parent.forgetDeps();
// After we forget all our children, this.dirtyChildren must be empty
// and therefore must have been reset to null.
assert(parent.dirtyChildren === null);
}
function forgetChild(parent: AnyEntry, child: AnyEntry) {
child.parents.delete(parent);
parent.childValues.delete(child);
removeDirtyChild(parent, child);
}
function maybeSubscribe(entry: AnyEntry, args: any[]) {
if (typeof entry.subscribe === "function") {
try {
maybeUnsubscribe(entry); // Prevent double subscriptions.
entry.unsubscribe = entry.subscribe.apply(null, 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;
}

25
node_modules/optimism/src/helpers.ts generated vendored Normal file
View File

@@ -0,0 +1,25 @@
export type NoInfer<T> = [T][T extends any ? 0 : never];
export const {
hasOwnProperty,
} = Object.prototype;
export const arrayFromSet: <T>(set: Set<T>) => T[] =
Array.from ||
function (set) {
const array: any[] = [];
set.forEach(item => array.push(item));
return array;
};
export type Unsubscribable = {
unsubscribe?: void | (() => any);
}
export function maybeUnsubscribe(entryOrDep: Unsubscribable) {
const { unsubscribe } = entryOrDep;
if (typeof unsubscribe === "function") {
entryOrDep.unsubscribe = void 0;
unsubscribe();
}
}

246
node_modules/optimism/src/index.ts generated vendored Normal file
View File

@@ -0,0 +1,246 @@
import { Trie } from "@wry/trie";
import { StrongCache, CommonCache } from "@wry/caches";
import { Entry, AnyEntry } from "./entry.js";
import { parentEntrySlot } from "./context.js";
import type { NoInfer } from "./helpers.js";
// These helper functions are important for making optimism work with
// asynchronous code. In order to register parent-child dependencies,
// optimism needs to know about any currently active parent computations.
// In ordinary synchronous code, the parent context is implicit in the
// execution stack, but asynchronous code requires some extra guidance in
// order to propagate context from one async task segment to the next.
export {
bindContext,
noContext,
nonReactive,
setTimeout,
asyncFromGen,
Slot,
} from "./context.js";
// A lighter-weight dependency, similar to OptimisticWrapperFunction, except
// with only one argument, no makeCacheKey, no wrapped function to recompute,
// and no result value. Useful for representing dependency leaves in the graph
// of computation. Subscriptions are supported.
export { dep, OptimisticDependencyFunction } from "./dep.js";
// 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.
let defaultKeyTrie: Trie<object> | undefined;
export function defaultMakeCacheKey(...args: any[]): object {
const trie = defaultKeyTrie || (
defaultKeyTrie = new Trie(typeof WeakMap === "function")
);
return trie.lookupArray(args);
}
// If you're paranoid about memory leaks, or you want to avoid using WeakMap
// under the hood, but you still need the behavior of defaultMakeCacheKey,
// import this constructor to create your own tries.
export { Trie as KeyTrie }
export type OptimisticWrapperFunction<
TArgs extends any[],
TResult,
TKeyArgs extends any[] = TArgs,
TCacheKey = any,
> = ((...args: TArgs) => TResult) & {
// Get the current number of Entry objects in the LRU cache.
readonly size: number;
// Snapshot of wrap options used to create this wrapper function.
options: OptionsWithCacheInstance<TArgs, TKeyArgs, TCacheKey>;
// "Dirty" any cached Entry stored for the given arguments, marking that Entry
// and its ancestors as potentially needing to be recomputed. The .dirty(...)
// method of an optimistic function takes the same parameter types as the
// original function by default, unless a keyArgs function is configured, and
// then it matters that .dirty takes TKeyArgs instead of TArgs.
dirty: (...args: TKeyArgs) => void;
// A version of .dirty that accepts a key returned by .getKey.
dirtyKey: (key: TCacheKey | undefined) => void;
// Examine the current value without recomputing it.
peek: (...args: TKeyArgs) => TResult | undefined;
// A version of .peek that accepts a key returned by .getKey.
peekKey: (key: TCacheKey | undefined) => TResult | undefined;
// Completely remove the entry from the cache, dirtying any parent entries.
forget: (...args: TKeyArgs) => boolean;
// A version of .forget that accepts a key returned by .getKey.
forgetKey: (key: TCacheKey | undefined) => boolean;
// In order to use the -Key version of the above functions, you need a key
// rather than the arguments used to compute the key. These two functions take
// TArgs or TKeyArgs and return the corresponding TCacheKey. If no keyArgs
// function has been configured, TArgs will be the same as TKeyArgs, and thus
// getKey and makeCacheKey will be synonymous.
getKey: (...args: TArgs) => TCacheKey | undefined;
// This property is equivalent to the makeCacheKey function provided in the
// OptimisticWrapOptions, or (if no options.makeCacheKey function is provided)
// a default implementation of makeCacheKey. This function is also exposed as
// optimistic.options.makeCacheKey, somewhat redundantly.
makeCacheKey: (...args: TKeyArgs) => TCacheKey | undefined;
};
export { CommonCache }
export interface CommonCacheConstructor<TCacheKey, TResult, TArgs extends any[]> extends Function {
new <K extends TCacheKey, V extends Entry<TArgs, TResult>>(max?: number, dispose?: (value: V, key?: K) => void): CommonCache<K,V>;
}
export type OptimisticWrapOptions<
TArgs extends any[],
TKeyArgs extends any[] = TArgs,
TCacheKey = any,
TResult = any,
> = {
// The maximum number of cache entries that should be retained before the
// cache begins evicting the oldest ones.
max?: number;
// Transform the raw arguments to some other type of array, which will then
// be passed to makeCacheKey.
keyArgs?: (...args: TArgs) => TKeyArgs;
// The makeCacheKey function takes the same arguments that were passed to
// the wrapper function and returns a single value that can be used as a key
// in a Map to identify the cached result.
makeCacheKey?: (...args: NoInfer<TKeyArgs>) => TCacheKey | undefined;
// Called when a new value is computed to allow efficient normalization of
// results over time, for example by returning older if equal(newer, older).
normalizeResult?: (newer: TResult, older: TResult) => TResult;
// If provided, the subscribe function should either return an unsubscribe
// function or return nothing.
subscribe?: (...args: TArgs) => void | (() => any);
cache?: CommonCache<NoInfer<TCacheKey>, Entry<NoInfer<TArgs>, NoInfer<TResult>>>
| CommonCacheConstructor<NoInfer<TCacheKey>, NoInfer<TResult>, NoInfer<TArgs>>;
};
export interface OptionsWithCacheInstance<
TArgs extends any[],
TKeyArgs extends any[] = TArgs,
TCacheKey = any,
TResult = any,
> extends OptimisticWrapOptions<TArgs, TKeyArgs, TCacheKey, TResult> {
cache: CommonCache<NoInfer<TCacheKey>, Entry<NoInfer<TArgs>, NoInfer<TResult>>>;
};
const caches = new Set<CommonCache<any, AnyEntry>>();
export function wrap<
TArgs extends any[],
TResult,
TKeyArgs extends any[] = TArgs,
TCacheKey = any,
>(originalFunction: (...args: TArgs) => TResult, {
max = Math.pow(2, 16),
keyArgs,
makeCacheKey = (defaultMakeCacheKey as () => TCacheKey),
normalizeResult,
subscribe,
cache: cacheOption = StrongCache,
}: OptimisticWrapOptions<TArgs, TKeyArgs, TCacheKey, TResult> = Object.create(null)) {
const cache: CommonCache<TCacheKey, Entry<TArgs, TResult>> =
typeof cacheOption === "function"
? new cacheOption(max, entry => entry.dispose())
: cacheOption;
const optimistic = function (): TResult {
const key = makeCacheKey.apply(
null,
keyArgs ? keyArgs.apply(null, arguments as any) : arguments as any
);
if (key === void 0) {
return originalFunction.apply(null, arguments as any);
}
let entry = cache.get(key)!;
if (!entry) {
cache.set(key, entry = new Entry(originalFunction));
entry.normalizeResult = normalizeResult;
entry.subscribe = subscribe;
// Give the Entry the ability to trigger cache.delete(key), even though
// the Entry itself does not know about key or cache.
entry.forget = () => cache.delete(key);
}
const value = entry.recompute(
Array.prototype.slice.call(arguments) as TArgs,
);
// 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(cache => cache.clean());
caches.clear();
}
return value;
} as OptimisticWrapperFunction<TArgs, TResult, TKeyArgs, TCacheKey>;
Object.defineProperty(optimistic, "size", {
get: () => cache.size,
configurable: false,
enumerable: false,
});
Object.freeze(optimistic.options = {
max,
keyArgs,
makeCacheKey,
normalizeResult,
subscribe,
cache,
});
function dirtyKey(key: TCacheKey | undefined) {
const entry = key && cache.get(key);
if (entry) {
entry.setDirty();
}
}
optimistic.dirtyKey = dirtyKey;
optimistic.dirty = function dirty() {
dirtyKey(makeCacheKey.apply(null, arguments as any));
};
function peekKey(key: TCacheKey | undefined) {
const entry = key && cache.get(key);
if (entry) {
return entry.peek();
}
}
optimistic.peekKey = peekKey;
optimistic.peek = function peek() {
return peekKey(makeCacheKey.apply(null, arguments as any));
};
function forgetKey(key: TCacheKey | undefined) {
return key ? cache.delete(key) : false;
}
optimistic.forgetKey = forgetKey;
optimistic.forget = function forget() {
return forgetKey(makeCacheKey.apply(null, arguments as any));
};
optimistic.makeCacheKey = makeCacheKey;
optimistic.getKey = keyArgs ? function getKey() {
return makeCacheKey.apply(null, keyArgs.apply(null, arguments as any));
} : makeCacheKey as (...args: any[]) => TCacheKey | undefined;
return Object.freeze(optimistic);
}

968
node_modules/optimism/src/tests/api.ts generated vendored Normal file
View File

@@ -0,0 +1,968 @@
import * as assert from "assert";
import { createHash } from "crypto";
import {
wrap,
defaultMakeCacheKey,
OptimisticWrapperFunction,
CommonCache,
} from "../index";
import { equal } from '@wry/equality';
import { wrapYieldingFiberMethods } from '@wry/context';
import { dep } from "../dep";
import { permutations } from "./test-utils";
type NumThunk = OptimisticWrapperFunction<[], number>;
describe("optimism", function () {
it("sanity", function () {
assert.strictEqual(typeof wrap, "function");
assert.strictEqual(typeof defaultMakeCacheKey, "function");
});
it("works with single functions", function () {
const test = wrap(function (x: string) {
return x + salt;
}, {
makeCacheKey: function (x: string) {
return x;
}
});
let salt = "salt";
assert.strictEqual(test("a"), "asalt");
salt = "NaCl";
assert.strictEqual(test("a"), "asalt");
assert.strictEqual(test("b"), "bNaCl");
test.dirty("a");
assert.strictEqual(test("a"), "aNaCl");
});
it("can manually specify a cache instance", () => {
class Cache<K, V> implements CommonCache<K, V> {
private _cache = new Map<K, V>()
has = this._cache.has.bind(this._cache);
get = this._cache.get.bind(this._cache);
delete = this._cache.delete.bind(this._cache);
get size(){ return this._cache.size }
set(key: K, value: V): V {
this._cache.set(key, value);
return value;
}
clean(){};
}
const cache = new Cache<String, any>();
const wrapped = wrap(
(obj: { value: string }) => obj.value + " transformed",
{
cache,
makeCacheKey(obj) {
return obj.value;
},
}
);
assert.ok(cache instanceof Cache);
assert.strictEqual(wrapped({ value: "test" }), "test transformed");
assert.strictEqual(wrapped({ value: "test" }), "test transformed");
cache.get("test").value[0] = "test modified";
assert.strictEqual(wrapped({ value: "test" }), "test modified");
});
it("can manually specify a cache constructor", () => {
class Cache<K, V> implements CommonCache<K, V> {
private _cache = new Map<K, V>()
has = this._cache.has.bind(this._cache);
get = this._cache.get.bind(this._cache);
delete = this._cache.delete.bind(this._cache);
get size(){ return this._cache.size }
set(key: K, value: V): V {
this._cache.set(key, value);
return value;
}
clean(){};
}
const wrapped = wrap(
(obj: { value: string }) => obj.value + " transformed",
{
cache: Cache,
makeCacheKey(obj) {
return obj.value;
},
}
);
assert.ok(wrapped.options.cache instanceof Cache);
assert.strictEqual(wrapped({ value: "test" }), "test transformed");
assert.strictEqual(wrapped({ value: "test" }), "test transformed");
wrapped.options.cache.get("test").value[0] = "test modified";
assert.strictEqual(wrapped({ value: "test" }), "test modified");
});
it("works with two layers of functions", function () {
const files: { [key: string]: string } = {
"a.js": "a",
"b.js": "b"
};
const fileNames = Object.keys(files);
const read = wrap(function (path: string) {
return files[path];
});
const hash = wrap(function (paths: string[]) {
const h = createHash("sha1");
paths.forEach(function (path) {
h.update(read(path));
});
return h.digest("hex");
});
const hash1 = hash(fileNames);
files["a.js"] += "yy";
const hash2 = hash(fileNames);
read.dirty("a.js");
const hash3 = hash(fileNames);
files["b.js"] += "ee";
read.dirty("b.js");
const hash4 = hash(fileNames);
assert.strictEqual(hash1, hash2);
assert.notStrictEqual(hash1, hash3);
assert.notStrictEqual(hash1, hash4);
assert.notStrictEqual(hash3, hash4);
});
it("works with subscription functions", function () {
let dirty: () => void;
let sep = ",";
const unsubscribed = Object.create(null);
const test = wrap(function (x: string) {
return [x, x, x].join(sep);
}, {
max: 1,
subscribe: function (x: string) {
dirty = function () {
test.dirty(x);
};
delete unsubscribed[x];
return function () {
unsubscribed[x] = true;
};
}
});
assert.strictEqual(test("a"), "a,a,a");
assert.strictEqual(test("b"), "b,b,b");
assert.deepEqual(unsubscribed, { a: true });
assert.strictEqual(test("c"), "c,c,c");
assert.deepEqual(unsubscribed, {
a: true,
b: true
});
sep = ":";
assert.strictEqual(test("c"), "c,c,c");
assert.deepEqual(unsubscribed, {
a: true,
b: true
});
dirty!();
assert.strictEqual(test("c"), "c:c:c");
assert.deepEqual(unsubscribed, {
a: true,
b: true
});
assert.strictEqual(test("d"), "d:d:d");
assert.deepEqual(unsubscribed, {
a: true,
b: true,
c: true
});
});
// The fibers coroutine library no longer works with Node.js v16.
it.skip("is not confused by fibers", function () {
const Fiber = wrapYieldingFiberMethods(require("fibers"));
const order = [];
let result1 = "one";
let result2 = "two";
const f1 = new Fiber(function () {
order.push(1);
const o1 = wrap(function () {
Fiber.yield();
return result1;
});
order.push(2);
assert.strictEqual(o1(), "one");
order.push(3);
result1 += ":dirty";
assert.strictEqual(o1(), "one");
order.push(4);
Fiber.yield();
order.push(5);
assert.strictEqual(o1(), "one");
order.push(6);
o1.dirty();
order.push(7);
assert.strictEqual(o1(), "one:dirty");
order.push(8);
assert.strictEqual(o2(), "two:dirty");
order.push(9);
});
result2 = "two"
const o2 = wrap(function () {
return result2;
});
order.push(0);
f1.run();
assert.deepEqual(order, [0, 1, 2]);
// The primary goal of this test is to make sure this call to o2()
// does not register a dirty-chain dependency for o1.
assert.strictEqual(o2(), "two");
f1.run();
assert.deepEqual(order, [0, 1, 2, 3, 4]);
// If the call to o2() captured o1() as a parent, then this o2.dirty()
// call will report the o1() call dirty, which is not what we want.
result2 += ":dirty";
o2.dirty();
f1.run();
// The call to o1() between order.push(5) and order.push(6) should not
// yield, because it should still be cached, because it should not be
// dirty. However, the call to o1() between order.push(7) and
// order.push(8) should yield, because we call o1.dirty() explicitly,
// which is why this assertion stops at 7.
assert.deepEqual(order, [0, 1, 2, 3, 4, 5, 6, 7]);
f1.run();
assert.deepEqual(order, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
});
it("marks evicted cache entries dirty", function () {
let childSalt = "*";
let child = wrap(function (x: string) {
return x + childSalt;
}, { max: 1 });
let parentSalt = "^";
const parent = wrap(function (x: string) {
return child(x) + parentSalt;
});
assert.strictEqual(parent("asdf"), "asdf*^");
childSalt = "&";
parentSalt = "%";
assert.strictEqual(parent("asdf"), "asdf*^");
assert.strictEqual(child("zxcv"), "zxcv&");
assert.strictEqual(parent("asdf"), "asdf&%");
});
it("handles children throwing exceptions", function () {
const expected = new Error("oyez");
const child = wrap(function () {
throw expected;
});
const parent = wrap(function () {
try {
child();
} catch (e) {
return e;
}
});
assert.strictEqual(parent(), expected);
assert.strictEqual(parent(), expected);
child.dirty();
assert.strictEqual(parent(), expected);
parent.dirty();
assert.strictEqual(parent(), expected);
});
it("reports clean children to correct parents", function () {
let childResult = "a";
const child = wrap(function () {
return childResult;
});
const parent = wrap(function (x: any) {
return child() + x;
});
assert.strictEqual(parent(1), "a1");
assert.strictEqual(parent(2), "a2");
childResult = "b";
child.dirty();
// If this call to parent(1) mistakenly reports child() as clean to
// parent(2), then the second assertion will fail by returning "a2".
assert.strictEqual(parent(1), "b1");
assert.strictEqual(parent(2), "b2");
});
it("supports object cache keys", function () {
let counter = 0;
const wrapped = wrap(function (a: any, b: any) {
return counter++;
});
const a = {};
const b = {};
// Different combinations of distinct object references should
// increment the counter.
assert.strictEqual(wrapped(a, a), 0);
assert.strictEqual(wrapped(a, b), 1);
assert.strictEqual(wrapped(b, a), 2);
assert.strictEqual(wrapped(b, b), 3);
// But the same combinations of arguments should return the same
// cached values when passed again.
assert.strictEqual(wrapped(a, a), 0);
assert.strictEqual(wrapped(a, b), 1);
assert.strictEqual(wrapped(b, a), 2);
assert.strictEqual(wrapped(b, b), 3);
});
it("supports falsy non-void cache keys", function () {
let callCount = 0;
const wrapped = wrap((key: number | string | null | boolean | undefined) => {
++callCount;
return key;
}, {
makeCacheKey(key) {
return key;
},
});
assert.strictEqual(wrapped(0), 0);
assert.strictEqual(callCount, 1);
assert.strictEqual(wrapped(0), 0);
assert.strictEqual(callCount, 1);
assert.strictEqual(wrapped(""), "");
assert.strictEqual(callCount, 2);
assert.strictEqual(wrapped(""), "");
assert.strictEqual(callCount, 2);
assert.strictEqual(wrapped(null), null);
assert.strictEqual(callCount, 3);
assert.strictEqual(wrapped(null), null);
assert.strictEqual(callCount, 3);
assert.strictEqual(wrapped(false), false);
assert.strictEqual(callCount, 4);
assert.strictEqual(wrapped(false), false);
assert.strictEqual(callCount, 4);
assert.strictEqual(wrapped(0), 0);
assert.strictEqual(wrapped(""), "");
assert.strictEqual(wrapped(null), null);
assert.strictEqual(wrapped(false), false);
assert.strictEqual(callCount, 4);
assert.strictEqual(wrapped(1), 1);
assert.strictEqual(wrapped("oyez"), "oyez");
assert.strictEqual(wrapped(true), true);
assert.strictEqual(callCount, 7);
assert.strictEqual(wrapped(void 0), void 0);
assert.strictEqual(wrapped(void 0), void 0);
assert.strictEqual(wrapped(void 0), void 0);
assert.strictEqual(callCount, 10);
});
it("detects problematic cycles", function () {
const self: NumThunk = wrap(function () {
return self() + 1;
});
const mutualA: NumThunk = wrap(function () {
return mutualB() + 1;
});
const mutualB: NumThunk = wrap(function () {
return mutualA() + 1;
});
function check(fn: typeof self) {
try {
fn();
throw new Error("should not get here");
} catch (e: any) {
assert.strictEqual(e.message, "already recomputing");
}
// Try dirtying the function, now that there's a cycle in the Entry
// graph. This should succeed.
fn.dirty();
}
check(self);
check(mutualA);
check(mutualB);
let returnZero = true;
const fn: NumThunk = wrap(function () {
if (returnZero) {
returnZero = false;
return 0;
}
returnZero = true;
return fn() + 1;
});
assert.strictEqual(fn(), 0);
assert.strictEqual(returnZero, false);
returnZero = true;
assert.strictEqual(fn(), 0);
assert.strictEqual(returnZero, true);
fn.dirty();
returnZero = false;
check(fn);
});
it("tolerates misbehaving makeCacheKey functions", function () {
type NumNum = OptimisticWrapperFunction<[number], number>;
let chaos = false;
let counter = 0;
const allOddsDep = wrap(() => ++counter);
const sumOdd: NumNum = wrap((n: number) => {
allOddsDep();
if (n < 1) return 0;
if (n % 2 === 1) {
return n + sumEven(n - 1);
}
return sumEven(n);
}, {
makeCacheKey(n) {
// Even though the computation completes, returning "constant" causes
// cycles in the Entry graph.
return chaos ? "constant" : n;
}
});
const sumEven: NumNum = wrap((n: number) => {
if (n < 1) return 0;
if (n % 2 === 0) {
return n + sumOdd(n - 1);
}
return sumOdd(n);
});
function check() {
sumEven.dirty(10);
sumOdd.dirty(10);
if (chaos) {
try {
sumOdd(10);
} catch (e: any) {
assert.strictEqual(e.message, "already recomputing");
}
try {
sumEven(10);
} catch (e: any) {
assert.strictEqual(e.message, "already recomputing");
}
} else {
assert.strictEqual(sumEven(10), 55);
assert.strictEqual(sumOdd(10), 55);
}
}
check();
allOddsDep.dirty();
sumEven.dirty(10);
check();
allOddsDep.dirty();
allOddsDep();
check();
chaos = true;
check();
allOddsDep.dirty();
allOddsDep();
check();
allOddsDep.dirty();
check();
chaos = false;
allOddsDep.dirty();
check();
chaos = true;
sumOdd.dirty(9);
sumOdd.dirty(7);
sumOdd.dirty(5);
check();
chaos = false;
check();
});
it("supports options.keyArgs", function () {
const sumNums = wrap((...args: any[]) => ({
sum: args.reduce(
(sum, arg) => typeof arg === "number" ? arg + sum : sum,
0,
) as number,
}), {
keyArgs(...args) {
return args.filter(arg => typeof arg === "number");
},
});
assert.strictEqual(sumNums().sum, 0);
assert.strictEqual(sumNums("asdf", true, sumNums).sum, 0);
const sumObj1 = sumNums(1, "zxcv", true, 2, false, 3);
assert.strictEqual(sumObj1.sum, 6);
// These results are === sumObj1 because the numbers involved are identical.
assert.strictEqual(sumNums(1, 2, 3), sumObj1);
assert.strictEqual(sumNums("qwer", 1, 2, true, 3, [3]), sumObj1);
assert.strictEqual(sumNums("backwards", 3, 2, 1).sum, 6);
assert.notStrictEqual(sumNums("backwards", 3, 2, 1), sumObj1);
sumNums.dirty(1, 2, 3);
const sumObj2 = sumNums(1, 2, 3);
assert.strictEqual(sumObj2.sum, 6);
assert.notStrictEqual(sumObj2, sumObj1);
assert.strictEqual(sumNums("a", 1, "b", 2, "c", 3), sumObj2);
});
it("supports wrap(fn, {...}).options to reflect input options", function () {
const keyArgs: () => [] = () => [];
function makeCacheKey() { return "constant"; }
function subscribe() {}
let normalizeCalls: [number, number][] = [];
function normalizeResult(newer: number, older: number) {
normalizeCalls.push([newer, older]);
return newer;
}
let counter1 = 0;
const wrapped = wrap(() => ++counter1, {
max: 10,
keyArgs,
makeCacheKey,
normalizeResult,
subscribe,
});
assert.strictEqual(wrapped.options.max, 10);
assert.strictEqual(wrapped.options.keyArgs, keyArgs);
assert.strictEqual(wrapped.options.makeCacheKey, makeCacheKey);
assert.strictEqual(wrapped.options.normalizeResult, normalizeResult);
assert.strictEqual(wrapped.options.subscribe, subscribe);
assert.deepEqual(normalizeCalls, []);
assert.strictEqual(wrapped(), 1);
assert.deepEqual(normalizeCalls, []);
assert.strictEqual(wrapped(), 1);
assert.deepEqual(normalizeCalls, []);
wrapped.dirty();
assert.deepEqual(normalizeCalls, []);
assert.strictEqual(wrapped(), 2);
assert.deepEqual(normalizeCalls, [[2, 1]]);
assert.strictEqual(wrapped(), 2);
wrapped.dirty();
assert.strictEqual(wrapped(), 3);
assert.deepEqual(normalizeCalls, [[2, 1], [3, 2]]);
assert.strictEqual(wrapped(), 3);
assert.deepEqual(normalizeCalls, [[2, 1], [3, 2]]);
assert.strictEqual(wrapped(), 3);
let counter2 = 0;
const wrappedWithDefaults = wrap(() => ++counter2);
assert.strictEqual(wrappedWithDefaults.options.max, Math.pow(2, 16));
assert.strictEqual(wrappedWithDefaults.options.keyArgs, void 0);
assert.strictEqual(typeof wrappedWithDefaults.options.makeCacheKey, "function");
assert.strictEqual(wrappedWithDefaults.options.normalizeResult, void 0);
assert.strictEqual(wrappedWithDefaults.options.subscribe, void 0);
});
it("tolerates cycles when propagating dirty/clean signals", function () {
let counter = 0;
const dep = wrap(() => ++counter);
const callChild = () => child();
let parentBody = callChild;
const parent = wrap(() => {
dep();
return parentBody();
});
const callParent = () => parent();
let childBody = () => "child";
const child = wrap(() => {
dep();
return childBody();
});
assert.strictEqual(parent(), "child");
childBody = callParent;
parentBody = () => "parent";
child.dirty();
assert.strictEqual(child(), "parent");
dep.dirty();
assert.strictEqual(child(), "parent");
});
it("is not confused by eviction during recomputation", function () {
const fib: OptimisticWrapperFunction<[number], number> =
wrap(function (n: number) {
if (n > 1) {
return fib(n - 1) + fib(n - 2);
}
return n;
}, {
max: 10
});
assert.strictEqual(fib.options.max, 10);
assert.strictEqual(fib(78), 8944394323791464);
assert.strictEqual(fib(68), 72723460248141);
assert.strictEqual(fib(58), 591286729879);
assert.strictEqual(fib(48), 4807526976);
assert.strictEqual(fib(38), 39088169);
assert.strictEqual(fib(28), 317811);
assert.strictEqual(fib(18), 2584);
assert.strictEqual(fib(8), 21);
});
it("allows peeking the current value", function () {
const sumFirst = wrap(function (n: number): number {
return n < 1 ? 0 : n + sumFirst(n - 1);
});
assert.strictEqual(sumFirst.peek(3), void 0);
assert.strictEqual(sumFirst.peek(2), void 0);
assert.strictEqual(sumFirst.peek(1), void 0);
assert.strictEqual(sumFirst.peek(0), void 0);
assert.strictEqual(sumFirst(3), 6);
assert.strictEqual(sumFirst.peek(3), 6);
assert.strictEqual(sumFirst.peek(2), 3);
assert.strictEqual(sumFirst.peek(1), 1);
assert.strictEqual(sumFirst.peek(0), 0);
assert.strictEqual(sumFirst.peek(7), void 0);
assert.strictEqual(sumFirst(10), 55);
assert.strictEqual(sumFirst.peek(9), 55 - 10);
assert.strictEqual(sumFirst.peek(8), 55 - 10 - 9);
assert.strictEqual(sumFirst.peek(7), 55 - 10 - 9 - 8);
sumFirst.dirty(7);
// Everything from 7 and above is now unpeekable.
assert.strictEqual(sumFirst.peek(10), void 0);
assert.strictEqual(sumFirst.peek(9), void 0);
assert.strictEqual(sumFirst.peek(8), void 0);
assert.strictEqual(sumFirst.peek(7), void 0);
// Since 6 < 7, its value is still cached.
assert.strictEqual(sumFirst.peek(6), 6 * 7 / 2);
});
it("allows forgetting entries", function () {
const ns: number[] = [];
const sumFirst = wrap(function (n: number): number {
ns.push(n);
return n < 1 ? 0 : n + sumFirst(n - 1);
});
function inclusiveDescendingRange(n: number, limit = 0) {
const range: number[] = [];
while (n >= limit) range.push(n--);
return range;
}
assert.strictEqual(sumFirst(10), 55);
assert.deepStrictEqual(ns, inclusiveDescendingRange(10));
assert.strictEqual(sumFirst.forget(6), true);
assert.strictEqual(sumFirst(4), 10);
assert.deepStrictEqual(ns, inclusiveDescendingRange(10));
assert.strictEqual(sumFirst(11), 66);
assert.deepStrictEqual(ns, [
...inclusiveDescendingRange(10),
...inclusiveDescendingRange(11, 6),
]);
assert.strictEqual(sumFirst.forget(3), true);
assert.strictEqual(sumFirst(7), 28);
assert.deepStrictEqual(ns, [
...inclusiveDescendingRange(10),
...inclusiveDescendingRange(11, 6),
...inclusiveDescendingRange(7, 3),
]);
assert.strictEqual(sumFirst.forget(123), false);
assert.strictEqual(sumFirst.forget(-1), false);
assert.strictEqual(sumFirst.forget("7" as any), false);
assert.strictEqual((sumFirst.forget as any)(6, 4), false);
});
it("allows forgetting entries by key", function () {
const ns: number[] = [];
const sumFirst = wrap(function (n: number): number {
ns.push(n);
return n < 1 ? 0 : n + sumFirst(n - 1);
}, {
makeCacheKey: function (x: number) {
return x * 2;
}
});
assert.strictEqual(sumFirst.options.makeCacheKey!(7), 14);
assert.strictEqual(sumFirst(10), 55);
/*
* Verify:
* 1- Calling forgetKey will remove the entry.
* 2- Calling forgetKey again will return false.
* 3- Callling forget on the same entry will return false.
*/
assert.strictEqual(sumFirst.forgetKey(6 * 2), true);
assert.strictEqual(sumFirst.forgetKey(6 * 2), false);
assert.strictEqual(sumFirst.forget(6), false);
/*
* Verify:
* 1- Calling forget will remove the entry.
* 2- Calling forget again will return false.
* 3- Callling forgetKey on the same entry will return false.
*/
assert.strictEqual(sumFirst.forget(7), true);
assert.strictEqual(sumFirst.forget(7), false);
assert.strictEqual(sumFirst.forgetKey(7 * 2), false);
/*
* Verify you can query an entry key.
*/
assert.strictEqual(sumFirst.getKey(9), 18);
assert.strictEqual(sumFirst.forgetKey(sumFirst.getKey(9)), true);
assert.strictEqual(sumFirst.forgetKey(sumFirst.getKey(9)), false);
assert.strictEqual(sumFirst.forget(9), false);
});
it("exposes optimistic.{size,options.cache.size} properties", function () {
const d = dep<string>();
const fib = wrap((n: number): number => {
d("shared");
return n > 1 ? fib(n - 1) + fib(n - 2) : n;
}, {
makeCacheKey(n) {
return n;
},
});
function size() {
assert.strictEqual(fib.options.cache.size, fib.size);
return fib.size;
}
assert.strictEqual(size(), 0);
assert.strictEqual(fib(0), 0);
assert.strictEqual(fib(1), 1);
assert.strictEqual(fib(2), 1);
assert.strictEqual(fib(3), 2);
assert.strictEqual(fib(4), 3);
assert.strictEqual(fib(5), 5);
assert.strictEqual(fib(6), 8);
assert.strictEqual(fib(7), 13);
assert.strictEqual(fib(8), 21);
assert.strictEqual(size(), 9);
fib.dirty(6);
// Merely dirtying an Entry does not remove it from the LRU cache.
assert.strictEqual(size(), 9);
fib.forget(6);
// Forgetting an Entry both dirties it and removes it from the LRU cache.
assert.strictEqual(size(), 8);
fib.forget(4);
assert.strictEqual(size(), 7);
// This way of calling d.dirty causes any parent Entry objects to be
// forgotten (removed from the LRU cache).
d.dirty("shared", "forget");
assert.strictEqual(size(), 0);
});
describe("wrapOptions.normalizeResult", function () {
it("can normalize array results", function () {
const normalizeArgs: [number[], number[]][] = [];
const range = wrap((n: number) => {
let result = [];
for (let i = 0; i < n; ++i) {
result[i] = i;
}
return result;
}, {
normalizeResult(newer, older) {
normalizeArgs.push([newer, older]);
return equal(newer, older) ? older : newer;
},
});
const r3a = range(3);
assert.deepStrictEqual(r3a, [0, 1, 2]);
// Nothing surprising, just regular caching.
assert.strictEqual(r3a, range(3));
// Force range(3) to be recomputed below.
range.dirty(3);
const r3b = range(3);
assert.deepStrictEqual(r3b, [0, 1, 2]);
assert.strictEqual(r3a, r3b);
assert.deepStrictEqual(normalizeArgs, [
[r3b, r3a],
]);
// Though r3a and r3b ended up ===, the normalizeResult callback should
// have been called with two !== arrays.
assert.notStrictEqual(
normalizeArgs[0][0],
normalizeArgs[0][1],
);
});
it("can normalize recursive array results", function () {
const range = wrap((n: number): number[] => {
if (n <= 0) return [];
return range(n - 1).concat(n - 1);
}, {
normalizeResult: (newer, older) => equal(newer, older) ? older : newer,
});
const ranges = [
range(0),
range(1),
range(2),
range(3),
range(4),
];
assert.deepStrictEqual(ranges[0], []);
assert.deepStrictEqual(ranges[1], [0]);
assert.deepStrictEqual(ranges[2], [0, 1]);
assert.deepStrictEqual(ranges[3], [0, 1, 2]);
assert.deepStrictEqual(ranges[4], [0, 1, 2, 3]);
const perms = permutations(ranges[4]);
assert.strictEqual(perms.length, 4 * 3 * 2 * 1);
// For each permutation of the range sizes, check that strict equality
// holds for r[i] and range(i) for all i after dirtying each number.
let count = 0;
perms.forEach(perm => {
perm.forEach(toDirty => {
range.dirty(toDirty);
perm.forEach(i => {
assert.strictEqual(ranges[i], range(i));
++count;
});
})
});
assert.strictEqual(count, perms.length * 4 * 4);
});
it("exceptions thrown by normalizeResult are ignored", function () {
const normalizeCalls: [string | number, string | number][] = [];
const maybeThrow = wrap((value: string | number, shouldThrow: boolean) => {
if (shouldThrow) throw value;
return value;
}, {
makeCacheKey(value, shouldThrow) {
return JSON.stringify({
// Coerce the value to a string so we can trigger normalizeResult
// using either 2 or "2" below.
value: String(value),
shouldThrow,
});
},
normalizeResult(a, b) {
normalizeCalls.push([a, b]);
throw new Error("from normalizeResult (expected)");
},
});
assert.strictEqual(maybeThrow(1, false), 1);
assert.strictEqual(maybeThrow(2, false), 2);
maybeThrow.dirty(2, false);
assert.strictEqual(maybeThrow("2", false), "2");
assert.strictEqual(maybeThrow(2, false), "2");
maybeThrow.dirty(2, false);
assert.strictEqual(maybeThrow(2, false), 2);
assert.strictEqual(maybeThrow("2", false), 2);
assert.throws(
() => maybeThrow(3, true),
error => error === 3,
);
assert.throws(
() => maybeThrow("3", true),
// Still 3 because the previous maybeThrow(3, true) exception is cached.
error => error === 3,
);
maybeThrow.dirty(3, true);
assert.throws(
() => maybeThrow("3", true),
error => error === "3",
);
// Even though the exception thrown by normalizeResult was ignored, check
// that it was in fact called (twice).
assert.deepStrictEqual(normalizeCalls, [
["2", 2],
[2, "2"],
]);
});
});
});

110
node_modules/optimism/src/tests/cache.ts generated vendored Normal file
View File

@@ -0,0 +1,110 @@
import * as assert from "assert";
import { StrongCache as Cache } from "@wry/caches";
describe("least-recently-used cache", function () {
it("can hold lots of elements", function () {
const cache = new Cache();
const count = 1000000;
for (let i = 0; i < count; ++i) {
cache.set(i, String(i));
}
cache.clean();
assert.strictEqual((cache as any).map.size, count);
assert.ok(cache.has(0));
assert.ok(cache.has(count - 1));
assert.strictEqual(cache.get(43), "43");
});
it("evicts excess old elements", function () {
const max = 10;
const evicted = [];
const cache = new Cache(max, (value, key) => {
assert.strictEqual(String(key), value);
evicted.push(key);
});
const count = 100;
const keys = [];
for (let i = 0; i < count; ++i) {
cache.set(i, String(i));
keys.push(i);
}
cache.clean();
assert.strictEqual((cache as any).map.size, max);
assert.strictEqual(evicted.length, count - max);
for (let i = count - max; i < count; ++i) {
assert.ok(cache.has(i));
}
});
it("can cope with small max values", function () {
const cache = new Cache(2);
function check(...sequence: number[]) {
cache.clean();
let entry = (cache as any).newest;
const forwards = [];
while (entry) {
forwards.push(entry.key);
entry = entry.older;
}
assert.deepEqual(forwards, sequence);
const backwards = [];
entry = (cache as any).oldest;
while (entry) {
backwards.push(entry.key);
entry = entry.newer;
}
backwards.reverse();
assert.deepEqual(backwards, sequence);
sequence.forEach(function (n) {
assert.strictEqual((cache as any).map.get(n).value, n + 1);
});
if (sequence.length > 0) {
assert.strictEqual((cache as any).newest.key, sequence[0]);
assert.strictEqual(
(cache as any).oldest.key,
sequence[sequence.length - 1]
);
}
}
cache.set(1, 2);
check(1);
cache.set(2, 3);
check(2, 1);
cache.set(3, 4);
check(3, 2);
cache.get(2);
check(2, 3);
cache.set(4, 5);
check(4, 2);
assert.strictEqual(cache.has(1), false);
assert.strictEqual(cache.get(2), 3);
assert.strictEqual(cache.has(3), false);
assert.strictEqual(cache.get(4), 5);
cache.delete(2);
check(4);
cache.delete(4);
check();
assert.strictEqual((cache as any).newest, null);
assert.strictEqual((cache as any).oldest, null);
});
});

208
node_modules/optimism/src/tests/context.ts generated vendored Normal file
View File

@@ -0,0 +1,208 @@
import * as assert from "assert";
import {
wrap,
setTimeout,
asyncFromGen,
noContext,
nonReactive,
Slot,
} from '../index.js';
describe("asyncFromGen", function () {
it("is importable", function () {
assert.strictEqual(typeof asyncFromGen, "function");
});
it("works like an async function", asyncFromGen(function*(): Generator<
number | Promise<number>,
Promise<string>,
number
> {
let sum = 0;
const limit = yield new Promise<number>(resolve => {
setTimeout(() => resolve(10), 10);
});
for (let i = 0; i < limit; ++i) {
sum += yield i + 1;
}
assert.strictEqual(sum, 55);
return Promise.resolve("ok");
}));
it("properly handles exceptions", async function () {
const fn = asyncFromGen(function*(throwee?: object): Generator<
Promise<string> | object,
string,
string
> {
const result = yield Promise.resolve("ok");
if (throwee) {
throw yield throwee;
}
return result;
});
const okPromise = fn();
const expected = {};
const koPromise = fn(expected);
assert.strictEqual(await okPromise, "ok");
try {
await koPromise;
throw new Error("not reached");
} catch (error) {
assert.strictEqual(error, expected);
}
try {
await fn(Promise.resolve("oyez"));
throw new Error("not reached");
} catch (thrown) {
assert.strictEqual(thrown, "oyez");
}
const catcher = asyncFromGen(function*() {
try {
yield Promise.reject(new Error("expected"));
throw new Error("not reached");
} catch (error: any) {
assert.strictEqual(error.message, "expected");
}
return "ok";
});
return catcher().then(result => {
assert.strictEqual(result, "ok");
});
});
it("can be cached", async function () {
let parentCounter = 0;
const parent = wrap(asyncFromGen(function*(x: number): Generator<
Promise<number>,
number,
number
> {
++parentCounter;
const a = yield new Promise<number>(resolve => setTimeout(() => {
resolve(child(x));
}, 10));
const b = yield new Promise<number>(resolve => setTimeout(() => {
resolve(child(x + 1));
}, 20));
return a * b;
}));
let childCounter = 0;
const child = wrap((x: number) => {
return ++childCounter;
});
assert.strictEqual(parentCounter, 0);
assert.strictEqual(childCounter, 0);
const parentPromise = parent(123);
assert.strictEqual(parentCounter, 1);
assert.strictEqual(await parentPromise, 2);
assert.strictEqual(childCounter, 2);
assert.strictEqual(parent(123), parentPromise);
assert.strictEqual(parentCounter, 1);
assert.strictEqual(childCounter, 2);
child.dirty(123);
assert.strictEqual(await parent(123), 3 * 2);
assert.strictEqual(parentCounter, 2);
assert.strictEqual(childCounter, 3);
assert.strictEqual(await parent(456), 4 * 5);
assert.strictEqual(parentCounter, 3);
assert.strictEqual(childCounter, 5);
assert.strictEqual(parent(666), parent(666));
assert.strictEqual(await parent(666), await parent(666));
assert.strictEqual(parentCounter, 4);
assert.strictEqual(childCounter, 7);
child.dirty(667);
assert.strictEqual(await parent(667), 8 * 9);
assert.strictEqual(await parent(667), 8 * 9);
assert.strictEqual(parentCounter, 5);
assert.strictEqual(childCounter, 9);
assert.strictEqual(await parent(123), 3 * 2);
assert.strictEqual(parentCounter, 5);
assert.strictEqual(childCounter, 9);
});
});
describe("noContext", function () {
it("prevents registering dependencies", function () {
let parentCounter = 0;
const parent = wrap(() => {
return [++parentCounter, noContext(child)];
});
let childCounter = 0;
const child = wrap(() => ++childCounter);
assert.deepEqual(parent(), [1, 1]);
assert.deepEqual(parent(), [1, 1]);
parent.dirty();
assert.deepEqual(parent(), [2, 1]);
// Calling child.dirty() does not dirty the parent:
child.dirty();
assert.deepEqual(parent(), [2, 1]);
parent.dirty();
assert.deepEqual(parent(), [3, 2]);
assert.deepEqual(parent(), [3, 2]);
parent.dirty();
assert.deepEqual(parent(), [4, 2]);
});
});
describe("nonReactive", function () {
const otherSlot = new Slot<string>();
it("censors only optimism-related context", function () {
let innerCounter = 0;
const inner = wrap(() => ++innerCounter);
const outer = wrap(() => ({
fromInner: nonReactive(() => inner()),
fromOther: nonReactive(() => otherSlot.getValue()),
}));
assert.strictEqual(otherSlot.getValue(), undefined);
otherSlot.withValue("preserved", () => {
assert.deepEqual(outer(), { fromInner: 1, fromOther: "preserved" });
assert.deepEqual(outer(), { fromInner: 1, fromOther: "preserved" });
inner.dirty();
assert.deepEqual(outer(), { fromInner: 1, fromOther: "preserved" });
assert.strictEqual(inner(), 2);
outer.dirty();
assert.deepEqual(outer(), { fromInner: 2, fromOther: "preserved" });
});
assert.strictEqual(otherSlot.getValue(), undefined);
});
it("same test using noContext, for comparison", function () {
let innerCounter = 0;
const inner = wrap(() => ++innerCounter);
const outer = wrap(() => ({
fromInner: noContext(inner),
fromOther: noContext(() => otherSlot.getValue()),
}));
assert.strictEqual(otherSlot.getValue(), undefined);
otherSlot.withValue("preserved", () => {
assert.deepEqual(outer(), { fromInner: 1, fromOther: void 0 });
assert.deepEqual(outer(), { fromInner: 1, fromOther: void 0 });
inner.dirty();
assert.deepEqual(outer(), { fromInner: 1, fromOther: void 0 });
assert.strictEqual(inner(), 2);
outer.dirty();
assert.deepEqual(outer(), { fromInner: 2, fromOther: void 0 });
});
assert.strictEqual(otherSlot.getValue(), undefined);
});
});

183
node_modules/optimism/src/tests/deps.ts generated vendored Normal file
View File

@@ -0,0 +1,183 @@
import * as assert from "assert";
import { wrap, dep } from "../index";
describe("OptimisticDependencyFunction<TKey>", () => {
it("can dirty OptimisticWrapperFunctions", () => {
const numberDep = dep<number>();
const stringDep = dep<string>();
let callCount = 0;
const fn = wrap((n: number, s: string) => {
numberDep(n);
stringDep(s);
++callCount;
return s.repeat(n);
});
assert.strictEqual(fn(0, "oyez"), "");
assert.strictEqual(callCount, 1);
assert.strictEqual(fn(1, "oyez"), "oyez");
assert.strictEqual(callCount, 2);
assert.strictEqual(fn(2, "oyez"), "oyezoyez");
assert.strictEqual(callCount, 3);
assert.strictEqual(fn(0, "oyez"), "");
assert.strictEqual(fn(1, "oyez"), "oyez");
assert.strictEqual(fn(2, "oyez"), "oyezoyez");
assert.strictEqual(callCount, 3);
numberDep.dirty(0);
assert.strictEqual(fn(0, "oyez"), "");
assert.strictEqual(callCount, 4);
assert.strictEqual(fn(1, "oyez"), "oyez");
assert.strictEqual(callCount, 4);
assert.strictEqual(fn(2, "oyez"), "oyezoyez");
assert.strictEqual(callCount, 4);
stringDep.dirty("mlem");
assert.strictEqual(fn(0, "oyez"), "");
assert.strictEqual(callCount, 4);
stringDep.dirty("oyez");
assert.strictEqual(fn(2, "oyez"), "oyezoyez");
assert.strictEqual(callCount, 5);
assert.strictEqual(fn(1, "oyez"), "oyez");
assert.strictEqual(callCount, 6);
assert.strictEqual(fn(0, "oyez"), "");
assert.strictEqual(callCount, 7);
assert.strictEqual(fn(0, "oyez"), "");
assert.strictEqual(fn(1, "oyez"), "oyez");
assert.strictEqual(fn(2, "oyez"), "oyezoyez");
assert.strictEqual(callCount, 7);
});
it("should be forgotten when parent is recomputed", () => {
const d = dep<string>();
let callCount = 0;
let shouldDepend = true;
const parent = wrap((id: string) => {
if (shouldDepend) d(id);
return ++callCount;
});
assert.strictEqual(parent("oyez"), 1);
assert.strictEqual(parent("oyez"), 1);
assert.strictEqual(parent("mlem"), 2);
assert.strictEqual(parent("mlem"), 2);
d.dirty("mlem");
assert.strictEqual(parent("oyez"), 1);
assert.strictEqual(parent("mlem"), 3);
d.dirty("oyez");
assert.strictEqual(parent("oyez"), 4);
assert.strictEqual(parent("mlem"), 3);
parent.dirty("oyez");
shouldDepend = false;
assert.strictEqual(parent("oyez"), 5);
assert.strictEqual(parent("mlem"), 3);
d.dirty("oyez");
shouldDepend = true;
assert.strictEqual(parent("oyez"), 5);
assert.strictEqual(parent("mlem"), 3);
// This still has no effect because the previous call to parent("oyez")
// was cached.
d.dirty("oyez");
assert.strictEqual(parent("oyez"), 5);
assert.strictEqual(parent("mlem"), 3);
parent.dirty("oyez");
assert.strictEqual(parent("oyez"), 6);
assert.strictEqual(parent("mlem"), 3);
d.dirty("oyez");
assert.strictEqual(parent("oyez"), 7);
assert.strictEqual(parent("mlem"), 3);
parent.dirty("mlem");
shouldDepend = false;
assert.strictEqual(parent("oyez"), 7);
assert.strictEqual(parent("mlem"), 8);
d.dirty("oyez");
d.dirty("mlem");
assert.strictEqual(parent("oyez"), 9);
assert.strictEqual(parent("mlem"), 8);
d.dirty("oyez");
d.dirty("mlem");
assert.strictEqual(parent("oyez"), 9);
assert.strictEqual(parent("mlem"), 8);
shouldDepend = true;
parent.dirty("mlem");
assert.strictEqual(parent("oyez"), 9);
assert.strictEqual(parent("mlem"), 10);
d.dirty("oyez");
d.dirty("mlem");
assert.strictEqual(parent("oyez"), 9);
assert.strictEqual(parent("mlem"), 11);
});
it("supports subscribing and unsubscribing", function () {
let subscribeCallCount = 0;
let unsubscribeCallCount = 0;
let parentCallCount = 0;
function check(counts: {
subscribe: number;
unsubscribe: number;
parent: number;
}) {
assert.strictEqual(counts.subscribe, subscribeCallCount);
assert.strictEqual(counts.unsubscribe, unsubscribeCallCount);
assert.strictEqual(counts.parent, parentCallCount);
}
const d = dep({
subscribe(key: string) {
++subscribeCallCount;
return () => {
++unsubscribeCallCount;
};
},
});
assert.strictEqual(subscribeCallCount, 0);
assert.strictEqual(unsubscribeCallCount, 0);
const parent = wrap((key: string) => {
d(key);
return ++parentCallCount;
});
assert.strictEqual(parent("rawr"), 1);
check({ subscribe: 1, unsubscribe: 0, parent: 1 });
assert.strictEqual(parent("rawr"), 1);
check({ subscribe: 1, unsubscribe: 0, parent: 1 });
assert.strictEqual(parent("blep"), 2);
check({ subscribe: 2, unsubscribe: 0, parent: 2 });
assert.strictEqual(parent("rawr"), 1);
check({ subscribe: 2, unsubscribe: 0, parent: 2 });
assert.strictEqual(parent("blep"), 2);
check({ subscribe: 2, unsubscribe: 0, parent: 2 });
d.dirty("blep");
check({ subscribe: 2, unsubscribe: 1, parent: 2 });
assert.strictEqual(parent("rawr"), 1);
check({ subscribe: 2, unsubscribe: 1, parent: 2 });
d.dirty("blep"); // intentionally redundant
check({ subscribe: 2, unsubscribe: 1, parent: 2 });
assert.strictEqual(parent("blep"), 3);
check({ subscribe: 3, unsubscribe: 1, parent: 3 });
assert.strictEqual(parent("blep"), 3);
check({ subscribe: 3, unsubscribe: 1, parent: 3 });
d.dirty("rawr");
check({ subscribe: 3, unsubscribe: 2, parent: 3 });
assert.strictEqual(parent("blep"), 3);
check({ subscribe: 3, unsubscribe: 2, parent: 3 });
assert.strictEqual(parent("rawr"), 4);
check({ subscribe: 4, unsubscribe: 2, parent: 4 });
assert.strictEqual(parent("blep"), 3);
check({ subscribe: 4, unsubscribe: 2, parent: 4 });
});
});

76
node_modules/optimism/src/tests/exceptions.ts generated vendored Normal file
View File

@@ -0,0 +1,76 @@
import * as assert from "assert";
import { wrap } from "../index.js";
describe("exceptions", function () {
it("should be cached", function () {
const error = new Error("expected");
let threw = false;
function throwOnce() {
if (!threw) {
threw = true;
throw error;
}
return "already threw";
}
const wrapper = wrap(throwOnce);
try {
wrapper();
throw new Error("unreached");
} catch (e) {
assert.strictEqual(e, error);
}
try {
wrapper();
throw new Error("unreached");
} catch (e) {
assert.strictEqual(e, error);
}
wrapper.dirty();
assert.strictEqual(wrapper(), "already threw");
assert.strictEqual(wrapper(), "already threw");
wrapper.dirty();
assert.strictEqual(wrapper(), "already threw");
});
it("should memoize a throwing fibonacci function", function () {
const fib = wrap((n: number) => {
if (n < 2) throw n;
try {
fib(n - 1);
} catch (minusOne: any) {
try {
fib(n - 2);
} catch (minusTwo: any) {
throw minusOne + minusTwo;
}
}
throw new Error("unreached");
});
function check(n: number, expected: number) {
try {
fib(n);
throw new Error("unreached");
} catch (result) {
assert.strictEqual(result, expected);
}
}
check(78, 8944394323791464);
check(68, 72723460248141);
check(58, 591286729879);
check(48, 4807526976);
fib.dirty(28);
check(38, 39088169);
check(28, 317811);
check(18, 2584);
check(8, 21);
fib.dirty(20);
check(78, 8944394323791464);
check(10, 55);
});
});

90
node_modules/optimism/src/tests/key-trie.ts generated vendored Normal file
View File

@@ -0,0 +1,90 @@
import * as assert from "assert";
import { KeyTrie } from "../index";
describe("KeyTrie", function () {
it("can be imported", function () {
assert.strictEqual(typeof KeyTrie, "function");
});
it("can hold objects weakly", function () {
const trie = new KeyTrie<object>(true);
assert.strictEqual((trie as any).weakness, true);
const obj1 = {};
assert.strictEqual(
trie.lookup(obj1, 2, 3),
trie.lookup(obj1, 2, 3),
);
const obj2 = {};
assert.notStrictEqual(
trie.lookup(1, obj2),
trie.lookup(1, obj2, 3),
);
assert.strictEqual((trie as any).weak.has(obj1), true);
assert.strictEqual((trie as any).strong.has(obj1), false);
assert.strictEqual((trie as any).strong.get(1).weak.has(obj2), true);
assert.strictEqual((trie as any).strong.get(1).weak.get(obj2).strong.has(3), true);
});
it("can disable WeakMap", function () {
const trie = new KeyTrie<object>(false);
assert.strictEqual((trie as any).weakness, false);
const obj1 = {};
assert.strictEqual(
trie.lookup(obj1, 2, 3),
trie.lookup(obj1, 2, 3),
);
const obj2 = {};
assert.notStrictEqual(
trie.lookup(1, obj2),
trie.lookup(1, obj2, 3),
);
assert.strictEqual(typeof (trie as any).weak, "undefined");
assert.strictEqual((trie as any).strong.has(obj1), true);
assert.strictEqual((trie as any).strong.has(1), true);
assert.strictEqual((trie as any).strong.get(1).strong.has(obj2), true);
assert.strictEqual((trie as any).strong.get(1).strong.get(obj2).strong.has(3), true);
});
it("can produce data types other than Object", function () {
const symbolTrie = new KeyTrie(true, args => Symbol.for(args.join(".")));
const s123 = symbolTrie.lookup(1, 2, 3);
assert.strictEqual(s123.toString(), "Symbol(1.2.3)");
assert.strictEqual(s123, symbolTrie.lookup(1, 2, 3));
assert.strictEqual(s123, symbolTrie.lookupArray([1, 2, 3]));
const sNull = symbolTrie.lookup();
assert.strictEqual(sNull.toString(), "Symbol()");
const regExpTrie = new KeyTrie(true, args => new RegExp("^(" + args.join("|") + ")$"));
const rXYZ = regExpTrie.lookup("x", "y", "z");
assert.strictEqual(rXYZ.test("w"), false);
assert.strictEqual(rXYZ.test("x"), true);
assert.strictEqual(rXYZ.test("y"), true);
assert.strictEqual(rXYZ.test("z"), true);
assert.strictEqual(String(rXYZ), "/^(x|y|z)$/");
class Data {
constructor(public readonly args: any[]) {}
}
const dataTrie = new KeyTrie(true, args => new Data(args));
function checkData(...args: any[]) {
const data = dataTrie.lookupArray(args);
assert.strictEqual(data instanceof Data, true);
assert.notStrictEqual(data.args, args);
assert.deepEqual(data.args, args);
assert.strictEqual(data, dataTrie.lookup(...args));
assert.strictEqual(data, dataTrie.lookupArray(arguments));
return data;
}
const datas = [
checkData(),
checkData(1),
checkData(1, 2),
checkData(2),
checkData(2, 3),
checkData(true, "a"),
checkData(/asdf/i, "b", function oyez() {}),
];
// Verify that all Data objects are distinct.
assert.strictEqual(new Set(datas).size, datas.length);
});
});

7
node_modules/optimism/src/tests/main.ts generated vendored Normal file
View File

@@ -0,0 +1,7 @@
import "./api";
import "./deps";
import "./cache";
import "./key-trie";
import "./context";
import "./exceptions";
import "./performance";

86
node_modules/optimism/src/tests/performance.ts generated vendored Normal file
View File

@@ -0,0 +1,86 @@
import * as assert from "assert";
import { wrap, dep, KeyTrie } from "../index";
describe("performance", function () {
this.timeout(30000);
it("should be able to tolerate lots of Entry objects", function () {
let counter = 0;
const child = wrap((a: any, b: any) => counter++);
const parent = wrap((obj1: object, num: number, obj2: object) => {
child(obj1, counter);
child(counter, obj2);
return counter++;
});
for (let i = 0; i < 100000; ++i) {
parent({}, i, {});
}
});
const keys: object[] = [];
for (let i = 0; i < 100000; ++i) {
keys.push({ i });
}
it("should be able to tolerate lots of deps", function () {
const d = dep<object>();
const parent = wrap((id: number) => {
keys.forEach(d);
return id;
});
parent(1);
parent(2);
parent(3);
keys.forEach(key => d.dirty(key));
});
it("can speed up sorting with O(array.length) cache lookup", function () {
let counter = 0;
const trie = new KeyTrie(false);
const sort = wrap((array: number[]) => {
++counter;
return array.slice(0).sort();
}, {
makeCacheKey(array) {
return trie.lookupArray(array);
}
});
assert.deepEqual(sort([2, 1, 5, 4]), [1, 2, 4, 5]);
assert.strictEqual(counter, 1);
assert.strictEqual(
sort([2, 1, 5, 4]),
sort([2, 1, 5, 4]),
);
assert.strictEqual(counter, 1);
assert.deepEqual(sort([3, 2, 1]), [1, 2, 3]);
assert.strictEqual(counter, 2);
const bigArray: number[] = [];
for (let i = 0; i < 100000; ++i) {
bigArray.push(Math.round(Math.random() * 100));
}
const bigArrayCopy = bigArray.slice(0);
const rawSortStartTime = Date.now();
bigArrayCopy.sort();
const rawSortTime = Date.now() - rawSortStartTime;
assert.deepEqual(
sort(bigArray),
bigArrayCopy,
);
const cachedSortStartTime = Date.now();
const cached = sort(bigArray);
const cachedSortTime = Date.now() - cachedSortStartTime;
assert.deepEqual(cached, bigArrayCopy);
assert.ok(
cachedSortTime <= rawSortTime,
`cached: ${cachedSortTime}ms, raw: ${rawSortTime}ms`,
);
assert.strictEqual(counter, 3);
});
});

14
node_modules/optimism/src/tests/test-utils.ts generated vendored Normal file
View File

@@ -0,0 +1,14 @@
export function permutations<T>(array: T[], start = 0): T[][] {
if (start === array.length) return [[]];
const item = array[start];
const results: T[][] = [];
permutations<T>(array, start + 1).forEach(perm => {
perm.forEach((_, i) => {
const copy = perm.slice(0);
copy.splice(i, 0, item);
results.push(copy);
});
results.push(perm.concat(item));
});
return results;
}