wip library to store cold objects in s3, warm objects on disk, and hot objects in memory
nodejs typescript

placement tiers

nekomimi.pet 2f40f17b df3feb08

verified
+48 -11
README.md
···
warm: new DiskStorageTier({ directory: './cache' }),
cold: new S3StorageTier({ bucket: 'my-bucket', region: 'us-east-1' }),
},
-
compression: true,
+
placementRules: [
+
{ pattern: '**/index.html', tiers: ['hot', 'warm', 'cold'] },
+
{ pattern: '**/*.{jpg,png,gif,mp4}', tiers: ['warm', 'cold'] },
+
{ pattern: '**', tiers: ['warm', 'cold'] },
+
],
})
-
// critical file: keep in memory for instant serving
-
await storage.set('site:abc/index.html', indexHtml)
-
-
// big files: skip hot, let them live in warm + cold
-
await storage.set('site:abc/video.mp4', videoData, { skipTiers: ['hot'] })
-
await storage.set('site:abc/hero.png', imageData, { skipTiers: ['hot'] })
+
// just set - rules decide where it goes
+
await storage.set('site:abc/index.html', indexHtml) // → hot + warm + cold
+
await storage.set('site:abc/hero.png', imageData) // → warm + cold
+
await storage.set('site:abc/video.mp4', videoData) // → warm + cold
-
// on read, bubbles up from wherever it lives
-
const result = await storage.getWithMetadata('site:abc/index.html')
-
console.log(result.source) // 'hot' - served from memory
+
// reads bubble up from wherever it lives
+
const page = await storage.getWithMetadata('site:abc/index.html')
+
console.log(page.source) // 'hot'
const video = await storage.getWithMetadata('site:abc/video.mp4')
-
console.log(video.source) // 'warm' - served from disk, never touches memory
+
console.log(video.source) // 'warm'
// nuke entire site
await storage.invalidate('site:abc/')
···
```
A file that hasn't been accessed eventually gets evicted from hot (LRU), then warm (size limit + policy). Next request fetches from cold and promotes it back up.
+
+
## Placement rules
+
+
Define once which keys go where, instead of passing `skipTiers` on every `set()`:
+
+
```typescript
+
const storage = new TieredStorage({
+
tiers: {
+
hot: new MemoryStorageTier({ maxSizeBytes: 50 * 1024 * 1024 }),
+
warm: new DiskStorageTier({ directory: './cache' }),
+
cold: new S3StorageTier({ bucket: 'my-bucket', region: 'us-east-1' }),
+
},
+
placementRules: [
+
// index.html goes everywhere for instant serving
+
{ pattern: '**/index.html', tiers: ['hot', 'warm', 'cold'] },
+
+
// images and video skip hot
+
{ pattern: '**/*.{jpg,png,gif,webp,mp4}', tiers: ['warm', 'cold'] },
+
+
// assets directory skips hot
+
{ pattern: 'assets/**', tiers: ['warm', 'cold'] },
+
+
// everything else: warm + cold only
+
{ pattern: '**', tiers: ['warm', 'cold'] },
+
],
+
})
+
+
// just call set() - rules handle placement
+
await storage.set('site:abc/index.html', html) // → hot + warm + cold
+
await storage.set('site:abc/hero.png', image) // → warm + cold
+
await storage.set('site:abc/assets/font.woff', font) // → warm + cold
+
await storage.set('site:abc/about.html', html) // → warm + cold
+
```
+
+
Rules are evaluated in order. First match wins. Cold is always included.
## API
+17 -10
serve-example.ts
···
prefix: 'demo-sites/',
}),
},
+
placementRules: [
+
// index.html goes to all tiers for instant serving
+
{ pattern: '**/index.html', tiers: ['hot', 'warm', 'cold'] },
+
+
// everything else: warm + cold only
+
{ pattern: '**', tiers: ['warm', 'cold'] },
+
],
compression: true,
defaultTTL: 14 * 24 * 60 * 60 * 1000,
promotionStrategy: 'lazy',
···
console.log('\n📦 Loading example site into tiered storage...\n');
const files = [
-
{ name: 'index.html', skipTiers: [], mimeType: 'text/html' },
-
{ name: 'about.html', skipTiers: ['hot'], mimeType: 'text/html' },
-
{ name: 'docs.html', skipTiers: ['hot'], mimeType: 'text/html' },
-
{ name: 'style.css', skipTiers: ['hot'], mimeType: 'text/css' },
-
{ name: 'script.js', skipTiers: ['hot'], mimeType: 'application/javascript' },
+
{ name: 'index.html', mimeType: 'text/html' },
+
{ name: 'about.html', mimeType: 'text/html' },
+
{ name: 'docs.html', mimeType: 'text/html' },
+
{ name: 'style.css', mimeType: 'text/css' },
+
{ name: 'script.js', mimeType: 'application/javascript' },
];
for (const file of files) {
···
const key = `${siteId}/${siteName}/${file.name}`;
await storage.set(key, content, {
-
skipTiers: file.skipTiers as ('hot' | 'warm')[],
metadata: { mimeType: file.mimeType },
});
-
const tierInfo =
-
file.skipTiers.length === 0
-
? '🔥 hot + 💾 warm + ☁️ cold'
-
: `💾 warm + ☁️ cold (skipped hot)`;
+
// Determine which tiers this file went to based on placement rules
+
const isIndex = file.name === 'index.html';
+
const tierInfo = isIndex
+
? '🔥 hot + 💾 warm + ☁️ cold'
+
: '💾 warm + ☁️ cold (skipped hot)';
const sizeKB = (content.length / 1024).toFixed(2);
console.log(` ✓ ${file.name.padEnd(15)} ${sizeKB.padStart(6)} KB → ${tierInfo}`);
}
+47 -11
src/TieredStorage.ts
···
StorageMetadata,
AllTierStats,
StorageSnapshot,
-
} from './types/index.js';
+
PlacementRule,
+
} from './types/index';
import { compress, decompress } from './utils/compression.js';
import { defaultSerialize, defaultDeserialize } from './utils/serialization.js';
import { calculateChecksum } from './utils/checksum.js';
+
import { matchGlob } from './utils/glob.js';
/**
* Main orchestrator for tiered storage system.
···
// 3. Create metadata
const metadata = this.createMetadata(key, finalData, options);
-
// 4. Write to all tiers (cascading down)
+
// 4. Determine which tiers to write to
+
const allowedTiers = this.getTiersForKey(key, options?.skipTiers);
+
+
// 5. Write to tiers
const tiersWritten: ('hot' | 'warm' | 'cold')[] = [];
-
// Write to hot (if configured and not skipped)
-
if (this.config.tiers.hot && !options?.skipTiers?.includes('hot')) {
+
if (this.config.tiers.hot && allowedTiers.includes('hot')) {
await this.config.tiers.hot.set(key, finalData, metadata);
tiersWritten.push('hot');
+
}
-
// Hot writes cascade to warm
-
if (this.config.tiers.warm && !options?.skipTiers?.includes('warm')) {
-
await this.config.tiers.warm.set(key, finalData, metadata);
-
tiersWritten.push('warm');
-
}
-
} else if (this.config.tiers.warm && !options?.skipTiers?.includes('warm')) {
-
// Write to warm (if hot skipped)
+
if (this.config.tiers.warm && allowedTiers.includes('warm')) {
await this.config.tiers.warm.set(key, finalData, metadata);
tiersWritten.push('warm');
}
···
tiersWritten.push('cold');
return { key, metadata, tiersWritten };
+
}
+
+
/**
+
* Determine which tiers a key should be written to.
+
*
+
* @param key - The key being stored
+
* @param skipTiers - Explicit tiers to skip (overrides placement rules)
+
* @returns Array of tiers to write to
+
*
+
* @remarks
+
* Priority: skipTiers option > placementRules > all configured tiers
+
*/
+
private getTiersForKey(
+
key: string,
+
skipTiers?: ('hot' | 'warm')[]
+
): ('hot' | 'warm' | 'cold')[] {
+
// If explicit skipTiers provided, use that
+
if (skipTiers && skipTiers.length > 0) {
+
const allTiers: ('hot' | 'warm' | 'cold')[] = ['hot', 'warm', 'cold'];
+
return allTiers.filter((t) => !skipTiers.includes(t as 'hot' | 'warm'));
+
}
+
+
// Check placement rules
+
if (this.config.placementRules) {
+
for (const rule of this.config.placementRules) {
+
if (matchGlob(rule.pattern, key)) {
+
// Ensure cold is always included
+
if (!rule.tiers.includes('cold')) {
+
return [...rule.tiers, 'cold'];
+
}
+
return rule.tiers;
+
}
+
}
+
}
+
+
// Default: write to all configured tiers
+
return ['hot', 'warm', 'cold'];
}
/**
+1
src/index.ts
···
TierStats,
AllTierStats,
TieredStorageConfig,
+
PlacementRule,
SetOptions,
StorageResult,
SetResult,
+45
src/types/index.ts
···
}
/**
+
* Rule for automatic tier placement based on key patterns.
+
*
+
* @remarks
+
* Rules are evaluated in order. First matching rule wins.
+
* Use this to define which keys go to which tiers without
+
* specifying skipTiers on every set() call.
+
*
+
* @example
+
* ```typescript
+
* placementRules: [
+
* { pattern: 'index.html', tiers: ['hot', 'warm', 'cold'] },
+
* { pattern: '*.html', tiers: ['warm', 'cold'] },
+
* { pattern: 'assets/**', tiers: ['warm', 'cold'] },
+
* { pattern: '**', tiers: ['warm', 'cold'] }, // default
+
* ]
+
* ```
+
*/
+
export interface PlacementRule {
+
/**
+
* Glob pattern to match against keys.
+
*
+
* @remarks
+
* Supports basic globs:
+
* - `*` matches any characters except `/`
+
* - `**` matches any characters including `/`
+
* - Exact matches work too: `index.html`
+
*/
+
pattern: string;
+
+
/**
+
* Which tiers to write to for matching keys.
+
*
+
* @remarks
+
* Cold is always included (source of truth).
+
* Use `['hot', 'warm', 'cold']` for critical files.
+
* Use `['warm', 'cold']` for large files.
+
* Use `['cold']` for archival only.
+
*/
+
tiers: ('hot' | 'warm' | 'cold')[];
+
}
+
+
/**
* Configuration for the TieredStorage system.
*
* @typeParam T - The type of data being stored (for serialization)
···
/** Required cold tier - slowest, largest capacity (e.g., S3, R2, object storage) */
cold: StorageTier;
};
+
+
/** Rules for automatic tier placement based on key patterns. First match wins. */
+
placementRules?: PlacementRule[];
/**
* Whether to automatically compress data before storing.
+40
src/utils/glob.ts
···
+
/**
+
* Simple glob pattern matching for key placement rules.
+
*
+
* Supports:
+
* - `*` matches any characters except `/`
+
* - `**` matches any characters including `/` (including empty string)
+
* - `{a,b,c}` matches any of the alternatives
+
* - Exact strings match exactly
+
*/
+
export function matchGlob(pattern: string, key: string): boolean {
+
// Handle exact match
+
if (!pattern.includes('*') && !pattern.includes('{')) {
+
return pattern === key;
+
}
+
+
// Escape regex special chars (except * and {})
+
let regex = pattern.replace(/[.+^$|\\()[\]]/g, '\\$&');
+
+
// Handle {a,b,c} alternation
+
regex = regex.replace(/\{([^}]+)\}/g, (_, alts) => `(${alts.split(',').join('|')})`);
+
+
// Use placeholder to avoid double-processing
+
const DOUBLE = '\x00DOUBLE\x00';
+
const SINGLE = '\x00SINGLE\x00';
+
+
// Mark ** and * with placeholders
+
regex = regex.replace(/\*\*/g, DOUBLE);
+
regex = regex.replace(/\*/g, SINGLE);
+
+
// Replace placeholders with regex patterns
+
// ** matches anything (including /)
+
// When followed by /, it's optional (matches zero or more path segments)
+
regex = regex
+
.replace(new RegExp(`${DOUBLE}/`, 'g'), '(?:.*/)?') // **/ -> optional path prefix
+
.replace(new RegExp(`/${DOUBLE}`, 'g'), '(?:/.*)?') // /** -> optional path suffix
+
.replace(new RegExp(DOUBLE, 'g'), '.*') // ** alone -> match anything
+
.replace(new RegExp(SINGLE, 'g'), '[^/]*'); // * -> match non-slash
+
+
return new RegExp(`^${regex}$`).test(key);
+
}
+175
test/TieredStorage.test.ts
···
expect(stats.hot?.items).toBe(2);
});
});
+
+
describe('Placement Rules', () => {
+
it('should place index.html in all tiers based on rule', async () => {
+
const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 });
+
const warm = new DiskStorageTier({ directory: `${testDir}/warm` });
+
const cold = new DiskStorageTier({ directory: `${testDir}/cold` });
+
+
const storage = new TieredStorage({
+
tiers: { hot, warm, cold },
+
placementRules: [
+
{ pattern: '**/index.html', tiers: ['hot', 'warm', 'cold'] },
+
{ pattern: '**', tiers: ['warm', 'cold'] },
+
],
+
});
+
+
await storage.set('site:abc/index.html', { content: 'hello' });
+
+
expect(await hot.exists('site:abc/index.html')).toBe(true);
+
expect(await warm.exists('site:abc/index.html')).toBe(true);
+
expect(await cold.exists('site:abc/index.html')).toBe(true);
+
});
+
+
it('should skip hot tier for non-matching files', async () => {
+
const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 });
+
const warm = new DiskStorageTier({ directory: `${testDir}/warm` });
+
const cold = new DiskStorageTier({ directory: `${testDir}/cold` });
+
+
const storage = new TieredStorage({
+
tiers: { hot, warm, cold },
+
placementRules: [
+
{ pattern: '**/index.html', tiers: ['hot', 'warm', 'cold'] },
+
{ pattern: '**', tiers: ['warm', 'cold'] },
+
],
+
});
+
+
await storage.set('site:abc/about.html', { content: 'about' });
+
+
expect(await hot.exists('site:abc/about.html')).toBe(false);
+
expect(await warm.exists('site:abc/about.html')).toBe(true);
+
expect(await cold.exists('site:abc/about.html')).toBe(true);
+
});
+
+
it('should match directory patterns', async () => {
+
const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 });
+
const warm = new DiskStorageTier({ directory: `${testDir}/warm` });
+
const cold = new DiskStorageTier({ directory: `${testDir}/cold` });
+
+
const storage = new TieredStorage({
+
tiers: { hot, warm, cold },
+
placementRules: [
+
{ pattern: 'assets/**', tiers: ['warm', 'cold'] },
+
{ pattern: '**', tiers: ['hot', 'warm', 'cold'] },
+
],
+
});
+
+
await storage.set('assets/images/logo.png', { data: 'png' });
+
await storage.set('index.html', { data: 'html' });
+
+
// assets/** should skip hot
+
expect(await hot.exists('assets/images/logo.png')).toBe(false);
+
expect(await warm.exists('assets/images/logo.png')).toBe(true);
+
+
// everything else goes to all tiers
+
expect(await hot.exists('index.html')).toBe(true);
+
});
+
+
it('should match file extension patterns', async () => {
+
const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 });
+
const warm = new DiskStorageTier({ directory: `${testDir}/warm` });
+
const cold = new DiskStorageTier({ directory: `${testDir}/cold` });
+
+
const storage = new TieredStorage({
+
tiers: { hot, warm, cold },
+
placementRules: [
+
{ pattern: '**/*.{jpg,png,gif,mp4}', tiers: ['warm', 'cold'] },
+
{ pattern: '**', tiers: ['hot', 'warm', 'cold'] },
+
],
+
});
+
+
await storage.set('site/hero.png', { data: 'image' });
+
await storage.set('site/video.mp4', { data: 'video' });
+
await storage.set('site/index.html', { data: 'html' });
+
+
// Images and video skip hot
+
expect(await hot.exists('site/hero.png')).toBe(false);
+
expect(await hot.exists('site/video.mp4')).toBe(false);
+
+
// HTML goes everywhere
+
expect(await hot.exists('site/index.html')).toBe(true);
+
});
+
+
it('should use first matching rule', async () => {
+
const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 });
+
const warm = new DiskStorageTier({ directory: `${testDir}/warm` });
+
const cold = new DiskStorageTier({ directory: `${testDir}/cold` });
+
+
const storage = new TieredStorage({
+
tiers: { hot, warm, cold },
+
placementRules: [
+
// Specific rule first
+
{ pattern: 'assets/critical.css', tiers: ['hot', 'warm', 'cold'] },
+
// General rule second
+
{ pattern: 'assets/**', tiers: ['warm', 'cold'] },
+
{ pattern: '**', tiers: ['warm', 'cold'] },
+
],
+
});
+
+
await storage.set('assets/critical.css', { data: 'css' });
+
await storage.set('assets/style.css', { data: 'css' });
+
+
// critical.css matches first rule -> hot
+
expect(await hot.exists('assets/critical.css')).toBe(true);
+
+
// style.css matches second rule -> no hot
+
expect(await hot.exists('assets/style.css')).toBe(false);
+
});
+
+
it('should allow skipTiers to override placement rules', async () => {
+
const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 });
+
const warm = new DiskStorageTier({ directory: `${testDir}/warm` });
+
const cold = new DiskStorageTier({ directory: `${testDir}/cold` });
+
+
const storage = new TieredStorage({
+
tiers: { hot, warm, cold },
+
placementRules: [
+
{ pattern: '**', tiers: ['hot', 'warm', 'cold'] },
+
],
+
});
+
+
// Explicit skipTiers should override the rule
+
await storage.set('large-file.bin', { data: 'big' }, { skipTiers: ['hot'] });
+
+
expect(await hot.exists('large-file.bin')).toBe(false);
+
expect(await warm.exists('large-file.bin')).toBe(true);
+
expect(await cold.exists('large-file.bin')).toBe(true);
+
});
+
+
it('should always include cold tier even if not in rule', async () => {
+
const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 });
+
const warm = new DiskStorageTier({ directory: `${testDir}/warm` });
+
const cold = new DiskStorageTier({ directory: `${testDir}/cold` });
+
+
const storage = new TieredStorage({
+
tiers: { hot, warm, cold },
+
placementRules: [
+
// Rule doesn't include cold (should be auto-added)
+
{ pattern: '**', tiers: ['hot', 'warm'] },
+
],
+
});
+
+
await storage.set('test-key', { data: 'test' });
+
+
expect(await cold.exists('test-key')).toBe(true);
+
});
+
+
it('should write to all tiers when no rules match', async () => {
+
const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 });
+
const warm = new DiskStorageTier({ directory: `${testDir}/warm` });
+
const cold = new DiskStorageTier({ directory: `${testDir}/cold` });
+
+
const storage = new TieredStorage({
+
tiers: { hot, warm, cold },
+
placementRules: [
+
{ pattern: 'specific-pattern-only', tiers: ['warm', 'cold'] },
+
],
+
});
+
+
// This doesn't match any rule
+
await storage.set('other-key', { data: 'test' });
+
+
expect(await hot.exists('other-key')).toBe(true);
+
expect(await warm.exists('other-key')).toBe(true);
+
expect(await cold.exists('other-key')).toBe(true);
+
});
+
});
});
+95
test/glob.test.ts
···
+
import { describe, it, expect } from 'vitest';
+
import { matchGlob } from '../src/utils/glob.js';
+
+
describe('matchGlob', () => {
+
describe('exact matches', () => {
+
it('should match exact strings', () => {
+
expect(matchGlob('index.html', 'index.html')).toBe(true);
+
expect(matchGlob('index.html', 'about.html')).toBe(false);
+
});
+
+
it('should match paths exactly', () => {
+
expect(matchGlob('site/index.html', 'site/index.html')).toBe(true);
+
expect(matchGlob('site/index.html', 'other/index.html')).toBe(false);
+
});
+
});
+
+
describe('* wildcard', () => {
+
it('should match any characters except /', () => {
+
expect(matchGlob('*.html', 'index.html')).toBe(true);
+
expect(matchGlob('*.html', 'about.html')).toBe(true);
+
expect(matchGlob('*.html', 'style.css')).toBe(false);
+
});
+
+
it('should not match across path separators', () => {
+
expect(matchGlob('*.html', 'dir/index.html')).toBe(false);
+
});
+
+
it('should work with prefix and suffix', () => {
+
expect(matchGlob('index.*', 'index.html')).toBe(true);
+
expect(matchGlob('index.*', 'index.css')).toBe(true);
+
expect(matchGlob('index.*', 'about.html')).toBe(false);
+
});
+
});
+
+
describe('** wildcard', () => {
+
it('should match any characters including /', () => {
+
expect(matchGlob('**', 'anything')).toBe(true);
+
expect(matchGlob('**', 'path/to/file.txt')).toBe(true);
+
});
+
+
it('should match deeply nested paths', () => {
+
expect(matchGlob('**/index.html', 'index.html')).toBe(true);
+
expect(matchGlob('**/index.html', 'site/index.html')).toBe(true);
+
expect(matchGlob('**/index.html', 'a/b/c/index.html')).toBe(true);
+
expect(matchGlob('**/index.html', 'a/b/c/about.html')).toBe(false);
+
});
+
+
it('should match directory prefixes', () => {
+
expect(matchGlob('assets/**', 'assets/style.css')).toBe(true);
+
expect(matchGlob('assets/**', 'assets/images/logo.png')).toBe(true);
+
expect(matchGlob('assets/**', 'other/style.css')).toBe(false);
+
});
+
+
it('should match in the middle of a path', () => {
+
expect(matchGlob('site/**/index.html', 'site/index.html')).toBe(true);
+
expect(matchGlob('site/**/index.html', 'site/pages/index.html')).toBe(true);
+
expect(matchGlob('site/**/index.html', 'site/a/b/c/index.html')).toBe(true);
+
});
+
});
+
+
describe('{a,b,c} alternation', () => {
+
it('should match any of the alternatives', () => {
+
expect(matchGlob('*.{html,css,js}', 'index.html')).toBe(true);
+
expect(matchGlob('*.{html,css,js}', 'style.css')).toBe(true);
+
expect(matchGlob('*.{html,css,js}', 'app.js')).toBe(true);
+
expect(matchGlob('*.{html,css,js}', 'image.png')).toBe(false);
+
});
+
+
it('should work with ** and alternation', () => {
+
expect(matchGlob('**/*.{jpg,png,gif}', 'logo.png')).toBe(true);
+
expect(matchGlob('**/*.{jpg,png,gif}', 'images/logo.png')).toBe(true);
+
expect(matchGlob('**/*.{jpg,png,gif}', 'a/b/photo.jpg')).toBe(true);
+
expect(matchGlob('**/*.{jpg,png,gif}', 'style.css')).toBe(false);
+
});
+
});
+
+
describe('edge cases', () => {
+
it('should handle empty strings', () => {
+
expect(matchGlob('', '')).toBe(true);
+
expect(matchGlob('', 'something')).toBe(false);
+
expect(matchGlob('**', '')).toBe(true);
+
});
+
+
it('should escape regex special characters', () => {
+
expect(matchGlob('file.txt', 'file.txt')).toBe(true);
+
expect(matchGlob('file.txt', 'filextxt')).toBe(false);
+
expect(matchGlob('file[1].txt', 'file[1].txt')).toBe(true);
+
});
+
+
it('should handle keys with colons (common in storage)', () => {
+
expect(matchGlob('site:*/index.html', 'site:abc/index.html')).toBe(true);
+
expect(matchGlob('site:**/index.html', 'site:abc/pages/index.html')).toBe(true);
+
});
+
});
+
});