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
74 changes: 74 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is **node-ts-cache**, a TypeScript/Node.js caching library featuring:

- Decorator-based caching (@Cache, @SyncCache, @MultiCache)
- Multiple storage backends (Memory, File System, Redis, LRU, etc.)
- Flexible expiration strategies (TTL-based with lazy/eager invalidation)

It's a monorepo using Lerna with independent versioning.

## Common Commands

### Building

```bash
npm run build # Build all packages
```

### Testing

```bash
npm test # Run all tests across all packages
```

### Linting and Type Checking

```bash
npm run lint # Run ESLint on all source files
npm run lint:fix # Run ESLint with auto-fix
```

### Formatting

```bash
npm run format # Check formatting with Prettier
npm run format:fix # Fix formatting with Prettier
```

## Before Committing

Always run these checks before committing:

1. `npm run lint` - Ensure no linting errors
2. `npm run format` - Ensure code is properly formatted
3. `npm test` - Ensure all tests pass
4. `npm run build` - Ensure the project builds successfully

## Project Structure

```
├── ts-cache/ # Core caching package (@node-ts-cache/core)
│ ├── src/
│ │ ├── decorator/ # @Cache, @SyncCache, @MultiCache decorators
│ │ ├── storage/ # MemoryStorage, FsJsonStorage
│ │ ├── strategy/ # ExpirationStrategy, key strategies
│ │ └── types/ # TypeScript interfaces
│ └── test/ # Test files
├── storages/ # Storage adapter packages
│ ├── lru/ # LRU cache storage
│ ├── redis/ # Redis storage (redis package v3.x)
│ ├── redisio/ # Redis storage (ioredis with compression)
│ ├── node-cache/ # node-cache storage
│ └── lru-redis/ # Two-tier LRU + Redis storage
```

## Testing Framework

- Uses Mocha with ts-node ESM loader
- Tests use Node's built-in `assert` module
- Mock Redis instances using `redis-mock` and `ioredis-mock`
227 changes: 227 additions & 0 deletions ts-cache/test/cache.decorator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,231 @@ describe('CacheDecorator', () => {
Assert.strictEqual(await strategy.getItem<string[]>('getUsersPromise'), data);
});
});

describe('DISABLE_CACHE_DECORATOR environment variable', () => {
afterEach(() => {
delete process.env.DISABLE_CACHE_DECORATOR;
});

it('Should skip caching when DISABLE_CACHE_DECORATOR is set', async () => {
process.env.DISABLE_CACHE_DECORATOR = 'true';

const disableStorage = new MemoryStorage();
const disableStrategy = new ExpirationStrategy(disableStorage);

class TestClassDisabled {
callCount = 0;

@Cache(disableStrategy, { ttl: 1000 })
public getUsers(): string[] {
this.callCount++;
return data;
}
}

const myClass = new TestClassDisabled();

await myClass.getUsers();
await myClass.getUsers();
await myClass.getUsers();

// Method should be called every time when cache is disabled
Assert.strictEqual(myClass.callCount, 3);
});

it('Should work normally when DISABLE_CACHE_DECORATOR is not set', async () => {
delete process.env.DISABLE_CACHE_DECORATOR;

const normalStorage = new MemoryStorage();
const normalStrategy = new ExpirationStrategy(normalStorage);

class TestClassNormal {
callCount = 0;

@Cache(normalStrategy, { ttl: 1000 })
public getUsers(): string[] {
this.callCount++;
return data;
}
}

const myClass = new TestClassNormal();

await myClass.getUsers();
await myClass.getUsers();
await myClass.getUsers();

// Method should be called only once when caching is enabled
Assert.strictEqual(myClass.callCount, 1);
});
});

describe('Cache error handling', () => {
it('Should handle cache read errors gracefully', async () => {
const failingStorage = {
getItem: () => {
throw new Error('Read error');
},
setItem: async () => {},
clear: async () => {}
};
const failStrategy = new ExpirationStrategy(failingStorage as unknown as MemoryStorage);

class TestClassReadFail {
callCount = 0;

@Cache(failStrategy, { ttl: 1000 })
public getUsers(): string[] {
this.callCount++;
return data;
}
}

const myClass = new TestClassReadFail();

// Should not throw, just log warning and continue
const result = await myClass.getUsers();
Assert.deepStrictEqual(result, data);
});

it('Should handle cache write errors gracefully', async () => {
const failingStorage = {
getItem: async () => undefined,
setItem: async () => {
throw new Error('Write error');
},
clear: async () => {}
};
const failStrategy = new ExpirationStrategy(failingStorage as unknown as MemoryStorage);

class TestClassWriteFail {
callCount = 0;

@Cache(failStrategy, { ttl: 1000 })
public getUsers(): string[] {
this.callCount++;
return data;
}
}

const myClass = new TestClassWriteFail();

// Should not throw, just log warning and continue
const result = await myClass.getUsers();
Assert.deepStrictEqual(result, data);
});
});

describe('Different argument types', () => {
const argStorage = new MemoryStorage();
const argStrategy = new ExpirationStrategy(argStorage);

beforeEach(async () => {
await argStrategy.clear();
});

class TestClassArgs {
callCount = 0;

@Cache(argStrategy, { ttl: 1000 })
public getWithArgs(...args: unknown[]): unknown[] {
this.callCount++;
return args;
}
}

it('Should cache with object arguments', async () => {
const myClass = new TestClassArgs();
const arg = { id: 1, name: 'test' };

await myClass.getWithArgs(arg);
await myClass.getWithArgs(arg);

Assert.strictEqual(myClass.callCount, 1);
});

it('Should cache with array arguments', async () => {
const myClass = new TestClassArgs();
const arg = [1, 2, 3];

await myClass.getWithArgs(arg);
await myClass.getWithArgs(arg);

Assert.strictEqual(myClass.callCount, 1);
});

it('Should differentiate between different arguments', async () => {
const myClass = new TestClassArgs();

await myClass.getWithArgs(1);
await myClass.getWithArgs(2);
await myClass.getWithArgs(3);

Assert.strictEqual(myClass.callCount, 3);
});

it('Should cache with multiple arguments', async () => {
const myClass = new TestClassArgs();

await myClass.getWithArgs('a', 1, true);
await myClass.getWithArgs('a', 1, true);

Assert.strictEqual(myClass.callCount, 1);
});
});

describe('Error propagation', () => {
it('Should propagate errors from decorated method', async () => {
const errorStorage = new MemoryStorage();
const errorStrategy = new ExpirationStrategy(errorStorage);

class TestClassError {
@Cache(errorStrategy, { ttl: 1000 })
public async throwingMethod(): Promise<string> {
throw new Error('Test error');
}
}

const myClass = new TestClassError();

await Assert.rejects(
async () => {
await myClass.throwingMethod();
},
{ message: 'Test error' }
);
});

it('Should not cache failed method calls', async () => {
const errorStorage = new MemoryStorage();
const errorStrategy = new ExpirationStrategy(errorStorage);

class TestClassErrorCount {
callCount = 0;

@Cache(errorStrategy, { ttl: 1000 })
public async throwingMethod(): Promise<string> {
this.callCount++;
throw new Error('Test error');
}
}

const myClass = new TestClassErrorCount();

try {
await myClass.throwingMethod();
} catch {
// Expected
}

try {
await myClass.throwingMethod();
} catch {
// Expected
}

// Each call should execute since errors are not cached
Assert.strictEqual(myClass.callCount, 2);
});
});
});
Loading