strata-storage
Version:
Zero-dependency universal storage plugin providing a unified API for all storage operations across web, Android, and iOS platforms
191 lines (190 loc) • 6 kB
JavaScript
/**
* Memory Adapter - In-memory storage implementation
* Provides fast, non-persistent storage using Map
*/
import { BaseAdapter } from "../../core/BaseAdapter.js";
import { deepClone } from "../../utils/index.js";
import { QuotaExceededError } from "../../utils/errors.js";
/**
* In-memory storage adapter using Map
*/
export class MemoryAdapter extends BaseAdapter {
name = 'memory';
capabilities = {
persistent: false,
synchronous: false, // We use async for consistency
observable: true,
transactional: false,
queryable: true,
maxSize: -1, // No hard limit, but configurable
binary: true,
encrypted: false, // Encryption handled by feature layer
crossTab: false, // Memory is per-instance
};
storage = new Map();
maxSize;
currentSize = 0;
/**
* Check if adapter is available (always true for memory)
*/
async isAvailable() {
return true;
}
/**
* Initialize the adapter
*/
async initialize(config) {
this.maxSize = config?.maxSize;
this.startTTLCleanup();
}
/**
* Get a value from memory
*/
async get(key) {
const value = this.storage.get(key);
if (!value)
return null;
// Check TTL
if (this.isExpired(value)) {
await this.remove(key);
return null;
}
// Return a deep clone to prevent external modifications
return deepClone(value);
}
/**
* Set a value in memory
*/
async set(key, value) {
const oldValue = this.storage.get(key);
const newSize = this.calculateSize(value);
// Check size limit if configured
if (this.maxSize && this.maxSize > 0) {
const oldSize = oldValue ? this.calculateSize(oldValue) : 0;
const projectedSize = this.currentSize - oldSize + newSize;
if (projectedSize > this.maxSize) {
throw new QuotaExceededError('Memory storage size limit exceeded', {
limit: this.maxSize,
current: this.currentSize,
projected: projectedSize,
key,
});
}
}
// Store a deep clone to prevent external modifications
const clonedValue = deepClone(value);
this.storage.set(key, clonedValue);
// Update size tracking
if (oldValue) {
this.currentSize -= this.calculateSize(oldValue);
}
this.currentSize += newSize;
// Emit change event
this.emitChange(key, oldValue?.value, value.value, 'local');
}
/**
* Remove a value from memory
*/
async remove(key) {
const value = this.storage.get(key);
if (!value)
return;
this.storage.delete(key);
this.currentSize -= this.calculateSize(value);
// Emit change event
this.emitChange(key, value.value, undefined, 'local');
}
/**
* Clear memory storage
*/
async clear(options) {
if (!options ||
(!options.pattern && !options.prefix && !options.tags && !options.expiredOnly)) {
// Clear everything
this.storage.clear();
this.currentSize = 0;
this.emitChange('*', undefined, undefined, 'local');
return;
}
// Use base implementation for filtered clear
await super.clear(options);
// Recalculate size after filtered clear
this.currentSize = 0;
for (const value of this.storage.values()) {
this.currentSize += this.calculateSize(value);
}
}
/**
* Check if key exists
*/
async has(key) {
const value = this.storage.get(key);
return value !== undefined && !this.isExpired(value);
}
/**
* Get all keys
*/
async keys(pattern) {
const allKeys = Array.from(this.storage.keys());
// Remove expired entries
const validKeys = [];
for (const key of allKeys) {
const value = this.storage.get(key);
if (value && !this.isExpired(value)) {
validKeys.push(key);
}
else if (value) {
// Clean up expired entry
await this.remove(key);
}
}
return this.filterKeys(validKeys, pattern);
}
/**
* Query implementation for memory adapter
*/
async query(condition) {
const results = [];
for (const [key, item] of this.storage.entries()) {
if (!this.isExpired(item)) {
// Check if querying storage metadata (tags, metadata, etc) or the actual value
let matches = false;
// Check for storage-level properties
const storageProps = ['tags', 'metadata', 'created', 'updated', 'expires'];
const isStorageQuery = Object.keys(condition).some((k) => storageProps.includes(k));
if (isStorageQuery) {
// Query against the storage wrapper
matches = this.queryEngine.matches(item, condition);
}
else {
// Query against the stored value
matches = this.queryEngine.matches(item.value, condition);
}
if (matches) {
results.push({
key,
value: deepClone(item.value),
});
}
}
}
return results;
}
/**
* Get current memory usage
*/
getMemoryUsage() {
return {
used: this.currentSize,
limit: this.maxSize,
};
}
/**
* Close the adapter
*/
async close() {
await super.close();
this.storage.clear();
this.currentSize = 0;
}
}