10
10
11
11
'use strict' ;
12
12
13
+ import * as assert from 'assert' ;
13
14
import * as express from 'express' ;
14
15
import * as findPort from 'find-port' ;
16
+ import * as mime from 'mime' ;
15
17
// TODO: Switch to node-http2 when compatible with express
16
18
// https://github.com/molnarg/node-http2/issues/100
17
19
import * as http from 'spdy' ;
@@ -24,6 +26,9 @@ import * as fs from 'mz/fs';
24
26
25
27
import { makeApp } from './make_app' ;
26
28
29
+ /** h2 push manifest cache */
30
+ let _pushManifest = { } ;
31
+
27
32
export interface ServerOptions {
28
33
29
34
/** The root directory to serve **/
@@ -58,6 +63,9 @@ export interface ServerOptions {
58
63
59
64
/** Path to TLS certificate for HTTPS */
60
65
certPath ?: string ;
66
+
67
+ /** Path to H2 push-manifest file */
68
+ pushManifestPath ?: string ;
61
69
}
62
70
63
71
function applyDefaultOptions ( options : ServerOptions ) : ServerOptions {
@@ -121,9 +129,17 @@ export function getApp(options: ServerOptions) {
121
129
122
130
app . use ( '/components/' , polyserve ) ;
123
131
132
+ // Preload the h2-push manifest to avoid the cost on first push
133
+ if ( options . pushManifestPath ) {
134
+ getPushManifest ( options . root , options . pushManifestPath ) ;
135
+ }
136
+
124
137
app . get ( '/*' , ( req , res ) => {
138
+
139
+ pushResources ( options , req , res ) ;
140
+
125
141
let filePath = req . path ;
126
- send ( req , filePath , { root : root , } )
142
+ send ( req , filePath , { root : root } )
127
143
. on ( 'error' , ( error : send . SendError ) => {
128
144
if ( ( error ) . status == 404 && ! filePath . endsWith ( '.html' ) ) {
129
145
send ( req , '/' , { root : root } ) . pipe ( res ) ;
@@ -340,3 +356,82 @@ function startWithPort(userOptions: ServerOptions): Promise<http.Server> {
340
356
} )
341
357
) ;
342
358
}
359
+
360
+ /**
361
+ * Asserts file existence for all specified files in a push-manifest
362
+ * @param {string } root path to root directory
363
+ * @param {string } manifest manifest object
364
+ */
365
+ function assertValidManifest ( root : string , manifest : { [ path : string ] : any } ) {
366
+
367
+ function assertExists ( filename : string ) {
368
+ const fname = path . join ( root , filename ) ;
369
+ try {
370
+ // fs.statSync also verifies file existence
371
+ assert ( fs . statSync ( fname ) . isFile ( ) , `file not found: ${ fname } ` ) ;
372
+ } catch ( err ) {
373
+ console . error ( 'invalid h2-push manifest' ) ;
374
+ throw err ;
375
+ }
376
+ }
377
+
378
+ for ( const refFile of Object . keys ( manifest ) ) {
379
+ assertExists ( refFile ) ;
380
+ for ( const pushFile of Object . keys ( manifest [ refFile ] ) ) {
381
+ assertExists ( pushFile ) ;
382
+ }
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Reads a push-manifest from the specified path, or a cached version
388
+ * of the file
389
+ * @param {string } root path to root directory
390
+ * @param {string } manifestPath path to manifest file
391
+ * @returns {any } the manifest
392
+ */
393
+ function getPushManifest ( root : string , manifestPath : string ) : { [ path : string ] : any } {
394
+ if ( ! _pushManifest [ manifestPath ] ) {
395
+ const data = fs . readFileSync ( manifestPath ) ;
396
+ const manifest = JSON . parse ( data . toString ( ) ) ;
397
+ assertValidManifest ( root , manifest ) ;
398
+ _pushManifest [ manifestPath ] = manifest ;
399
+ }
400
+ return _pushManifest [ manifestPath ] ;
401
+ }
402
+
403
+ /**
404
+ * Pushes any resources for the requested file
405
+ * @param options server options
406
+ * @param req HTTP request
407
+ * @param res HTTP response
408
+ */
409
+ function pushResources ( options : ServerOptions , req : any , res : any ) {
410
+ if ( res . push
411
+ && options . protocol === 'h2'
412
+ && options . pushManifestPath
413
+ && ! req . get ( 'x-is-push' ) ) {
414
+
415
+ const pushManifest = getPushManifest ( options . root , options . pushManifestPath ) ;
416
+ const resources = pushManifest [ req . path ] ;
417
+ if ( resources ) {
418
+ const root = options . root ;
419
+ for ( const filename of Object . keys ( resources ) ) {
420
+ const stream : NodeJS . WritableStream = res . push ( filename ,
421
+ {
422
+ request : {
423
+ accept : '*/*'
424
+ } ,
425
+ response : {
426
+ 'content-type' : mime . lookup ( filename ) ,
427
+
428
+ // Add an X-header to the pushed request so we don't trigger pushes for pushes
429
+ 'x-is-push' : 'true'
430
+ }
431
+ } )
432
+ . on ( 'error' , ( err : any ) => console . error ( 'failed to push' , filename , err ) ) ;
433
+ fs . createReadStream ( path . join ( root , filename ) ) . pipe ( stream ) ;
434
+ }
435
+ }
436
+ }
437
+ }
0 commit comments