Skip to content
Draft
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
3 changes: 3 additions & 0 deletions packages/cache/.mocharc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node-option:
- import=tsx
spec: test/**/*.test.ts
121 changes: 121 additions & 0 deletions packages/cache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# @dappnode/cache

A robust caching package for DappNode applications using LRU (Least Recently Used) cache with TTL (Time To Live) support.

## Features

- **LRU Cache**: Automatically evicts least recently used items when cache reaches maximum capacity
- **TTL Support**: Items expire after a configurable time period
- **Pre-configured Instances**: Ready-to-use cache instances for common DappNode use cases
- **TypeScript Support**: Full TypeScript type safety

## Installation

This package is part of the DappNode monorepo workspace and is automatically available to other packages.

## Usage

### Pre-configured Cache Instances

The package provides three pre-configured cache instances optimized for different use cases:

```typescript
import { dappstoreCache, contractAddressCache, stakerConfigCache, getOrSet } from "@dappnode/cache";

// Cache dappstore package release data (30 min TTL, 500 items max)
const release = await getOrSet(
dappstoreCache,
`release:${packageName}`,
async () => await dappnodeInstaller.getRelease(packageName)
);

// Cache smart contract addresses (24 hour TTL, 200 items max)
contractAddressCache.set(dnpName, contractAddress);
const address = contractAddressCache.get(dnpName);

// Cache staker configuration (10 min TTL, 50 items max)
const config = await getOrSet(
stakerConfigCache,
`stakerConfig:${network}`,
async () => await fetchStakerConfig(network)
);
```

### Cache Configuration

Each pre-configured cache has specific settings optimized for its use case:

- **dappstoreCache**: For package metadata and release information
- TTL: 30 minutes (dappstore data changes infrequently)
- Max items: 500 (accommodate many packages)

- **contractAddressCache**: For smart contract addresses
- TTL: 24 hours (contract addresses rarely change)
- Max items: 200 (limited number of contracts)

- **stakerConfigCache**: For staker configuration data
- TTL: 10 minutes (more frequent updates expected)
- Max items: 50 (limited number of networks)

### Cache Invalidation

Manually invalidate cache entries when data changes:

```typescript
// Invalidate specific cache entry
stakerConfigCache.delete(`stakerConfig:${network}`);

// Clear entire cache
dappstoreCache.clear();
```

### Custom Cache Instance

Create a custom cache instance using the `DappnodeCache` class:

```typescript
import { DappnodeCache } from "@dappnode/cache";

const customCache = new DappnodeCache({
max: 100, // Maximum 100 items
ttl: 1000 * 60 * 5, // 5 minutes TTL
updateAgeOnGet: true, // Update item age when accessed
});
```

## Integration Points

This caching solution is integrated into the following DappNode components:

1. **fetchDirectory** (`dappmanager`): Caches dappstore package release data to reduce Ethereum RPC and IPFS gateway calls
2. **updateMyPackages** (`daemons`): Caches smart contract addresses for auto-update functionality
3. **stakerConfigGet** (`dappmanager`): Caches staker configuration to reduce repeated database queries

## Performance Benefits

- **Reduced Network Calls**: Avoids repeated Ethereum RPC and IPFS gateway requests
- **Lower Latency**: Cached responses are returned immediately without network round-trips
- **Improved Reliability**: Reduces dependency on external services for frequently accessed data
- **Resource Efficiency**: Prevents redundant processing and API calls

## Implementation Details

- Built on top of `lru-cache` v11.x for robust LRU eviction policies
- Automatic TTL-based expiration ensures data freshness
- Thread-safe operations suitable for concurrent access
- Memory-efficient with configurable size limits
- TypeScript-first design with full type safety

## Testing

The package includes comprehensive tests covering:
- Basic cache operations (get, set, delete, clear)
- LRU eviction behavior
- TTL expiration
- getOrSet pattern functionality
- Integration scenarios

