1- /** Async domain data loading with progress callbacks. */
1+ /** Async domain data loading with progress callbacks and request deduplication . */
22
33import { $domainCache } from '../state/store.js' ;
44import { getDescendants } from './registry.js' ;
55
66const PROGRESS_THROTTLE_MS = 100 ;
77
8+ /** In-flight request deduplication — prevents duplicate fetches for the same domain. */
9+ const inflight = new Map ( ) ;
10+
811/**
912 * Load a domain bundle, with caching and streaming progress.
13+ * Concurrent calls for the same domainId share a single fetch request.
1014 * @param {string } domainId
1115 * @param {{ onProgress?, onComplete?, onError? } } [callbacks={}]
1216 * @param {string } [basePath] - Defaults to import.meta.env.BASE_URL || '/mapper/'
@@ -24,36 +28,54 @@ export async function load(domainId, callbacks = {}, basePath) {
2428 return cached ;
2529 }
2630
27- try {
28- const url = `${ base } data/domains/${ domainId } .json` ;
29- const res = await fetch ( url ) ;
30- if ( ! res . ok ) {
31- throw new Error ( `Failed to fetch domain ${ domainId } : ${ res . status } ${ res . statusText } ` ) ;
32- }
33-
34- let bundle ;
35- const contentLength = res . headers . get ( 'Content-Length' ) ;
36- const total = contentLength ? parseInt ( contentLength , 10 ) : 0 ;
37-
38- if ( total > 0 && res . body ) {
39- bundle = await readWithProgress ( res . body , total , onProgress ) ;
40- } else {
41- onProgress ?. ( { loaded : 0 , total : 0 , percent : 0 } ) ;
42- bundle = await res . json ( ) ;
43- onProgress ?. ( { loaded : 1 , total : 1 , percent : 100 } ) ;
44- }
31+ // Deduplicate: if a fetch is already in-flight for this domain, await it
32+ if ( inflight . has ( domainId ) ) {
33+ const bundle = await inflight . get ( domainId ) ;
34+ onProgress ?. ( { loaded : 1 , total : 1 , percent : 100 } ) ;
35+ onComplete ?. ( bundle ) ;
36+ return bundle ;
37+ }
4538
46- // Cache the result
47- const next = new Map ( $domainCache . get ( ) ) ;
48- next . set ( domainId , bundle ) ;
49- $domainCache . set ( next ) ;
39+ const promise = _fetchAndCache ( domainId , base , onProgress ) ;
40+ inflight . set ( domainId , promise ) ;
5041
42+ try {
43+ const bundle = await promise ;
5144 onComplete ?. ( bundle ) ;
5245 return bundle ;
5346 } catch ( err ) {
5447 onError ?. ( err ) ;
5548 throw err ;
49+ } finally {
50+ inflight . delete ( domainId ) ;
51+ }
52+ }
53+
54+ async function _fetchAndCache ( domainId , base , onProgress ) {
55+ const url = `${ base } data/domains/${ domainId } .json` ;
56+ const res = await fetch ( url ) ;
57+ if ( ! res . ok ) {
58+ throw new Error ( `Failed to fetch domain ${ domainId } : ${ res . status } ${ res . statusText } ` ) ;
59+ }
60+
61+ let bundle ;
62+ const contentLength = res . headers . get ( 'Content-Length' ) ;
63+ const total = contentLength ? parseInt ( contentLength , 10 ) : 0 ;
64+
65+ if ( total > 0 && res . body ) {
66+ bundle = await readWithProgress ( res . body , total , onProgress ) ;
67+ } else {
68+ onProgress ?. ( { loaded : 0 , total : 0 , percent : 0 } ) ;
69+ bundle = await res . json ( ) ;
70+ onProgress ?. ( { loaded : 1 , total : 1 , percent : 100 } ) ;
5671 }
72+
73+ // Cache the result
74+ const next = new Map ( $domainCache . get ( ) ) ;
75+ next . set ( domainId , bundle ) ;
76+ $domainCache . set ( next ) ;
77+
78+ return bundle ;
5779}
5880
5981/** @param {ReadableStream } body */
@@ -103,7 +125,8 @@ async function readWithProgress(body, total, onProgress) {
103125export async function loadQuestionsForDomain ( domainId , basePath ) {
104126 const idsToLoad = [ domainId , ...getDescendants ( domainId ) ] ;
105127
106- // Load all bundles in parallel (cached ones resolve instantly)
128+ // Load all bundles in parallel (cached ones resolve instantly,
129+ // in-flight ones share the existing fetch via dedup)
107130 const bundles = await Promise . all (
108131 idsToLoad . map ( id => load ( id , { } , basePath ) . catch ( ( ) => null ) )
109132 ) ;
0 commit comments