Skip to content

Commit

Permalink
Add cache.function method (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
fregante authored Jan 6, 2020
1 parent cbde631 commit e7872c8
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 17 deletions.
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

0 comments on commit e7872c8

Please sign in to comment.