load in optimization and error correction
This commit is contained in:
+177
-20
@@ -13,12 +13,18 @@ export const getFavoriteTag = () => {
|
||||
};
|
||||
|
||||
const LIST_FIELDS = 'id,collectionId,collectionName,created,updated,title,startDate,dueDate,priority,status,completed,tags,labelTags,shareRefs,noteRefs,createdBy,user,size,recurrence,deletedAt';
|
||||
const NOTE_LIST_FIELDS = 'id,created,updated,title,key,tags,isPrivate,user,tasks,deletedAt';
|
||||
|
||||
const getStorageKey = () => {
|
||||
const userId = pb.authStore.model?.id;
|
||||
return userId ? `${STORAGE_KEY_PREFIX}_data_${userId}` : null;
|
||||
};
|
||||
|
||||
const getPersistedStoreSnapshot = () => {
|
||||
const { isInitializing, quickloadFadeTaskIds, isNotesLoading, hasLoadedNotesMetadata, loadingNoteContentIds, ...persisted } = store;
|
||||
return persisted;
|
||||
};
|
||||
|
||||
export const [now, setNow] = createSignal(Date.now());
|
||||
// Context: 'mine' is a temporary bootstrap alias for the current user's personal context bucket.
|
||||
type TaskContext = 'mine' | { userId: string, name: string } | { bucketId: string, name: string, isPersonal?: boolean };
|
||||
@@ -395,6 +401,10 @@ export interface Bucket {
|
||||
interface TaskStore {
|
||||
tasks: Task[];
|
||||
isInitializing: boolean;
|
||||
quickloadFadeTaskIds: string[];
|
||||
isNotesLoading: boolean;
|
||||
hasLoadedNotesMetadata: boolean;
|
||||
loadingNoteContentIds: string[];
|
||||
pWeight: number;
|
||||
uWeight: number;
|
||||
matrixScaleDays: number;
|
||||
@@ -419,6 +429,10 @@ interface TaskStore {
|
||||
export const [store, setStore] = createStore<TaskStore>({
|
||||
tasks: [],
|
||||
isInitializing: false,
|
||||
quickloadFadeTaskIds: [],
|
||||
isNotesLoading: false,
|
||||
hasLoadedNotesMetadata: false,
|
||||
loadingNoteContentIds: [],
|
||||
pWeight: 1.0,
|
||||
uWeight: 1.0,
|
||||
matrixScaleDays: 30,
|
||||
@@ -639,6 +653,25 @@ const getPersonalContext = (userId = pb.authStore.model?.id) =>
|
||||
) || null
|
||||
: null;
|
||||
|
||||
export const isCurrentUserContextTag = (tag: string) => {
|
||||
const normalizedTag = tag.trim().toLowerCase();
|
||||
if (!normalizedTag.startsWith("@")) return false;
|
||||
|
||||
const personalContext = getPersonalContext();
|
||||
const currentUserDisplayName = (pb.authStore.model?.name || pb.authStore.model?.email || "").trim();
|
||||
const hiddenTags = new Set<string>();
|
||||
|
||||
if (personalContext?.displayName) {
|
||||
hiddenTags.add(buildShareTag(personalContext.displayName).toLowerCase());
|
||||
}
|
||||
|
||||
if (currentUserDisplayName) {
|
||||
hiddenTags.add(buildShareTag(currentUserDisplayName).toLowerCase());
|
||||
}
|
||||
|
||||
return hiddenTags.has(normalizedTag);
|
||||
};
|
||||
|
||||
const getSubscribedBucketContexts = () =>
|
||||
store.subscribedBuckets
|
||||
.map(id => store.contexts.find(context => context.id === id))
|
||||
@@ -779,7 +812,7 @@ createRoot(() => {
|
||||
createEffect(() => {
|
||||
const key = getStorageKey();
|
||||
if (key) {
|
||||
localStorage.setItem(key, JSON.stringify(store));
|
||||
localStorage.setItem(key, JSON.stringify(getPersistedStoreSnapshot()));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -896,6 +929,11 @@ export const getCombinedScore = (task: Task): number => {
|
||||
// -- Persistence & Sync --
|
||||
|
||||
let heartbeatInterval: number | undefined;
|
||||
let quickloadFadeTimer: number | undefined;
|
||||
let notesLoadPromise: Promise<void> | null = null;
|
||||
let deferredNotesLoadTimer: number | undefined;
|
||||
const loadedNoteDetailIds = new Set<string>();
|
||||
const failedNoteDetailIds = new Set<string>();
|
||||
|
||||
const mapRecordToTask = (r: any): Task => {
|
||||
const legacyTags: string[] = r.tags || [];
|
||||
@@ -1143,12 +1181,12 @@ const mapRecordToContext = (r: any): ShareContext => ({
|
||||
deletedBy: unwrapRelationId(r.deletedBy)
|
||||
});
|
||||
|
||||
const mapRecordToNote = (r: any): Note => {
|
||||
const mapRecordToNote = (r: any, includeContent = true): Note => {
|
||||
return {
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
key: r.key || normalizeNoteKey(r.title),
|
||||
content: r.content,
|
||||
content: includeContent ? r.content : undefined,
|
||||
tags: Array.isArray(r.tags) && r.tags.length > 0 ? r.tags : [buildNoteTag(r.key || r.title)],
|
||||
isPrivate: r.isPrivate || false,
|
||||
user: unwrapRelationId(r.user) || "",
|
||||
@@ -1159,6 +1197,22 @@ const mapRecordToNote = (r: any): Note => {
|
||||
};
|
||||
};
|
||||
|
||||
const reconcileFetchedNotes = (records: any[], includeContent = false) => {
|
||||
const existingById = new Map(store.notes.map(note => [note.id, note]));
|
||||
const nextNotes = records.map((record: any) => {
|
||||
const mapped = mapRecordToNote(record, includeContent);
|
||||
const existing = existingById.get(mapped.id);
|
||||
|
||||
if (!includeContent && existing?.content !== undefined) {
|
||||
mapped.content = existing.content;
|
||||
}
|
||||
|
||||
return mapped;
|
||||
});
|
||||
|
||||
setStore("notes", nextNotes);
|
||||
};
|
||||
|
||||
export const fetchNoteById = async (id: string): Promise<Note | null> => {
|
||||
try {
|
||||
const record = await pb.collection(NOTES_COLLECTION).getOne(id);
|
||||
@@ -1171,6 +1225,10 @@ export const fetchNoteById = async (id: string): Promise<Note | null> => {
|
||||
};
|
||||
|
||||
export const updateStoreWithNote = (note: Note) => {
|
||||
failedNoteDetailIds.delete(note.id);
|
||||
if (note.content !== undefined) {
|
||||
loadedNoteDetailIds.add(note.id);
|
||||
}
|
||||
setStore("notes", (currentNotes) => {
|
||||
const index = currentNotes.findIndex(n => n.id === note.id);
|
||||
if (index !== -1) {
|
||||
@@ -1182,6 +1240,83 @@ export const updateStoreWithNote = (note: Note) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const loadNotesMetadata = async () => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
if (store.hasLoadedNotesMetadata) return;
|
||||
if (notesLoadPromise) return notesLoadPromise;
|
||||
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
setStore("isNotesLoading", true);
|
||||
|
||||
notesLoadPromise = (async () => {
|
||||
try {
|
||||
const notesRecords = await pb.collection(NOTES_COLLECTION).getFullList({
|
||||
filter: `isPrivate = false || ${relationFilter("user", currentUserId)}`,
|
||||
sort: '-created',
|
||||
fields: NOTE_LIST_FIELDS,
|
||||
requestKey: null
|
||||
});
|
||||
|
||||
reconcileFetchedNotes(notesRecords, false);
|
||||
setStore("hasLoadedNotesMetadata", true);
|
||||
} catch (notesErr) {
|
||||
console.warn("Failed to load notes (collection might not exist yet):", notesErr);
|
||||
} finally {
|
||||
setStore("isNotesLoading", false);
|
||||
notesLoadPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return notesLoadPromise;
|
||||
};
|
||||
|
||||
export const scheduleDeferredNotesMetadataLoad = () => {
|
||||
if (!pb.authStore.isValid || store.hasLoadedNotesMetadata || notesLoadPromise || deferredNotesLoadTimer) return;
|
||||
|
||||
deferredNotesLoadTimer = window.setTimeout(() => {
|
||||
deferredNotesLoadTimer = undefined;
|
||||
void loadNotesMetadata();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
export const requestImmediateNotesMetadataLoad = async () => {
|
||||
if (deferredNotesLoadTimer) {
|
||||
clearTimeout(deferredNotesLoadTimer);
|
||||
deferredNotesLoadTimer = undefined;
|
||||
}
|
||||
|
||||
await loadNotesMetadata();
|
||||
};
|
||||
|
||||
export const loadNoteContent = async (id: string) => {
|
||||
if (!id) return;
|
||||
if (loadedNoteDetailIds.has(id)) return;
|
||||
if (failedNoteDetailIds.has(id)) return;
|
||||
if (store.loadingNoteContentIds.includes(id)) return;
|
||||
|
||||
setStore("loadingNoteContentIds", ids => ids.includes(id) ? ids : [...ids, id]);
|
||||
|
||||
try {
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
const visibilityFilter = `id = "${id}" && (isPrivate = false || ${relationFilter("user", currentUserId)})`;
|
||||
const record = await pb.collection(NOTES_COLLECTION).getFirstListItem(visibilityFilter, {
|
||||
requestKey: `note-content-${id}`
|
||||
});
|
||||
const note = mapRecordToNote(record, true);
|
||||
updateStoreWithNote(note);
|
||||
loadedNoteDetailIds.add(id);
|
||||
} catch (err) {
|
||||
const status = (err as any)?.status;
|
||||
if (status === 404) {
|
||||
failedNoteDetailIds.add(id);
|
||||
return;
|
||||
}
|
||||
console.error("Failed to load note content:", err);
|
||||
} finally {
|
||||
setStore("loadingNoteContentIds", ids => ids.filter(existingId => existingId !== id));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Helper to check if a task should be visible based on ownership, shares, or rules
|
||||
const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
|
||||
@@ -1314,10 +1449,16 @@ export const subscribeToRealtime = async () => {
|
||||
|
||||
if (e.action === 'create' || e.action === 'update') {
|
||||
if (isVisible) {
|
||||
const updatedNote = mapRecordToNote(e.record);
|
||||
const shouldSyncContent = loadedNoteDetailIds.has(e.record.id) || activeNoteId() === e.record.id;
|
||||
const updatedNote = mapRecordToNote(e.record, shouldSyncContent);
|
||||
setStore("notes", prev => {
|
||||
const exists = prev.find(n => n.id === updatedNote.id);
|
||||
if (exists) return prev.map(n => n.id === updatedNote.id ? updatedNote : n);
|
||||
if (exists) {
|
||||
const mergedNote = shouldSyncContent || exists.content === undefined
|
||||
? updatedNote
|
||||
: { ...updatedNote, content: exists.content };
|
||||
return prev.map(n => n.id === updatedNote.id ? mergedNote : n);
|
||||
}
|
||||
return [updatedNote, ...prev];
|
||||
});
|
||||
} else {
|
||||
@@ -1616,6 +1757,7 @@ export const initStore = async () => {
|
||||
setStore("isInitializing", true);
|
||||
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
const refreshedTaskIds = new Set<string>();
|
||||
|
||||
// STAGE 1: Instant Focus from Local Auth Model (Top 20 + Fallback)
|
||||
// Goal: < 100ms First Paint. Doesn't wait for any other network requests.
|
||||
@@ -1649,6 +1791,13 @@ export const initStore = async () => {
|
||||
.filter(t => !t.tags?.includes("__template__"));
|
||||
|
||||
setStore("tasks", focusTasks);
|
||||
setStore("quickloadFadeTaskIds", focusTasks.map(task => task.id));
|
||||
focusTasks.forEach(task => refreshedTaskIds.add(task.id));
|
||||
if (quickloadFadeTimer) clearTimeout(quickloadFadeTimer);
|
||||
quickloadFadeTimer = window.setTimeout(() => {
|
||||
setStore("quickloadFadeTaskIds", []);
|
||||
quickloadFadeTimer = undefined;
|
||||
}, 200);
|
||||
} catch (focusErr) {
|
||||
console.warn("Stage 1 load failed:", focusErr);
|
||||
}
|
||||
@@ -1660,7 +1809,17 @@ export const initStore = async () => {
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved) {
|
||||
try {
|
||||
setStore(JSON.parse(saved));
|
||||
const parsed = JSON.parse(saved);
|
||||
const cachedTasks = Array.isArray(parsed?.tasks) ? parsed.tasks : [];
|
||||
const mergedCachedTasks = [
|
||||
...store.tasks,
|
||||
...cachedTasks.filter((task: Task) => !refreshedTaskIds.has(task.id))
|
||||
];
|
||||
setStore({
|
||||
...parsed,
|
||||
tasks: mergedCachedTasks,
|
||||
isInitializing: true
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to parse cached data", e);
|
||||
}
|
||||
@@ -1739,19 +1898,6 @@ export const initStore = async () => {
|
||||
console.warn("Failed to load contexts (collection might not exist yet):", contextErr);
|
||||
}
|
||||
|
||||
// 1.3 Fetch Notes
|
||||
try {
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
const notesRecords = await pb.collection(NOTES_COLLECTION).getFullList({
|
||||
filter: `isPrivate = false || ${relationFilter("user", currentUserId)}`,
|
||||
sort: '-created',
|
||||
requestKey: null
|
||||
});
|
||||
setStore("notes", notesRecords.map(mapRecordToNote));
|
||||
} catch (notesErr) {
|
||||
console.warn("Failed to load notes (collection might not exist yet):", notesErr);
|
||||
}
|
||||
|
||||
setStore("tagDefinitions", defs => {
|
||||
const plainDefs = defs.filter(def => !def.name.startsWith("@"));
|
||||
return [...plainDefs, ...deriveSystemTagDefinitions(store.contexts)];
|
||||
@@ -1814,6 +1960,7 @@ export const initStore = async () => {
|
||||
const taskMap = new Map(currentTasks.map(t => [t.id, t]));
|
||||
allIncomplete.forEach((r: any) => {
|
||||
if (r.tags?.includes("__template__")) return;
|
||||
if (refreshedTaskIds.has(r.id)) return;
|
||||
|
||||
const existing = taskMap.get(r.id);
|
||||
|
||||
@@ -1828,13 +1975,16 @@ export const initStore = async () => {
|
||||
newTask.content = existing.content;
|
||||
}
|
||||
taskMap.set(r.id, newTask);
|
||||
refreshedTaskIds.add(r.id);
|
||||
});
|
||||
return Array.from(taskMap.values());
|
||||
});
|
||||
|
||||
// 2b. Content Backfill for Incomplete Tasks
|
||||
// Fetch content for tasks that don't have it yet, in chunks
|
||||
const tasksNeedingContent = store.tasks.filter(t => !t.completed && t.content === undefined).map(t => t.id);
|
||||
const tasksNeedingContent = store.tasks
|
||||
.filter(t => !t.completed && t.content === undefined && !refreshedTaskIds.has(t.id))
|
||||
.map(t => t.id);
|
||||
if (tasksNeedingContent.length > 0) {
|
||||
// Fetch in chunks of 20 to avoid URL length limits
|
||||
const chunkSize = 20;
|
||||
@@ -1936,6 +2086,9 @@ export const initStore = async () => {
|
||||
|
||||
// 2.8 Fetch Filter Templates
|
||||
await loadFilterTemplates();
|
||||
|
||||
// Defer note metadata sync until task loading is fully settled.
|
||||
scheduleDeferredNotesMetadataLoad();
|
||||
} catch (err) {
|
||||
if (store.tasks.length === 0) {
|
||||
console.error("Failed to load data:", err);
|
||||
@@ -2543,6 +2696,10 @@ export const removeTagDefinition = async (name: string) => {
|
||||
export const updateNote = async (id: string, updates: Partial<Note>) => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
|
||||
if (updates.content !== undefined) {
|
||||
loadedNoteDetailIds.add(id);
|
||||
}
|
||||
|
||||
// Optimistic update
|
||||
const optimisticUpdates = {
|
||||
...updates,
|
||||
|
||||
Reference in New Issue
Block a user