/** * Copyright schukai GmbH and contributors 2022. All Rights Reserved. * Node module: @schukai/monster * This file is licensed under the AGPLv3 License. * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html */ import {internalSymbol, instanceSymbol} from "../../constants.mjs"; import {isString, isObject} from "../../types/is.mjs"; import {WebConnect} from "../../net/webconnect.mjs"; import {Message} from "../../net/webconnect/message.mjs"; import {Datasource} from "../datasource.mjs"; import {Pathfinder} from "../pathfinder.mjs"; import {Pipe} from "../pipe.mjs"; export {WebSocketDatasource} /** * @private * @type {Symbol} * * hint: this name is used in the tests. if you want to change it, please change it in the tests as well. */ const webConnectSymbol = Symbol("connection"); /** * * @param self * @param obj * @returns {*} */ function doTransform(type, obj) { const self = this; let transformation = self.getOption(type + '.mapping.transformer'); if (transformation !== undefined) { const pipe = new Pipe(transformation); const callbacks = self.getOption(type + '.mapping.callbacks') if (isObject(callbacks)) { for (const key in callbacks) { if (callbacks.hasOwnProperty(key) && typeof callbacks[key] === 'function') { pipe.setCallback(key, callbacks[key]); } } } obj = pipe.run(obj); } return obj; } /** * The RestAPI is a class that enables a REST API server. * * @externalExample ../../../example/data/storage/restapi.mjs * @license AGPLv3 * @since 3.1.0 * @copyright schukai GmbH * @memberOf Monster.Data.Datasource * @summary The LocalStorage class encapsulates the access to data objects. */ class WebSocketDatasource extends Datasource { /** * * @param {Object} [options] options contains definitions for the datasource. */ constructor(options) { super(); const self = this; if (isString(options)) { options = {url: options}; } if (!isObject(options)) options = {}; this.setOptions(options); this[webConnectSymbol] = new WebConnect({ url: self.getOption('url'), connection: { timeout: self.getOption('connection.timeout'), reconnect: { timeout: self.getOption('connection.reconnect.timeout'), attempts: self.getOption('connection.reconnect.attempts'), enabled: self.getOption('connection.reconnect.enabled') } } }); } /** * * @returns {Promise} */ connect() { return this[webConnectSymbol].connect(); } /** * @returns {boolean} */ isConnected() { return this[webConnectSymbol].isConnected(); } /** * This method is called by the `instanceof` operator. * @returns {symbol} */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/data/datasource/websocket"); } /** * @property {string} url=undefined Defines the resource that you wish to fetch. * @property {Object} connection * @property {Object} connection.timeout=5000 Defines the timeout for the connection. * @property {Number} connection.reconnect.timeout The timeout in milliseconds for the reconnect. * @property {Number} connection.reconnect.attempts The maximum number of reconnects. * @property {Bool} connection.reconnect.enabled If the reconnect is enabled. * @property {Object} write={} Options * @property {Object} write.mapping the mapping is applied before writing. * @property {String} write.mapping.transformer Transformer to select the appropriate entries * @property {Monster.Data.Datasource~exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing. * @property {Object} write.sheathing * @property {Object} write.sheathing.object Object to be wrapped * @property {string} write.sheathing.path Path to the data * @property {Object} read={} Options * @property {String} read.path Path to data * @property {Object} read.mapping the mapping is applied after reading. * @property {String} read.mapping.transformer Transformer to select the appropriate entries * @property {Monster.Data.Datasource~exampleCallback[]} read.mapping.callback with the help of the callback, the structures can be adjusted after reading. */ get defaults() { return Object.assign({}, super.defaults, { url: undefined, write: { mapping: { transformer: undefined, callbacks: {} }, sheathing: { object: undefined, path: undefined, }, }, read: { mapping: { transformer: undefined, callbacks: {} }, path: undefined, }, connection: { timeout: 5000, reconnect: { timeout: 1000, attempts: 1, enabled: false, } } }); } /** * This method closes the connection. * * @returns {Promise} */ close() { return this[webConnectSymbol].close(); } /** * @return {Promise} */ read() { const self = this; return new Promise((resolve, reject) => { while (this[webConnectSymbol].dataReceived() === true) { let obj = this[webConnectSymbol].poll(); if (!isObject(obj)) { reject(new Error('The received data is not an object.')); return; } if (!(obj instanceof Message)) { reject(new Error('The received data is not a Message.')); return; } obj = obj.getData(); obj = self.transformServerPayload.call(self, obj); self.set( obj); } resolve(self.get()); }) }; /** * This prepares the data that comes from the server. * Should not be called directly. * * @private * @param {Object} payload * @returns {Object} */ transformServerPayload(payload) { const self = this; payload = doTransform.call(self, 'read', payload); const dataPath = self.getOption('read.path'); if (dataPath) { payload = (new Pathfinder(payload)).getVia(dataPath); } return payload; } /** * This prepares the data for writing and should not be called directly. * * @private * @param {Object} payload * @returns {Object} */ prepareServerPayload(payload) { const self = this; payload = doTransform.call(self, 'write', payload); let sheathingObject = self.getOption('write.sheathing.object'); let sheathingPath = self.getOption('write.sheathing.path'); if (sheathingObject && sheathingPath) { const sub = payload; payload = sheathingObject; (new Pathfinder(payload)).setVia(sheathingPath, sub); } return payload; } /** * @return {Promise} */ write() { const self = this; let obj = self.prepareServerPayload(self.get()); return self[webConnectSymbol].send(obj) } /** * @return {RestAPI} */ getClone() { const self = this; return new WebSocketDatasource(self[internalSymbol].getRealSubject()['options']); } }