match main firebase behavior; fix contract + mismatches
Contract rewritten to match main prod truth:
- require id injection in all doc/collection results
- honor setDoc({merge:true}) (main L1624 + 4 activeDisplay sites)
- drop invented bare<->prefixed cross-lookup tests (main never does this)
- add setDoc{merge}, batch set-only/update-only contract coverage
Fix mismatches vs main:
- subscribeCollection hook now forwards queryConstraints (was dropped;
LOG_QUERY sort+limit honored again). Mock onSnapshot honors orderBy+limit.
2 new contract tests prove the chain.
- subscribeDoc/subscribeCollection adapters now forward errCb. App hooks +
DisplayView propagate subscribe errors to UI (match main onSnapshot 3rd arg).
- ws adapter signatures accept queryConstraints + errCb (interface match).
activeDisplay writes match main:
- 5 unguarded sites -> setDoc({merge:true}) (create-if-missing, not updateDoc)
- 3 guarded sites stay updateDoc
Delete memory adapter: third storage system added complexity, zero value.
firebase-mock covers fast tests, ws covers server path. Factory throws on
unknown mode now.
Delete phantom 'Phase A/B' comment in firebase.js header.
Tests updated to assert setDoc{merge} (not updateDoc) for activeDisplay writes.
83/83 green.
This commit is contained in:
+23
-7
@@ -205,6 +205,11 @@ function useFirestoreDocument(docPath) {
|
||||
const unsubscribe = storage.subscribeDoc(docPath, (doc) => {
|
||||
setData(doc);
|
||||
setIsLoading(false);
|
||||
}, (err) => {
|
||||
console.error(`Error fetching document ${docPath}:`, err);
|
||||
setError(err.message || "Failed to fetch document.");
|
||||
setIsLoading(false);
|
||||
setData(null);
|
||||
});
|
||||
return () => { if (typeof unsubscribe === 'function') unsubscribe(); };
|
||||
}, [docPath]);
|
||||
@@ -233,6 +238,11 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
|
||||
const unsubscribe = storage.subscribeCollection(collectionPath, (items) => {
|
||||
setData(items);
|
||||
setIsLoading(false);
|
||||
}, queryConstraints, (err) => {
|
||||
console.error(`Error fetching collection ${collectionPath}:`, err);
|
||||
setError(err.message || "Failed to fetch collection.");
|
||||
setIsLoading(false);
|
||||
setData([]);
|
||||
});
|
||||
return () => { if (typeof unsubscribe === 'function') unsubscribe(); };
|
||||
// queryString, not array ref
|
||||
@@ -1496,7 +1506,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
||||
const handleToggleHidePlayerHp = async () => {
|
||||
if (!db) return;
|
||||
try {
|
||||
await storage.updateDoc(getPath.activeDisplay(), combatToggleHidePlayerHp(hidePlayerHp).patch);
|
||||
await storage.setDoc(getPath.activeDisplay(), combatToggleHidePlayerHp(hidePlayerHp).patch, { merge: true });
|
||||
} catch (err) {
|
||||
console.error("Error toggling hidePlayerHp:", err);
|
||||
}
|
||||
@@ -1505,7 +1515,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
||||
const handleToggleHideNpcHp = async () => {
|
||||
if (!db) return;
|
||||
try {
|
||||
await storage.updateDoc(getPath.activeDisplay(), { hideNpcHp: !hideNpcHp });
|
||||
await storage.setDoc(getPath.activeDisplay(), { hideNpcHp: !hideNpcHp }, { merge: true });
|
||||
} catch (err) {
|
||||
console.error("Error toggling hideNpcHp:", err);
|
||||
}
|
||||
@@ -1520,7 +1530,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
||||
try {
|
||||
const { patch, log } = startEncounter(encounter);
|
||||
await storage.updateDoc(encounterPath, patch);
|
||||
await storage.updateDoc(getPath.activeDisplay(), activateDisplay({ campaignId, encounterId: encounter.id }).patch);
|
||||
await storage.setDoc(getPath.activeDisplay(), activateDisplay({ campaignId, encounterId: encounter.id }).patch, { merge: true });
|
||||
logAction(log.message, { encounterName: encounter.name }, {
|
||||
encounterPath,
|
||||
updates: log.undo,
|
||||
@@ -1573,7 +1583,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
||||
try {
|
||||
const { patch, log } = endEncounter(encounter);
|
||||
await storage.updateDoc(encounterPath, patch);
|
||||
await storage.updateDoc(getPath.activeDisplay(), clearDisplay().patch);
|
||||
await storage.setDoc(getPath.activeDisplay(), clearDisplay().patch, { merge: true });
|
||||
logAction(log.message, { encounterName: encounter.name }, {
|
||||
encounterPath,
|
||||
updates: log.undo,
|
||||
@@ -1788,9 +1798,9 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
|
||||
const currentActiveEncounter = activeDisplayInfo?.activeEncounterId;
|
||||
|
||||
if (currentActiveCampaign === campaignId && currentActiveEncounter === encounterId) {
|
||||
await storage.updateDoc(getPath.activeDisplay(), clearDisplay().patch);
|
||||
await storage.setDoc(getPath.activeDisplay(), clearDisplay().patch, { merge: true });
|
||||
} else {
|
||||
await storage.updateDoc(getPath.activeDisplay(), {
|
||||
await storage.setDoc(getPath.activeDisplay(), {
|
||||
activeCampaignId: campaignId,
|
||||
activeEncounterId: encounterId,
|
||||
});
|
||||
@@ -2265,7 +2275,8 @@ function DisplayView() {
|
||||
getPath.campaign(activeCampaignId),
|
||||
(camp) => {
|
||||
setCampaignBackgroundUrl((camp && camp.playerDisplayBackgroundUrl) || '');
|
||||
}
|
||||
},
|
||||
(err) => console.error("Error fetching campaign background:", err)
|
||||
);
|
||||
|
||||
unsubscribeEncounter = storage.subscribeDoc(
|
||||
@@ -2278,6 +2289,11 @@ function DisplayView() {
|
||||
setEncounterError("Active encounter data not found.");
|
||||
}
|
||||
setIsLoadingEncounter(false);
|
||||
},
|
||||
(err) => {
|
||||
console.error("Error fetching active encounter details:", err);
|
||||
setEncounterError("Error loading active encounter data.");
|
||||
setIsLoadingEncounter(false);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user