onilib
Version:
A modular Node.js library for real-time online integration in games and web applications
422 lines (360 loc) • 9.82 kB
JavaScript
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 };