<commit_before>(function($) {

"use strict";

if (Echo.Utils.isComponentDefined("Echo.StreamServer.API")) return;

if (!Echo.StreamServer) Echo.StreamServer = {};

Echo.StreamServer.API = {};

Echo.StreamServer.API.Request = function(config) {
	var self = this;
	config = $.extend({
		"liveUpdatesTimeout": 5,
		"submissionProxyURL": window.location.protocol + "//apps.echoenabled.com/v2/esp/activity"
	}, config);
	Echo.StreamServer.API.Request.parent.constructor.call(this, config);
	this.transport.onData = function() {
		return self.onData.apply(self, arguments);
	};
};

Echo.Utils.inherit(Echo.StreamServer.API.Request, Echo.API.Request);

Echo.StreamServer.API.Request.prototype._search = Echo.StreamServer.API.Request.prototype._count = function(data) {
	if (this.config.get("recurring")) {
		this._initLiveUpdates();
		this._startLiveUpdates();
		return;
	}
	this.request(data);
};

Echo.StreamServer.API.Request.prototype.onData = function(response) {
	response = response || {};
	if (response.result === "error") {
		this._handleErrorResponse(response, {
			"callback": this.config.get("onError")
		});
		return;
	}
	this.nextSince = response.nextSince;
	if (this.liveUpdates && this.liveUpdates.responseHandler) {
		this.liveUpdates.responseHandler(response);
	}
};

Echo.StreamServer.API.Request.prototype._submit = function() {
	this.transport.config.set("url", this.config.get("submissionProxyURL"));
	this.request(
		$.extend(this.config.get("data"), {
			"content": this._AS2KVL(this.config.get("data.content"))
		})
	);
};

Echo.StreamServer.API.Request.prototype._initLiveUpdates = function() {
	var self = this;
	this.liveUpdates = {
		"originalTimeout": this.config.get("liveUpdatesTimeout"),
		"timers": {},
		"timeouts": [],
		"responseHandler": function(data) {
			if (self.liveUpdates.timers.watchdog) {
				clearTimeout(self.liveUpdates.timers.watchdog);
			}
			self._changeLiveUpdatesTimeout(data);
		}
	};
};

Echo.StreamServer.API.Request.prototype._changeLiveUpdatesTimeout = function(data) {
	var self = this;
	// backwards compatibility
	if (typeof data == "string") {
		data = {"liveUpdatesTimeout": data};
	}
	data.liveUpdatesTimeout = parseInt(data.liveUpdatesTimeout);
	var applyServerDefinedTimeout = function(timeout) {
		if (!timeout && self.liveUpdates.originalTimeout != self.config.get("liveUpdatesTimeout")) {
			self.config.set("liveUpdatesTimeout", self.liveUpdates.originalTimeout);
		} else if (timeout && timeout > self.config.get("liveUpdatesTimeout")) {
			self.config.set("liveUpdatesTimeout", timeout);
		}
	};
	var hasNewData = function(data) {
		// for "v1/search" endpoint at the moment
		return !!(data.entries && data.entries.length);
	};
	if (!this.nextSince) {
		applyServerDefinedTimeout(data.liveUpdatesTimeout);
		return;
	}
	var currentTimeout = this.config.get("liveUpdatesTimeout");
	var since = parseInt(this.nextSince);
	var currentTime = Math.floor((new Date()).getTime() / 1000);
	// calculate the delay before starting next request:
	//   - have new data but still behind and need to catch up - use minimum timeout
	//   - have new data but on the track - increase timeout by 1 second
	//   - have no new data - increase timeout by 2 seconds
	var timeout = hasNewData(data)
		? currentTime - since > currentTimeout
			? this.config.get("liveUpdatesTimeoutMin", 3)
			: currentTimeout + 1
		: currentTimeout + 2;
	if (timeout > this.liveUpdates.originalTimeout) {
		timeout = this.liveUpdates.originalTimeout;
	}
	this.config.set("liveUpdatesTimeout", timeout);
	// if timeout remains the same, take server side value into account
	if (timeout == this.liveUpdates.originalTimeout) {
		applyServerDefinedTimeout(data.liveUpdatesTimeout);
	}
};

Echo.StreamServer.API.Request.prototype._stopLiveUpdates = function() {
	if (this.liveUpdates.timers.regular) {
		clearTimeout(this.liveUpdates.timers.regular);
	}
	if (this.liveUpdates.timers.watchdog) {
		clearTimeout(this.liveUpdates.timers.watchdog);
	}
};

Echo.StreamServer.API.Request.prototype._startLiveUpdates = function(force) {
	var self = this;
	if (!this.liveUpdates) return;
	this._stopLiveUpdates();
	if (force) {
		// if live updates requests were forced after some operation, we will
		// perform 3 attempts to get live updates: immediately, in 1 second
		// and in 3 seconds after first one
		this.liveUpdates.timeouts = [0, 1, 3];
	}
	var timeout = this.liveUpdates.timeouts.length
		? this.liveUpdates.timeouts.shift()
		: this.config.get("liveUpdatesTimeout");
	this.liveUpdates.timers.regular = setTimeout(function() {
		// if no response in the reasonable time just restart live updates
		self.liveUpdates.timers.watchdog = setTimeout(function() {
			self._startLiveUpdates();
		}, 5000);
		self.request({
			"nextSince": self.nextSince
		});
	}, timeout * 1000);
};

Echo.StreamServer.API.Request.prototype._isWaitingForData = function(data) {
	return data && $.inArray(data.errorCode, ["waiting", "timeout", "busy", "view_limit", "view_update_capacity_exceeded"]) >= 0;
};

Echo.StreamServer.API.Request.prototype._isErrorWithTimer = function(data) {
	return data && $.inArray(data.errorCode, ["view_limit", "view_update_capacity_exceeded"]) >= 0;
};

Echo.StreamServer.API.Request.prototype._handleErrorResponse = function(data, config) {
	var self = this;
	config = config || {};
	var calcWaitingTimeout = function() {
		// interval is calculated as e^x, x=[1..4]
		if (self.waitingTimeoutStep > 0) {
			if (self.waitingTimeoutStep < 4) {
				self.waitingTimeoutStep++;
			}
		} else {
			self.waitingTimeoutStep = 1;
		}
		return Math.round(Math.exp(self.waitingTimeoutStep)) * 1000;
	};
	var maxWaitingTimeout = 0;
	var timeElapsed = 0;
	if (this._isWaitingForData(data)) {
		maxWaitingTimeout = calcWaitingTimeout();
		timeout = this._isErrorWithTimer(data) ? 1000 : maxWaitingTimeout;
		this.waitingTimer = setInterval(function() {
			timeElapsed += timeout;
			if (timeElapsed == maxWaitingTimeout) {
				self._cleanupErrorHandlers();
				if (config.waitingHandler) {
					config.waitingHandler();
				}
			} else {
				self.transport.abort();
				self.config.get("onError")(data);
			}
		}, timeout);
	} else {
		this.waitingTimeoutStep = 0;
	}
	if (config.callback) config.callback(data);
};

Echo.StreamServer.API.Request.prototype._cleanupErrorHandlers = function(successResponseReceived) {
	if (successResponseReceived) {
		this.waitingTimeoutStep = 0;
	}
	if (this.waitingTimer) {
		clearInterval(this.waitingTimer);
	}
};

Echo.StreamServer.API.Request.prototype._AS2KVL = function(entries) {
	var self = this;
	entries = $.isArray(entries) ? entries : [entries];
	var strip = function(value) {
		return value
			.replace("http://activitystrea.ms/schema/1.0/", "")
			.replace("http://js-kit.com/spec/e2/v1/", "");
	};
	var prepareActivity = function(activity, meta) {
		return {
			"avatar": activity.actor.avatar,
			"content": activity.object.content,
			"markers": meta.markers ? $.trim(meta.markers) : undefined,
			"name": activity.actor.name || activity.actor.title,
			"source": activity.source,
			"tags": meta.tags ? $.trim(meta.tags) : undefined,
			"target": activity.targets[0].id,
			"verb": verb(activity),
			"type": type(activity),
			"itemURIPattern": self.config.get("itemURIPattern", "")
		};
	};
	var verb = function(entry) { return strip(entry.verbs[0]); };
	var type = function(entry) { return strip(entry.object.objectTypes[0]); };
	var post, meta = {"markers": "", "tags": ""};
	$.map(entries, function(entry) {
		if (verb(entry) == "tag" && /tag|marker/.test(type(entry))) {
			meta[type(entry) + "s"] = entry.object.content;
		}
		if (verb(entry) == "post") {
			post = entry;
		}
	});
	if (post) {
		return prepareActivity(post, meta);
	}
	return $.map(entries, function(entry) {
		return prepareActivity(enry);
	});
};

Echo.StreamServer.API.request = function(config) {
	return (new Echo.StreamServer.API.Request(config));
};

})(jQuery);
<commit_msg>fixed bug with submit params<commit_after>(function($) {

"use strict";

if (Echo.Utils.isComponentDefined("Echo.StreamServer.API")) return;

if (!Echo.StreamServer) Echo.StreamServer = {};

Echo.StreamServer.API = {};

Echo.StreamServer.API.Request = function(config) {
	var self = this;
	config = $.extend({
		"liveUpdatesTimeout": 5,
		"submissionProxyURL": window.location.protocol + "//apps.echoenabled.com/v2/esp/activity"
	}, config);
	Echo.StreamServer.API.Request.parent.constructor.call(this, config);
	this.transport.onData = function() {
		return self.onData.apply(self, arguments);
	};
};

Echo.Utils.inherit(Echo.StreamServer.API.Request, Echo.API.Request);

Echo.StreamServer.API.Request.prototype._search = Echo.StreamServer.API.Request.prototype._count = function(data) {
	if (this.config.get("recurring")) {
		this._initLiveUpdates();
		this._startLiveUpdates();
		return;
	}
	this.request(data);
};

Echo.StreamServer.API.Request.prototype.onData = function(response) {
	response = response || {};
	if (response.result === "error") {
		this._handleErrorResponse(response, {
			"callback": this.config.get("onError")
		});
		return;
	}
	this.nextSince = response.nextSince;
	if (this.liveUpdates && this.liveUpdates.responseHandler) {
		this.liveUpdates.responseHandler(response);
	}
};

Echo.StreamServer.API.Request.prototype._submit = function() {
	this.transport.config.set("url", this.config.get("submissionProxyURL"));
	this.request(
		$.extend(this.config.get("data"), {
			"content": Echo.Utils.object2JSON(this._AS2KVL(this.config.get("data.content")))
		})
	);
};

Echo.StreamServer.API.Request.prototype._initLiveUpdates = function() {
	var self = this;
	this.liveUpdates = {
		"originalTimeout": this.config.get("liveUpdatesTimeout"),
		"timers": {},
		"timeouts": [],
		"responseHandler": function(data) {
			if (self.liveUpdates.timers.watchdog) {
				clearTimeout(self.liveUpdates.timers.watchdog);
			}
			self._changeLiveUpdatesTimeout(data);
		}
	};
};

Echo.StreamServer.API.Request.prototype._changeLiveUpdatesTimeout = function(data) {
	var self = this;
	// backwards compatibility
	if (typeof data == "string") {
		data = {"liveUpdatesTimeout": data};
	}
	data.liveUpdatesTimeout = parseInt(data.liveUpdatesTimeout);
	var applyServerDefinedTimeout = function(timeout) {
		if (!timeout && self.liveUpdates.originalTimeout != self.config.get("liveUpdatesTimeout")) {
			self.config.set("liveUpdatesTimeout", self.liveUpdates.originalTimeout);
		} else if (timeout && timeout > self.config.get("liveUpdatesTimeout")) {
			self.config.set("liveUpdatesTimeout", timeout);
		}
	};
	var hasNewData = function(data) {
		// for "v1/search" endpoint at the moment
		return !!(data.entries && data.entries.length);
	};
	if (!this.nextSince) {
		applyServerDefinedTimeout(data.liveUpdatesTimeout);
		return;
	}
	var currentTimeout = this.config.get("liveUpdatesTimeout");
	var since = parseInt(this.nextSince);
	var currentTime = Math.floor((new Date()).getTime() / 1000);
	// calculate the delay before starting next request:
	//   - have new data but still behind and need to catch up - use minimum timeout
	//   - have new data but on the track - increase timeout by 1 second
	//   - have no new data - increase timeout by 2 seconds
	var timeout = hasNewData(data)
		? currentTime - since > currentTimeout
			? this.config.get("liveUpdatesTimeoutMin", 3)
			: currentTimeout + 1
		: currentTimeout + 2;
	if (timeout > this.liveUpdates.originalTimeout) {
		timeout = this.liveUpdates.originalTimeout;
	}
	this.config.set("liveUpdatesTimeout", timeout);
	// if timeout remains the same, take server side value into account
	if (timeout == this.liveUpdates.originalTimeout) {
		applyServerDefinedTimeout(data.liveUpdatesTimeout);
	}
};

Echo.StreamServer.API.Request.prototype._stopLiveUpdates = function() {
	if (this.liveUpdates.timers.regular) {
		clearTimeout(this.liveUpdates.timers.regular);
	}
	if (this.liveUpdates.timers.watchdog) {
		clearTimeout(this.liveUpdates.timers.watchdog);
	}
};

Echo.StreamServer.API.Request.prototype._startLiveUpdates = function(force) {
	var self = this;
	if (!this.liveUpdates) return;
	this._stopLiveUpdates();
	if (force) {
		// if live updates requests were forced after some operation, we will
		// perform 3 attempts to get live updates: immediately, in 1 second
		// and in 3 seconds after first one
		this.liveUpdates.timeouts = [0, 1, 3];
	}
	var timeout = this.liveUpdates.timeouts.length
		? this.liveUpdates.timeouts.shift()
		: this.config.get("liveUpdatesTimeout");
	this.liveUpdates.timers.regular = setTimeout(function() {
		// if no response in the reasonable time just restart live updates
		self.liveUpdates.timers.watchdog = setTimeout(function() {
			self._startLiveUpdates();
		}, 5000);
		self.request({
			"nextSince": self.nextSince
		});
	}, timeout * 1000);
};

Echo.StreamServer.API.Request.prototype._isWaitingForData = function(data) {
	return data && $.inArray(data.errorCode, ["waiting", "timeout", "busy", "view_limit", "view_update_capacity_exceeded"]) >= 0;
};

Echo.StreamServer.API.Request.prototype._isErrorWithTimer = function(data) {
	return data && $.inArray(data.errorCode, ["view_limit", "view_update_capacity_exceeded"]) >= 0;
};

Echo.StreamServer.API.Request.prototype._handleErrorResponse = function(data, config) {
	var self = this;
	config = config || {};
	var calcWaitingTimeout = function() {
		// interval is calculated as e^x, x=[1..4]
		if (self.waitingTimeoutStep > 0) {
			if (self.waitingTimeoutStep < 4) {
				self.waitingTimeoutStep++;
			}
		} else {
			self.waitingTimeoutStep = 1;
		}
		return Math.round(Math.exp(self.waitingTimeoutStep)) * 1000;
	};
	var maxWaitingTimeout = 0;
	var timeElapsed = 0;
	if (this._isWaitingForData(data)) {
		maxWaitingTimeout = calcWaitingTimeout();
		timeout = this._isErrorWithTimer(data) ? 1000 : maxWaitingTimeout;
		this.waitingTimer = setInterval(function() {
			timeElapsed += timeout;
			if (timeElapsed == maxWaitingTimeout) {
				self._cleanupErrorHandlers();
				if (config.waitingHandler) {
					config.waitingHandler();
				}
			} else {
				self.transport.abort();
				self.config.get("onError")(data);
			}
		}, timeout);
	} else {
		this.waitingTimeoutStep = 0;
	}
	if (config.callback) config.callback(data);
};

Echo.StreamServer.API.Request.prototype._cleanupErrorHandlers = function(successResponseReceived) {
	if (successResponseReceived) {
		this.waitingTimeoutStep = 0;
	}
	if (this.waitingTimer) {
		clearInterval(this.waitingTimer);
	}
};

Echo.StreamServer.API.Request.prototype._AS2KVL = function(entries) {
	var self = this;
	entries = $.isArray(entries) ? entries : [entries];
	var strip = function(value) {
		return value
			.replace("http://activitystrea.ms/schema/1.0/", "")
			.replace("http://js-kit.com/spec/e2/v1/", "");
	};
	var prepareActivity = function(activity, meta) {
		return {
			"avatar": activity.actor.avatar,
			"content": activity.object.content,
			"markers": meta.markers ? $.trim(meta.markers) : undefined,
			"name": activity.actor.name || activity.actor.title,
			"source": activity.source,
			"tags": meta.tags ? $.trim(meta.tags) : undefined,
			"target": activity.targets[0].id,
			"verb": verb(activity),
			"type": type(activity),
			"itemURIPattern": self.config.get("itemURIPattern", "")
		};
	};
	var verb = function(entry) { return strip(entry.verbs[0]); };
	var type = function(entry) { return strip(entry.object.objectTypes[0]); };
	var post, meta = {"markers": "", "tags": ""};
	$.map(entries, function(entry) {
		if (verb(entry) == "tag" && /tag|marker/.test(type(entry))) {
			meta[type(entry) + "s"] = entry.object.content;
		}
		if (verb(entry) == "post") {
			post = entry;
		}
	});
	if (post) {
		return prepareActivity(post, meta);
	}
	return $.map(entries, function(entry) {
		return prepareActivity(enry);
	});
};

Echo.StreamServer.API.request = function(config) {
	return (new Echo.StreamServer.API.Request(config));
};

})(jQuery);
<|endoftext|>


<commit_before>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2016
 */

import { SyncQueue } from './SyncQueue';
import { SyncDatabase } from './SyncDatabase';
import { generateSyncJson } from './outgoingSyncUtils';
import { integrateRecord } from './incomingSyncUtils';
import { SETTINGS_KEYS } from '../settings';
import {
  incrementSyncProgress,
  setSyncProgress,
  setSyncProgressMessage,
  setSyncTotal,
  setSyncError,
  setSyncIsSyncing,
  setSyncCompletionTime,
} from './actions';

const {
  SYNC_IS_INITIALISED,
  SYNC_PRIOR_FAILED,
  SYNC_SITE_NAME,
  SYNC_SERVER_ID,
  SYNC_SITE_ID,
  SYNC_URL,
} = SETTINGS_KEYS;

const INITIAL_BATCH_SIZE = 20; // Number of records to sync at one time

/**
 * Provides core synchronization functionality, initilising the database with an
 * initial full sync, pushing, and pulling (any regular scheduling of synchronization
 * must be coordinated externally)
 * @param  {Realm}             database       The local database
 * @param  {SyncAuthenticator} authenticator  Provides authentication with the sync server
 * @param  {Settings}          settings       Access to locally stored settings
 */
export class Synchroniser {
  constructor(database, authenticator, settings, dispatch) {
    this.database = new SyncDatabase(database);
    this.authenticator = authenticator;
    this.settings = settings;
    this.syncQueue = new SyncQueue(this.database);
    this.dispatch = dispatch;
    this.refreshSyncParams();
    if (this.isInitialised()) this.syncQueue.enable();
  }

  /**
   * Redux progress setting functions
   */
  setTotal = totalCount => this.dispatch(setSyncTotal(totalCount));
  incrementProgress = increment => this.dispatch(incrementSyncProgress(increment));
  setProgress = currentCount => this.dispatch(setSyncProgress(currentCount));
  setProgressMessage = message => this.dispatch(setSyncProgressMessage(message));
  setError = errorMessage => this.dispatch(setSyncError(errorMessage));
  setIsSyncing = isSyncing => this.dispatch(setSyncIsSyncing(isSyncing));
  setCompletionTime = time => this.dispatch(setSyncCompletionTime(time));

  refreshSyncParams = () => {
    this.serverURL = this.settings.get(SYNC_URL);
    this.thisSiteId = this.settings.get(SYNC_SITE_ID);
    this.serverId = this.settings.get(SYNC_SERVER_ID);
    this.thisSiteName = this.settings.get(SYNC_SITE_NAME);
    this.authHeader = this.authenticator.getAuthHeader();
    this.batchSize = INITIAL_BATCH_SIZE;
  };

  /**
   * Wipe the current database, check that the given url can be synced against
   * using the given name and password, and if so populate the database by pulling
   * from the remote server.
   * @param  {string} serverURL        The URL to be synced against
   * @param  {string} syncSiteName     The username to use in sync auth
   * @param  {string} syncSitePassword The password to use in sync auth
   * @return {Promise}                 Resolve if successfully authenticated and
   *                                   initialised, otherwise throw error
   */
  initialise = async (serverURL, syncSiteName, syncSitePassword) => {
    this.setIsSyncing(true);
    this.setProgressMessage('Initialising...');
    this.syncQueue.disable(); // Stop sync queue listening to database changes
    // Check if the serverURL passed in is the same as one we have already been using during
    // initialisation, in which case we are continuing a failed partial initialisation. If the
    // serverURL is different, it is either completely fresh, or the URL has been changed so we
    // should start afresh. Do the same for syncSiteName, so that it isn't possible to start syncing
    // data from a site different to what initialisation was previously started with.
    const oldSyncURL = this.serverURL;
    const oldSyncSiteName = this.thisSiteName;
    const isFresh =
      !oldSyncURL || serverURL !== oldSyncURL || !syncSiteName || syncSiteName !== oldSyncSiteName;

    if (isFresh) {
      this.database.write(() => {
        this.database.deleteAll();
      });
    }
    try {
      await this.authenticator.authenticate(serverURL, syncSiteName, syncSitePassword);
      this.refreshSyncParams(); // authenticate sets all the sync settings in database, so refresh

      if (isFresh) {
        // If a fresh initialisation, tell the server to prepare required sync records
        await fetch(
          `${this.serverURL}/sync/v3/initial_dump/` +
            `?from_site=${this.thisSiteId}&to_site=${this.serverId}`,
          {
            headers: {
              Authorization: this.authHeader,
            },
          },
        );
        // If the initial_dump has been successful, serverURL is valid, and should now have all sync
        // records queued and ready to send. Safe to store as this.serverURL
        this.serverURL = serverURL;
      }
      await this.pull();
    } catch (error) {
      // Did not authenticate, sync error, or no internet, pass error up
      this.setError(error.message);
      this.setIsSyncing(false);
      throw error;
    }

    this.settings.set(SYNC_IS_INITIALISED, 'true');
    this.syncQueue.enable(); // Begin the sync queue listening to database changes
    this.setIsSyncing(false);
  };

  /**
   * Return whether the synchroniser has been initialised.
   * @return {boolean} True if initial sync has been completed successfully
   */
  isInitialised = () => {
    const syncIsInitialised = this.settings.get(SYNC_IS_INITIALISED);
    return syncIsInitialised && syncIsInitialised === 'true';
  };

  /**
   * Return whether or not the last sync of the app failed
   * @return {boolean} 'true' if the last call of synchronise failed
   */
  lastSyncFailed = () => {
    const lastSyncFailed = this.settings.get(SYNC_PRIOR_FAILED);
    return lastSyncFailed && lastSyncFailed === 'true';
  };

  /**
   * Carry out a synchronization, first pushing any local changes, then pulling
   * down remote changes and integrating them into the local database.
   * @return {none}
   */
  synchronise = async () => {
    try {
      if (!this.isInitialised()) throw new Error('Not yet initialised');
      this.setIsSyncing(true);
      this.refreshSyncParams();

      // Keeps track between app close/open whether last sync was successful
      this.settings.set(SYNC_PRIOR_FAILED, 'true');

      // Using async/await here means that any errors thrown by push or pull will be caught by the
      // outer try/catch
      await this.push();
      await this.pull();

      // Store persistent sync details in settings
      this.settings.set(SYNC_PRIOR_FAILED, 'false');

      // Store sync completion progress in redux
      this.setIsSyncing(false);
      this.setCompletionTime(new Date().getTime());
    } catch (error) {
      this.setError(error.message);
      this.setIsSyncing(false);
    }
  };

  /**
   * Push batches of changes to the local database up to the remote server, until
   * all local changes have been synced.
   * @return {Promise} Resolves if successful, or passes up any error thrown
   */
  push = async () => {
    this.setProgressMessage('Pushing changes to the server');
    this.setProgress(0);
    this.setTotal(this.syncQueue.length);
    while (this.syncQueue.length > 0) {
      const recordsToSync = this.syncQueue.next(this.batchSize);
      const translatedRecords = recordsToSync
        .map(this.translateRecord) // Apply map function to get translated records
        .filter(record => !!record); // If error thrown, may be null so filter falsy values out
      await this.pushRecords(translatedRecords);
      // Records that threw errors in translateRecord() are still removed
      this.syncQueue.use(recordsToSync);
      this.incrementProgress(recordsToSync.length);
    }
  };

  /**
   * Handles translating records for push() and catching errors
   * @param {object} record The outgoing record to be translated
   * @return {object} The translated record output
   */
  translateRecord = record => {
    try {
      // Try translate the record
      return generateSyncJson(this.database, this.settings, record);
    } catch (error) {
      // If error not safe to continue sync throw it again to pass it up to next handler
      // See outgoingSyncUtils.js
      if (!error.canDeleteSyncOut) throw error;
      return null;
    }
  };

  /**
   * Pushes a collection of records to the remote sync server
   * @param  {array}   records The records to push
   * @return {Promise}         Resolves if successful, or passes up any error thrown
   */
  pushRecords = async records => {
    const response = await fetch(
      `${this.serverURL}/sync/v3/queued_records/` +
        `?from_site=${this.thisSiteId}&to_site=${this.serverId}`,
      {
        method: 'POST',
        headers: {
          Authorization: this.authHeader,
        },
        body: JSON.stringify(records),
      },
    );
    let responseJson;
    try {
      responseJson = await response.json();
    } catch (error) {
      throw new Error('Unexpected response from sync server');
    }
    if (responseJson.error.length > 0) {
      throw new Error('Server rejected pushed records');
    }
  };

  /**
   * Pulls any changes to data on the sync server down to the local database
   * @return {Promise} Resolves if successful, or passes up any error thrown
   */
  pull = async () => {
    let waitingRecordCount = await this.getWaitingRecordCount();
    let total = waitingRecordCount;
    let progress = 0;

    this.setProgressMessage('Pulling changes from the server');
    this.setProgress(progress);
    this.setTotal(total);

    // Pull this.batchSize amount of records at a time from server
    while (progress < total) {
      // Get a batch of records and integrate them
      const incomingRecords = await this.getIncomingRecords();
      this.integrateRecords(incomingRecords);
      await this.acknowledgeRecords(incomingRecords);
      this.incrementProgress(incomingRecords.length);
      progress += incomingRecords.length;

      // Check no new records added to incoming queue on last iteration.
      if (progress >= total) {
        waitingRecordCount = await this.getWaitingRecordCount();
        if (waitingRecordCount > 0) {
          total += waitingRecordCount;
          this.setTotal(total);
        }
      }
    }
  };

  /**
   * Returns the number of records left to pull
   * @return {Promise}           Resolves with the record count, or passes up any error thrown
   */
  getWaitingRecordCount = async () => {
    const response = await fetch(
      `${this.serverURL}/sync/v3/queued_records/count` +
        `?from_site=${this.thisSiteId}&to_site=${this.serverId}`,
      {
        headers: {
          Authorization: this.authHeader,
        },
      },
    );
    if (response.status < 200 || response.status >= 300) {
      throw new Error('Connection failure while attempting to sync.');
    }
    const responseJson = await response.json();
    if (responseJson.error && responseJson.error.length > 0) {
      throw new Error(responseJson.error);
    }
    if (typeof responseJson.NumRecords !== 'number') {
      throw new Error('Unexpected response from server');
    }
    return responseJson.NumRecords;
  };

  /**
   * Returns the next batch of incoming sync records
   * @return {Promise}            Resolves with the records, or passes up any error thrown
   */
  getIncomingRecords = async () => {
    const response = await fetch(
      `${this.serverURL}/sync/v3/queued_records` +
        `?from_site=${this.thisSiteId}&to_site=${this.serverId}&limit=${this.batchSize}`,
      {
        headers: {
          Authorization: this.authHeader,
        },
      },
    );
    if (response.status < 200 || response.status >= 300) {
      throw new Error('Connection failure while pulling sync records.');
    }
    const responseJson = await response.json();
    if (responseJson.error && responseJson.error.length > 0) {
      throw new Error(responseJson.error);
    }
    return responseJson;
  };

  /**
   * Parse the batch of incoming records, and integrate them into the local database
   * @param  {object} syncJson  The json object the server sent to represent records
   * @return {none}
   */
  integrateRecords(syncJson) {
    this.database.write(() => {
      syncJson.forEach(syncRecord => {
        integrateRecord(this.database, this.settings, syncRecord);
      });
    });
  }

  /**
   * Sends the sync server a message to indicate the sync records have been consumed
   * @param  {array}   records    Sync records that have been integrated
   * @return {none}
   */
  acknowledgeRecords = async records => {
    const syncIds = records.map(record => record.SyncID);
    const requestBody = {
      SyncRecordIDs: syncIds,
    };
    await fetch(
      `${this.serverURL}/sync/v3/acknowledged_records` +
        `?from_site=${this.thisSiteId}&to_site=${this.serverId}`,
      {
        method: 'POST',
        headers: {
          Authorization: this.authHeader,
        },
        body: JSON.stringify(requestBody),
      },
    );
  };
}
<commit_msg>fixes<commit_after>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2016
 */

import { SyncQueue } from './SyncQueue';
import { SyncDatabase } from './SyncDatabase';
import { generateSyncJson } from './outgoingSyncUtils';
import { integrateRecord } from './incomingSyncUtils';
import { SETTINGS_KEYS } from '../settings';
import {
  incrementSyncProgress,
  setSyncProgress,
  setSyncProgressMessage,
  setSyncTotal,
  setSyncError,
  setSyncIsSyncing,
  setSyncCompletionTime,
} from './actions';

const {
  SYNC_IS_INITIALISED,
  SYNC_PRIOR_FAILED,
  SYNC_SITE_NAME,
  SYNC_SERVER_ID,
  SYNC_SITE_ID,
  SYNC_URL,
} = SETTINGS_KEYS;

const INITIAL_BATCH_SIZE = 20; // Number of records to sync at one time

/**
 * Provides core synchronization functionality, initilising the database with an
 * initial full sync, pushing, and pulling (any regular scheduling of synchronization
 * must be coordinated externally)
 * @param  {Realm}             database       The local database
 * @param  {SyncAuthenticator} authenticator  Provides authentication with the sync server
 * @param  {Settings}          settings       Access to locally stored settings
 */
export class Synchroniser {
  constructor(database, authenticator, settings, dispatch) {
    this.database = new SyncDatabase(database);
    this.authenticator = authenticator;
    this.settings = settings;
    this.syncQueue = new SyncQueue(this.database);
    this.dispatch = dispatch;
    this.refreshSyncParams();
    if (this.isInitialised()) this.syncQueue.enable();
  }

  /**
   * Redux progress setting functions
   */
  setTotal = totalCount => this.dispatch(setSyncTotal(totalCount));
  incrementProgress = increment => this.dispatch(incrementSyncProgress(increment));
  setProgress = currentCount => this.dispatch(setSyncProgress(currentCount));
  setProgressMessage = message => this.dispatch(setSyncProgressMessage(message));
  setError = errorMessage => this.dispatch(setSyncError(errorMessage));
  setIsSyncing = isSyncing => this.dispatch(setSyncIsSyncing(isSyncing));
  setCompletionTime = time => this.dispatch(setSyncCompletionTime(time));

  refreshSyncParams = () => {
    this.serverURL = this.settings.get(SYNC_URL);
    this.thisSiteId = this.settings.get(SYNC_SITE_ID);
    this.serverId = this.settings.get(SYNC_SERVER_ID);
    this.thisSiteName = this.settings.get(SYNC_SITE_NAME);
    this.authHeader = this.authenticator.getAuthHeader();
    this.batchSize = INITIAL_BATCH_SIZE;
  };

  /**
   * Wipe the current database, check that the given url can be synced against
   * using the given name and password, and if so populate the database by pulling
   * from the remote server.
   * @param  {string} serverURL        The URL to be synced against
   * @param  {string} syncSiteName     The username to use in sync auth
   * @param  {string} syncSitePassword The password to use in sync auth
   * @return {Promise}                 Resolve if successfully authenticated and
   *                                   initialised, otherwise throw error
   */
  initialise = async (serverURL, syncSiteName, syncSitePassword) => {
    this.setIsSyncing(true);
    this.setProgressMessage('Initialising...');
    this.syncQueue.disable(); // Stop sync queue listening to database changes
    // Check if the serverURL passed in is the same as one we have already been using during
    // initialisation, in which case we are continuing a failed partial initialisation. If the
    // serverURL is different, it is either completely fresh, or the URL has been changed so we
    // should start afresh. Do the same for syncSiteName, so that it isn't possible to start syncing
    // data from a site different to what initialisation was previously started with.
    const oldSyncURL = this.serverURL;
    const oldSyncSiteName = this.thisSiteName;
    const isFresh =
      !oldSyncURL || serverURL !== oldSyncURL || !syncSiteName || syncSiteName !== oldSyncSiteName;

    if (isFresh) {
      this.database.write(() => {
        this.database.deleteAll();
      });
    }
    try {
      await this.authenticator.authenticate(serverURL, syncSiteName, syncSitePassword);
      this.refreshSyncParams(); // authenticate sets all the sync settings in database, so refresh

      if (isFresh) {
        // If a fresh initialisation, tell the server to prepare required sync records
        await fetch(
          `${this.serverURL}/sync/v3/initial_dump/` +
            `?from_site=${this.thisSiteId}&to_site=${this.serverId}`,
          {
            headers: {
              Authorization: this.authHeader,
            },
          },
        );
        // If the initial_dump has been successful, serverURL is valid, and should now have all sync
        // records queued and ready to send. Safe to store as this.serverURL
        this.serverURL = serverURL;
      }
      await this.pull();
    } catch (error) {
      // Did not authenticate, sync error, or no internet, pass error up
      this.setError(error.message);
      this.setIsSyncing(false);
      throw error;
    }

    this.settings.set(SYNC_IS_INITIALISED, 'true');
    this.syncQueue.enable(); // Begin the sync queue listening to database changes
    this.setIsSyncing(false);
  };

  /**
   * Return whether the synchroniser has been initialised.
   * @return {boolean} True if initial sync has been completed successfully
   */
  isInitialised = () => {
    const syncIsInitialised = this.settings.get(SYNC_IS_INITIALISED);
    return syncIsInitialised && syncIsInitialised === 'true';
  };

  /**
   * Return whether or not the last sync of the app failed
   * @return {boolean} 'true' if the last call of synchronise failed
   */
  lastSyncFailed = () => {
    const lastSyncFailed = this.settings.get(SYNC_PRIOR_FAILED);
    return lastSyncFailed && lastSyncFailed === 'true';
  };

  /**
   * Carry out a synchronization, first pushing any local changes, then pulling
   * down remote changes and integrating them into the local database.
   * @return {none}
   */
  synchronise = async () => {
    try {
      if (!this.isInitialised()) throw new Error('Not yet initialised');
      this.setIsSyncing(true);
      this.refreshSyncParams();

      // Keeps track between app close/open whether last sync was successful
      this.settings.set(SYNC_PRIOR_FAILED, 'true');

      // Using async/await here means that any errors thrown by push or pull will be caught by the
      // outer try/catch
      await this.push();
      await this.pull();

      // Store persistent sync details in settings
      this.settings.set(SYNC_PRIOR_FAILED, 'false');

      // Store sync completion progress in redux
      this.setIsSyncing(false);
      this.setCompletionTime(new Date().getTime());
    } catch (error) {
      this.setError(error.message);
      this.setIsSyncing(false);
    }
  };

  /**
   * Push batches of changes to the local database up to the remote server, until
   * all local changes have been synced.
   * @return {Promise} Resolves if successful, or passes up any error thrown
   */
  push = async () => {
    this.setProgressMessage('Pushing changes to the server');
    this.setProgress(0);
    this.setTotal(this.syncQueue.length);
    while (this.syncQueue.length > 0) {
      const recordsToSync = this.syncQueue.next(this.batchSize);
      const translatedRecords = recordsToSync
        .map(this.translateRecord) // Apply map function to get translated records
        .filter(record => !!record); // If error thrown, may be null so filter falsy values out
      await this.pushRecords(translatedRecords);
      // Records that threw errors in translateRecord() are still removed
      this.syncQueue.use(recordsToSync);
      this.incrementProgress(recordsToSync.length);
    }
  };

  /**
   * Handles translating records for push() and catching errors
   * @param {object} record The outgoing record to be translated
   * @return {object} The translated record output
   */
  translateRecord = record => {
    try {
      // Try translate the record
      return generateSyncJson(this.database, this.settings, record);
    } catch (error) {
      // If error not safe to continue sync throw it again to pass it up to next handler
      // See outgoingSyncUtils.js
      if (!error.canDeleteSyncOut) throw error;
      return null;
    }
  };

  /**
   * Pushes a collection of records to the remote sync server
   * @param  {array}   records The records to push
   * @return {Promise}         Resolves if successful, or passes up any error thrown
   */
  pushRecords = async records => {
    const response = await fetch(
      `${this.serverURL}/sync/v3/queued_records/` +
        `?from_site=${this.thisSiteId}&to_site=${this.serverId}`,
      {
        method: 'POST',
        headers: {
          Authorization: this.authHeader,
        },
        body: JSON.stringify(records),
      },
    );
    let responseJson;
    try {
      responseJson = await response.json();
    } catch (error) {
      throw new Error('Unexpected response from sync server');
    }
    if (responseJson.error.length > 0) {
      throw new Error('Server rejected pushed records');
    }
  };

  /**
   * Pulls any changes to data on the sync server down to the local database
   * @return {Promise} Resolves if successful, or passes up any error thrown
   */
  pull = async () => {
    let waitingRecordCount = await this.getWaitingRecordCount();
    let total = waitingRecordCount;
    let progress = 0;

    this.setProgressMessage('Pulling changes from the server');
    this.setProgress(progress);
    this.setTotal(total);

    // Pull this.batchSize amount of records at a time from server
    while (progress < total) {
      // Get a batch of records and integrate them
      const incomingRecords = await this.getIncomingRecords();
      this.integrateRecords(incomingRecords);
      await this.acknowledgeRecords(incomingRecords);
      progress += incomingRecords.length;

      // Check no new records added to incoming queue on last iteration.
      if (progress >= total) {
        waitingRecordCount = await this.getWaitingRecordCount();
        if (waitingRecordCount > 0) {
          total = progress + waitingRecordCount;
          this.setTotal(total);
        }
      }
      this.incrementProgress(incomingRecords.length);
    }
  };

  /**
   * Returns the number of records left to pull
   * @return {Promise}           Resolves with the record count, or passes up any error thrown
   */
  getWaitingRecordCount = async () => {
    const response = await fetch(
      `${this.serverURL}/sync/v3/queued_records/count` +
        `?from_site=${this.thisSiteId}&to_site=${this.serverId}`,
      {
        headers: {
          Authorization: this.authHeader,
        },
      },
    );
    if (response.status < 200 || response.status >= 300) {
      throw new Error('Connection failure while attempting to sync.');
    }
    const responseJson = await response.json();
    if (responseJson.error && responseJson.error.length > 0) {
      throw new Error(responseJson.error);
    }
    if (typeof responseJson.NumRecords !== 'number') {
      throw new Error('Unexpected response from server');
    }
    return responseJson.NumRecords;
  };

  /**
   * Returns the next batch of incoming sync records
   * @return {Promise}            Resolves with the records, or passes up any error thrown
   */
  getIncomingRecords = async () => {
    const response = await fetch(
      `${this.serverURL}/sync/v3/queued_records` +
        `?from_site=${this.thisSiteId}&to_site=${this.serverId}&limit=${this.batchSize}`,
      {
        headers: {
          Authorization: this.authHeader,
        },
      },
    );
    if (response.status < 200 || response.status >= 300) {
      throw new Error('Connection failure while pulling sync records.');
    }
    const responseJson = await response.json();
    if (responseJson.error && responseJson.error.length > 0) {
      throw new Error(responseJson.error);
    }
    return responseJson;
  };

  /**
   * Parse the batch of incoming records, and integrate them into the local database
   * @param  {object} syncJson  The json object the server sent to represent records
   * @return {none}
   */
  integrateRecords(syncJson) {
    this.database.write(() => {
      syncJson.forEach(syncRecord => {
        integrateRecord(this.database, this.settings, syncRecord);
      });
    });
  }

  /**
   * Sends the sync server a message to indicate the sync records have been consumed
   * @param  {array}   records    Sync records that have been integrated
   * @return {none}
   */
  acknowledgeRecords = async records => {
    const syncIds = records.map(record => record.SyncID);
    const requestBody = {
      SyncRecordIDs: syncIds,
    };
    await fetch(
      `${this.serverURL}/sync/v3/acknowledged_records` +
        `?from_site=${this.thisSiteId}&to_site=${this.serverId}`,
      {
        method: 'POST',
        headers: {
          Authorization: this.authHeader,
        },
        body: JSON.stringify(requestBody),
      },
    );
  };
}
<|endoftext|>


<commit_before>import React from "react";
import { graphql } from "gatsby";
import * as PropTypes from "prop-types";
import ArticleHeader from "../components/ArticleHeader";
import DisqusThread from "../components/DisqusThread";
import Author from "../components/Author";
import OG from "../components/seo/og";
import Article from "../components/seo/article";
import General from "../components/seo/general";
import Twitter from "../components/seo/twitter";
import getArticleModule from "../utils/getArticleModule";
import Layout from "../components/layout";

const propTypes = {
  data: PropTypes.object.isRequired,
};

class ArticleTemplate extends React.Component {
  render() {
    const article = this.props.data.contentfulArticle;
    const {
      title,
      slug,
      contentModules,
      featureImage,
      author,
      publishDate,
      updatedAt,
    } = article;
    const featuredImageUrl =
      featureImage && featureImage.file ? featureImage.file.url : "";
    const excerpt =
      contentModules !== null
        ? contentModules[0].copy.childMarkdownRemark.excerpt
        : "";
    return (
      <Layout>

            <OG
              title={title}
              locale="en-US"
              type="article"
              description={excerpt}
              url={`https://chocolate-free.com/article/${slug}`}
              siteName="Chocolate Free"
              updateTime={updatedAt}
              publishedTime={publishDate}
              image={`https:${featuredImageUrl}?w=1200&h=630`}
              imageSecure={`https:${featuredImageUrl}?w=1200&h=630`}
            />

            <Article
              updateTime={updatedAt}
              publishedTime={publishDate}
              url={`https://chocolate-free.com/article/${slug}`}
            />

            <Twitter
              title={title}
              description={excerpt}
              image={`https:${featuredImageUrl}?w=1200&h=630`}
            />
            <General title={title} description={excerpt} />
            <ArticleHeader node={article} />
            {contentModules &&
              contentModules.map((module, i) => getArticleModule(module, i))}
            <Author author={author} />
            <DisqusThread id={slug} path={slug} title={title} />

      </Layout>
    );
  }
}

ArticleTemplate.propTypes = propTypes;

export default ArticleTemplate;

export const pageQuery = graphql`
  query articleQuery($slug: String!) {
    contentfulArticle(slug: { eq: $slug }) {
      title
      slug
      contentModules {
        ... on ContentfulArticleRecipe {
          internal {
            type
          }
          serves
          ingredients {
            childMarkdownRemark {
              html
            }
          }
          instructions {
            childMarkdownRemark {
              html
            }
          }
          image {
            title
            gatsbyImageData(
              width: 400
              placeholder: TRACED_SVG
              quality: 80
            )
          }
          title
          totalTime
          prepTime
          cookTime
        }
        ... on ContentfulArticleCopy {
          internal {
            type
          }
          copy {
            childMarkdownRemark {
              html
              excerpt
            }
          }
        }
        ... on ContentfulArticleTwoImages {
          internal {
            type
          }
          left {
            title
            description
            gatsbyImageData(
              width: 420
              placeholder: TRACED_SVG
              quality: 80
            )
          }
          right {
            title
            description
            gatsbyImageData(
              width: 420
              placeholder: TRACED_SVG
              quality: 80
            )
          }
        }
        ... on ContentfulArticleImage {
          internal {
            type
          }
          image {
            title
            gatsbyImageData(
              width: 600
              placeholder: TRACED_SVG
              quality: 80
            )
          }
        }
      }
      publishDate
      updatedAt
      featureImage {
        title
        gatsbyImageData(
          width: 420
          placeholder: TRACED_SVG
          quality: 80
        )
      }
      author {
        fullName
        website
        bio {
          bio
        }
        avatar {
          gatsbyImageData(width: 150)
        }
      }
    }
  }
`;
<commit_msg>Change the placeholder<commit_after>import React from "react";
import { graphql } from "gatsby";
import * as PropTypes from "prop-types";
import ArticleHeader from "../components/ArticleHeader";
import DisqusThread from "../components/DisqusThread";
import Author from "../components/Author";
import OG from "../components/seo/og";
import Article from "../components/seo/article";
import General from "../components/seo/general";
import Twitter from "../components/seo/twitter";
import getArticleModule from "../utils/getArticleModule";
import Layout from "../components/layout";

const propTypes = {
  data: PropTypes.object.isRequired,
};

class ArticleTemplate extends React.Component {
  render() {
    const article = this.props.data.contentfulArticle;
    const {
      title,
      slug,
      contentModules,
      featureImage,
      author,
      publishDate,
      updatedAt,
    } = article;
    const featuredImageUrl =
      featureImage && featureImage.file ? featureImage.file.url : "";
    const excerpt =
      contentModules !== null
        ? contentModules[0].copy.childMarkdownRemark.excerpt
        : "";
    return (
      <Layout>

            <OG
              title={title}
              locale="en-US"
              type="article"
              description={excerpt}
              url={`https://chocolate-free.com/article/${slug}`}
              siteName="Chocolate Free"
              updateTime={updatedAt}
              publishedTime={publishDate}
              image={`https:${featuredImageUrl}?w=1200&h=630`}
              imageSecure={`https:${featuredImageUrl}?w=1200&h=630`}
            />

            <Article
              updateTime={updatedAt}
              publishedTime={publishDate}
              url={`https://chocolate-free.com/article/${slug}`}
            />

            <Twitter
              title={title}
              description={excerpt}
              image={`https:${featuredImageUrl}?w=1200&h=630`}
            />
            <General title={title} description={excerpt} />
            <ArticleHeader node={article} />
            {contentModules &&
              contentModules.map((module, i) => getArticleModule(module, i))}
            <Author author={author} />
            <DisqusThread id={slug} path={slug} title={title} />

      </Layout>
    );
  }
}

ArticleTemplate.propTypes = propTypes;

export default ArticleTemplate;

export const pageQuery = graphql`
  query articleQuery($slug: String!) {
    contentfulArticle(slug: { eq: $slug }) {
      title
      slug
      contentModules {
        ... on ContentfulArticleRecipe {
          internal {
            type
          }
          serves
          ingredients {
            childMarkdownRemark {
              html
            }
          }
          instructions {
            childMarkdownRemark {
              html
            }
          }
          image {
            title
            gatsbyImageData(
              width: 400
              formats: AUTO
              placeholder: BLURRED
              quality: 80
            )
          }
          title
          totalTime
          prepTime
          cookTime
        }
        ... on ContentfulArticleCopy {
          internal {
            type
          }
          copy {
            childMarkdownRemark {
              html
              excerpt
            }
          }
        }
        ... on ContentfulArticleTwoImages {
          internal {
            type
          }
          left {
            title
            description
            gatsbyImageData(
              width: 420
              formats: AUTO
              placeholder: BLURRED
              quality: 80
            )
          }
          right {
            title
            description
            gatsbyImageData(
              width: 420
              formats: AUTO
              placeholder: BLURRED
              quality: 80
            )
          }
        }
        ... on ContentfulArticleImage {
          internal {
            type
          }
          image {
            title
            gatsbyImageData(
              width: 600
              formats: AUTO
              placeholder: BLURRED
              quality: 80
            )
          }
        }
      }
      publishDate
      updatedAt
      featureImage {
        title
        gatsbyImageData(
          width: 420
          formats: AUTO
          placeholder: BLURRED
          quality: 80
        )
      }
      author {
        fullName
        website
        bio {
          bio
        }
        avatar {
          gatsbyImageData(
            width: 150
            formats: AUTO
            placeholder: BLURRED
            quality: 80
          )
        }
      }
    }
  }
`;
<|endoftext|>


<commit_before>workersActions = {

    maybeHarvest: function(creep) {
        if(creep.memory.state == 'upgrading' && creep.carry.energy == 0) {
            Memory.lastUpgraded = Game.time;
        }
        if(creep.memory.state != 'harvesting' && creep.carry.energy == 0) {
            creep.memory.state = 'harvesting';
        }
    },

    harvest: function(creep) {
        if(creep.memory.state == 'harvesting' && creep.carry.energy == creep.carryCapacity) {
            _.pull(Memory.energySources[creep.memory.targetEnergy], creep.name);
            creep.memory.targetEnergy = null;
            creep.memory.state = 'deciding';
        }
        if(creep.memory.state == 'harvesting') {
            if(!('targetEnergy' in creep.memory) || creep.memory.targetEnergy == null) {
                workersActions.selectResource(creep);
            }
            var source = Game.getObjectById(creep.memory.targetEnergy);
            if(creep.harvest(source) == ERR_NOT_IN_RANGE) {
                creep.moveTo(source);
            }
        }
    },

    selectResource: function(creep) {

        // initialize the global registration if it has not been already
        if(!('energySources' in Memory)) {
            Memory.energySources = {};
            sources = creep.room.find(FIND_SOURCES);
            for (var i=0; i < sources.length; i++) {
                Memory.energySources[sources[i].id] = [];
            }
        }

        // pick by two criteria, the one with least users or in the case of a tie closest
        var lowestCount = Infinity;
        var lowestSources = [];
        for (var source in Memory.energySources) {
            var currentLength = Memory.energySources[source].length;
            if(currentLength < lowestCount) {
                lowestCount = currentLength;
                lowestSources = [source];
            } else if(currentLength == lowestCount) {
                lowestSources.push(source)
            }
        }

        // register the creep on the source
        if(lowestSources.length == 1) {
            var source = lowestSources[0];
            Memory.energySources[source].push(creep.name)
            creep.memory.targetEnergy = source;
            return source;
        } else {
            // todo : replace this random decision with picking the closest
            var source = _.shuffle(lowestSources)[0];
            Memory.energySources[source].push(creep.name)
            creep.memory.targetEnergy = source;
            return source;
        }

    },

    upgrade: function(creep) {
        if(creep.upgradeController(creep.room.controller) == ERR_NOT_IN_RANGE) {
            creep.moveTo(creep.room.controller);
        }
    },

    build: function(creep) {
        // try to build item at target, if invalid target, look for ramparts and repair them
        if('buildTarget' in creep.memory && creep.memory.buildTarget != null) {
            var target = Game.getObjectById(creep.memory.buildTarget);
            var attempt = creep.build(target);
            if(attempt == ERR_NOT_IN_RANGE) {
                creep.moveTo(target);
            } else if(attempt == ERR_INVALID_TARGET) {
                var rampart = creep.memory.buildPos.lookFor(LOOK_STRUCTURES);
                _.remove(rampart, filter: (structure) => {
                    return structure.structureType != STRUCTURE_RAMPART;
                });
                if(rampart.length > 0) {
                    creep.repair(rampart[0]);
                }
                creep.memory.buildTarget = null;
                creep.memory.buildPos = null;
            }
        } else {
            // find something to build, and save it as the target
            var targets = creep.room.find(FIND_CONSTRUCTION_SITES);
            if(targets.length) {
                creep.memory.buildTarget = targets[0].id;
                creep.memory.buildPos = targets[0].pos;
            } else {
                creep.memory.state = 'deciding';
            }
        }
    },

    repair: function(creep) {
        if(!workersActions.repairType(creep, STRUCTURE_RAMPART, .5)) {
            if(!workersActions.repairType(creep, STRUCTURE_ROAD, .5)) {
                if(!workersActions.repairType(creep, STRUCTURE_WALL, .01)) {
                    creep.memory.state = 'deciding';
                }
            }
        }
    },

    repairType: function(creep, structureType, percentage) {
        var targets = creep.room.find(FIND_STRUCTURES, {
            filter: (structure) => {
                return structure.structureType == structureType && (structure.hits / structure.hitsMax < percentage);
            }
        });
        if(targets.length > 0) {
            // unlike other actions, the huge number of repair targets possible could make a huge mess of this!
            if(creep.repair(targets[0])) {
                creep.moveTo(targets[0]);
            }
            return true;
        } else {
            return false;
        }
    },

    store: function(creep) {
        var targets = creep.room.find(FIND_STRUCTURES, {
            filter: (structure) => {
                return (structure.structureType == STRUCTURE_EXTENSION ||
                    structure.structureType == STRUCTURE_SPAWN ||
                    structure.structureType == STRUCTURE_TOWER) && structure.energy < structure.energyCapacity;
            }
        });
        if(targets.length > 0) {
            if(creep.transfer(targets[0], RESOURCE_ENERGY) == ERR_NOT_IN_RANGE) {
                creep.moveTo(targets[0]);
            }
        } else {
            creep.memory.state = 'deciding';
        }
    }
};

