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

Update dependencies #46

Merged
merged 1 commit into from
Jun 11, 2024
Merged
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
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,19 @@
},
"dependencies": {
"@sindresorhus/to-milliseconds": "^2.0.0",
"type-fest": "^3.11.0",
"webext-detect-page": "^4.1.0",
"type-fest": "^4.20.0",
"webext-detect-page": "^5.0.1",
"webext-polyfill-kinda": "^1.0.2"
},
"devDependencies": {
"@sindresorhus/tsconfig": "^3.0.1",
"@types/chrome": "0.0.236",
"@types/sinon-chrome": "^2.2.11",
"@sindresorhus/tsconfig": "^5.0.0",
"@types/chrome": "0.0.268",
"@types/sinon-chrome": "^2.2.15",
"sinon-chrome": "^3.0.1",
"tsd": "^0.28.1",
"typescript": "^5.0.4",
"vitest": "^0.31.1",
"xo": "^0.54.2"
"tsd": "^0.31.0",
"typescript": "^5.4.5",
"vitest": "^1.6.0",
"xo": "^0.58.0"
},
"engines": {
"node": ">=18"
Expand Down
16 changes: 10 additions & 6 deletions source/cached-function.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable n/file-extension-in-import -- No alternative until this file is changed to .test.ts */
import {test, beforeEach, vi, assert, expect} from 'vitest';
import {
test, beforeEach, vi, assert, expect,
} from 'vitest';
import toMilliseconds from '@sindresorhus/to-milliseconds';
import CachedFunction from './cached-function.ts';

Expand All @@ -13,10 +15,12 @@ function createCache(daysFromToday, wholeCache) {
for (const [key, data] of Object.entries(wholeCache)) {
chrome.storage.local.get
.withArgs(key)
.yields({[key]: {
data,
maxAge: timeInTheFuture({days: daysFromToday}),
}});
.yields({
[key]: {
data,
maxAge: timeInTheFuture({days: daysFromToday}),
},
});
}
}

Expand Down Expand Up @@ -284,7 +288,7 @@ test('`updater` avoids concurrent function calls with complex arguments via cach

const updaterItem = new CachedFunction('spy', {
updater: spy,
cacheKey: ([fn, user]) => JSON.stringify([fn.name, user]),
cacheKey: ([function_, user]) => JSON.stringify([function_.name, user]),
});

expect(spy).not.toHaveBeenCalled();
Expand Down
54 changes: 27 additions & 27 deletions source/cached-function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,35 @@ import cache, {type CacheKey, _get, timeInTheFuture} from './legacy.js';
function getUserKey<Arguments extends any[]>(
name: string,
cacheKey: CacheKey<Arguments> | undefined,
args: Arguments,
arguments_: Arguments,
): string {
if (!cacheKey) {
if (args.length === 0) {
if (arguments_.length === 0) {
return name;
}

cacheKey = JSON.stringify;
}

return `${name}:${cacheKey(args)}`;
return `${name}:${cacheKey(arguments_)}`;
}

export default class CachedFunction<
// TODO: Review this type. While `undefined/null` can't be stored, the `updater` can return it to clear the cache
Updater extends ((...args: any[]) => Promise<CacheValue>),
Updater extends ((...arguments_: any[]) => Promise<CacheValue>),
ScopedValue extends AsyncReturnType<Updater>,
Arguments extends Parameters<Updater>,
> {
readonly maxAge: TimeDescriptor;
readonly staleWhileRevalidate: TimeDescriptor;

// The only reason this is not a constructor method is TypeScript: `get` must be `typeof Updater`
get = (async (...args: Arguments) => {
get = (async (...arguments_: Arguments) => {
const getSet = async (
userKey: string,
args: Arguments,
arguments__: Arguments,
): Promise<ScopedValue | undefined> => {
const freshValue = await this.#updater(...args);
const freshValue = await this.#updater(...arguments__);
if (freshValue === undefined) {
await cache.delete(userKey);
return;
Expand All @@ -45,28 +45,28 @@ export default class CachedFunction<
return cache.set(userKey, freshValue, {milliseconds}) as Promise<ScopedValue>;
};

const memoizeStorage = async (userKey: string, ...args: Arguments) => {
const memoizeStorage = async (userKey: string, ...arguments__: Arguments) => {
const cachedItem = await _get<ScopedValue>(userKey, false);
if (cachedItem === undefined || this.#shouldRevalidate?.(cachedItem.data)) {
return getSet(userKey, args);
return getSet(userKey, arguments__);
}

// When the expiration is earlier than the number of days specified by `staleWhileRevalidate`, it means `maxAge` has already passed and therefore the cache is stale.
if (timeInTheFuture(this.staleWhileRevalidate) > cachedItem.maxAge) {
setTimeout(getSet, 0, userKey, args);
setTimeout(getSet, 0, userKey, arguments__);
}

return cachedItem.data;
};

const userKey = getUserKey(this.name, this.#cacheKey, args);
const userKey = getUserKey(this.name, this.#cacheKey, arguments_);
const cached = this.#inFlightCache.get(userKey);
if (cached) {
// Avoid calling the same function twice while pending
return cached as Promise<ScopedValue>;
}

const promise = memoizeStorage(userKey, ...args);
const promise = memoizeStorage(userKey, ...arguments_);
this.#inFlightCache.set(userKey, promise);
const del = () => {
this.#inFlightCache.delete(userKey);
Expand All @@ -76,10 +76,10 @@ export default class CachedFunction<
return promise as Promise<ScopedValue>;
}) as unknown as Updater;

#updater: Updater;
#cacheKey: CacheKey<Arguments> | undefined;
#shouldRevalidate: ((cachedValue: ScopedValue) => boolean) | undefined;
#inFlightCache = new Map<string, Promise<ScopedValue | undefined>>();
readonly #updater: Updater;
readonly #cacheKey: CacheKey<Arguments> | undefined;
readonly #shouldRevalidate: ((cachedValue: ScopedValue) => boolean) | undefined;
readonly #inFlightCache = new Map<string, Promise<ScopedValue | undefined>>();

constructor(
public name: string,
Expand All @@ -98,35 +98,35 @@ export default class CachedFunction<
this.staleWhileRevalidate = options.staleWhileRevalidate ?? {days: 0};
}

async getCached(...args: Arguments): Promise<ScopedValue | undefined> {
const userKey = getUserKey<Arguments>(this.name, this.#cacheKey, args);
async getCached(...arguments_: Arguments): Promise<ScopedValue | undefined> {
const userKey = getUserKey<Arguments>(this.name, this.#cacheKey, arguments_);
return cache.get(userKey) as Promise<ScopedValue>;
}

async applyOverride(args: Arguments, value: ScopedValue) {
async applyOverride(arguments_: Arguments, value: ScopedValue) {
if (arguments.length === 0) {
throw new TypeError('Expected a value to be stored');
}

const userKey = getUserKey<Arguments>(this.name, this.#cacheKey, args);
const userKey = getUserKey<Arguments>(this.name, this.#cacheKey, arguments_);
return cache.set(userKey, value, this.maxAge);
}

async getFresh(...args: Arguments): Promise<ScopedValue> {
async getFresh(...arguments_: Arguments): Promise<ScopedValue> {
if (this.#updater === undefined) {
throw new TypeError('Cannot get fresh value without updater');
}

const userKey = getUserKey<Arguments>(this.name, this.#cacheKey, args);
return cache.set(userKey, await this.#updater(...args)) as Promise<ScopedValue>;
const userKey = getUserKey<Arguments>(this.name, this.#cacheKey, arguments_);
return cache.set(userKey, await this.#updater(...arguments_)) as Promise<ScopedValue>;
}

async delete(...args: Arguments) {
const userKey = getUserKey<Arguments>(this.name, this.#cacheKey, args);
async delete(...arguments_: Arguments) {
const userKey = getUserKey<Arguments>(this.name, this.#cacheKey, arguments_);
return cache.delete(userKey);
}

async isCached(...args: Arguments) {
return (await this.get(...args)) !== undefined;
async isCached(...arguments_: Arguments) {
return (await this.get(...arguments_)) !== undefined;
}
}
10 changes: 6 additions & 4 deletions source/cached-value.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ function createCache(daysFromToday, wholeCache) {
for (const [key, data] of Object.entries(wholeCache)) {
chrome.storage.local.get
.withArgs(key)
.yields({[key]: {
data,
maxAge: timeInTheFuture({days: daysFromToday}),
}});
.yields({
[key]: {
data,
maxAge: timeInTheFuture({days: daysFromToday}),
},
});
}
}

Expand Down
10 changes: 6 additions & 4 deletions source/legacy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ function createCache(daysFromToday, wholeCache) {
for (const [key, data] of Object.entries(wholeCache)) {
chrome.storage.local.get
.withArgs(key)
.yields({[key]: {
data,
maxAge: timeInTheFuture({days: daysFromToday}),
}});
.yields({
[key]: {
data,
maxAge: timeInTheFuture({days: daysFromToday}),
},
});
}
}

Expand Down
2 changes: 1 addition & 1 deletion source/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async function clear(): Promise<void> {
await deleteWithLogic();
}

export type CacheKey<Arguments extends unknown[]> = (args: Arguments) => string;
export type CacheKey<Arguments extends unknown[]> = (arguments_: Arguments) => string;

export type MemoizedFunctionOptions<Arguments extends unknown[], ScopedValue> = {
maxAge?: TimeDescriptor;
Expand Down
1 change: 0 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// eslint-disable-next-line n/file-extension-in-import -- Export map unsupported
import {defineConfig} from 'vitest/config';

export default defineConfig({
Expand Down
Loading