ws adapter: match main truth (id injection + setDoc merge)

- getDoc/getCollection/subscribe inject {id,...data} (main firebase shape)
- getCollection normalizes backend {id,data}[] OR bare data[] → {id,...data}
- setDoc(opts.merge) → PATCH (create-on-miss); else PUT (replace)
- ws-reconnect test asserts id now present

24/24 server green. All suites green: App 83, shared 91, server 24.
This commit is contained in:
david raistrick
2026-07-04 12:35:28 -04:00
parent 79af61cb8b
commit 6baecd374e
2 changed files with 31 additions and 6 deletions
+1 -1
View File
@@ -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));
}
+30 -5
View File
@@ -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<cb>
const collSubs = new Map(); // collPath -> Set<cb>
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(() => {