@@ -12,6 +12,9 @@ export interface Pack {
1212 lazy : Map < string , string > ;
1313 // path -> the image file holding it and its offset there
1414 offsets : Map < string , { image : string ; at : number } > ;
15+ // path -> where the bytes from each start on are, for a file split across
16+ // image files, sorted by start; offsets has the first part
17+ parts : Map < string , { start : number ; image : string ; at : number } [ ] > ;
1518 // path -> parts of a file read in ranges (the JDK image), as [offset, bytes]
1619 // sorted by offset
1720 ranges : Map < string , [ number , Uint8Array ] [ ] > ;
@@ -25,8 +28,10 @@ const utf8 = new TextDecoder();
2528// Pack layout: repeated [u16 path length][path][u32 size][bytes]. In the boot
2629// pack, "/index.prefix" holds prefix-coded "d path" and "f path size
2730// [image:offset] [pack]" lines. A file with an offset is read in ranges from
28- // <image>.jar; a named pack is <name>.pack next to it and is
29- // fetched whole the first time one of its files is read. An entry named
31+ // <image>.jar, and a "p path start image:offset" line after it says the
32+ // file's bytes from start on are in another image. A named pack is
33+ // <name>.pack next to it and is fetched whole the first time one of its files
34+ // is read. An entry named
3035// "@<offset>:<path>" holds part of a file read in ranges, and one named
3136// "@lazy:<pack>:<path>" lists, as u32 start and end pairs, the parts of it
3237// that pack holds. Images are .jar
@@ -44,7 +49,7 @@ export function parsePack(buf: ArrayBuffer): Pack {
4449 files . set ( path , u8 . subarray ( p + 4 , p + 4 + size ) ) ;
4550 p += 4 + size ;
4651 }
47- const pack : Pack = { files, index : null , lazy : new Map ( ) , offsets : new Map ( ) , ranges : new Map ( ) , lazyRanges : new Map ( ) } ;
52+ const pack : Pack = { files, index : null , lazy : new Map ( ) , offsets : new Map ( ) , parts : new Map ( ) , ranges : new Map ( ) , lazyRanges : new Map ( ) } ;
4853 for ( const [ p , data ] of files ) {
4954 if ( p [ 0 ] !== "@" ) continue ;
5055 files . delete ( p ) ;
@@ -169,10 +174,34 @@ export function addIndex(pack: Pack, text: string, prefix = false) {
169174 b = c ;
170175 }
171176 pack . lazy . set ( path , line . slice ( b + 1 ) ) ;
177+ } else if ( line [ 0 ] === "p" ) {
178+ const b = line . indexOf ( " " , a + 1 ) , colon = line . lastIndexOf ( ":" ) ;
179+ const name = line . slice ( b + 1 , colon ) ;
180+ let image = images . get ( name ) ;
181+ if ( image === undefined ) images . set ( name , ( image = `/${ name } .jar` ) ) ;
182+ let parts = pack . parts . get ( path ) ;
183+ if ( ! parts ) pack . parts . set ( path , ( parts = [ ] ) ) ;
184+ parts . push ( { start : + line . slice ( a + 1 , b ) , image, at : + line . slice ( colon + 1 ) } ) ;
172185 }
173186 }
174187}
175188
189+ // Where byte off of a file read in ranges is: the image file, the offset
190+ // there, and how many bytes from off on that part of the file holds.
191+ export function imagePart ( pack : Pack , path : string , off : number ) : { image : string ; at : number ; len : number } | undefined {
192+ const first = pack . offsets . get ( path ) ;
193+ if ( ! first ) return undefined ;
194+ let { image, at } = first , start = 0 , end = pack . index ! . get ( path ) ! ;
195+ for ( const p of pack . parts . get ( path ) ?? [ ] ) {
196+ if ( p . start > off ) {
197+ end = p . start ;
198+ break ;
199+ }
200+ ( { image, at, start } = p ) ;
201+ }
202+ return { image, at : at + off - start , len : end - off } ;
203+ }
204+
176205const BLOCK = 64 << 10 ;
177206
178207function encodePath ( path : string ) : string {
@@ -204,8 +233,10 @@ export class HttpBackend implements Backend {
204233 private fetchedPacks = new Set < string > ( ) ;
205234 // 64 KB blocks of files read in parts, by path
206235 private blocks = new Map < string , Map < number , Uint8Array > > ( ) ;
236+ // files read in ranges that the server does not have
237+ private missing = new Set < string > ( ) ;
207238
208- constructor ( base : string , pack : Pack = { files : new Map ( ) , index : null , lazy : new Map ( ) , offsets : new Map ( ) , ranges : new Map ( ) , lazyRanges : new Map ( ) } ) {
239+ constructor ( base : string , pack : Pack = { files : new Map ( ) , index : null , lazy : new Map ( ) , offsets : new Map ( ) , parts : new Map ( ) , ranges : new Map ( ) , lazyRanges : new Map ( ) } ) {
209240 this . base = base . endsWith ( "/" ) ? base . slice ( 0 , - 1 ) : base ;
210241 this . pack = pack ;
211242 }
@@ -290,8 +321,18 @@ export class HttpBackend implements Backend {
290321 return this . readRange ( path , off , len ) ;
291322 }
292323 // has() parses the path's module first; image files themselves are not indexed
293- const at = this . pack . index ?. has ( path ) ? this . pack . offsets . get ( path ) : undefined ;
294- if ( at !== undefined ) return this . readRange ( at . image , at . at + off , len ) ;
324+ const at = this . pack . index ?. has ( path ) ? imagePart ( this . pack , path , off ) : undefined ;
325+ if ( at !== undefined ) {
326+ if ( len <= at . len || at . len <= 0 ) return this . readRange ( at . image , at . at , len ) ;
327+ const head = this . readRange ( at . image , at . at , at . len ) ;
328+ const rest = head && this . readRange ( path , off + at . len , len - at . len ) ;
329+ if ( ! head || ! rest ) return null ;
330+ const out = new Uint8Array ( len ) ;
331+ out . set ( head ) ;
332+ out . set ( rest , head . length ) ;
333+ return out ;
334+ }
335+ if ( this . missing . has ( path ) ) return null ;
295336 let blocks = this . blocks . get ( path ) ;
296337 if ( ! blocks ) this . blocks . set ( path , ( blocks = new Map ( ) ) ) ;
297338 const first = Math . floor ( off / BLOCK ) , last = Math . floor ( ( off + len - 1 ) / BLOCK ) ;
@@ -315,6 +356,10 @@ export class HttpBackend implements Backend {
315356 this . blocks . delete ( path ) ;
316357 return data . subarray ( off , off + len ) ;
317358 }
359+ if ( xhr . status === 404 ) {
360+ this . missing . add ( path ) ;
361+ console . warn ( `java-web: ${ this . base } ${ path } is missing, so the classes in it do not load` ) ;
362+ }
318363 if ( xhr . status !== 206 ) return null ;
319364 if ( ! rangeOk ( xhr , b * BLOCK , data . length ) ) return this . readWhole ( path , off , len ) ;
320365 for ( let i = b ; i <= end ; i ++ ) blocks . set ( i , data . subarray ( ( i - b ) * BLOCK , ( i - b + 1 ) * BLOCK ) ) ;
@@ -332,11 +377,13 @@ export class HttpBackend implements Backend {
332377 }
333378
334379 // When ranges of a file come back wrong, reads all of it once: a whole
335- // response decodes correctly even from a host that compresses it.
380+ // response decodes correctly even from a host that compresses it. A range
381+ // that fails to decode throws as a network error does, so a failed read
382+ // here is taken as the network's, and later reads try again.
336383 private readWhole ( path : string , off : number , len : number ) : Uint8Array | null {
337- console . warn ( `java-web: range reads of ${ this . base } ${ path } came back compressed or wrong, so it is read whole. Serve it without compression (as .jar files are on GitHub Pages).` ) ;
338384 const xhr = this . request ( "GET" , path ) ;
339385 if ( ! xhr ) return null ;
386+ console . warn ( `java-web: range reads of ${ this . base } ${ path } came back compressed or wrong, so it is read whole. Serve it without compression (as .jar files are on GitHub Pages).` ) ;
340387 const data = new Uint8Array ( xhr . response as ArrayBuffer ) ;
341388 this . cache . set ( path , data ) ;
342389 this . blocks . delete ( path ) ;
0 commit comments