Express 用 Azure Web Pubsub CloudEvents ハンドラー

Azure Web PubSub サービス は、開発者がリアルタイム機能と発行/サブスクライブ パターンを使用して Web アプリケーションを簡単に構築するのに役立つ Azure マネージド サービスです。 サーバーとクライアント間、またはクライアント間でリアルタイムの発行/サブスクライブ メッセージングを必要とするシナリオでは、Azure Web PubSub サービスを使用できます。 サーバーからのポーリングや HTTP 要求の送信が必要な従来のリアルタイム機能でも、Azure Web PubSub サービスを使用できます。

WebSocket 接続が接続されると、Web PubSub サービスは接続ライフサイクルとメッセージを CloudEvents 形式のイベントに変換します。 このライブラリは、次の図に示すように、WebSocket 接続のライフサイクルとメッセージを表すイベントを処理する高速ミドルウェアを提供します。

クラウドイベント

ここで使われる用語の詳細は 「キーコンセプト 」セクションで説明されています。

ソース コード | パッケージ (NPM) | API リファレンス ドキュメント | 製品ドキュメント | サンプル

作業の開始

現在サポートされている環境

前提条件

1. @azure/web-pubsub-express パッケージをインストールする

npm install @azure/web-pubsub-express

2. WebPubSubEventHandler を作成してください

import { WebPubSubEventHandler } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat");

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

主な概念

接続

接続はクライアントまたはクライアント接続とも呼ばれ、Web PubSubサービスに接続された個々のWebSocket接続を指します。 接続が成功すると、Web PubSubサービスによってこの接続に固有の接続IDが割り当てられます。

ハブ

ハブは、一連のクライアント接続の論理的な概念です。 通常は一つのハブを一つの目的、例えばチャット用や通知用に使います。 クライアント接続が作成されると、ハブに接続され、その有効期間中にそのハブに属します。 異なるアプリケーションでは、異なるハブ名を使用して、1 つの Azure Web PubSub サービスを共有できます。

グループ

グループは、ハブへの接続のサブセットです。 必要に応じて、グループにクライアント接続を追加したり、グループからクライアント接続を削除したりできます。 たとえば、クライアントがチャット ルームに参加したり、クライアントがチャット ルームを離れたりするときに、このチャット ルームをグループと見なすことができます。 クライアントは複数のグループに参加できます。また、1 つのグループに複数のクライアントを含めることもできます。

User

Web PubSub への接続は、1 人のユーザーに属することができます。 1 人のユーザーが複数のデバイスまたは複数のブラウザー タブに接続されている場合など、ユーザーが複数の接続を持つ場合があります。

クライアント イベント

イベントはクライアント接続のライフサイクル中に作成されます。 例えば、単純なWebSocketクライアント接続は、サービスに接続しようとしたときに connect イベント、サービスに接続成功したときに connected イベント、サービスにメッセージを送信した際に message イベント、サービスから切断されたときに disconnected イベントを発生させます。

イベント ハンドラー

イベントハンドラにはクライアントイベントを処理するロジックが含まれています。 イベントハンドラは事前にポータルやAzure CLIを通じてサービスに登録・設定する必要があります。 イベントハンドラロジックをホストする場所は、一般的にサーバー側と考えられます。

connect要求を処理して割り当てる<userId>

import { WebPubSubEventHandler } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat", {
  handleConnect: (req, res) => {
    // auth the connection and set the userId of the connection
    res.success({
      userId: "<userId>",
    });
  },
  allowedEndpoints: ["https://<yourAllowedService>.webpubsub.azure.com"],
});

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

connectリクエストを処理し、認証が失敗した場合は接続を拒否します

import { WebPubSubEventHandler } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat", {
  handleConnect: (req, res) => {
    // auth the connection and reject the connection if auth failed
    res.fail(401, "Unauthorized");
    // the following method is also a valid approach
    // res.failWith({ code: 401, detail: "Unauthorized" });
  },
  allowedEndpoints: ["https://<yourAllowedService>.webpubsub.azure.com"],
});

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

connectedリクエストを処理してください

import { WebPubSubEventHandler } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat", {
  onConnected: (connectedRequest) => {
    // Your onConnected logic goes here
  },
  allowedEndpoints: ["https://<yourAllowedService>.webpubsub.azure.com"],
});

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

onGroupJoinedリクエストを処理してください

import { WebPubSubEventHandler } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat", {
  onGroupJoined: (groupJoinedRequest) => {
    console.log(
      `Connection ${groupJoinedRequest.context.connectionId} joined group ${groupJoinedRequest.group}`,
    );
  },
  allowedEndpoints: ["https://<yourAllowedService>.webpubsub.azure.com"],
});

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

onGroupLeftリクエストを処理してください

import { WebPubSubEventHandler } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat", {
  onGroupLeft: (groupLeftRequest) => {
    console.log(
      `Connection ${groupLeftRequest.context.connectionId} left group ${groupLeftRequest.group}`,
    );
  },
  allowedEndpoints: ["https://<yourAllowedService>.webpubsub.azure.com"],
});

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

