// memory.js — in-process storage impl. Test seed. // Map. EventEmitter for subscribe. // Mirrors firebase semantics: setDoc=replace, updateDoc=shallow merge, addDoc=auto-id. 'use strict'; import { EventEmitter } from 'events'; function createMemoryStorage() { const docs = new Map(); // path -> data obj const bus = new EventEmitter(); bus.setMaxListeners(1000); // Firebase-prefixed paths (artifacts/{APP_ID}/public/data/...) normalized to // bare canonical. Matches ws.js norm() so all impls share path identity. function norm(p) { if (!p) return p; return p.replace(/^[\s\S]*\/public\/data\//, ''); } // ---- path helpers ---- // collection path = path with even number of segments OR known collection. // doc path = odd segments (coll/doc, coll/doc/subcoll/subdoc). // getCollection(path) returns all docs whose path === path/id for any single id segment. function isCollectionPath(p) { return p.split('/').length % 2 === 1; } function emitDoc(path, data) { bus.emit('doc:' + path, data); } function emitCollection(collPath) { const children = collectionDocs(collPath); bus.emit('coll:' + collPath, children); } function collectionDocs(collPath) { const out = []; const segLen = collPath.split('/').length + 1; for (const [p, data] of docs) { const segs = p.split('/'); if (segs.length !== segLen) continue; const parent = segs.slice(0, -1).join('/'); if (parent === collPath) out.push(data); } return out; } function genId() { return (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : `id_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; } const storage = { async getDoc(rawPath) { const path = norm(rawPath); return docs.has(path) ? deepClone(docs.get(path)) : null; }, async setDoc(rawPath, data) { const path = norm(rawPath); docs.set(path, deepClone(data)); emitDoc(path, deepClone(data)); const segs = path.split('/'); if (segs.length >= 2) emitCollection(segs.slice(0, -1).join('/')); }, async updateDoc(rawPath, patch) { const path = norm(rawPath); const existing = docs.has(path) ? docs.get(path) : {}; const merged = { ...existing, ...patch }; docs.set(path, merged); emitDoc(path, deepClone(merged)); const segs = path.split('/'); if (segs.length >= 2) emitCollection(segs.slice(0, -1).join('/')); }, async deleteDoc(rawPath) { const path = norm(rawPath); docs.delete(path); emitDoc(path, null); const segs = path.split('/'); if (segs.length >= 2) emitCollection(segs.slice(0, -1).join('/')); }, async addDoc(rawCollectionPath, data) { const collectionPath = norm(rawCollectionPath); const id = genId(); const path = `${collectionPath}/${id}`; docs.set(path, deepClone(data)); emitDoc(path, deepClone(data)); emitCollection(collectionPath); return { id, path }; }, async getCollection(rawCollPath) { const collPath = norm(rawCollPath); return collectionDocs(collPath).map(deepClone); }, async batchWrite(ops) { for (const op of ops) { const mop = { ...op, path: norm(op.path) }; if (mop.type === 'set') await storage.setDoc(mop.path, mop.data); else if (mop.type === 'delete') await storage.deleteDoc(mop.path); else if (mop.type === 'update') await storage.updateDoc(mop.path, mop.data); } }, subscribeDoc(rawPath, cb) { const path = norm(rawPath); const cur = docs.has(path) ? deepClone(docs.get(path)) : null; Promise.resolve().then(() => cb(cur)); const handler = (data) => cb(data); bus.on('doc:' + path, handler); return () => bus.off('doc:' + path, handler); }, subscribeCollection(rawCollPath, cb) { const collPath = norm(rawCollPath); Promise.resolve().then(() => cb(collectionDocs(collPath).map(deepClone))); const handler = (docs) => cb(docs); bus.on('coll:' + collPath, handler); return () => bus.off('coll:' + collPath, handler); }, dispose() { bus.removeAllListeners(); docs.clear(); }, // test/debug _docs: docs, }; return storage; } function deepClone(v) { if (v === null || v === undefined) return v; return JSON.parse(JSON.stringify(v)); } export { createMemoryStorage };