Azure Web PubSub
Store that uses Azure Web PubSub infrastructure as the transport layer
Introduction
The Azure Web PubSub Store is store for Weave.js that leverages Azure Web PubSub as the transport layer. Built on top of Yjs, it uses Azure's scalable infrastructure to broadcast updates and sync shared state across all connected clients.
This store is ideal for applications that need enterprise-grade reliability, scalability, and easy integration with other Azure services. It enables Weave.js apps to support seamless real-time collaboration with minimal setup and strong cloud support.
It supports features like user presence, shared document syncing, and optional data persistence on the backend side.
Server frameworks supported
On the server side we provide some utilities to setup the backend server.
As today we support this server-side frameworks:
Other server frameworks
For other server types, you can take a look at the code and build your own support.
On our roadmap we will inform when we will give support to other server frameworks.
Usage
Check the Azure Web PubSub store package API reference for an overview of how to use it both:
Server setup
Setup the frontend is easy, you just need to follow this steps on your Express-based backend:
Install the WebSocket Store backend dependencies
First lets install the necessary dependencies on the backend-side:
npm install canvas @azure/web-pubsub @azure/web-pubsub-express y-protocols yjs @inditextech/weave-store-azure-web-pubsub
Setup the server
Then setup your server as needed, here you have an example of a simple server, where the persistence is handled on the file system:
import { fileURLToPath } from "url";
import path from "path";
import fs from "fs/promises";
import cors from "cors";
import express, { Router } from "express";
import { WeaveAzureWebPubsubServer } from "@inditextech/weave-store-azure-web-pubsub/server"; // (1)
// eslint-disable-next-line @typescript-eslint/naming-convention
const __filename = fileURLToPath(import.meta.url);
// eslint-disable-next-line @typescript-eslint/naming-convention
const __dirname = path.dirname(__filename);
const host = process.env.WEAVE_AZURE_WEB_PUBSUB_HOST || "localhost";
const port = parseInt(process.env.WEAVE_AZURE_WEB_PUBSUB_PORT || "1234");
const endpoint = process.env.WEAVE_AZURE_WEB_PUBSUB_ENDPOINT; // (2)
const key = process.env.WEAVE_AZURE_WEB_PUBSUB_KEY; // (2)
const hubName = process.env.WEAVE_AZURE_WEB_PUBSUB_HUB_NAME; // (2)
if (!endpoint || !key || !hubName) {
throw new Error("Missing required environment variables");
}
// prettier-ignore
const azureWebPubsubServer = new WeaveAzureWebPubsubServer({ // (3)
pubsubConfig: {
endpoint,
key,
hubName,
}, // (4)
fetchRoom: async (docName: string) => {
try {
const roomsFolder = path.join(__dirname, "rooms");
const roomsFile = path.join(roomsFolder, docName);
return await fs.readFile(roomsFile);
} catch (e) {
return null;
}
}, // (5)
persistRoom: async (
docName: string,
actualState: Uint8Array<ArrayBufferLike>
) => {
try {
const roomsFolder = path.join(__dirname, "rooms");
let folderExists = false;
try {
await fs.access(roomsFolder);
folderExists = true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
folderExists = false;
}
if (!folderExists) {
await fs.mkdir(roomsFolder, { recursive: true });
}
const roomsFile = path.join(roomsFolder, docName);
await fs.writeFile(roomsFile, actualState);
} catch (ex) {
console.error(ex);
}
}, // (6)
});
const app = express();
const corsOptions = {
origin: true,
};
app.use(cors(corsOptions));
const router = Router();
router.use(azureWebPubsubServer.getMiddleware()); // (7)
router.get(`/rooms/:roomId/connect`, async (req, res) => {
const roomId = req.params.roomId;
const url = await azureWebPubsubServer.clientConnect(roomId);
res.json({ url });
}); // (8)
app.use(`/api/v1/${hubName}`, router); // (9)
app.listen(port, host, (err: Error | undefined) => {
// (10)
if (err) throw err;
// eslint-disable-next-line no-console
console.log(`Server started @ http://${host}:${port}\n`);
// eslint-disable-next-line no-console
console.log(`Connection endpoint: /api/v1/${hubName}/{roomId}/connect`);
});
File explanation:
(1)
: Import the Weave.js server dependencies.(2)
: Define the endpoint, key and hubName of the Azure Web PubSub instance where we will connect.(3)
: Instantiate theWeaveAzureWebPubsubServer
class. Which returns the server utilities to handle Azure Web PubSub connections.(4)
: Define the Azure Web Pubsub properties.(5)
: Define thefetchRoom
function that handles theget
part of the persistence, in this case we fetch a binary file with the shared-state from the file system if exists.(6)
: Define thepersistRoom
function that handles theset
part of the persistence, in this case we save to a file on the file system the shared-state.(7)
: Setup the middleware that handles the Azure Web PubSub connections.(8)
: Setup a route that is responsible to call theclientConnect
method of the server in order to obtain a secure connection URL to the Azure Web PubSub Hub.(9)
: Setup the server routes.(10)
: Start the server.
When an user tries to join a room, it calls the defined route with the room Id where he
want to join, then the WeaveAzureWebPubsubServer
using the clientConnect
function, call the Azure Web PubSub
Hub to extract a secure connection and this URL is passed as response to the client. Then the client connects to
this URL and the server does the rest: persistence, handling the messages sent over the connection, etc.
Client setup
Setup the frontend is easy, you just need to follow this steps:
Install the WebSocket Store frontend dependency
First lets install the necessary dependencies for the frontend client:
npm install @inditextech/weave-store-azure-web-pubsub
Setup the client on the Weave instance
Then you need to add the Azure Web PubSub store client to the Weave instance in order to handle the connection when the user tries to join a room. This is done:
import { WeaveStoreAzureWebPubsub } from "@inditextech/weave-store-azure-web-pubsub/client"; // (1)
const roomId = "MyRoom"; // (2)
const store = new WeaveStoreAzureWebPubsub( // (3)
{
getUser, // (4)
undoManagerOptions: {
captureTimeout: 500,
}, // (5)
},
{
roomId, // (6)
url: http://localhost:1234/api/v1/rooms/${roomId}/connect`, // (7)
}
);
const instance = new Weave(
{
...
store, // (8)
},
...
);
Explanation:
(1)
: Import the Weave.js client dependencies.(2)
: Define the room Id to connect to.(3)
: Instantiate theWeaveStoreAzureWebPubsub
class. Which returns the Azure Web PubSub store client.(4)
: Define thegetUser
function that return the metadata of the user that wants to connect.(5)
: We customize thecaptureTimeout
of the Undo-Redo manager of the store, to capture changes every 500ms.(6)
: We set the room Id where we want to connect to.(7)
: Define url where we want to connect and fetch the connection URL to the Azure Web PubSub.(8)
: Set the instantiated store to the Weave instance.
Client events
When the store client tries to connect, we provide an event were we return the connection status,
the event is named onStoreConnectionStatusChange
, and it returns the actual connection status,
being this status:
const WEAVE_STORE_CONNECTION_STATUS = {
["ERROR"]: "error",
["CONNECTING"]: "connecting",
["CONNECTED"]: "connected",
["DISCONNECTED"]: "disconnected",
} as const;
type WeaveStoreConnectionStatusKeys =
keyof typeof WEAVE_STORE_CONNECTION_STATUS;
type WeaveStoreConnectionStatus =
(typeof WEAVE_STORE_CONNECTION_STATUS)[WeaveStoreConnectionStatusKeys];
type WeaveStoreOnStoreConnectionStatusChangeEvent = WeaveStoreConnectionStatus;
Client connectivity flow
We've prepared this simple diagram showcasing how the connectivity works on the Weave.js instance
when using this store. The lines connecting display the value of the connection status
returned
on the onStoreConnectionStatusChange
event.