Run tests:
```bash
yarn test
```
27 changes: 27 additions & 0 deletions packages/cache/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@dappnode/cache",
"type": "module",
"version": "0.1.0",
"license": "GPL-3.0",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js"
},
"./package.json": "./package.json"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "TEST=true mocha --config ./.mocharc.yaml --recursive ./test/unit",
"dev": "tsc -w"
},
"dependencies": {
"lru-cache": "^11.0.0"
},
"devDependencies": {
"@types/mocha": "^10",
"mocha": "^10.7.0"
}
}
101 changes: 101 additions & 0 deletions packages/cache/src/DappnodeCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { LRUCache } from "lru-cache";

export interface CacheOptions {
max?: number;
ttl?: number; // Time to live in milliseconds
allowStale?: boolean;
updateAgeOnGet?: boolean;
updateAgeOnHas?: boolean;
}

/**
* A generic LRU cache wrapper for DappNode applications.
* Provides controlled cache eviction and performance improvements.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export class DappnodeCache<K extends {} = string, V extends {} = object> {
private cache: LRUCache<K, V>;

constructor(options: CacheOptions = {}) {
this.cache = new LRUCache<K, V>({
max: options.max || 100, // Default max 100 items
ttl: options.ttl || 1000 * 60 * 15, // Default 15 minutes TTL
allowStale: options.allowStale || false,
updateAgeOnGet: options.updateAgeOnGet || true,
updateAgeOnHas: options.updateAgeOnHas || true,
});
}

/**
* Get a value from the cache
*/
get(key: K): V | undefined {
return this.cache.get(key);
}

/**
* Set a value in the cache
*/
set(key: K, value: V): void {
this.cache.set(key, value);
}

/**
* Check if a key exists in the cache
*/
has(key: K): boolean {
return this.cache.has(key);
}

/**
* Delete a key from the cache
*/
delete(key: K): boolean {
return this.cache.delete(key);
}

/**
* Clear all items from the cache
*/
clear(): void {
this.cache.clear();
}

/**
* Get cache size
*/
get size(): number {
return this.cache.size;
}

/**
* Get or set a value with a factory function
* If the key exists, return the cached value
* If not, call the factory function, cache the result, and return it
*/
async getOrSet(key: K, factory: () => Promise<V>): Promise<V> {
const existing = this.get(key);
if (existing !== undefined) {
return existing;
}

const value = await factory();
this.set(key, value);
return value;
}

/**
* Get cache statistics
*/
getStats(): {
size: number;
max: number;
calculatedSize: number;
} {
return {
size: this.cache.size,
max: this.cache.max,
calculatedSize: this.cache.calculatedSize,
};
}
}
55 changes: 55 additions & 0 deletions packages/cache/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export { DappnodeCache, type CacheOptions } from "./DappnodeCache.js";

// Pre-configured cache instances for common use cases
import { LRUCache } from "lru-cache";

/**
* Cache for dappstore directory data
* Longer TTL since dappstore data doesn't change frequently
*/
export const dappstoreCache = new LRUCache<string, object>({
max: 500, // Allow more items for directory packages
ttl: 1000 * 60 * 30, // 30 minutes TTL
updateAgeOnGet: true,
});

/**
* Cache for smart contract addresses
* Very long TTL since contract addresses rarely change
*/
export const contractAddressCache = new LRUCache<string, string>({
max: 200,
ttl: 1000 * 60 * 60 * 24, // 24 hours TTL
updateAgeOnGet: true,
});

/**
* Cache for staker configuration data
* Shorter TTL for more frequent updates
*/
export const stakerConfigCache = new LRUCache<string, object>({
max: 50,
ttl: 1000 * 60 * 10, // 10 minutes TTL
updateAgeOnGet: true,
});

/**
* Get or set pattern for LRU cache
* If the key exists, return the cached value
* If not, call the factory function, cache the result, and return it
*/
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export async function getOrSet<T extends {}>(
cache: LRUCache<string, T>,
key: string,
factory: () => Promise<T>
): Promise<T> {
const existing = cache.get(key);
if (existing !== undefined) {
return existing;
}

const value = await factory();
cache.set(key, value);
return value;
}
Loading
Loading