<commit_before>import {isBrowser, isNode} from './environment.js';
import Timer from './Timer.js';
import Logger from './Logger.js';
import getLoadControlClass from './LoadControl.js';
import Action from './Action.js';
import stringify from './stringify.js';
import {resetAssertCount, bumpAssertCount, getAssertCount, assert} from './assert.js';
import {getUrlArgs, argsToOptions, processOptions} from './config.js';
import testTypes from './testTypes.js';
import {defTest, orderTests} from './defTests.js';
import {queueActions, getQueuedActions, run, runDefault, getCapabilities} from './runners.js';

const defaultOptions = {
    nameSeparator: '/',
    quitOnFirstFail: false,
    include: [],
    logExcludes: true,
    concurrent: false,
    autoRun: false,
    load: [],
    provider: false,
    capabilities: isNode ? require(`${__dirname}/src/capabilities.js`) : {},
    cap: [],
    capPreset: [],
    user: {}
};

// the test stack which is populated with defTest, defBrowserTest, defBrowserTestRef, defNodeTest, and defRemoteTest
let smokeTests = [];

const LoadControl = getLoadControlClass(
    // log
    // eslint-disable-next-line no-use-before-define
    (...args) => (smoke.logger.log('smoke:load', 0, args)),

    // onResourceLoadComplete
    control => {
        smokeTests.forEach(test => {
            if (!('order' in test)) {
                test.order = 10000 + control.order;
            }
        });
    },

    // onLoadingComplete
    () => (smokeTests = orderTests(smokeTests)),
);

function pause(ms) {
    return new Promise(resolve => {
        setTimeout(resolve, ms);
    });
}

async function loaderIdle() {
    while (!LoadControl.loadingError) {
        const loadingPromise = LoadControl.loadingPromise;
        // eslint-disable-next-line no-await-in-loop
        await loadingPromise;
        // eslint-disable-next-line no-await-in-loop
        await pause(20);
        if (loadingPromise === LoadControl.loadingPromise) {
            break;
        }
    }
    return LoadControl.loadingError;
}

function processArgs(defaults, args) {
    const options = smoke.argsToOptions(args);
    Object.keys(defaults).forEach(k => {
        if (options[k] === undefined) {
            options[k] = defaults[k];
        }
    });
    return options;
}

const smoke = {
    get oem() {
        return 'altoviso';
    },
    get version() {
        return '1.5.0';
    },
    isBrowser,
    isNode,

    // eslint-disable-next-line no-undef
    isAmd: isBrowser && typeof define !== 'undefined' && define.amd,

    options: defaultOptions,

    Timer,
    Logger,
    logger: new Logger(defaultOptions),

    testTypes,
    get tests() {
        return smokeTests;
    },

    resetAssertCount,
    bumpAssertCount,
    getAssertCount,
    assert,

    pause,

    stringify,

    Action,

    injectScript: LoadControl.injectScript,
    injectCss: LoadControl.injectCss,
    loadNodeModule: LoadControl.loadNodeModule,
    loadAmdModule: LoadControl.loadAmdModule,

    get loadingPromise() {
        return LoadControl.loadingPromise.proxy;
    },

    get loaderIdle() {
        return loaderIdle();
    },

    get loadedResources() {
        const result = [];
        for (const control of LoadControl.injections.values()) {
            result.push({
                resource: control.resourceName,
                loadedName: control.loadedName,
                status: control.status,
                promise: control.promise.proxy,
                errorInfo: control.errorInfo
            });
        }
        return result;
    },

    get loadedResourcesCount() {
        return LoadControl.injections.size;
    },

    getLoadedResource(resourceName) {
        let control = LoadControl.injections.get(resourceName);
        if (control) {
            control = {
                resource: control.resourceName,
                loadedName: control.loadedName,
                status: control.status,
                promise: control.promise.proxy,
                errorInfo: control.errorInfo
            };
        }
        return control;
    },

    get loadingComplete() {
        return LoadControl.loadingPromise.resolved;
    },

    get loadingError() {
        return LoadControl.loadingError;
    },

    argsToOptions,

    getUrlArgs,
    processOptions,

    configureBrowser(defaults) {
        defaults = defaults || {};
        if (!defaults.root && /\/node_modules\/bd-smoke\/$/.test(window.location.pathname)) {
            // set the root directory to the project root if running from node_modules
            defaults.root = window.location.pathname.replace(/^(.+)\/node_modules\/bd-smoke\/(.+)$/, '$1/');
        }
        return smoke.configure(processArgs(defaults, smoke.getUrlArgs()));
    },

    configureNode(defaults) {
        defaults = defaults || {};
        const smokeFilespec = process.argv[1];
        if (!defaults.root) {
            if (/\/node_modules\/bd-smoke\/$/.test(smokeFilespec)) {
                // set the root directory to the project root if running from node_modules
                defaults.root = smokeFilespec.replace(/^(.+)\/node_modules\/bd-smoke\/(.+)$/, '$1/');
            } else {
                defaults.root = process.cwd();
            }
        }
        return smoke.configure(processArgs(defaults, process.argv.slice(2)));
    },

    configure(options, dest) {
        dest = dest || smoke.options;
        if (options.root) {
            const root = options.root;
            LoadControl.injectRelativePrefix = /\/$/.test(root) ? root : `${root}/`;
        }
        smoke.processOptions(options, dest);
        if (dest === smoke.options) {
            if (smoke.options.remotelyControlled) {
                delete smoke.options.concurrent;
            }
            smoke.logger.updateOptions(dest);
        }
        (dest.load || []).slice().forEach(resource => {
            if (/\.css/i.test(resource)) {
                if (isNode) {
                    smoke.logger.log('smoke:info', 0, ['CSS resource ignored on node']);
                } else {
                    smoke.injectCss(resource);
                }
            } else if (smoke.isAmd && !/\.js$/.test(resource)) {
                // assume resource is an AMD module if an AMD loader is present and resource does not have a .js file type
                smoke.loadAmdModule(resource);
            } else if (isNode) {
                if (/\.es6\.js$/.test(resource)) {
                    smoke.logger.log('smoke:info', 0, ['es6 resource ignored on node']);
                } else {
                    smoke.loadNodeModule(resource);
                }
            } else {
                smoke.injectScript(resource);
            }
        });

        return smoke.loadingPromise;
    },

    checkConfig(options) {
        options = { ...options || smoke.options };
        options.capabilities =
            getCapabilities(options.capabilities, options.provider, options.cap, options.capPreset, smoke.logger)[0];
        options.load = [];
        for (const control of LoadControl.injections.values()) {
            options.load.push(
                `${control.resourceName}:${
                    control.status === false ? 'failed' : (control.status === true ? 'loaded' : control.status)}`
            );
        }
        options.tests = smoke.tests.map(test => test.id);
        // eslint-disable-next-line no-console
        console.log(isNode ? smoke.stringify(options) : options);
    },

    defTest(...args) {
        // add a test definition that works on both the browser and node
        defTest(testTypes.both, smoke.logger, smokeTests, ...args);
    },

    defBrowserTest(...args) {
        defTest(testTypes.browser, smoke.logger, smokeTests, ...args);
    },

    defBrowserTestRef(...args) {
        // define test ids that can be run remotely without having to load those tests locally
        // this is important because some tests use JS6 import/export which node cannot consume
        args.forEach(test => {
            if (typeof test !== 'string') {
                smoke.logger.log('smoke:bad-test-spec', 0, ['arguments to defBrowserTestRef must be strings']);
            } else {
                defTest(testTypes.browser, smoke.logger, smokeTests, { id: test, test: _ => _ });
            }
        });
    },

    defNodeTest(...args) {
        // add a test definition that can _only_ run in node
        defTest(testTypes.node, smoke.logger, smokeTests, ...args);
    },

    defRemoteTest(...args) {
        // add a test definition controls a remote browser
        defTest(testTypes.remote, smoke.logger, smokeTests, ...args);
    },

    queueActions,
    getQueuedActions,

    run(testInstruction, logger, options, remote, resetLog) {
        // autoRun is canceled after the first run (prevents running twice when user configs call runDefault explicitly)
        smoke.options.autoRun = false;
        return run(
            smokeTests,
            testInstruction,
            logger || options.logger || smoke.logger,
            options || smoke.options,
            remote, resetLog
        );
    },

    runDefault() {
        // autoRun is canceled after the first run (prevents running twice when user configs call runDefault explicitly)
        if (smoke.options.checkConfig) {
            smoke.checkConfig();
            smoke.options.autoRun = false;
            return Promise.resolve(smoke.logger);
        } else {
            smoke.options.autoRun = false;
            return runDefault(smokeTests, smoke.options, smoke.options.logger || smoke.logger);
        }
    },

    async defaultStart(configPromise) {
        if (configPromise) {
            await configPromise;
        }
        if (!smoke.loadedResourcesCount) {
            await smoke.configure({ load: './smoke.config.js' });
        }

        const loadError = await loaderIdle();
        if (!loadError) {
            if (!smoke.loadingError && smoke.options.autoRun && !smoke.options.remotelyControlled) {
                const result = await smoke.runDefault();
                if (smoke.options.checkConfig) {
                    smoke.logger.log('smoke:exitCode', 0, ['only printed configuration, no tests ran', 0]);
                    isNode && process.exit(0);
                } else if (result.ranRemote) {
                    const exitCode = result.remoteLogs.failCount +
                        result.remoteLogs.scaffoldFailCount +
                        result.localLog.failCount +
                        result.localLog.scaffoldFailCount;
                    smoke.logger.log('smoke:exitCode', 0, ['default tests run on remote browser(s) completed', exitCode]);
                    isNode && process.exit(exitCode);
                } else {
                    const exitCode = result.localLog.failCount + result.localLog.scaffoldFailCount;
                    smoke.logger.log('smoke:exitCode', 0, ['default tests run locally completed', exitCode]);
                    isNode && process.exit(exitCode);
                }
            }
        } else {
            isNode && process.exit(-1);
        }
    }
};

export {smoke};
<commit_msg>enhance configuration methods to include onLoad and onError callbacks<commit_after>import {isBrowser, isNode} from './environment.js';
import Timer from './Timer.js';
import Logger from './Logger.js';
import getLoadControlClass from './LoadControl.js';
import Action from './Action.js';
import stringify from './stringify.js';
import {resetAssertCount, bumpAssertCount, getAssertCount, assert} from './assert.js';
import {getUrlArgs, argsToOptions, processOptions} from './config.js';
import testTypes from './testTypes.js';
import {defTest, orderTests} from './defTests.js';
import {queueActions, getQueuedActions, run, runDefault, getCapabilities} from './runners.js';

const defaultOptions = {
    nameSeparator: '/',
    quitOnFirstFail: false,
    include: [],
    logExcludes: true,
    concurrent: false,
    autoRun: false,
    load: [],
    provider: false,
    capabilities: isNode ? require(`${__dirname}/src/capabilities.js`) : {},
    cap: [],
    capPreset: [],
    user: {}
};

// the test stack which is populated with defTest, defBrowserTest, defBrowserTestRef, defNodeTest, and defRemoteTest
let smokeTests = [];

const LoadControl = getLoadControlClass(
    // log
    // eslint-disable-next-line no-use-before-define
    (...args) => (smoke.logger.log('smoke:load', 0, args)),

    // onResourceLoadComplete
    control => {
        smokeTests.forEach(test => {
            if (!('order' in test)) {
                test.order = 10000 + control.order;
            }
        });
    },

    // onLoadingComplete
    () => (smokeTests = orderTests(smokeTests)),
);

function pause(ms) {
    return new Promise(resolve => {
        setTimeout(resolve, ms);
    });
}

async function loaderIdle() {
    while (!LoadControl.loadingError) {
        const loadingPromise = LoadControl.loadingPromise;
        // eslint-disable-next-line no-await-in-loop
        await loadingPromise;
        // eslint-disable-next-line no-await-in-loop
        await pause(20);
        if (loadingPromise === LoadControl.loadingPromise) {
            break;
        }
    }
    return LoadControl.loadingError;
}

function processArgs(defaults, args) {
    const options = smoke.argsToOptions(args);
    Object.keys(defaults).forEach(k => {
        if (options[k] === undefined) {
            options[k] = defaults[k];
        }
    });
    return options;
}

const smoke = {
    get oem() {
        return 'altoviso';
    },
    get version() {
        return '1.5.0';
    },
    isBrowser,
    isNode,

    // eslint-disable-next-line no-undef
    isAmd: isBrowser && typeof define !== 'undefined' && define.amd,

    options: defaultOptions,

    Timer,
    Logger,
    logger: new Logger(defaultOptions),

    testTypes,
    get tests() {
        return smokeTests;
    },

    resetAssertCount,
    bumpAssertCount,
    getAssertCount,
    assert,

    pause,

    stringify,

    Action,

    injectScript: LoadControl.injectScript,
    injectCss: LoadControl.injectCss,
    loadNodeModule: LoadControl.loadNodeModule,
    loadAmdModule: LoadControl.loadAmdModule,

    get loadingPromise() {
        return LoadControl.loadingPromise.proxy;
    },

    get loaderIdle() {
        return loaderIdle();
    },

    get loadedResources() {
        const result = [];
        for (const control of LoadControl.injections.values()) {
            result.push({
                resource: control.resourceName,
                loadedName: control.loadedName,
                status: control.status,
                promise: control.promise.proxy,
                errorInfo: control.errorInfo
            });
        }
        return result;
    },

    get loadedResourcesCount() {
        return LoadControl.injections.size;
    },

    getLoadedResource(resourceName) {
        let control = LoadControl.injections.get(resourceName);
        if (control) {
            control = {
                resource: control.resourceName,
                loadedName: control.loadedName,
                status: control.status,
                promise: control.promise.proxy,
                errorInfo: control.errorInfo
            };
        }
        return control;
    },

    get loadingComplete() {
        return LoadControl.loadingPromise.resolved;
    },

    get loadingError() {
        return LoadControl.loadingError;
    },

    argsToOptions,

    getUrlArgs,
    processOptions,

    configureBrowser(defaults, onLoad, onError) {
        defaults = defaults || {};
        if (!defaults.root && /\/node_modules\/bd-smoke\/[^/]+\.html$/.test(window.location.pathname)) {
            // set the root directory to the project root if running from node_modules
            defaults.root = window.location.pathname.replace(/^(.+)\/node_modules\/bd-smoke\/(.+)$/, '$1/');
        }
        return smoke.configure(processArgs(defaults, smoke.getUrlArgs()), onLoad, onError);
    },

    configureNode(defaults, onLoad, onError) {
        defaults = defaults || {};
        const smokeFilespec = process.argv[1];
        if (!defaults.root) {
            if (/\/node_modules\/bd-smoke\//.test(smokeFilespec)) {
                // set the root directory to the project root if running from node_modules
                defaults.root = smokeFilespec.replace(/^(.+)\/node_modules\/bd-smoke\/[^/]+\.js$/, '$1/');
            } else {
                defaults.root = process.cwd();
            }
        }
        return smoke.configure(processArgs(defaults, process.argv.slice(2)), onLoad, onError);
    },

    configure(options, onLoad, onError) {
        const dest = smoke.options;
        if (options.root) {
            const root = options.root;
            LoadControl.injectRelativePrefix = /\/$/.test(root) ? root : `${root}/`;
        }
        smoke.processOptions(options, dest);
        if (smoke.options.remotelyControlled) {
            // gotta make smoke global so the remote controller can access it via an injected script
            window.smoke = smoke;
            delete smoke.options.concurrent;
        }
        smoke.logger.updateOptions(dest);
        (dest.load || []).slice().forEach(resource => {
            if (/\.css/i.test(resource)) {
                if (isNode) {
                    smoke.logger.log('smoke:info', 0, ['CSS resource ignored on node']);
                } else {
                    smoke.injectCss(resource);
                }
            } else if (smoke.isAmd && !/\.js$/.test(resource)) {
                // assume resource is an AMD module if an AMD loader is present and resource does not have a .js file type
                smoke.loadAmdModule(resource);
            } else if (isNode) {
                if (/\.es6\.js$/.test(resource)) {
                    smoke.logger.log('smoke:info', 0, ['es6 resource ignored on node']);
                } else {
                    smoke.loadNodeModule(resource);
                }
            } else {
                smoke.injectScript(resource);
            }
        });
        if (onLoad || onError) {
            const noop = () => 0;
            return smoke.loadingPromise.then(onLoad || noop, onError || noop);
        }
        return smoke.loadingPromise;
    },

    checkConfig(options) {
        options = { ...options || smoke.options };
        options.capabilities =
            getCapabilities(options.capabilities, options.provider, options.cap, options.capPreset, smoke.logger)[0];
        options.load = [];
        for (const control of LoadControl.injections.values()) {
            options.load.push(
                `${control.resourceName}:${
                    control.status === false ? 'failed' : (control.status === true ? 'loaded' : control.status)}`
            );
        }
        options.tests = smoke.tests.map(test => test.id);
        // eslint-disable-next-line no-console
        console.log(isNode ? smoke.stringify(options) : options);
    },

    defTest(...args) {
        // add a test definition that works on both the browser and node
        defTest(testTypes.both, smoke.logger, smokeTests, ...args);
    },

    defBrowserTest(...args) {
        defTest(testTypes.browser, smoke.logger, smokeTests, ...args);
    },

    defBrowserTestRef(...args) {
        // define test ids that can be run remotely without having to load those tests locally
        // this is important because some tests use JS6 import/export which node cannot consume
        args.forEach(test => {
            if (typeof test !== 'string') {
                smoke.logger.log('smoke:bad-test-spec', 0, ['arguments to defBrowserTestRef must be strings']);
            } else {
                defTest(testTypes.browser, smoke.logger, smokeTests, { id: test, test: _ => _ });
            }
        });
    },

    defNodeTest(...args) {
        // add a test definition that can _only_ run in node
        defTest(testTypes.node, smoke.logger, smokeTests, ...args);
    },

    defRemoteTest(...args) {
        // add a test definition controls a remote browser
        defTest(testTypes.remote, smoke.logger, smokeTests, ...args);
    },

    queueActions,
    getQueuedActions,

    run(testInstruction, logger, options, remote, resetLog) {
        // autoRun is canceled after the first run (prevents running twice when user configs call runDefault explicitly)
        smoke.options.autoRun = false;
        return run(
            smokeTests,
            testInstruction,
            logger || options.logger || smoke.logger,
            options || smoke.options,
            remote, resetLog
        );
    },

    runDefault() {
        // autoRun is canceled after the first run (prevents running twice when user configs call runDefault explicitly)
        if (smoke.options.checkConfig) {
            smoke.checkConfig();
            smoke.options.autoRun = false;
            return Promise.resolve(smoke.logger);
        } else {
            smoke.options.autoRun = false;
            return runDefault(smokeTests, smoke.options, smoke.options.logger || smoke.logger);
        }
    },

    async defaultStart(configPromise) {
        if (configPromise) {
            await configPromise;
        }
        if (!smoke.loadedResourcesCount) {
            await smoke.configure({ load: './smoke.config.js' });
        }

        const loadError = await loaderIdle();
        if (!loadError) {
            if (!smoke.loadingError && smoke.options.autoRun && !smoke.options.remotelyControlled) {
                const result = await smoke.runDefault();
                if (smoke.options.checkConfig) {
                    smoke.logger.log('smoke:exitCode', 0, ['only printed configuration, no tests ran', 0]);
                    isNode && process.exit(0);
                } else if (result.ranRemote) {
                    const exitCode = result.remoteLogs.failCount +
                        result.remoteLogs.scaffoldFailCount +
                        result.localLog.failCount +
                        result.localLog.scaffoldFailCount;
                    smoke.logger.log('smoke:exitCode', 0, ['default tests run on remote browser(s) completed', exitCode]);
                    isNode && process.exit(exitCode);
                } else {
                    const exitCode = result.localLog.failCount + result.localLog.scaffoldFailCount;
                    smoke.logger.log('smoke:exitCode', 0, ['default tests run locally completed', exitCode]);
                    isNode && process.exit(exitCode);
                }
            }
        } else {
            isNode && process.exit(-1);
        }
    }
};

export {smoke};
<|endoftext|>


<commit_before>Crafty.extend({
	/**@
	* #Crafty.audio
	* @category Audio
	* Add sound files and play them. Chooses best format for browser support.
	* Due to the nature of HTML5 audio, three types of audio files will be
	* required for cross-browser capabilities. These formats are MP3, Ogg and WAV.
	*/
	audio: {
		_elems: {},
		_muted: false,

		/**@
		* #Crafty.audio.MAX_CHANNELS
		* @comp Crafty.audio
		* Amount of Audio objects for a sound so overlapping of the
		* same sound can occur. More channels means more of the same sound
		* playing at the same time.
		*/
		MAX_CHANNELS: 5,

		type: {
			'mp3': 'audio/mpeg;',
			'ogg': 'audio/ogg; codecs="vorbis"',
			'wav': 'audio/wav; codecs="1"',
			'mp4': 'audio/mp4; codecs="mp4a.40.2"'
		},

		/**@
		* #Crafty.audio.add
		* @comp Crafty.audio
		* @sign public this Crafty.audio.add(String id, String url)
		* @param id - A string to reffer to sounds
		* @param url - A string pointing to the sound file
		* @sign public this Crafty.audio.add(String id, Array urls)
		* @param urls - Array of urls pointing to different format of the same sound, selecting the first that is playable
		* @sign public this Crafty.audio.add(Object map)
		* @param map - key-value pairs where the key is the `id` and the value is either a `url` or `urls`
		*
		* Loads a sound to be played. Due to the nature of HTML5 audio,
		* three types of audio files will be required for cross-browser capabilities.
		* These formats are MP3, Ogg and WAV.
		*
		* Passing an array of URLs will determine which format the browser can play and select it over any other.
		*
		* Accepts an object where the key is the audio name and
		* either a URL or an Array of URLs (to determine which type to use).
		*
		* The ID you use will be how you refer to that sound when using `Crafty.audio.play`.
		*
		* @example
		* ~~~
		* //adding audio from an object
		* Crafty.audio.add({
		* 	shoot: ["sounds/shoot.wav",
		* 			"sounds/shoot.mp3",
		* 			"sounds/shoot.ogg"],
		*
		* 	coin: "sounds/coin.mp3"
		* });
		*
		* //adding a single sound
		* Crafty.audio.add("walk", [
		* 	"sounds/walk.mp3",
		* 	"sounds/walk.ogg",
		* 	"sounds/walk.wav"
		* ]);
		*
		* //only one format
		* Crafty.audio.add("jump", "sounds/jump.mp3");
		* ~~~
		* @see Crafty.audio.play, Crafty.audio.settings
		*/
		add: function(id, url) {
			if(!Crafty.support.audio) return this;

			var elem,
				key,
				audio = new Audio(),
				canplay,
				i = 0,
				sounds = [];

			//if an object is passed
			if(arguments.length === 1 && typeof id === "object") {
				for(key in id) {
					if(!id.hasOwnProperty(key)) continue;

					//if array passed, add fallback sources
					if(typeof id[key] !== "string") {
						var sources = id[key], i = 0, l = sources.length,
							source;

						for(;i<l;++i) {
							source = sources[i];
							//get the file extension
							ext = source.substr(source.lastIndexOf('.')+1).toLowerCase();
							canplay = audio.canPlayType(this.type[ext]);

							//if browser can play this type, use it
							if(canplay !== "" && canplay !== "no") {
								url = source;
								break;
							}
						}
					} else {
						url = id[key];
					}

					for(;i<this.MAX_CHANNELS;i++) {
						audio = new Audio(url);
						audio.preload = "auto";
						audio.load();
						sounds.push(audio);
					}
					this._elems[key] = sounds;
					if(!Crafty.assets[url]) Crafty.assets[url] = this._elems[key][0];
				}

				return this;
			}
			//standard method
			if(typeof url !== "string") {
				var i = 0, l = url.length,
					source;

				for(;i<l;++i) {
					source = url[i];
					//get the file extension
					ext = source.substr(source.lastIndexOf('.')+1);
					canplay = audio.canPlayType(this.type[ext]);

					//if browser can play this type, use it
					if(canplay !== "" && canplay !== "no") {
						url = source;
						break;
					}
				}
			}

			//create a new Audio object and add it to assets
			for(;i<this.MAX_CHANNELS;i++) {
				audio = new Audio(url);
				audio.preload = "auto";
				audio.load();
				sounds.push(audio);
			}
			this._elems[id] = sounds;
			if(!Crafty.assets[url]) Crafty.assets[url] = this._elems[id][0];

			return this;
		},
		/**@
		* #Crafty.audio.play
		* @sign public this Crafty.audio.play(String id)
		* @sign public this Crafty.audio.play(String id, Number repeatCount)
		* @param id - A string to reffer to sounds
		* @param repeatCount - Repeat count for the file, where -1 stands for repeat forever.
		*
		* Will play a sound previously added by using the ID that was used in `Crafty.audio.add`.
		* Has a default maximum of 5 channels so that the same sound can play simultaneously unless all of the channels are playing.

		* *Note that the implementation of HTML5 Audio is buggy at best.*
		*
		* @example
		* ~~~
		* Crafty.audio.play("walk");
		*
		* //play and repeat forever
		* Crafty.audio.play("backgroundMusic", -1);
		* ~~~
		* @see Crafty.audio.add, Crafty.audio.settings
		*/
		play: function(id, repeat) {
			if(!Crafty.support.audio) return;

			var sounds = this._elems[id],
				sound,
				i = 0, l = sounds.length;

			for(;i<l;i++) {
				sound = sounds[i];
				//go through the channels and play a sound that is stopped
				if(sound.ended || !sound.currentTime) {
					sound.play();
					break;
				} else if(i === l-1) { //if all sounds playing, try stop the last one
					sound.currentTime = 0;
					sound.play();
				}
			}
			if (typeof repeat == "number") {
				var j=0;
				//i is still set to the sound we played
				sounds[i].addEventListener('ended', function(){
					if (repeat == -1 || j <= repeat){
						this.currentTime = 0;
						j++;
					}
				}, false);
			}
			return this;
		},

		/**@
		* #Crafty.audio.settings
		* @comp Crafty.audio
		* @sign public this Crafty.audio.settings(String id, Object settings)
		* @param id - The audio instance added by `Crafty.audio.add`
		* @param settings - An object where the key is the setting and the value is what to modify the setting with
		* Used to modify settings of the HTML5 `Audio` object. For a list of all the settings available,
		* see the [Mozilla Documentation](https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIDOMHTMLMediaElement).
		*/
		settings: function(id, settings) {
			//apply to all
			if(!settings) {
				for(var key in this._elems) {
					this.settings(key, id);
				}
				return this;
			}

			var sounds = this._elems[id],
				sound,
				setting,
				i = 0, l = sounds.length;

			for(var setting in settings) {
				for(;i<l;i++) {
					sound = sounds[i];
					sound[setting] = settings[setting];
				}
			}

			return this;
		},

		/**@
		* #Crafty.audio.mute
		* @sign public this Crafty.audio.mute(void)
		* Mute or unmute every Audio instance that is playing. Toggles between
		* pausing or playing depending on the state.
		*/
		mute: function() {
			var sounds, sound, i, l, elem;

			//loop over every sound
			for(sounds in this._elems) {
				elem = this._elems[sounds];

				//loop over every channel for a sound
				for(i = 0, l = elem.length; i < l; ++i) {
					sound = elem[i];

					//if playing, stop
					if(!sound.ended && sound.currentTime) {
						if(this._muted) sound.pause();
						else sound.play();
					}
				}
			}
			this._muted = !this._muted;
			return this;
		}
	}
});

