added instant sync and fixed saving debounce
This commit is contained in:
+111
-20
@@ -287,6 +287,94 @@ export const getCombinedScore = (task: Task): number => {
|
||||
|
||||
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 () => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
|
||||
@@ -305,7 +393,10 @@ export const initStore = async () => {
|
||||
|
||||
// Heartbeat - Ensure only one interval is running
|
||||
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
||||
heartbeatInterval = setInterval(() => setNow(Date.now()), 60000);
|
||||
heartbeatInterval = setInterval(() => {
|
||||
setNow(Date.now());
|
||||
checkRecurringTasks(); // Check for recurrence triggers (minutely, etc.)
|
||||
}, 60000);
|
||||
|
||||
try {
|
||||
// 1. Fetch Preferences from User Profile (Weights, Scale, Tags)
|
||||
@@ -367,19 +458,7 @@ export const initStore = async () => {
|
||||
content: meta.content || ""
|
||||
});
|
||||
} else {
|
||||
allTasks.push({
|
||||
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
|
||||
});
|
||||
allTasks.push(mapRecordToTask(r));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -389,6 +468,9 @@ export const initStore = async () => {
|
||||
// Check recurring tasks after load
|
||||
checkRecurringTasks();
|
||||
|
||||
// 3. Subscribe to Realtime Updates
|
||||
await subscribeToRealtime();
|
||||
|
||||
// 4. Reconstructive Migration: Verify all tags in tasks have definitions
|
||||
const foundTagNames = new Set<string>();
|
||||
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)
|
||||
setStore("tasks", (t) => t.map(task => task.id === tempId ? {
|
||||
...task,
|
||||
id: record.id,
|
||||
created: record.created,
|
||||
updated: record.updated
|
||||
} : task));
|
||||
setStore("tasks", (t) => {
|
||||
// Check if REAL ID exists already (from subscription race)
|
||||
const realExists = t.some(x => x.id === record.id);
|
||||
if (realExists) {
|
||||
// If real exists, just remove temp.
|
||||
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) {
|
||||
console.error("create failed", err);
|
||||
toast.error("Failed to save task.");
|
||||
|
||||
Reference in New Issue
Block a user