Skip to content

Adding socket mode support in Teams SDK#582

Draft
RajuRoopani wants to merge 1 commit into
microsoft:mainfrom
RajuRoopani:user/rroopani/socketmode-sdk-changes
Draft

Adding socket mode support in Teams SDK#582
RajuRoopani wants to merge 1 commit into
microsoft:mainfrom
RajuRoopani:user/rroopani/socketmode-sdk-changes

Conversation

@RajuRoopani
Copy link
Copy Markdown

No description provided.

Copilot AI review requested due to automatic review settings May 18, 2026 17:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds Socket Mode support to the Teams SDK by introducing a SocketModeApp that negotiates Azure SignalR connections, routes envelopes into App.onActivity, and provides lifecycle/diagnostic events.

Changes:

  • Introduces SocketModeApp, SignalR client wrapper, negotiate helper, envelope guard, synthesized token, and backoff utilities.
  • Exposes the Socket Mode surface via packages/apps/src/socket-mode/index.ts and re-exports it from packages/apps/src/index.ts.
  • Adds Jest unit tests and a draft proposal document; adds @microsoft/signalr dependency.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/apps/src/socket-mode/synthesize-token.ts Adds helper to synthesize an IToken for socket-delivered activities.
packages/apps/src/socket-mode/socket-mode-client.ts Adds a thin wrapper around SignalR HubConnection for production + test seams.
packages/apps/src/socket-mode/socket-mode-app.ts Adds SocketModeApp lifecycle, negotiate/connect, envelope routing, dedupe, and events.
packages/apps/src/socket-mode/socket-mode-app.spec.ts Adds unit tests for socket mode lifecycle, routing, dedupe, and multi-connection behavior.
packages/apps/src/socket-mode/negotiate.ts Implements negotiate call and a typed 503-unavailable error.
packages/apps/src/socket-mode/index.ts Re-exports socket-mode public API.
packages/apps/src/socket-mode/envelope.ts Defines envelope shape + type guard.
packages/apps/src/socket-mode/backoff.ts Adds jittered exponential backoff + sleep helper.
packages/apps/src/index.ts Re-exports socket-mode module from package root.
packages/apps/package.json Adds @microsoft/signalr dependency.
docs/proposals/socket-mode.md Adds draft design proposal for Socket Mode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if (ctx.previousRetryCount === 0) return 0;
if (ctx.previousRetryCount === 1) return 2_000;
if (ctx.previousRetryCount === 2) return 10_000;
return 30_000;
Comment on lines +63 to +84
async connect(url: string, accessToken: string): Promise<void> {
const conn = new HubConnectionBuilder()
.withUrl(url, { accessTokenFactory: () => accessToken })
.withAutomaticReconnect(DEFAULT_RETRY_POLICY)
.build();

if (this.activityHandler) {
conn.on('activity', this.activityHandler);
}
if (this.closeHandler) {
conn.onclose(this.closeHandler);
}
if (this.reconnectingHandler) {
conn.onreconnecting(this.reconnectingHandler);
}
if (this.reconnectedHandler) {
conn.onreconnected(this.reconnectedHandler);
}

await conn.start();
this.connection = conn;
}
Comment on lines +187 to +222
// Boot the App: plugin onInit, plugin onStart, HTTP server bind.
// The HTTP server keeps handling invokes / OAuth callbacks / tabs / remote functions.
await this.app.start(port);

// Create slots up front so handlers can reference them.
this.slots = [];
this.sessionIds = new Array<string | undefined>(this.opts.connections);
for (let i = 0; i < this.opts.connections; i++) {
const slot: Slot = {
index: i,
client: this.clientFactory(i),
expiry: 0,
backoff: new Backoff(this.opts.backoff),
};
this.wireClient(slot);
this.slots.push(slot);
}

// Open all slots in parallel. We don't rethrow per-slot failures here — each slot's
// own reconnect/backoff loop handles transient errors. If `fallbackOn503` is false
// and ALL slots get a 503, surface that to the caller.
const results = await Promise.allSettled(
this.slots.map((s) => this.negotiateAndConnect(s)),
);

