Closed
Description
Operating System
macOS Sonoma 14.4
Environment (if applicable)
Chrome 127.0.6533.100
Firebase SDK Version
10.13.1
Firebase SDK Product(s)
Firestore
Project Tooling
No-bundle (using cdn to reproduce)
Detailed Problem Description
- Update data.
- GetDocs before updated data is fully reflected onSnapshot.
- Random freeze.
- persistentLocalCache and security rules trigger this defect with high probability.
Steps and code to reproduce issue
- create new firebase project, and setup firestore.
- set security rules as below:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function getSheetRule(sheet_id) {
return get(/databases/$(database)/documents/sheet_rules/$(sheet_id)).data;
}
match /sheets/{sheet_id}/{document=**} {
allow read: if getSheetRule(sheet_id).read;
allow create: if getSheetRule(sheet_id).create;
allow update: if getSheetRule(sheet_id).update;
allow delete: if getSheetRule(sheet_id).delete;
}
match /sheet_rules/{sheet_id} {
allow read, write: if true;
}
}
}
- save the following as html, open it, and check the console in the developer Tools:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.13.1/firebase-app.js";
import {
initializeFirestore,
runTransaction,
onSnapshot,
setDoc,
doc,
collection,
getDocs,
where,
deleteDoc,
query,
persistentLocalCache,
persistentSingleTabManager,
} from "https://www.gstatic.com/firebasejs/10.13.1/firebase-firestore.js";
// TODO: Replace the following with your app's [Firebase project configuration](https://firebase.google.com/docs/web/learn-more?hl=ja#config-object)
const firebaseConfig = {
// ..
};
const app = initializeApp(firebaseConfig);
const firestore = initializeFirestore(app, {
localCache: persistentLocalCache({
tabManager: persistentSingleTabManager({}),
}),
});
const nanoid = () => {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let id = "";
for (let i = 0; i < 21; i++) {
id += chars[Math.floor(Math.random() * chars.length)];
}
return id;
};
let idA = "TjmWMWv7YsteLgkRMcRgA";
let idB = "rmRSk7ya35bZHssCncdlg";
let idC = "OEYRzjFB5NqhejLOXixna";
let firstRecv = true;
const sheetId = "hxP3Qe7UywllnEPMqmn9F";
const docSheet = doc(firestore, "sheets", sheetId);
const docItem = (id) => doc(firestore, "sheets", sheetId, "items", id);
const colItems = collection(firestore, "sheets", sheetId, "items");
const flowLog = (msg, ct = false) =>
console.log(
`%c${msg}`,
`color:#000;background-color:${ct ? "#ff0" : "#6ff"};padding:0 5px`
);
const resetTestData = async () => {
/* A -> B -> C -> null */
await setDoc(docItem(idA), {
id: idA,
next_id: idB,
});
await setDoc(docItem(idB), {
id: idB,
next_id: idC,
});
await setDoc(docItem(idC), {
id: idC,
next_id: null,
});
};
let tryCount = 0;
const testRun = async () => {
flowLog(`(${++tryCount}) trying...`, true);
await runTransaction(firestore, async (t) => {
/*
A -> B -> C -> null
to
A -> null
*/
await t.delete(docItem(idB));
await t.delete(docItem(idC));
await t.set(docItem(idA), {
id: idA,
next_id: null,
});
await t.set(docSheet, {
last_run: nanoid(),
});
});
flowLog("transaction completed");
idB = nanoid();
idC = nanoid();
let getDocsStarted = Date.now();
await getDocs(query(colItems, where("next_id", "==", null)));
const duration = Date.now() - getDocsStarted;
flowLog(`getDocs took ${duration}ms`);
return duration;
};
const main = async () => {
flowLog("------ START ------");
while (true) {
const duration = await testRun();
if (duration > 1000 * 5) {
console.error(`getDocs took too long (${duration}ms). exit.`);
break;
}
const randomWait = new Promise((r) =>
setTimeout(r, 500 + 100 * Math.floor(Math.random() * 10))
);
await randomWait;
await resetTestData();
}
};
setDoc(doc(firestore, "sheet_rules", sheetId), {
create: true,
delete: true,
read: true,
update: true,
}).then(() => {
onSnapshot(colItems, async (qs) => {
const r = [];
qs.forEach((doc) => {
r.push({ id: doc.id, ...doc.data() });
});
console.group(
`onSnapshot(/sheets/${sheetId}/items) size=${r.length}`
);
qs.docChanges().forEach((change) => {
console.log(
`%c${change.type}`,
`color:${
{
added: "#0f0",
modified: "#0ff",
removed: "#f60",
}[change.type]
};background-color:#000;`,
change.doc.data()
);
});
console.groupEnd();
if (firstRecv) {
firstRecv = false;
for (const { id } of r) {
await deleteDoc(docItem(id));
}
await resetTestData();
await new Promise((r) => setTimeout(r, 2000));
//console.clear();
main();
}
});
});
</script>
</body>
</html>
- you should be able to confirm that an error has occurred.