/**
- * Manager responsible for the communication between the experiment running in the participant's browser and the pavlovia.org server.
+ * Manager responsible for the communication between the experiment running in the participant's browser and the
+ * pavlovia.org server.
*
* @author Alain Pitiot
- * @version 2021.2.0
- * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2021 Open Science Tools Ltd. (https://opensciencetools.org)
+ * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2024 Open Science Tools Ltd.
+ * (https://opensciencetools.org)
* @license Distributed under the terms of the MIT License
*/
-import { Howl } from 'howler';
-import {PsychoJS} from './PsychoJS';
-import {PsychObject} from '../util/PsychObject';
-import * as util from '../util/Util';
-import {ExperimentHandler} from "../data/ExperimentHandler";
-import {MonotonicClock} from "../util/Clock";
-
+import { Howl } from "howler";
+import { ExperimentHandler } from "../data/ExperimentHandler.js";
+import { Clock, MonotonicClock } from "../util/Clock.js";
+import { PsychObject } from "../util/PsychObject.js";
+import * as util from "../util/Util.js";
+import { Scheduler } from "../util/Scheduler.js";
+import { PsychoJS } from "./PsychoJS.js";
/**
- * <p>This manager handles all communications between the experiment running in the participant's browser and the [pavlovia.org]{@link http://pavlovia.org} server, <em>in an asynchronous manner</em>.</p>
- * <p>It is responsible for reading the configuration file of an experiment, for opening and closing a session, for listing and downloading resources, and for uploading results, logs, and audio recordings.</p>
+ * <p>This manager handles all communications between the experiment running in the participant's browser and the
+ * [pavlovia.org]{@link http://pavlovia.org} server, <em>in an asynchronous manner</em>.</p>
+ * <p>It is responsible for reading the configuration file of an experiment, for opening and closing a session, for
+ * listing and downloading resources, and for uploading results, logs, and audio recordings.</p>
*
- * @name module:core.ServerManager
- * @class
* @extends PsychObject
- * @param {Object} options
- * @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
- * @param {boolean} [options.autoLog= false] - whether or not to log
*/
export class ServerManager extends PsychObject
{
@@ -60,17 +82,22 @@ Source: core/ServerManager.js
* Used to indicate to the ServerManager that all resources must be registered (and
* subsequently downloaded)
*
- * @type {symbol}
+ * @type {Symbol}
* @readonly
* @public
*/
- static ALL_RESOURCES = Symbol.for('ALL_RESOURCES');
-
+ static ALL_RESOURCES = Symbol.for("ALL_RESOURCES");
+ /**
+ * @memberof module:core
+ * @param {Object} options
+ * @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
+ * @param {boolean} [options.autoLog= false] - whether or not to log
+ */
constructor({
- psychoJS,
- autoLog = false
- } = {})
+ psychoJS,
+ autoLog = false,
+ } = {})
{
super(psychoJS);
@@ -79,11 +106,19 @@ Source: core/ServerManager.js
// resources is a map of <name: string, { path: string, status: ResourceStatus, data: any }>
this._resources = new Map();
+ this._nbLoadedResources = 0;
+ this._setupPreloadQueue();
- this._addAttribute('autoLog', autoLog);
- this._addAttribute('status', ServerManager.Status.READY);
- }
+ // throttling period for calls to uploadData and uploadLog (in mn):
+ // note: (a) the period is potentially updated when a session is opened to reflect that associated with
+ // the experiment on the back-end database
+ // (b) throttling is also enforced on the back-end: artificially altering the period
+ // on the participant's browser will result in server errors
+ this._uploadThrottlePeriod = 5;
+ this._addAttribute("autoLog", autoLog);
+ this._addAttribute("status", ServerManager.Status.READY);
+ }
/**
* @typedef ServerManager.GetConfigurationPromise
@@ -95,43 +130,53 @@ Source: core/ServerManager.js
/**
* Read the configuration file for the experiment.
*
- * @name module:core.ServerManager#getConfiguration
- * @function
- * @public
* @param {string} configURL - the URL of the configuration file
- *
* @returns {Promise<ServerManager.GetConfigurationPromise>} the response
*/
getConfiguration(configURL)
{
const response = {
- origin: 'ServerManager.getConfiguration',
- context: 'when reading the configuration file: ' + configURL
+ origin: "ServerManager.getConfiguration",
+ context: "when reading the configuration file: " + configURL,
};
- this._psychoJS.logger.debug('reading the configuration file: ' + configURL);
+ this._psychoJS.logger.debug("reading the configuration file: " + configURL);
const self = this;
- return new Promise((resolve, reject) =>
+ return new Promise(async (resolve, reject) =>
{
- jQuery.get(configURL, 'json')
- .done((config, textStatus) =>
+ try
+ {
+ const getResponse = await fetch(configURL, {
+ method: "GET",
+ mode: "cors",
+ cache: "no-cache",
+ credentials: "same-origin",
+ redirect: "follow",
+ referrerPolicy: "no-referrer"
+ });
+ if (getResponse.status === 404)
{
- // resolve({ ...response, config });
- resolve(Object.assign(response, {config}));
- })
- .fail((jqXHR, textStatus, errorThrown) =>
+ throw "the configuration file could not be found";
+ }
+ else if (getResponse.status !== 200)
{
- self.setStatus(ServerManager.Status.ERROR);
+ throw `unable to read the configuration file: status= ${getResponse.status}`;
+ }
- const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
- console.error('error:', errorMsg);
+ // the configuration file should be valid json:
+ const config = await getResponse.json();
+ resolve(Object.assign(response, { config }));
+ }
+ catch (error)
+ {
+ self.setStatus(ServerManager.Status.ERROR);
+ console.error("error:", error);
- reject(Object.assign(response, {error: errorMsg}));
- });
+ reject(Object.assign(response, { error }));
+ }
});
}
-
/**
* @typedef ServerManager.OpenSessionPromise
* @property {string} origin the calling method
@@ -140,182 +185,187 @@ Source: core/ServerManager.js
* @property {Object.<string, *>} [error] an error message if we could not open the session
*/
/**
- * Open a session for this experiment on the remote PsychoJS manager.
+ * Open a session for this experiment on the pavlovia server.
+ *
+ * @param {Object} params - the open session parameters
*
- * @name module:core.ServerManager#openSession
- * @function
- * @public
* @returns {Promise<ServerManager.OpenSessionPromise>} the response
*/
- openSession()
+ openSession(params = {})
{
const response = {
- origin: 'ServerManager.openSession',
- context: 'when opening a session for experiment: ' + this._psychoJS.config.experiment.fullpath
+ origin: "ServerManager.openSession",
+ context: "when opening a session for experiment: " + this._psychoJS.config.experiment.fullpath,
};
-
- this._psychoJS.logger.debug('opening a session for experiment: ' + this._psychoJS.config.experiment.fullpath);
+ this._psychoJS.logger.debug("opening a session for experiment: " + this._psychoJS.config.experiment.fullpath);
this.setStatus(ServerManager.Status.BUSY);
- // prepare POST query:
- let data = {};
- if (this._psychoJS._serverMsg.has('__pilotToken'))
- {
- data.pilotToken = this._psychoJS._serverMsg.get('__pilotToken');
- }
-
- // query pavlovia server:
+ // query the server:
const self = this;
- return new Promise((resolve, reject) =>
+ return new Promise(async (resolve, reject) =>
{
- const url = this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) + '/sessions';
- jQuery.post(url, data, null, 'json')
- .done((data, textStatus) =>
- {
- if (!('token' in data))
- {
- self.setStatus(ServerManager.Status.ERROR);
- reject(Object.assign(response, {error: 'unexpected answer from server: no token'}));
- // reject({...response, error: 'unexpected answer from server: no token'});
- }
- if (!('experiment' in data))
- {
- self.setStatus(ServerManager.Status.ERROR);
- // reject({...response, error: 'unexpected answer from server: no experiment'});
- reject(Object.assign(response, {error: 'unexpected answer from server: no experiment'}));
- }
+ try
+ {
+ const postResponse = await this._queryServerAPI(
+ "POST",
+ `experiments/${this._psychoJS.config.gitlab.projectId}/sessions`,
+ params,
+ "FORM"
+ );
- self._psychoJS.config.session = {
- token: data.token,
- status: 'OPEN'
- };
- self._psychoJS.config.experiment.status = data.experiment.status2;
- self._psychoJS.config.experiment.saveFormat = Symbol.for(data.experiment.saveFormat);
- self._psychoJS.config.experiment.saveIncompleteResults = data.experiment.saveIncompleteResults;
- self._psychoJS.config.experiment.license = data.experiment.license;
- self._psychoJS.config.experiment.runMode = data.experiment.runMode;
-
- // secret keys for various services, e.g. Google Speech API
- if ('keys' in data.experiment)
- {
- self._psychoJS.config.experiment.keys = data.experiment.keys;
- }
- else
- {
- self._psychoJS.config.experiment.keys = [];
- }
+ const openSessionResponse = await postResponse.json();
- self.setStatus(ServerManager.Status.READY);
- // resolve({ ...response, token: data.token, status: data.status });
- resolve(Object.assign(response, {token: data.token, status: data.status}));
- })
- .fail((jqXHR, textStatus, errorThrown) =>
+ if (postResponse.status !== 200)
+ {
+ throw ('error' in openSessionResponse) ? openSessionResponse.error : openSessionResponse;
+ }
+ if (!("token" in openSessionResponse))
+ {
+ self.setStatus(ServerManager.Status.ERROR);
+ throw "unexpected answer from the server: no token";
+ }
+ if (!("experiment" in openSessionResponse))
{
self.setStatus(ServerManager.Status.ERROR);
+ throw "unexpected answer from server: no experiment";
+ }
+
+ self._psychoJS.config.session = {
+ token: openSessionResponse.token,
+ status: "OPEN",
+ };
+ const experiment = openSessionResponse.experiment;
+ self._psychoJS.config.experiment.status = experiment.status2;
+ self._psychoJS.config.experiment.saveFormat = Symbol.for(experiment.saveFormat);
+ self._psychoJS.config.experiment.saveIncompleteResults = experiment.saveIncompleteResults;
+ self._psychoJS.config.experiment.license = experiment.license;
+ self._psychoJS.config.experiment.runMode = experiment.runMode;
+
+ // secret keys for various services, e.g. Google Speech API
+ if ("keys" in experiment)
+ {
+ self._psychoJS.config.experiment.keys = experiment.keys;
+ }
+ else
+ {
+ self._psychoJS.config.experiment.keys = [];
+ }
- const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
- console.error('error:', errorMsg);
+ // partial results upload options:
+ if ("partialResultsUploadPeriod" in experiment)
+ {
+ // note: resultsUpload is initialised in PsychoJS._configure but we reinitialise it here
+ // all the same (belt and braces approach)
+ self._psychoJS.config.experiment.resultsUpload = {
+ period: experiment.partialResultsUploadPeriod,
+ intervalId: -1
+ };
+ }
+ if ("uploadThrottlePeriod" in experiment)
+ {
+ this._uploadThrottlePeriod = experiment.uploadThrottlePeriod;
+ }
- reject(Object.assign(response, {error: errorMsg}));
- });
+ self.setStatus(ServerManager.Status.READY);
+ resolve({...response, token: openSessionResponse.token, status: openSessionResponse.status });
+ }
+ catch (error)
+ {
+ console.error(error);
+ self.setStatus(ServerManager.Status.ERROR);
+ reject({...response, error});
+ }
});
}
-
/**
* @typedef ServerManager.CloseSessionPromise
* @property {string} origin the calling method
* @property {string} context the context
- * @property {Object.<string, *>} [error] an error message if we could not close the session (e.g. if it has not previously been opened)
+ * @property {Object.<string, *>} [error] an error message if we could not close the session (e.g. if it has not
+ * previously been opened)
*/
/**
- * Close the session for this experiment on the remote PsychoJS manager.
+ * Close the session for this experiment on the pavlovia server.
*
- * @name module:core.ServerManager#closeSession
- * @function
- * @public
- * @param {boolean} [isCompleted= false] - whether or not the experiment was completed
- * @param {boolean} [sync= false] - whether or not to communicate with the server in a synchronous manner
+ * @param {boolean} [isCompleted= false] - whether the experiment was completed
+ * @param {boolean} [sync= false] - whether to communicate with the server in a synchronous manner
* @returns {Promise<ServerManager.CloseSessionPromise> | void} the response
*/
async closeSession(isCompleted = false, sync = false)
{
const response = {
- origin: 'ServerManager.closeSession',
- context: 'when closing the session for experiment: ' + this._psychoJS.config.experiment.fullpath
+ origin: "ServerManager.closeSession",
+ context: "when closing the session for experiment: " + this._psychoJS.config.experiment.fullpath,
};
-
- this._psychoJS.logger.debug('closing the session for experiment: ' + this._psychoJS.config.experiment.name);
+ this._psychoJS.logger.debug("closing the session for experiment: " + this._psychoJS.config.experiment.name);
this.setStatus(ServerManager.Status.BUSY);
- // prepare DELETE query:
- const url = this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) + '/sessions/' + this._psychoJS.config.session.token;
-
- // synchronous query the pavlovia server:
+ // synchronously query the pavlovia server:
if (sync)
{
- /* This is now deprecated in most browsers.
- const request = new XMLHttpRequest();
- request.open("DELETE", url, false);
- request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
- request.send(JSON.stringify(data));
- */
- /* This does not work in Chrome before of a CORS bug
- await fetch(url, {
- method: 'DELETE',
- headers: { 'Content-Type': 'application/json;charset=UTF-8' },
- body: JSON.stringify(data),
- // keepalive makes it possible for the request to outlive the page (e.g. when the participant closes the tab)
- keepalive: true
- });
- */
+ const url = this._psychoJS.config.pavlovia.URL
+ + "/api/v2/experiments/" + this._psychoJS.config.gitlab.projectId
+ + "/sessions/" + this._psychoJS.config.session.token + "/delete";
const formData = new FormData();
- formData.append('isCompleted', isCompleted);
- navigator.sendBeacon(url + '/delete', formData);
- this._psychoJS.config.session.status = 'CLOSED';
+ formData.append("isCompleted", isCompleted);
+ if (typeof this._psychoJS._surveyId !== "undefined")
+ {
+ formData.append("surveyId", this._psychoJS._surveyId);
+ }
+
+ navigator.sendBeacon(url, formData);
+ this._psychoJS.config.session.status = "CLOSED";
}
// asynchronously query the pavlovia server:
else
{
const self = this;
- return new Promise((resolve, reject) =>
+ return new Promise(async (resolve, reject) =>
{
- jQuery.ajax({
- url,
- type: 'delete',
- data: {isCompleted},
- dataType: 'json'
- })
- .done((data, textStatus) =>
+ try
+ {
+ const data = {
+ isCompleted
+ };
+ if (typeof this._psychoJS._surveyId !== "undefined")
{
- self.setStatus(ServerManager.Status.READY);
- self._psychoJS.config.session.status = 'CLOSED';
+ data["surveyId"] = this._psychoJS._surveyId;
+ }
- // resolve({ ...response, data });
- resolve(Object.assign(response, {data}));
- })
- .fail((jqXHR, textStatus, errorThrown) =>
- {
- self.setStatus(ServerManager.Status.ERROR);
+ const deleteResponse = await this._queryServerAPI(
+ "DELETE",
+ `experiments/${this._psychoJS.config.gitlab.projectId}/sessions/${this._psychoJS.config.session.token}`,
+ data,
+ "FORM"
+ );
- const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
- console.error('error:', errorMsg);
+ const closeSessionResponse = await deleteResponse.json();
- reject(Object.assign(response, {error: errorMsg}));
- });
+ if (deleteResponse.status !== 200)
+ {
+ throw ('error' in closeSessionResponse) ? closeSessionResponse.error : closeSessionResponse;
+ }
+
+ self.setStatus(ServerManager.Status.READY);
+ self._psychoJS.config.session.status = "CLOSED";
+ resolve({ ...response, ...closeSessionResponse });
+ }
+ catch (error)
+ {
+ console.error(error);
+ self.setStatus(ServerManager.Status.ERROR);
+ reject({...response, error});
+ }
});
}
}
-
/**
* Get the value of a resource.
*
- * @name module:core.ServerManager#getResource
- * @function
- * @public
* @param {string} name - name of the requested resource
* @param {boolean} [errorIfNotDownloaded = false] whether or not to throw an exception if the
* resource status is not DOWNLOADED
@@ -326,83 +376,143 @@ Source: core/ServerManager.js
getResource(name, errorIfNotDownloaded = false)
{
const response = {
- origin: 'ServerManager.getResource',
- context: 'when getting the value of resource: ' + name
+ origin: "ServerManager.getResource",
+ context: "when getting the value of resource: " + name,
};
const pathStatusData = this._resources.get(name);
- if (typeof pathStatusData === 'undefined')
+ if (typeof pathStatusData === "undefined")
{
// throw { ...response, error: 'unknown resource' };
- throw Object.assign(response, {error: 'unknown resource'});
+ throw Object.assign(response, { error: "unknown resource" });
}
if (errorIfNotDownloaded && pathStatusData.status !== ServerManager.ResourceStatus.DOWNLOADED)
{
throw Object.assign(response, {
- error: name + ' is not available for use (yet), its current status is: ' +
- util.toString(pathStatusData.status)
+ error: name + " is not available for use (yet), its current status is: "
+ + util.toString(pathStatusData.status),
});
}
return pathStatusData.data;
}
-
/**
- * Get the status of a resource.
+ * Release a resource.
*
- * @name module:core.ServerManager#getResourceStatus
- * @function
- * @public
- * @param {string} name of the requested resource
- * @return {core.ServerManager.ResourceStatus} status of the resource
- * @throws {Object.<string, *>} exception if no resource with that name has previously been registered
+ * @param {string} name - the name of the resource to release
+ * @return {boolean} true if a resource with the given name was previously registered with the manager,
+ * false otherwise.
*/
- getResourceStatus(name)
+ releaseResource(name)
{
const response = {
- origin: 'ServerManager.getResourceStatus',
- context: 'when getting the status of resource: ' + name
+ origin: "ServerManager.releaseResource",
+ context: "when releasing resource: " + name,
};
const pathStatusData = this._resources.get(name);
- if (typeof pathStatusData === 'undefined')
+
+ if (typeof pathStatusData === "undefined")
{
- // throw { ...response, error: 'unknown resource' };
- throw Object.assign(response, {error: 'unknown resource'});
+ return false;
}
- return pathStatusData.status;
+ // TODO check the current status: prevent the release of a resources currently downloading
+
+ this._psychoJS.logger.debug(`releasing resource: ${name}`);
+ this._resources.delete(name);
+ return true;
}
+ /**
+ * Get the status of a single resource or the reduced status of an array of resources.
+ *
+ * <p>If an array of resources is given, getResourceStatus returns a single, reduced status
+ * that is the status furthest away from DOWNLOADED, with the status ordered as follow:
+ * ERROR (furthest from DOWNLOADED), REGISTERED, DOWNLOADING, and DOWNLOADED</p>
+ * <p>For example, given three resources:
+ * <ul>
+ * <li>if at least one of the resource status is ERROR, the reduced status is ERROR</li>
+ * <li>if at least one of the resource status is DOWNLOADING, the reduced status is DOWNLOADING</li>
+ * <li>if the status of all three resources is REGISTERED, the reduced status is REGISTERED</li>
+ * <li>if the status of all three resources is DOWNLOADED, the reduced status is DOWNLOADED</li>
+ * </ul>
+ * </p>
+ *
+ * @param {string | string[]} names names of the resources whose statuses are requested
+ * @return {module:core.ServerManager.ResourceStatus} status of the resource if there is only one, or reduced status
+ * otherwise
+ * @throws {Object.<string, *>} if at least one of the names is not that of a previously
+ * registered resource
+ */
+ getResourceStatus(names)
+ {
+ const response = {
+ origin: "ServerManager.getResourceStatus",
+ context: `when getting the status of resource(s): ${JSON.stringify(names)}`,
+ };
+
+ // sanity checks:
+ if (typeof names === 'string')
+ {
+ names = [names];
+ }
+ if (!Array.isArray(names))
+ {
+ throw Object.assign(response, { error: "names should be either a string or an array of strings" });
+ }
+ const statusOrder = new Map([
+ [Symbol.keyFor(ServerManager.ResourceStatus.ERROR), 0],
+ [Symbol.keyFor(ServerManager.ResourceStatus.REGISTERED), 1],
+ [Symbol.keyFor(ServerManager.ResourceStatus.DOWNLOADING), 2],
+ [Symbol.keyFor(ServerManager.ResourceStatus.DOWNLOADED), 3]
+ ]);
+ let reducedStatus = ServerManager.ResourceStatus.DOWNLOADED;
+ for (const name of names)
+ {
+ const pathStatusData = this._resources.get(name);
+
+ if (typeof pathStatusData === "undefined")
+ {
+ // throw { ...response, error: 'unknown resource' };
+ throw Object.assign(response, {
+ error: `unable to find a previously registered resource with name: ${name}`
+ });
+ }
+
+ // update the reduced status according to the order given by statusOrder:
+ if (statusOrder.get(Symbol.keyFor(pathStatusData.status)) <
+ statusOrder.get(Symbol.keyFor(reducedStatus)))
+ {
+ reducedStatus = pathStatusData.status;
+ }
+ }
+
+ return reducedStatus;
+ }
/**
* Set the resource manager status.
- *
- * @name module:core.ServerManager#setStatus
- * @function
- * @public
*/
setStatus(status)
{
const response = {
- origin: 'ServerManager.setStatus',
- context: 'when changing the status of the server manager to: ' + util.toString(status)
+ origin: "ServerManager.setStatus",
+ context: "when changing the status of the server manager to: " + util.toString(status),
};
// check status:
- const statusKey = (typeof status === 'symbol') ? Symbol.keyFor(status) : null;
+ const statusKey = (typeof status === "symbol") ? Symbol.keyFor(status) : null;
if (!statusKey)
- // throw { ...response, error: 'status must be a symbol' };
- {
- throw Object.assign(response, {error: 'status must be a symbol'});
+ { // throw { ...response, error: 'status must be a symbol' };
+ throw Object.assign(response, { error: "status must be a symbol" });
}
if (!ServerManager.Status.hasOwnProperty(statusKey))
- // throw { ...response, error: 'unknown status' };
- {
- throw Object.assign(response, {error: 'unknown status'});
+ { // throw { ...response, error: 'unknown status' };
+ throw Object.assign(response, { error: "unknown status" });
}
this._status = status;
@@ -413,13 +523,9 @@ Source: core/ServerManager.js
return this._status;
}
-
/**
* Reset the resource manager status to ServerManager.Status.READY.
*
- * @name module:core.ServerManager#resetStatus
- * @function
- * @public
* @return {ServerManager.Status.READY} the new status
*/
resetStatus()
@@ -427,7 +533,6 @@ Source: core/ServerManager.js
return this.setStatus(ServerManager.Status.READY);
}
-
/**
* Prepare resources for the experiment: register them with the server manager and possibly
* start downloading them right away.
@@ -441,19 +546,17 @@ Source: core/ServerManager.js
* <li>If resources is null, then we do not download any resources</li>
* </ul>
*
- * @name module:core.ServerManager#prepareResources
- * @param {Array.<{name: string, path: string, download: boolean} | Symbol>} [resources=[]] - the list of resources
- * @function
- * @public
+ * @param {String | Array.<{name: string, path: string, download: boolean} | String | Symbol>} [resources=[]] - the
+ * list of resources or a single resource
*/
async prepareResources(resources = [])
{
const response = {
- origin: 'ServerManager.prepareResources',
- context: 'when preparing resources for experiment: ' + this._psychoJS.config.experiment.name
+ origin: "ServerManager.prepareResources",
+ context: "when preparing resources for experiment: " + this._psychoJS.config.experiment.name,
};
- this._psychoJS.logger.debug('preparing resources for experiment: ' + this._psychoJS.config.experiment.name);
+ this._psychoJS.logger.debug("preparing resources for experiment: " + this._psychoJS.config.experiment.name);
try
{
@@ -462,19 +565,24 @@ Source: core/ServerManager.js
// register the resources:
if (resources !== null)
{
+ if (typeof resources === "string")
+ {
+ resources = [resources];
+ }
if (!Array.isArray(resources))
{
- throw "resources should be an array of objects";
+ throw "resources should be either (a) a string or (b) an array of string or objects";
}
// whether all resources have been requested:
- const allResources = (resources.length === 1 && resources[0] === ServerManager.ALL_RESOURCES);
+ const allResources = (resources.length === 1 &&
+ resources[0] === ServerManager.ALL_RESOURCES);
// if the experiment is hosted on the pavlovia.org server and
// resources is [ServerManager.ALL_RESOURCES], then we register all the resources
// in the "resources" sub-directory
- if (this._psychoJS.config.environment === ExperimentHandler.Environment.SERVER
- && allResources)
+ if (this._psychoJS.config.environment === ExperimentHandler.Environment.SERVER &&
+ allResources)
{
// list the resources from the resources directory of the experiment on the server:
const serverResponse = await this._listResources();
@@ -485,53 +593,123 @@ Source: core/ServerManager.js
{
if (!this._resources.has(name))
{
- const path = serverResponse.resourceDirectory + '/' + name;
+ const path = serverResponse.resourceDirectory + "/" + name;
this._resources.set(name, {
status: ServerManager.ResourceStatus.REGISTERED,
path,
- data: undefined
+ data: undefined,
});
- this._psychoJS.logger.debug('registered resource:', name, path);
+ this._psychoJS.logger.debug(`registered resource: name= ${name}, path= ${path}`);
resourcesToDownload.add(name);
}
}
}
-
// if the experiment is hosted locally (localhost) or if specific resources were given
// then we register those specific resources, if they have not been registered already
else
{
// we cannot ask for all resources to be registered locally, since we cannot list
// them:
- if (this._psychoJS.config.environment === ExperimentHandler.Environment.LOCAL
- && allResources)
+ if (this._psychoJS.config.environment === ExperimentHandler.Environment.LOCAL &&
+ allResources)
{
throw "resources must be manually specified when the experiment is running locally: ALL_RESOURCES cannot be used";
}
- for (let {name, path, download} of resources)
+ // pre-process the resources:
+ for (let r = 0; r < resources.length; ++r)
+ {
+ // convert those resources that are only a string to an object with name and path:
+ if (typeof resources[r] === "string")
+ {
+ resources[r] = {
+ name: resources[r],
+ path: resources[r],
+ download: true
+ };
+ }
+
+ const resource = resources[r];
+
+ // deal with survey models:
+ if ("surveyId" in resource)
+ {
+ // survey models can only be downloaded if the experiment is hosted on the pavlovia.org server:
+ if (this._psychoJS.config.environment !== ExperimentHandler.Environment.SERVER)
+ {
+ throw "survey models cannot be downloaded when the experiment is running locally";
+ }
+
+ // we add a .sid extension so _downloadResources knows what to download the associated
+ // survey model from the server
+ resources[r] = {
+ name: `${resource["surveyId"]}.sid`,
+ path: resource["surveyId"],
+ download: true
+ };
+ }
+
+ // deal with survey libraries:
+ if ("surveyLibrary" in resource)
+ {
+ // add the SurveyJS and PsychoJS Survey .js and .css resources:
+ resources[r] = {
+ name: "jquery-3.5.1.min.js",
+ path: "./lib/vendors/jquery-3.5.1.min.js",
+ // name: "jquery-3.6.0.min.js",
+ // path: "./lib/vendors/jquery-3.6.0.min.js",
+ download: true
+ };
+ resources.push({
+ name: "surveyjs.jquery-1.9.126.min.js",
+ path: "./lib/vendors/surveyjs.jquery-1.9.126.min.js",
+ // name: "survey.jquery-1.9.50.min.js",
+ // path: "./lib/vendors/survey.jquery-1.9.50.min.js",
+ download: true
+ });
+ resources.push({
+ name: "surveyjs.defaultV2-1.9.126-OST.min.css",
+ path: "./lib/vendors/surveyjs.defaultV2-1.9.126-OST.min.css",
+ // name: "survey.defaultV2-1.9.50.min.css",
+ // path: "./lib/vendors/survey.defaultV2-1.9.50.min.css",
+ download: true
+ });
+ resources.push({
+ name: "survey.widgets.css",
+ path: "./lib/vendors/survey.widgets.css",
+ download: true
+ });
+ resources.push({
+ name: "survey.grey_style.css",
+ path: "./lib/vendors/survey.grey_style.css",
+ download: true
+ });
+ }
+ }
+
+ for (let { name, path, download } of resources)
{
if (!this._resources.has(name))
{
// to deal with potential CORS issues, we use the pavlovia.org proxy for resources
// not hosted on pavlovia.org:
- if ((path.toLowerCase().indexOf('www.') === 0 ||
- path.toLowerCase().indexOf('http:') === 0 ||
- path.toLowerCase().indexOf('https:') === 0) &&
- (path.indexOf('pavlovia.org') === -1))
+ if ( (path.toLowerCase().indexOf("www.") === 0 ||
+ path.toLowerCase().indexOf("http:") === 0 ||
+ path.toLowerCase().indexOf("https:") === 0) &&
+ (path.indexOf("pavlovia.org") === -1) )
{
- path = 'https://pavlovia.org/api/v2/proxy/' + path;
+ path = "https://pavlovia.org/api/v2/proxy/" + path;
}
this._resources.set(name, {
status: ServerManager.ResourceStatus.REGISTERED,
path,
- data: undefined
+ data: undefined,
});
- this._psychoJS.logger.debug('registered resource:', name, path);
+ this._psychoJS.logger.debug(`registered resource: name= ${name}, path= ${path}`);
// download resources by default:
- if (typeof download === 'undefined' || download)
+ if (typeof download === "undefined" || download)
{
resourcesToDownload.add(name);
}
@@ -540,25 +718,45 @@ Source: core/ServerManager.js
}
}
- // download those registered resources for which download = true:
- /*await*/ this._downloadResources(resourcesToDownload);
+ // download those registered resources for which download = true
+ // note: we return a Promise that will be resolved when all the resources are downloaded
+ if (resourcesToDownload.size === 0)
+ {
+ this.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.DOWNLOAD_COMPLETED,
+ });
+
+ return Promise.resolve();
+ }
+ else
+ {
+ return new Promise((resolve, reject) =>
+ {
+ const uuid = this.on(ServerManager.Event.RESOURCE, (signal) =>
+ {
+ if (signal.message === ServerManager.Event.DOWNLOAD_COMPLETED)
+ {
+ this.off(ServerManager.Event.RESOURCE, uuid);
+ resolve();
+ }
+ });
+
+ this._downloadResources(resourcesToDownload);
+ });
+ }
}
catch (error)
{
- console.log('error', error);
- throw Object.assign(response, {error});
+ console.error("error", error);
+ throw Object.assign(response, { error });
// throw { ...response, error: error };
}
}
-
/**
* Block the experiment until the specified resources have been downloaded.
*
- * @name module:core.ServerManager#waitForResources
* @param {Array.<{name: string, path: string}>} [resources=[]] - the list of resources
- * @function
- * @public
*/
waitForResources(resources = [])
{
@@ -566,11 +764,11 @@ Source: core/ServerManager.js
this._waitForDownloadComponent = {
status: PsychoJS.Status.NOT_STARTED,
clock: new Clock(),
- resources: new Set()
+ resources: new Set(),
};
const self = this;
- return () =>
+ return async () =>
{
const t = self._waitForDownloadComponent.clock.getTime();
@@ -583,75 +781,77 @@ Source: core/ServerManager.js
// if resources is an empty array, we consider all registered resources:
if (resources.length === 0)
{
- for (const [name, {status, path, data}] of this._resources)
+ for (const [name, { status, path, data }] of this._resources)
{
- resources.append({ name, path });
+ resources.push({ name, path });
}
}
- // only download those resources not already downloaded or downloading:
+ // only download those resources not already downloaded and not downloading:
const resourcesToDownload = new Set();
- for (let {name, path} of resources)
+ for (let { name, path } of resources)
{
// to deal with potential CORS issues, we use the pavlovia.org proxy for resources
// not hosted on pavlovia.org:
- if ( (path.toLowerCase().indexOf('www.') === 0 ||
- path.toLowerCase().indexOf('http:') === 0 ||
- path.toLowerCase().indexOf('https:') === 0) &&
- (path.indexOf('pavlovia.org') === -1) )
+ if (
+ (path.toLowerCase().indexOf("www.") === 0
+ || path.toLowerCase().indexOf("http:") === 0
+ || path.toLowerCase().indexOf("https:") === 0)
+ && (path.indexOf("pavlovia.org") === -1)
+ )
{
- path = 'https://devlovia.org/api/v2/proxy/' + path;
+ path = "https://pavlovia.org/api/v2/proxy/" + path;
}
const pathStatusData = this._resources.get(name);
// the resource has not been registered yet:
- if (typeof pathStatusData === 'undefined')
+ if (typeof pathStatusData === "undefined")
{
self._resources.set(name, {
status: ServerManager.ResourceStatus.REGISTERED,
path,
- data: undefined
+ data: undefined,
});
self._waitForDownloadComponent.resources.add(name);
resourcesToDownload.add(name);
- self._psychoJS.logger.debug('registered resource:', name, path);
+ self._psychoJS.logger.debug("registered resource:", name, path);
}
// the resource has been registered but is not downloaded yet:
else if (typeof pathStatusData.status !== ServerManager.ResourceStatus.DOWNLOADED)
- // else if (typeof pathStatusData.data === 'undefined')
- {
+ { // else if (typeof pathStatusData.data === 'undefined')
self._waitForDownloadComponent.resources.add(name);
}
-
}
+ self._waitForDownloadComponent.status = PsychoJS.Status.STARTED;
+
// start the download:
self._downloadResources(resourcesToDownload);
}
- // check whether all resources have been downloaded:
- for (const name of self._waitForDownloadComponent.resources)
+ if (self._waitForDownloadComponent.status === PsychoJS.Status.STARTED)
{
- const pathStatusData = this._resources.get(name);
-
- // the resource has not been downloaded yet: loop this component
- if (typeof pathStatusData.status !== ServerManager.ResourceStatus.DOWNLOADED)
- // if (typeof pathStatusData.data === 'undefined')
+ // check whether all resources have been downloaded:
+ for (const name of self._waitForDownloadComponent.resources)
{
- return Scheduler.Event.FLIP_REPEAT;
+ const pathStatusData = this._resources.get(name);
+
+ // the resource has not been downloaded yet: loop this component
+ if (pathStatusData.status !== ServerManager.ResourceStatus.DOWNLOADED)
+ { // if (typeof pathStatusData.data === 'undefined')
+ return Scheduler.Event.FLIP_REPEAT;
+ }
}
- }
- // all resources have been downloaded: move to the next component:
- self._waitForDownloadComponent.status = PsychoJS.Status.FINISHED;
- return Scheduler.Event.NEXT;
+ // all resources have been downloaded: move to the next component:
+ self._waitForDownloadComponent.status = PsychoJS.Status.FINISHED;
+ return Scheduler.Event.NEXT;
+ }
};
-
}
-
/**
* @typedef ServerManager.UploadDataPromise
* @property {string} origin the calling method
@@ -661,76 +861,77 @@ Source: core/ServerManager.js
/**
* Asynchronously upload experiment data to the pavlovia server.
*
- * @name module:core.ServerManager#uploadData
- * @function
- * @public
* @param {string} key - the data key (e.g. the name of .csv file)
* @param {string} value - the data value (e.g. a string containing the .csv header and records)
* @param {boolean} [sync= false] - whether or not to communicate with the server in a synchronous manner
- *
* @returns {Promise<ServerManager.UploadDataPromise>} the response
*/
uploadData(key, value, sync = false)
{
const response = {
- origin: 'ServerManager.uploadData',
- context: 'when uploading participant\'s results for experiment: ' + this._psychoJS.config.experiment.fullpath
+ origin: "ServerManager.uploadData",
+ context: "when uploading participant's results for experiment: " + this._psychoJS.config.experiment.fullpath,
};
+ this._psychoJS.logger.debug("uploading data for experiment: " + this._psychoJS.config.experiment.fullpath);
+
+ // data upload throttling:
+ const now = MonotonicClock.getReferenceTime();
+ const checkThrottling = (typeof this._psychoJS.config.experiment.resultsUpload.lastUploadTimestamp !== "undefined");
+ if (checkThrottling && (now - this._psychoJS.config.experiment.resultsUpload.lastUploadTimestamp < this._uploadThrottlePeriod * 60))
+ {
+ return Promise.reject({ ...response, error: "upload canceled by throttling"});
+ }
+ this._psychoJS.config.experiment.resultsUpload.lastUploadTimestamp = now;
- this._psychoJS.logger.debug('uploading data for experiment: ' + this._psychoJS.config.experiment.fullpath);
this.setStatus(ServerManager.Status.BUSY);
- const url = this._psychoJS.config.pavlovia.URL +
- '/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) +
- '/sessions/' + this._psychoJS.config.session.token +
- '/results';
+ const path = `experiments/${this._psychoJS.config.gitlab.projectId}/sessions/${this._psychoJS.config.session.token}/results`;
- // synchronous query the pavlovia server:
+ // synchronously query the pavlovia server:
if (sync)
{
const formData = new FormData();
- formData.append('key', key);
- formData.append('value', value);
- navigator.sendBeacon(url, formData);
+ formData.append("key", key);
+ formData.append("value", value);
+ navigator.sendBeacon(`${this._psychoJS.config.pavlovia.URL}/api/v2/${path}`, formData);
}
// asynchronously query the pavlovia server:
else
{
const self = this;
- return new Promise((resolve, reject) =>
+ return new Promise(async (resolve, reject) =>
{
- const data = {
- key,
- value
- };
-
- jQuery.post(url, data, null, 'json')
- .done((serverData, textStatus) =>
- {
- self.setStatus(ServerManager.Status.READY);
- resolve(Object.assign(response, {serverData}));
- })
- .fail((jqXHR, textStatus, errorThrown) =>
+ try
+ {
+ const postResponse = await this._queryServerAPI(
+ "POST",
+ `experiments/${this._psychoJS.config.gitlab.projectId}/sessions/${this._psychoJS.config.session.token}/results`,
+ { key, value },
+ "FORM"
+ );
+ const uploadDataResponse = await postResponse.json();
+
+ if (postResponse.status !== 200)
{
- self.setStatus(ServerManager.Status.ERROR);
-
- const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
- console.error('error:', errorMsg);
+ throw ('error' in uploadDataResponse) ? uploadDataResponse.error : uploadDataResponse;
+ }
- reject(Object.assign(response, {error: errorMsg}));
- });
+ self.setStatus(ServerManager.Status.READY);
+ resolve({ ...response, ...uploadDataResponse });
+ }
+ catch (error)
+ {
+ console.error(error);
+ self.setStatus(ServerManager.Status.ERROR);
+ reject({...response, error});
+ }
});
}
}
-
-
/**
* Asynchronously upload experiment logs to the pavlovia server.
*
- * @name module:core.ServerManager#uploadLog
- * @function
- * @public
* @param {string} logs - the base64 encoded, compressed, formatted logs
* @param {boolean} [compressed=false] - whether or not the logs are compressed
* @returns {Promise<ServerManager.UploadDataPromise>} the response
@@ -738,19 +939,17 @@ Source: core/ServerManager.js
uploadLog(logs, compressed = false)
{
const response = {
- origin: 'ServerManager.uploadLog',
- context: 'when uploading participant\'s log for experiment: ' + this._psychoJS.config.experiment.fullpath
+ origin: "ServerManager.uploadLog",
+ context: "when uploading participant's log for experiment: " + this._psychoJS.config.experiment.fullpath,
};
+ this._psychoJS.logger.debug("uploading server log for experiment: " + this._psychoJS.config.experiment.fullpath);
- this._psychoJS.logger.debug('uploading server log for experiment: ' + this._psychoJS.config.experiment.fullpath);
this.setStatus(ServerManager.Status.BUSY);
- // prepare the POST query:
+ // prepare a POST query:
const info = this.psychoJS.experiment.extraInfo;
- const participant = ((typeof info.participant === 'string' && info.participant.length > 0) ? info.participant : 'PARTICIPANT');
- const experimentName = (typeof info.expName !== 'undefined') ? info.expName : this.psychoJS.config.experiment.name;
- const datetime = ((typeof info.date !== 'undefined') ? info.date : MonotonicClock.getDateStr());
- const filename = participant + '_' + experimentName + '_' + datetime + '.log';
+ const filenameWithoutPath = this.psychoJS.experiment.dataFileName.split(/[\\/]/).pop();
+ const filename = `${filenameWithoutPath}.log`;
const data = {
filename,
logs,
@@ -759,356 +958,467 @@ Source: core/ServerManager.js
// query the pavlovia server:
const self = this;
- return new Promise((resolve, reject) =>
+ return new Promise(async (resolve, reject) =>
{
- const url = self._psychoJS.config.pavlovia.URL +
- '/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) +
- '/sessions/' + self._psychoJS.config.session.token +
- '/logs';
+ try
+ {
+ const postResponse = await this._queryServerAPI(
+ "POST",
+ `experiments/${this._psychoJS.config.gitlab.projectId}/sessions/${self._psychoJS.config.session.token}/logs`,
+ data,
+ "FORM"
+ );
- jQuery.post(url, data, null, 'json')
- .done((serverData, textStatus) =>
- {
- self.setStatus(ServerManager.Status.READY);
- resolve(Object.assign(response, {serverData}));
- })
- .fail((jqXHR, textStatus, errorThrown) =>
+ const uploadLogsResponse = await postResponse.json();
+
+ if (postResponse.status !== 200)
{
- self.setStatus(ServerManager.Status.ERROR);
+ throw ('error' in uploadLogsResponse) ? uploadLogsResponse.error : uploadLogsResponse;
+ }
- const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
- console.error('error:', errorMsg);
+ self.setStatus(ServerManager.Status.READY);
+ resolve({...response, ...uploadLogsResponse });
+ }
+ catch (error)
+ {
+ console.error(error);
+ self.setStatus(ServerManager.Status.ERROR);
+ reject({...response, error});
+ }
- reject(Object.assign(response, {error: errorMsg}));
- });
});
}
-
-
/**
- * Asynchronously upload audio data to the pavlovia server.
+ * Synchronously or asynchronously upload audio data to the pavlovia server.
*
- * @name module:core.ServerManager#uploadAudio
- * @function
- * @public
- * @param {Blob} audioBlob - the audio blob to be uploaded
- * @param {string} tag - additional tag
+ * @param @param {Object} options
+ * @param {Blob} options.mediaBlob - the audio or video blob to be uploaded
+ * @param {string} options.tag - additional tag
+ * @param {boolean} [options.waitForCompletion=false] - whether or not to wait for completion
+ * before returning
+ * @param {boolean} [options.showDialog=false] - whether or not to open a dialog box to inform the participant to
+ * wait for the data to be uploaded to the server
+ * @param {string} [options.dialogMsg="Please wait a few moments while the data is uploading to the server"] -
+ * default message informing the participant to wait for the data to be uploaded to the server
* @returns {Promise<ServerManager.UploadDataPromise>} the response
*/
- async uploadAudio(audioBlob, tag)
+ async uploadAudioVideo({mediaBlob, tag, waitForCompletion = false, showDialog = false, dialogMsg = "Please wait a few moments while the data is uploading to the server"})
{
const response = {
- origin: 'ServerManager.uploadAudio',
- context: 'when uploading audio data for experiment: ' + this._psychoJS.config.experiment.fullpath
+ origin: "ServerManager.uploadAudio",
+ context: "when uploading media data for experiment: " + this._psychoJS.config.experiment.fullpath,
};
try
{
- if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER ||
- this._psychoJS.config.experiment.status !== 'RUNNING' ||
- this._psychoJS._serverMsg.has('__pilotToken'))
+ if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER
+ || this._psychoJS.config.experiment.status !== "RUNNING"
+ || this._psychoJS._serverMsg.has("__pilotToken"))
{
- throw 'audio recordings can only be uploaded to the server for experiments running on the server';
+ throw "media recordings can only be uploaded to the server for experiments running on the server";
}
- this._psychoJS.logger.debug('uploading audio data for experiment: ' + this._psychoJS.config.experiment.fullpath);
+ this._psychoJS.logger.debug(`uploading media data for experiment: ${this._psychoJS.config.experiment.fullpath}`);
this.setStatus(ServerManager.Status.BUSY);
+ // open pop-up dialog:
+ if (showDialog)
+ {
+ this.psychoJS.gui.dialog({
+ warning: dialogMsg,
+ showOK: false,
+ });
+ }
+
// prepare the request:
const info = this.psychoJS.experiment.extraInfo;
- const participant = ((typeof info.participant === 'string' && info.participant.length > 0) ? info.participant : 'PARTICIPANT');
- const experimentName = (typeof info.expName !== 'undefined') ? info.expName : this.psychoJS.config.experiment.name;
- const datetime = ((typeof info.date !== 'undefined') ? info.date : MonotonicClock.getDateStr());
- const filename = participant + '_' + experimentName + '_' + datetime + '_' + tag;
+ const participant = ((typeof info.participant === "string" && info.participant.length > 0) ? info.participant : "PARTICIPANT");
+ const experimentName = (typeof info.expName !== "undefined") ? info.expName : this.psychoJS.config.experiment.name;
+ const datetime = ((typeof info.date !== "undefined") ? info.date : MonotonicClock.getDateStr());
+ const filename = participant + "_" + experimentName + "_" + datetime + "_" + tag;
const formData = new FormData();
- formData.append('audio', audioBlob, filename);
-
- const url = this._psychoJS.config.pavlovia.URL +
- '/api/v2/experiments/' + this._psychoJS.config.gitlab.projectId +
- '/sessions/' + this._psychoJS.config.session.token +
- '/audio';
-
- // query the pavlovia server:
- const response = await fetch(url, {
- method: 'POST',
- mode: 'cors', // no-cors, *cors, same-origin
- cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
- credentials: 'same-origin', // include, *same-origin, omit
- redirect: 'follow', // manual, *follow, error
- referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
- body: formData
+ formData.append("media", mediaBlob, filename);
+
+ let url = this._psychoJS.config.pavlovia.URL
+ + "/api/v2/experiments/" + this._psychoJS.config.gitlab.projectId
+ + "/sessions/" + this._psychoJS.config.session.token
+ + "/media";
+
+ // query the server:
+ let response = await fetch(url, {
+ method: "POST",
+ mode: "cors",
+ cache: "no-cache",
+ credentials: "same-origin",
+ redirect: "follow",
+ referrerPolicy: "no-referrer",
+ body: formData,
});
- const jsonResponse = await response.json();
+ const postMediaResponse = await response.json();
+ this._psychoJS.logger.debug(`post media response: ${JSON.stringify(postMediaResponse)}`);
// deal with server errors:
if (!response.ok)
{
- throw jsonResponse;
+ throw postMediaResponse;
+ }
+
+ // wait until the upload has completed:
+ if (waitForCompletion)
+ {
+ if (!("uploadToken" in postMediaResponse))
+ {
+ throw "incorrect server response: missing uploadToken";
+ }
+ const uploadToken = postMediaResponse['uploadToken'];
+
+ while (true)
+ {
+ // wait a bit:
+ await new Promise(r =>
+ {
+ setTimeout(r, 1000);
+ });
+
+ // check the status of the upload:
+ url = this._psychoJS.config.pavlovia.URL
+ + "/api/v2/experiments/" + this._psychoJS.config.gitlab.projectId
+ + "/sessions/" + this._psychoJS.config.session.token
+ + "/media/" + uploadToken + "/status";
+
+ response = await fetch(url, {
+ method: "GET",
+ mode: "cors",
+ cache: "no-cache",
+ credentials: "same-origin",
+ redirect: "follow",
+ referrerPolicy: "no-referrer"
+ });
+ const checkStatusResponse = await response.json();
+ this._psychoJS.logger.debug(`check upload status response: ${JSON.stringify(checkStatusResponse)}`);
+
+ if (("status" in checkStatusResponse) && checkStatusResponse["status"] === "COMPLETED")
+ {
+ break;
+ }
+ }
+ }
+
+ if (showDialog)
+ {
+ this.psychoJS.gui.closeDialog();
}
this.setStatus(ServerManager.Status.READY);
- return jsonResponse;
+ return postMediaResponse;
}
catch (error)
{
this.setStatus(ServerManager.Status.ERROR);
console.error(error);
- throw {...response, error};
+ throw { ...response, error };
}
-
}
-
-
/**
- * List the resources available to the experiment.
-
- * @name module:core.ServerManager#_listResources
- * @function
- * @private
+ * Asynchronously upload a survey response to the pavlovia server.
+ *
+ * @returns {Promise<ServerManager.UploadDataPromise>} a promise resolved when the survey response has been uploaded
*/
- _listResources()
+ async uploadSurveyResponse(surveyId, surveyResponse, isComplete)
{
const response = {
- origin: 'ServerManager._listResourcesSession',
- context: 'when listing the resources for experiment: ' + this._psychoJS.config.experiment.fullpath
+ origin: "ServerManager.uploadSurveyResponse",
+ context: `when uploading the survey response for experiment: ${this._psychoJS.config.experiment.fullpath} and survey: ${surveyId}`
};
- this._psychoJS.logger.debug('listing the resources for experiment: ' +
- this._psychoJS.config.experiment.fullpath);
+ if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER ||
+ this._psychoJS.config.experiment.status !== "RUNNING" ||
+ this._psychoJS._serverMsg.has("__pilotToken"))
+ {
+ throw "survey responses can only be uploaded to the server for experiments running on the server";
+ }
+ this._psychoJS.logger.debug(`uploading a survey response for experiment: ${this._psychoJS.config.experiment.fullpath} and survey: ${surveyId}`);
this.setStatus(ServerManager.Status.BUSY);
- // prepare GET data:
- const data = {
- 'token': this._psychoJS.config.session.token
- };
-
- // query pavlovia server:
const self = this;
- return new Promise((resolve, reject) =>
+ return new Promise(async (resolve, reject) =>
{
- const url = this._psychoJS.config.pavlovia.URL +
- '/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) +
- '/resources';
-
- jQuery.get(url, data, null, 'json')
- .done((data, textStatus) =>
- {
- if (!('resources' in data))
- {
- self.setStatus(ServerManager.Status.ERROR);
- // reject({ ...response, error: 'unexpected answer from server: no resources' });
- reject(Object.assign(response, {error: 'unexpected answer from server: no resources'}));
- }
- if (!('resourceDirectory' in data))
+ try
+ {
+ const info = this._psychoJS.experiment.extraInfo;
+ const participant = (typeof info.participant === "string" && info.participant.length > 0) ?
+ info.participant :
+ "PARTICIPANT";
+
+ const postResponse = await this._queryServerAPI(
+ "POST",
+ `surveys/${surveyId}`,
{
- self.setStatus(ServerManager.Status.ERROR);
- // reject({ ...response, error: 'unexpected answer from server: no resourceDirectory' });
- reject(Object.assign(response, {error: 'unexpected answer from server: no resourceDirectory'}));
- }
-
- self.setStatus(ServerManager.Status.READY);
- // resolve({ ...response, resources: data.resources, resourceDirectory: data.resourceDirectory });
- resolve(Object.assign(response, {
- resources: data.resources,
- resourceDirectory: data.resourceDirectory
- }));
- })
- .fail((jqXHR, textStatus, errorThrown) =>
+ experimentId: this._psychoJS.config.gitlab.projectId,
+ sessionToken: this._psychoJS.config.session.token,
+ participant: participant,
+ surveyResponse,
+ isComplete
+ },
+ "JSON"
+ );
+ const uploadDataResponse = await postResponse.json();
+
+ if (postResponse.status !== 200)
{
- self.setStatus(ServerManager.Status.ERROR);
-
- const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
- console.error('error:', errorMsg);
+ throw ('error' in uploadDataResponse) ? uploadDataResponse.error : uploadDataResponse;
+ }
- reject(Object.assign(response, {error: errorMsg}));
- });
+ self.setStatus(ServerManager.Status.READY);
+ resolve({ ...response, ...uploadDataResponse });
+ }
+ catch (error)
+ {
+ console.error(error);
+ self.setStatus(ServerManager.Status.ERROR);
+ reject({...response, error});
+ }
});
-
}
-
-
/**
- * Download the specified resources.
+ * Asynchronously get a survey's experiment parameters from the pavlovia server, and update experimentInfo
*
- * <p>Note: we use the [preloadjs library]{@link https://www.createjs.com/preloadjs}.</p>
+ * @note only those fields not previously defined in experimentInfo are updated
*
- * @name module:core.ServerManager#_downloadResources
- * @function
- * @protected
- * @param {Set} resources - a set of names of previously registered resources
+ * @param surveyId
+ * @param experimentInfo
+ * @returns {Promise} a promise resolved when the survey experiment parameters have been downloaded
*/
- _downloadResources(resources)
+ async getSurveyExperimentParameters(surveyId, experimentInfo)
{
const response = {
- origin: 'ServerManager._downloadResources',
- context: 'when downloading resources for experiment: ' + this._psychoJS.config.experiment.name
+ origin: "ServerManager.getSurveyExperimentParameters",
+ context: `when downloading the experiment parameters for survey: ${surveyId}`
};
- this._psychoJS.logger.debug('downloading resources for experiment: ' + this._psychoJS.config.experiment.name);
+ if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER)
+ {
+ throw "survey experiment parameters cannot be downloaded when the experiment is running locally";
+ }
+ this._psychoJS.logger.debug(`downloading the experiment parameters of survey: ${surveyId}`);
this.setStatus(ServerManager.Status.BUSY);
- this.emit(ServerManager.Event.RESOURCE, {
- message: ServerManager.Event.DOWNLOADING_RESOURCES,
- count: resources.size
- });
-
- this._nbLoadedResources = 0;
-
-
- // (*) set-up preload.js:
- this._resourceQueue = new createjs.LoadQueue(true, '', true);
const self = this;
-
- // the loading of a specific resource has started:
- this._resourceQueue.addEventListener("filestart", event =>
+ return new Promise(async (resolve, reject) =>
{
- const pathStatusData = self._resources.get(event.item.id);
- pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADING;
+ try
+ {
+ const getResponse = await this._queryServerAPI(
+ "GET",
+ `surveys/${surveyId}/experiment`
+ );
+ const getExperimentParametersResponse = await getResponse.json();
- self.emit(ServerManager.Event.RESOURCE, {
- message: ServerManager.Event.DOWNLOADING_RESOURCE,
- resource: event.item.id
- });
- });
+ if (getResponse.status !== 200)
+ {
+ throw ('error' in getExperimentParametersResponse) ? getExperimentParametersResponse.error : getExperimentParametersResponse;
+ }
- // the loading of a specific resource has completed:
- this._resourceQueue.addEventListener("fileload", event =>
- {
- const pathStatusData = self._resources.get(event.item.id);
- pathStatusData.data = event.result;
- pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADED;
+ if (getExperimentParametersResponse["experimentParameters"] === null)
+ {
+ throw "either there is no survey with the given id, or it is not currently active";
+ }
- ++ self._nbLoadedResources;
- self.emit(ServerManager.Event.RESOURCE, {
- message: ServerManager.Event.RESOURCE_DOWNLOADED,
- resource: event.item.id
- });
- });
+ // update the info with the survey experiment parameters:
+ const experimentParameters = getExperimentParametersResponse['experimentParameters'];
+ for (const parameter in experimentParameters)
+ {
+ if (typeof experimentInfo[parameter] === "undefined")
+ {
+ experimentInfo[parameter] = experimentParameters[parameter];
+ }
+ }
- // the loading of all given resources completed:
- this._resourceQueue.addEventListener("complete", event =>
- {
- self._resourceQueue.close();
- if (self._nbLoadedResources === resources.size)
- {
self.setStatus(ServerManager.Status.READY);
- self.emit(ServerManager.Event.RESOURCE, {
- message: ServerManager.Event.DOWNLOAD_COMPLETED
- });
+ resolve({ ...response, ...getExperimentParametersResponse });
+ }
+ catch (error)
+ {
+ console.error(error);
+ self.setStatus(ServerManager.Status.ERROR);
+ reject({...response, error});
}
});
+ }
- // error: we throw an exception
- this._resourceQueue.addEventListener("error", event =>
+ /**
+ * List the resources available to the experiment.
+ *
+ * @protected
+ */
+ _listResources()
+ {
+ const response = {
+ origin: "ServerManager._listResourcesSession",
+ context: "when listing the resources for experiment: " + this._psychoJS.config.experiment.fullpath,
+ };
+ this._psychoJS.logger.debug(`listing the resources for experiment: ${this._psychoJS.config.experiment.fullpath}`);
+
+ this.setStatus(ServerManager.Status.BUSY);
+
+ // prepare a GET query:
+ const data = {
+ "token": this._psychoJS.config.session.token,
+ };
+
+ // query the server:
+ const self = this;
+ return new Promise(async (resolve, reject) =>
{
- self.setStatus(ServerManager.Status.ERROR);
- if (typeof event.item !== 'undefined')
- {
- const pathStatusData = self._resources.get(event.item.id);
- pathStatusData.status = ServerManager.ResourceStatus.ERROR;
- throw Object.assign(response, {
- error: 'unable to download resource: ' + event.item.id + ' (' + event.title + ')'
- });
- }
- else
+ try
{
- console.error(event);
+ const getResponse = await this._queryServerAPI(
+ "GET",
+ `experiments/${this._psychoJS.config.gitlab.projectId}/resources`,
+ data
+ );
- if (event.title === 'FILE_LOAD_ERROR' && typeof event.data !== 'undefined')
- {
- const id = event.data.id;
- const title = event.data.src;
+ const getResourcesResponse = await getResponse.json();
- throw Object.assign(response, {
- error: 'unable to download resource: ' + id + ' (' + title + ')'
- });
+ if (!("resources" in getResourcesResponse))
+ {
+ self.setStatus(ServerManager.Status.ERROR);
+ throw "unexpected answer from server: no resources";
}
-
- else
+ if (!("resourceDirectory" in getResourcesResponse))
{
- throw Object.assign(response, {
- error: 'unspecified download error'
- });
+ self.setStatus(ServerManager.Status.ERROR);
+ throw "unexpected answer from server: no resourceDirectory";
}
+ self.setStatus(ServerManager.Status.READY);
+ resolve({ ...response, resources: data.resources, resourceDirectory: data.resourceDirectory });
+ }
+ catch (error)
+ {
+ console.error(error);
+ self.setStatus(ServerManager.Status.ERROR);
+ reject({...response, error});
}
});
+ }
+ /**
+ * Download the specified resources.
+ *
+ * <p>Note: we use the [preloadjs library]{@link https://www.createjs.com/preloadjs}.</p>
+ *
+ * @protected
+ * @param {Set} resources - a set of names of previously registered resources
+ */
+ async _downloadResources(resources)
+ {
+ const response = {
+ origin: "ServerManager._downloadResources",
+ context: "when downloading resources for experiment: " + this._psychoJS.config.experiment.name,
+ };
- // (*) dispatch resources to preload.js or howler.js based on extension:
- let manifest = [];
+ this._psychoJS.logger.debug("downloading resources for experiment: " + this._psychoJS.config.experiment.name);
+
+ this.setStatus(ServerManager.Status.BUSY);
+ this.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.DOWNLOADING_RESOURCES,
+ count: resources.size,
+ });
+
+ // based on the resource extension either (a) add it to the preload manifest, (b) mark it for
+ // download by howler, (c) add it to the document fonts, or (d) download the associated survey model
+ // from the server
+ const preloadManifest = [];
const soundResources = new Set();
+ const fontResources = [];
+ const surveyModelResources = [];
for (const name of resources)
{
- const nameParts = name.toLowerCase().split('.');
+ const nameParts = name.toLowerCase().split(".");
const extension = (nameParts.length > 1) ? nameParts.pop() : undefined;
// warn the user if the resource does not have any extension:
- if (typeof extension === 'undefined')
+ if (typeof extension === "undefined")
{
this.psychoJS.logger.warn(`"${name}" does not appear to have an extension, which may negatively impact its loading. We highly recommend you add an extension.`);
}
const pathStatusData = this._resources.get(name);
- if (typeof pathStatusData === 'undefined')
+ if (typeof pathStatusData === "undefined")
{
- throw Object.assign(response, {error: name + ' has not been previously registered'});
+ throw Object.assign(response, { error: name + " has not been previously registered" });
}
if (pathStatusData.status !== ServerManager.ResourceStatus.REGISTERED)
{
- throw Object.assign(response, {error: name + ' is already downloaded or is currently already downloading'});
+ throw Object.assign(response, { error: name + " is already downloaded or is currently already downloading" });
}
- // preload.js with forced binary for xls and xlsx:
- if (['csv', 'odp', 'xls', 'xlsx'].indexOf(extension) > -1)
+ const pathParts = pathStatusData.path.toLowerCase().split(".");
+ const pathExtension = (pathParts.length > 1) ? pathParts.pop() : undefined;
+
+ // preload.js with forced binary:
+ if (["csv", "odp", "xls", "xlsx", "json"].indexOf(extension) > -1)
{
- manifest.push(/*new createjs.LoadItem().set(*/{
+ preloadManifest.push(/*new createjs.LoadItem().set(*/ {
id: name,
src: pathStatusData.path,
type: createjs.Types.BINARY,
- crossOrigin: 'Anonymous'
- }/*)*/);
+ crossOrigin: "Anonymous",
+ } /*)*/);
}
- /* ascii .csv are adequately handled in binary format
+
+ /* note: ascii .csv are adequately handled in binary format, no need to treat them separately
// forced text for .csv:
else if (['csv'].indexOf(resourceExtension) > -1)
manifest.push({ id: resourceName, src: resourceName, type: createjs.Types.TEXT });
*/
- // sound files are loaded through howler.js:
- else if (['mp3', 'mpeg', 'opus', 'ogg', 'oga', 'wav', 'aac', 'caf', 'm4a', 'weba', 'dolby', 'flac'].indexOf(extension) > -1)
+ // sound files:
+ else if (["mp3", "mpeg", "opus", "ogg", "oga", "wav", "aac", "caf", "m4a", "weba", "dolby", "flac"].indexOf(extension) > -1)
{
soundResources.add(name);
- if (extension === 'wav')
+ if (extension === "wav")
{
this.psychoJS.logger.warn(`wav files are not supported by all browsers. We recommend you convert "${name}" to another format, e.g. mp3`);
}
}
- // preload.js for the other extensions (download type decided by preload.js):
+ // font files:
+ else if (["ttf", "otf", "woff", "woff2","eot"].indexOf(pathExtension) > -1)
+ {
+ fontResources.push(name);
+ }
+
+ // survey models:
+ else if (["sid"].indexOf(extension) > -1)
+ {
+ surveyModelResources.push(name);
+ }
+
+ // all other extensions handled by preload.js (download type decided by preload.js):
else
{
- manifest.push(/*new createjs.LoadItem().set(*/{
+ preloadManifest.push(/*new createjs.LoadItem().set(*/ {
id: name,
src: pathStatusData.path,
- crossOrigin: 'Anonymous'
- }/*)*/);
+ crossOrigin: "Anonymous",
+ } /*)*/);
}
}
-
- // (*) start loading non-sound resources:
- if (manifest.length > 0)
+ // start loading resources marked for preload.js:
+ if (preloadManifest.length > 0)
{
- this._resourceQueue.loadManifest(manifest);
+ this._preloadQueue.loadManifest(preloadManifest);
}
else
{
@@ -1116,156 +1426,410 @@ Source: core/ServerManager.js
{
this.setStatus(ServerManager.Status.READY);
this.emit(ServerManager.Event.RESOURCE, {
- message: ServerManager.Event.DOWNLOAD_COMPLETED});
+ message: ServerManager.Event.DOWNLOAD_COMPLETED,
+ });
}
}
+ // start loading fonts:
+ for (const name of fontResources)
+ {
+ const pathStatusData = this._resources.get(name);
+ pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADING;
+ this.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.DOWNLOADING_RESOURCE,
+ resource: name,
+ });
- // (*) prepare and start loading sound resources:
+ const pathExtension = pathStatusData.path.toLowerCase().split(".").pop();
+ try
+ {
+ const newFont = await new FontFace(name, `url('${pathStatusData.path}') format('${pathExtension}')`).load();
+ document.fonts.add(newFont);
+
+ ++this._nbLoadedResources;
+
+ pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADED;
+ this.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.RESOURCE_DOWNLOADED,
+ resource: name,
+ });
+
+ if (this._nbLoadedResources === resources.size)
+ {
+ this.setStatus(ServerManager.Status.READY);
+ this.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.DOWNLOAD_COMPLETED,
+ });
+ }
+ }
+ catch (error)
+ {
+ console.error(error);
+ this.setStatus(ServerManager.Status.ERROR);
+ pathStatusData.status = ServerManager.ResourceStatus.ERROR;
+ throw Object.assign(response, {
+ error: `unable to download resource: ${name}: ${error}`
+ });
+ }
+ }
+
+ // start loading the survey models:
+ const self = this;
+ for (const name of surveyModelResources)
+ {
+ const pathStatusData = this._resources.get(name);
+ pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADING;
+ this.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.DOWNLOADING_RESOURCE,
+ resource: name,
+ });
+
+ try
+ {
+ const getResponse = await this._queryServerAPI("GET", `surveys/${pathStatusData.path}/model`);
+
+ const getModelResponse = await getResponse.json();
+
+ if (getResponse.status !== 200)
+ {
+ const error = ("error" in getModelResponse) ? getModelResponse.error : getModelResponse;
+ throw util.toString(error);
+ }
+
+ if (getModelResponse["model"] === null)
+ {
+ throw "either there is no survey with the given id, or it is not currently active";
+ }
+
+ ++self._nbLoadedResources;
+
+ // note: we encode the json model as a string since it will be decoded in Survey.setModel,
+ // just like the model loaded directly from a resource by preloadJS
+ pathStatusData.data = new TextEncoder().encode(JSON.stringify(getModelResponse['model']));
+
+ pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADED;
+ self.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.RESOURCE_DOWNLOADED,
+ resource: name,
+ });
+
+ if (self._nbLoadedResources === resources.size)
+ {
+ self.setStatus(ServerManager.Status.READY);
+ self.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.DOWNLOAD_COMPLETED,
+ });
+ }
+ }
+ catch(error)
+ {
+ console.error(error);
+ self.setStatus(ServerManager.Status.ERROR);
+ throw { ...response, error: `unable to download resource: ${name}: ${util.toString(error)}` };
+ }
+ }
+
+ // start loading resources marked for howler.js:
+ // TODO load them sequentially, not all at once!
for (const name of soundResources)
{
const pathStatusData = this._resources.get(name);
- pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADING;
+ pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADING;
this.emit(ServerManager.Event.RESOURCE, {
message: ServerManager.Event.DOWNLOADING_RESOURCE,
- resource: name
+ resource: name,
});
const howl = new Howl({
src: pathStatusData.path,
preload: false,
- autoplay: false
+ autoplay: false,
});
- howl.on('load', (event) =>
+ howl.on("load", (event) =>
{
- ++ self._nbLoadedResources;
+ ++self._nbLoadedResources;
pathStatusData.data = howl;
- pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADED;
+ pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADED;
self.emit(ServerManager.Event.RESOURCE, {
message: ServerManager.Event.RESOURCE_DOWNLOADED,
- resource: name
+ resource: name,
});
if (self._nbLoadedResources === resources.size)
{
self.setStatus(ServerManager.Status.READY);
self.emit(ServerManager.Event.RESOURCE, {
- message: ServerManager.Event.DOWNLOAD_COMPLETED});
+ message: ServerManager.Event.DOWNLOAD_COMPLETED,
+ });
}
});
- howl.on('loaderror', (id, error) =>
+ howl.on("loaderror", (id, error) =>
{
// throw { ...response, error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' };
- throw Object.assign(response, {error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')'});
+ throw Object.assign(response, { error: "unable to download resource: " + name + " (" + util.toString(error) + ")" });
});
howl.load();
}
+ }
+
+ /**
+ * Setup the preload.js queue, and the associated callbacks.
+ *
+ * @protected
+ */
+ _setupPreloadQueue()
+ {
+ const response = {
+ origin: "ServerManager.[preload]",
+ context: "when downloading resources"
+ };
+
+ this._preloadQueue = new createjs.LoadQueue(true, "", true);
+
+ const self = this;
+
+ // the loading of a specific resource has started:
+ this._preloadQueue.addEventListener("filestart", (event) =>
+ {
+ const pathStatusData = self._resources.get(event.item.id);
+ pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADING;
+ self.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.DOWNLOADING_RESOURCE,
+ resource: event.item.id,
+ });
+ });
+
+ // the loading of a specific resource has completed:
+ this._preloadQueue.addEventListener("fileload", (event) =>
+ {
+ const pathStatusData = self._resources.get(event.item.id);
+ pathStatusData.data = event.result;
+ pathStatusData.status = ServerManager.ResourceStatus.DOWNLOADED;
+
+ ++self._nbLoadedResources;
+ self.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.RESOURCE_DOWNLOADED,
+ resource: event.item.id,
+ });
+ });
+
+ // the loading of all given resources completed:
+ this._preloadQueue.addEventListener("complete", (event) =>
+ {
+ self._preloadQueue.close();
+ if (self._nbLoadedResources === self._resources.size)
+ {
+ self.setStatus(ServerManager.Status.READY);
+ self.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.DOWNLOAD_COMPLETED,
+ });
+ }
+ });
+
+ // error: we throw an exception
+ this._preloadQueue.addEventListener("error", (event) =>
+ {
+ self.setStatus(ServerManager.Status.ERROR);
+ if (typeof event.item !== "undefined")
+ {
+ const pathStatusData = self._resources.get(event.item.id);
+ pathStatusData.status = ServerManager.ResourceStatus.ERROR;
+ throw Object.assign(response, {
+ error: "unable to download resource: " + event.item.id + " (" + event.title + ")",
+ });
+ }
+ else
+ {
+ console.error(event);
+
+ if (event.title === "FILE_LOAD_ERROR" && typeof event.data !== "undefined")
+ {
+ const id = event.data.id;
+ const title = event.data.src;
+
+ throw Object.assign(response, {
+ error: "unable to download resource: " + id + " (" + title + ")",
+ });
+ }
+ else
+ {
+ throw Object.assign(response, {
+ error: "unspecified download error",
+ });
+ }
+ }
+ });
}
-}
+ /**
+ * Query the pavlovia server API.
+ *
+ * @protected
+ * @param method the HTTP method, i.e. GET, PUT, POST, or DELETE
+ * @param path the resource path, without the server address
+ * @param data the data to be sent
+ * @param {string} [contentType="JSON"] the content type, either JSON or FORM
+ */
+ _queryServerAPI(method, path, data, contentType = "JSON")
+ {
+ const fullPath = `${this._psychoJS.config.pavlovia.URL}/api/v2/${path}`;
+
+ if (method === "PUT" || method === "POST" || method === "DELETE")
+ {
+ if (contentType === "JSON")
+ {
+ return fetch(fullPath, {
+ method,
+ mode: 'cors',
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ redirect: 'follow',
+ referrerPolicy: 'no-referrer',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(data)
+ });
+ }
+ else
+ {
+ const formData = new FormData();
+ for (const attribute in data)
+ {
+ formData.append(attribute, data[attribute]);
+ }
+
+ return fetch(fullPath, {
+ method,
+ mode: 'cors',
+ cache: 'no-cache',
+ credentials: 'same-origin',
+ redirect: 'follow',
+ referrerPolicy: 'no-referrer',
+ body: formData
+ });
+ }
+ }
+
+ if (method === "GET")
+ {
+ let url = new URL(fullPath);
+ url.search = new URLSearchParams(data).toString();
+
+ return fetch(url, {
+ method: "GET",
+ mode: "cors",
+ cache: "no-cache",
+ credentials: "same-origin",
+ redirect: "follow",
+ referrerPolicy: "no-referrer"
+ });
+ }
+
+ throw {
+ origin: "ServerManager._queryServer",
+ context: "when querying the server",
+ error: "the method should be GET, PUT, POST, or DELETE"
+ };
+ }
+}
/**
* Server event
*
- * <p>A server event is emitted by the manager to inform its listeners of either a change of status, or of a resource related event (e.g. download started, download is completed).</p>
+ * <p>A server event is emitted by the manager to inform its listeners of either a change of status, or of a resource
+ * related event (e.g. download started, download is completed).</p>
*
- * @name module:core.ServerManager#Event
* @enum {Symbol}
* @readonly
- * @public
*/
ServerManager.Event = {
/**
* Event type: resource event
*/
- RESOURCE: Symbol.for('RESOURCE'),
+ RESOURCE: Symbol.for("RESOURCE"),
/**
* Event: resources have started to download
*/
- DOWNLOADING_RESOURCES: Symbol.for('DOWNLOADING_RESOURCES'),
+ DOWNLOADING_RESOURCES: Symbol.for("DOWNLOADING_RESOURCES"),
/**
* Event: a specific resource download has started
*/
- DOWNLOADING_RESOURCE: Symbol.for('DOWNLOADING_RESOURCE'),
+ DOWNLOADING_RESOURCE: Symbol.for("DOWNLOADING_RESOURCE"),
/**
* Event: a specific resource has been downloaded
*/
- RESOURCE_DOWNLOADED: Symbol.for('RESOURCE_DOWNLOADED'),
+ RESOURCE_DOWNLOADED: Symbol.for("RESOURCE_DOWNLOADED"),
/**
* Event: resources have all downloaded
*/
- DOWNLOADS_COMPLETED: Symbol.for('DOWNLOAD_COMPLETED'),
+ DOWNLOAD_COMPLETED: Symbol.for("DOWNLOAD_COMPLETED"),
/**
* Event type: status event
*/
- STATUS: Symbol.for('STATUS')
+ STATUS: Symbol.for("STATUS"),
};
-
/**
* Server status
*
- * @name module:core.ServerManager#Status
* @enum {Symbol}
* @readonly
- * @public
*/
ServerManager.Status = {
/**
* The manager is ready.
*/
- READY: Symbol.for('READY'),
+ READY: Symbol.for("READY"),
/**
* The manager is busy, e.g. it is downloaded resources.
*/
- BUSY: Symbol.for('BUSY'),
+ BUSY: Symbol.for("BUSY"),
/**
* The manager has encountered an error, e.g. it was unable to download a resource.
*/
- ERROR: Symbol.for('ERROR')
+ ERROR: Symbol.for("ERROR"),
};
-
/**
* Resource status
*
- * @name module:core.ServerManager#ResourceStatus
* @enum {Symbol}
* @readonly
- * @public
*/
ServerManager.ResourceStatus = {
/**
- * The resource has been registered.
+ * There was an error during downloading, or the resource is in an unknown state.
*/
- REGISTERED: Symbol.for('REGISTERED'),
+ ERROR: Symbol.for("ERROR"),
/**
- * The resource is currently downloading.
+ * The resource has been registered.
*/
- DOWNLOADING: Symbol.for('DOWNLOADING'),
+ REGISTERED: Symbol.for("REGISTERED"),
/**
- * The resource has been downloaded.
+ * The resource is currently downloading.
*/
- DOWNLOADED: Symbol.for('DOWNLOADED'),
+ DOWNLOADING: Symbol.for("DOWNLOADING"),
/**
- * There was an error during downloading, or the resource is in an unknown state.
+ * The resource has been downloaded.
*/
- ERROR: Symbol.for('ERROR'),
+ DOWNLOADED: Symbol.for("DOWNLOADED"),
};
@@ -1274,19 +1838,23 @@