Skip to content
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
62 changes: 38 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@

# amemo

**amemo** is an experimental drop-in, type safe, persistent (or not), zero-dependency memoization library.
**amemo** is an experimental drop-in, type-safe, persistent (or not), zero-dependency memoization library.

It could be used to save time and resources by caching the results of expensive function calls, such as paid or rate limited API calls.
It can be used to save time and resources by caching the results of expensive function calls, paid or rate-limited API calls.

An in memory cache is also provided for non-persistent caching for environments where fs is not available.
It is designed to work with deeply nested objecst such as API SDKs, sync or async methods.
Copy link

Copilot AI Jun 19, 2025

Choose a reason for hiding this comment

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

Correct the spelling of 'objecst' to 'objects' to improve clarity.

Suggested change
It is designed to work with deeply nested objecst such as API SDKs, sync or async methods.
It is designed to work with deeply nested objects such as API SDKs, sync or async methods.

Copilot uses AI. Check for mistakes.

An in-memory cache is also provided for non-persistent caching in environments where the file system is not available.

It works in both Node.js and browser environments, but FileCacheStore is only available in Node.js. In browser environments, you can use MemCacheStore or implement your own CacheStore interface. When MemCacheStore is used, the cache will not be persistent and will be lost when the page is reloaded.

> [!WARNING]
> If the function being cached has side effects (i.e., it modifies an input object), these side effects won't run when the function result is served from cache.

## Usage

