perf: fix N+1 query pattern across all modules + rack numbering

CRITICAL PERF BUG: Every hook did storage.list() (1 HTTP call fetching ALL
items with values, discarding values, returning only keys) then storage.get()
for EACH key (N individual HTTP calls re-fetching values one by one).

With 6 entries + contacts + tags, Registratura page fired ~40 sequential
HTTP requests on load, where 3 would suffice.

Fix: Replace list()+N*get() with single exportAll() call in ALL hooks:
- registratura/registry-service.ts (added exportAll to RegistryStorage interface)
- address-book/use-contacts.ts
- it-inventory/use-inventory.ts
- password-vault/use-vault.ts
- word-templates/use-templates.ts
- prompt-generator/use-prompt-generator.ts
- hot-desk/use-reservations.ts
- email-signature/use-saved-signatures.ts
- digital-signatures/use-signatures.ts
- ai-chat/use-chat.ts
- core/tagging/tag-service.ts (uses storage.export())

Additional fixes:
- registratura/use-registry.ts: addEntry uses optimistic local state update
  instead of double-refresh; closeEntry batches saves with Promise.all +
  single refresh
- server-rack.tsx: reversed slot rendering so U1 is at bottom (standard
  rack numbering, per user's physical rack)

Performance impact: ~90% reduction in HTTP requests on page load for all modules
This commit is contained in:
AI Assistant
2026-02-27 22:26:11 +02:00
parent 4cd793fbbc
commit c45a30ec14
13 changed files with 580 additions and 354 deletions
+27 -18
View File
@@ -59,7 +59,8 @@ export function useRegistry() {
async (
data: Omit<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
) => {
// Fetch fresh entries to prevent duplicate numbers from stale state
// Fetch fresh entries to prevent duplicate numbers from stale state.
// This single call replaces what was previously list() + N×get().
const freshEntries = await getAllEntries(storage);
const now = new Date().toISOString();
const number = generateRegistryNumber(
@@ -76,10 +77,11 @@ export function useRegistry() {
updatedAt: now,
};
await saveEntry(storage, entry);
await refresh();
// Update local state directly to avoid a second full fetch
setEntries((prev) => [entry, ...prev]);
return entry;
},
[storage, refresh],
[storage],
);
const updateEntry = useCallback(
@@ -108,29 +110,36 @@ export function useRegistry() {
[storage, refresh],
);
/** Close an entry and optionally its linked entries */
/** Close an entry and optionally its linked entries.
* Batches all saves, then does a single refresh at the end.
*/
const closeEntry = useCallback(
async (id: string, closeLinked: boolean) => {
const entry = entries.find((e) => e.id === id);
if (!entry) return;
await updateEntry(id, { status: "inchis" });
// Save main entry as closed
const now = new Date().toISOString();
const closedMain: RegistryEntry = {
...entry,
status: "inchis",
updatedAt: now,
};
await saveEntry(storage, closedMain);
// Close linked entries in parallel
const linked = entry.linkedEntryIds ?? [];
if (closeLinked && linked.length > 0) {
for (const linkedId of linked) {
const linked = entries.find((e) => e.id === linkedId);
if (linked && linked.status !== "inchis") {
const updatedLinked: RegistryEntry = {
...linked,
status: "inchis",
updatedAt: new Date().toISOString(),
};
await saveEntry(storage, updatedLinked);
}
}
await refresh();
const saves = linked
.map((linkedId) => entries.find((e) => e.id === linkedId))
.filter((e): e is RegistryEntry => !!e && e.status !== "inchis")
.map((e) =>
saveEntry(storage, { ...e, status: "inchis", updatedAt: now }),
);
await Promise.all(saves);
}
// Single refresh at the end
await refresh();
},
[entries, updateEntry, storage, refresh],
[entries, storage, refresh],
);
const updateFilter = useCallback(
@@ -8,17 +8,22 @@ export interface RegistryStorage {
set<T>(key: string, value: T): Promise<void>;
delete(key: string): Promise<void>;
list(): Promise<string[]>;
exportAll(): Promise<Record<string, unknown>>;
}
/**
* Load all registry entries in a SINGLE request.
* Uses exportAll() which fetches namespace data in one HTTP call,
* avoiding the N+1 pattern (list keys → get each one individually).
*/
export async function getAllEntries(
storage: RegistryStorage,
): Promise<RegistryEntry[]> {
const keys = await storage.list();
const all = await storage.exportAll();
const entries: RegistryEntry[] = [];
for (const key of keys) {
if (key.startsWith(STORAGE_PREFIX)) {
const entry = await storage.get<RegistryEntry>(key);
if (entry) entries.push(entry);
for (const [key, value] of Object.entries(all)) {
if (key.startsWith(STORAGE_PREFIX) && value) {
entries.push(value as RegistryEntry);
}
}
entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt));