UNPKG

@microsoft/signalr

Version:
266 lines (226 loc) 8.71 kB
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. import { HttpClient } from "./HttpClient"; import { MessageHeaders } from "./IHubProtocol"; import { ILogger, LogLevel } from "./ILogger"; import { NullLogger } from "./Loggers"; import { IStreamSubscriber, ISubscription } from "./Stream"; import { Subject } from "./Subject"; // Version token that will be replaced by the prepack command /** The version of the SignalR client. */ export const VERSION: string = "0.0.0-DEV_BUILD"; /** @private */ export class Arg { public static isRequired(val: any, name: string): void { if (val === null || val === undefined) { throw new Error(`The '${name}' argument is required.`); } } public static isNotEmpty(val: string, name: string): void { if (!val || val.match(/^\s*$/)) { throw new Error(`The '${name}' argument should not be empty.`); } } public static isIn(val: any, values: any, name: string): void { // TypeScript enums have keys for **both** the name and the value of each enum member on the type itself. if (!(val in values)) { throw new Error(`Unknown ${name} value: ${val}.`); } } } /** @private */ export class Platform { public static get isBrowser(): boolean { return typeof window === "object"; } public static get isWebWorker(): boolean { return typeof self === "object" && "importScripts" in self; } public static get isNode(): boolean { return !this.isBrowser && !this.isWebWorker; } } /** @private */ export function getDataDetail(data: any, includeContent: boolean): string { let detail = ""; if (isArrayBuffer(data)) { detail = `Binary data of length ${data.byteLength}`; if (includeContent) { detail += `. Content: '${formatArrayBuffer(data)}'`; } } else if (typeof data === "string") { detail = `String data of length ${data.length}`; if (includeContent) { detail += `. Content: '${data}'`; } } return detail; } /** @private */ export function formatArrayBuffer(data: ArrayBuffer): string { const view = new Uint8Array(data); // Uint8Array.map only supports returning another Uint8Array? let str = ""; view.forEach((num) => { const pad = num < 16 ? "0" : ""; str += `0x${pad}${num.toString(16)} `; }); // Trim of trailing space. return str.substr(0, str.length - 1); } // Also in signalr-protocol-msgpack/Utils.ts /** @private */ export function isArrayBuffer(val: any): val is ArrayBuffer { return val && typeof ArrayBuffer !== "undefined" && (val instanceof ArrayBuffer || // Sometimes we get an ArrayBuffer that doesn't satisfy instanceof (val.constructor && val.constructor.name === "ArrayBuffer")); } /** @private */ export async function sendMessage(logger: ILogger, transportName: string, httpClient: HttpClient, url: string, accessTokenFactory: (() => string | Promise<string>) | undefined, content: string | ArrayBuffer, logMessageContent: boolean, withCredentials: boolean, defaultHeaders: MessageHeaders): Promise<void> { let headers = {}; if (accessTokenFactory) { const token = await accessTokenFactory(); if (token) { headers = { ["Authorization"]: `Bearer ${token}`, }; } } const [name, value] = getUserAgentHeader(); headers[name] = value; logger.log(LogLevel.Trace, `(${transportName} transport) sending data. ${getDataDetail(content, logMessageContent)}.`); const responseType = isArrayBuffer(content) ? "arraybuffer" : "text"; const response = await httpClient.post(url, { content, headers: { ...headers, ...defaultHeaders}, responseType, withCredentials, }); logger.log(LogLevel.Trace, `(${transportName} transport) request complete. Response status: ${response.statusCode}.`); } /** @private */ export function createLogger(logger?: ILogger | LogLevel) { if (logger === undefined) { return new ConsoleLogger(LogLevel.Information); } if (logger === null) { return NullLogger.instance; } if ((logger as ILogger).log) { return logger as ILogger; } return new ConsoleLogger(logger as LogLevel); } /** @private */ export class SubjectSubscription<T> implements ISubscription<T> { private subject: Subject<T>; private observer: IStreamSubscriber<T>; constructor(subject: Subject<T>, observer: IStreamSubscriber<T>) { this.subject = subject; this.observer = observer; } public dispose(): void { const index: number = this.subject.observers.indexOf(this.observer); if (index > -1) { this.subject.observers.splice(index, 1); } if (this.subject.observers.length === 0 && this.subject.cancelCallback) { this.subject.cancelCallback().catch((_) => { }); } } } /** @private */ export class ConsoleLogger implements ILogger { private readonly minimumLogLevel: LogLevel; // Public for testing purposes. public outputConsole: { error(message: any): void, warn(message: any): void, info(message: any): void, log(message: any): void, }; constructor(minimumLogLevel: LogLevel) { this.minimumLogLevel = minimumLogLevel; this.outputConsole = console; } public log(logLevel: LogLevel, message: string): void { if (logLevel >= this.minimumLogLevel) { switch (logLevel) { case LogLevel.Critical: case LogLevel.Error: this.outputConsole.error(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`); break; case LogLevel.Warning: this.outputConsole.warn(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`); break; case LogLevel.Information: this.outputConsole.info(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`); break; default: // console.debug only goes to attached debuggers in Node, so we use console.log for Trace and Debug this.outputConsole.log(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`); break; } } } } /** @private */ export function getUserAgentHeader(): [string, string] { let userAgentHeaderName = "X-SignalR-User-Agent"; if (Platform.isNode) { userAgentHeaderName = "User-Agent"; } return [ userAgentHeaderName, constructUserAgent(VERSION, getOsName(), getRuntime(), getRuntimeVersion()) ]; } /** @private */ export function constructUserAgent(version: string, os: string, runtime: string, runtimeVersion: string | undefined): string { // Microsoft SignalR/[Version] ([Detailed Version]; [Operating System]; [Runtime]; [Runtime Version]) let userAgent: string = "Microsoft SignalR/"; const majorAndMinor = version.split("."); userAgent += `${majorAndMinor[0]}.${majorAndMinor[1]}`; userAgent += ` (${version}; `; if (os && os !== "") { userAgent += `${os}; `; } else { userAgent += "Unknown OS; "; } userAgent += `${runtime}`; if (runtimeVersion) { userAgent += `; ${runtimeVersion}`; } else { userAgent += "; Unknown Runtime Version"; } userAgent += ")"; return userAgent; } function getOsName(): string { if (Platform.isNode) { switch (process.platform) { case "win32": return "Windows NT"; case "darwin": return "macOS"; case "linux": return "Linux"; default: return process.platform; } } else { return ""; } } function getRuntimeVersion(): string | undefined { if (Platform.isNode) { return process.versions.node; } return undefined; } function getRuntime(): string { if (Platform.isNode) { return "NodeJS"; } else { return "Browser"; } }