<commit_before>angular.module('smartSelect', [])
    .factory('smartSelectHandler', function () {
        function SmartSelectHandler(options) {
            var config = angular.merge({
                multiple: false,
                values: [],
                onCreateValue: function (query) {
                    //todo promise
                    if (typeof(query) != 'string') {
                        query = '';
                    }
                    query = query.trim();
                    if (query == '') {
                        return false;
                    }
                    return query;
                },
                checkUnique: function (compareValue) {
                    var flag = true;
                    angular.forEach(this.values, function (e) {
                        if (e == compareValue) {
                            flag = false;
                        }
                    });
                    return flag;
                }
            }, options);

            this.createValue = function (query) {
                var flag = false;
                var value = config.onCreateValue(query);
                if (value !== false) {
                    if (config.checkUnique(value)) {
                        config.values.push(value);
                        flag = true;
                    }
                }
                return flag;
            };

            this.getValues = function () {
                return config.values;
            };

            this.removeValue = function (value) {
                config.values.splice(config.values.indexOf(value), 1);
            };

            this.focusInput = false;
            this.focusValue = null; //todo
        }

        return function (options) {
            return new SmartSelectHandler(options)
        }
    })
    .directive('smartSelect', function () {
        return {
            restrict: 'E',
            scope: {
                handler: '='
            },
            template: '\

\

\
                    <smart-select-value value="value" ng-repeat="value in handler.getValues()"></smart-select-value>\
                    <smart-select-input></smart-select-input>\

\

\

',
            controller: function ($scope) {
                this.handler = $scope.handler;

                $scope.focusInput = function ($event) {
                    if ($event.currentTarget == $event.target) {
                        $scope.handler.focusInput = true;
                    }
                };
            }
        }
    })
    .directive('smartSelectInput', function ($timeout) {
        //todo custom template
        return {
            restrict: 'E',
            replace: true,
            require: '^smartSelect',
            scope: {},
            template: '\
            <input type="text"\
                   ng-trim="false"\
                   ng-model="query"\
                   ng-focus="handler.focusInput = true;"\
                   ng-blur="timeoutUpdate(); handler.focusInput = false;"\
                   ng-cut="timeoutUpdate()"\
                   ng-paste="timeoutUpdate()"\
                   ng-keydown="keyDown($event)">',
            link: function ($scope, $element, $attr, $smartSelectCtrl) {
                $scope.handler = $smartSelectCtrl.handler;
                var wrapper = angular.element('#smart-select-input-wrapper');
                if (!wrapper.length) {
                    wrapper = angular.element('

');
                    angular.element('body').append(wrapper);
                }
                var mirror = angular.element('');
                angular.forEach([
                    'fontFamily',
                    'fontSize',
                    'fontWeight',
                    'fontStyle',
                    'letterSpacing',
                    'textTransform',
                    'wordSpacing',
                    'textIndent',
                    'boxSizing',
                    'borderRightWidth',
                    'borderLeftWidth',
                    'borderLeftStyle',
                    'borderRightStyle',
                    'paddingLeft',
                    'paddingRight',
                    'marginLeft',
                    'marginRight'
                ], function (value) {
                    mirror.css(value, $element.css(value));
                });
                wrapper.append(mirror);
                function update(text) {
                    text = text ? text : ' ';
                    mirror.text(text || $attr.placeholder);
                    $element.css('width', mirror.outerWidth() + 1);
                }

                $scope.query = null;

                function getPos(element) {
                    if ('selectionStart' in element) {
                        return element.selectionStart;
                    } else if (document.selection) {
                        element.focus();
                        var sel = document.selection.createRange();
                        var selLen = document.selection.createRange().text.length;
                        sel.moveStart('character', -element.value.length);
                        return sel.text.length - selLen;
                    }
                }

                $scope.keyDown = function ($event) {
                    switch ($event.which) {
                        case 13: // enter
                            if ($scope.handler.createValue($scope.query)) {
                                $scope.query = null;
                                $timeout(function () {
                                    $element.css('width', 0);
                                })
                            }
                            break;
                        case 8:
                            if (getPos($element[0]) == 0) {
                                //focus last
                                $scope.handler.focusValue = $scope.handler.getValues()[$scope.handler.getValues().length - 1]
                            }
                            break;
                    }
                };
                $scope.$watch('query', function (newText) {
                    update(newText);
                });

                $scope.timeoutUpdate = function () {
                    $timeout(function () {
                        update($scope.query);
                    })
                };

                $scope.$watch(
                    function () {
                        return $smartSelectCtrl.handler.focusInput
                    },
                    function (isFocus) {
                        if (isFocus) {
                            $element.focus();
                        }
                    }
                );

                $scope.$on('$destroy', function () {
                    mirror.remove();
                });
            }
        }
    })
    .directive('smartSelectValue', function () {
        //todo custom template
        return {
            restrict: 'E',
            replace: true,
            require: '^smartSelect',
            scope: {
                value: '='
            },
            template: '\

\
                \
                ×\

',
            link: function ($scope, $element, $attr, $smartSelectCtrl) {
                $scope.handler = $smartSelectCtrl.handler;

                $scope.remove = function () {
                    $scope.handler.removeValue($scope.value);
                };

                $scope.onKeyDown = function ($event) {
                    if ($event.which == 8 || $event.which == 46) {
                        var index;
                        switch ($event.which) {
                            case 8:
                                index = $scope.handler.getValues().indexOf($scope.value) - 1;
                                break;
                            case 46:
                                index = $scope.handler.getValues().indexOf($scope.value) + 1;
                                break;
                        }

                        if ($scope.handler.getValues()[index]) {
                            $scope.handler.focusValue = $scope.handler.getValues()[index]
                        } else {
                            $scope.handler.focusValue = null;
                            $scope.handler.focusInput = true;
                        }
                        $scope.remove();
                    }
                };

                $scope.$watch(
                    function () {
                        return $scope.handler.focusValue
                    },
                    function (focusValue) {
                        if (focusValue == $scope.value) {
                            $element.focus();
                        }
                    }
                );
            }
        }
    });<commit_msg>- remove value<commit_after>angular.module('smartSelect', [])
    .factory('smartSelectHandler', function () {
        function SmartSelectHandler(options) {
            var config = angular.merge({
                multiple: false,
                values: [],
                onCreateValue: function (query) {
                    //todo promise
                    if (typeof(query) != 'string') {
                        query = '';
                    }
                    query = query.trim();
                    if (query == '') {
                        return false;
                    }
                    return query;
                },
                checkUnique: function (compareValue) {
                    var flag = true;
                    angular.forEach(this.values, function (e) {
                        if (e == compareValue) {
                            flag = false;
                        }
                    });
                    return flag;
                }
            }, options);

            this.createValue = function (query) {
                var flag = false;
                var value = config.onCreateValue(query);
                if (value !== false) {
                    if (config.checkUnique(value)) {
                        config.values.push(value);
                        flag = true;
                    }
                }
                return flag;
            };

            this.getValues = function () {
                return config.values;
            };

            this.removeValue = function (value) {
                config.values.splice(config.values.indexOf(value), 1);
            };

            this.focusInput = false;
            this.focusValue = null; //todo
        }

        return function (options) {
            return new SmartSelectHandler(options)
        }
    })
    .directive('smartSelect', function () {
        return {
            restrict: 'E',
            scope: {
                handler: '='
            },
            template: '\

\

\
                    <smart-select-value value="value" ng-repeat="value in handler.getValues()"></smart-select-value>\
                    <smart-select-input></smart-select-input>\

\

\

',
            controller: function ($scope) {
                this.handler = $scope.handler;

                $scope.focusInput = function ($event) {
                    if ($event.currentTarget == $event.target) {
                        $scope.handler.focusInput = true;
                    }
                };
            }
        }
    })
    .directive('smartSelectInput', function ($timeout) {
        //todo custom template
        return {
            restrict: 'E',
            replace: true,
            require: '^smartSelect',
            scope: {},
            template: '\
            <input type="text"\
                   ng-trim="false"\
                   ng-model="query"\
                   ng-focus="handler.focusInput = true;"\
                   ng-blur="timeoutUpdate(); handler.focusInput = false;"\
                   ng-cut="timeoutUpdate()"\
                   ng-paste="timeoutUpdate()"\
                   ng-keydown="keyDown($event)">',
            link: function ($scope, $element, $attr, $smartSelectCtrl) {
                $scope.handler = $smartSelectCtrl.handler;
                var wrapper = angular.element('#smart-select-input-wrapper');
                if (!wrapper.length) {
                    wrapper = angular.element('

');
                    angular.element('body').append(wrapper);
                }
                var mirror = angular.element('');
                angular.forEach([
                    'fontFamily',
                    'fontSize',
                    'fontWeight',
                    'fontStyle',
                    'letterSpacing',
                    'textTransform',
                    'wordSpacing',
                    'textIndent',
                    'boxSizing',
                    'borderRightWidth',
                    'borderLeftWidth',
                    'borderLeftStyle',
                    'borderRightStyle',
                    'paddingLeft',
                    'paddingRight',
                    'marginLeft',
                    'marginRight'
                ], function (value) {
                    mirror.css(value, $element.css(value));
                });
                wrapper.append(mirror);
                function update(text) {
                    text = text ? text : ' ';
                    mirror.text(text || $attr.placeholder);
                    $element.css('width', mirror.outerWidth() + 1);
                }

                $scope.query = null;

                function getPos(element) {
                    if ('selectionStart' in element) {
                        return element.selectionStart;
                    } else if (document.selection) {
                        element.focus();
                        var sel = document.selection.createRange();
                        var selLen = document.selection.createRange().text.length;
                        sel.moveStart('character', -element.value.length);
                        return sel.text.length - selLen;
                    }
                }

                $scope.keyDown = function ($event) {
                    switch ($event.which) {
                        case 13: // enter
                            if ($scope.handler.createValue($scope.query)) {
                                $scope.query = null;
                                $timeout(function () {
                                    $element.css('width', 0);
                                })
                            }
                            break;
                        case 8:
                            if (getPos($element[0]) == 0) {
                                //focus last
                                $scope.handler.focusValue = $scope.handler.getValues()[$scope.handler.getValues().length - 1]
                            }
                            break;
                    }
                };
                $scope.$watch('query', function (newText) {
                    update(newText);
                });

                $scope.timeoutUpdate = function () {
                    $timeout(function () {
                        update($scope.query);
                    })
                };

                $scope.$watch(
                    function () {
                        return $smartSelectCtrl.handler.focusInput
                    },
                    function (isFocus) {
                        if (isFocus) {
                            $element.focus();
                        }
                    }
                );

                $scope.$on('$destroy', function () {
                    mirror.remove();
                });
            }
        }
    })
    .directive('smartSelectValue', function () {
        //todo custom template
        return {
            restrict: 'E',
            replace: true,
            require: '^smartSelect',
            scope: {
                value: '='
            },
            template: '\

\
                \
                ×\

',
            link: function ($scope, $element, $attr, $smartSelectCtrl) {
                $scope.handler = $smartSelectCtrl.handler;

                $scope.remove = function () {
                    $scope.handler.removeValue($scope.value);
                };

                $scope.onKeyDown = function ($event) {
                    if ($event.which == 8 || $event.which == 46) {
                        var index;
                        switch ($event.which) {
                            case 8:
                                index = $scope.handler.getValues().indexOf($scope.value) - 1;
                                break;
                            case 46:
                                index = $scope.handler.getValues().indexOf($scope.value) + 1;
                                break;
                        }

                        if ($scope.handler.getValues()[index]) {
                            $scope.handler.focusValue = $scope.handler.getValues()[index]
                        } else {
                            $scope.handler.focusValue = null;
                            $scope.handler.focusInput = true;
                        }
                        $scope.remove();
                    }
                };
                $scope.$watch(
                    function () {
                        return $scope.handler.focusValue
                    },
                    function (focusValue) {
                        if (focusValue == $scope.value) {
                            $element.focus();
                        }
                    }
                );
            }
        }
    });<|endoftext|>


<commit_before>/* Copyright 2013 Chris Wilson

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

window.AudioContext = window.AudioContext || window.webkitAudioContext;

var audioContext = new AudioContext();
var audioInput = null,
    realAudioInput = null,
    inputPoint = null,
    audioRecorder = null;

var rafID = null;
var analyserContext = null;
var canvasWidth, canvasHeight;
var recIndex = 0;

/* TODO:

- offer mono option
- "Monitor input" switch
*/

function saveAudio() {
    audioRecorder.exportWAV( doneEncoding );
    // could get mono instead by saying
    // audioRecorder.exportMonoWAV( doneEncoding );
}

function gotBuffers( buffers ) {
    var canvas = document.getElementById( "wavedisplay" );

    drawBuffer( canvas.width, canvas.height, canvas.getContext('2d'), buffers[0] );

    // the ONLY time gotBuffers is called is right after a new recording is completed -
    // so here's where we should set up the download.
    audioRecorder.exportWAV( doneEncoding );
}

function doneEncoding( blob ) {
    Recorder.setupDownload( blob, "myRecording" + ((recIndex<10)?"0":"") + recIndex + ".wav" );
    recIndex++;
}

function toggleRecording( e ) {
    if (e.classList.contains("recording")) {
       //stop recording
        audioRecorder.stop();
        e.classList.remove("recording");
        audioRecorder.getBuffers( gotBuffers );
		//end timer
        Recordtimer(audioRecorder);
    } else {
        //start recording
        if (!audioRecorder)
            return;
        e.classList.add("recording");
        audioRecorder.clear();
        audioRecorder.record();
		//Start timer
		Recordtimer(audioRecorder);

    }
}

function Recordtimer(audioRecorder){

			if (audioRecorder){
			//to record time
			var Start_time = new Date();

			}
			else {
			//To stop recording
		    var end_time = new Date();
			var recordingtime = end_time - Start_time;
			var seconds = Math.round(Start_time/1000);
			var minutes = Math.round(seconds/60);
			var hours = Math.round(minutes /60);
			document.getElementById("Recordingtime").innerHTML= (hours":"minutes":"seconds);
			}
}

function convertToMono( input ) {
    var splitter = audioContext.createChannelSplitter(2);
    var merger = audioContext.createChannelMerger(2);

    input.connect( splitter );
    splitter.connect( merger, 0, 0 );
    splitter.connect( merger, 0, 1 );
    return merger;
}

function cancelAnalyserUpdates() {
    window.cancelAnimationFrame( rafID );
    rafID = null;
}

function updateAnalysers(            ) {
    if (!analyserContext) {
        var canvas = document.getElementById("analyser");
        canvasWidth = canvas.width;
        canvasHeight = canvas.height;
        analyserContext = canvas.getContext('2d');
    }

    // analyzer draw code here
    {
        var SPACING = 3;
        var BAR_WIDTH = 1;
        var numBars = Math.round(canvasWidth / SPACING);
        var freqByteData = new Uint8Array(analyserNode.frequencyBinCount);

        analyserNode.getByteFrequencyData(freqByteData);

        analyserContext.clearRect(0, 0, canvasWidth, canvasHeight);
        analyserContext.fillStyle = '#F6D565';
        analyserContext.lineCap = 'round';
        var multiplier = analyserNode.frequencyBinCount / numBars;

        // Draw rectangle for each frequency bin.
        for (var i = 0; i < numBars; ++i) {
            var magnitude = 0;
            var offset = Math.floor( i * multiplier );
            // gotta sum/average the block, or we miss narrow-bandwidth spikes
            for (var j = 0; j< multiplier; j++)
                magnitude += freqByteData[offset + j];
            magnitude = magnitude / multiplier;
            var magnitude2 = freqByteData[i * multiplier];
            analyserContext.fillStyle = "hsl( " + Math.round((i*360)/numBars) + ", 100%, 50%)";
            analyserContext.fillRect(i * SPACING, canvasHeight, BAR_WIDTH, -magnitude);
        }
    }

    rafID = window.requestAnimationFrame( updateAnalysers );
}

function toggleMono() {
    if (audioInput != realAudioInput) {
        audioInput.disconnect();
        realAudioInput.disconnect();
        audioInput = realAudioInput;
    } else {
        realAudioInput.disconnect();
        audioInput = convertToMono( realAudioInput );
    }

    audioInput.connect(inputPoint);
}

function gotStream(stream) {
    inputPoint = audioContext.createGain();

    // Create an AudioNode from the stream.
    realAudioInput = audioContext.createMediaStreamSource(stream);
    audioInput = realAudioInput;
    audioInput.connect(inputPoint);

//    audioInput = convertToMono( input );

    analyserNode = audioContext.createAnalyser();
    analyserNode.fftSize = 2048;
    inputPoint.connect( analyserNode );

    audioRecorder = new Recorder( inputPoint );

    zeroGain = audioContext.createGain();
    zeroGain.gain.value = 0.0;
    inputPoint.connect( zeroGain );
    zeroGain.connect( audioContext.destination );
    updateAnalysers();
}

function initAudio() {
        if (!navigator.getUserMedia)
            navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
        if (!navigator.cancelAnimationFrame)
            navigator.cancelAnimationFrame = navigator.webkitCancelAnimationFrame || navigator.mozCancelAnimationFrame;
        if (!navigator.requestAnimationFrame)
            navigator.requestAnimationFrame = navigator.webkitRequestAnimationFrame || navigator.mozRequestAnimationFrame;

    navigator.getUserMedia(
        {
            "audio": {
                "mandatory": {
                    "googEchoCancellation": "false",
                    "googAutoGainControl": "false",
                    "googNoiseSuppression": "false",
                    "googHighpassFilter": "false"
                },
                "optional": []
            },
        }, gotStream, function(e) {
            alert('Error getting audio');
            console.log(e);
        });
}

window.addEventListener('load', initAudio );
<commit_msg>Update main.js<commit_after>/* Copyright 2013 Chris Wilson

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

window.AudioContext = window.AudioContext || window.webkitAudioContext;

var audioContext = new AudioContext();
var audioInput = null,
    realAudioInput = null,
    inputPoint = null,
    audioRecorder = null;

var rafID = null;
var analyserContext = null;
var canvasWidth, canvasHeight;
var recIndex = 0;

/* TODO:

- offer mono option
- "Monitor input" switch
*/

function saveAudio() {
    audioRecorder.exportWAV( doneEncoding );
    // could get mono instead by saying
    // audioRecorder.exportMonoWAV( doneEncoding );
}

function gotBuffers( buffers ) {
    var canvas = document.getElementById( "wavedisplay" );

    drawBuffer( canvas.width, canvas.height, canvas.getContext('2d'), buffers[0] );

    // the ONLY time gotBuffers is called is right after a new recording is completed -
    // so here's where we should set up the download.
    audioRecorder.exportWAV( doneEncoding );
}

function doneEncoding( blob ) {
    Recorder.setupDownload( blob, "myRecording" + ((recIndex<10)?"0":"") + recIndex + ".wav" );
    recIndex++;
}

function toggleRecording( e ) {
    if (e.classList.contains("recording")) {
       //stop recording
        audioRecorder.stop();
        e.classList.remove("recording");
        audioRecorder.getBuffers( gotBuffers );
		//end timer
        Recordtimer(audioRecorder);
    } else {
        //start recording
        if (!audioRecorder)
            return;
        e.classList.add("recording");
        audioRecorder.clear();
        audioRecorder.record();
		//Start timer
		Recordtimer(audioRecorder);

    }
}

function Recordtimer(audioRecorder){

			if (audioRecorder){
			//to record time
			var Start_time = new Date();

			}
			else {
			//To stop recording
		    var end_time = new Date();
			var recordingtime = end_time - Start_time;
			var seconds = Math.round(Start_time/1000);
			var minutes = Math.round(seconds/60);
			var hours = Math.round(minutes /60);
			document.getElementById("Recordingtime").innerHTML= hours":"minutes":"seconds;
			}
}

function convertToMono( input ) {
    var splitter = audioContext.createChannelSplitter(2);
    var merger = audioContext.createChannelMerger(2);

    input.connect( splitter );
    splitter.connect( merger, 0, 0 );
    splitter.connect( merger, 0, 1 );
    return merger;
}

function cancelAnalyserUpdates() {
    window.cancelAnimationFrame( rafID );
    rafID = null;
}

function updateAnalysers(            ) {
    if (!analyserContext) {
        var canvas = document.getElementById("analyser");
        canvasWidth = canvas.width;
        canvasHeight = canvas.height;
        analyserContext = canvas.getContext('2d');
    }

    // analyzer draw code here
    {
        var SPACING = 3;
        var BAR_WIDTH = 1;
        var numBars = Math.round(canvasWidth / SPACING);
        var freqByteData = new Uint8Array(analyserNode.frequencyBinCount);

        analyserNode.getByteFrequencyData(freqByteData);

        analyserContext.clearRect(0, 0, canvasWidth, canvasHeight);
        analyserContext.fillStyle = '#F6D565';
        analyserContext.lineCap = 'round';
        var multiplier = analyserNode.frequencyBinCount / numBars;

        // Draw rectangle for each frequency bin.
        for (var i = 0; i < numBars; ++i) {
            var magnitude = 0;
            var offset = Math.floor( i * multiplier );
            // gotta sum/average the block, or we miss narrow-bandwidth spikes
            for (var j = 0; j< multiplier; j++)
                magnitude += freqByteData[offset + j];
            magnitude = magnitude / multiplier;
            var magnitude2 = freqByteData[i * multiplier];
            analyserContext.fillStyle = "hsl( " + Math.round((i*360)/numBars) + ", 100%, 50%)";
            analyserContext.fillRect(i * SPACING, canvasHeight, BAR_WIDTH, -magnitude);
        }
    }

    rafID = window.requestAnimationFrame( updateAnalysers );
}

function toggleMono() {
    if (audioInput != realAudioInput) {
        audioInput.disconnect();
        realAudioInput.disconnect();
        audioInput = realAudioInput;
    } else {
        realAudioInput.disconnect();
        audioInput = convertToMono( realAudioInput );
    }

    audioInput.connect(inputPoint);
}

function gotStream(stream) {
    inputPoint = audioContext.createGain();

    // Create an AudioNode from the stream.
    realAudioInput = audioContext.createMediaStreamSource(stream);
    audioInput = realAudioInput;
    audioInput.connect(inputPoint);

//    audioInput = convertToMono( input );

    analyserNode = audioContext.createAnalyser();
    analyserNode.fftSize = 2048;
    inputPoint.connect( analyserNode );

    audioRecorder = new Recorder( inputPoint );

    zeroGain = audioContext.createGain();
    zeroGain.gain.value = 0.0;
    inputPoint.connect( zeroGain );
    zeroGain.connect( audioContext.destination );
    updateAnalysers();
}

function initAudio() {
        if (!navigator.getUserMedia)
            navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
        if (!navigator.cancelAnimationFrame)
            navigator.cancelAnimationFrame = navigator.webkitCancelAnimationFrame || navigator.mozCancelAnimationFrame;
        if (!navigator.requestAnimationFrame)
            navigator.requestAnimationFrame = navigator.webkitRequestAnimationFrame || navigator.mozRequestAnimationFrame;

    navigator.getUserMedia(
        {
            "audio": {
                "mandatory": {
                    "googEchoCancellation": "false",
                    "googAutoGainControl": "false",
                    "googNoiseSuppression": "false",
                    "googHighpassFilter": "false"
                },
                "optional": []
            },
        }, gotStream, function(e) {
            alert('Error getting audio');
            console.log(e);
        });
}

window.addEventListener('load', initAudio );
<|endoftext|>


<commit_before>(function(root, factory) {
  if (typeof define === "function" && define.amd) {
    // AMD
    define([], factory);
  } else if (typeof exports === "object") {
    // CommonJS
    module.exports = factory();
  } else {
    // Browser globals
    root.IncrementalIndexedDBAdapter = factory();
  }
})(this, function() {
  return (function() {
    "use strict";

    /**
     * An improved Loki persistence adapter for IndexedDB (not compatible with LokiIndexedAdapter)
     *     Unlike LokiIndexedAdapter, the database is saved not as one big JSON blob, but split into
     *     small chunks with individual collection documents. When saving, only the chunks with changed
     *     documents (and database metadata) is saved to IndexedDB. This speeds up small incremental
     *     saves by an order of magnitude on large (tens of thousands of records) databases. It also
     *     avoids Safari 13 bug that would cause the database to balloon in size to gigabytes
     *
     *     The `appname` argument is not provided - to distinguish between multiple app on the same
     *     domain, simply use a different Loki database name
     *
     * @example
     * var adapter = new IncrementalIndexedDBAdapter();
     *
     * @constructor IncrementalIndexedDBAdapter
     */
    function IncrementalIndexedDBAdapter() {
      this.mode = "incremental";
      this.chunkSize = 100;
      this.idb = null; // will be lazily loaded on first operation that needs it
    }

    // chunkId - index of the data chunk - e.g. chunk 0 will be lokiIds 0-99
    IncrementalIndexedDBAdapter.prototype._getChunk = function(collection, chunkId) {
      // 0-99, 100-199, etc.
      var minId = chunkId * this.chunkSize;
      var maxId = minId + this.chunkSize - 1;

      // use idIndex to find first collection.data position within the $loki range
      var idIndex = collection.idIndex;

      var firstDataPosition = null;

      var max = idIndex.length - 1,
        min = 0,
        mid;

      while (idIndex[min] < idIndex[max]) {
        mid = (min + max) >> 1;

        if (idIndex[mid] < minId) {
          min = mid + 1;
        } else {
          max = mid;
        }
      }

      if (max === min && idIndex[min] >= minId) {
        firstDataPosition = min;
      }

      if (firstDataPosition === null) {
        // no elements in this chunk
        return [];
      }

      // find last position
      // if loki IDs are contiguous (no removed elements), last position will be first + chunk - 1
      // (and we look back in case there are missing pieces)
      // TODO: Binary search (not as important as first position, worst case scanario is only chunkSize steps)
      var lastDataPosition = null;
      for (var i = firstDataPosition + this.chunkSize - 1; i >= firstDataPosition; i--) {
        if (idIndex[i] <= maxId) {
          lastDataPosition = i;
          break;
        }
      }

      // TODO: remove sanity checks when everything is fully tested
      var firstElement = collection.data[firstDataPosition];
      if (!(firstElement && firstElement.$loki >= minId && firstElement.$loki <= maxId)) {
        throw new Error("broken invariant firstelement");
      }

      var lastElement = collection.data[lastDataPosition];
      if (!(lastElement && lastElement.$loki >= minId && lastElement.$loki <= maxId)) {
        throw new Error("broken invariant lastElement");
      }

      // this will have *up to* 'this.chunkSize' elements (might have less, because $loki ids
      // will have holes when data is deleted)
      var chunkData = collection.data.slice(firstDataPosition, lastDataPosition + 1);

      // TODO: remove sanity checks when everything is fully tested
      if (chunkData.length > this.chunkSize) {
        throw new Error("broken invariant - chunk size");
      }

      return chunkData;
    };

    /**
     * Incrementally saves the database to IndexedDB
     *
     * @example
     * var idbAdapter = new IncrementalIndexedDBAdapter();
     * var db = new loki('test', { adapter: idbAdapter });
     * var coll = db.addCollection('testColl');
     * coll.insert({test: 'val'});
     * db.saveDatabase();
     *
     * @param {string} dbname - the name to give the serialized database
     * @param {object} dbcopy - copy of the Loki database
     * @param {function} callback - (Optional) callback passed obj.success with true or false
     * @memberof IncrementalIndexedDBAdapter
     */
    IncrementalIndexedDBAdapter.prototype.saveDatabase = function(dbname, loki, callback) {
      var that = this;
      console.log("-- exportDatabase - begin");
      console.time("exportDatabase");

      var chunksToSave = [];

      console.time("makeChunks");
      loki.collections.forEach(function(collection, i) {
        // Find dirty chunk ids
        var dirtyChunks = new Set();
        collection.dirtyIds.forEach(function(lokiId) {
          var chunkId = (lokiId / that.chunkSize) | 0;
          dirtyChunks.add(chunkId);
        });
        collection.dirtyIds = [];

        // Serialize chunks to save
        dirtyChunks.forEach(function(chunkId) {
          var chunkData = that._getChunk(collection, chunkId);
          // we must stringify now, because IDB is asynchronous, and underlying objects are mutable
          chunksToSave.push({
            key: collection.name + ".chunk." + chunkId,
            value: JSON.stringify(chunkData),
          });
        });

        collection.data = [];
        // this is recreated on load anyway, so we can make metadata smaller
        collection.isIndex = [];

        // save collection metadata as separate chunk, leave only names in loki
        // TODO: To reduce IO, we should only save this chunk when it has changed
        chunksToSave.push({
          key: collection.name + ".metadata",
          value: JSON.stringify(collection),
        });
        loki.collections[i] = { name: collection.name };
      });
      console.timeEnd("makeChunks");

      var serializedMetadata = JSON.stringify(loki);
      loki = null; // allow GC of the DB copy

      // console.log(chunksToSave)
      // console.log(chunkIdsToRemove)
      // console.log(JSON.parse(serializedMetadata))

      chunksToSave.push({ key: "loki", value: serializedMetadata });

      that._saveChunks(dbname, chunksToSave, callback);
    };

    /**
     * Retrieves a serialized db string from the catalog.
     *
     * @example
     * // LOAD
     * var idbAdapter = new IncrementalIndexedDBAdapter();
     * var db = new loki('test', { adapter: idbAdapter });
     * db.loadDatabase(function(result) {
     *   console.log('done');
     * });
     *
     * @param {string} dbname - the name of the database to retrieve.
     * @param {function} callback - callback should accept string param containing serialized db string.
     * @memberof IncrementalIndexedDBAdapter
     */
    IncrementalIndexedDBAdapter.prototype.loadDatabase = function(dbname, callback) {
      var that = this;
      console.log("-- loadDatabase - begin");
      console.time("loadDatabase");
      this._getAllChunks(dbname, function(chunks) {
        if (!Array.isArray(chunks)) {
          // we got an error
          callback(chunks);
        }

        if (!chunks.length) {
          callback(null);
          return;
        }

        console.log("Found chunks:", chunks.length);

        that._sortChunksInPlace(chunks);

        // repack chunks into a map
        var loki;
        var chunkCollections = {};

        // console.time('repack')
        chunks.forEach(function(object) {
          var key = object.key;
          var value = object.value;
          if (key === "loki") {
            loki = value;
            return;
          } else if (key.includes(".")) {
            var keySegments = key.split(".");
            if (keySegments.length === 3 && keySegments[1] === "chunk") {
              var colName = keySegments[0];
              if (chunkCollections[colName]) {
                chunkCollections[colName].dataChunks.push(value);
              } else {
                chunkCollections[colName] = {
                  metadata: null,
                  dataChunks: [value],
                };
              }
              return;
            } else if (keySegments.length === 2 && keySegments[1] === "metadata") {
              var name = keySegments[0];
              if (chunkCollections[name]) {
                chunkCollections[name].metadata = value;
              } else {
                chunkCollections[name] = { metadata: value, dataChunks: [] };
              }
              return;
            }
          }

          console.error("Unknown chunk " + key);
          callback(new Error("Invalid database - unknown chunk found"));
        });
        chunks = null;
        // console.timeEnd('repack')
        // console.log('chunkCollections', chunkCollections)

        if (!loki) {
          callback(new Error("Invalid database - missing database metadata"));
        }

        // parse Loki object
        loki = JSON.parse(loki);

        // populate collections with data
        console.time("populate");
        that._populate(loki, chunkCollections);
        chunkCollections = null;
        console.timeEnd("populate");

        console.timeEnd("loadDatabase");
        callback(loki);
      });
    };

    IncrementalIndexedDBAdapter.prototype._sortChunksInPlace = function(chunks) {
      // sort chunks in place to load data in the right order (ascending loki ids)
      // on both Safari and Chrome, we'll get chunks in order like this: 0, 1, 10, 100...
      // console.time('sort')
      var getSortKey = function(object) {
        var key = object.key;
        if (key.includes(".")) {
          var segments = key.split(".");
          if (segments.length === 3 && segments[1] === "chunk") {
            return parseInt(segments[2], 10);
          }
        }

        return -1; // consistent type must be returned
      };
      chunks.sort(function(a, b) {
        var aKey = getSortKey(a),
          bKey = getSortKey(b);
        if (aKey < bKey) return -1;
        if (aKey > bKey) return 1;
        return 0;
      });
      // console.timeEnd('sort')
      // console.log('Sorted chunks', chunks)
    };

    IncrementalIndexedDBAdapter.prototype._populate = function(loki, chunkCollections) {
      loki.collections.forEach(function(collectionStub, i) {
        var chunkCollection = chunkCollections[collectionStub.name];

        if (chunkCollection) {
          // TODO: What if metadata is missing?
          var collection = JSON.parse(chunkCollection.metadata);
          chunkCollection.metadata = null;

          loki.collections[i] = collection;

          var dataChunks = chunkCollection.dataChunks;
          dataChunks.forEach(function(chunkObj, i) {
            var chunk = JSON.parse(chunkObj);
            chunkObj = null; // make string available for GC
            dataChunks[i] = null;

            chunk.forEach(function(doc) {
              collection.data.push(doc);
            });
          });
        }
      });
    };

    IncrementalIndexedDBAdapter.prototype._initializeIDB = function(dbname, onError, onSuccess) {
      var that = this;
      console.log("initializing idb");

      if (this.idbInitInProgress) {
        throw new Error("Cannot open IndexedDB because open is already in progress");
      }
      this.idbInitInProgress = true;

      var openRequest = indexedDB.open(dbname, 1);

      openRequest.onupgradeneeded = function(e) {
        var db = e.target.result;

        if (e.oldVersion < 1) {
          // Version 1 - Initial - Create database
          db.createObjectStore('LokiIncrementalData', { keyPath: "key" });
        } else {
          // Unknown version
          throw new Error("Invalid old version " + e.oldVersion + " for IndexedDB upgrade");
        }
      };

      openRequest.onsuccess = function(e) {
        that.idbInitInProgress = false;
        console.log("init success");
        that.idb = e.target.result;
        onSuccess();
      };

      openRequest.onblocked = function(e) {
        console.error("IndexedDB open is blocked", e);
        onError(new Error("IndexedDB open is blocked by open connection"));
      };

      openRequest.onerror = function(e) {
        that.idbInitInProgress = false;
        console.error("IndexeddB open error", e);
        onError(e);
      };
    };

    IncrementalIndexedDBAdapter.prototype._saveChunks = function(dbname, chunks, callback) {
      var that = this;
      if (!this.idb) {
        this._initializeIDB(dbname, callback, function() {
          that._saveChunks(dbname, chunks, callback);
        });
        return;
      }

      console.time("save chunks to idb");

      if (this.operationInProgress) {
        throw new Error("Error while saving to database - another operation is already in progress. Please use throttledSaves=true option on Loki object");
      }

      this.operationInProgress = true;

      var tx = this.idb.transaction(['LokiIncrementalData'], "readwrite");
      tx.oncomplete = function() {
        that.operationInProgress = false;
        console.timeEnd("save chunks to idb");
        console.timeEnd("exportDatabase");
        callback();
      };

      tx.onerror = function(e) {
        that.operationInProgress = false;
        console.error("Error while saving data to database", e);
        callback(e);
      };

      tx.onabort = function(e) {
        that.operationInProgress = false;
        console.error("Abort while saving data to database", e);
        callback(e);
      };

      var store = tx.objectStore('LokiIncrementalData');

      console.time("put");
      // console.log(chunks)
      chunks.forEach(function(object) {
        store.put(object);
      });
      console.timeEnd("put");
    };

    IncrementalIndexedDBAdapter.prototype._getAllChunks = function(dbname, callback) {
      var that = this;
      if (!this.idb) {
        this._initializeIDB(dbname, callback, function() {
          that._getAllChunks(dbname, callback);
        });
        return;
      }
      console.log("getting all chunks");
      console.time("getChunks");

      if (this.operationInProgress) {
        throw new Error("Error while loading database - another operation is already in progress. Please use throttledSaves=true option on Loki object");
      }

      this.operationInProgress = true;

      var tx = this.idb.transaction(['LokiIncrementalData'], "readonly");

      var request = tx.objectStore('LokiIncrementalData').getAll();
      request.onsuccess = function(e) {
        that.operationInProgress = false;
        var chunks = e.target.result;
        console.timeEnd("getChunks");
        callback(chunks);
      };

      request.onerror = function(e) {
        that.operationInProgress = false;
        console.error("Error while fetching data from IndexedDB", e);
        callback(e);
      };
    };

    /**
     * Deletes a database from IndexedDB
     *
     * @example
     * // DELETE DATABASE
     * // delete 'finance'/'test' value from catalog
     * idbAdapter.deleteDatabase('test', function {
     *   // database deleted
     * });
     *
     * @param {string} dbname - the name of the database to delete from IDB
     * @param {function=} callback - (Optional) executed on database delete
     * @memberof IncrementalIndexedDBAdapter
     */
    IncrementalIndexedDBAdapter.prototype.deleteDatabase = function(dbname, callback) {
      var that = this;
      console.log("deleteDatabase");
      console.time("deleteDatabase");

      if (this.operationInProgress) {
        throw new Error("Error while deleting database - another operation is already in progress. Please use throttledSaves=true option on Loki object");
      }

      this.operationInProgress = true;

      if (this.idb) {
        this.idb.close();
        this.idb = null;
      }

      var request = indexedDB.deleteDatabase(dbname);

      request.onsuccess = function() {
        that.operationInProgress = false;
        console.timeEnd("deleteDatabase");
        console.log("deleteDatabase done");
        callback({ success: true });
      };

      request.onerror = function(e) {
        that.operationInProgress = false;
        console.error("Error while deleting database", e);
        callback({ success: false });
      };

      console.log("deleteDatabase - exit fn");
    };

    return IncrementalIndexedDBAdapter;
  })();
});
<commit_msg>[IncrementalIDB] diagnostics…<commit_after>(function(root, factory) {
  if (typeof define === "function" && define.amd) {
    // AMD
    define([], factory);
  } else if (typeof exports === "object") {
    // CommonJS
    module.exports = factory();
  } else {
    // Browser globals
    root.IncrementalIndexedDBAdapter = factory();
  }
})(this, function() {
  return (function() {
    "use strict";

    /**
     * An improved Loki persistence adapter for IndexedDB (not compatible with LokiIndexedAdapter)
     *     Unlike LokiIndexedAdapter, the database is saved not as one big JSON blob, but split into
     *     small chunks with individual collection documents. When saving, only the chunks with changed
     *     documents (and database metadata) is saved to IndexedDB. This speeds up small incremental
     *     saves by an order of magnitude on large (tens of thousands of records) databases. It also
     *     avoids Safari 13 bug that would cause the database to balloon in size to gigabytes
     *
     *     The `appname` argument is not provided - to distinguish between multiple app on the same
     *     domain, simply use a different Loki database name
     *
     * @example
     * var adapter = new IncrementalIndexedDBAdapter();
     *
     * @constructor IncrementalIndexedDBAdapter
     */
    function IncrementalIndexedDBAdapter() {
      this.mode = "incremental";
      this.chunkSize = 100;
      this.idb = null; // will be lazily loaded on first operation that needs it
    }

    // chunkId - index of the data chunk - e.g. chunk 0 will be lokiIds 0-99
    IncrementalIndexedDBAdapter.prototype._getChunk = function(collection, chunkId) {
      // 0-99, 100-199, etc.
      var minId = chunkId * this.chunkSize;
      var maxId = minId + this.chunkSize - 1;

      // use idIndex to find first collection.data position within the $loki range
      var idIndex = collection.idIndex;

      var firstDataPosition = null;

      var max = idIndex.length - 1,
        min = 0,
        mid;

      while (idIndex[min] < idIndex[max]) {
        mid = (min + max) >> 1;

        if (idIndex[mid] < minId) {
          min = mid + 1;
        } else {
          max = mid;
        }
      }

      if (max === min && idIndex[min] >= minId) {
        firstDataPosition = min;
      }

      if (firstDataPosition === null) {
        // no elements in this chunk
        return [];
      }

      // find last position
      // if loki IDs are contiguous (no removed elements), last position will be first + chunk - 1
      // (and we look back in case there are missing pieces)
      // TODO: Binary search (not as important as first position, worst case scanario is only chunkSize steps)
      var lastDataPosition = null;
      for (var i = firstDataPosition + this.chunkSize - 1; i >= firstDataPosition; i--) {
        if (idIndex[i] <= maxId) {
          lastDataPosition = i;
          break;
        }
      }

      // TODO: remove sanity checks when everything is fully tested
      var firstElement = collection.data[firstDataPosition];
      if (!(firstElement && firstElement.$loki >= minId && firstElement.$loki <= maxId)) {
        throw new Error("broken invariant firstelement");
      }

      var lastElement = collection.data[lastDataPosition];
      if (!(lastElement && lastElement.$loki >= minId && lastElement.$loki <= maxId)) {
        throw new Error("broken invariant lastElement");
      }

      // this will have *up to* 'this.chunkSize' elements (might have less, because $loki ids
      // will have holes when data is deleted)
      var chunkData = collection.data.slice(firstDataPosition, lastDataPosition + 1);

      // TODO: remove sanity checks when everything is fully tested
      if (chunkData.length > this.chunkSize) {
        throw new Error("broken invariant - chunk size");
      }

      return chunkData;
    };

    /**
     * Incrementally saves the database to IndexedDB
     *
     * @example
     * var idbAdapter = new IncrementalIndexedDBAdapter();
     * var db = new loki('test', { adapter: idbAdapter });
     * var coll = db.addCollection('testColl');
     * coll.insert({test: 'val'});
     * db.saveDatabase();
     *
     * @param {string} dbname - the name to give the serialized database
     * @param {object} dbcopy - copy of the Loki database
     * @param {function} callback - (Optional) callback passed obj.success with true or false
     * @memberof IncrementalIndexedDBAdapter
     */
    IncrementalIndexedDBAdapter.prototype.saveDatabase = function(dbname, loki, callback) {
      var that = this;
      console.log("-- exportDatabase - begin");
      console.time("exportDatabase");

      var chunksToSave = [];

      console.time("makeChunks");
      loki.collections.forEach(function(collection, i) {
        // Find dirty chunk ids
        var dirtyChunks = new Set();
        collection.dirtyIds.forEach(function(lokiId) {
          var chunkId = (lokiId / that.chunkSize) | 0;
          dirtyChunks.add(chunkId);
        });
        collection.dirtyIds = [];

        // Serialize chunks to save
        dirtyChunks.forEach(function(chunkId) {
          var chunkData = that._getChunk(collection, chunkId);
          // we must stringify now, because IDB is asynchronous, and underlying objects are mutable
          chunksToSave.push({
            key: collection.name + ".chunk." + chunkId,
            value: JSON.stringify(chunkData),
          });
        });

        collection.data = [];
        // this is recreated on load anyway, so we can make metadata smaller
        collection.isIndex = [];

        // save collection metadata as separate chunk, leave only names in loki
        // TODO: To reduce IO, we should only save this chunk when it has changed
        chunksToSave.push({
          key: collection.name + ".metadata",
          value: JSON.stringify(collection),
        });
        loki.collections[i] = { name: collection.name };
      });
      console.timeEnd("makeChunks");

      var serializedMetadata = JSON.stringify(loki);
      loki = null; // allow GC of the DB copy

      // console.log(chunksToSave)
      // console.log(chunkIdsToRemove)
      // console.log(JSON.parse(serializedMetadata))

      chunksToSave.push({ key: "loki", value: serializedMetadata });

      that._saveChunks(dbname, chunksToSave, callback);
    };

    /**
     * Retrieves a serialized db string from the catalog.
     *
     * @example
     * // LOAD
     * var idbAdapter = new IncrementalIndexedDBAdapter();
     * var db = new loki('test', { adapter: idbAdapter });
     * db.loadDatabase(function(result) {
     *   console.log('done');
     * });
     *
     * @param {string} dbname - the name of the database to retrieve.
     * @param {function} callback - callback should accept string param containing serialized db string.
     * @memberof IncrementalIndexedDBAdapter
     */
    IncrementalIndexedDBAdapter.prototype.loadDatabase = function(dbname, callback) {
      var that = this;
      console.log("-- loadDatabase - begin");
      console.time("loadDatabase");
      this._getAllChunks(dbname, function(chunks) {
        if (!Array.isArray(chunks)) {
          // we got an error
          console.timeEnd("loadDatabase");
          callback(chunks);
        }

        if (!chunks.length) {
          console.timeEnd("loadDatabase");
          callback(null);
          return;
        }

        console.log("Found chunks:", chunks.length);

        that._sortChunksInPlace(chunks);

        // repack chunks into a map
        var loki;
        var chunkCollections = {};

        // console.time('repack')
        chunks.forEach(function(object) {
          var key = object.key;
          var value = object.value;
          if (key === "loki") {
            loki = value;
            return;
          } else if (key.includes(".")) {
            var keySegments = key.split(".");
            if (keySegments.length === 3 && keySegments[1] === "chunk") {
              var colName = keySegments[0];
              if (chunkCollections[colName]) {
                chunkCollections[colName].dataChunks.push(value);
              } else {
                chunkCollections[colName] = {
                  metadata: null,
                  dataChunks: [value],
                };
              }
              return;
            } else if (keySegments.length === 2 && keySegments[1] === "metadata") {
              var name = keySegments[0];
              if (chunkCollections[name]) {
                chunkCollections[name].metadata = value;
              } else {
                chunkCollections[name] = { metadata: value, dataChunks: [] };
              }
              return;
            }
          }

          console.error("Unknown chunk " + key);
          callback(new Error("Invalid database - unknown chunk found"));
        });
        chunks = null;
        // console.timeEnd('repack')
        // console.log('chunkCollections', chunkCollections)

        if (!loki) {
          callback(new Error("Invalid database - missing database metadata"));
        }

        // parse Loki object
        loki = JSON.parse(loki);

        // populate collections with data
        console.time("populate");
        that._populate(loki, chunkCollections);
        chunkCollections = null;
        console.timeEnd("populate");

        console.timeEnd("loadDatabase");
        callback(loki);
      });
    };

    IncrementalIndexedDBAdapter.prototype._sortChunksInPlace = function(chunks) {
      // sort chunks in place to load data in the right order (ascending loki ids)
      // on both Safari and Chrome, we'll get chunks in order like this: 0, 1, 10, 100...
      // console.time('sort')
      var getSortKey = function(object) {
        var key = object.key;
        if (key.includes(".")) {
          var segments = key.split(".");
          if (segments.length === 3 && segments[1] === "chunk") {
            return parseInt(segments[2], 10);
          }
        }

        return -1; // consistent type must be returned
      };
      chunks.sort(function(a, b) {
        var aKey = getSortKey(a),
          bKey = getSortKey(b);
        if (aKey < bKey) return -1;
        if (aKey > bKey) return 1;
        return 0;
      });
      // console.timeEnd('sort')
      // console.log('Sorted chunks', chunks)
    };

    IncrementalIndexedDBAdapter.prototype._populate = function(loki, chunkCollections) {
      loki.collections.forEach(function(collectionStub, i) {
        var chunkCollection = chunkCollections[collectionStub.name];

        if (chunkCollection) {
          // TODO: What if metadata is missing?
          var collection = JSON.parse(chunkCollection.metadata);
          chunkCollection.metadata = null;

          loki.collections[i] = collection;

          var dataChunks = chunkCollection.dataChunks;
          dataChunks.forEach(function(chunkObj, i) {
            var chunk = JSON.parse(chunkObj);
            chunkObj = null; // make string available for GC
            dataChunks[i] = null;

            chunk.forEach(function(doc) {
              collection.data.push(doc);
            });
          });
        }
      });
    };

    IncrementalIndexedDBAdapter.prototype._initializeIDB = function(dbname, onError, onSuccess) {
      var that = this;
      console.log("initializing idb");

      if (this.idbInitInProgress) {
        throw new Error("Cannot open IndexedDB because open is already in progress");
      }
      this.idbInitInProgress = true;

      var openRequest = indexedDB.open(dbname, 1);

      openRequest.onupgradeneeded = function(e) {
        var db = e.target.result;
        console.log('onupgradeneeded, old version: ' + e.oldVersion);

        if (e.oldVersion < 1) {
          // Version 1 - Initial - Create database
          db.createObjectStore('LokiIncrementalData', { keyPath: "key" });
        } else {
          // Unknown version
          throw new Error("Invalid old version " + e.oldVersion + " for IndexedDB upgrade");
        }
      };

      openRequest.onsuccess = function(e) {
        that.idbInitInProgress = false;
        that.idb = e.target.result;

        if (!that.idb.objectStoreNames.contains('LokiIncrementalData')) {
          onError(new Error("Missing LokiIncrementalData"));
          return;
        }

        console.log("init success");
        onSuccess();
      };

      openRequest.onblocked = function(e) {
        console.error("IndexedDB open is blocked", e);
        onError(new Error("IndexedDB open is blocked by open connection"));
      };

      openRequest.onerror = function(e) {
        that.idbInitInProgress = false;
        console.error("IndexeddB open error", e);
        onError(e);
      };
    };

    IncrementalIndexedDBAdapter.prototype._saveChunks = function(dbname, chunks, callback) {
      var that = this;
      if (!this.idb) {
        this._initializeIDB(dbname, callback, function() {
          that._saveChunks(dbname, chunks, callback);
        });
        return;
      }

      if (this.operationInProgress) {
        throw new Error("Error while saving to database - another operation is already in progress. Please use throttledSaves=true option on Loki object");
      }

      this.operationInProgress = true;

      console.time("save chunks to idb");

      var tx = this.idb.transaction(['LokiIncrementalData'], "readwrite");
      tx.oncomplete = function() {
        that.operationInProgress = false;
        console.timeEnd("save chunks to idb");
        console.timeEnd("exportDatabase");
        callback();
      };

      tx.onerror = function(e) {
        that.operationInProgress = false;
        console.error("Error while saving data to database", e);
        callback(e);
      };

      tx.onabort = function(e) {
        that.operationInProgress = false;
        console.error("Abort while saving data to database", e);
        callback(e);
      };

      var store = tx.objectStore('LokiIncrementalData');

      console.time("put");
      // console.log(chunks)
      chunks.forEach(function(object) {
        store.put(object);
      });
      console.timeEnd("put");
    };

    IncrementalIndexedDBAdapter.prototype._getAllChunks = function(dbname, callback) {
      var that = this;
      if (!this.idb) {
        this._initializeIDB(dbname, callback, function() {
          that._getAllChunks(dbname, callback);
        });
        return;
      }

      if (this.operationInProgress) {
        throw new Error("Error while loading database - another operation is already in progress. Please use throttledSaves=true option on Loki object");
      }

      this.operationInProgress = true;

      console.log("getting all chunks");
      console.time("getChunks");

      var tx = this.idb.transaction(['LokiIncrementalData'], "readonly");

      var request = tx.objectStore('LokiIncrementalData').getAll();
      request.onsuccess = function(e) {
        that.operationInProgress = false;
        var chunks = e.target.result;
        console.timeEnd("getChunks");
        callback(chunks);
      };

      request.onerror = function(e) {
        that.operationInProgress = false;
        console.error("Error while fetching data from IndexedDB", e);
        callback(e);
      };
    };

    /**
     * Deletes a database from IndexedDB
     *
     * @example
     * // DELETE DATABASE
     * // delete 'finance'/'test' value from catalog
     * idbAdapter.deleteDatabase('test', function {
     *   // database deleted
     * });
     *
     * @param {string} dbname - the name of the database to delete from IDB
     * @param {function=} callback - (Optional) executed on database delete
     * @memberof IncrementalIndexedDBAdapter
     */
    IncrementalIndexedDBAdapter.prototype.deleteDatabase = function(dbname, callback) {
      if (this.operationInProgress) {
        throw new Error("Error while deleting database - another operation is already in progress. Please use throttledSaves=true option on Loki object");
      }

      this.operationInProgress = true;

      var that = this;
      console.log("deleteDatabase");
      console.time("deleteDatabase");

      if (this.idb) {
        this.idb.close();
        this.idb = null;
      }

      var request = indexedDB.deleteDatabase(dbname);

      request.onsuccess = function() {
        that.operationInProgress = false;
        console.timeEnd("deleteDatabase");
        console.log("deleteDatabase done");
        callback({ success: true });
      };

      request.onerror = function(e) {
        that.operationInProgress = false;
        console.error("Error while deleting database", e);
        callback({ success: false });
      };

      console.log("deleteDatabase - exit fn");
    };

    return IncrementalIndexedDBAdapter;
  })();
});
<|endoftext|>


<commit_before>// @flow

import path from 'path';
import React from 'react';
import chalk from 'chalk';
import { renderToString } from 'react-dom/server';
import webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import ExtractCssPlugin from 'mini-css-extract-plugin';
import WebpackCleanupPlugin from 'webpack-cleanup-plugin';
import nodeExternals from 'webpack-node-externals';
import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import frameworkConfig from '../../shared/framework-config';
import projectMetadata from '../../shared/project-metadata';
import frameworkMetadata from '../../shared/framework-metadata';
import { resolveFrameworkSource } from '../util/resolve-util';
import argv from '../rjs-argv';
import appArgv from '../../framework/common/app-argv/node';
import logger from '../../shared/logger';
import getWebpackSettings from '../../shared/webpack-settings';
import BaseHelmet from '../../framework/app/BaseHelmet';
import renderPage from '../../framework/server/setup-http-server/render-page';
import RjsDumpStatsPlugin from './DumpEntryPointsPlugin';
import RequireEnsureHookPlugin from './RequireEnsureHookPlugin';
import BaseFeature from './BaseFeature';
import WebpackConfigBuilder, * as wcbUtils from './WebpackConfigBuilder';
import sortDependencies from './sort-dependencies';
import featureClasses from './features';

const isDev = process.env.NODE_ENV === 'development';
const isTest = process.env.NODE_ENV === 'test';

function parseFeatures(str) {
  if (!str) {
    return {};
  }

  const features = {};

  str.split(',').forEach(featureName => {
    if (featureName.charAt(0) === '-') {
      features[featureName.substr(1)] = false;
    } else {
      features[featureName] = true;
    }
  });

  return features;
}

export default class WebpackBase {

  static SIDE_SERVER = 0;
  static SIDE_CLIENT = 1;

  /** @private */
  isDev: boolean;

  /** @private */
  isTest: boolean;

  /** @private */
  side: number;

  constructor(side: number) {
    this.isDev = isDev;
    this.isTest = isTest;
    this.side = side;

    this.webpackConfigBuilder = new WebpackConfigBuilder();

    this.injectFeatures();
  }

  /** @private */
  isServer() {
    return this.side === WebpackBase.SIDE_SERVER;
  }

  injectFeatures() {
    logger.debug('Injecting webpack Features.');
    const requestedFeatures = parseFeatures(argv.features);

    const features = featureClasses.map(FeatureClass => new FeatureClass(this.isServer(), process.env.NODE_ENV));
    sortDependencies(features);

    for (const feature of features) {
      this.injectFeature(feature, requestedFeatures);
    }
  }

  /** @private */
  injectFeature(feature: BaseFeature, requestedFeatures: { [string]: boolean }) {

    const name = feature.getFeatureName();
    const enabled = requestedFeatures[name] != null ? requestedFeatures[name] : feature.isDefaultEnabled();

    logger.debug(`${enabled ? chalk.green('✓') : chalk.red('✘')} Feature ${name}`);

    if (!enabled) {
      return false;
    }

    feature.visit(this.webpackConfigBuilder);

    return true;
  }

  buildConfig() {
    const config: Object = {
      cache: true,
      name: this.isServer() ? 'Server' : 'Client',
      entry: this.getEntry(),
      output: this.getOutput(),
      module: {
        rules: this.buildRules(),
      },
      plugins: this.getPlugins(),

      // Allow error boundaries to display errors in non-main chunks
      // https://reactjs.org/docs/cross-origin-errors.html
      devtool: 'cheap-module-source-map',
      performance: {
        hints: false,
      },
      target: this.isServer() ? 'node' : 'web',
      node: {
        // don't transform __dirname when running inside Node.
        __dirname: !this.isServer(),
      },
      resolve: {
        modules: ['node_modules'],
        extensions: [
          '.mjs',
          '.js',
          '.jsx',
          '.react.js',
        ],
        mainFields: [
          'webpack:main',
          'jsnext:module',
          'module',
          'jsnext:main',
          'main',
        ],
        alias: this.getAliases(),
      },
      mode: this.isDev ? 'development' : 'production',
      optimization: {
        noEmitOnErrors: true,
        minimize: false,
        removeAvailableModules: false,
      },
    };

    if (this.isServer()) {
      // exclude any absolute module (npm/node) from the build, except this module.
      // this module must be included otherwise the server build will have two
      // separate versions of the framework loaded.
      // const anyAbsoluteExceptFrameworkAndCss = new RegExp(`^(?!${}(\\/.+)?$)[a-z\\-_0-9]+(?!.+\\.css$).+$`);

      config.externals = [
        nodeExternals({
          whitelist: [
            new RegExp(`^${frameworkMetadata.name}`),
            /\.css$/i,
          ],
        }),
      ];
    } else {

      config.resolve.mainFields.unshift('web');
      config.resolve.mainFields.unshift('jsnext:web');
      config.resolve.mainFields.unshift('browser');
      config.resolve.mainFields.unshift('jsnext:browser');
    }

    return wcbUtils.mergeRaw(this.webpackConfigBuilder, config);
  }

  /** @private */
  getEntry(): string[] {
    // front-end entry point.
    const entry = [
      this.isServer()
        ? resolveFrameworkSource('server/index')
        : resolveFrameworkSource('client/index'),
    ];

    // front-end dev libs.
    if (this.isDev && !this.isServer()) {
      entry.unshift(
        // Necessary for hot reloading with IE
        'eventsource-polyfill',
        'webpack-hot-middleware/client',
        require.resolve('./dev-preamble'),
      );
    } else if (this.isDev && this.isServer()) {
      entry.unshift(
        // hot reload if parent sends signal SIGUSR2
        require.resolve('./hmr-server'),
      );
    }

    return entry;
  }

  /** @private */
  getOutput(): Object {
    // Output to build directory.
    const output = getWebpackSettings(this.isServer()).output;

    if (this.isServer()) {
      output.libraryTarget = 'commonjs2';
    }

    if (this.isDev && !this.isServer()) {
      // Allow error boundaries to display errors in non-main chunks
      // https://reactjs.org/docs/cross-origin-errors.html
      output.crossOriginLoading = 'anonymous';
    }

    if (this.isDev || this.isServer()) {
      // Don't use hashes in dev mode for better building performance.
      // Don't use them on the server because they're not needed and not user friendly.
      Object.assign(output, {
        filename: '[name].js',
        chunkFilename: '[name].chunk.js',

        // https://github.com/mxstbr/react-boilerplate/issues/443
        // publicPath: 'http://localhost:3000',
      });
    } else {
      // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets.
      Object.assign(output, {
        filename: '[name].[chunkhash].js',
        chunkFilename: '[name].[chunkhash].chunk.js',
      });
    }

    return output;
  }

  /** @private */
  buildRules() {

    // TODO consider using url-loader rather than file-loader maybe ?
    // Need to test perf gain.

    return [{
      test: /\.(eot|ttf|woff|woff2)(\?.*$|$)/i,
      loader: 'file-loader',
    }, {
      test: wcbUtils.getFileTypeRegExp(this.webpackConfigBuilder, WebpackConfigBuilder.FILE_TYPE_IMG),
      use: [
        {
          loader: 'srcset-loader',
          options: {
            lightweight: true,
          },
        },
        'file-loader',
      ],
    }, {
      test: /\.(mp4|webm)/i,
      loader: 'file-loader',
    }, {
      // FIXME temporary fix for webpack 4 https://github.com/webpack-contrib/bundle-loader/issues/74
      test: /\.json$/i,
      type: 'javascript/auto',
      loader: 'json-loader',
    }]
      .concat(this.buildCssLoaders())
      .concat(wcbUtils.buildRules(this.webpackConfigBuilder));
  }

  getCssLoader(options: Object = {}) {
    const loaderOptions: Object = {
      importLoaders: options.importLoaders || 0,
    };

    if (isDev) {
      Object.assign(loaderOptions, {
        sourceMap: true,
      });
    }

    if (options.modules) {
      Object.assign(loaderOptions, {
        modules: true,
        camelCase: true,
      });

      if (isDev) {
        Object.assign(loaderOptions, {
          localIdentName: '[local]__[hash:base64:5]',
        });
      }

      // in prod with pre-rendering, we don't generate the CSS. Only the mapping "css class" => "css module class"
      // the actual CSS is served directly from the client bundle.
      if (this.isServer() && !this.isDev) {
        Object.assign(loaderOptions, {
          exportOnlyLocals: true,
        });
      }
    }

    return {
      loader: 'css-loader',
      options: loaderOptions,
    };
  }

  /** @private */
  buildCssLoaders() {
    const pluggedCssLoaders = wcbUtils.getCssLoaders(this.webpackConfigBuilder);

    const cssLoaders = [{
      test: wcbUtils.getFileTypeRegExp(this.webpackConfigBuilder, WebpackConfigBuilder.FILE_TYPE_CSS),
      exclude: /node_modules/,
      use: [
        this.getCssLoader({
          modules: true,
          importLoaders: pluggedCssLoaders.length,
        }),
        ...pluggedCssLoaders,
      ],
    }, {
      test: /\.css$/i,
      include: /node_modules/,
      use: [this.getCssLoader({ modules: false })],
    }];

    const styleLoader = (() => {
      // in prod, extract CSS to separate .css files.
      // Note: The server ignores the file because it simply sends the ones built for the Client
      //       that match used bundle names.
      if (this.isDev) {
        // in dev, use <style> tags
        return this.isServer() ? 'node-style-loader' : 'style-loader';
      }

      // in prod, extract CSS to separate .css files
      // on the server, don't generate the CSS -- load client version
      return this.isServer() ? null : ExtractCssPlugin.loader;
    })();

    if (styleLoader != null) {
      for (const cssLoader of cssLoaders) {
        cssLoader.use = [styleLoader, ...cssLoader.use];
      }
    }

    return cssLoaders;
  }

  /** @private */
  getAliases() {
    const frameworkAliases = {

      // Framework configuration directories
      '@@pre-init': frameworkConfig['pre-init'] || resolveFrameworkSource('dummy/empty-function.js'),
      '@@render-html': frameworkConfig['render-html'] || resolveFrameworkSource('server/setup-http-server/default-render-page.js'),
      '@@directories.translations': frameworkConfig.directories.translations,

      // Support React Native Web
      // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
      'react-native': 'react-native-web',
    };

    const customAliases = wcbUtils.getAliases(this.webpackConfigBuilder);
    Object.assign(frameworkAliases, customAliases);

    return frameworkAliases;
  }

  /** @private */
  getDefinedVars() {
    const NODE_ENV = JSON.stringify(process.env.NODE_ENV);
    const SIDE = JSON.stringify(this.isServer() ? 'server' : 'client');

    const definedVariables: Object = {
      process: {
        env: {
          SIDE,
          NODE_ENV,
          PROCESS_NAME: JSON.stringify(`${projectMetadata.name} (${this.isServer() ? 'server' : 'client'})`),
        },
      },
      $$RJS_VARS$$: {
        FRAMEWORK_METADATA: JSON.stringify(frameworkMetadata),
        PROJECT_METADATA: JSON.stringify(projectMetadata),
        PARSED_ARGV: JSON.stringify(appArgv),
      },
    };

    if (this.isServer()) {
      const outputDirectory = getWebpackSettings(this.isServer()).output.path;

      // we only pass build & logs to bundled code
      // as they are the only folders that NEED to exist for the
      // app to run.
      // Using anything else MUST be an error.

      // We don't give any information to the client bundle as they cannot interact
      // with the filesystem at all and trying to MUST be an error too.

      // We give the path relative to the output directory so the project can be
      // moved around freely after being built. This can happen when updating a live app: The app will be built
      // in a temporary folder then moved to its final destination.

      // The path relative path will be transformed into an absolute path at runtime.

      // all executed JS files will be located at the root of outputDirectory, so we the new absolute path
      // will be the correct one no matter which bundle produces it.
      const FRAMEWORK_CONFIG = {
        directories: {
          logs: path.relative(outputDirectory, frameworkConfig.directories.logs),
          build: path.relative(outputDirectory, frameworkConfig.directories.build),
        },
      };

      definedVariables.$$RJS_VARS$$.FRAMEWORK_CONFIG = JSON.stringify(FRAMEWORK_CONFIG);
    }

    return flattenKeys(definedVariables);
  }

  /** @private */
  getPlugins() {
    // TODO inject DLLs <script data-dll='true' src='/${dllName}.dll.js'></script>`
    // TODO https://github.com/diurnalist/chunk-manifest-webpack-plugin
    const plugins = [
      // remove outdated assets from previous builds.
      new WebpackCleanupPlugin({ quiet: true }),
      new webpack.DefinePlugin(this.getDefinedVars()),
      new CopyWebpackPlugin([{
        from: { glob: `${frameworkConfig.directories.resources}/**/**/*` },
        to: './',
        toType: 'dir',
      }]),

      // Inject webpack bundle into HTML.
      new HtmlWebpackPlugin({
        inject: true,
        templateContent: buildIndexPage(),

        // FIXME temporary hack for webpack 4 https://github.com/jantimon/html-webpack-plugin/issues/870
        chunksSortMode: 'none',
        minify: this.isDev ? false : {
          removeComments: true,
          collapseWhitespace: true,
          removeRedundantAttributes: true,
          useShortDoctype: true,
          removeEmptyAttributes: true,
          removeStyleLinkTypeAttributes: true,
          keepClosingSlash: true,
          minifyJS: true,
          minifyCSS: true,
          minifyURLs: true,
        },
      }),

      // Dump chunk mapping and entryPoints so we can determine which client chunks to send depending on which
      // chunks were imported during server-side rendering
      new RjsDumpStatsPlugin(),
    ];

    if (this.isServer()) {
      plugins.push(
        // Hook import() directives on the server-side so we can know which
        // chunks are loaded for which routes and give them along with the HTTP response.
        new RequireEnsureHookPlugin(),
      );
    }

    // if (!this.isServer()) {
    //   plugins.push(
    //     new PolyfillInjectorPlugin({
    //       polyfills: ['Promise'],
    //     }),
    //   );
    // }

    if (this.isDev) {
      plugins.push(
        // enable hot reloading.
        new webpack.HotModuleReplacementPlugin(),

        // Watcher doesn't work well if you mistype casing in a path so we use
        // a plugin that prints an error when you attempt to do this.
        // See https://github.com/facebookincubator/create-react-app/issues/240
        new CaseSensitivePathsPlugin(),
      );
    }

    if (!this.isDev && !this.isServer()) {
      // Extract the CSS into a seperate file (only prod, and plugin is not compatible with SSR).
      plugins.push(
        new ExtractCssPlugin({
          filename: '[name].[contenthash].css',
        }),
      );
    }

    return plugins.concat(...wcbUtils.getPlugins(this.webpackConfigBuilder));
  }
}

function buildIndexPage() {
  return renderPage({
    body: renderToString(<BaseHelmet />),
  });
}

function flattenKeys(obj, out = {}, paths = []) {

  for (const key of Object.getOwnPropertyNames(obj)) {
    paths.push(key);

    if (typeof obj[key] === 'object') {
      flattenKeys(obj[key], out, paths);
    } else {
      out[paths.join('.')] = obj[key];
    }

    paths.pop();
  }

  return out;
}
<commit_msg>fix: remove .react.js from special file extension (.js is enough)<commit_after>// @flow

import path from 'path';
import React from 'react';
import chalk from 'chalk';
import { renderToString } from 'react-dom/server';
import webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import ExtractCssPlugin from 'mini-css-extract-plugin';
import WebpackCleanupPlugin from 'webpack-cleanup-plugin';
import nodeExternals from 'webpack-node-externals';
import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import frameworkConfig from '../../shared/framework-config';
import projectMetadata from '../../shared/project-metadata';
import frameworkMetadata from '../../shared/framework-metadata';
import { resolveFrameworkSource } from '../util/resolve-util';
import argv from '../rjs-argv';
import appArgv from '../../framework/common/app-argv/node';
import logger from '../../shared/logger';
import getWebpackSettings from '../../shared/webpack-settings';
import BaseHelmet from '../../framework/app/BaseHelmet';
import renderPage from '../../framework/server/setup-http-server/render-page';
import RjsDumpStatsPlugin from './DumpEntryPointsPlugin';
import RequireEnsureHookPlugin from './RequireEnsureHookPlugin';
import BaseFeature from './BaseFeature';
import WebpackConfigBuilder, * as wcbUtils from './WebpackConfigBuilder';
import sortDependencies from './sort-dependencies';
import featureClasses from './features';

const isDev = process.env.NODE_ENV === 'development';
const isTest = process.env.NODE_ENV === 'test';

function parseFeatures(str) {
  if (!str) {
    return {};
  }

  const features = {};

  str.split(',').forEach(featureName => {
    if (featureName.charAt(0) === '-') {
      features[featureName.substr(1)] = false;
    } else {
      features[featureName] = true;
    }
  });

  return features;
}

export default class WebpackBase {

  static SIDE_SERVER = 0;
  static SIDE_CLIENT = 1;

  /** @private */
  isDev: boolean;

  /** @private */
  isTest: boolean;

  /** @private */
  side: number;

  constructor(side: number) {
    this.isDev = isDev;
    this.isTest = isTest;
    this.side = side;

    this.webpackConfigBuilder = new WebpackConfigBuilder();

    this.injectFeatures();
  }

  /** @private */
  isServer() {
    return this.side === WebpackBase.SIDE_SERVER;
  }

  injectFeatures() {
    logger.debug('Injecting webpack Features.');
    const requestedFeatures = parseFeatures(argv.features);

    const features = featureClasses.map(FeatureClass => new FeatureClass(this.isServer(), process.env.NODE_ENV));
    sortDependencies(features);

    for (const feature of features) {
      this.injectFeature(feature, requestedFeatures);
    }
  }

  /** @private */
  injectFeature(feature: BaseFeature, requestedFeatures: { [string]: boolean }) {

    const name = feature.getFeatureName();
    const enabled = requestedFeatures[name] != null ? requestedFeatures[name] : feature.isDefaultEnabled();

    logger.debug(`${enabled ? chalk.green('✓') : chalk.red('✘')} Feature ${name}`);

    if (!enabled) {
      return false;
    }

    feature.visit(this.webpackConfigBuilder);

    return true;
  }

  buildConfig() {
    const config: Object = {
      cache: true,
      name: this.isServer() ? 'Server' : 'Client',
      entry: this.getEntry(),
      output: this.getOutput(),
      module: {
        rules: this.buildRules(),
      },
      plugins: this.getPlugins(),

      // Allow error boundaries to display errors in non-main chunks
      // https://reactjs.org/docs/cross-origin-errors.html
      devtool: 'cheap-module-source-map',
      performance: {
        hints: false,
      },
      target: this.isServer() ? 'node' : 'web',
      node: {
        // don't transform __dirname when running inside Node.
        __dirname: !this.isServer(),
      },
      resolve: {
        modules: ['node_modules'],
        extensions: [
          '.mjs',
          '.js',
          '.jsx',
        ],
        mainFields: [
          'webpack:main',
          'jsnext:module',
          'module',
          'jsnext:main',
          'main',
        ],
        alias: this.getAliases(),
      },
      mode: this.isDev ? 'development' : 'production',
      optimization: {
        noEmitOnErrors: true,
        minimize: false,
        removeAvailableModules: false,
      },
    };

    if (this.isServer()) {
      // exclude any absolute module (npm/node) from the build, except this module.
      // this module must be included otherwise the server build will have two
      // separate versions of the framework loaded.
      // const anyAbsoluteExceptFrameworkAndCss = new RegExp(`^(?!${}(\\/.+)?$)[a-z\\-_0-9]+(?!.+\\.css$).+$`);

      config.externals = [
        nodeExternals({
          whitelist: [
            new RegExp(`^${frameworkMetadata.name}`),
            /\.css$/i,
          ],
        }),
      ];
    } else {

      config.resolve.mainFields.unshift('web');
      config.resolve.mainFields.unshift('jsnext:web');
      config.resolve.mainFields.unshift('browser');
      config.resolve.mainFields.unshift('jsnext:browser');
    }

    return wcbUtils.mergeRaw(this.webpackConfigBuilder, config);
  }

  /** @private */
  getEntry(): string[] {
    // front-end entry point.
    const entry = [
      this.isServer()
        ? resolveFrameworkSource('server/index')
        : resolveFrameworkSource('client/index'),
    ];

    // front-end dev libs.
    if (this.isDev && !this.isServer()) {
      entry.unshift(
        // Necessary for hot reloading with IE
        'eventsource-polyfill',
        'webpack-hot-middleware/client',
        require.resolve('./dev-preamble'),
      );
    } else if (this.isDev && this.isServer()) {
      entry.unshift(
        // hot reload if parent sends signal SIGUSR2
        require.resolve('./hmr-server'),
      );
    }

    return entry;
  }

  /** @private */
  getOutput(): Object {
    // Output to build directory.
    const output = getWebpackSettings(this.isServer()).output;

    if (this.isServer()) {
      output.libraryTarget = 'commonjs2';
    }

    if (this.isDev && !this.isServer()) {
      // Allow error boundaries to display errors in non-main chunks
      // https://reactjs.org/docs/cross-origin-errors.html
      output.crossOriginLoading = 'anonymous';
    }

    if (this.isDev || this.isServer()) {
      // Don't use hashes in dev mode for better building performance.
      // Don't use them on the server because they're not needed and not user friendly.
      Object.assign(output, {
        filename: '[name].js',
        chunkFilename: '[name].chunk.js',

        // https://github.com/mxstbr/react-boilerplate/issues/443
        // publicPath: 'http://localhost:3000',
      });
    } else {
      // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets.
      Object.assign(output, {
        filename: '[name].[chunkhash].js',
        chunkFilename: '[name].[chunkhash].chunk.js',
      });
    }

    return output;
  }

  /** @private */
  buildRules() {

    // TODO consider using url-loader rather than file-loader maybe ?
    // Need to test perf gain.

    return [{
      test: /\.(eot|ttf|woff|woff2)(\?.*$|$)/i,
      loader: 'file-loader',
    }, {
      test: wcbUtils.getFileTypeRegExp(this.webpackConfigBuilder, WebpackConfigBuilder.FILE_TYPE_IMG),
      use: [
        {
          loader: 'srcset-loader',
          options: {
            lightweight: true,
          },
        },
        'file-loader',
      ],
    }, {
      test: /\.(mp4|webm)/i,
      loader: 'file-loader',
    }, {
      // FIXME temporary fix for webpack 4 https://github.com/webpack-contrib/bundle-loader/issues/74
      test: /\.json$/i,
      type: 'javascript/auto',
      loader: 'json-loader',
    }]
      .concat(this.buildCssLoaders())
      .concat(wcbUtils.buildRules(this.webpackConfigBuilder));
  }

  getCssLoader(options: Object = {}) {
    const loaderOptions: Object = {
      importLoaders: options.importLoaders || 0,
    };

    if (isDev) {
      Object.assign(loaderOptions, {
        sourceMap: true,
      });
    }

    if (options.modules) {
      Object.assign(loaderOptions, {
        modules: true,
        camelCase: true,
      });

      if (isDev) {
        Object.assign(loaderOptions, {
          localIdentName: '[local]__[hash:base64:5]',
        });
      }

      // in prod with pre-rendering, we don't generate the CSS. Only the mapping "css class" => "css module class"
      // the actual CSS is served directly from the client bundle.
      if (this.isServer() && !this.isDev) {
        Object.assign(loaderOptions, {
          exportOnlyLocals: true,
        });
      }
    }

    return {
      loader: 'css-loader',
      options: loaderOptions,
    };
  }

  /** @private */
  buildCssLoaders() {
    const pluggedCssLoaders = wcbUtils.getCssLoaders(this.webpackConfigBuilder);

    const cssLoaders = [{
      test: wcbUtils.getFileTypeRegExp(this.webpackConfigBuilder, WebpackConfigBuilder.FILE_TYPE_CSS),
      exclude: /node_modules/,
      use: [
        this.getCssLoader({
          modules: true,
          importLoaders: pluggedCssLoaders.length,
        }),
        ...pluggedCssLoaders,
      ],
    }, {
      test: /\.css$/i,
      include: /node_modules/,
      use: [this.getCssLoader({ modules: false })],
    }];

    const styleLoader = (() => {
      // in prod, extract CSS to separate .css files.
      // Note: The server ignores the file because it simply sends the ones built for the Client
      //       that match used bundle names.
      if (this.isDev) {
        // in dev, use <style> tags
        return this.isServer() ? 'node-style-loader' : 'style-loader';
      }

      // in prod, extract CSS to separate .css files
      // on the server, don't generate the CSS -- load client version
      return this.isServer() ? null : ExtractCssPlugin.loader;
    })();

    if (styleLoader != null) {
      for (const cssLoader of cssLoaders) {
        cssLoader.use = [styleLoader, ...cssLoader.use];
      }
    }

    return cssLoaders;
  }

  /** @private */
  getAliases() {
    const frameworkAliases = {

      // Framework configuration directories
      '@@pre-init': frameworkConfig['pre-init'] || resolveFrameworkSource('dummy/empty-function.js'),
      '@@render-html': frameworkConfig['render-html'] || resolveFrameworkSource('server/setup-http-server/default-render-page.js'),
      '@@directories.translations': frameworkConfig.directories.translations,

      // Support React Native Web
      // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
      'react-native': 'react-native-web',
    };

    const customAliases = wcbUtils.getAliases(this.webpackConfigBuilder);
    Object.assign(frameworkAliases, customAliases);

    return frameworkAliases;
  }

  /** @private */
  getDefinedVars() {
    const NODE_ENV = JSON.stringify(process.env.NODE_ENV);
    const SIDE = JSON.stringify(this.isServer() ? 'server' : 'client');

    const definedVariables: Object = {
      process: {
        env: {
          SIDE,
          NODE_ENV,
          PROCESS_NAME: JSON.stringify(`${projectMetadata.name} (${this.isServer() ? 'server' : 'client'})`),
        },
      },
      $$RJS_VARS$$: {
        FRAMEWORK_METADATA: JSON.stringify(frameworkMetadata),
        PROJECT_METADATA: JSON.stringify(projectMetadata),
        PARSED_ARGV: JSON.stringify(appArgv),
      },
    };

    if (this.isServer()) {
      const outputDirectory = getWebpackSettings(this.isServer()).output.path;

      // we only pass build & logs to bundled code
      // as they are the only folders that NEED to exist for the
      // app to run.
      // Using anything else MUST be an error.

      // We don't give any information to the client bundle as they cannot interact
      // with the filesystem at all and trying to MUST be an error too.

      // We give the path relative to the output directory so the project can be
      // moved around freely after being built. This can happen when updating a live app: The app will be built
      // in a temporary folder then moved to its final destination.

      // The path relative path will be transformed into an absolute path at runtime.

      // all executed JS files will be located at the root of outputDirectory, so we the new absolute path
      // will be the correct one no matter which bundle produces it.
      const FRAMEWORK_CONFIG = {
        directories: {
          logs: path.relative(outputDirectory, frameworkConfig.directories.logs),
          build: path.relative(outputDirectory, frameworkConfig.directories.build),
        },
      };

      definedVariables.$$RJS_VARS$$.FRAMEWORK_CONFIG = JSON.stringify(FRAMEWORK_CONFIG);
    }

    return flattenKeys(definedVariables);
  }

  /** @private */
  getPlugins() {
    // TODO inject DLLs <script data-dll='true' src='/${dllName}.dll.js'></script>`
    // TODO https://github.com/diurnalist/chunk-manifest-webpack-plugin
    const plugins = [
      // remove outdated assets from previous builds.
      new WebpackCleanupPlugin({ quiet: true }),
      new webpack.DefinePlugin(this.getDefinedVars()),
      new CopyWebpackPlugin([{
        from: { glob: `${frameworkConfig.directories.resources}/**/**/*` },
        to: './',
        toType: 'dir',
      }]),

      // Inject webpack bundle into HTML.
      new HtmlWebpackPlugin({
        inject: true,
        templateContent: buildIndexPage(),

        // FIXME temporary hack for webpack 4 https://github.com/jantimon/html-webpack-plugin/issues/870
        chunksSortMode: 'none',
        minify: this.isDev ? false : {
          removeComments: true,
          collapseWhitespace: true,
          removeRedundantAttributes: true,
          useShortDoctype: true,
          removeEmptyAttributes: true,
          removeStyleLinkTypeAttributes: true,
          keepClosingSlash: true,
          minifyJS: true,
          minifyCSS: true,
          minifyURLs: true,
        },
      }),

      // Dump chunk mapping and entryPoints so we can determine which client chunks to send depending on which
      // chunks were imported during server-side rendering
      new RjsDumpStatsPlugin(),
    ];

    if (this.isServer()) {
      plugins.push(
        // Hook import() directives on the server-side so we can know which
        // chunks are loaded for which routes and give them along with the HTTP response.
        new RequireEnsureHookPlugin(),
      );
    }

    // if (!this.isServer()) {
    //   plugins.push(
    //     new PolyfillInjectorPlugin({
    //       polyfills: ['Promise'],
    //     }),
    //   );
    // }

    if (this.isDev) {
      plugins.push(
        // enable hot reloading.
        new webpack.HotModuleReplacementPlugin(),

        // Watcher doesn't work well if you mistype casing in a path so we use
        // a plugin that prints an error when you attempt to do this.
        // See https://github.com/facebookincubator/create-react-app/issues/240
        new CaseSensitivePathsPlugin(),
      );
    }

    if (!this.isDev && !this.isServer()) {
      // Extract the CSS into a seperate file (only prod, and plugin is not compatible with SSR).
      plugins.push(
        new ExtractCssPlugin({
          filename: '[name].[contenthash].css',
        }),
      );
    }

    return plugins.concat(...wcbUtils.getPlugins(this.webpackConfigBuilder));
  }
}

function buildIndexPage() {
  return renderPage({
    body: renderToString(<BaseHelmet />),
  });
}

function flattenKeys(obj, out = {}, paths = []) {

  for (const key of Object.getOwnPropertyNames(obj)) {
    paths.push(key);

    if (typeof obj[key] === 'object') {
      flattenKeys(obj[key], out, paths);
    } else {
      out[paths.join('.')] = obj[key];
    }

    paths.pop();
  }

  return out;
}
<|endoftext|>