@microsoft/teams.apps
Version:
<p> <a href="https://www.npmjs.com/package/@microsoft/teams.apps" target="_blank"> <img src="https://img.shields.io/npm/v/@microsoft/teams.apps/latest" /> </a> <a href="https://www.npmjs.com/package/@microsoft/teams.apps?activeTab=code
207 lines • 16 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SignalRSocketConnection = void 0;
const signalr_1 = require("@microsoft/signalr");
const negotiate_1 = require("./negotiate");
const redact_1 = require("./redact");
/**
* A `@microsoft/signalr`-backed {@link ISocketConnection}. Negotiates through
* Teams backend service, opens the Azure SignalR socket, and does not resolve {@link start} until
* the Teams backend service `SocketReady` readiness frame arrives (or the readiness timeout
* elapses).
*
* Returning a value from the `Activity` handler is what drives SignalR client
* results: Teams backend service awaits the returned reply frame as the invocation result.
*/
class SignalRSocketConnection {
context;
handlers;
log;
/** Access-token lifetime from the negotiate response; drives proactive refresh. */
expiresInSeconds;
/** Local token-expiry deadline; see {@link ISocketConnection.expiresAtMs}. */
expiresAtMs;
/** This socket's server identity; see {@link ISocketConnection.connectionId}. */
connectionId;
connection;
stopped = false;
/**
* Whether the readiness gate has been settled (resolved or rejected). Guards
* the `SocketReady` handler so a duplicate or late readiness frame — including
* one that arrives after {@link stop} — cannot re-fire `onReady` or otherwise
* act on an already-settled connection.
*/
settled = false;
constructor(context, handlers, log) {
this.context = context;
this.handlers = handlers;
this.log = log;
}
/** Whether SignalR is connected and stopping has not begun. */
get isOpen() {
return !this.stopped && this.connection?.state === signalr_1.HubConnectionState.Connected;
}
async start(signal) {
this.stopped = false;
this.settled = false;
this.throwIfAborted(signal);
const neg = await (0, negotiate_1.negotiate)({
negotiateUrl: this.context.negotiateUrl,
client: this.context.client,
getBotToken: this.context.getBotToken,
getConnectionIdToReplace: this.context.getConnectionIdToReplace,
log: this.log,
});
this.expiresInSeconds = neg.expiresIn || undefined;
this.expiresAtMs = neg.expiresIn > 0 ? Date.now() + neg.expiresIn * 1000 : undefined;
this.throwIfAborted(signal);
// APX tokens are single-socket: disable transport fallback and leave automatic
// and stateful reconnect off. The supervisor negotiates afresh for every retry.
const connection = new signalr_1.HubConnectionBuilder()
.withUrl(neg.url, {
accessTokenFactory: () => neg.accessToken,
transport: signalr_1.HttpTransportType.WebSockets,
})
.configureLogging(this.signalRLogger([neg.accessToken]))
.build();
this.connection = connection;
// A readiness gate: the socket is not usable until Teams backend service
// confirms the connection is registered in the bot's group via `SocketReady`.
let markReady;
let failReady;
const ready = new Promise((resolve, reject) => {
// Settle exactly once: the first outcome (ready, close, timeout, or abort)
// wins, and every later signal — e.g. a duplicate `SocketReady` or a close
// after readiness — becomes a no-op instead of re-firing observers.
markReady = () => {
if (this.settled)
return;
this.settled = true;
resolve();
};
failReady = (err) => {
if (this.settled)
return;
this.settled = true;
reject(err);
};
});
connection.on('Activity', (envelope) => this.handlers.onActivity(envelope));
connection.on('SocketReady', (frame) => {
// Ignore a late/duplicate readiness frame once the gate has settled (e.g.
// after stop or a prior SocketReady) so `onReady` fires at most once.
if (this.settled || this.stopped)
return;
this.connectionId = frame?.connectionId;
this.log?.debug('socket-mode: SocketReady received; readiness satisfied');
// Complete the internal readiness transition FIRST so a throwing observer
// can't wedge the connection into a permanent not-ready state (readiness
// would otherwise time out); only then notify the server, behind an error
// boundary.
markReady();
try {
this.handlers.onReady(frame ?? {});
}
catch (err) {
this.log?.warn('socket-mode: onReady observer threw; connection is still ready', err);
}
});
connection.onclose((error) => {
if (!this.stopped) {
this.log?.warn('socket-mode: socket closed', error);
}
// A close before readiness rejects the gate; after readiness this is a
// no-op. Either way the connection is terminal — report it once so the
// supervisor can decide whether to negotiate a fresh connection.
failReady(error ?? new Error('socket closed before readiness'));
this.handlers.onClosed(error ?? undefined);
});
let connected = false;
// Stop immediately on abort because the supervisor cannot see this connection
// until it reaches readiness.
const onAbort = () => {
if (connected) {
failReady(new Error('Socket Mode connect aborted'));
}
void this.stop().catch((err) => {
this.log?.warn('socket-mode: failed to stop an aborted connection', err);
});
};
signal?.addEventListener('abort', onAbort, { once: true });
try {
await connection.start();
connected = true;
// An abort that landed during start won't have had a connection to stop; now
// that one exists, honor it explicitly.
if (signal?.aborted) {
await this.stop();
throw new Error('Socket Mode connect aborted');
}
this.log?.debug('socket-mode: socket connected; awaiting SocketReady');
const timeoutMs = this.context.readinessTimeoutMs;
const timer = setTimeout(() => {
this.log?.warn(`socket-mode: readiness timed out after ${timeoutMs}ms (no SocketReady frame)`);
failReady(new Error(`Socket Mode readiness timed out after ${timeoutMs}ms (no SocketReady frame).`));
}, timeoutMs);
try {
await ready;
}
catch (err) {
await this.stop();
throw err;
}
finally {
clearTimeout(timer);
}
}
finally {
signal?.removeEventListener('abort', onAbort);
}
}
async stop() {
this.stopped = true;
const connection = this.connection;
this.connection = undefined;
if (connection) {
await connection.stop();
}
}
throwIfAborted(signal) {
if (signal?.aborted) {
throw new Error('Socket Mode connect aborted');
}
}
/**
* Bridge the SignalR client's internal diagnostics into the app's
* {@link ILogger} so they surface through the SDK's logging pipeline (and
* respect its configured level) instead of SignalR's default console logger.
*/
signalRLogger(secrets = []) {
const log = this.log;
return {
log: (level, message) => {
const line = (0, redact_1.redactSocketModeSecrets)(`socket-mode signalr: ${message}`, secrets);
switch (level) {
case signalr_1.LogLevel.Critical:
case signalr_1.LogLevel.Error:
log?.error(line);
break;
case signalr_1.LogLevel.Warning:
log?.warn(line);
break;
case signalr_1.LogLevel.Information:
log?.info(line);
break;
case signalr_1.LogLevel.Debug:
case signalr_1.LogLevel.Trace:
log?.debug(line);
break;
default:
break;
}
},
};
}
}
exports.SignalRSocketConnection = SignalRSocketConnection;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic29ja2V0LWNvbm5lY3Rpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvc29ja2V0LW1vZGUvc29ja2V0LWNvbm5lY3Rpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsZ0RBQXFKO0FBR3JKLDJDQUF3QztBQUN4QyxxQ0FBbUQ7QUFTbkQ7Ozs7Ozs7O0dBUUc7QUFDSCxNQUFhLHVCQUF1QjtJQXFCZjtJQUNBO0lBQ0E7SUF0Qm5CLG1GQUFtRjtJQUNuRixnQkFBZ0IsQ0FBVTtJQUUxQiw4RUFBOEU7SUFDOUUsV0FBVyxDQUFVO0lBRXJCLGlGQUFpRjtJQUNqRixZQUFZLENBQVU7SUFFZCxVQUFVLENBQWlCO0lBQzNCLE9BQU8sR0FBRyxLQUFLLENBQUM7SUFDeEI7Ozs7O09BS0c7SUFDSyxPQUFPLEdBQUcsS0FBSyxDQUFDO0lBRXhCLFlBQ21CLE9BQWdDLEVBQ2hDLFFBQWtDLEVBQ2xDLEdBQWE7UUFGYixZQUFPLEdBQVAsT0FBTyxDQUF5QjtRQUNoQyxhQUFRLEdBQVIsUUFBUSxDQUEwQjtRQUNsQyxRQUFHLEdBQUgsR0FBRyxDQUFVO0lBQzdCLENBQUM7SUFFSiwrREFBK0Q7SUFDL0QsSUFBSSxNQUFNO1FBQ1IsT0FBTyxDQUFDLElBQUksQ0FBQyxPQUFPLElBQUksSUFBSSxDQUFDLFVBQVUsRUFBRSxLQUFLLEtBQUssNEJBQWtCLENBQUMsU0FBUyxDQUFDO0lBQ2xGLENBQUM7SUFFRCxLQUFLLENBQUMsS0FBSyxDQUFDLE1BQW9CO1FBQzlCLElBQUksQ0FBQyxPQUFPLEdBQUcsS0FBSyxDQUFDO1FBQ3JCLElBQUksQ0FBQyxPQUFPLEdBQUcsS0FBSyxDQUFDO1FBQ3JCLElBQUksQ0FBQyxjQUFjLENBQUMsTUFBTSxDQUFDLENBQUM7UUFFNUIsTUFBTSxHQUFHLEdBQUcsTUFBTSxJQUFBLHFCQUFTLEVBQUM7WUFDMUIsWUFBWSxFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMsWUFBWTtZQUN2QyxNQUFNLEVB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