perf(registratura): lightweight API mode strips base64 attachments from list

ROOT CAUSE: RegistryEntry stores file attachments as base64 strings in JSON.
A single 5MB PDF becomes ~6.7MB of base64. With 6 entries, the exportAll()
endpoint was sending 30-60MB of JSON on every page load  taking 2+ minutes.

Fix: Added ?lightweight=true parameter to /api/storage GET endpoint.
When enabled, stripHeavyFields() recursively removes large 'data' and
'fileData' string fields (>1KB) from JSON values, replacing with '__stripped__'.

Changes:
- /api/storage route.ts: stripHeavyFields() + lightweight query param
- StorageService.export(): accepts { lightweight?: boolean } option
- DatabaseStorageAdapter.export(): passes lightweight flag to API
- LocalStorageAdapter.export(): accepts option (no-op, localStorage is fast)
- useStorage.exportAll(): passes options through
- registry-service.ts: getAllEntries() uses lightweight=true by default
- registry-service.ts: new getFullEntry() loads single entry with full data
- use-registry.ts: exports loadFullEntry() for on-demand full loading
- registratura-module.tsx: handleEdit/handleNavigateEntry load full entry

Result: List loading transfers ~100KB instead of 30-60MB. Editing loads
full data for a single entry on demand (~5-10MB for one entry vs all).
This commit is contained in:
AI Assistant
2026-02-27 22:37:39 +02:00
parent db9bcd7192
commit c22848b471
8 changed files with 128 additions and 39 deletions
@@ -82,11 +82,16 @@ export class DatabaseStorageAdapter implements StorageService {
}
}
async export(namespace: string): Promise<Record<string, unknown>> {
async export(
namespace: string,
options?: { lightweight?: boolean },
): Promise<Record<string, unknown>> {
try {
const res = await fetch(
`/api/storage?namespace=${encodeURIComponent(namespace)}`,
);
let url = `/api/storage?namespace=${encodeURIComponent(namespace)}`;
if (options?.lightweight) {
url += "&lightweight=true";
}
const res = await fetch(url);
if (!res.ok) return {};
const data = await res.json();
return data.items || {};
+18 -9
View File
@@ -1,4 +1,4 @@
import type { StorageService } from '../types';
import type { StorageService } from "../types";
function nsKey(namespace: string, key: string): string {
return `architools:${namespace}:${key}`;
@@ -10,7 +10,7 @@ function nsPrefix(namespace: string): string {
export class LocalStorageAdapter implements StorageService {
async get<T>(namespace: string, key: string): Promise<T | null> {
if (typeof window === 'undefined') return null;
if (typeof window === "undefined") return null;
try {
const raw = window.localStorage.getItem(nsKey(namespace, key));
if (raw === null) return null;
@@ -21,17 +21,17 @@ export class LocalStorageAdapter implements StorageService {
}
async set<T>(namespace: string, key: string, value: T): Promise<void> {
if (typeof window === 'undefined') return;
if (typeof window === "undefined") return;
window.localStorage.setItem(nsKey(namespace, key), JSON.stringify(value));
}
async delete(namespace: string, key: string): Promise<void> {
if (typeof window === 'undefined') return;
if (typeof window === "undefined") return;
window.localStorage.removeItem(nsKey(namespace, key));
}
async list(namespace: string): Promise<string[]> {
if (typeof window === 'undefined') return [];
if (typeof window === "undefined") return [];
const prefix = nsPrefix(namespace);
const keys: string[] = [];
for (let i = 0; i < window.localStorage.length; i++) {
@@ -43,7 +43,10 @@ export class LocalStorageAdapter implements StorageService {
return keys;
}
async query<T>(namespace: string, predicate: (item: T) => boolean): Promise<T[]> {
async query<T>(
namespace: string,
predicate: (item: T) => boolean,
): Promise<T[]> {
const keys = await this.list(namespace);
const results: T[] = [];
for (const key of keys) {
@@ -56,14 +59,17 @@ export class LocalStorageAdapter implements StorageService {
}
async clear(namespace: string): Promise<void> {
if (typeof window === 'undefined') return;
if (typeof window === "undefined") return;
const keys = await this.list(namespace);
for (const key of keys) {
window.localStorage.removeItem(nsKey(namespace, key));
}
}
async export(namespace: string): Promise<Record<string, unknown>> {
async export(
namespace: string,
_options?: { lightweight?: boolean },
): Promise<Record<string, unknown>> {
const keys = await this.list(namespace);
const data: Record<string, unknown> = {};
for (const key of keys) {
@@ -72,7 +78,10 @@ export class LocalStorageAdapter implements StorageService {
return data;
}
async import(namespace: string, data: Record<string, unknown>): Promise<void> {
async import(
namespace: string,
data: Record<string, unknown>,
): Promise<void> {
for (const [key, value] of Object.entries(data)) {
await this.set(namespace, key, value);
}
+4 -1
View File
@@ -5,6 +5,9 @@ export interface StorageService {
list(namespace: string): Promise<string[]>;
query<T>(namespace: string, predicate: (item: T) => boolean): Promise<T[]>;
clear(namespace: string): Promise<void>;
export(namespace: string): Promise<Record<string, unknown>>;
export(
namespace: string,
options?: { lightweight?: boolean },
): Promise<Record<string, unknown>>;
import(namespace: string, data: Record<string, unknown>): Promise<void>;
}
+24 -15
View File
@@ -1,7 +1,7 @@
'use client';
"use client";
import { useCallback, useMemo } from 'react';
import { useStorageService } from './storage-provider';
import { useCallback, useMemo } from "react";
import { useStorageService } from "./storage-provider";
export interface NamespacedStorage {
get: <T>(key: string) => Promise<T | null>;
@@ -10,7 +10,9 @@ export interface NamespacedStorage {
list: () => Promise<string[]>;
query: <T>(predicate: (item: T) => boolean) => Promise<T[]>;
clear: () => Promise<void>;
exportAll: () => Promise<Record<string, unknown>>;
exportAll: (options?: {
lightweight?: boolean;
}) => Promise<Record<string, unknown>>;
importAll: (data: Record<string, unknown>) => Promise<void>;
}
@@ -18,38 +20,45 @@ export function useStorage(namespace: string): NamespacedStorage {
const service = useStorageService();
const get = useCallback(
<T,>(key: string) => service.get<T>(namespace, key),
[service, namespace]
<T>(key: string) => service.get<T>(namespace, key),
[service, namespace],
);
const set = useCallback(
<T,>(key: string, value: T) => service.set<T>(namespace, key, value),
[service, namespace]
<T>(key: string, value: T) => service.set<T>(namespace, key, value),
[service, namespace],
);
const del = useCallback(
(key: string) => service.delete(namespace, key),
[service, namespace]
[service, namespace],
);
const list = useCallback(() => service.list(namespace), [service, namespace]);
const query = useCallback(
<T,>(predicate: (item: T) => boolean) => service.query<T>(namespace, predicate),
[service, namespace]
<T>(predicate: (item: T) => boolean) =>
service.query<T>(namespace, predicate),
[service, namespace],
);
const clear = useCallback(() => service.clear(namespace), [service, namespace]);
const clear = useCallback(
() => service.clear(namespace),
[service, namespace],
);
const exportAll = useCallback(() => service.export(namespace), [service, namespace]);
const exportAll = useCallback(
(options?: { lightweight?: boolean }) => service.export(namespace, options),
[service, namespace],
);
const importAll = useCallback(
(data: Record<string, unknown>) => service.import(namespace, data),
[service, namespace]
[service, namespace],
);
return useMemo(
() => ({ get, set, delete: del, list, query, clear, exportAll, importAll }),
[get, set, del, list, query, clear, exportAll, importAll]
[get, set, del, list, query, clear, exportAll, importAll],
);
}