UNPKG

@salesforce/apex-node

Version:

Salesforce JS library for Apex

266 lines 11.3 kB
"use strict"; /* * Copyright (c) 2020, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StreamingClient = exports.Deferred = void 0; const faye_1 = require("faye"); const core_1 = require("@salesforce/core"); const types_1 = require("./types"); const i18n_1 = require("../i18n"); const utils_1 = require("../utils"); const TEST_RESULT_CHANNEL = '/systemTopic/TestResult'; const DEFAULT_STREAMING_TIMEOUT_SEC = 14400; class Deferred { promise; resolve; constructor() { this.promise = new Promise((resolve) => (this.resolve = resolve)); } } exports.Deferred = Deferred; class StreamingClient { // This should be a Client from Faye, but I'm not sure how to get around the type // that is exported from jsforce. // eslint-disable-next-line @typescript-eslint/no-explicit-any client; conn; progress; subscribedTestRunId; subscribedTestRunIdDeferred = new Deferred(); get subscribedTestRunIdPromise() { return this.subscribedTestRunIdDeferred.promise; } removeTrailingSlashURL(instanceUrl) { return instanceUrl ? instanceUrl.replace(/\/+$/, '') : ''; } getStreamURL(instanceUrl) { const urlElements = [ this.removeTrailingSlashURL(instanceUrl), 'cometd', this.conn.getApiVersion() ]; return urlElements.join('/'); } constructor(connection, progress) { this.conn = connection; this.progress = progress; const streamUrl = this.getStreamURL(this.conn.instanceUrl); this.client = new faye_1.Client(streamUrl, { timeout: DEFAULT_STREAMING_TIMEOUT_SEC }); this.client.on('transport:up', () => { this.progress?.report({ type: 'StreamingClientProgress', value: 'streamingTransportUp', message: i18n_1.nls.localize('streamingTransportUp') }); }); this.client.on('transport:down', () => { this.progress?.report({ type: 'StreamingClientProgress', value: 'streamingTransportDown', message: i18n_1.nls.localize('streamingTransportDown') }); }); this.client.addExtension({ incoming: async (message, callback) => { if (message?.error) { // throw errors on handshake errors if (message.channel === '/meta/handshake') { this.disconnect(); throw new Error(i18n_1.nls.localize('streamingHandshakeFail', message.error)); } // refresh auth on 401 errors if (message.error === "401::Authentication invalid" /* StreamingErrors.ERROR_AUTH_INVALID */) { await this.init(); callback(message); return; } // call faye callback on handshake advice if (message.advice && message.advice.reconnect === 'handshake') { callback(message); return; } // call faye callback on 403 unknown client errors if (message.error === "403::Unknown client" /* StreamingErrors.ERROR_UNKNOWN_CLIENT_ID */) { callback(message); return; } // default: disconnect and throw error this.disconnect(); throw new Error(message.error); } callback(message); } }); } // NOTE: There's an intermittent auth issue with Streaming API that requires the connection to be refreshed // The builtin org.refreshAuth() util only refreshes the connection associated with the instance of the org you provide, not all connections associated with that username's orgs async init() { await (0, utils_1.refreshAuth)(this.conn); const accessToken = this.conn.getConnectionOptions().accessToken; if (accessToken) { this.client.setHeader('Authorization', `OAuth ${accessToken}`); } else { throw new Error(i18n_1.nls.localize('noAccessTokenFound')); } } handshake() { return new Promise((resolve) => { this.client.handshake(() => { resolve(); }); }); } disconnect() { this.client.disconnect(); this.hasDisconnected = true; } hasDisconnected = false; async subscribe(action, testRunId, timeout) { return new Promise((subscriptionResolve, subscriptionReject) => { let intervalId; // start timeout const timeoutId = setTimeout(() => { this.disconnect(); clearInterval(intervalId); subscriptionResolve({ testRunId }); }, timeout?.milliseconds ?? DEFAULT_STREAMING_TIMEOUT_SEC * 1000); try { this.client.subscribe(TEST_RESULT_CHANNEL, async (message) => { const result = await this.handler(message); if (result) { this.disconnect(); clearInterval(intervalId); clearTimeout(timeoutId); subscriptionResolve({ runId: this.subscribedTestRunId, queueItem: result }); } }); if (action) { action() .then((id) => { this.subscribedTestRunId = id; this.subscribedTestRunIdDeferred.resolve(id); if (!this.hasDisconnected) { intervalId = setInterval(async () => { const result = await this.getCompletedTestRun(id); if (result) { this.disconnect(); clearInterval(intervalId); clearTimeout(timeoutId); subscriptionResolve({ runId: this.subscribedTestRunId, queueItem: result }); } }, types_1.RetrieveResultsInterval); } }) .catch((e) => { this.disconnect(); clearInterval(intervalId); clearTimeout(timeoutId); subscriptionReject(e); }); } else { this.subscribedTestRunId = testRunId; this.subscribedTestRunIdDeferred.resolve(testRunId); if (!this.hasDisconnected) { intervalId = setInterval(async () => { const result = await this.getCompletedTestRun(testRunId); if (result) { this.disconnect(); clearInterval(intervalId); clearTimeout(timeoutId); subscriptionResolve({ runId: this.subscribedTestRunId, queueItem: result }); } }, types_1.RetrieveResultsInterval); } } } catch (e) { this.disconnect(); clearTimeout(timeoutId); clearInterval(intervalId); subscriptionReject(e); } }); } isValidTestRunID(testRunId, subscribedId) { if (testRunId.length !== 15 && testRunId.length !== 18) { return false; } const testRunId15char = testRunId.substring(0, 14); if (subscribedId) { const subscribedTestRunId15char = subscribedId.substring(0, 14); return subscribedTestRunId15char === testRunId15char; } return true; } async handler(message, runId) { const testRunId = runId || message.sobject.Id; if (!this.isValidTestRunID(testRunId, this.subscribedTestRunId)) { return null; } const result = await this.getCompletedTestRun(testRunId); if (result) { return result; } this.progress?.report({ type: 'StreamingClientProgress', value: 'streamingProcessingTestRun', message: i18n_1.nls.localize('streamingProcessingTestRun', testRunId), testRunId }); return null; } async getCompletedTestRun(testRunId) { const queryApexTestQueueItem = `SELECT Id, Status, ApexClassId, TestRunResultId FROM ApexTestQueueItem WHERE ParentJobId = '${testRunId}'`; const result = await this.conn.tooling.query(queryApexTestQueueItem, { autoFetch: true }); if (result.records.length === 0) { throw new Error(i18n_1.nls.localize('noTestQueueResults', testRunId)); } this.progress?.report({ type: 'TestQueueProgress', value: result }); if (result.records.some((item) => item.Status === "Queued" /* ApexTestQueueItemStatus.Queued */ || item.Status === "Holding" /* ApexTestQueueItemStatus.Holding */ || item.Status === "Preparing" /* ApexTestQueueItemStatus.Preparing */ || item.Status === "Processing" /* ApexTestQueueItemStatus.Processing */)) { return null; } return result; } } exports.StreamingClient = StreamingClient; __decorate([ (0, utils_1.elapsedTime)() ], StreamingClient.prototype, "subscribe", null); __decorate([ (0, utils_1.elapsedTime)() ], StreamingClient.prototype, "handler", null); __decorate([ (0, utils_1.elapsedTime)('elapsedTime', core_1.LoggerLevel.TRACE) ], StreamingClient.prototype, "getCompletedTestRun", null); //# sourceMappingURL=streamingClient.js.map