module.exports = workersActions;
<commit_msg>syntax error<commit_after>workersActions = {

    maybeHarvest: function(creep) {
        if(creep.memory.state == 'upgrading' && creep.carry.energy == 0) {
            Memory.lastUpgraded = Game.time;
        }
        if(creep.memory.state != 'harvesting' && creep.carry.energy == 0) {
            creep.memory.state = 'harvesting';
        }
    },

    harvest: function(creep) {
        if(creep.memory.state == 'harvesting' && creep.carry.energy == creep.carryCapacity) {
            _.pull(Memory.energySources[creep.memory.targetEnergy], creep.name);
            creep.memory.targetEnergy = null;
            creep.memory.state = 'deciding';
        }
        if(creep.memory.state == 'harvesting') {
            if(!('targetEnergy' in creep.memory) || creep.memory.targetEnergy == null) {
                workersActions.selectResource(creep);
            }
            var source = Game.getObjectById(creep.memory.targetEnergy);
            if(creep.harvest(source) == ERR_NOT_IN_RANGE) {
                creep.moveTo(source);
            }
        }
    },

    selectResource: function(creep) {

        // initialize the global registration if it has not been already
        if(!('energySources' in Memory)) {
            Memory.energySources = {};
            sources = creep.room.find(FIND_SOURCES);
            for (var i=0; i < sources.length; i++) {
                Memory.energySources[sources[i].id] = [];
            }
        }

        // pick by two criteria, the one with least users or in the case of a tie closest
        var lowestCount = Infinity;
        var lowestSources = [];
        for (var source in Memory.energySources) {
            var currentLength = Memory.energySources[source].length;
            if(currentLength < lowestCount) {
                lowestCount = currentLength;
                lowestSources = [source];
            } else if(currentLength == lowestCount) {
                lowestSources.push(source)
            }
        }

        // register the creep on the source
        if(lowestSources.length == 1) {
            var source = lowestSources[0];
            Memory.energySources[source].push(creep.name)
            creep.memory.targetEnergy = source;
            return source;
        } else {
            // todo : replace this random decision with picking the closest
            var source = _.shuffle(lowestSources)[0];
            Memory.energySources[source].push(creep.name)
            creep.memory.targetEnergy = source;
            return source;
        }

    },

    upgrade: function(creep) {
        if(creep.upgradeController(creep.room.controller) == ERR_NOT_IN_RANGE) {
            creep.moveTo(creep.room.controller);
        }
    },

    build: function(creep) {
        // try to build item at target, if invalid target, look for ramparts and repair them
        if('buildTarget' in creep.memory && creep.memory.buildTarget != null) {
            var target = Game.getObjectById(creep.memory.buildTarget);
            var attempt = creep.build(target);
            if(attempt == ERR_NOT_IN_RANGE) {
                creep.moveTo(target);
            } else if(attempt == ERR_INVALID_TARGET) {
                var rampart = creep.memory.buildPos.lookFor(LOOK_STRUCTURES);
                _.remove(rampart, (structure) => {
                    return structure.structureType != STRUCTURE_RAMPART;
                });
                if(rampart.length > 0) {
                    creep.repair(rampart[0]);
                }
                creep.memory.buildTarget = null;
                creep.memory.buildPos = null;
            }
        } else {
            // find something to build, and save it as the target
            var targets = creep.room.find(FIND_CONSTRUCTION_SITES);
            if(targets.length) {
                creep.memory.buildTarget = targets[0].id;
                creep.memory.buildPos = targets[0].pos;
            } else {
                creep.memory.state = 'deciding';
            }
        }
    },

    repair: function(creep) {
        if(!workersActions.repairType(creep, STRUCTURE_RAMPART, .5)) {
            if(!workersActions.repairType(creep, STRUCTURE_ROAD, .5)) {
                if(!workersActions.repairType(creep, STRUCTURE_WALL, .01)) {
                    creep.memory.state = 'deciding';
                }
            }
        }
    },

    repairType: function(creep, structureType, percentage) {
        var targets = creep.room.find(FIND_STRUCTURES, {
            filter: (structure) => {
                return structure.structureType == structureType && (structure.hits / structure.hitsMax < percentage);
            }
        });
        if(targets.length > 0) {
            // unlike other actions, the huge number of repair targets possible could make a huge mess of this!
            if(creep.repair(targets[0])) {
                creep.moveTo(targets[0]);
            }
            return true;
        } else {
            return false;
        }
    },

    store: function(creep) {
        var targets = creep.room.find(FIND_STRUCTURES, {
            filter: (structure) => {
                return (structure.structureType == STRUCTURE_EXTENSION ||
                    structure.structureType == STRUCTURE_SPAWN ||
                    structure.structureType == STRUCTURE_TOWER) && structure.energy < structure.energyCapacity;
            }
        });
        if(targets.length > 0) {
            if(creep.transfer(targets[0], RESOURCE_ENERGY) == ERR_NOT_IN_RANGE) {
                creep.moveTo(targets[0]);
            }
        } else {
            creep.memory.state = 'deciding';
        }
    }
};

module.exports = workersActions;
<|endoftext|>


<commit_before>import Vector2 from '../../../lib/src/math/Vector2.js';

const VECTOR2 = new Vector2();

const DOM_MATRIX_A = new DOMMatrix();
const DOM_MATRIX_B = new DOMMatrix();

export default class ViewportElement extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: 'open' }).innerHTML = `

      <slot></slot>

    `;

    const content = this.shadowRoot.querySelector('#content');
    this._styleSheet = this.shadowRoot.querySelector('style').sheet;
    this._slotUID = 0;
    this._slots = new Set();
    this._selectedElements = new Set();
    const slotElementMap = new Map();
    const elementSlotMap = new Map();

    this.preventManipulation = function (event) {
      return false;
    };

    // Drag/Zoom

    const pointerEventMap = new Map();
    const pointerTargetMap = new Map();
    const targetPointersMap = new Map();

    let previousPinchSize = 0;

    let firstClientX = 0;
    let firstClientY = 0;

    const zoom = (scale, x, y) => {
      DOM_MATRIX_B.setMatrixValue('');
      DOM_MATRIX_B.scaleSelf(scale);
      DOM_MATRIX_B.m41 = -x * (scale - 1);
      DOM_MATRIX_B.m42 = -y * (scale - 1);

      for (const slot of this._slots) {
        DOM_MATRIX_A.setMatrixValue(slot.style.transform);
        DOM_MATRIX_A.preMultiplySelf(DOM_MATRIX_B);
        slot.style.transformOrigin = 'top left';
        slot.style.transform = DOM_MATRIX_A.toString();
      }
    };

    const reset = () => {
      previousPinchSize = 0;
      firstClientX = 0;
      firstClientY = 0;
    };

    const onPointerDown = (event) => {
      reset();

      if (event.target !== event.currentTarget && event.target.scrollHeight > event.target.offsetHeight) {
        return;
      }
      if (pointerEventMap.has(event.pointerId)) {
        return;
      }
      if (this.preventManipulation(event)) {
        return;
      }

      if (!pointerEventMap.size) {
        window.addEventListener('pointermove', onPointerMove);
        window.addEventListener('pointerup', onPointerUp);
        window.addEventListener('pointerout', onPointerUp);
      }
      pointerEventMap.set(event.pointerId, event);
      pointerTargetMap.set(event.pointerId, event.currentTarget);
      let targetPointersSet = targetPointersMap.get(event.currentTarget);
      if (!targetPointersSet) {
        targetPointersSet = new Set();
        targetPointersMap.set(event.currentTarget, targetPointersSet);
        const currentElement = slotElementMap.get(event.currentTarget);
        if (!event.shiftKey && !this._selectedElements.has(currentElement) || event.currentTarget === this) {
          for (const element of this._selectedElements) {
            const slot = elementSlotMap.get(element);
            slot.removeAttribute('selected');
          }
          this._selectedElements.clear();
        }
        if (event.currentTarget !== this) {
          event.currentTarget.setAttribute('disabled', '');
          if (event.shiftKey && this._selectedElements.has(currentElement)) {
            event.currentTarget.removeAttribute('selected');
            this._selectedElements.delete(currentElement);
          } else {
            event.currentTarget.setAttribute('selected', '');
            this._selectedElements.add(slotElementMap.get(event.currentTarget));
          }
        }
      }
      targetPointersSet.add(event.pointerId);
    };

    const onPointerMove = (event) => {
      if (!pointerEventMap.has(event.pointerId)) {
        return;
      }

      pointerEventMap.set(event.pointerId, event);
      const pointerIds = [...pointerEventMap.keys()];

      let isViewport = false;
      for (const target of pointerTargetMap.values()) {
        if (target === this) {
          isViewport = true;
        }
      }

      if (isViewport) {
        let sumMovementX = 0;
        let sumMovementY = 0;
        for (const pointer of pointerEventMap.values()) {
          sumMovementX += pointer.movementX;
          sumMovementY += pointer.movementY;
        }
        sumMovementX /= pointerEventMap.size;
        sumMovementY /= pointerEventMap.size;

        for (const slot of this._slots) {
          this._offsetElement(slot, sumMovementX / window.devicePixelRatio, sumMovementY / window.devicePixelRatio);
        }
      } else if (!isViewport) {
        const slot = pointerTargetMap.get(event.pointerId);
        const targetPointersSet = targetPointersMap.get(slot);

        let sumMovementX = 0;
        let sumMovementY = 0;
        for (const pointer of targetPointersSet) {
          const pointerEvent = pointerEventMap.get(pointer);
          sumMovementX += pointerEvent.movementX;
          sumMovementY += pointerEvent.movementY;
        }
        sumMovementX /= targetPointersSet.size * targetPointersSet.size;
        sumMovementY /= targetPointersSet.size * targetPointersSet.size;
        sumMovementX /= window.devicePixelRatio;
        sumMovementY /= window.devicePixelRatio;

        if (pointerTargetMap.size > 1) {
          this._offsetElement(slot, sumMovementX, sumMovementY);
        } else {
          for (const element of this._selectedElements) {
            const slot = elementSlotMap.get(element);
            this._offsetElement(slot, sumMovementX, sumMovementY);
          }
        }
        if (targetPointersSet.size === 2) {
          if (slot.style.resize === 'none') {
            isViewport = true;
          } else {
            const boundingClientRect = slot.getBoundingClientRect();

            DOM_MATRIX_A.setMatrixValue(slot.style.transform);

            let otherPointerEvent;

            for (const pointer of targetPointersSet) {
              const pointerEvent = pointerEventMap.get(pointer);
              if (pointerEvent !== event) {
                otherPointerEvent = pointerEvent;
              }
            }

            const pinchOffsetLeft = (event.clientX < otherPointerEvent.clientX ? event.movementX : 0) / window.devicePixelRatio;
            const pinchOffsetRight = (event.clientX > otherPointerEvent.clientX ? event.movementX : 0) / window.devicePixelRatio;
            const pinchOffsetTop = (event.clientY < otherPointerEvent.clientY ? event.movementY : 0) / window.devicePixelRatio;
            const pinchOffsetBottom = (event.clientY > otherPointerEvent.clientY ? event.movementY : 0) / window.devicePixelRatio;

            if (slot.style.resize === 'both' || slot.style.resize === 'horizontal') {
              slot.style.width = `${(boundingClientRect.width - pinchOffsetLeft + pinchOffsetRight) * (1 / DOM_MATRIX_A.a)}px`;
              DOM_MATRIX_A.m41 += pinchOffsetLeft - sumMovementX;
            }

            if (slot.style.resize === 'both' || slot.style.resize === 'vertical') {
              slot.style.height = `${(boundingClientRect.height - pinchOffsetTop + pinchOffsetBottom) * (1 / DOM_MATRIX_A.d)}px`;
              DOM_MATRIX_A.m42 += pinchOffsetTop - sumMovementY;
            }

            slot.style.transform = DOM_MATRIX_A.toString();
          }
        }
      }

      if (event.pointerId === pointerIds[0]) {
        firstClientX = event.clientX;
        firstClientY = event.clientY;
      }
      if (event.pointerId === pointerIds[1]) {
        if (firstClientX || firstClientY) {
          const x = (firstClientX + event.clientX) * .5 - this.clientWidth * .5;
          const y = (firstClientY + event.clientY) * .5 - this.clientHeight * .5;
          VECTOR2.x = firstClientX - event.clientX;
          VECTOR2.y = firstClientY - event.clientY;

          const pinchSize = VECTOR2.size;

          if (isViewport && previousPinchSize) {
            zoom(pinchSize / previousPinchSize, x, y);
          }

          previousPinchSize = pinchSize;
        }
      }
    };

    const onPointerUp = (event) => {
      reset();
      event.preventDefault();
      if (event.pointerType === 'mouse' && event.type === 'pointerout' || !pointerEventMap.has(event.pointerId)) {
        return;
      }
      const target = pointerTargetMap.get(event.pointerId);
      const targetPointersSet = targetPointersMap.get(target);
      targetPointersSet.delete(event.pointerId);
      if (!targetPointersSet.size) {
        targetPointersMap.delete(target);
        target.removeAttribute('disabled');
      }
      pointerEventMap.delete(event.pointerId);
      pointerTargetMap.delete(event.pointerId);
      if (!pointerEventMap.size) {
        window.removeEventListener('pointermove', onPointerMove);
        window.removeEventListener('pointerup', onPointerUp);
        window.removeEventListener('pointerout', onPointerUp);
      }
    };

    this.addEventListener('pointerdown', onPointerDown);

    this.addEventListener('wheel', (event) => {
      if (event.target.scrollHeight > event.target.clientHeight) {
        return;
      }
      const scale = 1 + (event.deltaY < 0 ? 1 : -1) * .1;
      const x = event.clientX - this.clientWidth * .5;
      const y = event.clientY - this.clientHeight * .5;
      zoom(scale, x, y);
    });

    // Mutation Observer
    const mutationCallback = (mutationsList) => {
      for (const mutation of mutationsList) {
        for (const node of mutation.addedNodes) {
          if (!(node instanceof HTMLElement)) {
            continue;
          }
          const boundingClientRect = node.getBoundingClientRect();
          const slot = document.createElement('slot');
          slot.name = `viewport-slot-${this._slotUID}`;
          node.slot = slot.name;
          slot.style.transform = `translate(${boundingClientRect.x}px, ${boundingClientRect.y}px)`;
          const style = getComputedStyle(node);
          slot.style.resize = style.resize;
          if (style.resize !== 'none') {
            let ruleString = `[name="${slot.name}"]::slotted(*) {`;
            if (/both|horizontal/.test(style.resize)) {
              slot.style.width = `${boundingClientRect.width}px`;
              ruleString += 'width: 100% !important;';
            }
            if (/both|vertical/.test(style.resize)) {
              slot.style.height = `${boundingClientRect.height}px`;
              ruleString += 'height: 100% !important;';
            }
            ruleString += '}';
            this._styleSheet.insertRule(ruleString, this._styleSheet.cssRules.length);
          }
          content.appendChild(slot);
          this._slotUID++;
          this._slots.add(slot);
          slotElementMap.set(slot, node);
          elementSlotMap.set(node, slot);
          slot.addEventListener('pointerdown', onPointerDown);
        }
        for (const node of mutation.removedNodes) {
          if (!(node instanceof HTMLElement)) {
            continue;
          }
          const slot = elementSlotMap.get(node);
          slot.removeEventListener('pointerdown', onPointerDown);
          slotElementMap.delete(slot);
          this._slots.delete(slot);
          slot.remove();
          for (const [index, rule] of [...this._styleSheet.cssRules].entries()) {
            if (rule.selectorText === `[name="${slot.name}"]::slotted(*)`) {
              this._styleSheet.deleteRule(index);
            }
          }
        }
      }
    };
    mutationCallback([{
      addedNodes: this.children,
      removedNodes: [],
    }]);
    const observer = new MutationObserver(mutationCallback);
    observer.observe(this, { childList: true });
  }

  get selectedElements() {
    return this._selectedElements;
  }

  centerView() {
    const domRect = new DOMRect(Infinity, Infinity, 0, 0);
    for (const slot of this._slots) {
      const boundingClientRect = slot.getBoundingClientRect();
      domRect.x = Math.min(domRect.x, boundingClientRect.x);
      domRect.y = Math.min(domRect.y, boundingClientRect.y);
      domRect.width = Math.max(domRect.width, boundingClientRect.x + boundingClientRect.width - domRect.x);
      domRect.height = Math.max(domRect.height, boundingClientRect.y + boundingClientRect.height - domRect.y);
    }

    const offsetX = -domRect.x - domRect.width * .5 + this.clientWidth * .5;
    const offsetY = -domRect.y - domRect.height * .5 + this.clientHeight * .5;
    for (const slot of this._slots) {
      this._offsetElement(slot, offsetX, offsetY);
    }
  }

  _offsetElement(element, offsetX, offsetY) {
    DOM_MATRIX_A.setMatrixValue(element.style.transform);
    DOM_MATRIX_A.m41 += offsetX;
    DOM_MATRIX_A.m42 += offsetY;
    element.style.transform = DOM_MATRIX_A.toString();
  }
}
<commit_msg>fix viewport zooming<commit_after>import Vector2 from '../../../lib/src/math/Vector2.js';

