├── .babelrc ├── .gitignore ├── .vscode └── launch.json ├── Dockerfile ├── certs ├── localhost.crt └── localhost.key ├── gulpfile.babel.js ├── package.json ├── public ├── index.html ├── modules │ ├── dep1.js │ ├── dep2.js │ ├── dep3.js │ ├── dep4.js │ ├── lib1.js │ ├── lib2.js │ ├── page1.js │ ├── page2.js │ └── page3.js ├── page2.html ├── page3.html └── src │ ├── Bloomfilter.js │ └── service-worker.js ├── readme.md ├── server.js └── src ├── analyze.js ├── app.js └── moduleTree.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-native-generators"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/bin 3 | /bin 4 | certs 5 | !certs/localhost.* -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/server.js", 9 | "stopOnEntry": false, 10 | "args": [], 11 | "cwd": "${workspaceRoot}", 12 | "preLaunchTask": null, 13 | "runtimeExecutable": null, 14 | "runtimeArgs": [ 15 | "--nolazy" 16 | ], 17 | "env": { 18 | "NODE_ENV": "development" 19 | }, 20 | "externalConsole": false, 21 | "sourceMaps": true, 22 | "outDir": null 23 | }, 24 | { 25 | "name": "Attach", 26 | "type": "node", 27 | "request": "attach", 28 | "port": 5858, 29 | "address": "localhost", 30 | "restart": false, 31 | "sourceMaps": false, 32 | "outDir": null, 33 | "localRoot": "${workspaceRoot}", 34 | "remoteRoot": null 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-onbuild 2 | 3 | ENV PORT=443 4 | EXPOSE 443 5 | 6 | RUN npm run build -------------------------------------------------------------------------------- /certs/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICDTCCAXYCCQC7iiBVXeTv1DANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJI 3 | VTETMBEGA1UECBMKU29tZS1TdGF0ZTETMBEGA1UEChMKbm9kZS1odHRwMjESMBAG 4 | A1UEAxMJbG9jYWxob3N0MB4XDTE0MTIwMjE4NDcwNFoXDTI0MTEyOTE4NDcwNFow 5 | SzELMAkGA1UEBhMCSFUxEzARBgNVBAgTClNvbWUtU3RhdGUxEzARBgNVBAoTCm5v 6 | ZGUtaHR0cDIxEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOB 7 | jQAwgYkCgYEA8As7rj7xdD+RuAmORju9NI+jtOScGgiAbfovaFyzTu0O0H9SCExi 8 | u6e2iXMRfzomTix/yjRvbdHEXfgONG1MnKUc0oC4GxHXshyMDEXq9LadgAmR/nDL 9 | UVT0eo7KqC21ufaca2nVS9qOdlSCE/p7IJdb2+BF1RmuC9pHpXvFW20CAwEAATAN 10 | BgkqhkiG9w0BAQUFAAOBgQDn8c/9ho9L08dOqEJ2WTBmv4dfRC3oTWR/0oIGsaXb 11 | RhQONy5CJv/ymPYE7nCFWTMaia+w8oFqMie/aNZ7VK6L+hafuUS93IjuTXVN++JP 12 | 4948B0BBagvXGTwNtvm/1sZHLrXTkH1dbRUEF8M+KUSRUu2zJgm+e1bD8WTKQOIL 13 | NA== 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /certs/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQDwCzuuPvF0P5G4CY5GO700j6O05JwaCIBt+i9oXLNO7Q7Qf1II 3 | TGK7p7aJcxF/OiZOLH/KNG9t0cRd+A40bUycpRzSgLgbEdeyHIwMRer0tp2ACZH+ 4 | cMtRVPR6jsqoLbW59pxradVL2o52VIIT+nsgl1vb4EXVGa4L2kele8VbbQIDAQAB 5 | AoGAKKB+FVup2hb4PsG/RrvNphu5hWA721wdAIAbjfpCjtUocLlb1PO4sjIMfu7u 6 | wy3AVfLKHhsJ0Phz18OoA8+L65NMoMRsHOGaLEnGIJzJcnDLT5+uTFN5di0a1+UK 7 | BzB828rlHBNoQisogVCoKTYlCPJAZuI3trEzupWAV28XjTECQQD5LUEwYq4xr62L 8 | dEq5Qj/+c5paK/jrEBY83VZUmWzYsFgUwmpdku2ITRILQlOM33j6rk8krZZb93sb 9 | 38ydmfwjAkEA9p30zyjOI9kKqTl9WdYNYtIXpyNGYa+Pga33o9pawTewiyS2uCYs 10 | wnQQV26bQ0YwQqLQhtIbo4fzCO6Ex0w7LwJBANHNbd8cp4kEX35U+3nDM3i+w477 11 | CUp6sA6tWrw+tqw4xuEr1T1WshOauP+r6AdsPkPsMo0yb7CdzxVoObPVbLsCQQCc 12 | sx0cjEb/TCeUAy186Z+zzN6umqFb7Jt4wLt7Z4EHCIWqw/c95zPFks3XYDZTdsOv 13 | c5igMdzR+c4ZPMUthWiNAkByx7If12G1Z/R2Y0vIB0WJq4BJnZCZ0mRR0oAmPoA+ 14 | sZbmwctZ3IU+68Rgr4EAhrU04ygjF67IiNyXX0qqu3VH 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import gulp from 'gulp'; 4 | import del from 'del'; 5 | import babel from 'gulp-babel'; 6 | import concat from 'gulp-concat'; 7 | import replace from 'gulp-replace'; 8 | import sourcemaps from 'gulp-sourcemaps'; 9 | 10 | // CLEAN 11 | 12 | gulp.task('clean-sw', () => { 13 | return del('public/bin/*'); 14 | }); 15 | 16 | gulp.task('clean-modules', () => { 17 | return del('public/modules/*'); 18 | }); 19 | 20 | gulp.task('clean-server', () => { 21 | return del('bin/*'); 22 | }); 23 | 24 | 25 | // BUILD 26 | 27 | gulp.task('build-sw', ['clean-sw'], () => { 28 | return gulp.src('public/src/*.js') 29 | .pipe(sourcemaps.init()) 30 | .pipe(babel({ 31 | presets: ['es2015-native-generators'], 32 | babelrc: false 33 | })) 34 | .pipe(concat('service-worker.js')) 35 | .pipe(replace('<% VERSION %>', new Date().toISOString())) 36 | .pipe(sourcemaps.write('.')) 37 | .pipe(gulp.dest('public/bin')); 38 | }); 39 | 40 | gulp.task('build-server', ['clean-server'], () => { 41 | return gulp.src('src/*.js') 42 | .pipe(sourcemaps.init()) 43 | .pipe(babel({ 44 | presets: ['es2015-native-generators'], 45 | plugins: ['add-module-exports'], 46 | babelrc: false 47 | })) 48 | .pipe(sourcemaps.write('.', {sourceRoot: '../src'})) 49 | .pipe(gulp.dest('bin')); 50 | }); 51 | 52 | gulp.task('build', ['build-sw', 'build-server'], () => {}) 53 | 54 | // WATCH 55 | 56 | gulp.task('watch-sw', ['build-sw'], () => { 57 | return gulp.watch('public/src/*.js', ['build-sw']); 58 | }); 59 | 60 | gulp.task('watch-server', ['build-server'], () => { 61 | return gulp.watch('src/*.js', ['build-server']); 62 | }); 63 | 64 | gulp.task('watch', ['watch-sw', 'watch-server'], () => {}); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "module-pusher", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "gulp build" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "analyze-es6-modules": "^0.6.0", 15 | "bloomfilter": "0.0.16", 16 | "finalhandler": "^0.4.1", 17 | "fs-promise": "^0.5.0", 18 | "glob-promise": "^1.0.6", 19 | "gulp-concat": "^2.6.0", 20 | "http2": "^3.3.2", 21 | "lodash": "^4.6.1", 22 | "precinct": "^3.6.0", 23 | "router": "^1.1.4", 24 | "serve-static": "^1.10.2", 25 | "system-trace": "^0.2.1" 26 | }, 27 | "devDependencies": { 28 | "babel-plugin-add-module-exports": "^0.1.2", 29 | "babel-plugin-transform-es2015-modules-systemjs": "^6.6.5", 30 | "babel-preset-es2015-native-generators": "^6.6.0", 31 | "babel-register": "^6.7.2", 32 | "del": "^2.2.0", 33 | "gulp": "^3.9.1", 34 | "gulp-babel": "^6.1.2", 35 | "gulp-replace": "^0.5.4", 36 | "gulp-sourcemaps": "^1.6.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | 24 |

Home

25 |

26 | 
27 | Page 2 | Page 3 28 |
29 | 30 | 31 | 32 | 35 | -------------------------------------------------------------------------------- /public/modules/dep1.js: -------------------------------------------------------------------------------- 1 | import lib1 from '/modules/lib1.js'; 2 | 3 | export default () => [ 4 | 'dep1', 5 | ...lib1().map(x => ` ${x}`) 6 | ]; -------------------------------------------------------------------------------- /public/modules/dep2.js: -------------------------------------------------------------------------------- 1 | import lib2 from '/modules/lib2.js'; 2 | 3 | export default () => [ 4 | 'dep2', 5 | ...lib2().map(x => ` ${x}`) 6 | ]; -------------------------------------------------------------------------------- /public/modules/dep3.js: -------------------------------------------------------------------------------- 1 | import lib2 from '/modules/lib2.js'; 2 | 3 | export default () => [ 4 | 'dep3', 5 | ...lib2().map(x => ` ${x}`) 6 | ]; -------------------------------------------------------------------------------- /public/modules/dep4.js: -------------------------------------------------------------------------------- 1 | import lib1 from '/modules/lib1.js'; 2 | 3 | export default () => [ 4 | 'dep4', 5 | ...lib1().map(x => ` ${x}`) 6 | ]; -------------------------------------------------------------------------------- /public/modules/lib1.js: -------------------------------------------------------------------------------- 1 | export default () => ['lib1']; -------------------------------------------------------------------------------- /public/modules/lib2.js: -------------------------------------------------------------------------------- 1 | export default () => ['lib2']; -------------------------------------------------------------------------------- /public/modules/page1.js: -------------------------------------------------------------------------------- 1 | import dep1 from '/modules/dep1.js'; 2 | import dep2 from '/modules/dep2.js'; 3 | 4 | const getDeps = () => [ 5 | 'page1', 6 | ...dep1().map(x => ` ${x}`), 7 | ...dep2().map(x => ` ${x}`) 8 | ]; 9 | 10 | document.querySelector('pre').innerText += getDeps().join('\n') + '\n' + window.performance.now(); -------------------------------------------------------------------------------- /public/modules/page2.js: -------------------------------------------------------------------------------- 1 | import dep3 from '/modules/dep3.js'; 2 | import dep4 from '/modules/dep4.js'; 3 | 4 | export const getDeps = () => [ 5 | 'page2', 6 | ...dep3().map(x => ` ${x}`), 7 | ...dep4().map(x => ` ${x}`) 8 | ]; 9 | document.querySelector('pre').innerText += getDeps().join('\n') + '\n' + window.performance.now(); -------------------------------------------------------------------------------- /public/modules/page3.js: -------------------------------------------------------------------------------- 1 | import dep1 from '/modules/dep1.js'; 2 | import dep2 from '/modules/dep2.js'; 3 | import dep3 from '/modules/dep3.js'; 4 | import dep4 from '/modules/dep4.js'; 5 | 6 | export const getDeps = () => [ 7 | 'page3', 8 | ...dep1().map(x => ` ${x}`), 9 | ...dep2().map(x => ` ${x}`), 10 | ...dep3().map(x => ` ${x}`), 11 | ...dep4().map(x => ` ${x}`) 12 | ]; 13 | 14 | document.querySelector('pre').innerText += getDeps().join('\n') + '\n' + window.performance.now(); -------------------------------------------------------------------------------- /public/page2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Page 2

4 |

5 | 
6 | Home | Page 3 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/page3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Page 3

4 |

5 | 
6 | Home | Page 2 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/src/Bloomfilter.js: -------------------------------------------------------------------------------- 1 | 2 | var BloomFilter = (function(exports) { 3 | // Creates a new bloom filter. If *m* is an array-like object, with a length 4 | // property, then the bloom filter is loaded with data from the array, where 5 | // each element is a 32-bit integer. Otherwise, *m* should specify the 6 | // number of bits. Note that *m* is rounded up to the nearest multiple of 7 | // 32. *k* specifies the number of hashing functions. 8 | function BloomFilter(m, k) { 9 | var a; 10 | if (typeof m !== "number") a = m, m = a.length * 32; 11 | 12 | var n = Math.ceil(m / 32), 13 | i = -1; 14 | this.m = m = n * 32; 15 | this.k = k; 16 | 17 | var buckets = this.buckets = []; 18 | if (a) while (++i < n) buckets[i] = a[i]; 19 | else while (++i < n) buckets[i] = 0; 20 | this._locations = []; 21 | } 22 | 23 | // See http://willwhim.wpengine.com/2011/09/03/producing-n-hash-functions-by-hashing-only-once/ 24 | BloomFilter.prototype.locations = function(v) { 25 | var k = this.k, 26 | m = this.m, 27 | r = this._locations, 28 | a = fnv_1a(v), 29 | b = fnv_1a_b(a), 30 | x = a % m; 31 | for (var i = 0; i < k; ++i) { 32 | r[i] = x < 0 ? (x + m) : x; 33 | x = (x + b) % m; 34 | } 35 | return r; 36 | }; 37 | 38 | BloomFilter.prototype.add = function(v) { 39 | var l = this.locations(v + ""), 40 | k = this.k, 41 | buckets = this.buckets; 42 | for (var i = 0; i < k; ++i) buckets[Math.floor(l[i] / 32)] |= 1 << (l[i] % 32); 43 | }; 44 | 45 | BloomFilter.prototype.test = function(v) { 46 | var l = this.locations(v + ""), 47 | k = this.k, 48 | buckets = this.buckets; 49 | for (var i = 0; i < k; ++i) { 50 | var b = l[i]; 51 | if ((buckets[Math.floor(b / 32)] & (1 << (b % 32))) === 0) { 52 | return false; 53 | } 54 | } 55 | return true; 56 | }; 57 | 58 | // Estimated cardinality. 59 | BloomFilter.prototype.size = function() { 60 | var buckets = this.buckets, 61 | bits = 0; 62 | for (var i = 0, n = buckets.length; i < n; ++i) bits += popcnt(buckets[i]); 63 | return -this.m * Math.log(1 - bits / this.m) / this.k; 64 | }; 65 | 66 | BloomFilter.prototype.toHex = function (){ 67 | return this.buckets 68 | .map(b => b.toString(16)) 69 | .join('|'); 70 | }; 71 | 72 | BloomFilter.fromKeys = function(m, k, keys){ 73 | const bf = new BloomFilter(m, k); 74 | keys.forEach(k => bf.add(k)); 75 | return bf; 76 | }; 77 | 78 | // http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel 79 | function popcnt(v) { 80 | v -= (v >> 1) & 0x55555555; 81 | v = (v & 0x33333333) + ((v >> 2) & 0x33333333); 82 | return ((v + (v >> 4) & 0xf0f0f0f) * 0x1010101) >> 24; 83 | } 84 | 85 | // Fowler/Noll/Vo hashing. 86 | function fnv_1a(v) { 87 | var a = 2166136261; 88 | for (var i = 0, n = v.length; i < n; ++i) { 89 | var c = v.charCodeAt(i), 90 | d = c & 0xff00; 91 | if (d) a = fnv_multiply(a ^ d >> 8); 92 | a = fnv_multiply(a ^ c & 0xff); 93 | } 94 | return fnv_mix(a); 95 | } 96 | 97 | // a * 16777619 mod 2**32 98 | function fnv_multiply(a) { 99 | return a + (a << 1) + (a << 4) + (a << 7) + (a << 8) + (a << 24); 100 | } 101 | 102 | // One additional iteration of FNV, given a hash. 103 | function fnv_1a_b(a) { 104 | return fnv_mix(fnv_multiply(a)); 105 | } 106 | 107 | // See https://web.archive.org/web/20131019013225/http://home.comcast.net/~bretm/hash/6.html 108 | function fnv_mix(a) { 109 | a += a << 13; 110 | a ^= a >>> 7; 111 | a += a << 3; 112 | a ^= a >>> 17; 113 | a += a << 5; 114 | return a & 0xffffffff; 115 | } 116 | 117 | return BloomFilter; 118 | })(); 119 | 120 | 121 | String.prototype.padStart = String.prototype.padStart || function (maxLength, fillString=' ') { 122 | let str = String(this); 123 | if (str.length >= maxLength) { 124 | return str; 125 | } 126 | 127 | fillString = String(fillString); 128 | if (fillString.length === 0) { 129 | fillString = ' '; 130 | } 131 | 132 | let fillLen = maxLength - str.length; 133 | let timesToRepeat = Math.ceil(fillLen / fillString.length); 134 | let truncatedStringFiller = fillString 135 | .repeat(timesToRepeat) 136 | .slice(0, fillLen); 137 | return truncatedStringFiller + str; 138 | }; -------------------------------------------------------------------------------- /public/src/service-worker.js: -------------------------------------------------------------------------------- 1 | var version = '<% VERSION %>'; 2 | 3 | self.addEventListener('install', function(event) { 4 | console.log('[ServiceWorker] Installed version', version); 5 | console.log('[ServiceWorker] Skip waiting on install'); 6 | event.waitUntil(self.skipWaiting()); 7 | }); 8 | 9 | // `onactivate` is usually called after a worker was installed and the page 10 | // got refreshed. Since we call `skipWaiting()` in `oninstall`, `onactivate` is 11 | // called immediately. 12 | self.addEventListener('activate', function(event) { 13 | event.waitUntil((async () => { 14 | await deleteAllCachesExcept(version); 15 | 16 | // `claim()` sets this worker as the active worker for all clients that 17 | // match the workers scope and triggers an `oncontrollerchange` event for 18 | // the clients. 19 | console.log('[ServiceWorker] Claiming clients for version', version); 20 | await self.clients.claim(); 21 | console.log('[ServiceWorker] Claimed clients for version', version); 22 | })()); 23 | }); 24 | 25 | self.addEventListener('fetch', function(event) { 26 | console.log('[ServiceWorker] fetching', event.request.url); 27 | if (event.request.url.includes('/modules/')) { 28 | console.log('[ServiceWorker] Serving', event.request.url); 29 | event.respondWith((async () => { 30 | try{ 31 | const cache = await caches.open(version); 32 | let response = await cache.match(event.request); 33 | if(response){ 34 | return response; 35 | } 36 | 37 | console.warn(`[ServiceWorker] Cache is missing ${event.request.url}, fetching!`); 38 | const keys = await cache.keys(); 39 | const paths = keys.map(k => new URL(k.url).pathname) 40 | const bfs = BloomFilter.fromKeys(256, 6, paths).toHex(); 41 | console.log('[ServiceWorker] keys:', paths, bfs); 42 | const fetchRequest = new Request(event.request.url, { 43 | headers: new Headers({ 44 | 'bloom-filter': bfs 45 | }) 46 | }); 47 | 48 | response = await fetch(fetchRequest); 49 | if(response && response.status === 200 && response.type === 'basic') { 50 | cache.put(event.request, response.clone()); 51 | } 52 | 53 | return response; 54 | }catch(e){ 55 | console.error(e); 56 | } 57 | })()); 58 | } 59 | }); 60 | 61 | self.addEventListener('message', function(event) { 62 | if(event.data.command === 'clearCache'){ 63 | deleteAllCachesExcept(''); 64 | } 65 | }); 66 | 67 | async function deleteAllCachesExcept(cacheToKeep) { 68 | try{ 69 | // Delete old cache entries that don't match the current version. 70 | const cacheNames = await caches.keys(); 71 | await Promise.all( 72 | cacheNames.filter(name => name !== cacheToKeep).map(function(cacheName) { 73 | console.log('[ServiceWorker] Deleting old cache:', cacheName); 74 | return caches.delete(cacheName); 75 | }) 76 | ); 77 | }catch(e){ 78 | console.error(e); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Module Pusher 2 | 3 | ## [Demo](https://module-pusher.mariusgundersen.net/) 4 | 5 | This project demonstrates efficient module loading without bundling, by using service worker, http2, bloom filters and a smart server. It's a quick and dirty PoC -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var app = require('./bin/app.js'); 2 | 3 | app().catch(e => console.error(e && e.stack)); -------------------------------------------------------------------------------- /src/analyze.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import fs from 'fs-promise'; 3 | import glob from 'glob-promise'; 4 | import path from 'path'; 5 | import precinct from 'precinct'; 6 | 7 | export default async function(){ 8 | console.log(__dirname); 9 | const files = await glob('modules/*.js', {cwd:'public'}); 10 | 11 | console.log(files); 12 | 13 | const nameAndContent = files.map(async name => { 14 | const content = await fs.readFile(path.join('public', name), 'utf-8'); 15 | const depTree = precinct(content, {type: 'es6'}); 16 | return ['/'+name, depTree]; 17 | }); 18 | 19 | const depLinks = new Map(await Promise.all(nameAndContent)); 20 | const modules = new Map(); 21 | for(var [name, deps] of depLinks){ 22 | modules.set(name, { 23 | dependencies: flattenDepTree(deps, depLinks, [name]) 24 | }); 25 | } 26 | console.log(modules); 27 | return modules; 28 | }; 29 | 30 | function flattenDepTree(imports, tree, ignore){ 31 | var deps = _.chain(imports) 32 | .flatMap(imp => tree.get(imp)) 33 | .map(imp => imp) 34 | .uniq() 35 | .difference(ignore) 36 | .value(); 37 | 38 | return _.chain(imports) 39 | .concat(deps) 40 | .concat(deps.length == 0 ? [] : flattenDepTree(deps, tree, ignore.concat(imports))) 41 | .uniq() 42 | .value(); 43 | } -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | 2 | import serveStatic from 'serve-static'; 3 | import finalhandler from 'finalhandler'; 4 | import fs from 'fs-promise'; 5 | import glob from 'glob-promise'; 6 | import http2 from 'http2'; 7 | import Router from 'router'; 8 | import moduleTree from './moduleTree'; 9 | 10 | export default async function(){ 11 | const tryPush = await moduleTree(); 12 | 13 | console.log('start'); 14 | 15 | var router = new Router(); 16 | router.use(function(req, res, next){ 17 | if (res.push && /^\/modules\/.*\.js$/.test(req.url)) { 18 | console.log(' '); 19 | tryPush(req, res); 20 | } 21 | next(); 22 | }); 23 | 24 | router.use('/', serveStatic('public/bin', { 25 | maxAge: 0, 26 | etag: false, 27 | lastModified: false 28 | })); 29 | 30 | router.use('/', serveStatic('public', { 31 | maxAge: 0, 32 | etag: false, 33 | lastModified: false 34 | })); 35 | 36 | const [key, cert] = await Promise.all([ 37 | glob('./certs/*.key').then(([key]) => fs.readFile(key)), 38 | glob('./certs/*.crt').then(([crt]) => fs.readFile(crt)) 39 | ]); 40 | 41 | http2.createServer( 42 | { 43 | key, 44 | cert 45 | }, 46 | (req, res) => router(req, res, finalhandler(req, res)) 47 | ).listen(process.env.PORT || 8080); 48 | }; -------------------------------------------------------------------------------- /src/moduleTree.js: -------------------------------------------------------------------------------- 1 | import {BloomFilter} from 'bloomfilter'; 2 | import fs from 'fs'; 3 | import _ from 'lodash'; 4 | import path from 'path'; 5 | import analyze from './analyze'; 6 | 7 | export default async function(){ 8 | const depTree = await analyze(); 9 | return function tryPush(req, res){ 10 | try{ 11 | const moduleName = req.url; 12 | console.log('⇒', moduleName); 13 | if(!depTree.has(moduleName)){ 14 | return; 15 | } 16 | 17 | const hex = req.headers['bloom-filter']; 18 | console.log('hex:', hex); 19 | const bf = getBloomFilter(hex); 20 | 21 | const module = depTree.get(moduleName); 22 | const deps = module.dependencies.map(dep => ({ 23 | name: dep, 24 | has: bf.test(dep) 25 | })); 26 | 27 | for(const {name, has} of deps){ 28 | console.log(' ', has ? '✔' : '✘', name); 29 | } 30 | 31 | console.log('⇐', moduleName); 32 | deps.filter(d => !d.has).forEach(({name}) => { 33 | try{ 34 | console.log('⇐', name); 35 | const push = res.push(name); 36 | push.stream.on('error', error => { 37 | push.stream.removeAllListeners(); 38 | }); 39 | push.setHeader('content-type', 'application/javascript'); 40 | push.writeHead(200); 41 | fs.createReadStream(path.join(process.cwd(), 'public', name)).pipe(push); 42 | }catch(e){ 43 | console.error(e.stack || e); 44 | } 45 | }); 46 | }catch(e){ 47 | console.error(e.stack || e); 48 | } 49 | } 50 | }; 51 | 52 | function getBloomFilter(hex){ 53 | const bloomArray = _.chain(hex) 54 | .split('|') 55 | .map(x => parseInt(x, 16)) 56 | .value(); 57 | return new BloomFilter(bloomArray, 6); 58 | } --------------------------------------------------------------------------------