first attempt at stream sidebar, tasks, and events

This commit is contained in:
Arjun Patel
2026-06-11 22:38:55 -07:00
parent 9e3a30f74e
commit 1baae790c6
219 changed files with 7463 additions and 809 deletions
+26 -7
View File
@@ -16,6 +16,7 @@ import {
arrayRemove,
FieldPath,
type DocumentData,
type DocumentSnapshot,
type FirestoreDataConverter,
type QueryDocumentSnapshot,
type SnapshotOptions,
@@ -129,6 +130,18 @@ const particleConverter: FirestoreDataConverter<Particle> = {
},
};
// Parse a snapshot defensively: newer clients may write particle types this
// app version doesn't know yet. A single unparseable doc must not break the
// whole subscription, so skip it instead of throwing inside onSnapshot.
function safeData(snap: DocumentSnapshot<Particle>): Particle | null {
try {
return snap.data() ?? null;
} catch (error) {
console.warn(`Skipping unparseable particle ${snap.ref.path}:`, error);
return null;
}
}
// --- Typed reference helpers ---
function typedDoc(path: string) {
@@ -149,7 +162,7 @@ export function subscribeToParticle(
return onSnapshot(
typedDoc(docPath),
(snap) => {
onData(snap.exists() ? snap.data() : null);
onData(safeData(snap));
},
onError,
);
@@ -160,7 +173,7 @@ export async function getParticle(docPath: string): Promise<Particle | null> {
if (!docSnap.exists()) {
return null;
}
return docSnap.data();
return safeData(docSnap);
}
export interface GetParticleChildrenOptions {
@@ -183,7 +196,9 @@ export async function getParticleChildren(
orderBy(orderByField, orderDirection),
);
const snap = await getDocs(q);
return snap.docs.map((d) => d.data());
return snap.docs
.map((d) => safeData(d))
.filter((p): p is Particle => p !== null);
}
export interface SubscribeToParticleChildrenOptions {
@@ -229,14 +244,18 @@ export function subscribeToParticleChildren(
return onSnapshot(
q,
(snap) => {
const updatedChildren = snap.docs.map((d) => d.data());
const updatedChildren = snap.docs
.map((d) => safeData(d))
.filter((p): p is Particle => p !== null);
onData(updatedChildren);
if (onAdded || onRemoved) {
for (const change of snap.docChanges()) {
if (change.type === 'added' && onAdded) onAdded(change.doc.data());
const changed = safeData(change.doc);
if (!changed) continue;
if (change.type === 'added' && onAdded) onAdded(changed);
if (change.type === 'removed' && onRemoved)
onRemoved(change.doc.data(), updatedChildren);
onRemoved(changed, updatedChildren);
}
}
},
@@ -257,7 +276,7 @@ export function subscribeToLatestChild(
return onSnapshot(
q,
(snap) => {
onData(snap.empty ? null : snap.docs[0].data());
onData(snap.empty ? null : safeData(snap.docs[0]));
},
onError,
);