How to create Web Server in Browser

In my GIT Web Terminal I use BrowserFS to write files (it’s required by isomorphic-git that I use to create git interface in browsers).
BrowserFS is implementation of node fs module but for browsers, it use few types of storage, I use indexedDB same as example for isomorphic-git.

I thought that i would be cool to edit the project itself and be able to view the files when I edit them in the browser (before I commit), like they where serve from web server. And it turn out it’s possible with Service Worker since you can access IndexedDB from that worker and you can create HTTP response from string or from arrayBuffer (BrowserFS is returning arrayBuffer from its readFile function).

Here is the code for service worker that show pages and directory listing:

self.addEventListener('install', function(evt) {
    self.skipWaiting();
    self.importScripts('https://cdn.jsdelivr.net/npm/browserfs');
    BrowserFS.configure({ fs: 'IndexedDB', options: {} }, function (err) {
        if (err) {
            console.log(err);
        } else {
            self.fs = BrowserFS.BFSRequire('fs');
            self.path = BrowserFS.BFSRequire('path');
        }
    });
});

self.addEventListener('fetch', function (event) {
    event.respondWith(new Promise(function(resolve, reject) {
        function sendFile(path) {
            fs.readFile(path, function(err, buffer) {
                if (err) {
                    err.fn = 'readFile(' + path + ')';
                    return reject(err);
                }
                resolve(new Response(buffer));
            });
        }
        var url = event.request.url;
        var m = url.match(/__browserfs__(.*)/);
        function redirect_dir() {
            return resolve(Response.redirect(url + '/', 301));
        }
        if (m && self.fs) {
            var path = m[1];
            if (path === '') {
                return redirect_dir();
            }
            console.log('serving ' + path + ' from browserfs');
            fs.stat(path, function(err, stat) {
                if (err) {
                    return resolve(textResponse(error404(path)));
                }
                if (stat.isFile()) {
                    sendFile(path);
                } else if (stat.isDirectory()) {
                    if (path.substr(-1, 1) !== '/') {
                        return redirect_dir();
                    }
                    fs.readdir(path, function(err, list) {
                        if (err) {
                            err.fn = 'readdir(' + path + ')';
                            return reject(err);
                        }
                        var len = list.length;
                        if (list.includes('index.html')) {
                            sendFile(path + '/index.html');
                        } else {
                            var output = [
                                '',
                                '',
                                '',
                                '<h1>BrowserFS</h1>',
                                '<ul>'
                            ];
                            if (path.match(/^\/(.*\/)/)) {
                                output.push('<li><a href="..">..</a></li>');
                            }
                            (function loop() {
                                var file = list.shift();
                                if (!file) {
                                    output = output.concat(['</ul>', '', '']);
                                    return resolve(textResponse(output.join('\n')));
                                }
                                fs.stat(path + '/' + file, function(err, stat) {
                                    if (err) {
                                        err.fn = 'stat(' + path + '/' + file + ')';
                                        return reject(err);
                                    }
                                    var name = file + (stat.isDirectory() ? '/' : '');
                                    output.push('<li><a href="' + name + '">' + name + '</a></li>');
                                    loop();
                                });
                            })();
                        }
                    });
                }
            });
        } else {
            fetch(event.request).then(resolve).catch(reject);
        }
    }));
});
function textResponse(string) {
    var blob = new Blob([string], {
        type: 'text/html'
    });
    return new Response(blob);
}

function error404(path) {
    var output = [
        '',
        '',
        '',
        '<h1>404 File Not Found</h1>',
        `File ${path} not found in browserfs`,
        '',
        ''
    ];
    return output.join('\n');
}

Service worker is using __browserfs__ marker to distinguish normal url from urls that are from BrowserFS/IndexedDB. Everything after it it’s serve from BrowserFS. So if you write file foo you can access it using __browserfs__/foo. Here is the code sample that will create a file using browserFS:

fs.writeFile('/foo', 'hello world', function(err) {
   if (err) {
     console.log('Error write file');
   }
});

You can see how this work in my GIT Web Terminal just clone any repo (best is the one that have directories) and view files with url https://jcubic.github.io/git/__browserfs__/repo/path/to/file.

Debugging code that call $resource in Angular with a Proxy

In angular you can add REST service using $resource factory, for instance you can create new REST object like this:

var studies =  $resource('http://example.com/api/studies/:id', {
  id: '@id'
}, {
  get: {
    method: 'GET',
    cache: false,
    interceptor: {
      response: doInterceptStudy
    }
  },
  query: {
    method: 'GET',
    cache: false,
    params: {
      size: 20
    }
  },
  lasarLookup: {
    bypassUiBlockInterceptor: true,
    method: 'GET',
    cache: false,
    params: {
      size: 20
    }
  },
  filteredQueryAll: {
    url: 'http://example.com/api/studies',
    method: 'POST',
    cache: false,
    params: {
      size: 20
    }
  }
});

and then call it using:

studies.get({id:123}, function(user) {
  $scope.user = user;
});

In chrome you can add breakpoint on XHR request but you will probably end up in angular code when you do that.

It’s probably better ot add breakpoint to lines where you call your $resource like studies.get, but if you have responses with lot of endpoints that are called in different places this is troublesome. But there is quick solution to add breakpoint on every call to resource with a ES6 proxy

studies = new Proxy(studies, {
    get: function(target, name) {
        if (typeof target[name] == 'function') {
            return function() {
                return target[name].apply(target, [].slice.call(arguments));
            };
        } else {
            return target[name];
        }
    }
 });

and you can add breakpoint inside function returned by get proxy method or you can add dubbugger statement:

studies = new Proxy(studies, {
    get: function(target, name) {
        if (typeof target[name] == 'function') {
            return function() {
                debugger;
                return target[name].apply(target, [].slice.call(arguments));
            };
        } else {
            return target[name];
        }
    }
 });

The support for Proxies is pretty decent, only IE don’t support it. You can check browser support on can I use.