UNPKG

@signiant/media-shuttle-sdk

Version:

The SDK for supporting file transfer to and from Media Shuttle

430 lines (429 loc) 23.1 kB
var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var _a; import PlatformStatus from '../constants/PlatformStatus'; import { TransferEventType, signiant_private, } from '@signiant/media-shuttle-sdk-base'; import OrderedTransferUpdateManager from './OrderedTransferUpdateManager'; import { cloneDeep, isFinite, isNil, negate, pickBy } from 'lodash'; var isNotNil = negate(isNil); var BITS_IN_A_BYTE = 8; var safeParseFloat = function (val) { /** * Tested with null, void 0, strings and objects. They all just return NaN, no errors. * * The only weird behaviour I could find is that values like 18m will be parsed to 18 which is arguably wrong. * That said, we are generally expecting either a number as a string or a nil, so we'll ignore this for now. */ if (isFinite(val)) { return val; } return null; }; var statusMap = (_a = {}, _a[TransferEventType.TRANSFER_STARTED] = PlatformStatus.IN_PROGRESS, _a[TransferEventType.TRANSFER_CONNECTING] = PlatformStatus.IN_PROGRESS, _a[TransferEventType.TRANSFER_CONNECTED] = PlatformStatus.IN_PROGRESS, _a[TransferEventType.TRANSFER_DISCONNECTING] = PlatformStatus.IN_PROGRESS, _a[TransferEventType.TRANSFER_DISCONNECTED] = PlatformStatus.IN_PROGRESS, _a[TransferEventType.TRANSFER_PROGRESS] = PlatformStatus.IN_PROGRESS, _a[TransferEventType.TRANSFER_COMPLETED] = PlatformStatus.COMPLETED, _a[TransferEventType.TRANSFER_ERROR] = PlatformStatus.ERROR, _a[TransferEventType.TRANSFER_CANCELED] = PlatformStatus.CANCELLED, _a); var PLATFORM_TERMINAL_STATES = [PlatformStatus.COMPLETED, PlatformStatus.ERROR, PlatformStatus.CANCELLED]; var PlatformTransferEventHandler = /** @class */ (function () { /** * @param {PlatformTransferEventHandlerOptions} options.platformService {PlatformService} - `platformService` - The `PlatformService` instance * @param {PlatformTransferEventHandlerOptions} options.portalId {string} - The portal used for transfers * @param {PlatformTransferEventHandlerOptions} options.accountId {string} - The accountId used for transfers * @param {PlatformTransferEventHandlerOptions} options.serviceId {string} - The serviceId used for transfers * @param {PlatformTransferEventHandlerOptions} options.transferUpdateIntervalMillis {number} - (Optional) Interval used to update the transfer state */ function PlatformTransferEventHandler(_a) { var platformService = _a.platformService, portalId = _a.portalId, userAgent = _a.userAgent, accountId = _a.accountId, serviceId = _a.serviceId, _b = _a.transferUpdateIntervalMillis, transferUpdateIntervalMillis = _b === void 0 ? 5000 : _b; this._orderedTransferUpdateManager = new OrderedTransferUpdateManager({ platformService: platformService, }); this._portalId = portalId; this._userAgent = userAgent; this._accountId = accountId; this._serviceId = serviceId; this._completedFileIds = []; this._intervalId = null; this._transferUpdateIntervalMillis = transferUpdateIntervalMillis; this._numInProgressUpdates = 0; this._transferRateSum = 0; } Object.defineProperty(PlatformTransferEventHandler.prototype, "files", { set: function (files) { this._files = files; }, enumerable: false, configurable: true }); Object.defineProperty(PlatformTransferEventHandler.prototype, "transferId", { set: function (transferId) { this._transferId = transferId; }, enumerable: false, configurable: true }); PlatformTransferEventHandler.prototype.enableEventDispatch = function () { if (this._intervalId) { //Already running return; } signiant_private.LogManager.debug("Enabling event dispatch for transfer [".concat(this._transferId, "]")); //Zap any pre-existing params; this._updateTransferOptionalParams = null; this._intervalId = setInterval(this.dispatchRequest.bind(this), this._transferUpdateIntervalMillis); }; PlatformTransferEventHandler.prototype.disableEventDispatch = function () { if (this._intervalId) { signiant_private.LogManager.debug("Disabling event dispatch for transfer[".concat(this._transferId, "]")); clearInterval(this._intervalId); this._intervalId = null; } }; PlatformTransferEventHandler.prototype.handleEvent = function (event) { return __awaiter(this, void 0, void 0, function () { var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: if (!this._intervalId) { /** * Ignore events if the dispatch loop is disabled * * Note that the dispatch loop is the first thing that is turned * on when a transfer is started or retried so this is mostly protect us from lingering * events after the dispatch loop is disabled. */ return [2 /*return*/]; } if (this.lastError) { this.disableEventDispatch(); throw this.lastError; } _a = event.type; switch (_a) { case TransferEventType.TRANSFER_FILE_STARTING: return [3 /*break*/, 1]; case TransferEventType.TRANSFER_STARTED: return [3 /*break*/, 2]; case TransferEventType.TRANSFER_CONNECTING: return [3 /*break*/, 4]; case TransferEventType.TRANSFER_CONNECTED: return [3 /*break*/, 4]; case TransferEventType.TRANSFER_DISCONNECTING: return [3 /*break*/, 4]; case TransferEventType.TRANSFER_DISCONNECTED: return [3 /*break*/, 4]; case TransferEventType.TRANSFER_COMPLETED: return [3 /*break*/, 4]; case TransferEventType.TRANSFER_ERROR: return [3 /*break*/, 4]; case TransferEventType.TRANSFER_CANCELED: return [3 /*break*/, 4]; case TransferEventType.TRANSFER_PROGRESS: return [3 /*break*/, 6]; } return [3 /*break*/, 8]; case 1: this.handleActiveFile(event.eventData); return [3 /*break*/, 9]; case 2: return [4 /*yield*/, this.handleProtocol(event.eventData)]; case 3: _b.sent(); return [3 /*break*/, 9]; case 4: return [4 /*yield*/, this.handleStatusChange(event)]; case 5: _b.sent(); return [3 /*break*/, 9]; case 6: return [4 /*yield*/, this.handleBytesSent(event.eventData)]; case 7: _b.sent(); return [3 /*break*/, 9]; case 8: return [3 /*break*/, 9]; case 9: return [2 /*return*/]; } }); }); }; PlatformTransferEventHandler.prototype.dispatchRequest = function () { return __awaiter(this, void 0, void 0, function () { var err_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); if (!this._updateTransferOptionalParams) { //Nothing to update here. return [2 /*return*/]; } this.lastError = null; return [4 /*yield*/, this.updateTransfer(this._updateTransferOptionalParams)]; case 1: _a.sent(); return [3 /*break*/, 3]; case 2: err_1 = _a.sent(); signiant_private.LogManager.error("Problem dispatching transfer update: ".concat(err_1.message)); this.lastError = err_1; return [3 /*break*/, 3]; case 3: return [2 /*return*/]; } }); }); }; /** * @param updateTransferOptionalParams Update params being evaluated. * @return {PlatformStatus} Evaluates the the updateTransferOptionalParams.status field for terminal state * (COMPLETED, ERROR, CANCELLED), returning if there is a match or undefined if there is not. * @private */ PlatformTransferEventHandler.prototype.getTerminalStatus = function (updateTransferOptionalParams) { return PLATFORM_TERMINAL_STATES.find(function (state) { return state === updateTransferOptionalParams.status; }); }; /** * @param updateTransferOptionalParams {UpdateTransferOptionalParams} The update params. * @param dispatchNow {boolean} In certain cases (like when we start a new file) we need to dispatch updates * immediately so important information is not missed. This flag controls that behaviour. * @private */ PlatformTransferEventHandler.prototype.setUpdateTransferOptions = function (updateTransferOptionalParams, dispatchNow) { var _a; if (dispatchNow === void 0) { dispatchNow = false; } return __awaiter(this, void 0, void 0, function () { var hasNewStatus; return __generator(this, function (_b) { switch (_b.label) { case 0: hasNewStatus = ((_a = this._updateTransferOptionalParams) === null || _a === void 0 ? void 0 : _a.status) !== updateTransferOptionalParams.status; this._updateTransferOptionalParams = updateTransferOptionalParams; if (!(hasNewStatus || dispatchNow)) return [3 /*break*/, 3]; if (!this._terminalState) return [3 /*break*/, 1]; /** * We cannot transition from a terminal state (by definition final). In principle this shouldn't * happen but if it does, ignore the update and log a warning. */ signiant_private.LogManager.warn("Attempting to update from terminal status [".concat(this._terminalState, "] to [").concat(this._updateTransferOptionalParams.status, "]. Ignoring")); return [3 /*break*/, 3]; case 1: this._terminalState = this.getTerminalStatus(updateTransferOptionalParams); if (this._terminalState) { /** * We have achieved a terminal state we should no longer fire periodic updates * at our _transferUpdateIntervalMillis interval (default 5 seconds). * * It's important to do this before the dispatchRequest below because in a worst case scenario * that request could take longer than the _transferUpdateIntervalMillis which could lead to * a situation where we try an invalid state transition. */ this.disableEventDispatch(); } /** * Force an immediate as we have a new status which we need to capture, or we have provided an explicit * override. */ return [4 /*yield*/, this.dispatchRequest()]; case 2: /** * Force an immediate as we have a new status which we need to capture, or we have provided an explicit * override. */ _b.sent(); _b.label = 3; case 3: return [2 /*return*/]; } }); }); }; PlatformTransferEventHandler.prototype.updateTransfer = function (updateTransferOptionalParams) { //We could add a debug log here but it would be noisy and in most browsers the network tab is basically just as good. return this._orderedTransferUpdateManager.updateTransfer({ serviceId: this._serviceId, accountId: this._accountId, body: __assign({ portalId: this._portalId, fileIds: [], /** * There is some discussion of removing this from the request entirely and extracting * the value from headers in PAPI. Pros and cons but we'll keep it this way in the short term. */ userAgent: this._userAgent }, updateTransferOptionalParams), transferId: this._transferId, }); }; /** * The protocol is dispatched once by the app but required at multiple stages of the update so we have little * choice but to cache the value and apply it when it exists :-/ * * @param params The original params * @return cloned version of the params, possibly with a cached protocol property added. */ PlatformTransferEventHandler.prototype.withProtocol = function (params) { var clonedParams = cloneDeep(params); if (this._lastProtocol) { clonedParams.protocol = this._lastProtocol; } return clonedParams; }; PlatformTransferEventHandler.prototype.handleStatusChange = function (_a) { var eventType = _a.type, eventData = _a.eventData; var platformStatus = statusMap[eventType]; var updateTransferParams = { status: platformStatus }; /** * We need to send this information as part of the completed event or it doesn't show up * in the PORTAL_TRANSFER record regardless of how many bytes we have sent previously. */ if (platformStatus === PlatformStatus.COMPLETED) { var transferSize = eventData.bytesTransferred; if (transferSize) { updateTransferParams.bytesTransferred = transferSize; } updateTransferParams.averageTransferRate = this._numInProgressUpdates ? Math.floor(this._transferRateSum / this._numInProgressUpdates) : 0; } updateTransferParams.fileIds = this.getStatusFileIds(platformStatus); if (this._lastRateInBytesPerSecond) { updateTransferParams.rateInBytesPerSecond = this._lastRateInBytesPerSecond; } return this.setUpdateTransferOptions(this.withProtocol(updateTransferParams)); }; PlatformTransferEventHandler.prototype.toPlatformProgressStats = function (eventData) { var bytesTransferred = eventData.bytesTransferred; var totalBytes = eventData.totalBytes; var bytesRemaining = null; if ([bytesTransferred, totalBytes].every(isFinite)) { bytesRemaining = totalBytes - bytesTransferred; } //Raw rate is in bits. var rateInBytesPerSecond = Math.floor(safeParseFloat(eventData.transferRateInBitsPerSec) / BITS_IN_A_BYTE); var etaMillis = safeParseFloat(eventData.etaSeconds) * 1000; // This if statement is required, since it is possible to // receive a rate of 0 at the end of the transfer, if that // is the case we keep the last reported value instead of using 0 as value if (rateInBytesPerSecond) { // Need to cache this for the completed event this._numInProgressUpdates++; this._transferRateSum += rateInBytesPerSecond; this._lastRateInBytesPerSecond = rateInBytesPerSecond; } return pickBy({ bytesTransferred: bytesTransferred, bytesRemaining: bytesRemaining, rateInBytesPerSecond: rateInBytesPerSecond, etaMillis: etaMillis, }, function (val) { return isNotNil(val) && val >= 0; }); }; PlatformTransferEventHandler.prototype.handleBytesSent = function (eventData) { var platformProgressStats = this.toPlatformProgressStats(eventData); return this.setUpdateTransferOptions(this.withProtocol(__assign({ status: PlatformStatus.IN_PROGRESS, fileIds: this._activeFileId ? [this._activeFileId] : [] }, platformProgressStats))); }; PlatformTransferEventHandler.prototype.handleProtocol = function (eventData) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: if (eventData.protocol) { this._lastProtocol = eventData.protocol; this._server = eventData.server; this._relayServer = eventData.relayServer; } // We still set the transfer options in case the app takes some time to send other events return [4 /*yield*/, this.setUpdateTransferOptions({ status: PlatformStatus.IN_PROGRESS, protocol: this._lastProtocol }, true)]; case 1: // We still set the transfer options in case the app takes some time to send other events _a.sent(); return [2 /*return*/]; } }); }); }; PlatformTransferEventHandler.prototype.handleActiveFile = function (_a) { var _this = this; var _b; var localFilePath = _a.localFilePath; if (this._activeFileId && !this._completedFileIds.includes(this._activeFileId)) { // Avoid duplicate file in case of restarted transfer this._completedFileIds.push(this._activeFileId); // Save previous active file as completed } // This line is required to solve inconsistent paths in Windows machines var standardPath = localFilePath.replace(/\\/g, '/'); this._activeFileId = (_b = this._files.find(function (file) { return (standardPath.startsWith("".concat(_this.destinationPath, "/").concat(file === null || file === void 0 ? void 0 : file.fileName)) || standardPath.startsWith("".concat(file === null || file === void 0 ? void 0 : file.filePath))); })) === null || _b === void 0 ? void 0 : _b.fileId; }; PlatformTransferEventHandler.prototype.getStatusFileIds = function (appStatus) { var fileIds = []; switch (appStatus) { case PlatformStatus.IN_PROGRESS: if (this._activeFileId) { fileIds.push(this._activeFileId); } break; case PlatformStatus.COMPLETED: fileIds.push.apply(fileIds, this._completedFileIds); if (this._activeFileId && !fileIds.includes(this._activeFileId)) { fileIds.push(this._activeFileId); } break; case PlatformStatus.ERROR: //Fallthrough case PlatformStatus.CANCELLED: fileIds.push.apply(fileIds, this._completedFileIds); break; default: // For all other statuses, do not add any files break; } return fileIds; }; Object.defineProperty(PlatformTransferEventHandler.prototype, "destinationPath", { get: function () { return this._destinationPath; }, set: function (destinationPath) { this._destinationPath = destinationPath; }, enumerable: false, configurable: true }); return PlatformTransferEventHandler; }()); export default PlatformTransferEventHandler;