//stop sounds on Pause
Crafty.bind("Pause", function() {Crafty.audio.mute()});
Crafty.bind("Unpause", function() {Crafty.audio.mute()});<commit_msg>fix muted according to https://groups.google.com/forum/#!topic/craftyjs/iFDftENd5f8 and add functionality to mute or unmute in addition to toggle.<commit_after>Crafty.extend({
	/**@
	* #Crafty.audio
	* @category Audio
	* Add sound files and play them. Chooses best format for browser support.
	* Due to the nature of HTML5 audio, three types of audio files will be
	* required for cross-browser capabilities. These formats are MP3, Ogg and WAV.
	*/
	audio: {
		_elems: {},
		_muted: false,

		/**@
		* #Crafty.audio.MAX_CHANNELS
		* @comp Crafty.audio
		* Amount of Audio objects for a sound so overlapping of the
		* same sound can occur. More channels means more of the same sound
		* playing at the same time.
		*/
		MAX_CHANNELS: 5,

		type: {
			'mp3': 'audio/mpeg;',
			'ogg': 'audio/ogg; codecs="vorbis"',
			'wav': 'audio/wav; codecs="1"',
			'mp4': 'audio/mp4; codecs="mp4a.40.2"'
		},

		/**@
		* #Crafty.audio.add
		* @comp Crafty.audio
		* @sign public this Crafty.audio.add(String id, String url)
		* @param id - A string to reffer to sounds
		* @param url - A string pointing to the sound file
		* @sign public this Crafty.audio.add(String id, Array urls)
		* @param urls - Array of urls pointing to different format of the same sound, selecting the first that is playable
		* @sign public this Crafty.audio.add(Object map)
		* @param map - key-value pairs where the key is the `id` and the value is either a `url` or `urls`
		*
		* Loads a sound to be played. Due to the nature of HTML5 audio,
		* three types of audio files will be required for cross-browser capabilities.
		* These formats are MP3, Ogg and WAV.
		*
		* Passing an array of URLs will determine which format the browser can play and select it over any other.
		*
		* Accepts an object where the key is the audio name and
		* either a URL or an Array of URLs (to determine which type to use).
		*
		* The ID you use will be how you refer to that sound when using `Crafty.audio.play`.
		*
		* @example
		* ~~~
		* //adding audio from an object
		* Crafty.audio.add({
		* 	shoot: ["sounds/shoot.wav",
		* 			"sounds/shoot.mp3",
		* 			"sounds/shoot.ogg"],
		*
		* 	coin: "sounds/coin.mp3"
		* });
		*
		* //adding a single sound
		* Crafty.audio.add("walk", [
		* 	"sounds/walk.mp3",
		* 	"sounds/walk.ogg",
		* 	"sounds/walk.wav"
		* ]);
		*
		* //only one format
		* Crafty.audio.add("jump", "sounds/jump.mp3");
		* ~~~
		* @see Crafty.audio.play, Crafty.audio.settings
		*/
		add: function(id, url) {
			if(!Crafty.support.audio) return this;

			var elem,
				key,
				audio = new Audio(),
				canplay,
				i = 0,
				sounds = [];

			//if an object is passed
			if(arguments.length === 1 && typeof id === "object") {
				for(key in id) {
					if(!id.hasOwnProperty(key)) continue;

					//if array passed, add fallback sources
					if(typeof id[key] !== "string") {
						var sources = id[key], i = 0, l = sources.length,
							source;

						for(;i<l;++i) {
							source = sources[i];
							//get the file extension
							ext = source.substr(source.lastIndexOf('.')+1).toLowerCase();
							canplay = audio.canPlayType(this.type[ext]);

							//if browser can play this type, use it
							if(canplay !== "" && canplay !== "no") {
								url = source;
								break;
							}
						}
					} else {
						url = id[key];
					}

					for(;i<this.MAX_CHANNELS;i++) {
						audio = new Audio(url);
						audio.preload = "auto";
						audio.load();
						sounds.push(audio);
					}
					this._elems[key] = sounds;
					if(!Crafty.assets[url]) Crafty.assets[url] = this._elems[key][0];
				}

				return this;
			}
			//standard method
			if(typeof url !== "string") {
				var i = 0, l = url.length,
					source;

				for(;i<l;++i) {
					source = url[i];
					//get the file extension
					ext = source.substr(source.lastIndexOf('.')+1);
					canplay = audio.canPlayType(this.type[ext]);

					//if browser can play this type, use it
					if(canplay !== "" && canplay !== "no") {
						url = source;
						break;
					}
				}
			}

			//create a new Audio object and add it to assets
			for(;i<this.MAX_CHANNELS;i++) {
				audio = new Audio(url);
				audio.preload = "auto";
				audio.load();
				sounds.push(audio);
			}
			this._elems[id] = sounds;
			if(!Crafty.assets[url]) Crafty.assets[url] = this._elems[id][0];

			return this;
		},
		/**@
		* #Crafty.audio.play
		* @sign public this Crafty.audio.play(String id)
		* @sign public this Crafty.audio.play(String id, Number repeatCount)
		* @param id - A string to reffer to sounds
		* @param repeatCount - Repeat count for the file, where -1 stands for repeat forever.
		*
		* Will play a sound previously added by using the ID that was used in `Crafty.audio.add`.
		* Has a default maximum of 5 channels so that the same sound can play simultaneously unless all of the channels are playing.

		* *Note that the implementation of HTML5 Audio is buggy at best.*
		*
		* @example
		* ~~~
		* Crafty.audio.play("walk");
		*
		* //play and repeat forever
		* Crafty.audio.play("backgroundMusic", -1);
		* ~~~
		* @see Crafty.audio.add, Crafty.audio.settings
		*/
		play: function(id, repeat) {
			if(!Crafty.support.audio) return;

			var sounds = this._elems[id],
				sound,
				i = 0, l = sounds.length;

			for(;i<l;i++) {
				sound = sounds[i];
				//go through the channels and play a sound that is stopped
				if(sound.ended || !sound.currentTime) {
					sound.play();
					break;
				} else if(i === l-1) { //if all sounds playing, try stop the last one
					sound.currentTime = 0;
					sound.play();
				}
			}
			if (typeof repeat == "number") {
				var j=0;
				//i is still set to the sound we played
				sounds[i].addEventListener('ended', function(){
					if (repeat == -1 || j <= repeat){
						this.currentTime = 0;
						j++;
					}
				}, false);
			}
			return this;
		},

		/**@
		* #Crafty.audio.settings
		* @comp Crafty.audio
		* @sign public this Crafty.audio.settings(String id, Object settings)
		* @param id - The audio instance added by `Crafty.audio.add`
		* @param settings - An object where the key is the setting and the value is what to modify the setting with
		* Used to modify settings of the HTML5 `Audio` object. For a list of all the settings available,
		* see the [Mozilla Documentation](https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIDOMHTMLMediaElement).
		*/
		settings: function(id, settings) {
			//apply to all
			if(!settings) {
				for(var key in this._elems) {
					this.settings(key, id);
				}
				return this;
			}

			var sounds = this._elems[id],
				sound,
				setting,
				i = 0, l = sounds.length;

			for(var setting in settings) {
				for(;i<l;i++) {
					sound = sounds[i];
					sound[setting] = settings[setting];
				}
			}

			return this;
		},

		/**@
		* #Crafty.audio.mute
		* @sign public this Crafty.audio.mute([Boolean mute])
		* Mute or unmute every Audio instance that is playing. Toggles between
		* pausing or playing depending on the state.
		* @example
		* ~~~
		* //toggle mute and unmute depending on current state
		* Crafty.audio.mute();
		*
		* //mute or unmute no matter what the current state is
		* Crafty.audio.mute(true);
		* Crafty.audio.mute(false);
		* ~~~

		*/
		mute: function(mute) {
			var sounds, sound, i, l, elem;
			this._muted = !this._muted;

			if (arguments.length == 1 && typeof(mute) == "boolean")
				this._muted = mute;

			//loop over every sound
			for(sounds in this._elems) {
				elem = this._elems[sounds];

				//loop over every channel for a sound
				for(i = 0, l = elem.length; i < l; ++i) {
					sound = elem[i];

					//if playing, stop
					if(!sound.ended && sound.currentTime) {
						if (this._muted)
							sound.pause();
						else
							sound.play();
					}
				}
			}
			return this;
		}
	}
});

//stop sounds on Pause
Crafty.bind("Pause", function() {Crafty.audio.mute()});
Crafty.bind("Unpause", function() {Crafty.audio.mute()});<|endoftext|>


<commit_before>/**
 *
 * Classy - State Helpers
 *
 * @module lib/state
 * @description
 *   Helpers for interfacing with Classy's state object
 */

import * as Utils from './utils';

/**
 *
 * Tracks the states of all Classy components
 * @type {Object}
 *
 */
const STATE = {};

/**
 *
 * The internal state prop defaults --
 * These are props that should only be manipulated
 * internally or through the use of `Utils` module methods.
 * Other state props (Component, subscriptions, and settings)
 * are not considered 'internal'.
 * @type {Object}
 *
 */
const INTERNAL_STATE_DEFAULTS = {
  isStyled: false,
  currentTheme: undefined,
  previousTheme: undefined,
  loadingStyles: false,
  cssText: undefined
};

/**
 *
 * Creates a Classy component state object
 *
 * @param  {ReactComponent} Component                          - A ReactComponent
 * @param  {Object}         settings                           - Settings object with the following options:
 * @param  {Boolean}        debug=false                        - Logs rendered cssText to debug console whens component styles are updated
 * @param  {Boolean}        hot=false                          - Applies two effects:
 *                                                               - Replace Component ref in state object when redecorated
 *                                                               - Always update styles when mounting an instance
 * @param  {String}         styleProp=style                    - Component prop to access for getting styles
 * @param  {String}         themeProp=theme                    - Component prop to access for getting themes
 * @param  {String}         alias=Component.name               - Key under which component state will be cached
 * @param  {String}         elemId=${alias}_${Utils.genHash()} - ID prop for component <style> tag
 * @param  {Object}         elemProps={ type: 'text/css' }     - Other props to apply to component <style> tag
 * @param  {String}         appendTo=head                      - Element to append component <style> tag to
 * @return {Object}                                            - Component state object
 */
export function createComponentState(
  Component,
  {
    debug      = false,
    hot        = false,
    styleProp  = 'style',
    themeProp  = 'theme',
    alias      = Component.name,
    elemId     = `${alias}_${Utils.genHash()}`,
    elemProps  = { type: 'text/css' },
    appendTo   = 'head'
  }={}
) {
  if (!alias) throw new ReferenceError(
    'Classy Error: createComponentState(...)\n' +
    `Component must be named or component's settings must specify an 'alias'.`
  );
  let state = getComponentState(alias);
  // Already has state
  if (state && !hot) {
    console.warn(
      'Classy Warning: createComponentState(...)\n' +
      `State has already been created for component ${alias}.`
    );
  }
  // Construct initial state
  else {
    setComponentState(alias, {
      Component,
      settings: {
        debug,
        hot,
        styleProp,
        themeProp,
        alias,
        elemId,
        elemProps,
        appendTo
      },
      subscriptions: {},
      ...INTERNAL_STATE_DEFAULTS
    });
    if (debug) console.debug(
      'Classy Debug: createComponentState(...)\n',
      alias+'\n',
      getComponentState(alias)
    );
  }
  return getComponentState(alias);
}

/**
 *
 * Assigns component state to `null`
 *
 * @param {String} alias - Component alias
 */
export function nullifyComponentState(alias) {
  STATE[alias] = null;
}

/**
 *
 * Gets a component's classy state object
 *
 * @param  {String} alias - Component alias
 * @return {Object}       - Copy of state object
 */
export function getComponentState(alias) {
  let state = STATE[alias];
  return state ? { ...state } : undefined;
}

/**
 *
 * Sets a component's classy state object
 *
 * @param {String} alias - Component alias
 * @param {Object} state - State object
 */
export function setComponentState(alias, state) {
  STATE[alias] = state;
}

/**
 *
 * Shallow merges object into component state
 *
 * @param {String} alias  - Component alias
 * @param {Object} state  - Object to be merged into state
 */
export function mergeComponentState(alias, state) {
  STATE[alias] = {
    ...STATE[alias],
    ...state
  };
}

/**
 *
 * Decrements a component state object prop
 *
 * @param {String} alias - Component alias
 * @param {String} key   - Key of prop to be decremented
 * @param {Number} num   - Amount to decrement
 */
