| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Common from '../../core/common/common.js'; |
| import * as Platform from '../../core/platform/platform.js'; |
| |
| import {ContentData, type ContentDataOrError} from './ContentData.js'; |
| |
| /** |
| * Usage of this class is mostly intended for content that is never "complete". |
| * E.g. streaming XHR/fetch requests. |
| * |
| * Due to the streaming nature this class only supports base64-encoded binary data. |
| * Decoding to text only happens on-demand by clients. This ensures that at most we have |
| * incomplete unicode at the end and not in-between chunks. |
| */ |
| export class StreamingContentData extends Common.ObjectWrapper.ObjectWrapper<EventTypes> { |
| readonly mimeType: string; |
| readonly #charset?: string; |
| |
| readonly #disallowStreaming: boolean; |
| |
| #chunks: string[] = []; |
| #contentData?: ContentData; |
| |
| private constructor(mimeType: string, charset?: string, initialContent?: ContentData) { |
| super(); |
| this.mimeType = mimeType; |
| this.#charset = charset; |
| this.#disallowStreaming = Boolean(initialContent && !initialContent.createdFromBase64); |
| this.#contentData = initialContent; |
| } |
| |
| /** |
| * Creates a new StreamingContentData with the given MIME type/charset. |
| */ |
| static create(mimeType: string, charset?: string): StreamingContentData { |
| return new StreamingContentData(mimeType, charset); |
| } |
| |
| /** |
| * Creates a new StringContentData from an existing ContentData instance. |
| * |
| * Calling `addChunk` is on the resulting `StreamingContentData` is illegal if |
| * `content` was not created from base64 data. The reason is that JavaScript TextEncoder |
| * only supports UTF-8. We can't convert text with arbitrary encoding back to base64 for concatenation. |
| */ |
| static from(content: ContentData): StreamingContentData { |
| return new StreamingContentData(content.mimeType, content.charset, content); |
| } |
| |
| /** @returns true, if this `ContentData` was constructed from text content or the mime type indicates text that can be decoded */ |
| get isTextContent(): boolean { |
| if (this.#contentData) { |
| return this.#contentData.isTextContent; |
| } |
| return Platform.MimeType.isTextType(this.mimeType); |
| } |
| |
| /** @param chunk base64 encoded data */ |
| addChunk(chunk: string): void { |
| if (this.#disallowStreaming) { |
| throw new Error('Cannot add base64 data to a text-only ContentData.'); |
| } |
| |
| this.#chunks.push(chunk); |
| this.dispatchEventToListeners(Events.CHUNK_ADDED, {content: this, chunk}); |
| } |
| |
| /** @returns An immutable ContentData with all the bytes received so far */ |
| content(): ContentData { |
| if (this.#contentData && this.#chunks.length === 0) { |
| return this.#contentData; |
| } |
| |
| const initialBase64 = this.#contentData?.base64 ?? ''; |
| const base64Content = |
| this.#chunks.reduce((acc, chunk) => Platform.StringUtilities.concatBase64(acc, chunk), initialBase64); |
| this.#contentData = new ContentData(base64Content, /* isBase64=*/ true, this.mimeType, this.#charset); |
| this.#chunks = []; |
| return this.#contentData; |
| } |
| } |
| |
| export type StreamingContentDataOrError = StreamingContentData|{error: string}; |
| |
| export const isError = function(contentDataOrError: StreamingContentDataOrError): contentDataOrError is { |
| error: |
| string, |
| } { |
| return 'error' in contentDataOrError; |
| }; |
| |
| export const asContentDataOrError = function(contentDataOrError: StreamingContentDataOrError): ContentDataOrError { |
| if (isError(contentDataOrError)) { |
| return contentDataOrError; |
| } |
| return contentDataOrError.content(); |
| }; |
| |
| export const enum Events { |
| CHUNK_ADDED = 'ChunkAdded', |
| } |
| |
| export interface EventTypes { |
| [Events.CHUNK_ADDED]: {content: StreamingContentData, chunk: string}; |
| } |