refactor: flatten Microsoft skills from nested to flat directory structure
Rewrote sync_microsoft_skills.py (v4) to use each SKILL.md's frontmatter 'name' field as the flat directory name under skills/, replacing the nested skills/official/microsoft/<lang>/<category>/<service>/ hierarchy. This fixes CI failures caused by the indexing, validation, and catalog scripts expecting skills/<id>/SKILL.md (depth 1). Changes: - Rewrite scripts/sync_microsoft_skills.py for flat output with collision detection - Update scripts/tests/inspect_microsoft_repo.py for flat name mapping - Update scripts/tests/test_comprehensive_coverage.py for name uniqueness checks - Delete skills/official/ nested directory - Add 129 Microsoft skills as flat directories (e.g. skills/azure-mgmt-botservice-dotnet/) - Move attribution files to docs/ (LICENSE-MICROSOFT, microsoft-skills-attribution.json) - Rebuild skills_index.json, CATALOG.md, README.md (845 total skills)
This commit is contained in:
310
skills/azure-web-pubsub-ts/SKILL.md
Normal file
310
skills/azure-web-pubsub-ts/SKILL.md
Normal file
@@ -0,0 +1,310 @@
|
||||
---
|
||||
name: azure-web-pubsub-ts
|
||||
description: Build real-time messaging applications using Azure Web PubSub SDKs for JavaScript (@azure/web-pubsub, @azure/web-pubsub-client). Use when implementing WebSocket-based real-time features, pub/sub messaging, group chat, or live notifications.
|
||||
package: @azure/web-pubsub, @azure/web-pubsub-client
|
||||
---
|
||||
|
||||
# Azure Web PubSub SDKs for TypeScript
|
||||
|
||||
Real-time messaging with WebSocket connections and pub/sub patterns.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Server-side management
|
||||
npm install @azure/web-pubsub @azure/identity
|
||||
|
||||
# Client-side real-time messaging
|
||||
npm install @azure/web-pubsub-client
|
||||
|
||||
# Express middleware for event handlers
|
||||
npm install @azure/web-pubsub-express
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
WEBPUBSUB_CONNECTION_STRING=Endpoint=https://<resource>.webpubsub.azure.com;AccessKey=<key>;Version=1.0;
|
||||
WEBPUBSUB_ENDPOINT=https://<resource>.webpubsub.azure.com
|
||||
```
|
||||
|
||||
## Server-Side: WebPubSubServiceClient
|
||||
|
||||
### Authentication
|
||||
|
||||
```typescript
|
||||
import { WebPubSubServiceClient, AzureKeyCredential } from "@azure/web-pubsub";
|
||||
import { DefaultAzureCredential } from "@azure/identity";
|
||||
|
||||
// Connection string
|
||||
const client = new WebPubSubServiceClient(
|
||||
process.env.WEBPUBSUB_CONNECTION_STRING!,
|
||||
"chat" // hub name
|
||||
);
|
||||
|
||||
// DefaultAzureCredential (recommended)
|
||||
const client2 = new WebPubSubServiceClient(
|
||||
process.env.WEBPUBSUB_ENDPOINT!,
|
||||
new DefaultAzureCredential(),
|
||||
"chat"
|
||||
);
|
||||
|
||||
// AzureKeyCredential
|
||||
const client3 = new WebPubSubServiceClient(
|
||||
process.env.WEBPUBSUB_ENDPOINT!,
|
||||
new AzureKeyCredential("<access-key>"),
|
||||
"chat"
|
||||
);
|
||||
```
|
||||
|
||||
### Generate Client Access Token
|
||||
|
||||
```typescript
|
||||
// Basic token
|
||||
const token = await client.getClientAccessToken();
|
||||
console.log(token.url); // wss://...?access_token=...
|
||||
|
||||
// Token with user ID
|
||||
const userToken = await client.getClientAccessToken({
|
||||
userId: "user123",
|
||||
});
|
||||
|
||||
// Token with permissions
|
||||
const permToken = await client.getClientAccessToken({
|
||||
userId: "user123",
|
||||
roles: [
|
||||
"webpubsub.joinLeaveGroup",
|
||||
"webpubsub.sendToGroup",
|
||||
"webpubsub.sendToGroup.chat-room", // specific group
|
||||
],
|
||||
groups: ["chat-room"], // auto-join on connect
|
||||
expirationTimeInMinutes: 60,
|
||||
});
|
||||
```
|
||||
|
||||
### Send Messages
|
||||
|
||||
```typescript
|
||||
// Broadcast to all connections in hub
|
||||
await client.sendToAll({ message: "Hello everyone!" });
|
||||
await client.sendToAll("Plain text", { contentType: "text/plain" });
|
||||
|
||||
// Send to specific user (all their connections)
|
||||
await client.sendToUser("user123", { message: "Hello!" });
|
||||
|
||||
// Send to specific connection
|
||||
await client.sendToConnection("connectionId", { data: "Direct message" });
|
||||
|
||||
// Send with filter (OData syntax)
|
||||
await client.sendToAll({ message: "Filtered" }, {
|
||||
filter: "userId ne 'admin'",
|
||||
});
|
||||
```
|
||||
|
||||
### Group Management
|
||||
|
||||
```typescript
|
||||
const group = client.group("chat-room");
|
||||
|
||||
// Add user/connection to group
|
||||
await group.addUser("user123");
|
||||
await group.addConnection("connectionId");
|
||||
|
||||
// Remove from group
|
||||
await group.removeUser("user123");
|
||||
|
||||
// Send to group
|
||||
await group.sendToAll({ message: "Group message" });
|
||||
|
||||
// Close all connections in group
|
||||
await group.closeAllConnections({ reason: "Maintenance" });
|
||||
```
|
||||
|
||||
### Connection Management
|
||||
|
||||
```typescript
|
||||
// Check existence
|
||||
const userExists = await client.userExists("user123");
|
||||
const connExists = await client.connectionExists("connectionId");
|
||||
|
||||
// Close connections
|
||||
await client.closeConnection("connectionId", { reason: "Kicked" });
|
||||
await client.closeUserConnections("user123");
|
||||
await client.closeAllConnections();
|
||||
|
||||
// Permissions
|
||||
await client.grantPermission("connectionId", "sendToGroup", { targetName: "chat" });
|
||||
await client.revokePermission("connectionId", "sendToGroup", { targetName: "chat" });
|
||||
```
|
||||
|
||||
## Client-Side: WebPubSubClient
|
||||
|
||||
### Connect
|
||||
|
||||
```typescript
|
||||
import { WebPubSubClient } from "@azure/web-pubsub-client";
|
||||
|
||||
// Direct URL
|
||||
const client = new WebPubSubClient("<client-access-url>");
|
||||
|
||||
// Dynamic URL from negotiate endpoint
|
||||
const client2 = new WebPubSubClient({
|
||||
getClientAccessUrl: async () => {
|
||||
const response = await fetch("/negotiate");
|
||||
const { url } = await response.json();
|
||||
return url;
|
||||
},
|
||||
});
|
||||
|
||||
// Register handlers BEFORE starting
|
||||
client.on("connected", (e) => {
|
||||
console.log(`Connected: ${e.connectionId}`);
|
||||
});
|
||||
|
||||
client.on("group-message", (e) => {
|
||||
console.log(`${e.message.group}: ${e.message.data}`);
|
||||
});
|
||||
|
||||
await client.start();
|
||||
```
|
||||
|
||||
### Send Messages
|
||||
|
||||
```typescript
|
||||
// Join group first
|
||||
await client.joinGroup("chat-room");
|
||||
|
||||
// Send to group
|
||||
await client.sendToGroup("chat-room", "Hello!", "text");
|
||||
await client.sendToGroup("chat-room", { type: "message", content: "Hi" }, "json");
|
||||
|
||||
// Send options
|
||||
await client.sendToGroup("chat-room", "Hello", "text", {
|
||||
noEcho: true, // Don't echo back to sender
|
||||
fireAndForget: true, // Don't wait for ack
|
||||
});
|
||||
|
||||
// Send event to server
|
||||
await client.sendEvent("userAction", { action: "typing" }, "json");
|
||||
```
|
||||
|
||||
### Event Handlers
|
||||
|
||||
```typescript
|
||||
// Connection lifecycle
|
||||
client.on("connected", (e) => {
|
||||
console.log(`Connected: ${e.connectionId}, User: ${e.userId}`);
|
||||
});
|
||||
|
||||
client.on("disconnected", (e) => {
|
||||
console.log(`Disconnected: ${e.message}`);
|
||||
});
|
||||
|
||||
client.on("stopped", () => {
|
||||
console.log("Client stopped");
|
||||
});
|
||||
|
||||
// Messages
|
||||
client.on("group-message", (e) => {
|
||||
console.log(`[${e.message.group}] ${e.message.fromUserId}: ${e.message.data}`);
|
||||
});
|
||||
|
||||
client.on("server-message", (e) => {
|
||||
console.log(`Server: ${e.message.data}`);
|
||||
});
|
||||
|
||||
// Rejoin failure
|
||||
client.on("rejoin-group-failed", (e) => {
|
||||
console.log(`Failed to rejoin ${e.group}: ${e.error}`);
|
||||
});
|
||||
```
|
||||
|
||||
## Express Event Handler
|
||||
|
||||
```typescript
|
||||
import express from "express";
|
||||
import { WebPubSubEventHandler } from "@azure/web-pubsub-express";
|
||||
|
||||
const app = express();
|
||||
|
||||
const handler = new WebPubSubEventHandler("chat", {
|
||||
path: "/api/webpubsub/hubs/chat/",
|
||||
|
||||
// Blocking: approve/reject connection
|
||||
handleConnect: (req, res) => {
|
||||
if (!req.claims?.sub) {
|
||||
res.fail(401, "Authentication required");
|
||||
return;
|
||||
}
|
||||
res.success({
|
||||
userId: req.claims.sub[0],
|
||||
groups: ["general"],
|
||||
roles: ["webpubsub.sendToGroup"],
|
||||
});
|
||||
},
|
||||
|
||||
// Blocking: handle custom events
|
||||
handleUserEvent: (req, res) => {
|
||||
console.log(`Event from ${req.context.userId}:`, req.data);
|
||||
res.success(`Received: ${req.data}`, "text");
|
||||
},
|
||||
|
||||
// Non-blocking
|
||||
onConnected: (req) => {
|
||||
console.log(`Client connected: ${req.context.connectionId}`);
|
||||
},
|
||||
|
||||
onDisconnected: (req) => {
|
||||
console.log(`Client disconnected: ${req.context.connectionId}`);
|
||||
},
|
||||
});
|
||||
|
||||
app.use(handler.getMiddleware());
|
||||
|
||||
// Negotiate endpoint
|
||||
app.get("/negotiate", async (req, res) => {
|
||||
const token = await serviceClient.getClientAccessToken({
|
||||
userId: req.user?.id,
|
||||
});
|
||||
res.json({ url: token.url });
|
||||
});
|
||||
|
||||
app.listen(8080);
|
||||
```
|
||||
|
||||
## Key Types
|
||||
|
||||
```typescript
|
||||
// Server
|
||||
import {
|
||||
WebPubSubServiceClient,
|
||||
WebPubSubGroup,
|
||||
GenerateClientTokenOptions,
|
||||
HubSendToAllOptions,
|
||||
} from "@azure/web-pubsub";
|
||||
|
||||
// Client
|
||||
import {
|
||||
WebPubSubClient,
|
||||
WebPubSubClientOptions,
|
||||
OnConnectedArgs,
|
||||
OnGroupDataMessageArgs,
|
||||
} from "@azure/web-pubsub-client";
|
||||
|
||||
// Express
|
||||
import {
|
||||
WebPubSubEventHandler,
|
||||
ConnectRequest,
|
||||
UserEventRequest,
|
||||
ConnectResponseHandler,
|
||||
} from "@azure/web-pubsub-express";
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Entra ID auth** - `DefaultAzureCredential` for production
|
||||
2. **Register handlers before start** - Don't miss initial events
|
||||
3. **Use groups for channels** - Organize messages by topic/room
|
||||
4. **Handle reconnection** - Client auto-reconnects by default
|
||||
5. **Validate in handleConnect** - Reject unauthorized connections early
|
||||
6. **Use noEcho** - Prevent message echo back to sender when needed
|
||||
Reference in New Issue
Block a user