@@ -128,8 +128,23 @@ type ToolkitDataMap = {
128128 * unbounded process-global cache.
129129 */
130130const loadsByDataDir = new Map < string , Promise < ToolkitDataMap > > ( ) ;
131+ /**
132+ * One in-flight/resolved read per (dataDir, normalized lookup key) in
133+ * production. `readToolkitData`'s direct-file fast path re-reads and
134+ * re-parses on every call without this — `loadAllToolkitData` only caches
135+ * the directory scan fallback, so generateMetadata + Page for the same
136+ * toolkit would each pay for a full file read during static generation.
137+ *
138+ * Skipped in development (generator can refresh JSON while `next dev` runs)
139+ * and when callers pass an explicit `dataDir` (tests use scratch fixtures
140+ * and expect fresh reads after mutating files).
141+ */
142+ const readsByLookupKey = new Map < string , Promise < ToolkitData | null > > ( ) ;
131143const DEFAULT_DATA_DIR = resolveToolkitDataDir ( ) ;
132144
145+ const readLookupCacheKey = ( dataDir : string , toolkitId : string ) : string =>
146+ `${ dataDir } \0${ normalizeToolkitId ( toolkitId ) } ` ;
147+
133148const loadAllToolkitDataUncached = async (
134149 dataDir : string
135150) : Promise < ToolkitDataMap > => {
@@ -206,19 +221,13 @@ export const loadAllToolkitData = cache(
206221 }
207222) ;
208223
209- export const readToolkitData = async (
224+ const readToolkitDataUncached = async (
210225 toolkitId : string ,
211- options ?: ToolkitDataOptions
226+ dataDir : string
212227) : Promise < ToolkitData | null > => {
213228 // Normalize the toolkit ID to lowercase alphanumeric
214229 const normalizedId = normalizeToolkitId ( toolkitId ) ;
215230
216- // Guard against empty normalized ID (e.g., input was only special characters)
217- if ( ! normalizedId ) {
218- return null ;
219- }
220-
221- const dataDir = resolveDataDir ( options ) ;
222231 // The API route normally receives the normalized toolkit id. Keep that
223232 // common path O(1), especially on a cold serverless instance: eagerly
224233 // loading every toolkit JSON file just to serve one toolkit adds tens of
@@ -238,6 +247,41 @@ export const readToolkitData = async (
238247 ) ;
239248} ;
240249
250+ export function readToolkitData (
251+ toolkitId : string ,
252+ options ?: ToolkitDataOptions
253+ ) : Promise < ToolkitData | null > {
254+ const normalizedId = normalizeToolkitId ( toolkitId ) ;
255+
256+ // Guard against empty normalized ID (e.g., input was only special characters)
257+ if ( ! normalizedId ) {
258+ return Promise . resolve ( null ) ;
259+ }
260+
261+ const dataDir = resolveDataDir ( options ) ;
262+
263+ if (
264+ process . env . NODE_ENV === "development" ||
265+ options ?. dataDir !== undefined
266+ ) {
267+ return readToolkitDataUncached ( toolkitId , dataDir ) ;
268+ }
269+
270+ const key = readLookupCacheKey ( dataDir , toolkitId ) ;
271+ let promise = readsByLookupKey . get ( key ) ;
272+ if ( ! promise ) {
273+ promise = readToolkitDataUncached ( toolkitId , dataDir ) ;
274+ readsByLookupKey . set ( key , promise ) ;
275+
276+ promise . catch ( ( ) => {
277+ if ( readsByLookupKey . get ( key ) === promise ) {
278+ readsByLookupKey . delete ( key ) ;
279+ }
280+ } ) ;
281+ }
282+ return promise ;
283+ }
284+
241285export const readToolkitIndex = async (
242286 options ?: ToolkitDataOptions
243287) : Promise < ToolkitIndex | null > => {
0 commit comments