@signiant/media-shuttle-sdk
Version:
The SDK for supporting file transfer to and from Media Shuttle
430 lines (429 loc) • 23.1 kB
JavaScript
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;