if (!this.opts.fallbackOn503) {
const allUnavailable =
results.length > 0 &&
results.every(
(r) =>
r.status === 'rejected' && r.reason instanceof NegotiateUnavailableError,
);
if (allUnavailable) {
const firstReason = (results[0] as PromiseRejectedResult).reason as NegotiateUnavailableError;
throw firstReason;
}
Comment on lines +324 to +356
private async renegotiate(slot: Slot): Promise<void> {
if (this.stopping) return;
try {
const result = await this.negotiateOnce();
// Make-before-break: bring up the new connection, then tear down the old one.
const oldClient = slot.client;
const newClient = this.clientFactory(slot.index);
slot.client = newClient;
this.wireClient(slot);
await newClient.connect(result.url, result.accessToken);
slot.sessionId = result.sessionId;
slot.expiry = Date.now() + result.expiresIn * 1000;
this.sessionIds[slot.index] = result.sessionId;
if (slot.index === 0) this.sessionId = result.sessionId;
this.emit('renegotiated', {
sessionId: result.sessionId,
expiresIn: result.expiresIn,
slot: slot.index,
});
this.scheduleRenegotiate(slot, result.expiresIn);
try {
await oldClient.disconnect();
} catch (err) {
this.log.debug(`old socket disconnect failed (slot ${slot.index})`, err);
}
} catch (err: unknown) {
this.log.warn(
`renegotiate failed (slot ${slot.index}); will retry via close handler`,
(err as Error)?.message ?? err,
);
// The existing connection will drop on token expiry; handleClose triggers full reconnect.
}
}
Comment on lines +141 to +150
this.opts = {
route: options.route ?? { kind: 'global' },
connections,
renegotiateAt: options.renegotiateAt ?? DEFAULT_RENEGOTIATE_AT,
backoff: options.backoff,
// Auto-enable dedup when running multiple sockets — APX fan-out delivers each
// envelope to every session, so duplicates are guaranteed.
dedupe: options.dedupe ?? (connections > 1),
fallbackOn503: options.fallbackOn503 ?? true,
};
Comment on lines +38 to +42
| Negotiate endpoint | `POST /v3/websockets/connect` (also `{cloud}/...`, `{cloud}/{tenantId}/...`) | [`WebSocketConnectController.cs`](file:///c:/Work6/Git/async_messaging_botapiservice/BotFrontEnd.Library/Controllers/WebSocketConnectController.cs) |
| Auth | `Authorization: Bearer {BF JWT}` — same MSA token used for other `/v3/...` calls | [`WebSocketConnectService.cs`](file:///c:/Work6/Git/async_messaging_botapiservice/Library/Services/WebSocketConnectService.cs) |
| Response | `{ url, accessToken, sessionId, expiresIn }` (`expiresIn` in seconds) | `WebSocketConnectController.cs` |
| Transport | Azure SignalR Service Default protocol. Reference JS client: `@microsoft/signalr` |
| Hub method | `"activity"` (server → client) | [`SocketModeDispatcher.cs:40`](file:///c:/Work6/Git/async_messaging_botapiservice/Library/Services/SocketModeDispatcher.cs#L40) |
Comment on lines +98 to +114
function envelopeOfType(type: string, name?: string, id = `env-${Math.random()}`): ISocketActivityEnvelope {
return {
type: 'activity',
envelopeId: id,
cv: 'cv.x',
payload: {
type,
...(name ? { name } : {}),
id: `act-${id}`,
serviceUrl: 'https://smba.trafficmanager.net/teams',
channelId: 'msteams',
from: { id: 'user-1' },
recipient: { id: 'bot-1' },
conversation: { id: 'conv-1', tenantId: 't1' },
},
};
}
Comment on lines +311 to +322
private scheduleRenegotiate(slot: Slot, expiresInSec: number): void {
if (slot.renegotiateTimer) clearTimeout(slot.renegotiateTimer);
const delayMs = Math.max(
1000,
Math.floor(expiresInSec * 1000 * this.opts.renegotiateAt),
);
slot.renegotiateTimer = setTimeout(() => {
void this.renegotiate(slot);
}, delayMs);
// Don't hold the event loop open just for the timer.
(slot.renegotiateTimer as unknown as { unref?: () => void }).unref?.();
}
@heyitsaamir heyitsaamir marked this pull request as draft May 18, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants