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,40 +1,45 @@
|
||||
import type { StorageService } from '@/core/storage/types';
|
||||
import type { Tag, TagCategory, TagScope } from './types';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { StorageService } from "@/core/storage/types";
|
||||
import type { Tag, TagCategory, TagScope } from "./types";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
const NAMESPACE = 'tags';
|
||||
const NAMESPACE = "tags";
|
||||
|
||||
export class TagService {
|
||||
constructor(private storage: StorageService) {}
|
||||
|
||||
async getAllTags(): Promise<Tag[]> {
|
||||
const keys = await this.storage.list(NAMESPACE);
|
||||
const all = await this.storage.export(NAMESPACE);
|
||||
const tags: Tag[] = [];
|
||||
for (const key of keys) {
|
||||
const tag = await this.storage.get<Tag>(NAMESPACE, key);
|
||||
if (tag) tags.push(tag);
|
||||
for (const value of Object.values(all)) {
|
||||
if (value) tags.push(value as Tag);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
async getTagsByCategory(category: TagCategory): Promise<Tag[]> {
|
||||
return this.storage.query<Tag>(NAMESPACE, (tag) => tag.category === category);
|
||||
return this.storage.query<Tag>(
|
||||
NAMESPACE,
|
||||
(tag) => tag.category === category,
|
||||
);
|
||||
}
|
||||
|
||||
async getTagsByScope(scope: TagScope, scopeId?: string): Promise<Tag[]> {
|
||||
return this.storage.query<Tag>(NAMESPACE, (tag) => {
|
||||
if (tag.scope !== scope) return false;
|
||||
if (scope === 'module' && scopeId) return tag.moduleId === scopeId;
|
||||
if (scope === 'company' && scopeId) return tag.companyId === scopeId;
|
||||
if (scope === "module" && scopeId) return tag.moduleId === scopeId;
|
||||
if (scope === "company" && scopeId) return tag.companyId === scopeId;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async getChildren(parentId: string): Promise<Tag[]> {
|
||||
return this.storage.query<Tag>(NAMESPACE, (tag) => tag.parentId === parentId);
|
||||
return this.storage.query<Tag>(
|
||||
NAMESPACE,
|
||||
(tag) => tag.parentId === parentId,
|
||||
);
|
||||
}
|
||||
|
||||
async createTag(data: Omit<Tag, 'id' | 'createdAt'>): Promise<Tag> {
|
||||
async createTag(data: Omit<Tag, "id" | "createdAt">): Promise<Tag> {
|
||||
const tag: Tag = {
|
||||
...data,
|
||||
id: uuid(),
|
||||
@@ -44,10 +49,17 @@ export class TagService {
|
||||
return tag;
|
||||
}
|
||||
|
||||
async updateTag(id: string, updates: Partial<Omit<Tag, 'id' | 'createdAt'>>): Promise<Tag | null> {
|
||||
async updateTag(
|
||||
id: string,
|
||||
updates: Partial<Omit<Tag, "id" | "createdAt">>,
|
||||
): Promise<Tag | null> {
|
||||
const existing = await this.storage.get<Tag>(NAMESPACE, id);
|
||||
if (!existing) return null;
|
||||
const updated: Tag = { ...existing, ...updates, updatedAt: new Date().toISOString() };
|
||||
const updated: Tag = {
|
||||
...existing,
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await this.storage.set(NAMESPACE, id, updated);
|
||||
return updated;
|
||||
}
|
||||
@@ -63,16 +75,20 @@ export class TagService {
|
||||
|
||||
async searchTags(query: string): Promise<Tag[]> {
|
||||
const lower = query.toLowerCase();
|
||||
return this.storage.query<Tag>(NAMESPACE, (tag) =>
|
||||
tag.label.toLowerCase().includes(lower) ||
|
||||
(tag.projectCode?.toLowerCase().includes(lower) ?? false)
|
||||
return this.storage.query<Tag>(
|
||||
NAMESPACE,
|
||||
(tag) =>
|
||||
tag.label.toLowerCase().includes(lower) ||
|
||||
(tag.projectCode?.toLowerCase().includes(lower) ?? false),
|
||||
);
|
||||
}
|
||||
|
||||
/** Bulk import tags (for seed data). Skips tags whose label already exists in same category. */
|
||||
async importTags(tags: Omit<Tag, 'id' | 'createdAt'>[]): Promise<number> {
|
||||
async importTags(tags: Omit<Tag, "id" | "createdAt">[]): Promise<number> {
|
||||
const existing = await this.getAllTags();
|
||||
const existingKeys = new Set(existing.map((t) => `${t.category}::${t.label}`));
|
||||
const existingKeys = new Set(
|
||||
existing.map((t) => `${t.category}::${t.label}`),
|
||||
);
|
||||
let imported = 0;
|
||||
for (const data of tags) {
|
||||
const key = `${data.category}::${data.label}`;
|
||||
|
||||
Reference in New Issue
Block a user