├── .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 | }
--------------------------------------------------------------------------------