UNPKG

virtual-storage-device-for-chat-systems

Version:

Virtual Storage Device for Chat Systems.

180 lines (158 loc) 6.37 kB
/* storedev.js Simple file-backed chat store (storage.json) for development / prototyping. Data model (storage.json): { "chat1": { "users": ["alice", "me"], "messages": [ { "id": "...", "sender": "alice", "content": "Hello Pawan", "ts": "2025-08-09T09:00:00.000Z" }, { "id": "...", "sender": "me", "content": "Hi! there", "ts": "2025-08-09T09:00:03.000Z" } ] }, "chat2": { "users": ["someone","me"], "messages": [] } } Why this model? - JSON object keyed by chatId keeps sessions easy to list. - "messages" is an array (preserves order) and allows multiple messages from the same sender. - Duplicate object keys are not allowed in JSON, so using an array is the correct approach. Exports: FileSystem with these async functions: - createSession(chatId, users = []) - addUserToSession(chatId, username) - removeUserFromSession(chatId, username) - appendMessage(chatId, sender, content, opts = { autoCreate: false }) - getMessages(chatId, opts = { since, limit }) - listSessions() - deleteSession(chatId) - getStoragePath() Notes: - This implementation uses an in-process queue to avoid races within the same Node process. - Writes are atomic (write to temp file, then rename) to minimize file corruption. - If you need multi-process safety or high concurrency, use a real DB (SQLite/ostgres/LevelDB). */ import fs from "fs/promises"; import path from "path"; import crypto from "crypto"; const DEFAULT_STORAGE = path.join(process.cwd(), "storage.json"); let STORAGE_FILE = DEFAULT_STORAGE; // simple in-process queue keyed by 'storage' to serialize read/write operations const queues = new Map(); function queueWork(key, work) { const prev = queues.get(key) || Promise.resolve(); const next = prev.then(() => work()); // attach a catch so rejections don't break the chain queues.set(key, next.catch(() => {})); // when done, remove if it's still the same promise next.finally(() => { if (queues.get(key) === next) queues.delete(key); }); return next; } async function readStorage() { try { const txt = await fs.readFile(STORAGE_FILE, "utf8"); return txt.trim() ? JSON.parse(txt) : {}; } catch (err) { if (err.code === "ENOENT") return {}; throw err; } } async function writeAtomic(filePath, dataString) { const dir = path.dirname(filePath); const tmp = path.join(dir, ".storage.tmp." + crypto.randomBytes(6).toString("hex")); await fs.writeFile(tmp, dataString, "utf8"); await fs.rename(tmp, filePath); } function makeMsg(sender, content) { return { id: (crypto.randomUUID && crypto.randomUUID()) || crypto.randomBytes(8).toString("hex"), sender, content, ts: new Date().toISOString() }; } export const FileSystem = { // you can change storage path before using the API setStoragePath(p) { STORAGE_FILE = path.resolve(p); }, getStoragePath() { return STORAGE_FILE; }, async createSession(chatId, users = []) { return queueWork("storage", async () => { const data = await readStorage(); if (data[chatId]) throw new Error(`Session '${chatId}' already exists`); data[chatId] = { users: Array.from(new Set(users)), messages: [] }; await writeAtomic(STORAGE_FILE, JSON.stringify(data, null, 2)); return data[chatId]; }); }, async addUserToSession(chatId, username) { return queueWork("storage", async () => { const data = await readStorage(); if (!data[chatId]) throw new Error(`Session '${chatId}' not found`); if (!data[chatId].users.includes(username)) data[chatId].users.push(username); await writeAtomic(STORAGE_FILE, JSON.stringify(data, null, 2)); return data[chatId]; }); }, async removeUserFromSession(chatId, username) { return queueWork("storage", async () => { const data = await readStorage(); if (!data[chatId]) throw new Error(`Session '${chatId}' not found`); data[chatId].users = data[chatId].users.filter(u => u !== username); await writeAtomic(STORAGE_FILE, JSON.stringify(data, null, 2)); return data[chatId]; }); }, async appendMessage(chatId, sender, content, opts = { autoCreate: false }) { return queueWork("storage", async () => { const data = await readStorage(); if (!data[chatId]) { if (!opts.autoCreate) throw new Error(`Session '${chatId}' not found`); data[chatId] = { users: [sender], messages: [] }; } // ensure sender in users list (optional behavior) if (!data[chatId].users.includes(sender)) data[chatId].users.push(sender); const msg = makeMsg(sender, content); data[chatId].messages.push(msg); await writeAtomic(STORAGE_FILE, JSON.stringify(data, null, 2)); return msg; }); }, // get messages, optional since timestamp or limit async getMessages(chatId, opts = { since: null, limit: null }) { // read only; still queue to keep consistent view with writes return queueWork("storage", async () => { const data = await readStorage(); if (!data[chatId]) return null; let msgs = data[chatId].messages.slice(); if (opts.since) msgs = msgs.filter(m => new Date(m.ts) > new Date(opts.since)); if (opts.limit) msgs = msgs.slice(-opts.limit); return { users: data[chatId].users.slice(), messages: msgs }; }); }, async listSessions() { const data = await readStorage(); return Object.keys(data); }, async deleteSession(chatId) { return queueWork("storage", async () => { const data = await readStorage(); if (!data[chatId]) throw new Error(`Session '${chatId}' not found`); delete data[chatId]; await writeAtomic(STORAGE_FILE, JSON.stringify(data, null, 2)); return true; }); } }; // If you want encryption hooks, add wrapper functions around appendMessage/getMessages // using your AES-GCM sharedKey and store ciphertext in `content` instead of plaintext. // Example usage (outside this module): // import { FileSystem } from './storedev.js'; // await FileSystem.createSession('chat1', ['username','me']); // await FileSystem.appendMessage('chat1', 'username', 'Hello, Pawan'); // await FileSystem.appendMessage('chat1', 'me', "Hi! there"); // const chat = await FileSystem.getMessages('chat1'); // console.log(chat);