const VECTOR2 = new Vector2();

const DOM_MATRIX_A = new DOMMatrix();
const DOM_MATRIX_B = new DOMMatrix();

export default class ViewportElement extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: 'open' }).innerHTML = `

      <slot></slot>

    `;

    const content = this.shadowRoot.querySelector('#content');
    this._styleSheet = this.shadowRoot.querySelector('style').sheet;
    this._slotUID = 0;
    this._slots = new Set();
    this._selectedElements = new Set();
    const slotElementMap = new Map();
    const elementSlotMap = new Map();

    this.preventManipulation = function (event) {
      return false;
    };

    // Drag/Zoom

    const pointerEventMap = new Map();
    const pointerTargetMap = new Map();
    const targetPointersMap = new Map();

    let previousPinchSize = 0;

    let firstClientX = 0;
    let firstClientY = 0;

    const zoom = (scale, x, y) => {
      DOM_MATRIX_B.setMatrixValue('');
      DOM_MATRIX_B.scaleSelf(scale);
      DOM_MATRIX_B.m41 = -x * (scale - 1);
      DOM_MATRIX_B.m42 = -y * (scale - 1);

      for (const slot of this._slots) {
        DOM_MATRIX_A.setMatrixValue(slot.style.transform);
        DOM_MATRIX_A.preMultiplySelf(DOM_MATRIX_B);
        slot.style.transformOrigin = 'top left';
        slot.style.transform = DOM_MATRIX_A.toString();
      }
    };

    const reset = () => {
      previousPinchSize = 0;
      firstClientX = 0;
      firstClientY = 0;
    };

    const onPointerDown = (event) => {
      reset();

      if (event.target !== event.currentTarget && event.target.scrollHeight > event.target.offsetHeight) {
        return;
      }
      if (pointerEventMap.has(event.pointerId)) {
        return;
      }
      if (this.preventManipulation(event)) {
        return;
      }

      if (!pointerEventMap.size) {
        window.addEventListener('pointermove', onPointerMove);
        window.addEventListener('pointerup', onPointerUp);
        window.addEventListener('pointerout', onPointerUp);
      }
      pointerEventMap.set(event.pointerId, event);
      pointerTargetMap.set(event.pointerId, event.currentTarget);
      let targetPointersSet = targetPointersMap.get(event.currentTarget);
      if (!targetPointersSet) {
        targetPointersSet = new Set();
        targetPointersMap.set(event.currentTarget, targetPointersSet);
        const currentElement = slotElementMap.get(event.currentTarget);
        if (!event.shiftKey && !this._selectedElements.has(currentElement) || event.currentTarget === this) {
          for (const element of this._selectedElements) {
            const slot = elementSlotMap.get(element);
            slot.removeAttribute('selected');
          }
          this._selectedElements.clear();
        }
        if (event.currentTarget !== this) {
          event.currentTarget.setAttribute('disabled', '');
          if (event.shiftKey && this._selectedElements.has(currentElement)) {
            event.currentTarget.removeAttribute('selected');
            this._selectedElements.delete(currentElement);
          } else {
            event.currentTarget.setAttribute('selected', '');
            this._selectedElements.add(slotElementMap.get(event.currentTarget));
          }
        }
      }
      targetPointersSet.add(event.pointerId);
    };

    const onPointerMove = (event) => {
      if (!pointerEventMap.has(event.pointerId)) {
        return;
      }

      pointerEventMap.set(event.pointerId, event);
      const pointerIds = [...pointerEventMap.keys()];

      let isViewport = false;
      for (const target of pointerTargetMap.values()) {
        if (target === this) {
          isViewport = true;
        }
      }

      if (isViewport) {
        let sumMovementX = 0;
        let sumMovementY = 0;
        for (const pointer of pointerEventMap.values()) {
          sumMovementX += pointer.movementX;
          sumMovementY += pointer.movementY;
        }
        sumMovementX /= pointerEventMap.size;
        sumMovementY /= pointerEventMap.size;

        for (const slot of this._slots) {
          this._offsetElement(slot, sumMovementX / window.devicePixelRatio, sumMovementY / window.devicePixelRatio);
        }
      } else if (!isViewport) {
        const slot = pointerTargetMap.get(event.pointerId);
        const targetPointersSet = targetPointersMap.get(slot);

        let sumMovementX = 0;
        let sumMovementY = 0;
        for (const pointer of targetPointersSet) {
          const pointerEvent = pointerEventMap.get(pointer);
          sumMovementX += pointerEvent.movementX;
          sumMovementY += pointerEvent.movementY;
        }
        sumMovementX /= targetPointersSet.size * targetPointersSet.size;
        sumMovementY /= targetPointersSet.size * targetPointersSet.size;
        sumMovementX /= window.devicePixelRatio;
        sumMovementY /= window.devicePixelRatio;

        if (pointerTargetMap.size > 1) {
          this._offsetElement(slot, sumMovementX, sumMovementY);
        } else {
          for (const element of this._selectedElements) {
            const slot = elementSlotMap.get(element);
            this._offsetElement(slot, sumMovementX, sumMovementY);
          }
        }
        if (targetPointersSet.size === 2) {
          if (slot.style.resize === 'none') {
            isViewport = true;
          } else {
            const boundingClientRect = slot.getBoundingClientRect();

            DOM_MATRIX_A.setMatrixValue(slot.style.transform);

            let otherPointerEvent;

            for (const pointer of targetPointersSet) {
              const pointerEvent = pointerEventMap.get(pointer);
              if (pointerEvent !== event) {
                otherPointerEvent = pointerEvent;
              }
            }

            const pinchOffsetLeft = (event.clientX < otherPointerEvent.clientX ? event.movementX : 0) / window.devicePixelRatio;
            const pinchOffsetRight = (event.clientX > otherPointerEvent.clientX ? event.movementX : 0) / window.devicePixelRatio;
            const pinchOffsetTop = (event.clientY < otherPointerEvent.clientY ? event.movementY : 0) / window.devicePixelRatio;
            const pinchOffsetBottom = (event.clientY > otherPointerEvent.clientY ? event.movementY : 0) / window.devicePixelRatio;

            if (slot.style.resize === 'both' || slot.style.resize === 'horizontal') {
              slot.style.width = `${(boundingClientRect.width - pinchOffsetLeft + pinchOffsetRight) * (1 / DOM_MATRIX_A.a)}px`;
              DOM_MATRIX_A.m41 += pinchOffsetLeft - sumMovementX;
            }

            if (slot.style.resize === 'both' || slot.style.resize === 'vertical') {
              slot.style.height = `${(boundingClientRect.height - pinchOffsetTop + pinchOffsetBottom) * (1 / DOM_MATRIX_A.d)}px`;
              DOM_MATRIX_A.m42 += pinchOffsetTop - sumMovementY;
            }

            slot.style.transform = DOM_MATRIX_A.toString();
          }
        }
      }

      if (event.pointerId === pointerIds[0]) {
        firstClientX = event.clientX;
        firstClientY = event.clientY;
      }
      if (event.pointerId === pointerIds[1]) {
        if (firstClientX || firstClientY) {
          const x = (firstClientX + event.clientX) * .5 - this.clientWidth * .5;
          const y = (firstClientY + event.clientY) * .5 - this.clientHeight * .5;
          VECTOR2.x = firstClientX - event.clientX;
          VECTOR2.y = firstClientY - event.clientY;

          const pinchSize = VECTOR2.size;

          if (isViewport && previousPinchSize) {
            zoom(pinchSize / previousPinchSize, x, y);
          }

          previousPinchSize = pinchSize;
        }
      }
    };

    const onPointerUp = (event) => {
      reset();
      event.preventDefault();
      if (event.pointerType === 'mouse' && event.type === 'pointerout' || !pointerEventMap.has(event.pointerId)) {
        return;
      }
      const target = pointerTargetMap.get(event.pointerId);
      const targetPointersSet = targetPointersMap.get(target);
      targetPointersSet.delete(event.pointerId);
      if (!targetPointersSet.size) {
        targetPointersMap.delete(target);
        target.removeAttribute('disabled');
      }
      pointerEventMap.delete(event.pointerId);
      pointerTargetMap.delete(event.pointerId);
      if (!pointerEventMap.size) {
        window.removeEventListener('pointermove', onPointerMove);
        window.removeEventListener('pointerup', onPointerUp);
        window.removeEventListener('pointerout', onPointerUp);
      }
    };

    this.addEventListener('pointerdown', onPointerDown);

    this.addEventListener('wheel', (event) => {
      if (event.target !== this && event.target.scrollHeight > event.target.clientHeight) {
        return;
      }
      const scale = 1 + (event.deltaY < 0 ? 1 : -1) * .1;
      const x = event.clientX - this.clientWidth * .5;
      const y = event.clientY - this.clientHeight * .5;
      zoom(scale, x, y);
    });

    // Mutation Observer
    const mutationCallback = (mutationsList) => {
      for (const mutation of mutationsList) {
        for (const node of mutation.addedNodes) {
          if (!(node instanceof HTMLElement)) {
            continue;
          }
          const boundingClientRect = node.getBoundingClientRect();
          const slot = document.createElement('slot');
          slot.name = `viewport-slot-${this._slotUID}`;
          node.slot = slot.name;
          slot.style.transform = `translate(${boundingClientRect.x}px, ${boundingClientRect.y}px)`;
          const style = getComputedStyle(node);
          slot.style.resize = style.resize;
          if (style.resize !== 'none') {
            let ruleString = `[name="${slot.name}"]::slotted(*) {`;
            if (/both|horizontal/.test(style.resize)) {
              slot.style.width = `${boundingClientRect.width}px`;
              ruleString += 'width: 100% !important;';
            }
            if (/both|vertical/.test(style.resize)) {
              slot.style.height = `${boundingClientRect.height}px`;
              ruleString += 'height: 100% !important;';
            }
            ruleString += '}';
            this._styleSheet.insertRule(ruleString, this._styleSheet.cssRules.length);
          }
          content.appendChild(slot);
          this._slotUID++;
          this._slots.add(slot);
          slotElementMap.set(slot, node);
          elementSlotMap.set(node, slot);
          slot.addEventListener('pointerdown', onPointerDown);
        }
        for (const node of mutation.removedNodes) {
          if (!(node instanceof HTMLElement)) {
            continue;
          }
          const slot = elementSlotMap.get(node);
          slot.removeEventListener('pointerdown', onPointerDown);
          slotElementMap.delete(slot);
          this._slots.delete(slot);
          slot.remove();
          for (const [index, rule] of [...this._styleSheet.cssRules].entries()) {
            if (rule.selectorText === `[name="${slot.name}"]::slotted(*)`) {
              this._styleSheet.deleteRule(index);
            }
          }
        }
      }
    };
    mutationCallback([{
      addedNodes: this.children,
      removedNodes: [],
    }]);
    const observer = new MutationObserver(mutationCallback);
    observer.observe(this, { childList: true });
  }

  get selectedElements() {
    return this._selectedElements;
  }

  centerView() {
    const domRect = new DOMRect(Infinity, Infinity, 0, 0);
    for (const slot of this._slots) {
      const boundingClientRect = slot.getBoundingClientRect();
      domRect.x = Math.min(domRect.x, boundingClientRect.x);
      domRect.y = Math.min(domRect.y, boundingClientRect.y);
      domRect.width = Math.max(domRect.width, boundingClientRect.x + boundingClientRect.width - domRect.x);
      domRect.height = Math.max(domRect.height, boundingClientRect.y + boundingClientRect.height - domRect.y);
    }

    const offsetX = -domRect.x - domRect.width * .5 + this.clientWidth * .5;
    const offsetY = -domRect.y - domRect.height * .5 + this.clientHeight * .5;
    for (const slot of this._slots) {
      this._offsetElement(slot, offsetX, offsetY);
    }
  }

  _offsetElement(element, offsetX, offsetY) {
    DOM_MATRIX_A.setMatrixValue(element.style.transform);
    DOM_MATRIX_A.m41 += offsetX;
    DOM_MATRIX_A.m42 += offsetY;
    element.style.transform = DOM_MATRIX_A.toString();
  }
}
<|endoftext|>


