feat: speed up admin clone import and enable Cache Components

- Clone import: defer FurnitureData.json writes and append all entries in a
  single batched write instead of one read-modify-write per item, removing
  the main serialization bottleneck for large batches.
- Clone import: raise SSE batch concurrency cap from 5 to 10 and bump the
  clone client/route default from 2 to 6.
- Add a flush hook to runSseBatch so callers can batch deferred work before
  batch_complete is emitted, and surface flush errors as an error event.
- Enable Next.js Cache Components (instant: false opt-out) and silence the
  related build warnings in next.config.ts.
- Switch isomorphic-dompurify to dompurify and refresh dependencies.
This commit is contained in:
openhands committed 2026-08-05 11:10:16 +02:00
1 parent ac9b3e3cb5
commit dc8fb8a6ed
254 files changed
+910 -551

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
import DOMPurify from "isomorphic-dompurify";
import DOMPurify from "dompurify";
const sanitizeHtml = (html: string) => DOMPurify.sanitize(html);
+27 -13
View File
@@ -39,6 +39,8 @@ export interface CloneResult {
skipped?: boolean;
warnings: string[];
error?: string;
/** Set when `deferFurniData` is used so the caller can batch-write once. */
furniDataEntry?: { entry: Record<string, unknown>; itemType: string };
}
interface FurniType {
@@ -161,8 +163,11 @@ export async function cloneSingleFurni(params: {
source: CloneSource;
entry: SourceFurni;
onProgress?: (status: string) => void;
/** Skip the per-item FurnitureData.json write and return the entry instead,
* so batch callers can append everything in one lock + write cycle. */
deferFurniData?: boolean;
}): Promise<CloneResult> {
const { source, entry, onProgress } = params;
const { source, entry, onProgress, deferFurniData } = params;
const { classname, itemType } = entry;
const warnings: string[] = [];
@@ -281,17 +286,19 @@ export async function cloneSingleFurni(params: {
// FurnitureData entry — reuse the source's furnitype object, with our id.
onProgress?.("writing_furnidata");
try {
await appendFurniEntry(
{ ...entry, id: newId } as Record<string, unknown>,
itemType,
);
} catch (err) {
logger.warn("[clone-import] FurnitureData append failed for", {
classname,
error: (err as Error).message,
});
warnings.push(`FurnitureData append failed: ${(err as Error).message}`);
const furniDataEntry = { ...entry, id: newId } as Record<string, unknown>;
if (deferFurniData) {
// Batch mode: the caller collects entries and appends them once at the end.
} else {
try {
await appendFurniEntry(furniDataEntry, itemType);
} catch (err) {
logger.warn("[clone-import] FurnitureData append failed for", {
classname,
error: (err as Error).message,
});
warnings.push(`FurnitureData append failed: ${(err as Error).message}`);
}
}
// Catalog entry (category sub-page + auto price), serialized id allocation.
@@ -312,7 +319,14 @@ export async function cloneSingleFurni(params: {
warnings.push(`catalog entry failed: ${(err as Error).message}`);
}
return { ok: true, classname, warnings };
return {
ok: true,
classname,
warnings,
furniDataEntry: deferFurniData
? { entry: furniDataEntry, itemType }
: undefined,
};
}
export async function getCloneList(params: {
+20 -2
View File
@@ -17,6 +17,13 @@ export interface RunSseBatchOptions<T> {
index: number,
report: (status: string) => void,
) => Promise<SseWorkerResult>;
/**
* Optional finalizer run once after every item settles, before
* batch_complete is emitted. Lets callers flush deferred work (e.g. a single
* FurnitureData.json write) instead of doing it per item. Failures are
* surfaced as an `error` event without aborting the stream.
*/
flush?: () => Promise<void>;
}
/**
@@ -25,8 +32,8 @@ export interface RunSseBatchOptions<T> {
* Stops starting new chunks when `signal` aborts or the client cancels the stream.
*/
export function runSseBatch<T>(opts: RunSseBatchOptions<T>): Response {
const { items, labelOf, worker, signal } = opts;
const concurrency = Math.min(Math.max(opts.concurrency || 3, 1), 5);
const { items, labelOf, worker, signal, flush } = opts;
const concurrency = Math.min(Math.max(opts.concurrency || 3, 1), 10);
const encoder = new TextEncoder();
const ac = new AbortController();
@@ -113,6 +120,17 @@ export function runSseBatch<T>(opts: RunSseBatchOptions<T>): Response {
);
}
if (flush) {
try {
await flush();
} catch (err) {
send({
type: "error",
message: (err as Error).message,
});
}
}
if (!ac.signal.aborted) {
send({
type: "batch_complete",
+7
View File
@@ -88,6 +88,13 @@ export async function runSseImport(
onFailed?.(classname, String(evt.error ?? ""));
}
}
if (evt.type === "error") {
toast.error(
typeof evt.message === "string"
? evt.message
: "Import finished with errors",
);
}
if (evt.type === "batch_complete") {
succeeded = Number(evt.succeeded ?? 0);
failed = Number(evt.failed ?? 0);