In-Memory Storage Adapter
Simple in-memory storage adapter for development and testing
In-Memory Storage Adapter
The createMemoryStorage function provides a simple in-memory storage adapter for Bantai. Perfect for development, testing, and prototyping. Data is stored in a Map and is not persisted across application restarts.
Installation
The in-memory storage adapter is available in both storage packages:
npm install @bantai-dev/with-storage @bantai-dev/core zodQuick Start
import { z } from 'zod';
import { defineContext, defineRule } from '@bantai-dev/core';
import { withStorage, createMemoryStorage } from '@bantai-dev/with-storage';
// 1. Define your schema
const userDataSchema = z.object({
userId: z.string(),
name: z.string(),
lastLogin: z.number(),
});
// 2. Create in-memory storage adapter
const storage = createMemoryStorage(userDataSchema);
// 3. Use with Bantai context
const context = withStorage(
defineContext(z.object({ userId: z.string() })),
storage
);
// 4. Use in rules
const userRule = defineRule(
context,
'get-user',
async (input, { tools }) => {
const user = await tools.storage.get(`user:${input.userId}`);
if (!user) {
return deny({ reason: 'User not found' });
}
return allow({ reason: `User: ${user.name}` });
}
);Features
- Schema Validation: Zod schema validation on write operations
- Atomic Updates: Lock-based atomic read-modify-write operations
- Simple API: Easy-to-use interface matching the
StorageAdapterinterface - Zero Dependencies: No external storage systems required
- Fast: In-memory operations are extremely fast
Limitations
⚠️ Important: This storage adapter is not suitable for production use:
- No Persistence: Data is lost when the application restarts
- Single Process: Data is not shared across multiple processes or instances
- No TTL Expiration: TTL values are accepted but not automatically enforced (for API compatibility)
- Memory Bound: All data must fit in memory
For production use, consider:
- Redis Storage - Production-ready distributed storage
- Custom storage adapter implementing the
StorageAdapterinterface
API Reference
createMemoryStorage
Creates an in-memory storage adapter that implements the StorageAdapter interface.
function createMemoryStorage<T extends z.ZodType>(
schema: T
): StorageAdapter<z.infer<T>>Parameters:
schema: Zod schema for validating stored values
Returns: StorageAdapter<z.infer<T>>
StorageAdapter Interface:
interface StorageAdapter<T> {
get(key: string): Promise<T | undefined>;
set(key: string, value: T, ttlMs?: number): Promise<void>;
delete(key: string): Promise<void>;
update?(
key: string,
updater: (current: T | undefined) => {
value: T;
ttlMs?: number;
} | null
): Promise<T | undefined>;
}Methods
get(key)
Retrieves a value by key. Returns undefined if not found.
const user = await storage.get('user:123');set(key, value, ttlMs?)
Sets a value with optional TTL (time-to-live) in milliseconds. The value is validated against the schema before storage.
Note: TTL is accepted for API compatibility but not automatically enforced. You'll need to manually check expiration if needed.
await storage.set('user:123', {
userId: '123',
name: 'John Doe',
lastLogin: Date.now(),
}, 3600000); // 1 hour TTL (not auto-expired)delete(key)
Deletes a value by key.
await storage.delete('user:123');update(key, updater)
Atomically updates a value using a lock mechanism. The updater function receives the current value and returns the new value with optional TTL, or null to delete.
const newValue = await storage.update('counter:123', (current) => {
const count = current?.count || 0;
return {
value: { count: count + 1 },
ttlMs: 3600000,
};
});Examples
Basic Storage Usage
import { z } from 'zod';
import { defineContext, defineRule } from '@bantai-dev/core';
import { withStorage, createMemoryStorage } from '@bantai-dev/with-storage';
const sessionSchema = z.object({
userId: z.string(),
expiresAt: z.number(),
});
const storage = createMemoryStorage(sessionSchema);
const context = withStorage(
defineContext(z.object({ sessionId: z.string() })),
storage
);
const sessionRule = defineRule(
context,
'check-session',
async (input, { tools }) => {
const session = await tools.storage.get(input.sessionId);
if (!session) {
return deny({ reason: 'Session not found' });
}
// Manual expiration check (TTL not auto-enforced)
if (session.expiresAt < Date.now()) {
await tools.storage.delete(input.sessionId);
return deny({ reason: 'Session expired' });
}
return allow({ reason: 'Session valid' });
}
);Atomic Counter Updates
The in-memory storage provides atomic updates using locking:
const counterSchema = z.object({
count: z.number().int().min(0),
});
const storage = createMemoryStorage(counterSchema);
const context = withStorage(
defineContext(z.object({ counterKey: z.string() })),
storage
);
const incrementRule = defineRule(
context,
'increment',
async (input, { tools }) => {
// Atomic increment - safe for concurrent access
const newValue = await tools.storage.update(
input.counterKey,
(current) => {
const count = current?.count || 0;
return {
value: { count: count + 1 },
};
}
);
return allow({ reason: `Counter: ${newValue?.count}` });
}
);Caching with Manual Expiration
Since TTL is not automatically enforced, you can implement manual expiration:
const cacheSchema = z.object({
data: z.string(),
cachedAt: z.number(),
expiresAt: z.number(),
});
const storage = createMemoryStorage(cacheSchema);
const context = withStorage(
defineContext(z.object({ cacheKey: z.string() })),
storage
);
const cacheRule = defineRule(
context,
'get-cached',
async (input, { tools }) => {
const cached = await tools.storage.get(input.cacheKey);
if (cached && cached.expiresAt > Date.now()) {
return allow({ reason: 'Cache hit' });
}
// Cache expired or not found
if (cached) {
await tools.storage.delete(input.cacheKey);
}
// Fetch and cache with expiration timestamp
const data = await fetchData();
const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes
await tools.storage.set(input.cacheKey, {
data,
cachedAt: Date.now(),
expiresAt,
});
return allow({ reason: 'Data cached' });
}
);Integration with Rate Limiting
Perfect for development and testing rate limiting:
import { withRateLimit, rateLimitSchema, createMemoryStorage } from '@bantai-dev/with-rate-limit';
const storage = createMemoryStorage(rateLimitSchema);
const rateLimitedContext = withRateLimit(baseContext, {
storage,
generateKey: (input) => `api:${input.userId}:${input.endpoint}`,
defaultValues: {
rateLimit: {
type: 'fixed-window',
limit: 100,
period: '1h',
},
},
});How It Works
Storage Mechanism
- Uses a JavaScript
Mapto store key-value pairs - All operations are synchronous (wrapped in Promises for async compatibility)
- No serialization needed - values are stored as-is
Locking Mechanism
The update method uses a promise-based locking mechanism:
- Lock Queue: Each key has a queue of pending operations
- Sequential Execution: Operations on the same key execute sequentially
- Automatic Release: Locks are automatically released after operation completion
This ensures atomic read-modify-write operations even in concurrent scenarios.
Schema Validation
Values are validated against the provided Zod schema on write operations:
set(): Validates before storingupdate(): Validates the new value before storing
Invalid values will throw a Zod validation error.
Use Cases
Development & Testing
- Local Development: Quick setup without external dependencies
- Unit Tests: Isolated test storage that resets between tests
- Prototyping: Fast iteration without infrastructure setup
When to Use
✅ Good for:
- Development and local testing
- Unit and integration tests
- Prototyping and demos
- Single-process applications
❌ Not suitable for:
- Production deployments
- Multi-process/multi-instance applications
- Data that needs to persist across restarts
- High-availability requirements
Migration to Production Storage
When ready for production, you can easily swap the storage adapter:
// Development
import { createMemoryStorage } from '@bantai-dev/with-storage';
const storage = createMemoryStorage(schema);
// Production
import { createRedisStorage } from '@bantai-dev/storage-redis';
const storage = createRedisStorage(
{ url: process.env.REDIS_URL },
schema
);
// Same interface - no code changes needed!
const context = withStorage(baseContext, storage);Requirements
- Node.js >= 18
- TypeScript >= 5.0
- Zod >= 4.3.5
- @bantai-dev/with-storage or @bantai-dev/with-rate-limit
- @bantai-dev/core
Related Documentation
- Storage Extension - Learn about storage adapters
- Redis Storage - Production-ready Redis storage
- Rate Limiting Extension - Uses in-memory storage for development
- Core Concepts - Understand contexts, rules, and policies