added instant sync and fixed saving debounce

This commit is contained in:
2026-02-04 16:21:41 -06:00
parent e7b0967bb3
commit 54a0da9896
2 changed files with 131 additions and 22 deletions
+20 -2
View File
@@ -42,13 +42,22 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
}); });
let debounceTimer: number | undefined; let debounceTimer: number | undefined;
let pendingContent: string | null = null;
const updateTaskContent = (html: string) => { const updateTaskContent = (html: string) => {
pendingContent = html;
clearTimeout(debounceTimer); clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => { debounceTimer = setTimeout(() => {
updateTask(props.task.id, { content: html }); if (pendingContent !== null) {
updateTask(props.task.id, { content: pendingContent });
pendingContent = null;
}
}, 1000); }, 1000);
}; };
// Removed onCleanup to avoid lockup
const updateTitle = (val: string) => { const updateTitle = (val: string) => {
setTitle(val); setTitle(val);
updateTask(props.task.id, { title: val }); updateTask(props.task.id, { title: val });
@@ -87,7 +96,16 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
const currentUrgency = () => calculateUrgencyFromDate(props.task.dueDate).toString(); const currentUrgency = () => calculateUrgencyFromDate(props.task.dueDate).toString();
return ( return (
<Sheet open={props.isOpen} onOpenChange={(open) => !open && props.onClose()}> <Sheet open={props.isOpen} onOpenChange={(open) => {
if (!open) {
// Flash save before closing to avoid lockup
if (pendingContent !== null) {
updateTask(props.task.id, { content: pendingContent });
pendingContent = null;
}
props.onClose();
}
}}>
<SheetContent <SheetContent
hideCloseButton hideCloseButton
onOpenAutoFocus={(e) => { onOpenAutoFocus={(e) => {
+111 -20
View File
@@ -287,6 +287,94 @@ export const getCombinedScore = (task: Task): number => {
let heartbeatInterval: number | undefined; let heartbeatInterval: number | undefined;
const mapRecordToTask = (r: any): Task => {
// Check if it's a template? The caller handles filtering templates usually,
// but here we just map fields.
const tags = r.tags || [];
return {
id: r.id,
title: r.title,
startDate: r.startDate,
dueDate: r.dueDate,
priority: r.priority,
completed: r.completed,
tags: tags,
content: r.content,
deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : undefined,
created: r.created,
updated: r.updated,
recurrence: r.recurrence
};
};
export const subscribeToRealtime = async () => {
// Unsubscribe first to avoid duplicates
await pb.collection(TASGRID_COLLECTION).unsubscribe('*');
pb.collection(TASGRID_COLLECTION).subscribe('*', (e) => {
console.log("Realtime event:", e.action, e.record.id);
if (e.action === 'create') {
// Check if it's a template
const isTemplate = e.record.tags?.includes("__template__");
if (isTemplate) {
// We don't auto-add templates to the main task list usually?
// Wait, `store.templates` is separate.
// We should handle templates too if we want sync on templates.
let meta: any = {};
try { meta = JSON.parse(e.record.content || "{}"); } catch (e) { }
const newTemplate: TaskTemplate = {
id: e.record.id,
name: e.record.title,
title: meta.title || "",
priority: e.record.priority,
urgency: meta.urgency || 5,
tags: meta.tags || [],
content: meta.content || ""
};
setStore("templates", t => [...(t || []), newTemplate]);
return;
}
// Regular Task
const exists = store.tasks.find(t => t.id === e.record.id);
if (!exists) {
const newTask = mapRecordToTask(e.record);
setStore("tasks", t => [newTask, ...t]);
}
}
if (e.action === 'update') {
// Is it a template?
const isTemplate = e.record.tags?.includes("__template__");
if (isTemplate) {
// Update template store
let meta: any = {};
try { meta = JSON.parse(e.record.content || "{}"); } catch (e) { }
setStore("templates", t => t.id === e.record.id, {
name: e.record.title,
title: meta.title || "",
priority: e.record.priority,
urgency: meta.urgency || 5,
tags: meta.tags || [],
content: meta.content || ""
});
return;
}
// Normal Task
const updatedTask = mapRecordToTask(e.record);
setStore("tasks", t => t.id === e.record.id, updatedTask);
}
if (e.action === 'delete') {
// Try deleting from both just in case
setStore("tasks", t => t.filter(x => x.id !== e.record.id));
setStore("templates", t => (t || []).filter(x => x.id !== e.record.id));
}
});
};
export const initStore = async () => { export const initStore = async () => {
if (!pb.authStore.isValid) return; if (!pb.authStore.isValid) return;
@@ -305,7 +393,10 @@ export const initStore = async () => {
// Heartbeat - Ensure only one interval is running // Heartbeat - Ensure only one interval is running
if (heartbeatInterval) clearInterval(heartbeatInterval); if (heartbeatInterval) clearInterval(heartbeatInterval);
heartbeatInterval = setInterval(() => setNow(Date.now()), 60000); heartbeatInterval = setInterval(() => {
setNow(Date.now());
checkRecurringTasks(); // Check for recurrence triggers (minutely, etc.)
}, 60000);
try { try {
// 1. Fetch Preferences from User Profile (Weights, Scale, Tags) // 1. Fetch Preferences from User Profile (Weights, Scale, Tags)
@@ -367,19 +458,7 @@ export const initStore = async () => {
content: meta.content || "" content: meta.content || ""
}); });
} else { } else {
allTasks.push({ allTasks.push(mapRecordToTask(r));
id: r.id,
title: r.title,
startDate: r.startDate,
dueDate: r.dueDate,
priority: r.priority,
completed: r.completed,
tags: tags,
content: r.content,
deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : undefined,
created: r.created,
updated: r.updated
});
} }
}); });
@@ -389,6 +468,9 @@ export const initStore = async () => {
// Check recurring tasks after load // Check recurring tasks after load
checkRecurringTasks(); checkRecurringTasks();
// 3. Subscribe to Realtime Updates
await subscribeToRealtime();
// 4. Reconstructive Migration: Verify all tags in tasks have definitions // 4. Reconstructive Migration: Verify all tags in tasks have definitions
const foundTagNames = new Set<string>(); const foundTagNames = new Set<string>();
allTasks.forEach(t => t.tags?.forEach(tag => foundTagNames.add(tag))); allTasks.forEach(t => t.tags?.forEach(tag => foundTagNames.add(tag)));
@@ -458,12 +540,21 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
}); });
// Replace temp task with real record (merging server fields like id, created, updated) // Replace temp task with real record (merging server fields like id, created, updated)
setStore("tasks", (t) => t.map(task => task.id === tempId ? { setStore("tasks", (t) => {
...task, // Check if REAL ID exists already (from subscription race)
id: record.id, const realExists = t.some(x => x.id === record.id);
created: record.created, if (realExists) {
updated: record.updated // If real exists, just remove temp.
} : task)); return t.filter(x => x.id !== tempId);
}
// Otherwise, replace temp with real
return t.map(task => task.id === tempId ? {
...task,
id: record.id,
created: record.created,
updated: record.updated
} : task);
});
} catch (err) { } catch (err) {
console.error("create failed", err); console.error("create failed", err);
toast.error("Failed to save task."); toast.error("Failed to save task.");