* Bump d3-selection from 1.3.2 to 3.0.0 Bumps [d3-selection](https://github.com/d3/d3-selection) from 1.3.2 to 3.0.0. - [Release notes](https://github.com/d3/d3-selection/releases) - [Commits](https://github.com/d3/d3-selection/compare/v1.3.2...v3.0.0) --- updated-dependencies: - dependency-name: d3-selection dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> * Remove snapshot * Fix imagery filter slider drag in flexible layouts (#5326) (#5350) * Dont' mutate a stacked plot unless its user initiated (#5357) * Port grid icons and imagery test to release 2.0.5 from master (#5360) * Port grid icons to release 2.0.5 from master * Port imagery test to release/2.0.5 * Restrict timestrip composition to time based plots, plans and imagery (#5161) * Restrict timestrip composition to time based plots, plans and imagery * Adds unit tests for timeline composition policy * Addresses review comments Improves tests * Reuse test objects Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov> * Include objectStyles reference to conditionSetIdentifier in imports (#5354) * Include objectStyles reference to conditionSetIdentifier in imports * Add tests for export * Refactored some code and removed console log * Remove workarounds for chrome 'scrollTop' issue (#5375) * Fix naming of method (#5368) * Imagery View does not discard old images when they fall out of bounds (#5351) * change to using telemetry collection * fix tests * added more unit tests * Cherrypicked commits (#5390) Co-authored-by: unlikelyzero <jchill2@gmail.com> * [Timer] Update 3dot menu actions appropriately (#5387) * Call `removeAllListeners()` after emit * Manually show/hide actions if within a view * remove sneaky `console.log()` * Add Timer e2e test * Add to comments * Avoid hard waits in Timer e2e test - Assert against timer view state instead of menu options * Let's also test actions from the Timer view * 5391 Add preview and drag support to Grand Search (#5394) * add preview and drag actions * added unit test, simplified remove action * do not hide search results in preview mode when clicking outside search results * add semantic aria labels to enable e2e tests * readd preview * add e2e test * remove commented out url * add percy snapshot and add search to ci * make percy stuff work * linting * fix percy again * move percy snapshots to a visual test * added separate visual test and changed test to fixtures * fix fixtures path * addressing review comments * 5361 tags not persisting locally (#5408) * fixed typo * remove unneeded lookup * fix tags adding and deleting * more reliable way to remove tags * break tests up for parallel execution * fixed notebook tagging test * enable e2e tests * made schedule index comment more clear and fix uppercase/lowercase issue * address e2e changes * add unit test to bump coverage * fix typo * need to check on annotation creation if provider exists or not * added fixtures * undo silly couchdb commit * Plot progress bar fix for 2.0.5 (#5386) * Add .bind(this) to stopLoading() in loadMoreData() * Replace load spinner with progress bar for plots * Add loading delay prop to swg * fix linting errors * match load order * Update accessibility * Add Math.max to timeout to handle negative inputs * Moved math.max to load delay variable * Add loading fix for stacked plots * Move loadingUpdate func into plot item for update * Merge conflict resolve * Check if delay is 0 and send, put post in a func * Put obj directly to model, removed computed prop * Lint fix * Fix template where legend was not displayed * Remove commented out template * Fixed failing test Co-authored-by: unlikelyzero <jchill2@gmail.com> * Make plans non editable. (#5377) * Make plans non editable. * Add unit test for fix * [CouchDB] Better determination of indicator status (#5415) * Add unknown state, remove maintenance state * Handle all CouchDB status codes - Set unknown status if we receive an unhandled code * Include status code in error messages * SharedWorker can send unknown status * Add test for unknown status * Gauge fixes for Firefox and units display (#5369) * Closes #5323, #5325. Parent branch is release/2.0.5. - Significant work refactoring SVG markup and CSS for dial gauge; - Fixed missing `v-if` to control display of units for #5325; - Fixed bad `.length` test for limit properties; * Closes #5323, #5325 - Add 'value out of range' indicator * Closes #5323, #5325 - More accurate element naming; - Fix cross-browser problems with current value display in dial gauge; - Refinements to "out of range" indicator approach; - Fixed size of "Amplitude" input in Sine Wave Generator; * Closes #5323, #5325 - Styles and stubbed in code to support needle meter type; * Closes #5323, #5325 - Stubbed in markup and CSS for needle-style meter; * Closes #5323, #5325 - Fixed missing `js-*` classes that were failing npm run test; * Closes #5323, #5325 - Fix to not display meter value bar unless a data value is expected; * Addressing PR comments - Renamed method for clarity; - Added null value check in method `valueExpected`; * [Static Root] Return leafValue if null/undefined/false (#5416) * Return leafValue if null/undefined/false * Added a null to the test json * Show a better default poll question (#5425) * 5361 Tags not persisting when several notebook entries are created at once (#5428) * add end to end test to catch multiple entry errors * click expansion triangle instead * fix race condition between annotation creation and mutation * make sure notebook tags run in e2e * address PR comments * Handle missing objects gracefully (#5399) * Handle missing object errors for display layouts * Handle missing object errors for Overlay Plots * Add check for this.config * Add try/catch statement & check if obj is missing * Changed console.error to console.warn * Lint fix * Fix for this.metadata.value is undefined * Add e2e test * Update comment text * Add reload check and @private, verify console.warn * Redid assignment and metadata check * Fix typo * Changed assignment and metadata check * Redid checks for isMissing(object) * Lint fix * Backmerge e2e code coverage changes and fixes into release/2.0.5 (#5431) * [Telemetry Collections] Respect "Latest" Strategy Option (#5421) * Respect latest strategy in Telemetry Collections to limit potential memory growth. * fix sourcemaps (#5373) Co-authored-by: John Hill <john.c.hill@nasa.gov> * Debounce status summary (#5448) Co-authored-by: John Hill <john.c.hill@nasa.gov> * No gauge (#5451) * Installed gauge plugin by default * Make gauge part of standard install in e2e suite and add restrictednotebook Co-authored-by: Andrew Henry <akhenry@gmail.com> * [CouchDB] Always subscribe to the CouchDB changes feed (#5434) * Add unknown state, remove maintenance state * Handle all CouchDB status codes - Set unknown status if we receive an unhandled code * Include status code in error messages * SharedWorker can send unknown status * Add test for unknown status * Always subscribe to CouchDB changes feed - Always subscribe to the CouchDB changes feed, even if there are no observable objects, since we are also checking the status of CouchDB via this feed. * Update indicator status if not using SharedWorker * Start listening to changes feed on first request * fix test * adjust test to hopefully avoid race condition * lint Co-authored-by: John Hill <john.c.hill@nasa.gov> Co-authored-by: Andrew Henry <akhenry@gmail.com> Co-authored-by: Scott Bell <scott@traclabs.com> * Fix for Fault Management Visual Bugs (#5376) * Closes #5365 * General visual improvements Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com> Co-authored-by: Andrew Henry <akhenry@gmail.com> * fix pathing (#5452) Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> * [Static Root] Static Root Plugin not loading (#5455) * Log if hitting falsy leafValue * Add some logging * Remove logs and specify null/undefined Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> * Allow endpoints with a single enum metadata value in Bar/Line graphs (#5443) * If there is only 1 metadata value, set yKey to none. Also, fix bug for determining the name of a metadata value * Update tests for enum metadata values Co-authored-by: John Hill <john.c.hill@nasa.gov> Co-authored-by: Andrew Henry <akhenry@gmail.com> * [Remote Clock] Wait for first tick and recalculate historical request bounds (#5433) * Updated to ES6 class * added request intercept functionality to telemetry api, added a request interceptor for remote clock * add remoteClock e2e test stub Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov> Co-authored-by: Andrew Henry <akhenry@gmail.com> * Fix for missing object for LADTableSet (#5458) * Handle missing object errors for display layouts Co-authored-by: Andrew Henry <akhenry@gmail.com> * removing the call for default import now that TelemetryAPI is an ES6 class (#5461) * [Remote Clock] Fix requestInterceptor typo (#5462) * Fix typo in telemetry request interceptor Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov> Co-authored-by: Andrew Henry <akhenry@gmail.com> * Lock model (#5457) * Lock event Model to prevent reactification * de-reactify all the things * Make API properties writable to allow test mocks to override them * Fix merge conflict * Added plot interceptor for missing series config (#5422) Co-authored-by: Andrew Henry <akhenry@gmail.com> Co-authored-by: Shefali Joshi <simplyrender@gmail.com> * Remove performance marks (#5465) * Remove performance marks * Retain performance mark in view large. It doesn't happen very often and it's needed for an automated performance test * Use timeKey for time comparison (#5471) * Fix couchdb no response (#5474) * Update the creation date only when the document is created for the first time * If there is no response from a bulk get, couch db has issues * Check the response - if it's null, don't apply interceptors * Fix shelved alarms (#5479) * Fix the logic around shelved alarms * Remove application router listener * Release 2.0.5 UI and Gauge fixes (#5470) * Various UI fixes - Tweak to Gauge properties form for clarity and usability. - Fix Gauge 'dial' type not obeying "Show units" property setting, closes #5325. - Tweaks to Operator Status UI label and layout for clarity. - Changed name and description of Graph object for clarity and consistency. - Fixed CSS classing that was coloring Export menu items text incorrectly. - Fixed icon-to-text vertical alignment in `.c-object-label`. - Fix for broken layout in imagery local controls (brightness, layers, magnification). Co-authored-by: Andrew Henry <akhenry@gmail.com> * Stacked plot interceptor rename (#5468) * Rename stacked plot interceptor and move to folder Co-authored-by: Andrew Henry <akhenry@gmail.com> * Clear data when time bounds are changed (#5482) * Clear data when time bounds are changed Also react to clear data action Ensure that the yKey is set to 'none' if there is no range with array Values * Refactor trace updates to a method * get rid of root (#5483) * Do not pass onPartialResponse option on to upstream telemetry (#5486) * Fix all of the e2e tests (#5477) * Fix timer test * be explicit about the warnings text * add full suite to CI to enable CircleCI Checks * add back in devtool=false for CI env so firefox tests run * add framework suite * Don't install webpack HMR in CI * Fix playwright version installs * exclude HMR if running tests in any environment - use NODE_ENV=TEST to exclude webpack HMR - deparameterize some of the playwright configs * use lower-case 'test' * timer hover fix * conditionally skip for firefox due to missing console events * increase timeouts to give time for mutation * no need to close save banner * remove devtool setting * revert * update snapshots * disable video to save some resources * use one worker * more timeouts :) * Remove `browser.close()` and `page.close()` as it was breaking other tests * Remove unnecessary awaits and fix func call syntax * Fix image reset test * fix restrictedNotebook tests * revert playwright-ci.config settings * increase timeout for polling imagery test * remove unnecessary waits * disable notebook lock test for chrome-beta as its unreliable - remove some unnecessary 'wait for save banner' logic - remove unused await - mark imagery test as slow in chrome-beta * LINT!! *shakes fist* * don't run full e2e suite per commit * disable video in all configs * add flakey zoom comment * exclude webpack HMR in non-development modes Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov> Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> * lint fix Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Joshi <simplyrender@gmail.com> Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov> Co-authored-by: Andrew Henry <akhenry@gmail.com> Co-authored-by: Scott Bell <scott@traclabs.com> Co-authored-by: unlikelyzero <jchill2@gmail.com> Co-authored-by: Alize Nguyen <alizenguyen@gmail.com> Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com> Co-authored-by: Khalid Adil <khalidadil29@gmail.com> Co-authored-by: rukmini-bose <48999852+rukmini-bose@users.noreply.github.com> Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
736 lines
23 KiB
JavaScript
736 lines
23 KiB
JavaScript
/*****************************************************************************
|
|
* Open MCT, Copyright (c) 2014-2022, United States Government
|
|
* as represented by the Administrator of the National Aeronautics and Space
|
|
* Administration. All rights reserved.
|
|
*
|
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations
|
|
* under the License.
|
|
*
|
|
* Open MCT includes source code licensed under additional open source
|
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
* this source code distribution or the Licensing information page available
|
|
* at runtime from the About dialog for additional information.
|
|
*****************************************************************************/
|
|
|
|
import CouchDocument from "./CouchDocument";
|
|
import CouchObjectQueue from "./CouchObjectQueue";
|
|
import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from "./CouchStatusIndicator";
|
|
import { isNotebookType } from '../../notebook/notebook-constants.js';
|
|
|
|
const REV = "_rev";
|
|
const ID = "_id";
|
|
const HEARTBEAT = 50000;
|
|
const ALL_DOCS = "_all_docs?include_docs=true";
|
|
|
|
class CouchObjectProvider {
|
|
constructor(openmct, options, namespace, indicator) {
|
|
options = this.#normalize(options);
|
|
this.openmct = openmct;
|
|
this.indicator = indicator;
|
|
this.url = options.url;
|
|
this.namespace = namespace;
|
|
this.objectQueue = {};
|
|
this.observers = {};
|
|
this.batchIds = [];
|
|
this.onEventMessage = this.onEventMessage.bind(this);
|
|
this.onEventError = this.onEventError.bind(this);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
#startSharedWorker() {
|
|
let provider = this;
|
|
let sharedWorker;
|
|
|
|
// eslint-disable-next-line no-undef
|
|
const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}couchDBChangesFeed.js`;
|
|
|
|
sharedWorker = new SharedWorker(sharedWorkerURL, 'CouchDB SSE Shared Worker');
|
|
sharedWorker.port.onmessage = provider.onSharedWorkerMessage.bind(this);
|
|
sharedWorker.port.onmessageerror = provider.onSharedWorkerMessageError.bind(this);
|
|
sharedWorker.port.start();
|
|
|
|
this.openmct.on('destroy', () => {
|
|
this.changesFeedSharedWorker.port.postMessage({
|
|
request: 'close',
|
|
connectionId: this.changesFeedSharedWorkerConnectionId
|
|
});
|
|
this.changesFeedSharedWorker.port.close();
|
|
});
|
|
|
|
return sharedWorker;
|
|
}
|
|
|
|
onSharedWorkerMessageError(event) {
|
|
console.log('Error', event);
|
|
}
|
|
|
|
isSynchronizedObject(object) {
|
|
return (object && object.type
|
|
&& this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES
|
|
&& this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES.includes(object.type));
|
|
|
|
}
|
|
|
|
onSharedWorkerMessage(event) {
|
|
if (event.data.type === 'connection') {
|
|
this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
|
|
} else if (event.data.type === 'state') {
|
|
const state = this.#messageToIndicatorState(event.data.state);
|
|
this.indicator.setIndicatorToState(state);
|
|
} else {
|
|
let objectChanges = event.data.objectChanges;
|
|
const objectIdentifier = {
|
|
namespace: this.namespace,
|
|
key: objectChanges.id
|
|
};
|
|
let keyString = this.openmct.objects.makeKeyString(objectIdentifier);
|
|
//TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have.
|
|
let observersForObject = this.observers[keyString];
|
|
|
|
if (observersForObject) {
|
|
observersForObject.forEach(async (observer) => {
|
|
const updatedObject = await this.get(objectIdentifier);
|
|
if (this.isSynchronizedObject(updatedObject)) {
|
|
observer(updatedObject);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Takes in a state message from the CouchDB SharedWorker and returns an IndicatorState.
|
|
* @private
|
|
* @param {'open'|'close'|'pending'} message
|
|
* @returns {import('./CouchStatusIndicator').IndicatorState}
|
|
*/
|
|
#messageToIndicatorState(message) {
|
|
let state;
|
|
switch (message) {
|
|
case 'open':
|
|
state = CONNECTED;
|
|
break;
|
|
case 'close':
|
|
state = DISCONNECTED;
|
|
break;
|
|
case 'pending':
|
|
state = PENDING;
|
|
break;
|
|
case 'unknown':
|
|
state = UNKNOWN;
|
|
break;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Takes an HTTP status code and returns an IndicatorState
|
|
* @private
|
|
* @param {number} statusCode
|
|
* @returns {import("./CouchStatusIndicator").IndicatorState}
|
|
*/
|
|
#statusCodeToIndicatorState(statusCode) {
|
|
let state;
|
|
switch (statusCode) {
|
|
case CouchObjectProvider.HTTP_OK:
|
|
case CouchObjectProvider.HTTP_CREATED:
|
|
case CouchObjectProvider.HTTP_ACCEPTED:
|
|
case CouchObjectProvider.HTTP_NOT_MODIFIED:
|
|
case CouchObjectProvider.HTTP_BAD_REQUEST:
|
|
case CouchObjectProvider.HTTP_UNAUTHORIZED:
|
|
case CouchObjectProvider.HTTP_FORBIDDEN:
|
|
case CouchObjectProvider.HTTP_NOT_FOUND:
|
|
case CouchObjectProvider.HTTP_METHOD_NOT_ALLOWED:
|
|
case CouchObjectProvider.HTTP_NOT_ACCEPTABLE:
|
|
case CouchObjectProvider.HTTP_CONFLICT:
|
|
case CouchObjectProvider.HTTP_PRECONDITION_FAILED:
|
|
case CouchObjectProvider.HTTP_REQUEST_ENTITY_TOO_LARGE:
|
|
case CouchObjectProvider.HTTP_UNSUPPORTED_MEDIA_TYPE:
|
|
case CouchObjectProvider.HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
|
|
case CouchObjectProvider.HTTP_EXPECTATION_FAILED:
|
|
case CouchObjectProvider.HTTP_SERVER_ERROR:
|
|
state = CONNECTED;
|
|
break;
|
|
case CouchObjectProvider.HTTP_SERVICE_UNAVAILABLE:
|
|
state = DISCONNECTED;
|
|
break;
|
|
default:
|
|
state = UNKNOWN;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
//backwards compatibility, options used to be a url. Now it's an object
|
|
#normalize(options) {
|
|
if (typeof options === 'string') {
|
|
return {
|
|
url: options
|
|
};
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
async request(subPath, method, body, signal) {
|
|
let fetchOptions = {
|
|
method,
|
|
body,
|
|
signal
|
|
};
|
|
|
|
// stringify body if needed
|
|
if (fetchOptions.body) {
|
|
fetchOptions.body = JSON.stringify(fetchOptions.body);
|
|
fetchOptions.headers = {
|
|
"Content-Type": "application/json"
|
|
};
|
|
}
|
|
|
|
let response = null;
|
|
|
|
if (!this.isObservingObjectChanges()) {
|
|
this.#observeObjectChanges();
|
|
}
|
|
|
|
try {
|
|
response = await fetch(this.url + '/' + subPath, fetchOptions);
|
|
const { status } = response;
|
|
const json = await response.json();
|
|
this.#handleResponseCode(status, json, fetchOptions);
|
|
|
|
return json;
|
|
} catch (error) {
|
|
// Network error, CouchDB unreachable.
|
|
if (response === null) {
|
|
this.indicator.setIndicatorToState(DISCONNECTED);
|
|
console.error(error.message);
|
|
throw new Error(`CouchDB Error - No response"`);
|
|
}
|
|
|
|
console.error(error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle the response code from a CouchDB request.
|
|
* Sets the CouchDB indicator status and throws an error if needed.
|
|
* @private
|
|
*/
|
|
#handleResponseCode(status, json, fetchOptions) {
|
|
this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
|
|
if (status === CouchObjectProvider.HTTP_CONFLICT) {
|
|
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
|
|
} else if (status >= CouchObjectProvider.HTTP_BAD_REQUEST) {
|
|
if (!json.error || !json.reason) {
|
|
throw new Error(`CouchDB Error ${status}`);
|
|
}
|
|
|
|
throw new Error(`CouchDB Error ${status}: "${json.error} - ${json.reason}"`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check the response to a create/update/delete request;
|
|
* track the rev if it's valid, otherwise return false to
|
|
* indicate that the request failed.
|
|
* persist any queued objects
|
|
* @private
|
|
*/
|
|
#checkResponse(response, intermediateResponse, key) {
|
|
let requestSuccess = false;
|
|
const id = response ? response.id : undefined;
|
|
let rev;
|
|
|
|
if (response && response.ok) {
|
|
rev = response.rev;
|
|
requestSuccess = true;
|
|
}
|
|
|
|
intermediateResponse.resolve(requestSuccess);
|
|
|
|
if (id) {
|
|
if (!this.objectQueue[id]) {
|
|
this.objectQueue[id] = new CouchObjectQueue(undefined, rev);
|
|
}
|
|
|
|
this.objectQueue[id].updateRevision(rev);
|
|
this.objectQueue[id].pending = false;
|
|
if (this.objectQueue[id].hasNext()) {
|
|
this.#updateQueued(id);
|
|
}
|
|
} else {
|
|
this.objectQueue[key].pending = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
#getModel(response) {
|
|
if (response && response.model) {
|
|
let key = response[ID];
|
|
let object = this.fromPersistedModel(response.model, key);
|
|
|
|
if (!this.objectQueue[key]) {
|
|
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
|
|
}
|
|
|
|
if (isNotebookType(object)) {
|
|
//Temporary measure until object sync is supported for all object types
|
|
//Always update notebook revision number because we have realtime sync, so always assume it's the latest.
|
|
this.objectQueue[key].updateRevision(response[REV]);
|
|
} else if (!this.objectQueue[key].pending) {
|
|
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
|
|
this.objectQueue[key].updateRevision(response[REV]);
|
|
}
|
|
|
|
return object;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
get(identifier, abortSignal) {
|
|
this.batchIds.push(identifier.key);
|
|
|
|
if (this.bulkPromise === undefined) {
|
|
this.bulkPromise = this.#deferBatchedGet(abortSignal);
|
|
}
|
|
|
|
return this.bulkPromise
|
|
.then((domainObjectMap) => {
|
|
return domainObjectMap[identifier.key];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
#deferBatchedGet(abortSignal) {
|
|
// We until the next event loop cycle to "collect" all of the get
|
|
// requests triggered in this iteration of the event loop
|
|
|
|
return this.#waitOneEventCycle().then(() => {
|
|
let batchIds = this.batchIds;
|
|
|
|
this.#clearBatch();
|
|
|
|
if (batchIds.length === 1) {
|
|
let objectKey = batchIds[0];
|
|
|
|
//If there's only one request, just do a regular get
|
|
return this.request(objectKey, "GET", undefined, abortSignal)
|
|
.then(this.#returnAsMap(objectKey));
|
|
} else {
|
|
return this.#bulkGet(batchIds, abortSignal);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
#returnAsMap(objectKey) {
|
|
return (result) => {
|
|
let objectMap = {};
|
|
objectMap[objectKey] = this.#getModel(result);
|
|
|
|
return objectMap;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
#clearBatch() {
|
|
this.batchIds = [];
|
|
delete this.bulkPromise;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
#waitOneEventCycle() {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
#bulkGet(ids, signal) {
|
|
ids = this.removeDuplicates(ids);
|
|
|
|
const query = {
|
|
'keys': ids
|
|
};
|
|
|
|
return this.request(ALL_DOCS, 'POST', query, signal).then((response) => {
|
|
if (response && response.rows !== undefined) {
|
|
return response.rows.reduce((map, row) => {
|
|
//row.doc === null if the document does not exist.
|
|
//row.doc === undefined if the document is not found.
|
|
if (row.doc !== undefined) {
|
|
map[row.key] = this.#getModel(row.doc);
|
|
}
|
|
|
|
return map;
|
|
}, {});
|
|
} else {
|
|
return {};
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
removeDuplicates(array) {
|
|
return Array.from(new Set(array));
|
|
}
|
|
|
|
search() {
|
|
// Dummy search function. It has to appear to support search,
|
|
// otherwise the in-memory indexer will index all of its objects,
|
|
// but actually search results will be provided by a separate search provider
|
|
// see CoucheSearchProvider.js
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
async getObjectsByFilter(filter, abortSignal) {
|
|
let objects = [];
|
|
|
|
let url = `${this.url}/_find`;
|
|
let body = {};
|
|
|
|
if (filter) {
|
|
body = JSON.stringify(filter);
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
signal: abortSignal,
|
|
body
|
|
});
|
|
|
|
const reader = response.body.getReader();
|
|
let completed = false;
|
|
let decoder = new TextDecoder("utf-8");
|
|
let decodedChunk = '';
|
|
while (!completed) {
|
|
const {done, value} = await reader.read();
|
|
//done is true when we lose connection with the provider
|
|
if (done) {
|
|
completed = true;
|
|
}
|
|
|
|
if (value) {
|
|
let chunk = new Uint8Array(value.length);
|
|
chunk.set(value, 0);
|
|
const partial = decoder.decode(chunk, {stream: !completed});
|
|
decodedChunk = decodedChunk + partial;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const json = JSON.parse(decodedChunk);
|
|
if (json) {
|
|
let docs = json.docs;
|
|
docs.forEach(doc => {
|
|
let object = this.#getModel(doc);
|
|
if (object) {
|
|
objects.push(object);
|
|
}
|
|
});
|
|
}
|
|
} catch (e) {
|
|
//do nothing
|
|
}
|
|
|
|
return objects;
|
|
}
|
|
|
|
observe(identifier, callback) {
|
|
const keyString = this.openmct.objects.makeKeyString(identifier);
|
|
this.observers[keyString] = this.observers[keyString] || [];
|
|
this.observers[keyString].push(callback);
|
|
|
|
if (!this.isObservingObjectChanges()) {
|
|
this.#observeObjectChanges();
|
|
}
|
|
|
|
return () => {
|
|
if (this.observers[keyString]) {
|
|
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
|
|
if (this.observers[keyString].length === 0) {
|
|
delete this.observers[keyString];
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
isObservingObjectChanges() {
|
|
return this.stopObservingObjectChanges !== undefined;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
#observeObjectChanges() {
|
|
const sseChangesPath = `${this.url}/_changes`;
|
|
const sseURL = new URL(sseChangesPath);
|
|
sseURL.searchParams.append('feed', 'eventsource');
|
|
sseURL.searchParams.append('style', 'main_only');
|
|
sseURL.searchParams.append('heartbeat', HEARTBEAT);
|
|
|
|
if (typeof SharedWorker === 'undefined') {
|
|
this.fetchChanges(sseURL.toString());
|
|
} else {
|
|
this.#initiateSharedWorkerFetchChanges(sseURL.toString());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
#initiateSharedWorkerFetchChanges(url) {
|
|
if (!this.changesFeedSharedWorker) {
|
|
this.changesFeedSharedWorker = this.#startSharedWorker();
|
|
|
|
if (this.isObservingObjectChanges()) {
|
|
this.stopObservingObjectChanges();
|
|
}
|
|
|
|
this.stopObservingObjectChanges = () => {
|
|
delete this.stopObservingObjectChanges;
|
|
};
|
|
|
|
this.changesFeedSharedWorker.port.postMessage({
|
|
request: 'changes',
|
|
url
|
|
});
|
|
}
|
|
}
|
|
|
|
onEventError(error) {
|
|
console.error('Error on feed', error);
|
|
const { readyState } = error.target;
|
|
this.#updateIndicatorStatus(readyState);
|
|
}
|
|
|
|
onEventOpen(event) {
|
|
const { readyState } = event.target;
|
|
this.#updateIndicatorStatus(readyState);
|
|
}
|
|
|
|
onEventMessage(event) {
|
|
const { readyState } = event.target;
|
|
const eventData = JSON.parse(event.data);
|
|
const identifier = {
|
|
namespace: this.namespace,
|
|
key: eventData.id
|
|
};
|
|
const keyString = this.openmct.objects.makeKeyString(identifier);
|
|
this.#updateIndicatorStatus(readyState);
|
|
let observersForObject = this.observers[keyString];
|
|
|
|
if (observersForObject) {
|
|
observersForObject.forEach(async (observer) => {
|
|
const updatedObject = await this.get(identifier);
|
|
if (this.isSynchronizedObject(updatedObject)) {
|
|
observer(updatedObject);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
fetchChanges(url) {
|
|
const controller = new AbortController();
|
|
let couchEventSource;
|
|
|
|
if (this.isObservingObjectChanges()) {
|
|
this.stopObservingObjectChanges();
|
|
}
|
|
|
|
this.stopObservingObjectChanges = () => {
|
|
controller.abort();
|
|
couchEventSource.removeEventListener('message', this.onEventMessage.bind(this));
|
|
delete this.stopObservingObjectChanges;
|
|
};
|
|
|
|
console.debug('⇿ Opening CouchDB change feed connection ⇿');
|
|
|
|
couchEventSource = new EventSource(url);
|
|
couchEventSource.onerror = this.onEventError.bind(this);
|
|
couchEventSource.onopen = this.onEventOpen.bind(this);
|
|
|
|
// start listening for events
|
|
couchEventSource.addEventListener('message', this.onEventMessage.bind(this));
|
|
|
|
console.debug('⇿ Opened connection ⇿');
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
#getIntermediateResponse() {
|
|
let intermediateResponse = {};
|
|
intermediateResponse.promise = new Promise(function (resolve, reject) {
|
|
intermediateResponse.resolve = resolve;
|
|
intermediateResponse.reject = reject;
|
|
});
|
|
|
|
return intermediateResponse;
|
|
}
|
|
|
|
/**
|
|
* Update the indicator status based on the readyState of the EventSource
|
|
* @private
|
|
*/
|
|
#updateIndicatorStatus(readyState) {
|
|
let message;
|
|
switch (readyState) {
|
|
case EventSource.CONNECTING:
|
|
message = 'pending';
|
|
break;
|
|
case EventSource.OPEN:
|
|
message = 'open';
|
|
break;
|
|
case EventSource.CLOSED:
|
|
message = 'close';
|
|
break;
|
|
default:
|
|
message = 'unknown';
|
|
break;
|
|
}
|
|
|
|
const indicatorState = this.#messageToIndicatorState(message);
|
|
this.indicator.setIndicatorToState(indicatorState);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
enqueueObject(key, model, intermediateResponse) {
|
|
if (this.objectQueue[key]) {
|
|
this.objectQueue[key].enqueue({
|
|
model,
|
|
intermediateResponse
|
|
});
|
|
} else {
|
|
this.objectQueue[key] = new CouchObjectQueue({
|
|
model,
|
|
intermediateResponse
|
|
});
|
|
}
|
|
}
|
|
|
|
create(model) {
|
|
let intermediateResponse = this.#getIntermediateResponse();
|
|
const key = model.identifier.key;
|
|
model = this.toPersistableModel(model);
|
|
this.enqueueObject(key, model, intermediateResponse);
|
|
|
|
if (!this.objectQueue[key].pending) {
|
|
this.objectQueue[key].pending = true;
|
|
const queued = this.objectQueue[key].dequeue();
|
|
let document = new CouchDocument(key, queued.model);
|
|
document.metadata.created = Date.now();
|
|
this.request(key, "PUT", document).then((response) => {
|
|
console.log('create check response', key);
|
|
this.#checkResponse(response, queued.intermediateResponse, key);
|
|
}).catch(error => {
|
|
queued.intermediateResponse.reject(error);
|
|
this.objectQueue[key].pending = false;
|
|
});
|
|
}
|
|
|
|
return intermediateResponse.promise;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
#updateQueued(key) {
|
|
if (!this.objectQueue[key].pending) {
|
|
this.objectQueue[key].pending = true;
|
|
const queued = this.objectQueue[key].dequeue();
|
|
let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev);
|
|
this.request(key, "PUT", document).then((response) => {
|
|
this.#checkResponse(response, queued.intermediateResponse, key);
|
|
}).catch((error) => {
|
|
queued.intermediateResponse.reject(error);
|
|
this.objectQueue[key].pending = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
update(model) {
|
|
let intermediateResponse = this.#getIntermediateResponse();
|
|
const key = model.identifier.key;
|
|
model = this.toPersistableModel(model);
|
|
|
|
this.enqueueObject(key, model, intermediateResponse);
|
|
this.#updateQueued(key);
|
|
|
|
return intermediateResponse.promise;
|
|
}
|
|
|
|
toPersistableModel(model) {
|
|
//First make a copy so we are not mutating the provided model.
|
|
const persistableModel = JSON.parse(JSON.stringify(model));
|
|
//Delete the identifier. Couch manages namespaces dynamically.
|
|
delete persistableModel.identifier;
|
|
|
|
return persistableModel;
|
|
}
|
|
|
|
fromPersistedModel(model, key) {
|
|
model.identifier = {
|
|
namespace: this.namespace,
|
|
key
|
|
};
|
|
|
|
return model;
|
|
}
|
|
}
|
|
|
|
// https://docs.couchdb.org/en/3.2.0/api/basics.html
|
|
CouchObjectProvider.HTTP_OK = 200;
|
|
CouchObjectProvider.HTTP_CREATED = 201;
|
|
CouchObjectProvider.HTTP_ACCEPTED = 202;
|
|
CouchObjectProvider.HTTP_NOT_MODIFIED = 304;
|
|
CouchObjectProvider.HTTP_BAD_REQUEST = 400;
|
|
CouchObjectProvider.HTTP_UNAUTHORIZED = 401;
|
|
CouchObjectProvider.HTTP_FORBIDDEN = 403;
|
|
CouchObjectProvider.HTTP_NOT_FOUND = 404;
|
|
CouchObjectProvider.HTTP_METHOD_NOT_ALLOWED = 404;
|
|
CouchObjectProvider.HTTP_NOT_ACCEPTABLE = 406;
|
|
CouchObjectProvider.HTTP_CONFLICT = 409;
|
|
CouchObjectProvider.HTTP_PRECONDITION_FAILED = 412;
|
|
CouchObjectProvider.HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
|
|
CouchObjectProvider.HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
|
|
CouchObjectProvider.HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
|
|
CouchObjectProvider.HTTP_EXPECTATION_FAILED = 417;
|
|
CouchObjectProvider.HTTP_SERVER_ERROR = 500;
|
|
// If CouchDB is containerized via Docker it will return 503 if service is unavailable.
|
|
CouchObjectProvider.HTTP_SERVICE_UNAVAILABLE = 503;
|
|
|
|
export default CouchObjectProvider;
|