export function decrProp(alias, key, num=1) {
  let { [key]: val } = getComponentState(alias);
  val -= num;
  mergeComponentState(alias, { [key]: val });
}

/**
 *
 * Increments a component state object prop
 *
 * @param {String} alias - Component alias
 * @param {String} key   - Key of prop to be incremented
 * @param {Number} num   - Amount to increment
 */
export function incrProp(alias, key, num=1) {
  let { [key]: val } = getComponentState(alias);
  val += num;
  mergeComponentState(alias, { [key]: val });
}

/**
 *
 * Adds component instance to the list of cached instances
 *
 * @param  {String} alias            - Component alias
 * @param  {ReactComponent} instance - Instance of ReactComponent
 * @return {Number}                  - Number of cached instances
 */
export function cacheComponentInstance(alias, instance) {
  let cached = false;
  let instances = getComponentInstances(alias);
  instances.forEach(c => c === instance ? (cached = true) : null);
  if (!cached) {
    instances.push(instance);
    setComponentInstances(alias, instances);
  }
  return getComponentInstances(alias).length;
}

/**
 *
 * Removes component instance from the list of cached instances
 *
 * @param  {String} alias            - Component alias
 * @param  {ReactComponent} instance - Instance of ReactComponent
 * @return {Number}                  - Number of cached instances
 */
export function clearComponentInstance(alias, instance) {
  let index;
  let instances = getComponentInstances(alias);
  instances.forEach((c, i) => c === instance ? (index = i) : null);
  if (index !== undefined) {
    instances.splice(index, 1);
    setComponentInstances(alias, instances);
  }
  return getComponentInstances(alias).length;
}

/**
 *
 * Gets the list of cached component instances
 *
 * @param  {String} alias - Component alias
 * @return {Array}        - Component instances
 */
export function getComponentInstances(alias) {
  let { symbol, Component } = getComponentState(alias);
  return Component[symbol].slice();
}

/**
 *
 * Sets the list of cached component instances
 *
 * @param {String} alias     - Component alias
 * @param {Array}  instances - Component instances
 */
export function setComponentInstances(alias, instances) {
  let { symbol, Component } = getComponentState(alias);
  STATE[alias].Component[symbol] = instances;
}

/**
 *
 * Resets the internal state props for all components
 *
 */
export function resetAllInternalStates() {
  let hasProp = STATE::{}.hasOwnProperty;
  for (let alias in STATE) {
    if (hasProp(alias)) resetInternalState(alias);
  }
}

/**
 *
 * Reset a component's internal state props
 *
 * @param {String} alias - Component alias
 */
export function resetInternalState(alias) {
  mergeComponentState(alias, INTERNAL_STATE_DEFAULTS);
}
<commit_msg>check component displayName<commit_after>/**
 *
 * Classy - State Helpers
 *
 * @module lib/state
 * @description
 *   Helpers for interfacing with Classy's state object
 */

import * as Utils from './utils';

/**
 *
 * Tracks the states of all Classy components
 * @type {Object}
 *
 */
const STATE = {};

/**
 *
 * The internal state prop defaults --
 * These are props that should only be manipulated
 * internally or through the use of `Utils` module methods.
 * Other state props (Component, subscriptions, and settings)
 * are not considered 'internal'.
 * @type {Object}
 *
 */
const INTERNAL_STATE_DEFAULTS = {
  isStyled: false,
  currentTheme: undefined,
  previousTheme: undefined,
  loadingStyles: false,
  cssText: undefined
};

/**
 *
 * Creates a Classy component state object
 *
 * @param  {ReactComponent} Component                          - A ReactComponent
 * @param  {Object}         settings                           - Settings object with the following options:
 * @param  {Boolean}        debug=false                        - Logs rendered cssText to debug console whens component styles are updated
 * @param  {Boolean}        hot=false                          - Applies two effects:
 *                                                               - Replace Component ref in state object when redecorated
 *                                                               - Always update styles when mounting an instance
 * @param  {String}         styleProp=style                    - Component prop to access for getting styles
 * @param  {String}         themeProp=theme                    - Component prop to access for getting themes
 * @param  {String}         alias=Component.name               - Key under which component state will be cached
 * @param  {String}         elemId=${alias}_${Utils.genHash()} - ID prop for component <style> tag
 * @param  {Object}         elemProps={ type: 'text/css' }     - Other props to apply to component <style> tag
 * @param  {String}         appendTo=head                      - Element to append component <style> tag to
 * @return {Object}                                            - Component state object
 */
export function createComponentState(
  Component,
  {
    debug      = false,
    hot        = false,
    styleProp  = 'style',
    themeProp  = 'theme',
    alias      = Component.prototype.displayName || Component.name,
    elemId     = `${alias}_${Utils.genHash()}`,
    elemProps  = { type: 'text/css' },
    appendTo   = 'head'
  }={}
) {
  if (!alias) throw new ReferenceError(
    'Classy Error: createComponentState(...)\n' +
    `Component must be named or component's settings must specify an 'alias'.`
  );
  let state = getComponentState(alias);
  // Already has state
  if (state && !hot) {
    console.warn(
      'Classy Warning: createComponentState(...)\n' +
      `State has already been created for component ${alias}.`
    );
  }
  // Construct initial state
  else {
    setComponentState(alias, {
      Component,
      settings: {
        debug,
        hot,
        styleProp,
        themeProp,
        alias,
        elemId,
        elemProps,
        appendTo
      },
      subscriptions: {},
      ...INTERNAL_STATE_DEFAULTS
    });
    if (debug) console.debug(
      'Classy Debug: createComponentState(...)\n',
      alias+'\n',
      getComponentState(alias)
    );
  }
  return getComponentState(alias);
}

/**
 *
 * Assigns component state to `null`
 *
 * @param {String} alias - Component alias
 */
export function nullifyComponentState(alias) {
  STATE[alias] = null;
}

/**
 *
 * Gets a component's classy state object
 *
 * @param  {String} alias - Component alias
 * @return {Object}       - Copy of state object
 */
export function getComponentState(alias) {
  let state = STATE[alias];
  return state ? { ...state } : undefined;
}

/**
 *
 * Sets a component's classy state object
 *
 * @param {String} alias - Component alias
 * @param {Object} state - State object
 */
export function setComponentState(alias, state) {
  STATE[alias] = state;
}

/**
 *
 * Shallow merges object into component state
 *
 * @param {String} alias  - Component alias
 * @param {Object} state  - Object to be merged into state
 */
export function mergeComponentState(alias, state) {
  STATE[alias] = {
    ...STATE[alias],
    ...state
  };
}

/**
 *
 * Decrements a component state object prop
 *
 * @param {String} alias - Component alias
 * @param {String} key   - Key of prop to be decremented
 * @param {Number} num   - Amount to decrement
 */
export function decrProp(alias, key, num=1) {
  let { [key]: val } = getComponentState(alias);
  val -= num;
  mergeComponentState(alias, { [key]: val });
}

/**
 *
 * Increments a component state object prop
 *
 * @param {String} alias - Component alias
 * @param {String} key   - Key of prop to be incremented
 * @param {Number} num   - Amount to increment
 */
export function incrProp(alias, key, num=1) {
  let { [key]: val } = getComponentState(alias);
  val += num;
  mergeComponentState(alias, { [key]: val });
}

/**
 *
 * Adds component instance to the list of cached instances
 *
 * @param  {String} alias            - Component alias
 * @param  {ReactComponent} instance - Instance of ReactComponent
 * @return {Number}                  - Number of cached instances
 */
export function cacheComponentInstance(alias, instance) {
  let cached = false;
  let instances = getComponentInstances(alias);
  instances.forEach(c => c === instance ? (cached = true) : null);
  if (!cached) {
    instances.push(instance);
    setComponentInstances(alias, instances);
  }
  return getComponentInstances(alias).length;
}

/**
 *
 * Removes component instance from the list of cached instances
 *
 * @param  {String} alias            - Component alias
 * @param  {ReactComponent} instance - Instance of ReactComponent
 * @return {Number}                  - Number of cached instances
 */
export function clearComponentInstance(alias, instance) {
  let index;
  let instances = getComponentInstances(alias);
  instances.forEach((c, i) => c === instance ? (index = i) : null);
  if (index !== undefined) {
    instances.splice(index, 1);
    setComponentInstances(alias, instances);
  }
  return getComponentInstances(alias).length;
}

/**
 *
 * Gets the list of cached component instances
 *
 * @param  {String} alias - Component alias
 * @return {Array}        - Component instances
 */
export function getComponentInstances(alias) {
  let { symbol, Component } = getComponentState(alias);
  return Component[symbol].slice();
}

/**
 *
 * Sets the list of cached component instances
 *
 * @param {String} alias     - Component alias
 * @param {Array}  instances - Component instances
 */
export function setComponentInstances(alias, instances) {
  let { symbol, Component } = getComponentState(alias);
  STATE[alias].Component[symbol] = instances;
}

/**
 *
 * Resets the internal state props for all components
 *
 */
export function resetAllInternalStates() {
  let hasProp = STATE::{}.hasOwnProperty;
  for (let alias in STATE) {
    if (hasProp(alias)) resetInternalState(alias);
  }
}

/**
 *
 * Reset a component's internal state props
 *
 * @param {String} alias - Component alias
 */
export function resetInternalState(alias) {
  mergeComponentState(alias, INTERNAL_STATE_DEFAULTS);
}
<|endoftext|>


<commit_before>import { storage as Storage, settings as Settings } from "./shared.js";
import { TabSet } from "./tabset.js";
import { History as Undo } from "./history.js";
import { reverse, sleep, debounce, first, parseQuery } from "./utils.js";
import Vuex from "vuex/dist/vuex.esm.js";

export default async () => {
  const windowid = await (async () => {
    const query = parseQuery(location.search);
    if ("windowid" in query) return parseInt(query.windowid, 10);
    return null;
  })();

  async function getWindowTabs() {
    const window = await (windowid === null
      ? browser.windows.getCurrent({ populate: true })
      : browser.windows.get(windowid, { populate: true }));
    return window.tabs;
  }

  const [items, settings, statesCount, windows, currentTabs] =
    await Promise.all([
      TabSet.getAll(),
      Settings.getAll(),
      Undo.count(),
      browser.windows.getAll({
        populate: true,
        windowTypes: ["normal"],
      }),
      browser.tabs.query({
        active: true,
      }),
    ]);

  const store = new Vuex.Store({
    state: {
      windows,
      items,
      settings,
      statesCount,
      notification: "",
      notificationCounter: 0,
      currentTabs,
    },
    getters: {
      itemsReversed(state) {
        return Array.from(reverse(state.items));
      },
      currentWindow(state) {
        const focused = first(state.windows, (x) => x.focused);
        if (focused === null) {
          return first(state.windows, (x) => x.id === windowid);
        }
        return focused;
      },
      windows(state) {
        let url = browser.runtime.getURL("");
        url = url.substr(0, url.length - 1);
        return state.windows.filter((x) => x.title.indexOf(url) !== 0);
      },
    },
    mutations: {
      updateItems(state, items) {
        state.items = items;
      },
      setSetting(state, { key, value }) {
        state.settings[key] = value;
      },
      setNotification(state, message) {
        state.notification = message;
      },
      incrementNotificationCounter(state, amount) {
        state.notificationCounter += amount;
      },
      updateStatesCount(state, count) {
        state.statesCount = count;
      },
      setCurrentTabs(state, tabs) {
        state.currentTabs = tabs;
      },
      setWindows(state, windows) {
        state.windows = windows;
      },
      removeWindowTab(state, tabId) {
        for (let window of state.windows) {
          for (let [index, tab] of window.tabs.entries()) {
            if (tab.id === tabId) {
              window.tabs.splice(index, 1);
              return;
            }
          }
        }
      },
    },
    actions: {
      async import(context, data) {
        await browser.runtime.sendMessage({
          type: "import",
          data,
        });
      },
      async export() {
        await browser.runtime.sendMessage("export");
      },
      async notify(context, message) {
        context.commit("setNotification", message);
        context.commit("incrementNotificationCounter", 1);
        await sleep(3000);
        context.commit("incrementNotificationCounter", -1);
        if (context.state.notificationCounter === 0)
          context.commit("setNotification", "");
      },
      async updateItems(context) {
        context.commit("updateItems", await TabSet.getAll());
      },
      async setSetting(context, { key, value }) {
        await Settings.set(key, value);
        context.commit("setSetting", { key, value });
      },
      async tabsetOpen(context, key = null) {
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "open",
          args: [key],
        });
      },
      async tabsetCreate(context, key = null) {
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "add",
          args: [key, await getWindowTabs()],
        });
      },
      async tabsetSave(context, { key, color, tabs = null }) {
        if (tabs === null) {
          tabs = await getWindowTabs();
          if (!context.state.settings.includePinned) {
            tabs = tabs.filter((x) => !x.pinned);
          }
        }
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "save",
          args: [key, tabs, color],
        });
      },
      async tabsetRename(context, [oldkey, newkey]) {
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "rename",
          args: [oldkey, newkey],
        });
      },
      async tabsetRemove(context, key) {
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "remove",
          args: [key],
        });
      },
      async tabsetMove(context, [tabsetKey, targetKey, after = true]) {
        await browser.runtime.sendMessage({
          domain: "tabset",
          action: "moveTabSet",
          args: [tabsetKey, targetKey, after],
        });
      },
      async tabsetAppend(context, key) {
        if (Array.isArray(key)) {
          await browser.runtime.sendMessage({
            domain: "tabset",
            action: "appendTab",
            args: [...key],
          });
        } else {
          await browser.runtime.sendMessage({
            domain: "tabset",
            action: "appendTab",
            args: [key],
          });
        }
      },
      async tabsetRemoveTab(context, [tabsetKey, tab]) {
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "removeTab",
          args: [tabsetKey, tab],
        });
      },
      async tabsetMoveTab(
        context,
        [tabsetKey, tab, targetTabsetKey, targetTab, after = true]
      ) {
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "moveTab",
          args: [tabsetKey, tab, targetTabsetKey, targetTab, after],
        });
      },
      async clearTabsets(context) {
        await Storage.set("tabs", []);
      },
      async openUrl(context, [url, identity, newTab = true]) {
        return await browser.runtime.sendMessage({
          domain: "openURL",
          args: [url, identity, newTab],
        });
      },
      async updateStatesCount(context) {
        const count = await Undo.count();
        context.commit("updateStatesCount", count);
      },
      async undo(context) {
        await browser.runtime.sendMessage("undo");
      },
      updateWindows: debounce(async (context) => {
        context.commit(
          "setWindows",
          await browser.windows.getAll({
            populate: true,
            windowTypes: ["normal"],
          })
        );
      }, 200),
    },
  });

  browser.storage.onChanged.addListener((diff, area) => {
    for (const [key, { oldValue, newValue }] of Object.entries(diff)) {
      if (key === "tabs") {
        store.dispatch("updateItems");
      } else if (key.indexOf("settings:") === 0) {
        const ckey = key.substr(9);
        store.commit("setSetting", { key: ckey, value: newValue });
      } else if (key.indexOf("history:states") === 0) {
        store.dispatch("updateStatesCount");
      }
    }
  });

  const updateCurrentTabs = async () => {
    const tabs = await browser.tabs.query({
      active: true,
    });
    store.commit("setCurrentTabs", tabs);
    store.dispatch("updateWindows");
  };

  browser.tabs.onActivated.addListener(updateCurrentTabs);
  browser.tabs.onUpdated.addListener(updateCurrentTabs);
  browser.tabs.onCreated.addListener(updateCurrentTabs);
  browser.tabs.onMoved.addListener(async () => {
    store.dispatch("updateWindows");
  });
  browser.tabs.onRemoved.addListener(async (tabId) => {
    store.commit("removeWindowTab", tabId);
  });
  browser.windows.onFocusChanged.addListener(async () => {
    store.dispatch("updateWindows");
  });
  browser.windows.onRemoved.addListener(async () => {
    store.dispatch("updateWindows");
  });
  browser.windows.onCreated.addListener(async () => {
    store.dispatch("updateWindows");
  });

  return store;
};
<commit_msg>Wrap handler with debounce<commit_after>import { storage as Storage, settings as Settings } from "./shared.js";
import { TabSet } from "./tabset.js";
import { History as Undo } from "./history.js";
import { reverse, sleep, debounce, first, parseQuery } from "./utils.js";
import Vuex from "vuex/dist/vuex.esm.js";

export default async () => {
  const windowid = await (async () => {
    const query = parseQuery(location.search);
    if ("windowid" in query) return parseInt(query.windowid, 10);
    return null;
  })();

  async function getWindowTabs() {
    const window = await (windowid === null
      ? browser.windows.getCurrent({ populate: true })
      : browser.windows.get(windowid, { populate: true }));
    return window.tabs;
  }

  const [items, settings, statesCount, windows, currentTabs] =
    await Promise.all([
      TabSet.getAll(),
      Settings.getAll(),
      Undo.count(),
      browser.windows.getAll({
        populate: true,
        windowTypes: ["normal"],
      }),
      browser.tabs.query({
        active: true,
      }),
    ]);

  const store = new Vuex.Store({
    state: {
      windows,
      items,
      settings,
      statesCount,
      notification: "",
      notificationCounter: 0,
      currentTabs,
    },
    getters: {
      itemsReversed(state) {
        return Array.from(reverse(state.items));
      },
      currentWindow(state) {
        const focused = first(state.windows, (x) => x.focused);
        if (focused === null) {
          return first(state.windows, (x) => x.id === windowid);
        }
        return focused;
      },
      windows(state) {
        let url = browser.runtime.getURL("");
        url = url.substr(0, url.length - 1);
        return state.windows.filter((x) => x.title.indexOf(url) !== 0);
      },
    },
    mutations: {
      updateItems(state, items) {
        state.items = items;
      },
      setSetting(state, { key, value }) {
        state.settings[key] = value;
      },
      setNotification(state, message) {
        state.notification = message;
      },
      incrementNotificationCounter(state, amount) {
        state.notificationCounter += amount;
      },
      updateStatesCount(state, count) {
        state.statesCount = count;
      },
      setCurrentTabs(state, tabs) {
        state.currentTabs = tabs;
      },
      setWindows(state, windows) {
        state.windows = windows;
      },
      removeWindowTab(state, tabId) {
        for (let window of state.windows) {
          for (let [index, tab] of window.tabs.entries()) {
            if (tab.id === tabId) {
              window.tabs.splice(index, 1);
              return;
            }
          }
        }
      },
    },
    actions: {
      async import(context, data) {
        await browser.runtime.sendMessage({
          type: "import",
          data,
        });
      },
      async export() {
        await browser.runtime.sendMessage("export");
      },
      async notify(context, message) {
        context.commit("setNotification", message);
        context.commit("incrementNotificationCounter", 1);
        await sleep(3000);
        context.commit("incrementNotificationCounter", -1);
        if (context.state.notificationCounter === 0)
          context.commit("setNotification", "");
      },
      async updateItems(context) {
        context.commit("updateItems", await TabSet.getAll());
      },
      async setSetting(context, { key, value }) {
        await Settings.set(key, value);
        context.commit("setSetting", { key, value });
      },
      async tabsetOpen(context, key = null) {
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "open",
          args: [key],
        });
      },
      async tabsetCreate(context, key = null) {
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "add",
          args: [key, await getWindowTabs()],
        });
      },
      async tabsetSave(context, { key, color, tabs = null }) {
        if (tabs === null) {
          tabs = await getWindowTabs();
          if (!context.state.settings.includePinned) {
            tabs = tabs.filter((x) => !x.pinned);
          }
        }
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "save",
          args: [key, tabs, color],
        });
      },
      async tabsetRename(context, [oldkey, newkey]) {
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "rename",
          args: [oldkey, newkey],
        });
      },
      async tabsetRemove(context, key) {
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "remove",
          args: [key],
        });
      },
      async tabsetMove(context, [tabsetKey, targetKey, after = true]) {
        await browser.runtime.sendMessage({
          domain: "tabset",
          action: "moveTabSet",
          args: [tabsetKey, targetKey, after],
        });
      },
      async tabsetAppend(context, key) {
        if (Array.isArray(key)) {
          await browser.runtime.sendMessage({
            domain: "tabset",
            action: "appendTab",
            args: [...key],
          });
        } else {
          await browser.runtime.sendMessage({
            domain: "tabset",
            action: "appendTab",
            args: [key],
          });
        }
      },
      async tabsetRemoveTab(context, [tabsetKey, tab]) {
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "removeTab",
          args: [tabsetKey, tab],
        });
      },
      async tabsetMoveTab(
        context,
        [tabsetKey, tab, targetTabsetKey, targetTab, after = true]
      ) {
        return await browser.runtime.sendMessage({
          domain: "tabset",
          action: "moveTab",
          args: [tabsetKey, tab, targetTabsetKey, targetTab, after],
        });
      },
      async clearTabsets(context) {
        await Storage.set("tabs", []);
      },
      async openUrl(context, [url, identity, newTab = true]) {
        return await browser.runtime.sendMessage({
          domain: "openURL",
          args: [url, identity, newTab],
        });
      },
      async updateStatesCount(context) {
        const count = await Undo.count();
        context.commit("updateStatesCount", count);
      },
      async undo(context) {
        await browser.runtime.sendMessage("undo");
      },
      updateWindows: debounce(async (context) => {
        context.commit(
          "setWindows",
          await browser.windows.getAll({
            populate: true,
            windowTypes: ["normal"],
          })
        );
      }, 200),
    },
  });

  browser.storage.onChanged.addListener((diff, area) => {
    for (const [key, { oldValue, newValue }] of Object.entries(diff)) {
      if (key === "tabs") {
        store.dispatch("updateItems");
      } else if (key.indexOf("settings:") === 0) {
        const ckey = key.substr(9);
        store.commit("setSetting", { key: ckey, value: newValue });
      } else if (key.indexOf("history:states") === 0) {
        store.dispatch("updateStatesCount");
      }
    }
  });

  const updateCurrentTabs = debounce(async () => {
    const tabs = await browser.tabs.query({
      active: true,
    });
    store.commit("setCurrentTabs", tabs);
    store.dispatch("updateWindows");
  }, 10);

  browser.tabs.onActivated.addListener(updateCurrentTabs);
  browser.tabs.onUpdated.addListener(updateCurrentTabs);
  browser.tabs.onCreated.addListener(updateCurrentTabs);
  browser.tabs.onMoved.addListener(async () => {
    store.dispatch("updateWindows");
  });
  browser.tabs.onRemoved.addListener(async (tabId) => {
    store.commit("removeWindowTab", tabId);
  });
  browser.windows.onFocusChanged.addListener(async () => {
    store.dispatch("updateWindows");
  });
  browser.windows.onRemoved.addListener(async () => {
    store.dispatch("updateWindows");
  });
  browser.windows.onCreated.addListener(async () => {
    store.dispatch("updateWindows");
  });

  return store;
};
<|endoftext|>


<commit_before>/* global module, require */

import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers/reducer';
import createLogger from 'redux-logger';

let logger;
// only set up logger in non-production mode
if (process.env.NODE_ENV !== 'production') {
    logger = createLogger();
}
let store;

/**
 * Initialize a Redux store with thunk, plus logging (only in development mode) middleware
 *
 * @returns {Store<GenericStoreEnhancer>}
 *  An initialized redux store with middleware and possible hot reloading of reducers
 */
const initializeStore = () => {
    if (store) {
        return store;
    }

    // only attach logger to middleware in non-production mode
    store =
        process.env.NODE_ENV === 'production'
            ? createStore(reducer, applyMiddleware(thunk))
            : createStore(reducer, applyMiddleware(thunk, logger));

    // TODO - Protect this under a debug mode?
    window.store = store; /* global window:true */

    if (module.hot) {
        // Enable hot module replacement for reducers
        module.hot.accept('./reducers/reducer', () => {
            const nextRootReducer = require('./reducers/reducer');

            store.replaceReducer(nextRootReducer);
        });
    }

    return store;
};

export default initializeStore;
<commit_msg>Add redux devtools support.<commit_after>/* global module, require */

import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers/reducer';
import createLogger from 'redux-logger';

let logger;
// only set up logger in non-production mode
if (process.env.NODE_ENV !== 'production') {
    logger = createLogger();
}
let store;

/**
 * Initialize a Redux store with thunk, plus logging (only in development mode) middleware
 *
 * @returns {Store<GenericStoreEnhancer>}
 *  An initialized redux store with middleware and possible hot reloading of reducers
 */
const initializeStore = () => {
    if (store) {
        return store;
    }

    // only attach logger to middleware in non-production mode
    store =
        process.env.NODE_ENV === 'production'
            ? createStore(reducer, applyMiddleware(thunk))
            : createStore(
                  reducer,
                  window.__REDUX_DEVTOOLS_EXTENSION__ &&
                      window.__REDUX_DEVTOOLS_EXTENSION__(),
                  applyMiddleware(thunk, logger)
              );

    // TODO - Protect this under a debug mode?
    window.store = store; /* global window:true */

    if (module.hot) {
        // Enable hot module replacement for reducers
        module.hot.accept('./reducers/reducer', () => {
            const nextRootReducer = require('./reducers/reducer');

            store.replaceReducer(nextRootReducer);
        });
    }

    return store;
};

export default initializeStore;
<|endoftext|>


<commit_before>import thunk from 'redux-thunk'
import createLogger from 'redux-logger'
import createBrowserHistory from 'history/lib/createBrowserHistory'
import { compose, createStore, applyMiddleware } from 'redux'
import { reduxReactRouter, routerStateReducer } from 'redux-router'
import { analytics, uploader, requester } from './middleware'
import { autoRehydrate } from 'redux-persist'
import routes from './routes'
import * as reducers from './reducers'

const logger = createLogger({ collapsed: true })
function reducer(state = {}, action) {
  return {
    json: reducers.json(state.json, action, state.router),
    devtools: reducers.devtools(state.devtools, action),
    modals: reducers.modals(state.modals, action),
    profile: reducers.profile(state.profile, action),
    router: routerStateReducer(state.router, action),
    stream: reducers.stream(state.stream, action),
  }
}
const store = compose(
  autoRehydrate(),
  applyMiddleware(thunk, uploader, requester, analytics, logger),
  reduxReactRouter({routes: routes, createHistory: createBrowserHistory})
)(createStore)(reducer)

export default store
<commit_msg>Return initial state when stat is undefined.<commit_after>import thunk from 'redux-thunk'
import createLogger from 'redux-logger'
import createBrowserHistory from 'history/lib/createBrowserHistory'
import { compose, createStore, applyMiddleware } from 'redux'
import { reduxReactRouter, routerStateReducer } from 'redux-router'
import { analytics, uploader, requester } from './middleware'
import { autoRehydrate } from 'redux-persist'
import routes from './routes'
import * as reducers from './reducers'

const logger = createLogger({ collapsed: true })
function reducer(state = {}, action) {
  if (!state && window.__INITIAL_STATE__) { return window.__INITIAL_STATE__ }
  return {
    json: reducers.json(state.json, action, state.router),
    devtools: reducers.devtools(state.devtools, action),
    modals: reducers.modals(state.modals, action),
    profile: reducers.profile(state.profile, action),
    router: routerStateReducer(state.router, action),
    stream: reducers.stream(state.stream, action),
  }
}
const store = compose(
  autoRehydrate(),
  applyMiddleware(thunk, uploader, requester, analytics, logger),
  reduxReactRouter({routes: routes, createHistory: createBrowserHistory})
)(createStore)(reducer)

export default store
<|endoftext|>


<commit_before>import S from "s-js";
import * as fastJsonPatch from "fast-json-patch";

function isKey(name, keys) {
  // inefficient check...  we are storing array of keys for this now...
  //return fn.toString().startsWith("function data(value)");
  return keys.indexOf(name) > -1;
}

function isArray(arr) {
  return !!arr && arr.constructor === Array;
}

function isObject(obj) {
  return !!obj && obj.constructor === Object;
}

const arrayMutators = [
  "splice",
  "push",
  "unshift",
  "pop",
  "shift",
  "copyWithin",
  "reverse"
];

export function autorun(thunk) {
  S.root(() => S(thunk));
}

function isKeyConflict(key, store) {
  if (key in store) {
    throw new RangeError(
      `actions: ${key} conflicts with a key already present in state`
    );
  }
}

function extendArray(val, theSetter) {
  const arrHandler = {
    get: function(target, name) {
      // wrap mutating functions...
      if (arrayMutators.indexOf(name) > -1) {
        return function() {
          // you need to delay the set of this value just like SArray... save mutation to temp first...
          let tmp = target.slice(0); // get clone of array
          const res = Array.prototype[name].apply(tmp, arguments);
          theSetter(tmp);
          return res;
        };
      }
      return target[name];
    }
  };
  return new Proxy(val, arrHandler);
}

function addData(key, val, opts, store, local) {
  const unobserved = local.unobserved;
  const observed = local.observed;
  if (opts && opts.observed === false) {
    unobserved.push(key);
    store[key] = val;
  } else {
    observed.push(key);
    store[key] = S.data(val);
  }
}

function addAction(key, val, opts, store, local) {
  const proxy = local.proxy;
  const fn = val;
  store[key] = function() {
    const actionArgs = arguments;
    S.freeze(() => {
      fn.apply(proxy, actionArgs);
    });
  };
}

function addComputed(key, val, opts, store, local) {
  const proxy = local.proxy;
  const computed = local.computed;
  const disposers = local.disposers;
  computed.push(key);
  const comp = val.bind(proxy);
  S.root(dispose => {
    store[key] = S(comp);
    disposers[key] = dispose;
  });
}

function addStore(key, val, store, local) {
  const stores = local.stores;
  const disposers = local.disposers;
  stores.push(key);
  const s = val.state || {};
  const a = val.actions || {};
  store[key] = Store(s, a);
  disposers[key] = () => {
    store[key]("dispose");
  };
}

function dispose(store, local) {
  const disposers = local.disposers;
  Object.keys(disposers).forEach(key => {
    disposers[key]();
  });
  Object.keys(store).forEach(key => {
    delete store[key];
  });
  const p = local.proxy;
  local = {
    proxy: p,
    computed: [],
    observed: [],
    unobserved: [],
    stores: [],
    disposers: {}
  };
  return local;
}

export function Store(state, actions) {
  let local = {
    proxy: undefined,
    computed: [],
    observed: [],
    unobserved: [],
    stores: [],
    disposers: {}
  };
  const store = function(t, key, val, opts) {
    if (key) {
      isKeyConflict(key, store);
    }

    if (t === "data") {
      addData(key, val, opts, store, local);
    } else if (t === "action") {
      addAction(key, val, opts, store, local);
    } else if (t === "computed") {
      addComputed(key, val, opts, store, local);
    } else if (t === "store") {
      addStore(key, val, store, local);
    } else if (t === "dispose") {
      local = dispose(store, local);
    } else {
      throw new RangeError(
        "type must be one of the following: data, dispose, action, computed, store"
      );
    }
  };
  local.proxy = new Proxy(store, {
    get: function(target, name) {
      if (name in target) {
        if (typeof target[name] === "function") {
          if (isKey(name, local.observed)) {
            let val = target[name]();
            if (isArray(val)) {
              val = extendArray(val, target[name]);
              return val;
            } else {
              return val;
            }
          } else if (isKey(name, local.computed)) {
            return target[name]();
          } else {
            return target[name];
          }
        } else {
          return target[name];
        }
      } else {
        if (name in actions) {
          return actions[name];
        } else {
          return undefined;
        }
      }
    },
    set: function(target, name, value) {
      if (name in target) {
        if (typeof target[name] === "function") {
          if (isKey(name, local.observed)) {
            target[name](value);
          } else if (isKey(name, local.stores)) {
            return false; // should I handle replacing a subStore?
          } else {
            //if (isKey(name, computedKeys)) { // missing case catch with this case...
            return false;
          }
        } else {
          target[name] = value;
        }
        return true;
      } else {
        // breaking from pojo behavior, changes made through apply only
        return false;
      }
    },
    has: function(target, name) {
      // we only want for..in loops to operate on state, not actions...
      if (
        local.computed.indexOf(name) > -1 ||
        local.observed.indexOf(name) > -1 ||
        local.unobserved.indexOf(name) > -1 ||
        local.stores.indexOf(name) > -1
      ) {
        return true;
      } else {
        return false;
      }
    },
    ownKeys: function(target) {
      return Reflect.ownKeys(target).filter(k => {
        return (
          local.computed.indexOf(k) > -1 ||
          local.observed.indexOf(k) > -1 ||
          local.unobserved.indexOf(k) > -1 ||
          k === "caller" ||
          k === "prototype" ||
          k === "arguments"
        );
      });
    },
    deleteProperty: function(target, name) {
      // handle removing observer stuff for this key if needed..
      if (name in target) {
        if (name in local.disposers) {
          local.disposers[name]();
          delete local.disposers[name];
        }
        delete target[name];
        if (local.observed.indexOf(name) > -1) {
          local.observed = local.observed.filter(key => key !== name);
        }
        if (local.computed.indexOf(name) > -1) {
          local.computed = local.computed.filter(key => key !== name);
        }
        if (local.stores.indexOf(name) > -1) {
          local.stores = local.stores.filter(key => key !== name);
        }
        return true;
      } else {
        return false;
      }
    }
  });

  // attach data and computations to proxy...
  Object.keys(state).forEach(key => {
    const val = state[key];
    if (typeof val === "function") {
      local.proxy("computed", key, val);
    } else if (isObject(val)) {
      local.proxy("store", key, { state: val });
    } else {
      local.proxy("data", key, val);
    }
  });

  Object.keys(actions).forEach(key => {
    local.proxy("action", key, actions[key]);
  });

  return local.proxy;
}
<commit_msg>removed extraneous conditional<commit_after>import S from "s-js";
import * as fastJsonPatch from "fast-json-patch";

function isKey(name, keys) {
  // inefficient check...  we are storing array of keys for this now...
  //return fn.toString().startsWith("function data(value)");
  return keys.indexOf(name) > -1;
}

function isArray(arr) {
  return !!arr && arr.constructor === Array;
}

function isObject(obj) {
  return !!obj && obj.constructor === Object;
}

const arrayMutators = [
  "splice",
  "push",
  "unshift",
  "pop",
  "shift",
  "copyWithin",
  "reverse"
];

export function autorun(thunk) {
  S.root(() => S(thunk));
}

function isKeyConflict(key, store) {
  if (key in store) {
    throw new RangeError(
      `actions: ${key} conflicts with a key already present in state`
    );
  }
}

function extendArray(val, theSetter) {
  const arrHandler = {
    get: function(target, name) {
      // wrap mutating functions...
      if (arrayMutators.indexOf(name) > -1) {
        return function() {
          // you need to delay the set of this value just like SArray... save mutation to temp first...
          let tmp = target.slice(0); // get clone of array
          const res = Array.prototype[name].apply(tmp, arguments);
          theSetter(tmp);
          return res;
        };
      }
      return target[name];
    }
  };
  return new Proxy(val, arrHandler);
}

function addData(key, val, opts, store, local) {
  const unobserved = local.unobserved;
  const observed = local.observed;
  if (opts && opts.observed === false) {
    unobserved.push(key);
    store[key] = val;
  } else {
    observed.push(key);
    store[key] = S.data(val);
  }
}

function addAction(key, val, opts, store, local) {
  const proxy = local.proxy;
  const fn = val;
  store[key] = function() {
    const actionArgs = arguments;
    S.freeze(() => {
      fn.apply(proxy, actionArgs);
    });
  };
}

function addComputed(key, val, opts, store, local) {
  const proxy = local.proxy;
  const computed = local.computed;
  const disposers = local.disposers;
  computed.push(key);
  const comp = val.bind(proxy);
  S.root(dispose => {
    store[key] = S(comp);
    disposers[key] = dispose;
  });
}

function addStore(key, val, store, local) {
  const stores = local.stores;
  const disposers = local.disposers;
  stores.push(key);
  const s = val.state || {};
  const a = val.actions || {};
  store[key] = Store(s, a);
  disposers[key] = () => {
    store[key]("dispose");
  };
}

function dispose(store, local) {
  const disposers = local.disposers;
  Object.keys(disposers).forEach(key => {
    disposers[key]();
  });
  Object.keys(store).forEach(key => {
    delete store[key];
  });
  const p = local.proxy;
  local = {
    proxy: p,
    computed: [],
    observed: [],
    unobserved: [],
    stores: [],
    disposers: {}
  };
  return local;
}

export function Store(state, actions) {
  let local = {
    proxy: undefined,
    computed: [],
    observed: [],
    unobserved: [],
    stores: [],
    disposers: {}
  };
  const store = function(t, key, val, opts) {
    if (key) {
      isKeyConflict(key, store);
    }

    if (t === "data") {
      addData(key, val, opts, store, local);
    } else if (t === "action") {
      addAction(key, val, opts, store, local);
    } else if (t === "computed") {
      addComputed(key, val, opts, store, local);
    } else if (t === "store") {
      addStore(key, val, store, local);
    } else if (t === "dispose") {
      local = dispose(store, local);
    } else {
      throw new RangeError(
        "type must be one of the following: data, dispose, action, computed, store"
      );
    }
  };
  local.proxy = new Proxy(store, {
    get: function(target, name) {
      if (name in target) {
        if (typeof target[name] === "function") {
          if (isKey(name, local.observed)) {
            let val = target[name]();
            if (isArray(val)) {
              val = extendArray(val, target[name]);
              return val;
            } else {
              return val;
            }
          } else if (isKey(name, local.computed)) {
            return target[name]();
          } else {
            return target[name];
          }
        } else {
          return target[name];
        }
      } else {
        if (name in actions) {
          return actions[name];
        } else {
          return undefined;
        }
      }
    },
    set: function(target, name, value) {
      if (name in target) {
        if (typeof target[name] === "function") {
          if (isKey(name, local.observed)) {
            target[name](value);
          } else {
            return false;
          }
        } else {
          target[name] = value;
        }
        return true;
      } else {
        // breaking from pojo behavior, changes made through apply only
        return false;
      }
    },
    has: function(target, name) {
      // we only want for..in loops to operate on state, not actions...
      if (
        local.computed.indexOf(name) > -1 ||
        local.observed.indexOf(name) > -1 ||
        local.unobserved.indexOf(name) > -1 ||
        local.stores.indexOf(name) > -1
      ) {
        return true;
      } else {
        return false;
      }
    },
    ownKeys: function(target) {
      return Reflect.ownKeys(target).filter(k => {
        return (
          local.computed.indexOf(k) > -1 ||
          local.observed.indexOf(k) > -1 ||
          local.unobserved.indexOf(k) > -1 ||
          k === "caller" ||
          k === "prototype" ||
          k === "arguments"
        );
      });
    },
    deleteProperty: function(target, name) {
      // handle removing observer stuff for this key if needed..
      if (name in target) {
        if (name in local.disposers) {
          local.disposers[name]();
          delete local.disposers[name];
        }
        delete target[name];
        if (local.observed.indexOf(name) > -1) {
          local.observed = local.observed.filter(key => key !== name);
        }
        if (local.computed.indexOf(name) > -1) {
          local.computed = local.computed.filter(key => key !== name);
        }
        if (local.stores.indexOf(name) > -1) {
          local.stores = local.stores.filter(key => key !== name);
        }
        return true;
      } else {
        return false;
      }
    }
  });

  // attach data and computations to proxy...
  Object.keys(state).forEach(key => {
    const val = state[key];
    if (typeof val === "function") {
      local.proxy("computed", key, val);
    } else if (isObject(val)) {
      local.proxy("store", key, { state: val });
    } else {
      local.proxy("data", key, val);
    }
  });

  Object.keys(actions).forEach(key => {
    local.proxy("action", key, actions[key]);
  });

  return local.proxy;
}
<|endoftext|>