UNPKG

@fullstory/server-api-client

Version:

The official FullStory server API client SDK for NodeJS.

284 lines 11.4 kB
"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