onDisconnectedリクエストを処理してください

import { WebPubSubEventHandler } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat", {
  onDisconnected: (disconnectedRequest) => {
    // Your onDisconnected logic goes here
  },
  allowedEndpoints: ["https://<yourAllowedService>.webpubsub.azure.com"],
});

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

mqttの connect リクエストを処理し、 <userId> および <mqtt> プロパティを割り当てます

import { WebPubSubEventHandler, MqttConnectRequest } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat", {
  handleConnect: (req, res) => {
    if (req.context.clientProtocol === "mqtt") {
      // return mqtt response when request is of MQTT kind
      // get connect request as mqtt request and print it
      const mqttRequest = req as MqttConnectRequest;
      console.log(mqttRequest);

      // auth the connection and return mqtt response
      res.success({
        userId: "user1",
        mqtt: { userProperties: [{ name: "a", value: "b" }] },
      });
    } else {
      res.success({
        userId: "user1",
      });
    }
  },
  allowedEndpoints: ["https://<yourAllowedService>.webpubsub.azure.com"],
});

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

mqttの connect リクエストを処理し、認証が失敗した場合は接続を拒否します

import { WebPubSubEventHandler, MqttConnectRequest } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat", {
  handleConnect: (req, res) => {
    // auth the connection and reject the connection if auth failed
    if (req.context.clientProtocol === "mqtt") {
      // return mqtt error response when request is of MQTT kind
      // get connect request as mqtt request and print it
      const mqttRequest = req as MqttConnectRequest;
      console.log(mqttRequest);

      // auth the connection and return mqtt failure response
      res.fail(401, "Not Authorized");

      // Or use below method for more fine-grained control over the MQTT return code
      // res.failWith({ mqtt: { code: MqttV500ConnectReasonCode.NotAuthorized } });
    } else res.success();
  },
  allowedEndpoints: ["https://<yourAllowedService>.webpubsub.azure.com"],
});

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

mqttリクエストの onDisconnected を処理してください

import { WebPubSubEventHandler, MqttDisconnectedRequest } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat", {
  onDisconnected: (disconnectedRequest) => {
    if (disconnectedRequest.context.clientProtocol === "mqtt") {
      // get disconnect request as mqtt request and print it
      const mqttRequest = disconnectedRequest as MqttDisconnectedRequest;
      console.log(mqttRequest.mqtt);
      // Your onDisconnected logic goes here
    } else {
      console.log(disconnectedRequest);
      // Your onDisconnected logic goes here
    }
  },
  allowedEndpoints: ["https://<yourAllowedService>.webpubsub.azure.com"],
});

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

指定されたエンドポイントのみを許可する

import { WebPubSubEventHandler } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat", {
  allowedEndpoints: [
    "https://<yourAllowedService1>.webpubsub.azure.com",
    "https://<yourAllowedService2>.webpubsub.azure.com",
  ],
});

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

カスタム イベント ハンドラー のパスを設定する

import { WebPubSubEventHandler } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat", {
  path: "/customPath1",
});

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  // Azure WebPubSub Upstream ready at http://localhost:3000/customPath1
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

接続状態の設定と読み取り

import { WebPubSubEventHandler } from "@azure/web-pubsub-express";
import express from "express";

const handler = new WebPubSubEventHandler("chat", {
  handleConnect(req, res) {
    // You can set the state for the connection, it lasts throughout the lifetime of the connection
    res.setState("calledTime", 1);
    res.success();
  },
  handleUserEvent(req, res) {
    const calledTime = req.context.states.calledTime++;
    console.log(calledTime);
    // You can also set the state here
    res.setState("calledTime", calledTime);
    res.success();
  },
});

const app = express();

app.use(handler.getMiddleware());

app.listen(3000, () =>
  console.log(`Azure WebPubSub Upstream ready at http://localhost:3000${handler.path}`),
);

Troubleshooting

ログを有効にする

ログ記録を有効にすると、エラーに関する有用な情報を明らかにするのに役立つ場合があります。 HTTP 要求と応答のログを表示するには、AZURE_LOG_LEVEL 環境変数を infoに設定します。

export AZURE_LOG_LEVEL=verbose

あるいは、実行時にsetLogLevel@azure/loggerを呼び出すことでログログを有効にすることもできます:

import { setLogLevel } from "@azure/logger";

setLogLevel("info");

ログを有効にする方法の詳細な手順については、 @azure/logger パッケージのドキュメントを参照してください。

ライブ トレース

Web PubSub サービス ポータルから ライブ トレース を使用して、ライブ トラフィックを表示します。

次のステップ

このライブラリの使い方については、samplesディレクトリをご覧ください。

Contributing

このライブラリに貢献したい場合は、コードのビルドやテスト方法について詳しく知るために、contributing guideをお読みください。