@salesforce/apex-node
Version:
Salesforce JS library for Apex
266 lines • 11.3 kB
JavaScript
"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