<commit_before>if (jsio.__env.name == 'browser') {
	jsio('external .sizzle import Sizzle');
	jsio('import math2D.Rect');

	function isWindow(el) {
		return el && !$.isElement(el) && $.isElement(el.document);
	}

	var singleId = /^#([\w-]+)$/;

	var $ = exports.$ = function(selector, win) {
		switch(typeof selector) {
			case 'object':
				if ($.isElement(selector)) {
					return $.remove(selector);
				} else if ($.isElement(selector.document && selector.document.body)) {
					return $.size(selector);
				}
				return $.create(selector);
			case 'string':
				if (singleId.test(selector)) { return $.id(selector.substring(1), win); }
				return Sizzle.apply(GLOBAL, arguments);
		}
	}

	var DOM2 = typeof HTMLElement === "object";
	$.isElement = function(el) {
		return el &&
			(DOM2 ? el instanceof HTMLElement
				: typeof el.nodeType == 'number' && typeof el.nodeName == 'string');
	}

	$.id = function(id, win) { return typeof id == 'string' ? (win || window).document.getElementById(id) : id; }

	$.apply = function(el, params) {
		if (params.attrs) {
			for(attr in params.attrs) {
				el.setAttribute(attr, params.attrs[attr]);
			}
		}

		if (params.style) { $.style(el, params.style); }
		if (params.src) { el.src = params.src; }
		if (params['class'] || params['className']) {
			el.className = params['class'] || params['className'];
		}

		var parent = params.parent || params.parentNode;
		if (params.before) {
			$.insertBefore(params.before.parentNode || parent, el, params.before);
		} else if (params.after) {
			$.insertAfter(params.after.parentNode || parent, el, params.after);
		} else if (parent) {
			parent.appendChild(el);
		}

		if (params.html) { el.innerHTML = params.html; }
		if (params.text) { $.setText(el, params.text); }
		return el;
	}

	$.insertBefore = function(parentNode, el, beforeNode) {
		if (!parentNode || !el) { return; }
		if (beforeNode && beforeNode.parentNode == parentNode) {
			parentNode.insertBefore(el, beforeNode);
		} else {
			parentNode.appendChild(el);
		}
	}

	$.insertAfter = function(parentNode, el, afterNode) {
		if (!parentNode || !el) { return; }
		if (!afterNode || afterNode.parentNode != parentNode) {
			$.insertBefore(parentNode, el, parentNode.firstChild);
		} else if (!afterNode.nextSibling) {
			parentNode.appendChild(el);
		} else {
			parentNode.insertBefore(el, afterNode.nextSibling);
		}
	}

	$.create = function(params) {
		var doc = (params.win || window).document;
		if (!params || typeof params == 'string') {
			return doc.createElement(params || 'div');
		};

		return $.apply(doc.createElement(params.tag || params.tagName || 'div'), params);
	}

	$.show = function(el, how) { $.id(el).style.display = how || 'block'; }
	$.hide = function(el) { $.id(el).style.display = 'none'; }

	// accepts an array or a space-delimited string of classNames
	$.addClass = function(el, classNames) {
		if (!el) { return; }
		var el = $.id(el);
		if(typeof classNames == "string") {
			classNames = classNames.split(' ');
		}

		var current = ' ' + el.className + ' ';
		for(var i = 0, len = classNames.length, c; (c = classNames[i]) || i < len; ++i) {
			if(current.indexOf(' '+c+' ') == -1) {
				current += c + ' ';
			}
		}

		el.className = current;
		return $;
	}

	$.getTag = function(from, tag) { return from.getElementsByTagName(tag); }

	$.removeClass = function(el, classNames) {
		if (!el) { return; }
		var el = $.id(el);
		el.className = (' ' + el.className + ' ')
			.replace(' ', '  ')
			.replace(new RegExp('( ' + classNames.replace('\s+', ' | ').replace('-','\-') + ' )', 'g'), '')
			.replace(/\s+/, ' ')
			.replace(/^\s+|\s+%/, '');
	}

	function ieGetAlpha(el) {
		try {
			return el.filters.item("alpha");
		} catch(e) {}

		try {
			return el.filters.item("progid:DXImageTransform.Microsoft.Alpha");
		} catch(e) {}

		return null;
	}

	$.style = function(el, style) {
		if(el instanceof Array) {
			for(var i = 0, o; o = el[i]; ++i) { $.style(o, style); }
			return;
		}

		el = $.id(el);
		var s = el.style;
		for(prop in style) {
			switch(prop) {
				case 'float':
					s.styleFloat = s.cssFloat = style[prop];
					break;
				case 'opacity':
					s.opacity = style[prop];
					if(el.filters) {
						try {
							var alpha = ieGetAlpha();
							var opacity = style[prop] == 1 ? 99.99 : style[prop] * 100;
							if(!alpha) {
								// TODO: this might destroy any existing filters?
								s.filter = "alpha(opacity=" + opacity + ")";
							} else {
								alpha.Opacity = opacity;
							}
						} catch(e) {}
					}
					break;
				case 'boxSizing':
					s.MsBoxSizing = s.MozBoxSizing = s.WebkitBoxSizing = style[prop];
				default:
					s[prop] = style[prop];
					break;
			}
		}
	};

	$.onEvent = function(el, name, f) {
		if (typeof f != 'function') {
			f = bind.apply(GLOBAL, Array.prototype.slice.call(arguments, 2));
		}

		var handler = f;

		el = $.id(el);
		if(el.addEventListener) {
			el.addEventListener(name, handler, false);
		} else {
			handler = function(e) {
				var evt = e || window.event;
				// TODO: normalize the event object
				f(evt);
			};

			el.attachEvent('on' + name, handler);
		}

		return bind($, 'removeEvent', el, name, handler);
	};

	$.removeEvent = function(el, name, f) {
		el = $.id(el);
		if (el.addEventListener) {
			el.removeEventListener(name, f, false);
		} else {
			el.detachEvent('on' + name, f);
		}
	}

	$.stopEvent = function(e) {
		e.cancelBubble = true;
		if(e.stopPropagation) e.stopPropagation();
		if(e.preventDefault) e.preventDefault();
	}

	$.setText = function(el, text) {
		el = $.id(el);
		text = String(text);
		if ('textContent' in el) {
			el.textContent = text;
		} else if ('innerText' in el) {
			el.innerText = text.replace(/\n/g, ' ');
		} else {
			el.innerHTML = '';
			el.appendChild(document.createTextNode(text));
		}
	}

	$.remove = function(el) {
		el = $.id(el);
		if(el && el.parentNode) {
			el.parentNode.removeChild(el);
		}
	}

	$.cursorPos = function(ev, el) {
		var offset = $.pos(el);
		offset.top = ev.clientY - offset.top + (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
		offset.left = ev.clientX - offset.left + (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft);
		return offset;
	}

	$.pos = function(el) {
		var parent = el;
		var offset = {top: 0, left: 0};
		while(parent && parent != document.body) {
			offset.left += parent.offsetLeft;
			offset.top += parent.offsetTop;
			while(parent.offsetParent != parent.parentNode) {
				offset.top -= parent.scrollTop; offset.left -= parent.scrollLeft;
				parent = parent.parentNode;
			}
			parent = parent.offsetParent;
		}
		return offset;
	}

	$.size = function(el) {
		if ($.isElement(el)) {
			return {width: el.offsetWidth, height: el.offsetHeight};
		} else if (el.document) {
			var doc = el.document.documentElement || el.document.body;
			return new math2D.Rect(
				doc.offsetTop,
				doc.offsetLeft,
				el.innerWidth || (doc.clientWidth || doc.clientWidth),
				el.innerHeight || (doc.clientHeight || doc.clientHeight)
			);
		}
	}

	$.insertCSSFile = function(filename) {
		document.getElementsByTagName('head')[0].appendChild($({
			tag: 'link',
			attrs: {
				rel: 'stylesheet',
				type: 'text/css',
				href: filename
			}
		}));
	}
}
<commit_msg>$.apply now accepts an id key<commit_after>if (jsio.__env.name == 'browser') {
	jsio('external .sizzle import Sizzle');
	jsio('import math2D.Rect');

	function isWindow(el) {
		return el && !$.isElement(el) && $.isElement(el.document);
	}

	var singleId = /^#([\w-]+)$/;

	var $ = exports.$ = function(selector, win) {
		switch(typeof selector) {
			case 'object':
				if ($.isElement(selector)) {
					return $.remove(selector);
				} else if ($.isElement(selector.document && selector.document.body)) {
					return $.size(selector);
				}
				return $.create(selector);
			case 'string':
				if (singleId.test(selector)) { return $.id(selector.substring(1), win); }
				return Sizzle.apply(GLOBAL, arguments);
		}
	}

	var DOM2 = typeof HTMLElement === "object";
	$.isElement = function(el) {
		return el &&
			(DOM2 ? el instanceof HTMLElement
				: typeof el.nodeType == 'number' && typeof el.nodeName == 'string');
	}

	$.id = function(id, win) { return typeof id == 'string' ? (win || window).document.getElementById(id) : id; }

	$.apply = function(el, params) {
		if (params.attrs) {
			for(attr in params.attrs) {
				el.setAttribute(attr, params.attrs[attr]);
			}
		}

		if (params.id) { el.id = params.id; }
		if (params.style) { $.style(el, params.style); }
		if (params.src) { el.src = params.src; }
		if (params['class'] || params['className']) {
			el.className = params['class'] || params['className'];
		}

		var parent = params.parent || params.parentNode;
		if (params.before) {
			$.insertBefore(params.before.parentNode || parent, el, params.before);
		} else if (params.after) {
			$.insertAfter(params.after.parentNode || parent, el, params.after);
		} else if (parent) {
			parent.appendChild(el);
		}

		if (params.html) { el.innerHTML = params.html; }
		if (params.text) { $.setText(el, params.text); }
		return el;
	}

	$.insertBefore = function(parentNode, el, beforeNode) {
		if (!parentNode || !el) { return; }
		if (beforeNode && beforeNode.parentNode == parentNode) {
			parentNode.insertBefore(el, beforeNode);
		} else {
			parentNode.appendChild(el);
		}
	}

	$.insertAfter = function(parentNode, el, afterNode) {
		if (!parentNode || !el) { return; }
		if (!afterNode || afterNode.parentNode != parentNode) {
			$.insertBefore(parentNode, el, parentNode.firstChild);
		} else if (!afterNode.nextSibling) {
			parentNode.appendChild(el);
		} else {
			parentNode.insertBefore(el, afterNode.nextSibling);
		}
	}

	$.create = function(params) {
		var doc = (params.win || window).document;
		if (!params || typeof params == 'string') {
			return doc.createElement(params || 'div');
		};

		return $.apply(doc.createElement(params.tag || params.tagName || 'div'), params);
	}

	$.show = function(el, how) { $.id(el).style.display = how || 'block'; }
	$.hide = function(el) { $.id(el).style.display = 'none'; }

	// accepts an array or a space-delimited string of classNames
	$.addClass = function(el, classNames) {
		if (!el) { return; }
		var el = $.id(el);
		if(typeof classNames == "string") {
			classNames = classNames.split(' ');
		}

		var current = ' ' + el.className + ' ';
		for(var i = 0, len = classNames.length, c; (c = classNames[i]) || i < len; ++i) {
			if(current.indexOf(' '+c+' ') == -1) {
				current += c + ' ';
			}
		}

		el.className = current;
		return $;
	}

	$.getTag = function(from, tag) { return from.getElementsByTagName(tag); }

	$.removeClass = function(el, classNames) {
		if (!el) { return; }
		var el = $.id(el);
		el.className = (' ' + el.className + ' ')
			.replace(' ', '  ')
			.replace(new RegExp('( ' + classNames.replace('\s+', ' | ').replace('-','\-') + ' )', 'g'), '')
			.replace(/\s+/, ' ')
			.replace(/^\s+|\s+%/, '');
	}

	function ieGetAlpha(el) {
		try {
			return el.filters.item("alpha");
		} catch(e) {}

		try {
			return el.filters.item("progid:DXImageTransform.Microsoft.Alpha");
		} catch(e) {}

		return null;
	}

	$.style = function(el, style) {
		if(el instanceof Array) {
			for(var i = 0, o; o = el[i]; ++i) { $.style(o, style); }
			return;
		}

		el = $.id(el);
		var s = el.style;
		for(prop in style) {
			switch(prop) {
				case 'float':
					s.styleFloat = s.cssFloat = style[prop];
					break;
				case 'opacity':
					s.opacity = style[prop];
					if(el.filters) {
						try {
							var alpha = ieGetAlpha();
							var opacity = style[prop] == 1 ? 99.99 : style[prop] * 100;
							if(!alpha) {
								// TODO: this might destroy any existing filters?
								s.filter = "alpha(opacity=" + opacity + ")";
							} else {
								alpha.Opacity = opacity;
							}
						} catch(e) {}
					}
					break;
				case 'boxSizing':
					s.MsBoxSizing = s.MozBoxSizing = s.WebkitBoxSizing = style[prop];
				default:
					s[prop] = style[prop];
					break;
			}
		}
	};

	$.onEvent = function(el, name, f) {
		if (typeof f != 'function') {
			f = bind.apply(GLOBAL, Array.prototype.slice.call(arguments, 2));
		}

		var handler = f;

		el = $.id(el);
		if(el.addEventListener) {
			el.addEventListener(name, handler, false);
		} else {
			handler = function(e) {
				var evt = e || window.event;
				// TODO: normalize the event object
				f(evt);
			};

			el.attachEvent('on' + name, handler);
		}

		return bind($, 'removeEvent', el, name, handler);
	};

	$.removeEvent = function(el, name, f) {
		el = $.id(el);
		if (el.addEventListener) {
			el.removeEventListener(name, f, false);
		} else {
			el.detachEvent('on' + name, f);
		}
	}

	$.stopEvent = function(e) {
		e.cancelBubble = true;
		if(e.stopPropagation) e.stopPropagation();
		if(e.preventDefault) e.preventDefault();
	}

	$.setText = function(el, text) {
		el = $.id(el);
		text = String(text);
		if ('textContent' in el) {
			el.textContent = text;
		} else if ('innerText' in el) {
			el.innerText = text.replace(/\n/g, ' ');
		} else {
			el.innerHTML = '';
			el.appendChild(document.createTextNode(text));
		}
	}

	$.remove = function(el) {
		el = $.id(el);
		if(el && el.parentNode) {
			el.parentNode.removeChild(el);
		}
	}

	$.cursorPos = function(ev, el) {
		var offset = $.pos(el);
		offset.top = ev.clientY - offset.top + (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
		offset.left = ev.clientX - offset.left + (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft);
		return offset;
	}

	$.pos = function(el) {
		var parent = el;
		var offset = {top: 0, left: 0};
		while(parent && parent != document.body) {
			offset.left += parent.offsetLeft;
			offset.top += parent.offsetTop;
			while(parent.offsetParent != parent.parentNode) {
				offset.top -= parent.scrollTop; offset.left -= parent.scrollLeft;
				parent = parent.parentNode;
			}
			parent = parent.offsetParent;
		}
		return offset;
	}

	$.size = function(el) {
		if ($.isElement(el)) {
			return {width: el.offsetWidth, height: el.offsetHeight};
		} else if (el.document) {
			var doc = el.document.documentElement || el.document.body;
			return new math2D.Rect(
				doc.offsetTop,
				doc.offsetLeft,
				el.innerWidth || (doc.clientWidth || doc.clientWidth),
				el.innerHeight || (doc.clientHeight || doc.clientHeight)
			);
		}
	}

	$.insertCSSFile = function(filename) {
		document.getElementsByTagName('head')[0].appendChild($({
			tag: 'link',
			attrs: {
				rel: 'stylesheet',
				type: 'text/css',
				href: filename
			}
		}));
	}
}
<|endoftext|>


<commit_before>/* Copyright 2019 Google Inc. All Rights Reserved.

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.
==============================================================================*/

import {ServerLoadStrategy} from './interfaces.js';
import {delay} from '../../lib/promise.js';
import {PromiseCache} from '../../lib/promise_cache.js';

import sharp from 'sharp';

export default function({debug}) {
  // LOAD FROM DRIVE STRATEGY
  // Here, we specify the server & client strategies that can load images from a
  // drive folder passed in the config. The drive folder should be shared
  // publicly or with the appropriate credentials.
  // TODO(applmak): Make the server-side filter out things the client can't
  // display.
  // TODO(applmak): Maybe make the server-side smarter about subfolders so as to
  // create collections that should play, rather than needing to change the config
  // every time.
  // Config:
  //   folderId: string - Drive folder ID from which to retrieve files.
  //   fileId: string - Drive file ID that is the file to download.
  //       Can't be specified with folderId.
  class LoadFromDriveServerStrategy extends ServerLoadStrategy {
    constructor(config) {
      super();
      this.config = config;

      // Drive client API.
      this.driveClient = null;

      // In-flight cache: A cache for metadata in-flight.
      this.inflightCache = new PromiseCache;

      // In-flight content cache: A cache for content in-flight. Note that as
      // soon as the data is downloaded, it's removed from this cache.
      this.inflightContent = new Map;
    }
    async init() {
      const {getAuthenticatedClient} = await import('../../server/util/googleapis.js');
      // Get an authenticated API. When init's promise is resolved, we succeeded.
      const client = await getAuthenticatedClient();
      debug('Initialized Drive Client.');
      // TODO(applmak): Don't assign credentials to the config, which is
      // readable on the status page!
      this.config.credentials = client.credentials;
      this.driveClient = client.googleapis.drive('v3');
    }
    async loadMoreContent(opt_paginationToken) {
      let response;
      if (this.config.folderId) {
        try {
          response = await this.driveClient.files.get({
            q: `"${this.config.folderId} in parents"`,
            maxResults: 1000,
            pageToken: opt_paginationToken
          });
        } catch (e) {
          debug('Failed to download more drive content! Delay a bit...');
          await delay(Math.random() * 4000 + 1000);
          return this.loadMoreContent(opt_paginationToken);
        }

        debug('Downloaded ' + response.data.items.length + ' more content ids.');
        return {
          content: response.data.items.map(i => ({fileId: i.id})),
          paginationToken: response.data.nextPageToken
        };
      } else if (this.config.fileId) {
        return {
          content: [{fileId: this.config.fileId}],
        };
      } else {
        throw new Error('Module does not specify how to load the items');
      }
    }
    async fetchFullContent(fileId) {
      debug(`Downloading media for ${fileId}`);
      const image = await this.driveClient.files.get({
        fileId,
        alt: 'media',
      }, {
        responseType: 'arraybuffer',
      });
      const {data} = image;
      debug(`Downloaded media for ${fileId}: ${data.byteLength}`);
      // Create a "buffer" view on data (an arraybuffer), so that sharp is happy.
      return Buffer.from(data);
    }
    async downloadFullContent(content, cache) {
      const {fileId} = content;

      // Check if we already have it cached.
      if (cache.has(fileId)) {
        debug(`Full content for ${fileId} is cached`);
        return cache.get(fileId);
      }
      // Check if we are already nabbing the original image.
      if (this.inflightContent.has(fileId)) {
        debug(`Full content for ${fileId} is being downloaded`);
        return await this.inflightContent.get(fileId);
      }
      // Try downloading the whole kit 'n' kaboodle.
      const promise = this.fetchFullContent(fileId);
      this.inflightContent.set(fileId, promise);
      const array = await promise;
      // Cache the image into the cache so we don't have to look this up again.
      cache.set(fileId, array);
      // Remove the promise from our cache so we don't retain this data.
      this.inflightContent.delete(fileId);
      return array;
    }
    async clipImage(content, clippingRect, cache) {
      const image = await this.downloadFullContent(content, cache);
      return await sharp(image)
        .extract({left: clippingRect.x, top: clippingRect.y, width: clippingRect.w, height: clippingRect.h})
        .png()
        .toBuffer();
    }
    async downloadContent(content, clippingRect, cache) {
      if (!clippingRect) {
        debug(`No clipping rect specified ${content.fileId}`);
        return await this.downloadFullContent(content, cache);
      }

      const {fileId} = content;
      const key = `${fileId} ${clippingRect.serialize()}`;

      // Check if we already have it cached.
      if (cache.has(key)) {
        debug(`Clipping region ${clippingRect.serialize()} for ${fileId} was cached`);
        return cache.get(key);
      }
      // Check if we are already creating a cropped form of this.
      if (this.inflightContent.has(key)) {
        debug(`Clipping region ${clippingRect.serialize()} for ${fileId} is being computed now`);
        return await this.inflightContent.get(key);
      }

      debug(`Clipping region ${clippingRect.serialize()} for ${fileId}`);
      const promise = this.clipImage(content, clippingRect, cache);
      this.inflightContent.set(key, promise);
      const clippedImage = await promise;
      debug(`Clipping region ${clippingRect.serialize()} for ${fileId} complete`);

      // Cache the cropped image into the cache so we don't have to look this up again.
      cache.set(key, clippedImage);

      // Remove the promise from our cache so we don't retain this data.
      this.inflightContent.delete(key);
      return clippedImage;
    }
    async fetchMetadata(content, cache) {
      const image = await this.downloadFullContent(content, cache);
      return await sharp(image).metadata();
    }
    async metadataForContent(content, cache) {
      if (!this.config.split) {
        // If we aren't set to split, we don't do any metadata fetching.
        return null;
      }

      const {inflightCache} = this;
      const {fileId} = content;
      // Maybe the caches have this?
      if (inflightCache.has(fileId)) {
        const metadata = inflightCache.get(fileId);
        debug(`Cached metadata for ${fileId}: ${JSON.stringify(metadata)}`);
        return metadata;
      }

      if (inflightCache.hasAsync(fileId)) {
        debug(`Cached metadata for ${fileId} is being fetched.`);
        return await inflightCache.getAsync(fileId);
      }

      debug(`Downloading media ${fileId} in order to calculate metadata...`);
      const promise = this.fetchMetadata(content, cache);
      inflightCache.setAsync(fileId, promise);
      const metadata = await promise;
      debug(`Calculated metadata for ${fileId}: ${metadata.width} x ${metadata.height}`);
      return inflightCache.get(fileId);
    }
    serializeForClient() {
      return {drive: this.config};
    }
  }

  return {
    Server: LoadFromDriveServerStrategy,
  };
}
<commit_msg>Fix the folder-content loading in slideshow.<commit_after>/* Copyright 2019 Google Inc. All Rights Reserved.

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.
==============================================================================*/

import {ServerLoadStrategy} from './interfaces.js';
import {delay} from '../../lib/promise.js';
import {PromiseCache} from '../../lib/promise_cache.js';

import sharp from 'sharp';

export default function({debug}) {
  // LOAD FROM DRIVE STRATEGY
  // Here, we specify the server & client strategies that can load images from a
  // drive folder passed in the config. The drive folder should be shared
  // publicly or with the appropriate credentials.
  // TODO(applmak): Make the server-side filter out things the client can't
  // display.
  // TODO(applmak): Maybe make the server-side smarter about subfolders so as to
  // create collections that should play, rather than needing to change the config
  // every time.
  // Config:
  //   folderId: string - Drive folder ID from which to retrieve files.
  //   fileId: string - Drive file ID that is the file to download.
  //       Can't be specified with folderId.
  class LoadFromDriveServerStrategy extends ServerLoadStrategy {
    constructor(config) {
      super();
      this.config = config;

      // Drive client API.
      this.driveClient = null;

      // In-flight cache: A cache for metadata in-flight.
      this.inflightCache = new PromiseCache;

      // In-flight content cache: A cache for content in-flight. Note that as
      // soon as the data is downloaded, it's removed from this cache.
      this.inflightContent = new Map;
    }
    async init() {
      const {getAuthenticatedClient} = await import('../../server/util/googleapis.js');
      // Get an authenticated API. When init's promise is resolved, we succeeded.
      const client = await getAuthenticatedClient();
      debug('Initialized Drive Client.');
      // TODO(applmak): Don't assign credentials to the config, which is
      // readable on the status page!
      this.config.credentials = client.credentials;
      this.driveClient = client.googleapis.drive('v3');
    }
    async loadMoreContent(opt_paginationToken) {
      let response;
      if (this.config.folderId) {
        try {
          response = await this.driveClient.files.list({
            q: `'${this.config.folderId}' in parents`,
            maxResults: 1000,
            pageToken: opt_paginationToken
          });
        } catch (e) {
          debug('Failed to download more drive content! Delay a bit...');
          debug.error(e);
          await delay(Math.random() * 4000 + 1000);
          return this.loadMoreContent(opt_paginationToken);
        }

        debug('Downloaded ' + response.data.files.length + ' more content ids.');
        return {
          content: response.data.files.map(i => ({fileId: i.id})),
          paginationToken: response.data.nextPageToken
        };
      } else if (this.config.fileId) {
        return {
          content: [{fileId: this.config.fileId}],
        };
      } else {
        throw new Error('Module does not specify how to load the items');
      }
    }
    async fetchFullContent(fileId) {
      debug(`Downloading media for ${fileId}`);
      const image = await this.driveClient.files.get({
        fileId,
        alt: 'media',
      }, {
        responseType: 'arraybuffer',
      });
      const {data} = image;
      debug(`Downloaded media for ${fileId}: ${data.byteLength}`);
      // Create a "buffer" view on data (an arraybuffer), so that sharp is happy.
      return Buffer.from(data);
    }
    async downloadFullContent(content, cache) {
      const {fileId} = content;

      // Check if we already have it cached.
      if (cache.has(fileId)) {
        debug(`Full content for ${fileId} is cached`);
        return cache.get(fileId);
      }
      // Check if we are already nabbing the original image.
      if (this.inflightContent.has(fileId)) {
        debug(`Full content for ${fileId} is being downloaded`);
        return await this.inflightContent.get(fileId);
      }
      // Try downloading the whole kit 'n' kaboodle.
      const promise = this.fetchFullContent(fileId);
      this.inflightContent.set(fileId, promise);
      const array = await promise;
      // Cache the image into the cache so we don't have to look this up again.
      cache.set(fileId, array);
      // Remove the promise from our cache so we don't retain this data.
      this.inflightContent.delete(fileId);
      return array;
    }
    async clipImage(content, clippingRect, cache) {
      const image = await this.downloadFullContent(content, cache);
      return await sharp(image)
        .extract({left: clippingRect.x, top: clippingRect.y, width: clippingRect.w, height: clippingRect.h})
        .png()
        .toBuffer();
    }
    async downloadContent(content, clippingRect, cache) {
      if (!clippingRect) {
        debug(`No clipping rect specified ${content.fileId}`);
        return await this.downloadFullContent(content, cache);
      }

      const {fileId} = content;
      const key = `${fileId} ${clippingRect.serialize()}`;

      // Check if we already have it cached.
      if (cache.has(key)) {
        debug(`Clipping region ${clippingRect.serialize()} for ${fileId} was cached`);
        return cache.get(key);
      }
      // Check if we are already creating a cropped form of this.
      if (this.inflightContent.has(key)) {
        debug(`Clipping region ${clippingRect.serialize()} for ${fileId} is being computed now`);
        return await this.inflightContent.get(key);
      }

      debug(`Clipping region ${clippingRect.serialize()} for ${fileId}`);
      const promise = this.clipImage(content, clippingRect, cache);
      this.inflightContent.set(key, promise);
      const clippedImage = await promise;
      debug(`Clipping region ${clippingRect.serialize()} for ${fileId} complete`);

      // Cache the cropped image into the cache so we don't have to look this up again.
      cache.set(key, clippedImage);

      // Remove the promise from our cache so we don't retain this data.
      this.inflightContent.delete(key);
      return clippedImage;
    }
    async fetchMetadata(content, cache) {
      const image = await this.downloadFullContent(content, cache);
      return await sharp(image).metadata();
    }
    async metadataForContent(content, cache) {
      if (!this.config.split) {
        // If we aren't set to split, we don't do any metadata fetching.
        return null;
      }

      const {inflightCache} = this;
      const {fileId} = content;
      // Maybe the caches have this?
      if (inflightCache.has(fileId)) {
        const metadata = inflightCache.get(fileId);
        debug(`Cached metadata for ${fileId}: ${JSON.stringify(metadata)}`);
        return metadata;
      }

      if (inflightCache.hasAsync(fileId)) {
        debug(`Cached metadata for ${fileId} is being fetched.`);
        return await inflightCache.getAsync(fileId);
      }

      debug(`Downloading media ${fileId} in order to calculate metadata...`);
      const promise = this.fetchMetadata(content, cache);
      inflightCache.setAsync(fileId, promise);
      const metadata = await promise;
      debug(`Calculated metadata for ${fileId}: ${metadata.width} x ${metadata.height}`);
      return inflightCache.get(fileId);
    }
    serializeForClient() {
      return {drive: this.config};
    }
  }

  return {
    Server: LoadFromDriveServerStrategy,
  };
}
<|endoftext|>


<commit_before>const jsonapi = require('@cardstack/jsonapi/middleware');
const supertest = require('supertest');
const Koa = require('koa');
const {
  createDefaultEnvironment,
  destroyDefaultEnvironment
} = require('@cardstack/server/tests/support');

describe('jsonapi', function() {

  let request, env;

  beforeEach(async function() {
    env = await createDefaultEnvironment([
      {
        type: 'content-types',
        id: 'articles',
        relationships: {
          fields: {
            data: [
              { type: 'fields', id: 'title' },
              { type: 'fields', id: 'body' }
            ]
          },
          'data-source': {
            data: { type: 'data-sources', id: 'default-git' }
          }
        }
      },
      {
        type: 'fields',
        id: 'title',
        attributes: {
          'field-type': 'string'
        }
      },
      {
        type: 'fields',
        id: 'body',
        attributes: {
          'field-type': 'string'
        }
      },
      {
        type: 'articles',
        id: '0',
        attributes: {
          title: "Hello world",
          body: "This is the first article"
        }
      },
      {
        type: 'articles',
        id: '1',
        attributes: {
          title: "Second",
          body: "This is the second article"
        }
      },
      {
        type: 'comments',
        id: '0',
        attributes: {
          body: 'This is a comment'
        }
      }
    ]);
    let app = new Koa();
    app.use(jsonapi(env.searcher, env.schemaCache));
    request = supertest(app.callback());
  }),

  afterEach(async function() {
    await destroyDefaultEnvironment(env);
  }),

  it('can get an individual resource', async function() {
    let response = await request.get('/articles/0');
    expect(response).to.have.property('status', 200);
    expect(response.body).deep.property('data.id', '0');
    expect(response.body).deep.property('data.attributes.title', 'Hello world');
    expect(response.body).not.deep.property('data.relationships');
  });

  it('returns 404 for missing individual resource', async function() {
    let response = await request.get('/articles/98766');
    expect(response).to.have.property('status', 404);
    expect(response.body).to.have.deep.property('errors[0].detail', 'No such resource /articles/98766');
  });

  it('can get a collection resource', async function() {
    let response = await request.get('/articles');
    expect(response).to.have.property('status', 200);
    expect(response.body).to.have.property('data');
    expect(response.body).to.have.deep.property('meta.total', 2);
    expect(response.body.data).length(2);
    expect(response.body.data).collectionContains({ type: 'articles', id: '0' });
    expect(response.body.data).collectionContains({ type: 'articles', id: '1' });
  });

  it('can sort a collection resource', async function() {
    let response = await request.get('/articles?sort=title');
    expect(response).to.have.property('status', 200);
    expect(response.body).to.have.property('data');
    expect(response.body).has.deep.property('data[0].attributes.title', 'Hello world');
    expect(response.body).has.deep.property('data[1].attributes.title', 'Second');
  });

  it('can reverse sort a collection resource', async function() {
    let response = await request.get('/articles?sort=-title');
    expect(response).has.property('status', 200);
    expect(response.body).has.property('data');
    expect(response.body).has.deep.property('data[1].attributes.title', 'Hello world');
    expect(response.body).has.deep.property('data[0].attributes.title', 'Second');
  });

  it('can filter a collection resource', async function() {
    let response = await request.get('/articles?filter[title]=world');
    expect(response).has.property('status', 200);
    expect(response.body).has.property('data');
    expect(response.body.data).length(1);
    expect(response.body).has.deep.property('data[0].attributes.title', 'Hello world');
  });

  it('can paginate a collection resource', async function() {
    let response = await request.get('/articles?page[size]=1&sort=title');
    expect(response).has.property('status', 200, 'first request');
    expect(response.body.data).length(1);
    expect(response.body).has.deep.property('data[0].attributes.title', 'Hello world');
    expect(response.body).has.deep.property('links.next');

    response = await request.get(response.body.links.next);
    expect(response).has.property('status', 200, 'second request');
    expect(response.body).has.deep.property('data[0].attributes.title', 'Second');
    expect(response.body.data).length(1);
  });

  it('gets 403 when creating unknown resource', async function() {
    let response = await request.post('/bogus').send({
      data: {
        type: 'bogus',
        attributes: {
          title: 'I am new'
        }
      }
    });
    expect(response.status).to.equal(403);
    expect(response.body).has.deep.property('errors[0].detail', '"bogus" is not a writable type');
  });

  it('gets 400 when creating a resource with no body', async function() {
    let response = await request.post('/articles');
    expect(response.status).to.equal(400);
    expect(response.body).has.deep.property('errors[0].detail', 'A body with a top-level "data" property is required');
  });

  it('gets 400 when creating a resource with no data property', async function() {
    let response = await request.post('/articles').send({datum: {}});
    expect(response.status).to.equal(400);
    expect(response.body).has.deep.property('errors[0].detail', 'A body with a top-level "data" property is required');
  });

  it('can create a new resource', async function() {
    let response = await request.post('/articles').send({
      data: {
        type: 'articles',
        attributes: {
          title: 'I am new'
        }
      }
    });

    expect(response).has.property('status', 201);
    expect(response.headers).has.property('location');
    expect(response.body).has.deep.property('data.id');
    expect(response.body).has.deep.property('data.attributes.title', 'I am new');
    expect(response.body).has.deep.property('data.meta.version');

    await env.indexer.update({ realTime: true });

    response = await request.get(response.headers.location);
    expect(response).has.property('status', 200);
    expect(response.body).has.deep.property('data.attributes.title', 'I am new', 'second time');

  });

  it.skip('can update an existing resource', async function() {
    let response = await request.get('/articles/0');
    expect(response).has.property('status', 200);
    expect(response).has.deep.property('body.data.meta.version');
    let { version } = response.body.data.meta;

    response = await request.patch('/articles/0').send({
      data: {
        id: '0',
        type: 'articles',
        attributes: {
          title: 'Updated title'
        },
        meta: { version }
      }
    });

    expect(response).has.property('status', 200);
    expect(response).has.deep.property('body.data.attributes.title', 'Updated title');
    expect(response).has.deep.property('body.data.attributes.body', "This is the first article");

    await env.indexer.update({ realTime: true });

    response = await request.get('/articles/0');
    expect(response).has.property('status', 200);
    expect(response).has.deep.property('body.data.attributes.title', 'Updated title', 'second time');
    expect(response).has.deep.property('body.data.attributes.body', "This is the first article", 'second time');

  });

});
<commit_msg>working middleware PATCH<commit_after>const jsonapi = require('@cardstack/jsonapi/middleware');
const supertest = require('supertest');
const Koa = require('koa');
const {
  createDefaultEnvironment,
  destroyDefaultEnvironment
} = require('@cardstack/server/tests/support');

describe('jsonapi', function() {

  let request, env;

  beforeEach(async function() {
    env = await createDefaultEnvironment([
      {
        type: 'content-types',
        id: 'articles',
        relationships: {
          fields: {
            data: [
              { type: 'fields', id: 'title' },
              { type: 'fields', id: 'body' }
            ]
          },
          'data-source': {
            data: { type: 'data-sources', id: 'default-git' }
          }
        }
      },
      {
        type: 'fields',
        id: 'title',
        attributes: {
          'field-type': 'string'
        }
      },
      {
        type: 'fields',
        id: 'body',
        attributes: {
          'field-type': 'string'
        }
      },
      {
        type: 'articles',
        id: '0',
        attributes: {
          title: "Hello world",
          body: "This is the first article"
        }
      },
      {
        type: 'articles',
        id: '1',
        attributes: {
          title: "Second",
          body: "This is the second article"
        }
      },
      {
        type: 'comments',
        id: '0',
        attributes: {
          body: 'This is a comment'
        }
      }
    ]);
    let app = new Koa();
    app.use(jsonapi(env.searcher, env.schemaCache));
    request = supertest(app.callback());
  }),

  afterEach(async function() {
    await destroyDefaultEnvironment(env);
  }),

  it('can get an individual resource', async function() {
    let response = await request.get('/articles/0');
    expect(response).to.have.property('status', 200);
    expect(response.body).deep.property('data.id', '0');
    expect(response.body).deep.property('data.attributes.title', 'Hello world');
    expect(response.body).not.deep.property('data.relationships');
  });

  it('returns 404 for missing individual resource', async function() {
    let response = await request.get('/articles/98766');
    expect(response).to.have.property('status', 404);
    expect(response.body).to.have.deep.property('errors[0].detail', 'No such resource /articles/98766');
  });

  it('can get a collection resource', async function() {
    let response = await request.get('/articles');
    expect(response).to.have.property('status', 200);
    expect(response.body).to.have.property('data');
    expect(response.body).to.have.deep.property('meta.total', 2);
    expect(response.body.data).length(2);
    expect(response.body.data).collectionContains({ type: 'articles', id: '0' });
    expect(response.body.data).collectionContains({ type: 'articles', id: '1' });
  });

  it('can sort a collection resource', async function() {
    let response = await request.get('/articles?sort=title');
    expect(response).to.have.property('status', 200);
    expect(response.body).to.have.property('data');
    expect(response.body).has.deep.property('data[0].attributes.title', 'Hello world');
    expect(response.body).has.deep.property('data[1].attributes.title', 'Second');
  });

  it('can reverse sort a collection resource', async function() {
    let response = await request.get('/articles?sort=-title');
    expect(response).has.property('status', 200);
    expect(response.body).has.property('data');
    expect(response.body).has.deep.property('data[1].attributes.title', 'Hello world');
    expect(response.body).has.deep.property('data[0].attributes.title', 'Second');
  });

  it('can filter a collection resource', async function() {
    let response = await request.get('/articles?filter[title]=world');
    expect(response).has.property('status', 200);
    expect(response.body).has.property('data');
    expect(response.body.data).length(1);
    expect(response.body).has.deep.property('data[0].attributes.title', 'Hello world');
  });

  it('can paginate a collection resource', async function() {
    let response = await request.get('/articles?page[size]=1&sort=title');
    expect(response).has.property('status', 200, 'first request');
    expect(response.body.data).length(1);
    expect(response.body).has.deep.property('data[0].attributes.title', 'Hello world');
    expect(response.body).has.deep.property('links.next');

    response = await request.get(response.body.links.next);
    expect(response).has.property('status', 200, 'second request');
    expect(response.body).has.deep.property('data[0].attributes.title', 'Second');
    expect(response.body.data).length(1);
  });

  it('gets 403 when creating unknown resource', async function() {
    let response = await request.post('/bogus').send({
      data: {
        type: 'bogus',
        attributes: {
          title: 'I am new'
        }
      }
    });
    expect(response.status).to.equal(403);
    expect(response.body).has.deep.property('errors[0].detail', '"bogus" is not a writable type');
  });

  it('gets 400 when creating a resource with no body', async function() {
    let response = await request.post('/articles');
    expect(response.status).to.equal(400);
    expect(response.body).has.deep.property('errors[0].detail', 'A body with a top-level "data" property is required');
  });

  it('gets 400 when creating a resource with no data property', async function() {
    let response = await request.post('/articles').send({datum: {}});
    expect(response.status).to.equal(400);
    expect(response.body).has.deep.property('errors[0].detail', 'A body with a top-level "data" property is required');
  });

  it('can create a new resource', async function() {
    let response = await request.post('/articles').send({
      data: {
        type: 'articles',
        attributes: {
          title: 'I am new'
        }
      }
    });

    expect(response).has.property('status', 201);
    expect(response.headers).has.property('location');
    expect(response.body).has.deep.property('data.id');
    expect(response.body).has.deep.property('data.attributes.title', 'I am new');
    expect(response.body).has.deep.property('data.meta.version');

    await env.indexer.update({ realTime: true });

    response = await request.get(response.headers.location);
    expect(response).has.property('status', 200);
    expect(response.body).has.deep.property('data.attributes.title', 'I am new', 'second time');

  });

  it('can update an existing resource', async function() {
    let response = await request.get('/articles/0');
    expect(response).has.property('status', 200);
    expect(response).has.deep.property('body.data.meta.version');
    let { version } = response.body.data.meta;

    response = await request.patch('/articles/0').send({
      data: {
        id: '0',
        type: 'articles',
        attributes: {
          title: 'Updated title'
        },
        meta: { version }
      }
    });

    expect(response).has.property('status', 200);
    expect(response).has.deep.property('body.data.attributes.title', 'Updated title');
    expect(response).has.deep.property('body.data.attributes.body', "This is the first article");

    await env.indexer.update({ realTime: true });

    response = await request.get('/articles/0');
    expect(response).has.property('status', 200);
    expect(response).has.deep.property('body.data.attributes.title', 'Updated title', 'second time');
    expect(response).has.deep.property('body.data.attributes.body', "This is the first article", 'second time');

  });

});
<|endoftext|>


<commit_before>/**
 * Plugin registry of available functions that can be used in scalable layout directives.
 *
 * These "scale functions" are used during rendering to return output (eg color) based on input value
 *
 * @module LocusZoom_ScaleFunctions
 * @see {@link module:LocusZoom_DataLayers~ScalableParameter} for details on how scale functions are used by datalayers
 */

import * as d3 from 'd3';

/**
 * Basic conditional function to evaluate the value of the input field and return based on equality.
 * @alias module:LocusZoom_ScaleFunctions~if
 * @param {Object} parameters
 * @param {*} parameters.field_value The value against which to test the input value.
 * @param {*} parameters.then The value to return if the input value matches the field value
 * @param {*} parameters.else  The value to return if the input value does not match the field value. Optional. If not
 *   defined this scale function will return null (or value of null_value parameter, if defined) when input value fails
 *   to match field_value.
 * @param {*} value value
 */
const if_value = (parameters, value) => {
    if (typeof value == 'undefined' || parameters.field_value !== value) {
        if (typeof parameters.else != 'undefined') {
            return parameters.else;
        } else {
            return null;
        }
    } else {
        return parameters.then;
    }
};

/**
 * Function to sort numerical values into bins based on numerical break points. Will only operate on numbers and
 *   return null (or value of null_value parameter, if defined) if provided a non-numeric input value. Parameters:
 * @function numerical_bin
 * @param {Object} parameters
 * @param {Number[]} parameters.breaks  Array of numerical break points against which to evaluate the input value.
 *   Must be of equal length to values parameter. If the input value is greater than or equal to break n and less than
 *   or equal to break n+1 (or break n+1 doesn't exist) then returned value is the nth entry in the values parameter.
 * @param {Array} parameters.values  Array of values to return given evaluations against break points. Must be of
 *   equal length to breaks parameter. Each entry n represents the value to return if the input value is greater than
 *   or equal to break n and less than or equal to break n+1 (or break n+1 doesn't exist).
 * @param {*} parameters.null_value
 * @param {*} value value
 * @returns {*}
 */
const numerical_bin = (parameters, value) => {
    const breaks = parameters.breaks || [];
    const values = parameters.values || [];
    if (typeof value == 'undefined' || value === null || isNaN(+value)) {
        return (parameters.null_value ? parameters.null_value : null);
    }
    const threshold = breaks.reduce(function (prev, curr) {
        if (+value < prev || (+value >= prev && +value < curr)) {
            return prev;
        } else {
            return curr;
        }
    });
    return values[breaks.indexOf(threshold)];
};

/**
 * Function to sort values of any type into bins based on direct equality testing with a list of categories.
 *   Will return null if provided an input value that does not match to a listed category.
 * @function categorical_bin
 * @param {Object} parameters
 * @param {Array} parameters.categories  Array of values against which to evaluate the input value. Must be of equal
 *   length to values parameter. If the input value is equal to category n then returned value is the nth entry in the
 *   values parameter.
 * @param {Array} parameters.values  Array of values to return given evaluations against categories. Must be of equal
 *   length to categories parameter. Each entry n represents the value to return if the input value is equal to the nth
 *   value in the categories parameter.
 * @param {*} parameters.null_value  Value to return if the input value fails to match to any categories. Optional.
 */
const categorical_bin = (parameters, value) => {
    if (typeof value == 'undefined' || !parameters.categories.includes(value)) {
        return (parameters.null_value ? parameters.null_value : null);
    } else {
        return parameters.values[parameters.categories.indexOf(value)];
    }
};

/**
 * Cycle through a set of options, so that the each element in a set of data receives a value different than the
 *  element before it. For example: "use this palette of 10 colors to visually distinguish 100 adjacent items"
 * This is useful when ADJACENT items must be guaranteed to yield a different result, but it leads to unstable color
 *  choices if the user pans to a region with a different number/order of items. (the same item is assigned a different color)
 *
 *  See also: stable_choice.
 * @function ordinal_cycle
 *  @param {Object} parameters
 *  @param {Array} parameters.values A list of option values
 * @return {*}
 */
const ordinal_cycle = (parameters, value, index) => {
    const options = parameters.values;
    return options[index % options.length];
};

/**
 * A scale function that auto-chooses something (like color) from a preset scheme, and makes the same choice every
 * time given the same value, regardless of ordering or what other data is in the region
 *
 * This is useful when categories must be stable (same color, every time). But sometimes it will assign adjacent values
 *  the same color due to hash collisions.
 *
 * For performance reasons, this is memoized once per instance. Eg, each scalable color parameter has its own cache.
 *  This function is therefore slightly less amenable to layout mutations like "changing the options after scaling
 *  function is used", but this is not expected to be a common use case.
 *
 *  CAVEAT: Some datasets do not return true datum ids, but instead append synthetic ID fields ("item 1, item2"...)
 *    just to appease D3. This hash function only works if there is a meaningful, stable identifier in the data,
 *    like a category or gene name.
 *
 * @function stable_choice
 *
 * @param parameters
 * @param {Array} [parameters.values] A list of options to choose from
 * @param {Number} [parameters.max_cache_size=500] The maximum number of values to cache. This option is mostly used
 *  for unit testing, because stable choice is intended for datasets with a relatively limited number of
 *  discrete categories.
 * @param value
 * @param index
 */
let stable_choice = (parameters, value, index) => {
    // Each place the function gets used has its own parameters object. This function thus memoizes per usage
    //  ("association - point color - directive 1") rather than globally ("all properties/panels")
    const cache = parameters._cache = parameters._cache || new Map();
    const max_cache_size = parameters.max_cache_size || 500;

    if (cache.size >= max_cache_size) {
        // Prevent cache from growing out of control (eg as user moves between regions a lot)
        cache.clear();
    }
    if (cache.has(value)) {
        return cache.get(value);
    }

    // Simple JS hashcode implementation, from:
    //  https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
    let hash = 0;
    value = String(value);
    for (let i = 0; i < value.length; i++) {
        let chr = value.charCodeAt(i);
        hash  = ((hash << 5) - hash) + chr;
        hash |= 0; // Convert to 32bit integer
    }
    // Convert signed 32 bit integer to be within the range of options allowed
    const options = parameters.values;
    const result = options[Math.abs(hash) % options.length];
    cache.set(value, result);
    return result;
};

/**
 * Function for continuous interpolation of numerical values along a gradient with arbitrarily many break points.
 * @function interpolate
 * @parameters {Object} parameters
 * @parameters {Number[]} parameters.breaks  Array of numerical break points against which to evaluate the input value.
 *   Must be of equal length to values parameter and contain at least two elements. Input value will be evaluated for
 *   relative position between two break points n and n+1 and the returned value will be interpolated at a relative
 *   position between values n and n+1.
 * @parameters {*[]} parameters.values  Array of values to interpolate and return given evaluations against break
 *   points. Must be of equal length to breaks parameter and contain at least two elements. Each entry n represents
 *   the value to return if the input value matches the nth entry in breaks exactly. Note that this scale function
 *   uses d3.interpolate to provide for effective interpolation of many different value types, including numbers,
 *   colors, shapes, etc.
 * @parameters {*} parameters.null_value
 */
const interpolate = (parameters, value) => {
    var breaks = parameters.breaks || [];
    var values = parameters.values || [];
    var nullval = (parameters.null_value ? parameters.null_value : null);
    if (breaks.length < 2 || breaks.length !== values.length) {
        return nullval;
    }
    if (typeof value == 'undefined' || value === null || isNaN(+value)) {
        return nullval;
    }
    if (+value <= parameters.breaks[0]) {
        return values[0];
    } else if (+value >= parameters.breaks[parameters.breaks.length - 1]) {
        return values[breaks.length - 1];
    } else {
        var upper_idx = null;
        breaks.forEach(function (brk, idx) {
            if (!idx) {
                return;
            }
            if (breaks[idx - 1] <= +value && breaks[idx] >= +value) {
                upper_idx = idx;
            }
        });
        if (upper_idx === null) {
            return nullval;
        }
        const normalized_input = (+value - breaks[upper_idx - 1]) / (breaks[upper_idx] - breaks[upper_idx - 1]);
        if (!isFinite(normalized_input)) {
            return nullval;
        }
        return d3.interpolate(values[upper_idx - 1], values[upper_idx])(normalized_input);
    }
};

/**
 * Calculate the effect direction based on beta, or the combination of beta and standard error.
 *   Typically used with phewas plots, to show point shape based on the beta and stderr_beta fields.
 *
 * @function effect_direction
 * @param parameters
 * @param parameters.'+' The value to return if the effect direction is positive
 * @param parameters.'-' The value to return if the effect direction is positive
 * @param parameters.beta_field The name of the field containing beta
 * @param parameters.stderr_beta_field The name of the field containing stderr_beta
 * @param {Object} input This function should receive the entire datum object, rather than one single field
 * @returns {null}
 */
function effect_direction(parameters, input) {
    if (input === undefined) {
        return null;
    }

    const { beta_field, stderr_beta_field, '+': plus_result = null, '-': neg_result = null } = parameters;

    if (!beta_field || !stderr_beta_field) {
        throw new Error(`effect_direction must specify how to find required 'beta' and 'stderr_beta' fields`);
    }

    const beta_val = input[beta_field];
    const se_val = input[stderr_beta_field];

    if (beta_val !== undefined) {
        if (se_val !== undefined) {
            if ((beta_val - 2 * se_val) > 0) {
                return plus_result;
            } else if ((beta_val + 2 * se_val) < 0) {
                return neg_result || null;
            }
        } else {
            if (beta_val > 0) {
                return plus_result;
            } else if (beta_val < 0) {
                return neg_result;
            }
        }
    }
    // Note: The original PheWeb implementation allowed odds ratio in place of beta/se. LZ core is a bit more rigid
    //   about expected data formats for layouts.
    return null;
}

export { categorical_bin, stable_choice, if_value, interpolate, numerical_bin, ordinal_cycle, effect_direction };
<commit_msg>Adjust effect direction scaling function for equivalent of a low significance threshold (p~0.05 per indiv snp)<commit_after>/**
 * Plugin registry of available functions that can be used in scalable layout directives.
 *
 * These "scale functions" are used during rendering to return output (eg color) based on input value
 *
 * @module LocusZoom_ScaleFunctions
 * @see {@link module:LocusZoom_DataLayers~ScalableParameter} for details on how scale functions are used by datalayers
 */

import * as d3 from 'd3';

/**
 * Basic conditional function to evaluate the value of the input field and return based on equality.
 * @alias module:LocusZoom_ScaleFunctions~if
 * @param {Object} parameters
 * @param {*} parameters.field_value The value against which to test the input value.
 * @param {*} parameters.then The value to return if the input value matches the field value
 * @param {*} parameters.else  The value to return if the input value does not match the field value. Optional. If not
 *   defined this scale function will return null (or value of null_value parameter, if defined) when input value fails
 *   to match field_value.
 * @param {*} value value
 */
const if_value = (parameters, value) => {
    if (typeof value == 'undefined' || parameters.field_value !== value) {
        if (typeof parameters.else != 'undefined') {
            return parameters.else;
        } else {
            return null;
        }
    } else {
        return parameters.then;
    }
};

/**
 * Function to sort numerical values into bins based on numerical break points. Will only operate on numbers and
 *   return null (or value of null_value parameter, if defined) if provided a non-numeric input value. Parameters:
 * @function numerical_bin
 * @param {Object} parameters
 * @param {Number[]} parameters.breaks  Array of numerical break points against which to evaluate the input value.
 *   Must be of equal length to values parameter. If the input value is greater than or equal to break n and less than
 *   or equal to break n+1 (or break n+1 doesn't exist) then returned value is the nth entry in the values parameter.
 * @param {Array} parameters.values  Array of values to return given evaluations against break points. Must be of
 *   equal length to breaks parameter. Each entry n represents the value to return if the input value is greater than
 *   or equal to break n and less than or equal to break n+1 (or break n+1 doesn't exist).
 * @param {*} parameters.null_value
 * @param {*} value value
 * @returns {*}
 */
const numerical_bin = (parameters, value) => {
    const breaks = parameters.breaks || [];
    const values = parameters.values || [];
    if (typeof value == 'undefined' || value === null || isNaN(+value)) {
        return (parameters.null_value ? parameters.null_value : null);
    }
    const threshold = breaks.reduce(function (prev, curr) {
        if (+value < prev || (+value >= prev && +value < curr)) {
            return prev;
        } else {
            return curr;
        }
    });
    return values[breaks.indexOf(threshold)];
};

/**
 * Function to sort values of any type into bins based on direct equality testing with a list of categories.
 *   Will return null if provided an input value that does not match to a listed category.
 * @function categorical_bin
 * @param {Object} parameters
 * @param {Array} parameters.categories  Array of values against which to evaluate the input value. Must be of equal
 *   length to values parameter. If the input value is equal to category n then returned value is the nth entry in the
 *   values parameter.
 * @param {Array} parameters.values  Array of values to return given evaluations against categories. Must be of equal
 *   length to categories parameter. Each entry n represents the value to return if the input value is equal to the nth
 *   value in the categories parameter.
 * @param {*} parameters.null_value  Value to return if the input value fails to match to any categories. Optional.
 */
const categorical_bin = (parameters, value) => {
    if (typeof value == 'undefined' || !parameters.categories.includes(value)) {
        return (parameters.null_value ? parameters.null_value : null);
    } else {
        return parameters.values[parameters.categories.indexOf(value)];
    }
};

/**
 * Cycle through a set of options, so that the each element in a set of data receives a value different than the
 *  element before it. For example: "use this palette of 10 colors to visually distinguish 100 adjacent items"
 * This is useful when ADJACENT items must be guaranteed to yield a different result, but it leads to unstable color
 *  choices if the user pans to a region with a different number/order of items. (the same item is assigned a different color)
 *
 *  See also: stable_choice.
 * @function ordinal_cycle
 *  @param {Object} parameters
 *  @param {Array} parameters.values A list of option values
 * @return {*}
 */
const ordinal_cycle = (parameters, value, index) => {
    const options = parameters.values;
    return options[index % options.length];
};

/**
 * A scale function that auto-chooses something (like color) from a preset scheme, and makes the same choice every
 * time given the same value, regardless of ordering or what other data is in the region
 *
 * This is useful when categories must be stable (same color, every time). But sometimes it will assign adjacent values
 *  the same color due to hash collisions.
 *
 * For performance reasons, this is memoized once per instance. Eg, each scalable color parameter has its own cache.
 *  This function is therefore slightly less amenable to layout mutations like "changing the options after scaling
 *  function is used", but this is not expected to be a common use case.
 *
 *  CAVEAT: Some datasets do not return true datum ids, but instead append synthetic ID fields ("item 1, item2"...)
 *    just to appease D3. This hash function only works if there is a meaningful, stable identifier in the data,
 *    like a category or gene name.
 *
 * @function stable_choice
 *
 * @param parameters
 * @param {Array} [parameters.values] A list of options to choose from
 * @param {Number} [parameters.max_cache_size=500] The maximum number of values to cache. This option is mostly used
 *  for unit testing, because stable choice is intended for datasets with a relatively limited number of
 *  discrete categories.
 * @param value
 * @param index
 */
let stable_choice = (parameters, value, index) => {
    // Each place the function gets used has its own parameters object. This function thus memoizes per usage
    //  ("association - point color - directive 1") rather than globally ("all properties/panels")
    const cache = parameters._cache = parameters._cache || new Map();
    const max_cache_size = parameters.max_cache_size || 500;

    if (cache.size >= max_cache_size) {
        // Prevent cache from growing out of control (eg as user moves between regions a lot)
        cache.clear();
    }
    if (cache.has(value)) {
        return cache.get(value);
    }

    // Simple JS hashcode implementation, from:
    //  https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
    let hash = 0;
    value = String(value);
    for (let i = 0; i < value.length; i++) {
        let chr = value.charCodeAt(i);
        hash  = ((hash << 5) - hash) + chr;
        hash |= 0; // Convert to 32bit integer
    }
    // Convert signed 32 bit integer to be within the range of options allowed
    const options = parameters.values;
    const result = options[Math.abs(hash) % options.length];
    cache.set(value, result);
    return result;
};

/**
 * Function for continuous interpolation of numerical values along a gradient with arbitrarily many break points.
 * @function interpolate
 * @parameters {Object} parameters
 * @parameters {Number[]} parameters.breaks  Array of numerical break points against which to evaluate the input value.
 *   Must be of equal length to values parameter and contain at least two elements. Input value will be evaluated for
 *   relative position between two break points n and n+1 and the returned value will be interpolated at a relative
 *   position between values n and n+1.
 * @parameters {*[]} parameters.values  Array of values to interpolate and return given evaluations against break
 *   points. Must be of equal length to breaks parameter and contain at least two elements. Each entry n represents
 *   the value to return if the input value matches the nth entry in breaks exactly. Note that this scale function
 *   uses d3.interpolate to provide for effective interpolation of many different value types, including numbers,
 *   colors, shapes, etc.
 * @parameters {*} parameters.null_value
 */
const interpolate = (parameters, value) => {
    var breaks = parameters.breaks || [];
    var values = parameters.values || [];
    var nullval = (parameters.null_value ? parameters.null_value : null);
    if (breaks.length < 2 || breaks.length !== values.length) {
        return nullval;
    }
    if (typeof value == 'undefined' || value === null || isNaN(+value)) {
        return nullval;
    }
    if (+value <= parameters.breaks[0]) {
        return values[0];
    } else if (+value >= parameters.breaks[parameters.breaks.length - 1]) {
        return values[breaks.length - 1];
    } else {
        var upper_idx = null;
        breaks.forEach(function (brk, idx) {
            if (!idx) {
                return;
            }
            if (breaks[idx - 1] <= +value && breaks[idx] >= +value) {
                upper_idx = idx;
            }
        });
        if (upper_idx === null) {
            return nullval;
        }
        const normalized_input = (+value - breaks[upper_idx - 1]) / (breaks[upper_idx] - breaks[upper_idx - 1]);
        if (!isFinite(normalized_input)) {
            return nullval;
        }
        return d3.interpolate(values[upper_idx - 1], values[upper_idx])(normalized_input);
    }
};

/**
 * Calculate the effect direction based on beta, or the combination of beta and standard error.
 *   Typically used with phewas plots, to show point shape based on the beta and stderr_beta fields.
 *
 * @function effect_direction
 * @param parameters
 * @param parameters.'+' The value to return if the effect direction is positive
 * @param parameters.'-' The value to return if the effect direction is positive
 * @param parameters.beta_field The name of the field containing beta
 * @param parameters.stderr_beta_field The name of the field containing stderr_beta
 * @param {Object} input This function should receive the entire datum object, rather than one single field
 * @returns {null}
 */
function effect_direction(parameters, input) {
    if (input === undefined) {
        return null;
    }

    const { beta_field, stderr_beta_field, '+': plus_result = null, '-': neg_result = null } = parameters;

    if (!beta_field || !stderr_beta_field) {
        throw new Error(`effect_direction must specify how to find required 'beta' and 'stderr_beta' fields`);
    }

    const beta_val = input[beta_field];
    const se_val = input[stderr_beta_field];

    if (beta_val !== undefined) {
        if (se_val !== undefined) {
            if ((beta_val - 1.96 * se_val) > 0) {
                return plus_result;
            } else if ((beta_val + 1.96 * se_val) < 0) {
                return neg_result || null;
            }
        } else {
            if (beta_val > 0) {
                return plus_result;
            } else if (beta_val < 0) {
                return neg_result;
            }
        }
    }
    // Note: The original PheWeb implementation allowed odds ratio in place of beta/se. LZ core is a bit more rigid
    //   about expected data formats for layouts.
    return null;
}

export { categorical_bin, stable_choice, if_value, interpolate, numerical_bin, ordinal_cycle, effect_direction };
<|endoftext|>


<commit_before>import React, { useEffect, useState, useRef } from 'react';
import { useTracking } from "./analyticsEvents";
import { hookToHoc } from "./hocUtils";

const useWindowEvent = (event, callback) => {
    useEffect(() => {
        window.addEventListener(event, callback);
        return () => window.removeEventListener(event, callback);
    }, [event, callback])
};

export const useIdleActivityTimer = (timeout=60000) => {
    const { captureEvent } = useTracking("activityDetection")
    const [userIsActive, setUserIsActive] = useState(true)
    const countdownTimer = useRef(null)

    useWindowEvent("mousemove", reset)
    useWindowEvent("keypress", reset)
    useWindowEvent("scroll", reset)

    function inactivityAlert() {
        captureEvent("activityDetection", {state: "inactive"})
        setUserIsActive(false)
    }

    function reset() {
        if (!userIsActive) {
            captureEvent("activityDetection", {state: "active"})
        }
        setUserIsActive(true)
        clearTimeout(countdownTimer.current)
        countdownTimer.current = setTimeout(inactivityAlert, timeout)
    }

    useEffect(() => {
        reset()
        return () => clearTimeout(countdownTimer.current)
    }, [])

    return { userIsActive }
};

export const withIdleTimer = hookToHoc(useIdleActivityTimer)

export function useCountUpTimer (increments=[10000, 30000], switchIncrement=60000) {
    const { captureEvent } = useTracking("timerEvent")
    const [seconds, setSeconds] = useState(0)
    const [isActive, setIsActive] = useState(true)
    const [smallIncrement, largeIncrement] = increments

    function toggle(state) {
        setIsActive(state)
    }

    function reset() {
        setSeconds(0)
        setIsActive(false)
    }

    useEffect(() => {
        let interval = null;
        let increment = smallIncrement

        if (isActive) {
            if (seconds < switchIncrement ) {increment = smallIncrement}
            else {increment = largeIncrement}
            interval = setInterval(() => {
                setSeconds(seconds => seconds + increment)
                captureEvent("timerEvent", {seconds, increment})
            }, increment)
        } else if (!isActive && seconds !== 0) {
            clearInterval(interval)
        }
        return () => clearInterval(interval)
    }, [isActive, seconds])

    return { seconds, isActive, toggle, reset }
}

export const withCountUpTimer = hookToHoc(useCountUpTimer)

export function useCountUpTimerActive () {
    const { toggle } = useCountUpTimer([10000, 30000])
    const { userIsActive } = useIdleActivityTimer(60000)

    useEffect(() => {
        toggle(userIsActive)
        }, [userIsActive])
    // return { seconds, isActive, userIsActive, toggle}
}

export const withCountUpTimerActive = hookToHoc(useCountUpTimerActive)

<commit_msg>Add pageVisibility hook and 1) send events on visibilty state change, incorporate in useCountUpTimeActive (timer that only increments while user is active)<commit_after>import React, { useEffect, useState, useRef } from 'react';
import { useTracking } from "./analyticsEvents";
import { hookToHoc } from "./hocUtils";

const useWindowEvent = (event, callback) => {
    useEffect(() => {
        window.addEventListener(event, callback);
        return () => window.removeEventListener(event, callback);
    }, [event, callback])
};

const usePageVisibility = () => {
  const { captureEvent } = useTracking()

  function handleVisibilityChange(e) {
    captureEvent("pageVisibilityChange", {hidden: document.hidden, visibilityState: document.visibilityState})
    console.log({hidden: document.hidden, visibilityState: document.visibilityState})
  }

  useEffect(() => {
    window.addEventListener("visibilitychange", handleVisibilityChange)
    return () => window.removeEventListener("visibilitychange", handleVisibilityChange)
    }, []
  )
  return { pageIsHidden: document?.hidden, pageVisibilityState: document?.visibilityState }
}

export const useIdleActivityTimer = (timeout=60) => {
    const { captureEvent } = useTracking("activityDetection")
    const [userIsActive, setUserIsActive] = useState(true)
    const countdownTimer = useRef(null)

    useWindowEvent("mousemove", reset)
    useWindowEvent("keypress", reset)
    useWindowEvent("scroll", reset)

    function inactivityAlert() {
        captureEvent("activityDetection", {state: "inactive"})
        setUserIsActive(false)
    }

    function reset() {
        if (!userIsActive) {
            captureEvent("activityDetection", {state: "active"})
        }
        setUserIsActive(true)
        clearTimeout(countdownTimer.current)
        countdownTimer.current = setTimeout(inactivityAlert, timeout)
    }

    useEffect(() => {
        reset()
        return () => clearTimeout(countdownTimer.current)
    }, [])

    return { userIsActive }
};

export const withIdleTimer = hookToHoc(useIdleActivityTimer)

export function useCountUpTimer (increments=[10, 30], switchIncrement=60) {
    const { captureEvent } = useTracking("timerEvent")
    const [seconds, setSeconds] = useState(0)
    const [isActive, setIsActive] = useState(true)
    const [smallIncrement, largeIncrement] = increments

    function setTimerState(state) {
        setIsActive(state)
    }

    function reset() {
        setSeconds(0)
        setIsActive(false)
    }

    useEffect(() => {
        let interval = null;
        let increment = smallIncrement

        if (isActive) {
            if (seconds < switchIncrement ) {increment = smallIncrement}
            else {increment = largeIncrement}
            interval = setInterval(() => {
                setSeconds(seconds => seconds + increment)
                captureEvent("timerEvent", {seconds, increment: increment})
            }, increment)
        } else if (!isActive && seconds !== 0) {
            clearInterval(interval)
        }
        return () => clearInterval(interval)
    }, [isActive, seconds])

    return { seconds, isActive, setTimerState, reset }
}

export const withCountUpTimer = hookToHoc(useCountUpTimer)

export function useCountUpTimerActive () {
    const { pageIsHidden } = usePageVisibility()
    const { setTimerState } = useCountUpTimer([10, 30], 60)
    const { userIsActive } = useIdleActivityTimer(60)

  useEffect(() => {
        setTimerState(userIsActive && !pageIsHidden)
        }, [userIsActive, pageIsHidden])
    // return { seconds, isActive, userIsActive, setTimerState}
}

export const withCountUpTimerActive = hookToHoc(useCountUpTimerActive)

<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */

import resolve from '@rollup/plugin-node-resolve';
import summary from 'rollup-plugin-summary';
import {terser} from 'rollup-plugin-terser';

const terserOptions = {
  warnings: true,
  ecma: 2020,
  compress: {
    unsafe: true,
    passes: 2,
  },
  output: {
    // "some" preserves @license and @preserve comments
    comments: 'some',
    inline_script: false,
  },
  mangle: {
    // TODO(aomarks) Find out why we can't do property renaming. Something in
    // MWC?
    properties: false,
  },
};

export default [
  {
    input: [
      'lib/components/copy-button.js',
      'lib/components/litdev-banner.js',
      'lib/components/litdev-version-selector.js',
      'lib/components/litdev-drawer.js',
      'lib/components/litdev-example.js',
      'lib/components/litdev-switchable-sample.js',
      'lib/components/litdev-tutorial.js',
      'lib/components/litdev-tutorial-card.js',
      'lib/components/litdev-search.js',
      'lib/components/playground-elements.js',
      'lib/components/resize-bar.js',
      'lib/components/litdev-playground-page.js',
      'lib/github/github-signin-receiver-page.js',
      'lib/global/mobile-nav.js',
      'lib/pages/docs.js',
      'lib/pages/home.js',
      'lib/pages/home-components.js',
      'lib/pages/playground-inline.js',
      'lib/util/lit-hydrate-support.js',
      'lib/util/dsd-polyfill.js',
    ],
    output: {
      dir: 'rollupout',
      format: 'esm',
      // Preserve directory structure for entrypoints.
      entryFileNames: ({facadeModuleId}) =>
        facadeModuleId.replace(`${__dirname}/lib/`, ''),
      manualChunks: (id) => {
        // Create some more logical shared chunks. In particular, people will
        // probably be looking for lit.js in devtools!
        const relative = id.replace(`${__dirname}/node_modules/`, '');
        if (
          relative.startsWith('lit/') ||
          relative.startsWith('lit-html/') ||
          relative.startsWith('lit-element/') ||
          relative.startsWith('@lit/reactive-element/')
        ) {
          return 'lit';
        }
        if (
          relative.startsWith('@material/mwc-base/') ||
          relative.startsWith('@material/base/')
        ) {
          return 'mwc-base';
        }
        if (relative.startsWith('tslib/')) {
          return 'tslib';
        }
      },
      // Skip the usual "-[hash]" suffix. We pre-load certain common chunks so
      // we want to know its exact name (e.g. "lit.js"), and we don't have cache
      // headers that would take advantage of hashed names anyway.
      chunkFileNames: '[name].js',
    },
    plugins: [
      resolve(),
      terser(terserOptions),
      summary({
        // Already minified.
        showMinifiedSize: false,
      }),
    ],
  },

  // These scripts are inlined and must run before first render because they set
  // global CSS classes/attributes that would otherwise cause restyle/relayout.
  //
  // We compile them separately here because they include imports for a small
  // amount of code that we want to inline directly (again, because we want to
  // execute immediately), even though that code is technically duplicated into
  // the asynchronously-loaded module bundles above.
  {
    input: [
      'lib/global/apply-mods.js',
      'lib/global/initialize-typescript-attribute.js',
      'lib/global/mobile-drawer.js',
      'lib/global/dsd-native.js',
    ],
    output: {
      dir: 'rollupout',
      format: 'esm',
      // Preserve directory structure for entrypoints.
      entryFileNames: ({facadeModuleId}) =>
        facadeModuleId.replace(`${__dirname}/lib/`, ''),
    },
    plugins: [
      terser({
        ...terserOptions,
        output: {
          ...terserOptions.output,
          // Remove license comment for inline script.
          comments: false,
        },
      }),
      summary({
        // Already minified.
        showMinifiedSize: false,
      }),
    ],
  },
];
<commit_msg>make iife bundles for inlinesyncjs<commit_after>/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */

import resolve from '@rollup/plugin-node-resolve';
import summary from 'rollup-plugin-summary';
import {terser} from 'rollup-plugin-terser';

const terserOptions = {
  warnings: true,
  ecma: 2020,
  compress: {
    unsafe: true,
    passes: 2,
  },
  output: {
    // "some" preserves @license and @preserve comments
    comments: 'some',
    inline_script: false,
  },
  mangle: {
    // TODO(aomarks) Find out why we can't do property renaming. Something in
    // MWC?
    properties: false,
  },
};

export default [
  {
    input: [
      'lib/components/copy-button.js',
      'lib/components/litdev-banner.js',
      'lib/components/litdev-version-selector.js',
      'lib/components/litdev-drawer.js',
      'lib/components/litdev-example.js',
      'lib/components/litdev-switchable-sample.js',
      'lib/components/litdev-tutorial.js',
      'lib/components/litdev-tutorial-card.js',
      'lib/components/litdev-search.js',
      'lib/components/playground-elements.js',
      'lib/components/resize-bar.js',
      'lib/components/litdev-playground-page.js',
      'lib/github/github-signin-receiver-page.js',
      'lib/global/mobile-nav.js',
      'lib/pages/docs.js',
      'lib/pages/home.js',
      'lib/pages/home-components.js',
      'lib/pages/playground-inline.js',
      'lib/global/lit-hydrate-support.js',
      'lib/global/dsd-polyfill.js',
    ],
    output: {
      dir: 'rollupout',
      format: 'esm',
      // Preserve directory structure for entrypoints.
      entryFileNames: ({facadeModuleId}) =>
        facadeModuleId.replace(`${__dirname}/lib/`, ''),
      manualChunks: (id) => {
        // Create some more logical shared chunks. In particular, people will
        // probably be looking for lit.js in devtools!
        const relative = id.replace(`${__dirname}/node_modules/`, '');
        if (
          relative.startsWith('lit/') ||
          relative.startsWith('lit-html/') ||
          relative.startsWith('lit-element/') ||
          relative.startsWith('@lit/reactive-element/')
        ) {
          return 'lit';
        }
        if (
          relative.startsWith('@material/mwc-base/') ||
          relative.startsWith('@material/base/')
        ) {
          return 'mwc-base';
        }
        if (relative.startsWith('tslib/')) {
          return 'tslib';
        }
      },
      // Skip the usual "-[hash]" suffix. We pre-load certain common chunks so
      // we want to know its exact name (e.g. "lit.js"), and we don't have cache
      // headers that would take advantage of hashed names anyway.
      chunkFileNames: '[name].js',
    },
    plugins: [
      resolve(),
      terser(terserOptions),
      summary({
        // Already minified.
        showMinifiedSize: false,
      }),
    ],
  },

  // These scripts are inlined and must run before first render because they set
  // global CSS classes/attributes that would otherwise cause restyle/relayout.
  //
  // We compile them separately here because they include imports for a small
  // amount of code that we want to inline directly (again, because we want to
  // execute immediately), even though that code is technically duplicated into
  // the asynchronously-loaded module bundles above.
  {
    input: [
      'lib/global/apply-mods.js',
      'lib/global/initialize-typescript-attribute.js',
      'lib/global/mobile-drawer.js',
    ],
    output: {
      dir: 'rollupout',
      format: 'esm',
      // Preserve directory structure for entrypoints.
      entryFileNames: ({facadeModuleId}) =>
        facadeModuleId.replace(`${__dirname}/lib/`, ''),
    },
    plugins: [
      terser({
        ...terserOptions,
        output: {
          ...terserOptions.output,
          // Remove license comment for inline script.
          comments: false,
        },
      }),
      summary({
        // Already minified.
        showMinifiedSize: false,
      }),
    ],
  },

  /**
   * Creates an iife for scripts that need to be inlined with inlinesyncjs and
   * without [type="module"].
   */
  {
    input: [
      'lib/global/dsd-native.js',
    ],
    output: {
      dir: 'rollupout',
      format: 'iife',
      // Preserve directory structure for entrypoints.
      entryFileNames: ({facadeModuleId}) =>
        facadeModuleId.replace(`${__dirname}/lib/`, ''),
    },
    plugins: [
      terser({
        ...terserOptions,
        output: {
          ...terserOptions.output,
          // Remove license comment for inline script.
          comments: false,
        },
      }),
      summary({
        // Already minified.
        showMinifiedSize: false,
      }),
    ],
  },
];
<|endoftext|>


<commit_before>// @param url {String} URL to Meteor app
//   "http://subdomain.meteor.com/" or "/" or
//   "ddp+sockjs://foo-**.meteor.com/sockjs"
LivedataTest.ClientStream = function (url) {
  var self = this;
  self._initCommon();

  //// Constants

  // how long between hearing heartbeat from the server until we declare
  // the connection dead. heartbeats come every 25s (stream_server.js)
  //
  // NOTE: this is a workaround until sockjs detects heartbeats on the
  // client automatically.
  // https://github.com/sockjs/sockjs-client/issues/67
  // https://github.com/sockjs/sockjs-node/issues/68
  self.HEARTBEAT_TIMEOUT = 60000;

  self.rawUrl = url;
  self.socket = null;

  self.sent_update_available = false;

  self.heartbeatTimer = null;

  // Listen to global 'online' event if we are running in a browser.
  // (IE8 does not support addEventListener)
  if (typeof window !== 'undefined' && window.addEventListener)
    window.addEventListener("online", _.bind(self._online, self),
                            false /* useCapture. make FF3.6 happy. */);

  //// Kickoff!
  self._launchConnection();
};

_.extend(LivedataTest.ClientStream.prototype, {

  // data is a utf8 string. Data sent while not connected is dropped on
  // the floor, and it is up the user of this API to retransmit lost
  // messages on 'reset'
  send: function (data) {
    var self = this;
    if (self.currentStatus.connected) {
      self.socket.send(data);
    }
  },

  // Changes where this connection points
  _changeUrl: function (url) {
    var self = this;
    self.rawUrl = url;
  },

  _connected: function (welcome_message) {
    var self = this;

    if (self.connectionTimer) {
      clearTimeout(self.connectionTimer);
      self.connectionTimer = null;
    }

    if (self.currentStatus.connected) {
      // already connected. do nothing. this probably shouldn't happen.
      return;
    }

    // inspect the welcome data and decide if we have to reload
    try {
      var welcome_data = JSON.parse(welcome_message);
    } catch (err) {
      Meteor._debug("DEBUG: malformed welcome packet", welcome_message);
    }

    if (welcome_data && welcome_data.server_id) {
      if (__meteor_runtime_config__.serverId &&
          __meteor_runtime_config__.serverId !== welcome_data.server_id &&
          !self.sent_update_available) {
        self.sent_update_available = true;
        _.each(self.eventCallbacks.update_available,
               function (callback) { callback(); });
      }
    } else
      Meteor._debug("DEBUG: invalid welcome packet", welcome_data);

    // update status
    self.currentStatus.status = "connected";
    self.currentStatus.connected = true;
    self.currentStatus.retryCount = 0;
    self.statusChanged();

    // fire resets. This must come after status change so that clients
    // can call send from within a reset callback.
    _.each(self.eventCallbacks.reset, function (callback) { callback(); });

  },

  _cleanup: function () {
    var self = this;

    self._clearConnectionAndHeartbeatTimers();
    if (self.socket) {
      self.socket.onmessage = self.socket.onclose
        = self.socket.onerror = function () {};
      self.socket.close();
      self.socket = null;
    }
  },

  _clearConnectionAndHeartbeatTimers: function () {
    var self = this;
    if (self.connectionTimer) {
      clearTimeout(self.connectionTimer);
      self.connectionTimer = null;
    }
    if (self.heartbeatTimer) {
      clearTimeout(self.heartbeatTimer);
      self.heartbeatTimer = null;
    }
  },

  _heartbeat_timeout: function () {
    var self = this;
    Meteor._debug("Connection timeout. No heartbeat received.");
    self._lostConnection();
  },

  _heartbeat_received: function () {
    var self = this;
    // If we've already permanently shut down this stream, the timeout is
    // already cleared, and we don't need to set it again.
    if (self._forcedToDisconnect)
      return;
    if (self.heartbeatTimer)
      clearTimeout(self.heartbeatTimer);
    self.heartbeatTimer = setTimeout(
      _.bind(self._heartbeat_timeout, self),
      self.HEARTBEAT_TIMEOUT);
  },

  _sockjsProtocolsWhitelist: function () {
    // only allow polling protocols. no streaming.  streaming
    // makes safari spin.
    var protocolsWhitelist = [
      'xdr-polling', 'xhr-polling', 'iframe-xhr-polling', 'jsonp-polling'];

    // iOS 4 and 5 and below crash when using websockets over certain
    // proxies. this seems to be resolved with iOS 6. eg
    // https://github.com/LearnBoost/socket.io/issues/193#issuecomment-7308865.
    //
    // iOS <4 doesn't support websockets at all so sockjs will just
    // immediately fall back to http
    var noWebsockets = navigator &&
          /iPhone|iPad|iPod/.test(navigator.userAgent) &&
          /OS 4_|OS 5_/.test(navigator.userAgent);

    if (!noWebsockets)
      protocolsWhitelist = ['websocket'].concat(protocolsWhitelist);

    return protocolsWhitelist;
  },

  _launchConnection: function () {
    var self = this;
    self._cleanup(); // cleanup the old socket, if there was one.

    // Convert raw URL to SockJS URL each time we open a connection, so that we
    // can connect to random hostnames and get around browser per-host
    // connection limits.
    self.socket = new SockJS(
      toSockjsUrl(self.rawUrl), undefined, {
        debug: false, protocols_whitelist: self._sockjsProtocolsWhitelist()
      });
    self.socket.onmessage = function (data) {
      self._heartbeat_received();

      // first message we get when we're connecting goes to _connected,
      // which connects us. All subsequent messages (while connected) go to
      // the callback.
      if (self.currentStatus.status === "connecting")
        self._connected(data.data);
      else if (self.currentStatus.connected)
        _.each(self.eventCallbacks.message, function (callback) {
          callback(data.data);
        });
    };
    self.socket.onclose = function () {
      // Meteor._debug("stream disconnect", _.toArray(arguments), (new Date()).toDateString());
      self._lostConnection();
    };
    self.socket.onerror = function () {
      // XXX is this ever called?
      Meteor._debug("stream error", _.toArray(arguments), (new Date()).toDateString());
    };

    self.socket.onheartbeat =  function () {
      self._heartbeat_received();
    };

    if (self.connectionTimer)
      clearTimeout(self.connectionTimer);
    self.connectionTimer = setTimeout(
      _.bind(self._lostConnection, self),
      self.CONNECT_TIMEOUT);
  }
});
<commit_msg>clear all sockjs handlers<commit_after>// @param url {String} URL to Meteor app
//   "http://subdomain.meteor.com/" or "/" or
//   "ddp+sockjs://foo-**.meteor.com/sockjs"
LivedataTest.ClientStream = function (url) {
  var self = this;
  self._initCommon();

  //// Constants

  // how long between hearing heartbeat from the server until we declare
  // the connection dead. heartbeats come every 25s (stream_server.js)
  //
  // NOTE: this is a workaround until sockjs detects heartbeats on the
  // client automatically.
  // https://github.com/sockjs/sockjs-client/issues/67
  // https://github.com/sockjs/sockjs-node/issues/68
  self.HEARTBEAT_TIMEOUT = 60000;

  self.rawUrl = url;
  self.socket = null;

  self.sent_update_available = false;

  self.heartbeatTimer = null;

  // Listen to global 'online' event if we are running in a browser.
  // (IE8 does not support addEventListener)
  if (typeof window !== 'undefined' && window.addEventListener)
    window.addEventListener("online", _.bind(self._online, self),
                            false /* useCapture. make FF3.6 happy. */);

  //// Kickoff!
  self._launchConnection();
};

_.extend(LivedataTest.ClientStream.prototype, {

  // data is a utf8 string. Data sent while not connected is dropped on
  // the floor, and it is up the user of this API to retransmit lost
  // messages on 'reset'
  send: function (data) {
    var self = this;
    if (self.currentStatus.connected) {
      self.socket.send(data);
    }
  },

  // Changes where this connection points
  _changeUrl: function (url) {
    var self = this;
    self.rawUrl = url;
  },

  _connected: function (welcome_message) {
    var self = this;

    if (self.connectionTimer) {
      clearTimeout(self.connectionTimer);
      self.connectionTimer = null;
    }

    if (self.currentStatus.connected) {
      // already connected. do nothing. this probably shouldn't happen.
      return;
    }

    // inspect the welcome data and decide if we have to reload
    try {
      var welcome_data = JSON.parse(welcome_message);
    } catch (err) {
      Meteor._debug("DEBUG: malformed welcome packet", welcome_message);
    }

    if (welcome_data && welcome_data.server_id) {
      if (__meteor_runtime_config__.serverId &&
          __meteor_runtime_config__.serverId !== welcome_data.server_id &&
          !self.sent_update_available) {
        self.sent_update_available = true;
        _.each(self.eventCallbacks.update_available,
               function (callback) { callback(); });
      }
    } else
      Meteor._debug("DEBUG: invalid welcome packet", welcome_data);

    // update status
    self.currentStatus.status = "connected";
    self.currentStatus.connected = true;
    self.currentStatus.retryCount = 0;
    self.statusChanged();

    // fire resets. This must come after status change so that clients
    // can call send from within a reset callback.
    _.each(self.eventCallbacks.reset, function (callback) { callback(); });

  },

  _cleanup: function () {
    var self = this;

    self._clearConnectionAndHeartbeatTimers();
    if (self.socket) {
      self.socket.onmessage = self.socket.onclose
        = self.socket.onerror = self.socket.onheartbeat = function () {};
      self.socket.close();
      self.socket = null;
    }
  },

  _clearConnectionAndHeartbeatTimers: function () {
    var self = this;
    if (self.connectionTimer) {
      clearTimeout(self.connectionTimer);
      self.connectionTimer = null;
    }
    if (self.heartbeatTimer) {
      clearTimeout(self.heartbeatTimer);
      self.heartbeatTimer = null;
    }
  },

  _heartbeat_timeout: function () {
    var self = this;
    Meteor._debug("Connection timeout. No heartbeat received.");
    self._lostConnection();
  },

  _heartbeat_received: function () {
    var self = this;
    // If we've already permanently shut down this stream, the timeout is
    // already cleared, and we don't need to set it again.
    if (self._forcedToDisconnect)
      return;
    if (self.heartbeatTimer)
      clearTimeout(self.heartbeatTimer);
    self.heartbeatTimer = setTimeout(
      _.bind(self._heartbeat_timeout, self),
      self.HEARTBEAT_TIMEOUT);
  },

  _sockjsProtocolsWhitelist: function () {
    // only allow polling protocols. no streaming.  streaming
    // makes safari spin.
    var protocolsWhitelist = [
      'xdr-polling', 'xhr-polling', 'iframe-xhr-polling', 'jsonp-polling'];

    // iOS 4 and 5 and below crash when using websockets over certain
    // proxies. this seems to be resolved with iOS 6. eg
    // https://github.com/LearnBoost/socket.io/issues/193#issuecomment-7308865.
    //
    // iOS <4 doesn't support websockets at all so sockjs will just
    // immediately fall back to http
    var noWebsockets = navigator &&
          /iPhone|iPad|iPod/.test(navigator.userAgent) &&
          /OS 4_|OS 5_/.test(navigator.userAgent);

    if (!noWebsockets)
      protocolsWhitelist = ['websocket'].concat(protocolsWhitelist);

    return protocolsWhitelist;
  },

  _launchConnection: function () {
    var self = this;
    self._cleanup(); // cleanup the old socket, if there was one.

    // Convert raw URL to SockJS URL each time we open a connection, so that we
    // can connect to random hostnames and get around browser per-host
    // connection limits.
    self.socket = new SockJS(
      toSockjsUrl(self.rawUrl), undefined, {
        debug: false, protocols_whitelist: self._sockjsProtocolsWhitelist()
      });
    self.socket.onmessage = function (data) {
      self._heartbeat_received();

      // first message we get when we're connecting goes to _connected,
      // which connects us. All subsequent messages (while connected) go to
      // the callback.
      if (self.currentStatus.status === "connecting")
        self._connected(data.data);
      else if (self.currentStatus.connected)
        _.each(self.eventCallbacks.message, function (callback) {
          callback(data.data);
        });
    };
    self.socket.onclose = function () {
      // Meteor._debug("stream disconnect", _.toArray(arguments), (new Date()).toDateString());
      self._lostConnection();
    };
    self.socket.onerror = function () {
      // XXX is this ever called?
      Meteor._debug("stream error", _.toArray(arguments), (new Date()).toDateString());
    };

    self.socket.onheartbeat =  function () {
      self._heartbeat_received();
    };

    if (self.connectionTimer)
      clearTimeout(self.connectionTimer);
    self.connectionTimer = setTimeout(
      _.bind(self._lostConnection, self),
      self.CONNECT_TIMEOUT);
  }
});
<|endoftext|>