Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use WeakMap and FinalizationRegistry when available. #132

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 59 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,17 @@ export type OptimisticWrapOptions<
// If provided, the subscribe function should either return an unsubscribe
// function or return nothing.
subscribe?: (...args: TArgs) => void | (() => any);
// If true, keys returned by makeCacheKey will be deleted from the LRU cache
// when they become unreachable. Defaults to true when WeakMap, WeakRef, and
// FinalizationRegistry are available. Otherwise always false.
useWeakKeys?: boolean,
};

const canUseWeakKeys =
typeof WeakMap === "function" &&
typeof WeakRef === "function" &&
typeof FinalizationRegistry === "function";

const caches = new Set<Cache<any, AnyEntry>>();

export function wrap<
Expand All @@ -125,10 +134,50 @@ export function wrap<
const makeCacheKey = options.makeCacheKey ||
makeDefaultMakeCacheKeyFunction<TKeyArgs, TCacheKey>();

// If options.useWeakKeys is true but canUseWeakKeys is false, the
// useWeakKeys variable must be false, since the FinalizationRegistry
// cannot be simulated or polyfilled.
const useWeakKeys = options.useWeakKeys === void 0
? canUseWeakKeys
: canUseWeakKeys && !!options.useWeakKeys;

// Optional WeakMap mapping object keys returned by makeCacheKey to
// empty object references that will be stored in the cache instead of
// the original key object. Undefined/unused if useWeakKeys is false.
// It's tempting to use WeakRef objects instead of empty objects, but
// we never actually need to call .deref(), and using WeakRef here
// noticeably slows down cache performance.
const weakRefs = useWeakKeys
? new WeakMap<object, {}>()
Comment on lines +144 to +151
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realizing that WeakRef is not necessary was the biggest (pleasant) surprise of this work.

: void 0;

// Optional registry allowing empty key references to be deleted from
// the cache after the original key objects become unreachable.
const registry = useWeakKeys
? new FinalizationRegistry(ref => cache.delete(ref))
: void 0;

// Wrapper for makeCacheKey that promotes object keys to empty reference
// objects, allowing the original key objects to be reclaimed by the
// garbage collector, which triggers the deletion of the references from
// the cache, using the registry, when useWeakKeys is true. Non-object
// keys returned by makeCacheKey (e.g. strings) are preserved.
function makeKey(keyArgs: IArguments | TKeyArgs) {
let key = makeCacheKey.apply(null, keyArgs as TKeyArgs);
if (useWeakKeys && key && typeof key === "object") {
let ref = weakRefs!.get(key)!;
if (!ref) {
weakRefs!.set(key, ref = {});
registry!.register(key, ref);
Comment on lines +167 to +171
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ! coercions here are safe because useWeakKeys implies weakRefs and registry must be defined.

}
key = ref;
}
return key;
}

const optimistic = function (): TResult {
const key = makeCacheKey.apply(
null,
keyArgs ? keyArgs.apply(null, arguments as any) : arguments as any
const key = makeKey(
keyArgs ? keyArgs.apply(null, arguments as any) : arguments
);

if (key === void 0) {
Expand Down Expand Up @@ -173,7 +222,7 @@ export function wrap<
}
optimistic.dirtyKey = dirtyKey;
optimistic.dirty = function dirty() {
dirtyKey(makeCacheKey.apply(null, arguments as any));
dirtyKey(makeKey(arguments));
};

function peekKey(key: TCacheKey) {
Expand All @@ -184,21 +233,23 @@ export function wrap<
}
optimistic.peekKey = peekKey;
optimistic.peek = function peek() {
return peekKey(makeCacheKey.apply(null, arguments as any));
return peekKey(makeKey(arguments));
};

function forgetKey(key: TCacheKey) {
return cache.delete(key);
}
optimistic.forgetKey = forgetKey;
optimistic.forget = function forget() {
return forgetKey(makeCacheKey.apply(null, arguments as any));
return forgetKey(makeKey(arguments));
};

optimistic.makeCacheKey = makeCacheKey;
optimistic.getKey = keyArgs ? function getKey() {
return makeCacheKey.apply(null, keyArgs.apply(null, arguments as any));
} : makeCacheKey as (...args: any[]) => TCacheKey;
return makeKey(keyArgs.apply(null, arguments as any));
} : function getKey() {
return makeKey(arguments);
};

return Object.freeze(optimistic);
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"sourceMap": true,
"declaration": true,
"importHelpers": true,
"lib": ["es2015"],
"lib": ["esnext"],
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change brings in the TypeScript typings for WeakRef and FinalizationRegistry.

"types": ["node", "mocha"],
"strict": true,
"noImplicitAny": true,
Expand Down