diff --git a/server/tests/ws-reconnect.test.js b/server/tests/ws-reconnect.test.js index c36692e..1b38a7b 100644 --- a/server/tests/ws-reconnect.test.js +++ b/server/tests/ws-reconnect.test.js @@ -48,7 +48,7 @@ describe('BUG-8: ws adapter reconnect after drop', () => { await flush(1000); const last = calls[calls.length - 1]; - expect(last).toEqual({ name: 'V2' }); + expect(last).toEqual({ id: 'a', name: 'V2' }); } finally { await new Promise(r => storage.dispose(r)); } diff --git a/src/storage/ws.js b/src/storage/ws.js index e6c98a6..e26b17b 100644 --- a/src/storage/ws.js +++ b/src/storage/ws.js @@ -33,6 +33,15 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { return p.replace(/^[\s\S]*\/public\/data\//, ''); } + // Inject id (last path segment) into doc — matches main firebase truth: + // { id: docSnap.id, ...snap.data() } + // Backend stores docs WITHOUT id; client adds it so all adapters share shape. + function withId(rawPath, data) { + if (data === null || data === undefined) return data; + const id = norm(rawPath).split('/').pop(); + return { id, ...data }; + } + const docSubs = new Map(); // path -> Set const collSubs = new Map(); // collPath -> Set let ws = null; @@ -154,12 +163,19 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { async getDoc(rawPath) { const p = norm(rawPath); const res = await api('GET', '/api/doc', { path: p }); - return res && res.data !== undefined ? res.data : null; + const data = res && res.data !== undefined ? res.data : null; + return withId(rawPath, data); }, - async setDoc(rawPath, data) { + async setDoc(rawPath, data, opts = {}) { const p = norm(rawPath); - await api('PUT', '/api/doc', null, { path: p, data }); + // merge flag -> PATCH (shallow merge, create-on-miss). Matches firebase + // setDoc(path, data, {merge:true}) semantics. + if (opts.merge) { + await api('PATCH', '/api/doc', null, { path: p, patch: data }); + } else { + await api('PUT', '/api/doc', null, { path: p, data }); + } }, async updateDoc(rawPath, patch) { @@ -180,7 +196,16 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { async getCollection(rawCollPath) { const p = norm(rawCollPath); - return await api('GET', '/api/collection', { path: p }); + const docs = await api('GET', '/api/collection', { path: p }); + // Backend returns array of { id, data } OR bare data[]; normalize to + // { id, ...data } (firebase truth). + if (!Array.isArray(docs)) return []; + return docs.map(d => { + if (d && typeof d === 'object' && 'id' in d && 'data' in d) { + return { id: d.id, ...d.data }; + } + return { ...d }; + }); }, async batchWrite(ops) { @@ -190,7 +215,7 @@ function createWsStorage({ baseUrl, wsUrl } = {}) { subscribeDoc(rawPath, cb, errCb) { const p = norm(rawPath); - // Initial value via REST (independent of WS connect). + // Initial value via REST (independent of WS connect). getDoc injects id. storage.getDoc(p).then(cb).catch(() => {}); // WS only for subsequent change notifications. ensureWs().then(() => {