mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 11:19:39 +02:00
perf(mana/web): index updatedAt for recent-X dashboard widgets
Schema bump V9 adds an updatedAt secondary index to the six tables
that the cross-app dashboard widgets use for "recent N" lookups:
conversations, images, presiDecks, documents, songs, mukkePlaylists.
Dexie builds the index lazily on first open — no migration code,
no data touched.
Recent-query refactor:
useRecentConversations
useRecentImages
useRecentDecks
useRecentDocuments
All four switched from `toArray() + JS sort + slice` to
`orderBy('updatedAt').reverse().filter().limit()`. Dexie walks the
BTree backwards and short-circuits as soon as `limit` matches
accumulate, so the cost is O(limit + filtered) instead of O(table).
For a dashboard with thousands of stored conversations or images,
the dashboard widget previously read every record on every render
(liveQuery re-runs on any write). Now it stops after 5–6 hits.
Verified: 20/20 sync.test.ts still passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
473b8c0091
commit
42c9eb1e17
2 changed files with 56 additions and 16 deletions
|
|
@ -117,11 +117,18 @@ export function useFavoriteContacts(limit = 5) {
|
|||
/** Recent conversations, sorted by updatedAt desc. */
|
||||
export function useRecentConversations(limit = 5) {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const all = await db.table<LocalConversation>('conversations').toArray();
|
||||
return all
|
||||
// Walk the indexed updatedAt BTree in reverse, filtering archived /
|
||||
// soft-deleted entries on the fly. Dexie's `.limit()` short-circuits
|
||||
// the iterator as soon as that many matches accumulate, so the cost
|
||||
// is O(limit + filtered) instead of the O(table) toArray+sort the
|
||||
// query used to do.
|
||||
return db
|
||||
.table<LocalConversation>('conversations')
|
||||
.orderBy('updatedAt')
|
||||
.reverse()
|
||||
.filter((c) => !c.isArchived && !c.deletedAt)
|
||||
.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
|
||||
.slice(0, limit);
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}, [] as LocalConversation[]);
|
||||
}
|
||||
|
||||
|
|
@ -145,11 +152,16 @@ export function useRandomFavorite() {
|
|||
/** Recent generated images. */
|
||||
export function useRecentImages(limit = 6) {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const all = await db.table<LocalImage>('images').toArray();
|
||||
return all
|
||||
// Reverse-walk the indexed updatedAt column. Generated images have
|
||||
// updatedAt stamped on creation and rarely move afterwards, so this
|
||||
// is effectively "newest first" for the dashboard widget's purpose.
|
||||
return db
|
||||
.table<LocalImage>('images')
|
||||
.orderBy('updatedAt')
|
||||
.reverse()
|
||||
.filter((i) => !i.isArchived && !i.deletedAt)
|
||||
.sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? ''))
|
||||
.slice(0, limit);
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}, [] as LocalImage[]);
|
||||
}
|
||||
|
||||
|
|
@ -231,11 +243,13 @@ export function useMusicStats() {
|
|||
/** Recent presentation decks. */
|
||||
export function useRecentDecks(limit = 5) {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const all = await db.table<LocalPresiDeck>('presiDecks').toArray();
|
||||
return all
|
||||
return db
|
||||
.table<LocalPresiDeck>('presiDecks')
|
||||
.orderBy('updatedAt')
|
||||
.reverse()
|
||||
.filter((d) => !d.deletedAt)
|
||||
.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
|
||||
.slice(0, limit);
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}, [] as LocalPresiDeck[]);
|
||||
}
|
||||
|
||||
|
|
@ -244,11 +258,13 @@ export function useRecentDecks(limit = 5) {
|
|||
/** Recent documents + spaces. */
|
||||
export function useRecentDocuments(limit = 5) {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const all = await db.table<LocalDocument>('documents').toArray();
|
||||
return all
|
||||
return db
|
||||
.table<LocalDocument>('documents')
|
||||
.orderBy('updatedAt')
|
||||
.reverse()
|
||||
.filter((d) => !d.deletedAt)
|
||||
.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
|
||||
.slice(0, limit);
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
}, [] as LocalDocument[]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -442,6 +442,30 @@ db.version(8).stores({
|
|||
_eventsTombstones: 'id, token, attempts, createdAt',
|
||||
});
|
||||
|
||||
// ─── Version 9: Add updatedAt indexes for "recent X" dashboard widgets ─
|
||||
//
|
||||
// Several cross-app queries (`useRecentConversations`, `useRecentImages`,
|
||||
// `useRecentDecks`, `useRecentDocuments`) used to load entire tables and
|
||||
// JS-sort by `updatedAt`. With these indexes Dexie can walk the BTree in
|
||||
// reverse and stop after N matches.
|
||||
//
|
||||
// `++` is NOT used — we are only adding secondary indexes to existing
|
||||
// stores. The full `stores()` line is repeated because Dexie's upgrade
|
||||
// API requires the complete schema for the version, even when most
|
||||
// fields are unchanged.
|
||||
//
|
||||
// No data migration needed: indexes are built lazily by Dexie at upgrade
|
||||
// time without touching record contents.
|
||||
|
||||
db.version(9).stores({
|
||||
conversations: 'id, isArchived, isPinned, spaceId, templateId, updatedAt',
|
||||
images: 'id, isFavorite, isPublic, isArchived, prompt, updatedAt',
|
||||
presiDecks: 'id, isPublic, updatedAt',
|
||||
documents: 'id, spaceId, type, pinned, title, [spaceId+type], updatedAt',
|
||||
songs: 'id, artist, album, genre, favorite, title, updatedAt',
|
||||
mukkePlaylists: 'id, name, updatedAt',
|
||||
});
|
||||
|
||||
// ─── Sync App Map ──────────────────────────────────────────
|
||||
// Maps each table to its appId for sync routing.
|
||||
// The SyncEngine uses this to group pending changes and push to /sync/{appId}.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue