├── readme.md ├── package.json ├── .gitignore └── themiddleware.js /readme.md: -------------------------------------------------------------------------------- 1 | # h2-server-push 2 | 3 | An express middleware that implements server push in HTTP2. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install h2-server-push --save 9 | ``` 10 | __Note__: Requires `node-spdy` for HTTP2 11 | 12 | ## Usage 13 | 14 | ```javascript 15 | const hsp = require('h2-server-push'); 16 | 17 | const registerParser = hsp('public'); 18 | 19 | app.get('/', registerParser, (req, res) => { 20 | res.sp('index.html', 'public') 21 | }); 22 | ``` 23 | 24 | ## License 25 | 26 | MIT -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "h2-server-push", 3 | "version": "1.0.5", 4 | "description": "HTTP2 Server Push Express Middleware", 5 | "main": "themiddleware.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/amnisH2/h2-server-push.git" 12 | }, 13 | "keywords": [ 14 | "http2", 15 | "h2", 16 | "express", 17 | "node", 18 | "server", 19 | "push" 20 | ], 21 | "author": "Liyang (https://github.com/amnisH2/speedy-push)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/amnisH2/h2-server-push/issues" 25 | }, 26 | "homepage": "https://github.com/amnisH2/h2-server-push#readme", 27 | "dependencies": { 28 | "golombcodedsets-with-base64": "0.0.4", 29 | "html-static-asset-path-extractor": "^1.0.5" 30 | }, 31 | "devDependencies": { 32 | "debug": "^2.6.8" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,macos,linux,windows 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### Node ### 47 | # Logs 48 | logs 49 | *.log 50 | npm-debug.log* 51 | yarn-debug.log* 52 | yarn-error.log* 53 | 54 | # Runtime data 55 | pids 56 | *.pid 57 | *.seed 58 | *.pid.lock 59 | 60 | # Directory for instrumented libs generated by jscoverage/JSCover 61 | lib-cov 62 | 63 | # Coverage directory used by tools like istanbul 64 | coverage 65 | 66 | # nyc test coverage 67 | .nyc_output 68 | 69 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 70 | .grunt 71 | 72 | # Bower dependency directory (https://bower.io/) 73 | bower_components 74 | 75 | # node-waf configuration 76 | .lock-wscript 77 | 78 | # Compiled binary addons (http://nodejs.org/api/addons.html) 79 | build/Release 80 | 81 | # Dependency directories 82 | node_modules/ 83 | jspm_packages/ 84 | 85 | # Typescript v1 declaration files 86 | typings/ 87 | 88 | # Optional npm cache directory 89 | .npm 90 | 91 | # Optional eslint cache 92 | .eslintcache 93 | 94 | # Optional REPL history 95 | .node_repl_history 96 | 97 | # Output of 'npm pack' 98 | *.tgz 99 | 100 | # Yarn Integrity file 101 | .yarn-integrity 102 | 103 | # dotenv environment variables file 104 | .env 105 | 106 | 107 | ### Windows ### 108 | # Windows thumbnail cache files 109 | Thumbs.db 110 | ehthumbs.db 111 | ehthumbs_vista.db 112 | 113 | # Folder config file 114 | Desktop.ini 115 | 116 | # Recycle Bin used on file shares 117 | $RECYCLE.BIN/ 118 | 119 | # Windows Installer files 120 | *.cab 121 | *.msi 122 | *.msm 123 | *.msp 124 | 125 | # Windows shortcuts 126 | *.lnk 127 | 128 | package-lock.json 129 | 130 | # End of https://www.gitignore.io/api/node,macos,linux,windows 131 | -------------------------------------------------------------------------------- /themiddleware.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('spParser') 2 | const fs = require('fs') 3 | const extractor = require('html-static-asset-path-extractor') 4 | const path = require('path') 5 | const gcs = require('golombcodedsets-with-base64') 6 | 7 | let parsedObj = {}; 8 | const parentPath = process.env.DEBUG ? '../speedy-push' : '../..' 9 | 10 | const readFileAsync = (filepath) => { 11 | return new Promise((resolve, reject) => { 12 | fs.readFile(filepath, (err, data) => { 13 | if (err) { 14 | reject(err); 15 | } else { 16 | resolve(data); 17 | } 18 | }) 19 | }) 20 | } 21 | 22 | const pushSingle = (res, resource, rootPath) => { 23 | return new Promise((resolve, reject) => { 24 | const {filePath, contentType} = resource 25 | readFileAsync(path.join(__dirname, parentPath, rootPath, filePath)) 26 | .then(file => { 27 | let pushStream = res.push('/' + filePath, { 28 | request: {'accept' : '**/*'}, 29 | response: { 30 | 'content-type' : contentType, 31 | 'cache-control': 'max-age=31536000, public' 32 | } 33 | }, (file) => resolve({ value: file, status: "resolved"})) 34 | pushStream.end(file) 35 | }, (error) => resolve({value:error, status: "rejected"})) 36 | .catch(err => { 37 | debug('err', err) 38 | return Promise.reject(res); 39 | }) 40 | // const {filePath, contentType} = resource 41 | // if (!res.push) reject(new Error('NO SPDY')) 42 | // let pushStream = res.push('/' + filePath, { 43 | // request: {'accept' : '**/*'}, 44 | // response: { 45 | // 'content-type' : contentType, 46 | // 'cache-control': 'max-age=31536000, public' 47 | // } 48 | // }, (file) => resolve({ value: file, status: "resolved"})) 49 | // //make sure to switch '../speedy-push' to '../..' once using real node module 50 | // fs.createReadStream(path.join(__dirname, '../speedy-push', rootPath, filePath)).pipe(pushStream); 51 | 52 | }) 53 | } 54 | 55 | const preParse = (folder) => { 56 | let htmlObj = fs.readdirSync(folder).filter(file => path.extname(file) === '.html' ? true : false); 57 | let mapped = htmlObj.map(paths => { 58 | //make sure to switch '../speedy-push' to '../..' once using real node module from /node_modules folder 59 | return extractor(path.join(__dirname ,parentPath, folder, paths)); 60 | }); 61 | Promise.all(mapped).then((paths => { 62 | parsedObj = paths.reduce((acc, resourcemap, currindex) => { 63 | acc[htmlObj[currindex]] = resourcemap; 64 | return acc; 65 | }, {}) 66 | console.log('PARSEDOBJ', parsedObj) 67 | })) 68 | } 69 | 70 | const spParser = (folder) => { 71 | preParse(folder) 72 | 73 | return registerParser = (req, res, next) => { 74 | const sp = (htmlPath, folderPath = "") => { 75 | let cacheHash = req.cookies.cache || undefined; 76 | //if we have a resource map for the html file we want to push 77 | if(parsedObj[htmlPath]) { 78 | //array to store assets that arent cached yet 79 | let resources = [] 80 | let setBuilder; 81 | //decide what to push based on cookie hash 82 | if(!cacheHash) { 83 | resources = parsedObj[htmlPath]; 84 | setBuilder = new gcs.GCSBuilder(50, 1000) 85 | resources.forEach((resource)=>{ 86 | setBuilder.add(resource.filePath) 87 | }) 88 | } else { 89 | //create queriable set from cookie hash 90 | let setQuery = new gcs.GCSQuery(cacheHash) 91 | setBuilder = setQuery.toBuilder() 92 | 93 | //leave only missing assets 94 | parsedObj[htmlPath].forEach((resource)=>{ 95 | if(!setQuery.query(resource.filePath)) { 96 | resources.push(resource) 97 | setBuilder.add(resource.filePath) 98 | } 99 | }) 100 | } 101 | 102 | debug("RESOURCES:", resources) 103 | const PromiseArr = resources.map(cur => pushSingle(res, cur, folderPath)) 104 | 105 | Promise.all(PromiseArr) 106 | .then((files)=> { 107 | //update cookie with new cache hash 108 | res.cookie('cache', setBuilder.toBase64()) 109 | const html = fs.createReadStream(path.join(folderPath, htmlPath)); 110 | html.pipe(res); 111 | }) 112 | .catch((err)=> { 113 | debug("error in streaming files:", err) 114 | res.status(500); 115 | res.send(err) 116 | }) 117 | } 118 | } 119 | res.sp = sp; 120 | next() 121 | } 122 | } 123 | 124 | module.exports = spParser; 125 | --------------------------------------------------------------------------------