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

Add cache.function method #5

Merged
merged 20 commits into from
Jan 6, 2020
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
35 changes: 35 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,38 @@ expectAssignable<Promise<boolean>>(cache.set('key', true));
expectAssignable<Promise<[boolean, string]>>(cache.set('key', [true, 'string']));
expectAssignable<Promise<Record<string, any[]>>>(cache.set('key', {wow: [true, 'string']}));
expectAssignable<Promise<number>>(cache.set('key', 1, 1));

const cachedPower = cache.function(async (n: number) => n ** 1000);
expectType<(n: number) => Promise<number>>(cachedPower);
expectType<number>(await cachedPower(1));

expectType<(n: string) => Promise<number>>(
cache.function(async (n: string) => Number(n))
);

expectType<(n: string) => Promise<number>>(
cache.function(async (n: string) => Number(n))
);

async function identity(x: string): Promise <string>;
async function identity(x: number): Promise <number>;
async function identity(x: number | string): Promise <number | string> {
return x;
}

expectType<Promise<number>>(cache.function(identity)(1));
expectType<Promise<string>>(cache.function(identity)('1'));
expectNotAssignable<Promise<string>>(cache.function(identity)(1));
expectNotAssignable<Promise<number>>(cache.function(identity)('1'));

expectType<(n: string) => Promise<number>>(
cache.function(async (n: string) => Number(n), {
expiration: 20
})
);

expectType<(date: Date) => Promise<string>>(
cache.function(async (date: Date) => String(date.getHours()), {
cacheKey: ([date]) => date.toLocaleString()
})
);
48 changes: 39 additions & 9 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,21 @@ const _remove = chrome.storage.local.remove.bind(chrome.storage.local);

async function has(key: string): Promise<boolean> {
const cachedKey = `cache:${key}`;
const values = await p<Cache>(_get, cachedKey);
return values[cachedKey] !== undefined;
return cachedKey in await p<Cache>(_get, cachedKey);
}

async function get<TValue extends Value>(key: string): Promise<TValue | undefined> {
const cachedKey = `cache:${key}`;
const values = await p<Cache<TValue>>(_get, cachedKey);
const value = values[cachedKey];
// If it's not in the cache, it's best to return "undefined"
if (value === undefined) {
return undefined;
// `undefined` means not in cache
return;
}

if (Date.now() > value.expiration) {
await p(_remove, cachedKey);
return undefined;
return;
}

return value.data;
Expand Down Expand Up @@ -84,15 +83,46 @@ async function purge(): Promise<void> {
}
}

// Automatically clear cache every day
if (isBackgroundPage()) {
setTimeout(purge, 60000); // Purge cache on launch, but wait a bit
setInterval(purge, 1000 * 3600 * 24);
interface MemoizedFunctionOptions<TArgs extends any[]> {
expiration?: number;
cacheKey?: (args: TArgs) => string;
}

function function_<
TValue extends Value,
TFunction extends (...args: any[]) => Promise<TValue>,
TArgs extends Parameters<TFunction>
>(
getter: TFunction,
options: MemoizedFunctionOptions<TArgs> = {}
): TFunction {
return (async (...args: TArgs) => {
const key = options.cacheKey ? options.cacheKey(args) : args[0] as string;
const cachedValue = await get<TValue>(key);
if (cachedValue !== undefined) {
return cachedValue;
}

const freshValue = await getter(...args);
await set<TValue>(key, freshValue, options.expiration);
return freshValue;
}) as TFunction;
}

function init(): void {
// Automatically clear cache every day
if (isBackgroundPage()) {
setTimeout(purge, 60000); // Purge cache on launch, but wait a bit
setInterval(purge, 1000 * 3600 * 24);
}
}

init();

export default {
has,
get,
set,
function: function_,
delete: delete_
};
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"module": "index.js",
"scripts": {
"build": "tsc",
"prepublishOnly": "tsc --silent",
"prepublishOnly": "tsc",
"test": "run-s build test:*",
"test:xo": "xo",
"test:tsd": "tsd; true",
Expand Down Expand Up @@ -64,12 +64,12 @@
"devDependencies": {
"@sindresorhus/tsconfig": "^0.6.0",
"@types/chrome": "0.0.91",
"@typescript-eslint/eslint-plugin": "^2.9.0",
"@typescript-eslint/parser": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
"eslint-config-xo-typescript": "^0.23.0",
"npm-run-all": "^4.1.5",
"tsd": "^0.11.0",
"typescript": "^3.5.0",
"typescript": "^3.7.3",
"xo": "*"
},
"type": "module"
Expand Down
68 changes: 64 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,24 @@ import cache from 'webext-storage-cache';
})();
```

The same code could be also written more effectively with `cache.function`:

```js
import cache from 'webext-storage-cache';

const cachedFunction = cache.function(someFunction, {
expiration: 3,
cacheKey: () => 'unique'
});

(async () => {
console.log(await cachedFunction());
})();
```

## API

Similar to a `Map()`, but **all methods a return a `Promise`.**
Similar to a `Map()`, but **all methods a return a `Promise`.** It also has a memoization method that hides the caching logic and makes it a breeze to use.

### cache.has(key)

Expand Down Expand Up @@ -80,11 +95,11 @@ Type: `string | number | boolean` or `array | object` of those three types

#### expiration

The number of days after which the cache item will expire.

Type: `number`
Type: `number`<br>
Default: 30

The number of days after which the cache item will expire.

### cache.delete(key)

Deletes the requested item from the cache.
Expand All @@ -93,6 +108,51 @@ Deletes the requested item from the cache.

Type: `string`

### cache.function(getter, options)

Caches the return value of the function based on the `cacheKey`. It works similarly to `lodash.memoize`:

```js
async function getHTML(url, options) {
const response = await fetch(url, options);
return response.text();
}

const cachedGetHTML = cache.function(getHTML);

const html = await cachedGetHTML('https://google.com', {});
// The HTML of google.com will be saved with the key 'https://google.com'
```

#### getter

Type: `async function` that returns a cacheable value.

#### options

##### cacheKey

Type: `function` that returns a string
Default: `function` that returns the first argument of the call

```js
const cachedOperate = cache.function(operate, {
cacheKey: args => args.join(',')
});

cachedOperate(1, 2, 3);
// The result of `operate(1, 2, 3)` will be stored in the key '1,2,3'
// Without a custom `cacheKey`, it would be stored in the key '1'
```


##### expiration

Type: `number`<br>
Default: 30

The number of days after which the cache item will expire.

## Related

* [webext-options-sync](https://github.com/fregante/webext-options-sync) - Helps you manage and autosave your extension's options.
Expand Down