perf: separate blob storage for registratura attachments

Root cause: even with SQL-level stripping, PostgreSQL must TOAST-decompress
entire multi-MB JSONB values from disk before any processing. For 5 entries
with PDF attachments (25-50MB total), this takes several seconds.

Fix: store base64 attachment data in separate namespace 'registratura-blobs'.
Main entries are inherently small (~1-2KB). List queries never touch heavy data.

Changes:
- registry-service.ts: extractBlobs/mergeBlobs split base64 on save/load,
  migrateEntryBlobs() one-time migration for existing entries
- use-registry.ts: dual namespace (registratura + registratura-blobs),
  migration runs on first mount
- registratura-module.tsx: removed useContacts/useTags hooks that triggered
  2 unnecessary API fetches on page load (write-only ops use direct storage)

Before: 3 API calls on mount, one reading 25-50MB from PostgreSQL
After: 1 API call on mount, reading ~5-10KB total
This commit is contained in:
AI Assistant
2026-02-27 23:35:04 +02:00
parent 8385041bb0
commit f8c19bb5b4
3 changed files with 227 additions and 53 deletions
@@ -24,8 +24,8 @@ import {
DialogFooter,
} from "@/shared/components/ui/dialog";
import { useRegistry } from "../hooks/use-registry";
import { useContacts } from "@/modules/address-book/hooks/use-contacts";
import { useTags } from "@/core/tagging";
import { useStorage, useStorageService } from "@/core/storage";
import { v4 as uuid } from "uuid";
import { RegistryFilters } from "./registry-filters";
import { RegistryTable } from "./registry-table";
import { RegistryEntryForm } from "./registry-entry-form";
@@ -55,15 +55,16 @@ export function RegistraturaModule() {
removeDeadline,
} = useRegistry();
const { addContact } = useContacts();
const { createTag } = useTags("document-type");
// Direct storage for write-only operations — avoids loading all contacts/tags on mount
const contactStorage = useStorage("address-book");
const tagStorageService = useStorageService();
const [viewMode, setViewMode] = useState<ViewMode>("list");
const [editingEntry, setEditingEntry] = useState<RegistryEntry | null>(null);
const [closingId, setClosingId] = useState<string | null>(null);
const [linkCheckId, setLinkCheckId] = useState<string | null>(null);
// ── Bidirectional Address Book integration ──
// ── Bidirectional Address Book integration (write-only, no eager fetch) ──
const handleCreateContact = useCallback(
async (data: {
name: string;
@@ -71,7 +72,9 @@ export function RegistraturaModule() {
email: string;
}): Promise<AddressContact | undefined> => {
try {
const contact = await addContact({
const now = new Date().toISOString();
const contact: AddressContact = {
id: uuid(),
name: data.name,
company: "",
type: "collaborator",
@@ -88,30 +91,36 @@ export function RegistraturaModule() {
tags: [],
notes: "Creat automat din Registratură",
visibility: "all",
});
createdAt: now,
updatedAt: now,
};
await contactStorage.set(`contact:${contact.id}`, contact);
return contact;
} catch {
return undefined;
}
},
[addContact],
[contactStorage],
);
// ── Bidirectional Tag Manager integration ──
// ── Bidirectional Tag Manager integration (write-only, no eager fetch) ──
const handleCreateDocType = useCallback(
async (label: string) => {
try {
await createTag({
const tagId = uuid();
await tagStorageService.set("tags", tagId, {
id: tagId,
label,
category: "document-type",
scope: "global",
color: "#64748b",
createdAt: new Date().toISOString(),
});
} catch {
// tag may already exist — ignore
}
},
[createTag],
[tagStorageService],
);
const handleAdd = async (