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:
1 parent
ac9b3e3cb5
commit
dc8fb8a6ed
254 files changed
+910
-551
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
const sanitizeHtml = (html: string) => DOMPurify.sanitize(html);
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user