@fullstory/server-api-client
Version:
The official FullStory server API client SDK for NodeJS.
284 lines • 11.4 kB
JavaScript
"use strict";
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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BatchJobImpl = exports.DefaultBatchJobOpts = void 0;
////////////////////////////////////
// Batch Imports
////////////////////////////////////
const index_1 = require("../model/index");
const errors_1 = require("../errors");
const base_1 = require("../errors/base");
const retry_1 = require("../utils/retry");
exports.DefaultBatchJobOpts = {
pollInterval: 2000,
maxRetry: 3,
};
class BatchJobImpl {
constructor(request,
// TODO(sabrina):these could be better typed
requester, opts = {}) {
this.requester = requester;
this.imports = [];
this.failedImports = [];
this.errors = [];
this._createdCallbacks = [];
this._processingCallbacks = [];
this._doneCallbacks = [];
this._abortCallbacks = [];
this._errorCallbacks = [];
this._executionStatus = 'not-started';
this._nextPollDelay = 0;
this._numRetries = 0;
this.request = request;
this.options = Object.assign({}, exports.DefaultBatchJobOpts, opts);
}
restart(jobId) {
if (!jobId && !this.getId()) {
throw new Error('unknown job id, no jobId in this job object nor jobId in restart request');
}
if (jobId && this.getId() && jobId != this.getId()) {
throw new Error('the current job already has an different id, can not mutate jobId');
}
// no need to poll if already completed
if (this._executionStatus == 'completed') {
return;
}
this.stopPolling();
jobId = jobId || this.getId();
if (jobId) {
this.setMetadata({ id: jobId, status: index_1.JobStatus.Processing, created: '' });
}
this._executionStatus = 'pending';
this.startPolling();
}
getId() {
var _a;
return (_a = this.metadata) === null || _a === void 0 ? void 0 : _a.id;
}
getStatus() {
var _a;
return (_a = this.metadata) === null || _a === void 0 ? void 0 : _a.status;
}
getImports() {
return this.imports;
}
getFailedImports() {
return this.failedImports;
}
add(...requests) {
if (this._executionStatus != 'not-started') {
throw new Error('Job already executed, can not add more requests.');
}
// TODO(sabrina): throw if max number of requests reached
this.request.requests.push(...requests);
return this;
}
execute() {
// don't execute again if the job had already been created.
if (this.getId()) {
return;
}
// don't execute again if the job is still executing
if (this._executionStatus === 'pending' || this._executionStatus === 'completed') {
return;
}
(0, retry_1.withRetry)(() => this.requester.requestCreateJob({ body: this.request }), this.handleError.bind(this), this.options.maxRetry)
.then(response => {
var _a;
// Successful response should always have ID.
// If not, something wrong had happened in calling server API
if (!((_a = response.job) === null || _a === void 0 ? void 0 : _a.id)) {
throw new errors_1.FSUnknownError(`Unable to get job ID after creating a job, job metadata received: ${response}`);
}
this._executionStatus = 'pending';
this.setMetadata(response.job);
this.startPolling();
this.handleJobCreated();
}).catch(_ => {
this.handleAbort();
});
}
on(type, callback) {
switch (type) {
case 'created':
if (this._executionStatus !== 'not-started') {
// job had already been started, invoke immediately.
callback(this);
}
this._createdCallbacks.push(callback);
break;
case 'processing':
this._processingCallbacks.push(callback);
break;
case 'done':
// if the we've already got the imports and failures, immediately invoke it with current values.
if (this._executionStatus === 'completed') {
callback(this.imports, this.failedImports);
}
this._doneCallbacks.push(callback);
break;
case 'error':
// if there are already errors encountered, immediately invoke with current values.
if (this.errors.length) {
callback(this.errors);
}
this._errorCallbacks.push(callback);
break;
case 'abort':
// if job had already been aborted, immediately invoke with current errors.
if (this._executionStatus === 'aborted') {
callback(this.errors);
}
this._abortCallbacks.push(callback);
break;
default:
throw new Error('Unknown event type.');
}
return this;
}
setMetadata(job) {
if (this.getId() && this.getId() != (job === null || job === void 0 ? void 0 : job.id)) {
throw new Error(`can not set existing job metadata ${this.getId()} to a different job ${job === null || job === void 0 ? void 0 : job.id}`);
}
this.metadata = job;
}
startPolling() {
const id = this.getId();
if (!id) {
throw new Error('Current job ID is unknown, make sure the job had been executed');
}
this._interval = setInterval(() => __awaiter(this, void 0, void 0, function* () {
var _a;
// if last poll is not resolved before next pull, ignore this interval.
if (this._statusPromise) {
return;
}
// start a new request with any rate limited retry delay, and set the new promise
this._statusPromise = (0, retry_1.withDelay)(() => this.requester.requestJobStatus(id), this._nextPollDelay);
try {
const statusRsp = yield this._statusPromise;
const metadata = statusRsp.job;
if (!metadata || !metadata.id) {
throw new errors_1.FSUnknownError('Invalid job metadata received: ' + statusRsp);
}
this.setMetadata(metadata);
// TODO(sabrina): maybe dispatch this as events rather than calling handlers here
switch (metadata.status) {
case index_1.JobStatus.Processing:
this.handleProcessing();
break;
case index_1.JobStatus.Completed:
this.handleCompleted(id);
break;
case index_1.JobStatus.Failed:
this.handleCompletedWithFailure(id);
break;
default:
throw new Error('Unknown job stats received: ' + ((_a = this.metadata) === null || _a === void 0 ? void 0 : _a.status));
}
}
catch (e) {
this.handleError(e);
this._numRetries++;
if (this._numRetries >= this.options.maxRetry || !(0, errors_1.isFSError)(e) || !e.canRetry()) {
this.handleAbort();
}
else {
this._nextPollDelay = e.getRetryAfter() + this._nextPollDelay * 2;
}
}
finally {
// clean up the current promise
delete this._statusPromise;
}
}), this.options.pollInterval);
}
stopPolling() {
clearInterval(this._interval);
}
handleJobCreated() {
for (const cb of this._createdCallbacks) {
cb(this);
}
}
handleProcessing() {
for (const cb of this._processingCallbacks) {
cb(this);
}
}
handleCompleted(id) {
return __awaiter(this, void 0, void 0, function* () {
this.stopPolling();
this._executionStatus = 'completed';
const imports = yield this.requestImportsWithPaging(id);
this.imports.push(...imports);
for (const cb of this._doneCallbacks) {
cb(this.imports, this.failedImports);
}
});
}
handleCompletedWithFailure(id) {
return __awaiter(this, void 0, void 0, function* () {
this.stopPolling();
this._executionStatus = 'completed';
const errors = yield this.requestImportErrorsWithPaging(id);
this.failedImports.push(...errors);
for (const cb of this._doneCallbacks) {
cb(this.imports, this.failedImports);
}
});
}
handleError(err) {
const error = (0, base_1.toError)(err);
if (!error)
return;
// TODO(sabrina): check for FSError
this.errors.push(error);
for (const cb of this._errorCallbacks) {
cb(error);
}
}
handleAbort() {
this._executionStatus = 'aborted';
this.stopPolling();
for (const cb of this._abortCallbacks) {
cb(this.errors);
}
}
requestImportsWithPaging(id) {
return __awaiter(this, void 0, void 0, function* () {
const imports = yield this.withPageToken((next) => this.requester.requestImports(id, next));
return imports.flatMap(i => i.results || []);
});
}
requestImportErrorsWithPaging(id) {
return __awaiter(this, void 0, void 0, function* () {
const errors = yield this.withPageToken((next) => this.requester.requestImportErrors(id, next));
return errors.flatMap(e => e.results || []);
});
}
withPageToken(method) {
return __awaiter(this, void 0, void 0, function* () {
const results = [];
let hasNextPage = true;
let pageToken;
while (hasNextPage) {
const res = yield (0, retry_1.withRetry)(() => method(pageToken), (err) => this.handleError(err));
results.push(res);
hasNextPage = !!res.next_page_token && res.next_page_token !== pageToken;
pageToken = res.next_page_token;
}
return results;
});
}
}
exports.BatchJobImpl = BatchJobImpl;
//# sourceMappingURL=index.js.map