<commit_before>// Require external dependencies.
var spawn = require('child_process').spawn;
var bake = require('blake').bake;

// Spawn child process with specified path and execute git pull.
var pull = function (path, callback) {
  spawn('git', ['pull'], { cwd: path }).on('exit', function (code) {
    return callback(code === 0 ? null : new Error(code));
  });
};

// We do very little to secure this post-receive hook. We assume that the
// URL of the post-receive hook is secret—not '/publish'. Here we check
// wether the request is a post request from one of GitHub's IP addresses
// and if it contains a payload object.
var validate = function (addresses, request, callback) {
  console.log(request);

  if (request.method != 'POST') {
    return callback(false);
  }

  var data = '';

  request.on('data', function (chunk) {
    data += chunk;
  });

  request.on('end', function () {
    if (!data) {
      return callback(false);
    }

    var isGitHub = function (remoteAddress) {
      var length = addresses.length;
      var address;

      for (var i = 0; i <= length; i++) {
        address = addresses[i];
        if (remoteAddress == address) return true;
      }

      return false;
    };

    if (!isGitHub(request.connection.remoteAddress)) {
      return callback(false);
    }

    var value = data.split('payload=')[1];

    if (!value) {
      return callback(false);
    }

    try {
      var payload = JSON.parse(unescape(value) || null);
    } catch(err) {
      return callback(false);
    }

    if (!payload) {
      return callback(false);
    }

    console.log(payload);

    return callback(true);
  });
};

// If the request is valid, pull latest version of input data, generate the
// site from it, and apply callback. If the request is not qualified for
// publication, apply callback with error.
module.exports = function (config, request, response, callback) {
  validate(config.addresses, request, function (isValid) {
    response.writeHead(200);
    response.end();

    if (!isValid) {
      return callback(new Error('Invalid post-receive request'));
    } else {
      pull(config.input, function (err) {
        if (err) {
          return callback(err);
        }

        bake(config.input, config.output, function (err) {
          callback(err);
        });
      });
    }
  });
};
<commit_msg>Improve publish<commit_after>// Require external dependencies.
var spawn = require('child_process').spawn;
var bake = require('blake').bake;

// Spawn child process with specified path and execute git pull.
var pull = function (path, callback) {
  spawn('git', ['pull'], { cwd: path }).on('exit', function (code) {
    return callback(code === 0 ? null : new Error(code));
  });
};

// We do very little to secure this post-receive hook. We assume that the
// URL of the post-receive hook is secret—not '/publish'. Here we check
// wether the request is a post request from one of GitHub's IP addresses
// and if it contains a payload object.
var validate = function (addresses, request, callback) {
  console.log(request);

  if (request.method != 'POST') {
    return callback(false);
  }

  var data = '';

  request.on('data', function (chunk) {
    data += chunk;
  });

  request.on('end', function () {
    if (!data) {
      return callback(false);
    }

    var isGitHub = function (remoteAddress) {
      var i = addresses.length;
      var address;

      while (i--) {
        address = addresses[i];
        if (remoteAddress === address) return true;
      }

      return false;
    };

    if (!isGitHub(request.connection.remoteAddress)) {
      return callback(false);
    }

    var value = data.split('payload=')[1];

    if (!value) {
      return callback(false);
    }

    try {
      var payload = JSON.parse(unescape(value) || null);
    } catch(err) {
      return callback(false);
    }

    if (!payload) {
      return callback(false);
    }

    console.log(payload);

    return callback(true);
  });
};

// If the request is valid, pull latest version of input data, generate the
// site from it, and apply callback. If the request is not qualified for
// publication, apply callback with error.
module.exports = function (config, request, response, callback) {
  validate(config.addresses, request, function (isValid) {
    response.writeHead(200);
    response.end();

    if (!isValid) {
      return callback(new Error('Invalid post-receive request'));
    } else {
      pull(config.input, function (err) {
        if (err) {
          return callback(err);
        }

        bake(config.input, config.output, function (err) {
          callback(err);
        });
      });
    }
  });
};
<|endoftext|>