Skip to content

Commit

Permalink
[rush-serve-plugin] Add WebSocket support
Browse files Browse the repository at this point in the history
  • Loading branch information
dmichon-msft committed Sep 14, 2023
1 parent f1f9894 commit a3e79eb
Show file tree
Hide file tree
Showing 8 changed files with 486 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Add support for optional build status notifications over a web socket connection to `@rushstack/rush-serve-plugin`.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
12 changes: 8 additions & 4 deletions common/config/rush/nonbrowser-approved-packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -406,14 +406,14 @@
"name": "compression",
"allowedCategories": [ "libraries" ]
},
{
"name": "cors",
"allowedCategories": [ "libraries" ]
},
{
"name": "constructs",
"allowedCategories": [ "tests" ]
},
{
"name": "cors",
"allowedCategories": [ "libraries" ]
},
{
"name": "css-loader",
"allowedCategories": [ "libraries", "tests" ]
Expand Down Expand Up @@ -826,6 +826,10 @@
"name": "wordwrap",
"allowedCategories": [ "libraries" ]
},
{
"name": "ws",
"allowedCategories": [ "libraries" ]
},
{
"name": "xmldoc",
"allowedCategories": [ "libraries" ]
Expand Down
74 changes: 74 additions & 0 deletions rush-plugins/rush-serve-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,77 @@ What happens:
- Rush uses the configuration in the aforementioned files to configure an Express server to serve project outputs as static (but not cached) content
- When a change happens to a source file, Rush's normal watch-mode machinery will rebuild all affected project phases, resulting in new files on disk
- The next time one of these files is requested, Rush will serve the new version. Optionally, may support signals for automatic refresh.

## Live Build Status via Web Socket

This plugin also provides a web socket server that notifies clients of the build status in real time. To use the server, configure the `buildStatusWebSocketPath` option in `common/config/rush-plugins/rush-serve-plugin.json`. Specifying `/` will make the web socket server available at `wss://localhost:<port>/`.

The recommended way to connect to the web socket is to serve a static HTML page from the serve plugin using the `globalRouting` configuration.

To use the socket:
```ts
import type {
IWebSocketEventMessage,
IOperationInfo,
IRushSessionInfo,
ReadableOperationStatus
} from '@rushstack/rush-serve-plugin/api';

const socket: WebSocket = new WebSocket(`wss://${self.location.host}${buildStatusWebSocketPath}`);

const operationsByName: Map<string, IOperationInfo> = new Map();
let buildStatus: ReadableOperationStatus = 'Ready';

function updateOperations(operations): void {
for (const operation of operations) {
operationsByName.set(operation.name, operation);
}

for (const [operationName, operation] of operationsByName) {
// Do something with the operation
}
}

function updateSessionInfo(sessionInfo: IRushSessionInfo): void {
const { actionName, repositoryIdentifier } = sessionInfo;
}

function updateBuildStatus(newStatus: ReadableOperationStatus): void {
buildStatus = newStatus;
// Render
}

socket.addEventListener('message', (ev) => {
const message: IWebSocketEventMessage = JSON.parse(ev.data);

switch (message.event) {
case 'before-execute': {
const { operations } = message;
updateOperations(operations);
updateBuildStatus('Executing');
break;
}

case 'status-change': {
const { operations } = message;
updateOperations(operations);
break;
}

case 'after-execute': {
const { status } = message;
updateBuildStatus(status);
break;
}

case 'sync': {
operationsByName.clear();
const { operations, status, sessionInfo } = message;
updateOperations(operations);
updateSessionInfo(sessionInfo);
updateBuildStatus(status);
break;
}
}
});
```
27 changes: 24 additions & 3 deletions rush-plugins/rush-serve-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"type": "git",
"directory": "rush-plugins/rush-serve-plugin"
},
"main": "lib/index.js",
"main": "lib-commonjs/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "heft test --clean",
Expand All @@ -25,7 +25,8 @@
"compression": "~1.7.4",
"cors": "~2.8.5",
"express": "4.18.1",
"http2-express-bridge": "~1.0.7"
"http2-express-bridge": "~1.0.7",
"ws": "~8.14.1"
},
"devDependencies": {
"@rushstack/eslint-config": "workspace:*",
Expand All @@ -35,6 +36,26 @@
"@types/cors": "~2.8.12",
"@types/express": "4.17.13",
"@types/heft-jest": "1.0.1",
"@types/node": "18.17.15"
"@types/node": "18.17.15",
"@types/ws": "8.5.5"
},
"exports": {
".": {
"require": "./lib/index.js",
"types": "./dist/rush-serve-plugin.d.ts"
},
"./api": {
"types": "./lib/api.types.d.ts"
}
},
"typesVersions": {
"*": {
".": [
"dist/rush-serve-plugin.d.ts"
],
"api": [
"lib/api.types.d.ts"
]
}
}
}
14 changes: 13 additions & 1 deletion rush-plugins/rush-serve-plugin/src/RushServePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,21 @@ export interface IRushServePluginOptions {
* The names of phased commands to which the plugin should be applied.
*/
phasedCommands: ReadonlyArray<string>;

/**
* The URL path at which to host the web socket connection for monitoring build status. If not specified, the web socket interface will not be enabled.
*/
buildStatusWebSocketPath?: string;

/**
* The name of a parameter that Rush is configured to use to pass a port number to underlying operations.
* If specified, the plugin will ensure the value is synchronized with the port used for its server.
*/
portParameterLongName?: string | undefined;

/**
* Routing rules for files that are associated with the entire workspace, rather than a single project (e.g. files output by Rush plugins).
*/
globalRouting?: IGlobalRoutingRuleJson[];
}

Expand All @@ -43,11 +52,13 @@ export class RushServePlugin implements IRushPlugin {
private readonly _phasedCommands: Set<string>;
private readonly _portParameterLongName: string | undefined;
private readonly _globalRoutingRules: IGlobalRoutingRuleJson[];
private readonly _buildStatusWebSocketPath: string | undefined;

public constructor(options: IRushServePluginOptions) {
this._phasedCommands = new Set(options.phasedCommands);
this._portParameterLongName = options.portParameterLongName;
this._globalRoutingRules = options.globalRouting ?? [];
this._buildStatusWebSocketPath = options.buildStatusWebSocketPath;
}

public apply(rushSession: RushSession, rushConfiguration: RushConfiguration): void {
Expand All @@ -73,7 +84,8 @@ export class RushServePlugin implements IRushPlugin {
rushConfiguration,
command,
portParameterLongName: this._portParameterLongName,
globalRoutingRules
globalRoutingRules,
buildStatusWebSocketPath: this._buildStatusWebSocketPath
});
};

Expand Down
127 changes: 127 additions & 0 deletions rush-plugins/rush-serve-plugin/src/api.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type { OperationStatus } from '@rushstack/rush-sdk';

/**
* Human readable status values. These are the PascalCase keys of the `OperationStatus` enumeration.
*/
export type ReadableOperationStatus = keyof typeof OperationStatus;

/**
* Information about an operation in the graph.
*/
export interface IOperationInfo {
/**
* The display name of the operation.
*/
name: string;

/**
* The npm package name of the containing Rush Project.
*/
packageName: string;

/**
* The name of the containing phase.
*/
phaseName: string;

/**
* If true, this operation is configured to be silent and is included for completeness.
*/
silent: boolean;
/**
* If true, this operation is configured to be a noop and is included for graph completeness.
*/
noop: boolean;

/**
* The current status of the operation. This value is in PascalCase and is the key of the corresponding `OperationStatus` constant.
*/
status: ReadableOperationStatus;

/**
* The start time of the operation, if it has started, in milliseconds. Not wall clock time.
*/
startTime: number | undefined;

/**
* The end time of the operation, if it has finished, in milliseconds. Not wall clock time.
*/
endTime: number | undefined;
}

/**
* Information about the current Rush session.
*/
export interface IRushSessionInfo {
/**
* The name of the command being run.
*/
actionName: string;

/**
* A unique identifier for the repository in which this Rush is running.
*/
repositoryIdentifier: string;
}

/**
* Message sent to a WebSocket client at the start of an execution pass.
*/
export interface IWebSocketBeforeExecuteEventMessage {
event: 'before-execute';
operations: IOperationInfo[];
}

/**
* Message sent to a WebSocket client at the end of an execution pass.
*/
export interface IWebSocketAfterExecuteEventMessage {
event: 'after-execute';
status: ReadableOperationStatus;
}

/**
* Message sent to a WebSocket client when one or more operations change status.
*
* Batched to reduce noise and improve throughput.
*/
export interface IWebSocketBatchStatusChangeEventMessage {
event: 'status-change';
operations: IOperationInfo[];
}

/**
* Message sent to a WebSocket client upon initial connection, or when explicitly requested.
*
* @see IWebSocketSyncCommandMessage
*/
export interface IWebSocketSyncEventMessage {
event: 'sync';
operations: IOperationInfo[];
sessionInfo: IRushSessionInfo;
status: ReadableOperationStatus;
}

/**
* The set of possible messages sent to a WebSocket client.
*/
export type IWebSocketEventMessage =
| IWebSocketBeforeExecuteEventMessage
| IWebSocketAfterExecuteEventMessage
| IWebSocketBatchStatusChangeEventMessage
| IWebSocketSyncEventMessage;

/**
* Message received from a WebSocket client to request a sync.
*/
export interface IWebSocketSyncCommandMessage {
command: 'sync';
}

/**
* The set of possible messages received from a WebSocket client.
*/
export type IWebSocketCommandMessage = IWebSocketSyncCommandMessage;
Loading

0 comments on commit a3e79eb

Please sign in to comment.