Expand All @@ -16,13 +23,13 @@ import {amemo} from 'amemo';
const complexType = new ComplexType();
const memoizedType = amemo(complexType); // drop-in replacement
memoizedType.nested.method({a: 1, b: 2}); // This will be memoized
memoizedType.nested.method({a: 1, b: 2}); // Free real estate
memoizedType.nested.method({a: 1}); // Not **memoized**
memoizedType.nested.method({a: 1, b: 2}); // Cache hit - no execution
memoizedType.nested.method({a: 1}); // Different arguments - not cached
```

## API

Options to configure, if you choose to do so.
Configuration options, if you choose to customize the behavior:

```typescript
export type CacheProxyOpts = {
Expand All @@ -49,32 +56,39 @@ export type CacheProxyOpts = {
```typescript
export type FileCacheStoreOpts = {
// Location of the cache file
// Path will be recursively created if it doesn't exist
// Directory will be created recursively if it doesn't exist
// Default: './.amemo.json'
path?: string;

// If True the cache will be written to disk on every cache miss.
// If False the cache will be written manually by calling the save method.
// If true, the cache will be written to disk on every cache miss
// If false, the cache must be saved manually by calling the save() method
// Default: true
autoSave?: boolean;
};
```

## Performance

By default the library aims to be extremely easy to use and requires no configuration. It can be used as a drop in replacement for easy gains.
By default, the library aims to be extremely easy to use and requires no configuration. It can be used as a drop-in replacement for easy performance gains.

And it must be just fine for most use cases. However, if you are looking for more performance, you can configure the cache store to use a more performant cache store.
It should be sufficient for most use cases, given that cached operations inherently take long time, the caching mechanism cost should be negligible. However, if you need more performance, you can configure the cache store to use a more performant implementation.

### FileCacheStore

> [!WARNING]
> FileCacheStore tries to resolve all cached results that are promises.
>
> TODO: Add a timeout option
>
> If that proves to be a problem, turning auto save off might help.

#### Constructor

Reads and parses the cache file synchronously (once).
Reads and parses the cache file synchronously (once during initialization).

#### set

Writes to the cache file synchronously when autoSave is true. Otherwise, save() method must be called by user to actually commit the cache to the disk. If not, cache store will act like an in-memory cache.
Writes to the cache file synchronously when autoSave is true. Otherwise, the save() method must be called manually to commit the cache to disk. If not called, the cache store will act like an in-memory cache.

#### autoSave

Expand All @@ -83,32 +97,32 @@ import {amemo, FileCacheStore} from 'amemo';

const cacheStore = new FileCacheStore({autoSave: false});
const complexType = new ComplexType();
const memoizedType = amemo(complexType, {cacheStore}); // drop-in replacement
const memoizedType = amemo(complexType, {cacheStore});
memoizedType.nested.method({a: 1, b: 2}); // This will be memoized
memoizedType.nested.method({a: 1, b: 2}); // Free real estate
memoizedType.nested.method({a: 1}); // Not **memoized**
memoizedType.nested.method({a: 1, b: 2}); // Cache hit - no execution
memoizedType.nested.method({a: 1}); // Different arguments - not cached

// Save the cache to the disk
cacheStore.save(); // <-- Commit the cache to the disk, otherwise store will act like a in-memory cache
// Manually save the cache to disk
cacheStore.save(); // Commit the cache to disk, otherwise it acts like in-memory cache
```

#### MemCacheStore

You can also use an in-memory for non persistent caching.
You can also use an in-memory store for non-persistent caching:

```typescript
import {amemo, MemCacheStorage} from 'amemo';
import {amemo, MemCacheStore} from 'amemo';

const cacheStore = new MemCacheStorage();
const cacheStore = new MemCacheStore();
const complexType = new ComplexType();
const memoizedType = amemo(complexType, {cacheStore}); // drop-in replacement
const memoizedType = amemo(complexType, {cacheStore});
memoizedType.nested.method({a: 1, b: 2}); // This will be memoized
memoizedType.nested.method({a: 1, b: 2}); // Free real estate
memoizedType.nested.method({a: 1, b: 2}); // Cache hit - no execution
```

### Alternative implementations

Alternative implementation, say a browser compatible interface like LocalStorage or IndexedDB , can be implemented by implementing the CacheStore interface.
Alternative implementations, such as browser-compatible interfaces like LocalStorage or IndexedDB, can be created by implementing the CacheStore interface:

```typescript
export interface CacheStore {
Expand Down
2 changes: 1 addition & 1 deletion badges/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion dist/index.browser.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ declare module "cache-store" {
export type Entry = {
timestamp: number;
value: unknown;
promise?: boolean;
rejected?: boolean;
};
export const NotFound: unique symbol;
export const SECOND = 1000;
Expand Down Expand Up @@ -49,6 +51,10 @@ declare module "amemo.browser" {
export * from "mem-cache-store";
export * from "cache-proxy";
}
declare module "utils" {
export function replacer(key: string, value: any): any;
export function reviver(key: string, value: any): any;
}
declare module "file-cache-store" {
import { MemCacheStore } from "mem-cache-store";
export type FileCacheStoreOpts = {
Expand Down
2 changes: 1 addition & 1 deletion dist/index.node.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "amemo",
"version": "3.0.0",
"version": "3.1.0",
"description": "amemo is an experimental drop-in typesafe memoization library",
"repository": "https://github.com/enda-automation/amemo",
"main": "dist/index.node.js",
Expand Down Expand Up @@ -58,5 +58,6 @@
"standard-version": "^9.5.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.4"
}
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}
2 changes: 2 additions & 0 deletions src/cache-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export abstract class CacheStore {
export type Entry = {
timestamp: number;
value: unknown;
promise?: boolean;
rejected?: boolean;
};

export const NotFound = Symbol("NotFound");
Expand Down
14 changes: 11 additions & 3 deletions src/file-cache-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as fs from "fs";
import * as path from "path";

import { MemCacheStore } from "./mem-cache-store";
import { replacer, reviver } from "./utils";

export type FileCacheStoreOpts = {
path?: string;
Expand All @@ -20,7 +21,7 @@ export class FileCacheStore extends MemCacheStore {
return;
}
const data = fs.readFileSync(this.cacheFile, "utf8");
super.cache = JSON.parse(data);
super.cache = JSON.parse(data, reviver);
} catch (e) {
// ignore
}
Expand All @@ -36,12 +37,19 @@ export class FileCacheStore extends MemCacheStore {
async save() {
for (const [key, entry] of Object.entries(this.cache)) {
if (entry.value instanceof Promise) {
this.cache[key].value = await entry.value;
this.cache[key].promise = true;
try {
this.cache[key].value = await entry.value;
} catch (e) {
// Error object is not serializable, so we just remove it
this.cache[key].value = e;
this.cache[key].rejected = true;
}
}
}

fs.mkdirSync(path.dirname(this.cacheFile), { recursive: true });
fs.writeFileSync(this.cacheFile, JSON.stringify(this.cache));
fs.writeFileSync(this.cacheFile, JSON.stringify(this.cache, replacer));
}

clear() {
Expand Down
6 changes: 6 additions & 0 deletions src/mem-cache-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export class MemCacheStore implements CacheStore {
if (Date.now() - entry.timestamp > expire) {
return NotFound;
}
if (entry.promise) {
if (entry.rejected) {
return Promise.reject(entry.value);
}
return Promise.resolve(entry.value);
}
return entry.value;
}

Expand Down
Loading