UNPKG

botframework-streaming

Version:

Streaming library for the Microsoft Bot Framework

170 lines (144 loc) 6.24 kB
/** * @module botframework-streaming */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { SubscribableStream } from '../subscribableStream'; import { StreamManager, PayloadTypes } from '../payloads'; import { ContentStream } from '../contentStream'; import { IAssemblerParams, IHeader, IResponsePayload, IReceiveResponse, IReceiveRequest, IRequestPayload, } from '../interfaces'; /** * Assembles payloads for streaming library. * * @internal */ export class PayloadAssembler { id: string; end: boolean; contentLength: number; payloadType: string | PayloadTypes; private stream: SubscribableStream; private readonly _onCompleted: (id: string, receiveResponse: IReceiveResponse | IReceiveRequest) => Promise<void>; private readonly _byteOrderMark = 0xfeff; private readonly _utf: BufferEncoding = 'utf8'; /** * Initializes a new instance of the [PayloadAssembler](xref:botframework-streaming.PayloadAssembler) class. * * @param streamManager The [StreamManager](xref:botframework-streaming.StreamManager) managing the stream being assembled. * @param params Parameters for a streaming assembler. */ constructor( private readonly streamManager: StreamManager, params: IAssemblerParams, ) { if (params.header) { this.id = params.header.id; this.payloadType = params.header.payloadType; this.contentLength = params.header.payloadLength; this.end = params.header.end; } else { this.id = params.id; } if (!this.id) { throw Error('An ID must be supplied when creating an assembler.'); } this._onCompleted = params.onCompleted; } /** * Retrieves the assembler's payload as a stream. * * @returns A [SubscribableStream](xref:botframework-streaming.SubscribableStream) of the assembler's payload. */ getPayloadStream(): SubscribableStream { if (!this.stream) { this.stream = this.createPayloadStream(); } return this.stream; } /** * The action the assembler executes when new bytes are received on the incoming stream. * * @param header The stream's Header. * @param stream The incoming stream being assembled. * @param _contentLength The length of the stream, if finite. */ onReceive(header: IHeader, stream: SubscribableStream, _contentLength: number): void { this.end = header.end; if (header.payloadType === PayloadTypes.response || header.payloadType === PayloadTypes.request) { this.process(stream).then().catch(); } else if (header.end) { stream.end(); } } /** * Closes the assembler. */ close(): void { this.streamManager.closeStream(this.id); } /** * Creates a new [SubscribableStream](xref:botframework-streaming.SubscribableStream) instance. * * @returns The new stream ready for consumption. */ private createPayloadStream(): SubscribableStream { return new SubscribableStream(); } private payloadFromJson<T>(json: string): T { return JSON.parse(json.charCodeAt(0) === this._byteOrderMark ? json.slice(1) : json) as T; } private stripBOM(input: string): string { return input.charCodeAt(0) === this._byteOrderMark ? input.slice(1) : input; } private async process(stream: SubscribableStream): Promise<void> { const streamData: Buffer = stream.read(stream.length) as Buffer; if (!streamData) { return; } const streamDataAsString = streamData.toString(this._utf); if (this.payloadType === PayloadTypes.request) { await this.processRequest(streamDataAsString); } else if (this.payloadType === PayloadTypes.response) { await this.processResponse(streamDataAsString); } } private async processResponse(streamDataAsString: string): Promise<void> { const responsePayload = this.payloadFromJson<IResponsePayload>(this.stripBOM(streamDataAsString)); const receiveResponse: IReceiveResponse = { streams: [], statusCode: responsePayload.statusCode }; await this.processStreams(responsePayload, receiveResponse); } private async processRequest(streamDataAsString: string): Promise<void> { const requestPayload = this.payloadFromJson<IRequestPayload>(streamDataAsString); const receiveRequest: IReceiveRequest = { streams: [], path: requestPayload.path, verb: requestPayload.verb }; await this.processStreams(requestPayload, receiveRequest); } private async processStreams( responsePayload: IResponsePayload | IRequestPayload, receiveResponse: IReceiveResponse | IReceiveRequest, ): Promise<void> { responsePayload.streams?.forEach((responseStream) => { // There was a bug in how streams were handled. In .NET, the StreamDescription definiton mapped the // "type" JSON property to `contentType`, but in Typescript there was no mapping. This is a workaround // to resolve this inconsistency. // // .NET code: // https://github.com/microsoft/botbuilder-dotnet/blob/a79036ddf6625ec3fd68a6f7295886eb7831bc1c/libraries/Microsoft.Bot.Streaming/Payloads/Models/StreamDescription.cs#L28-L29 const contentType = (responseStream as unknown as Record<string, string>).type ?? responseStream.contentType; const contentAssembler = this.streamManager.getPayloadAssembler(responseStream.id); contentAssembler.payloadType = contentType; contentAssembler.contentLength = responseStream.length; receiveResponse.streams.push(new ContentStream(responseStream.id, contentAssembler)); }); await this._onCompleted(this.id, receiveResponse); } }