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:
@@ -1,31 +1,33 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useStorage } from '@/core/storage';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { AddressContact, ContactType } from '../types';
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useStorage } from "@/core/storage";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import type { AddressContact, ContactType } from "../types";
|
||||
|
||||
const PREFIX = 'contact:';
|
||||
const PREFIX = "contact:";
|
||||
|
||||
export interface ContactFilters {
|
||||
search: string;
|
||||
type: ContactType | 'all';
|
||||
type: ContactType | "all";
|
||||
}
|
||||
|
||||
export function useContacts() {
|
||||
const storage = useStorage('address-book');
|
||||
const storage = useStorage("address-book");
|
||||
const [contacts, setContacts] = useState<AddressContact[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState<ContactFilters>({ search: '', type: 'all' });
|
||||
const [filters, setFilters] = useState<ContactFilters>({
|
||||
search: "",
|
||||
type: "all",
|
||||
});
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const keys = await storage.list();
|
||||
const all = await storage.exportAll();
|
||||
const results: AddressContact[] = [];
|
||||
for (const key of keys) {
|
||||
if (key.startsWith(PREFIX)) {
|
||||
const item = await storage.get<AddressContact>(key);
|
||||
if (item) results.push(item);
|
||||
for (const [key, value] of Object.entries(all)) {
|
||||
if (key.startsWith(PREFIX) && value) {
|
||||
results.push(value as AddressContact);
|
||||
}
|
||||
}
|
||||
results.sort((a, b) => a.name.localeCompare(b.name));
|
||||
@@ -34,41 +36,60 @@ export function useContacts() {
|
||||
}, [storage]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
useEffect(() => { refresh(); }, [refresh]);
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const addContact = useCallback(async (data: Omit<AddressContact, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
const now = new Date().toISOString();
|
||||
const contact: AddressContact = { ...data, id: uuid(), createdAt: now, updatedAt: now };
|
||||
await storage.set(`${PREFIX}${contact.id}`, contact);
|
||||
await refresh();
|
||||
return contact;
|
||||
}, [storage, refresh]);
|
||||
const addContact = useCallback(
|
||||
async (data: Omit<AddressContact, "id" | "createdAt" | "updatedAt">) => {
|
||||
const now = new Date().toISOString();
|
||||
const contact: AddressContact = {
|
||||
...data,
|
||||
id: uuid(),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await storage.set(`${PREFIX}${contact.id}`, contact);
|
||||
await refresh();
|
||||
return contact;
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
const updateContact = useCallback(async (id: string, updates: Partial<AddressContact>) => {
|
||||
const existing = contacts.find((c) => c.id === id);
|
||||
if (!existing) return;
|
||||
const updated: AddressContact = {
|
||||
...existing,
|
||||
...updates,
|
||||
id: existing.id,
|
||||
createdAt: existing.createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await storage.set(`${PREFIX}${id}`, updated);
|
||||
await refresh();
|
||||
}, [storage, refresh, contacts]);
|
||||
const updateContact = useCallback(
|
||||
async (id: string, updates: Partial<AddressContact>) => {
|
||||
const existing = contacts.find((c) => c.id === id);
|
||||
if (!existing) return;
|
||||
const updated: AddressContact = {
|
||||
...existing,
|
||||
...updates,
|
||||
id: existing.id,
|
||||
createdAt: existing.createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await storage.set(`${PREFIX}${id}`, updated);
|
||||
await refresh();
|
||||
},
|
||||
[storage, refresh, contacts],
|
||||
);
|
||||
|
||||
const removeContact = useCallback(async (id: string) => {
|
||||
await storage.delete(`${PREFIX}${id}`);
|
||||
await refresh();
|
||||
}, [storage, refresh]);
|
||||
const removeContact = useCallback(
|
||||
async (id: string) => {
|
||||
await storage.delete(`${PREFIX}${id}`);
|
||||
await refresh();
|
||||
},
|
||||
[storage, refresh],
|
||||
);
|
||||
|
||||
const updateFilter = useCallback(<K extends keyof ContactFilters>(key: K, value: ContactFilters[K]) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
}, []);
|
||||
const updateFilter = useCallback(
|
||||
<K extends keyof ContactFilters>(key: K, value: ContactFilters[K]) => {
|
||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const filteredContacts = contacts.filter((c) => {
|
||||
if (filters.type !== 'all' && c.type !== filters.type) return false;
|
||||
if (filters.type !== "all" && c.type !== filters.type) return false;
|
||||
if (filters.search) {
|
||||
const q = filters.search.toLowerCase();
|
||||
return (
|
||||
@@ -76,12 +97,22 @@ export function useContacts() {
|
||||
c.company.toLowerCase().includes(q) ||
|
||||
c.email.toLowerCase().includes(q) ||
|
||||
c.phone.includes(q) ||
|
||||
(c.department ?? '').toLowerCase().includes(q) ||
|
||||
(c.role ?? '').toLowerCase().includes(q)
|
||||
(c.department ?? "").toLowerCase().includes(q) ||
|
||||
(c.role ?? "").toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return { contacts: filteredContacts, allContacts: contacts, loading, filters, updateFilter, addContact, updateContact, removeContact, refresh };
|
||||
return {
|
||||
contacts: filteredContacts,
|
||||
allContacts: contacts,
|
||||
loading,
|
||||
filters,
|
||||
updateFilter,
|
||||
addContact,
|
||||
updateContact,
|
||||
removeContact,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user