Skip to content

Commit 58cfde7

Browse files
committed
updated docs
1 parent 294ca70 commit 58cfde7

File tree

13 files changed

+519
-236
lines changed

13 files changed

+519
-236
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ const keys = createKeys({
584584
})
585585
```
586586

587-
**Setup Database Schema:**
587+
**Setup Database Schema (PostgreSQL):**
588588

589589
```sql
590590
CREATE TABLE api_keys (
@@ -596,6 +596,8 @@ CREATE TABLE api_keys (
596596
CREATE INDEX api_keys_key_hash_idx ON api_keys("keyHash");
597597
```
598598

599+
> **Note**: The column uses camelCase (`keyHash`) by default. For MySQL, omit the quotes around `keyHash`.
600+
599601
### Custom Storage
600602

601603
```typescript

docs/content/docs/concepts/caching.mdx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ const keys = createKeys({
1919
})
2020
```
2121

22+
The in-memory cache includes automatic memory management:
23+
24+
- **Max Size**: Default limit of 10,000 entries to prevent memory leaks
25+
- **LRU Eviction**: When the cache is full, the oldest entry is evicted
26+
- **Automatic Cleanup**: Expired entries are removed every 60 seconds
27+
- **Graceful Shutdown**: Call `dispose()` to clean up timers when shutting down
28+
2229
### Redis Cache
2330

2431
```typescript
@@ -39,6 +46,7 @@ const keys = createKeys({
3946
2. **Cache Lookup**: Subsequent verifications check cache first
4047
3. **Cache Miss**: If not in cache, query storage and cache result
4148
4. **TTL**: Cached entries expire after the configured TTL
49+
5. **Validation**: Cached data is validated on retrieval to prevent corruption issues
4250

4351
## Cache Invalidation
4452

@@ -50,6 +58,7 @@ await keys.invalidateCache(keyHash)
5058
// - Key is revoked
5159
// - Key is disabled/enabled
5260
// - Key is rotated
61+
// - Corrupted cache data is detected
5362
```
5463

5564
## Skip Cache
@@ -61,10 +70,27 @@ const result = await keys.verify(key, {
6170
})
6271
```
6372

73+
## Advanced: Custom Memory Cache Options
74+
75+
If you need to customize the in-memory cache behavior, you can create a `MemoryCache` instance directly:
76+
77+
```typescript
78+
import { MemoryCache } from 'keypal'
79+
80+
const cache = new MemoryCache({
81+
maxSize: 50000, // Maximum entries (default: 10,000)
82+
cleanupInterval: 30000 // Cleanup interval in ms (default: 60,000)
83+
})
84+
85+
// Use with createKeys via the cache option
86+
// Or use directly for custom caching needs
87+
```
88+
6489
## Best Practices
6590

6691
1. **Enable caching in production** to reduce database load
6792
2. **Use Redis cache** for distributed applications
6893
3. **Set appropriate TTL** based on your needs (60-300 seconds is common)
6994
4. **Monitor cache hit rates** to optimize performance
95+
5. **Call `dispose()`** on MemoryCache when shutting down to prevent timer leaks
7096

docs/content/docs/storage/kysely.mdx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,27 @@ Create the table in your database:
2525
```sql
2626
CREATE TABLE api_keys (
2727
id TEXT PRIMARY KEY,
28-
key_hash TEXT UNIQUE NOT NULL,
28+
"keyHash" TEXT UNIQUE NOT NULL,
2929
metadata JSONB NOT NULL
3030
);
3131

32-
CREATE INDEX api_keys_key_hash_idx ON api_keys(key_hash);
32+
CREATE INDEX api_keys_key_hash_idx ON api_keys("keyHash");
3333
```
3434

3535
For MySQL, use `JSON` instead of `JSONB`:
3636

3737
```sql
3838
CREATE TABLE api_keys (
3939
id TEXT PRIMARY KEY,
40-
key_hash TEXT UNIQUE NOT NULL,
40+
keyHash TEXT UNIQUE NOT NULL,
4141
metadata JSON NOT NULL
4242
);
4343

44-
CREATE INDEX api_keys_key_hash_idx ON api_keys(key_hash);
44+
CREATE INDEX api_keys_key_hash_idx ON api_keys(keyHash);
4545
```
4646

47+
> **Note**: The column name uses camelCase (`keyHash`) to match the default adapter configuration. If you prefer snake_case (`key_hash`), you can customize it using the `apiKeyColumns` option.
48+
4749
## Setup
4850

4951
```typescript
@@ -94,7 +96,7 @@ Define your database types for full type safety:
9496
interface Database {
9597
api_keys: {
9698
id: string
97-
key_hash: string
99+
keyHash: string
98100
metadata: unknown // or a more specific type
99101
}
100102
}

docs/content/docs/storage/memory.mdx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,26 @@ const keys = createKeys({
2727

2828
## How It Works
2929

30-
Keys are stored in an in-memory `Map` structure:
31-
- Key lookups are O(1) hash table operations
32-
- No external dependencies
33-
- No network calls
34-
- Data exists only in the current process
30+
Keys are stored in an in-memory `Map` structure with optimized indexing:
31+
32+
- **O(1) Hash Lookups**: A dedicated hash index maps key hashes to IDs for instant verification
33+
- **O(1) Owner Lookups**: An owner index maps owner IDs to their key sets for fast listing
34+
- **Hash Collision Detection**: Automatically detects and rejects duplicate key hashes
35+
- **No external dependencies**: Everything runs in-process
36+
- **No network calls**: Zero latency for storage operations
37+
38+
### Internal Structure
39+
40+
```typescript
41+
// Primary storage: id -> ApiKeyRecord
42+
keys: Map<string, ApiKeyRecord>
43+
44+
// Hash index for O(1) verification: keyHash -> id
45+
hashIndex: Map<string, string>
46+
47+
// Owner index for O(1) listing: ownerId -> Set<id>
48+
ownerIndex: Map<string, Set<string>>
49+
```
3550

3651
## Important Notes
3752

docs/content/docs/storage/redis.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ const keys = createKeys({
9797

9898
**High performance**: Redis operations are extremely fast, making it ideal for high-throughput applications.
9999

100+
**Pipeline error checking**: All Redis pipeline operations validate individual command results, ensuring data integrity even when multiple operations run together.
101+
102+
**JSON validation**: Data retrieved from Redis is validated before use, protecting against corruption and ensuring type safety.
103+
100104
## Production Setup
101105

102106
For production, consider:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "keypal",
3-
"version": "0.0.212",
3+
"version": "0.0.213",
44
"description": "A TypeScript library for secure API key management with cryptographic hashing, expiration, scopes, and pluggable storage",
55
"type": "module",
66
"main": "./dist/index.mjs",

src/core/cache.test.ts

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { beforeEach, describe, expect, it, vi } from "vitest";
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
import { MemoryCache, RedisCache } from "./cache";
33

44
describe("MemoryCache", () => {
55
let cache: MemoryCache;
66

77
beforeEach(() => {
8-
cache = new MemoryCache();
8+
cache = new MemoryCache({ cleanupInterval: 60_000 });
9+
});
10+
11+
afterEach(() => {
12+
cache.dispose();
913
});
1014

1115
it("should store and retrieve values", () => {
@@ -58,6 +62,83 @@ describe("MemoryCache", () => {
5862
expect(cache.get("key1")).toBeNull();
5963
expect(cache.get("key2")).toBe("value2");
6064
});
65+
66+
it("should respect max size limit", () => {
67+
const smallCache = new MemoryCache({ maxSize: 3, cleanupInterval: 60_000 });
68+
69+
smallCache.set("key1", "value1", 60);
70+
smallCache.set("key2", "value2", 60);
71+
smallCache.set("key3", "value3", 60);
72+
expect(smallCache.size).toBe(3);
73+
74+
smallCache.set("key4", "value4", 60);
75+
expect(smallCache.size).toBe(3);
76+
expect(smallCache.get("key4")).toBe("value4");
77+
78+
smallCache.dispose();
79+
});
80+
81+
it("should evict expired entries before evicting by LRU", async () => {
82+
const smallCache = new MemoryCache({ maxSize: 2, cleanupInterval: 60_000 });
83+
84+
// biome-ignore lint/style/noMagicNumbers: Short TTL for test
85+
smallCache.set("expiring", "will expire", 0.05);
86+
smallCache.set("permanent", "stays", 60);
87+
88+
// biome-ignore lint/style/noMagicNumbers: Wait for expiry
89+
await new Promise((resolve) => setTimeout(resolve, 100));
90+
91+
smallCache.set("new", "entry", 60);
92+
93+
expect(smallCache.get("expiring")).toBeNull();
94+
expect(smallCache.get("permanent")).toBe("stays");
95+
expect(smallCache.get("new")).toBe("entry");
96+
97+
smallCache.dispose();
98+
});
99+
100+
it("should cleanup expired entries", async () => {
101+
// biome-ignore lint/style/noMagicNumbers: Short TTL for test
102+
cache.set("expiring1", "value1", 0.05);
103+
// biome-ignore lint/style/noMagicNumbers: Short TTL for test
104+
cache.set("expiring2", "value2", 0.05);
105+
cache.set("permanent", "value3", 60);
106+
107+
expect(cache.size).toBe(3);
108+
109+
// biome-ignore lint/style/noMagicNumbers: Wait for expiry
110+
await new Promise((resolve) => setTimeout(resolve, 100));
111+
112+
cache.cleanup();
113+
114+
expect(cache.size).toBe(1);
115+
expect(cache.get("permanent")).toBe("value3");
116+
});
117+
118+
it("should report size correctly", () => {
119+
expect(cache.size).toBe(0);
120+
cache.set("key1", "value1", 60);
121+
expect(cache.size).toBe(1);
122+
cache.set("key2", "value2", 60);
123+
expect(cache.size).toBe(2);
124+
cache.del("key1");
125+
expect(cache.size).toBe(1);
126+
});
127+
128+
it("should allow updating existing key without eviction", () => {
129+
const smallCache = new MemoryCache({ maxSize: 2, cleanupInterval: 60_000 });
130+
131+
smallCache.set("key1", "value1", 60);
132+
smallCache.set("key2", "value2", 60);
133+
expect(smallCache.size).toBe(2);
134+
135+
smallCache.set("key1", "updated", 60);
136+
expect(smallCache.size).toBe(2);
137+
expect(smallCache.get("key1")).toBe("updated");
138+
expect(smallCache.get("key2")).toBe("value2");
139+
140+
smallCache.dispose();
141+
});
61142
});
62143

63144
describe("RedisCache", () => {

src/core/cache.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,55 @@ export type Cache = {
66
del(key: string): Promise<void> | void;
77
};
88

9+
export type MemoryCacheOptions = {
10+
maxSize?: number;
11+
cleanupInterval?: number;
12+
};
13+
14+
// biome-ignore lint/style/noMagicNumbers: Default cache configuration
15+
const DEFAULT_MAX_SIZE = 10_000;
16+
// biome-ignore lint/style/noMagicNumbers: Default cleanup interval (1 minute)
17+
const DEFAULT_CLEANUP_INTERVAL = 60_000;
18+
919
export class MemoryCache implements Cache {
1020
private readonly cache = new Map<
1121
string,
1222
{ value: string; expires: number }
1323
>();
24+
private readonly maxSize: number;
25+
private cleanupTimer: ReturnType<typeof setInterval> | null = null;
26+
27+
constructor(options: MemoryCacheOptions = {}) {
28+
this.maxSize = options.maxSize ?? DEFAULT_MAX_SIZE;
29+
const cleanupInterval = options.cleanupInterval ?? DEFAULT_CLEANUP_INTERVAL;
30+
31+
this.cleanupTimer = setInterval(() => this.cleanup(), cleanupInterval);
32+
this.cleanupTimer.unref?.();
33+
}
1434

1535
get(key: string): string | null {
1636
const item = this.cache.get(key);
17-
if (!item) {
18-
return null;
19-
}
37+
if (!item) return null;
2038

2139
if (item.expires < Date.now()) {
2240
this.cache.delete(key);
2341
return null;
2442
}
25-
2643
return item.value;
2744
}
2845

2946
set(key: string, value: string, ttl = 60): void {
47+
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
48+
this.cleanup();
49+
if (this.cache.size >= this.maxSize) {
50+
const firstKey = this.cache.keys().next().value;
51+
if (firstKey !== undefined) this.cache.delete(firstKey);
52+
}
53+
}
54+
3055
this.cache.set(key, {
3156
value,
32-
// biome-ignore lint/style/noMagicNumbers: 1000ms to seconds
57+
// biome-ignore lint/style/noMagicNumbers: Convert seconds to milliseconds
3358
expires: Date.now() + ttl * 1000,
3459
});
3560
}
@@ -41,6 +66,24 @@ export class MemoryCache implements Cache {
4166
clear(): void {
4267
this.cache.clear();
4368
}
69+
70+
cleanup(): void {
71+
const now = Date.now();
72+
for (const [key, item] of this.cache) {
73+
if (item.expires < now) this.cache.delete(key);
74+
}
75+
}
76+
77+
dispose(): void {
78+
if (this.cleanupTimer) {
79+
clearInterval(this.cleanupTimer);
80+
this.cleanupTimer = null;
81+
}
82+
}
83+
84+
get size(): number {
85+
return this.cache.size;
86+
}
4487
}
4588

4689
export class RedisCache implements Cache {
@@ -51,7 +94,7 @@ export class RedisCache implements Cache {
5194
}
5295

5396
async get(key: string): Promise<string | null> {
54-
return await this.client.get(key);
97+
return this.client.get(key);
5598
}
5699

57100
async set(key: string, value: string, ttl = 60): Promise<void> {

0 commit comments

Comments
 (0)