UNPKG

onilib

Version:

A modular Node.js library for real-time online integration in games and web applications

422 lines (360 loc) 9.82 kB
const path = require('path'); const fs = require('fs').promises; class StorageModule { constructor(config = {}) { this.config = { type: config.type || 'memory', path: config.path || './data/app.db', ...config }; this.adapter = null; } async initialize(noi) { this.noi = noi; this.core = noi.core; // Initialize the appropriate adapter switch (this.config.type) { case 'memory': this.adapter = new InMemoryAdapter(this.config); break; case 'sqlite': this.adapter = new SQLiteAdapter(this.config); break; case 'postgres': this.adapter = new PostgresAdapter(this.config); break; default: throw new Error(`Unknown storage type: ${this.config.type}`); } await this.adapter.initialize(); this.core.log('info', `Storage module initialized with ${this.config.type} adapter`); } // Proxy methods to adapter async get(key) { return await this.adapter.get(key); } async set(key, value, ttl = null) { return await this.adapter.set(key, value, ttl); } async delete(key) { return await this.adapter.delete(key); } async exists(key) { return await this.adapter.exists(key); } async keys(pattern = '*') { return await this.adapter.keys(pattern); } async clear() { return await this.adapter.clear(); } async increment(key, amount = 1) { return await this.adapter.increment(key, amount); } async expire(key, ttl) { return await this.adapter.expire(key, ttl); } async getStats() { return await this.adapter.getStats(); } async stop() { if (this.adapter && this.adapter.close) { await this.adapter.close(); } this.core.log('info', 'Storage module stopped'); } } // In-Memory Storage Adapter class InMemoryAdapter { constructor(config) { this.config = config; this.data = new Map(); this.expirations = new Map(); this.cleanupInterval = null; } async initialize() { // Start cleanup interval for expired keys this.cleanupInterval = setInterval(() => { this.cleanupExpired(); }, 60000); // Check every minute } async get(key) { if (this.isExpired(key)) { this.data.delete(key); this.expirations.delete(key); return null; } return this.data.get(key) || null; } async set(key, value, ttl = null) { this.data.set(key, value); if (ttl) { this.expirations.set(key, Date.now() + ttl * 1000); } else { this.expirations.delete(key); } return true; } async delete(key) { const existed = this.data.has(key); this.data.delete(key); this.expirations.delete(key); return existed; } async exists(key) { if (this.isExpired(key)) { this.data.delete(key); this.expirations.delete(key); return false; } return this.data.has(key); } async keys(pattern = '*') { const allKeys = Array.from(this.data.keys()); if (pattern === '*') { return allKeys.filter(key => !this.isExpired(key)); } // Simple pattern matching (only supports * wildcard) const regex = new RegExp(pattern.replace(/\*/g, '.*')); return allKeys.filter(key => !this.isExpired(key) && regex.test(key)); } async clear() { this.data.clear(); this.expirations.clear(); return true; } async increment(key, amount = 1) { const current = await this.get(key); const value = (typeof current === 'number' ? current : 0) + amount; await this.set(key, value); return value; } async expire(key, ttl) { if (!this.data.has(key)) { return false; } this.expirations.set(key, Date.now() + ttl * 1000); return true; } async getStats() { return { type: 'memory', keyCount: this.data.size, expiredKeys: Array.from(this.expirations.keys()).filter(key => this.isExpired(key)).length }; } isExpired(key) { const expiration = this.expirations.get(key); return expiration && Date.now() > expiration; } cleanupExpired() { let cleaned = 0; for (const key of this.expirations.keys()) { if (this.isExpired(key)) { this.data.delete(key); this.expirations.delete(key); cleaned++; } } return cleaned; } async close() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } } } // SQLite Storage Adapter class SQLiteAdapter { constructor(config) { this.config = config; this.db = null; } async initialize() { const sqlite3 = require('sqlite3'); // Ensure directory exists const dbDir = path.dirname(this.config.path); await fs.mkdir(dbDir, { recursive: true }); return new Promise((resolve, reject) => { this.db = new sqlite3.Database(this.config.path, (err) => { if (err) { reject(err); } else { this.createTables().then(resolve).catch(reject); } }); }); } async createTables() { return new Promise((resolve, reject) => { const sql = ` CREATE TABLE IF NOT EXISTS storage ( key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at INTEGER, created_at INTEGER DEFAULT (strftime('%s', 'now')), updated_at INTEGER DEFAULT (strftime('%s', 'now')) ); CREATE INDEX IF NOT EXISTS idx_expires_at ON storage(expires_at); `; this.db.exec(sql, (err) => { if (err) { reject(err); } else { resolve(); } }); }); } async get(key) { return new Promise((resolve, reject) => { const sql = ` SELECT value FROM storage WHERE key = ? AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) `; this.db.get(sql, [key], (err, row) => { if (err) { reject(err); } else { resolve(row ? JSON.parse(row.value) : null); } }); }); } async set(key, value, ttl = null) { return new Promise((resolve, reject) => { const expiresAt = ttl ? Math.floor(Date.now() / 1000) + ttl : null; const sql = ` INSERT OR REPLACE INTO storage (key, value, expires_at, updated_at) VALUES (?, ?, ?, strftime('%s', 'now')) `; this.db.run(sql, [key, JSON.stringify(value), expiresAt], (err) => { if (err) { reject(err); } else { resolve(true); } }); }); } async delete(key) { return new Promise((resolve, reject) => { const sql = 'DELETE FROM storage WHERE key = ?'; this.db.run(sql, [key], function(err) { if (err) { reject(err); } else { resolve(this.changes > 0); } }); }); } async exists(key) { return new Promise((resolve, reject) => { const sql = ` SELECT 1 FROM storage WHERE key = ? AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) `; this.db.get(sql, [key], (err, row) => { if (err) { reject(err); } else { resolve(!!row); } }); }); } async keys(pattern = '*') { return new Promise((resolve, reject) => { let sql = ` SELECT key FROM storage WHERE (expires_at IS NULL OR expires_at > strftime('%s', 'now')) `; const params = []; if (pattern !== '*') { sql += ' AND key LIKE ?'; params.push(pattern.replace(/\*/g, '%')); } this.db.all(sql, params, (err, rows) => { if (err) { reject(err); } else { resolve(rows.map(row => row.key)); } }); }); } async clear() { return new Promise((resolve, reject) => { const sql = 'DELETE FROM storage'; this.db.run(sql, (err) => { if (err) { reject(err); } else { resolve(true); } }); }); } async increment(key, amount = 1) { const current = await this.get(key); const value = (typeof current === 'number' ? current : 0) + amount; await this.set(key, value); return value; } async expire(key, ttl) { return new Promise((resolve, reject) => { const expiresAt = Math.floor(Date.now() / 1000) + ttl; const sql = 'UPDATE storage SET expires_at = ? WHERE key = ?'; this.db.run(sql, [expiresAt, key], function(err) { if (err) { reject(err); } else { resolve(this.changes > 0); } }); }); } async getStats() { return new Promise((resolve, reject) => { const sql = ` SELECT COUNT(*) as total_keys, COUNT(CASE WHEN expires_at IS NOT NULL AND expires_at <= strftime('%s', 'now') THEN 1 END) as expired_keys FROM storage `; this.db.get(sql, (err, row) => { if (err) { reject(err); } else { resolve({ type: 'sqlite', path: this.config.path, keyCount: row.total_keys, expiredKeys: row.expired_keys }); } }); }); } async close() { if (this.db) { return new Promise((resolve) => { this.db.close((err) => { if (err) console.error('Error closing SQLite database:', err); resolve(); }); }); } } } // Placeholder for PostgreSQL adapter class PostgresAdapter { constructor(config) { this.config = config; } async initialize() { throw new Error('PostgreSQL adapter not implemented yet'); } } module.exports = { StorageModule, InMemoryAdapter, SQLiteAdapter, PostgresAdapter };