"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpDecompressor = void 0;
const client_utils_1 = require("@fluid-internal/client-utils");
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const lz4js_1 = require("lz4js");
const compressionDefinitions_js_1 = require("../compressionDefinitions.js");
/**
 * State machine that "unrolls" contents of compressed batches of ops after decompressing them.
 * This class relies on some implicit contracts defined below:
 * 1. A compressed batch will have its first message with batch metadata set to true and compressed set to true
 * 2. Messages in the middle of a compressed batch will have neither batch metadata nor the compression property set
 * 3. The final message of a batch will have batch metadata set to false
 * 4. An individually compressed op will have undefined batch metadata and compression set to true
 *
 * Compressed batches from current code are always a single message but this class needs to handle a legacy compressed batch with multiple messages
 * because we need that functionality for back compat.
 */
class OpDecompressor {
    constructor(logger) {
        this.activeBatch = false;
        this.processedCount = 0;
        /**
         * Is the decompressed and stored batch only comprised of a single message
         */
        this.isSingleMessageBatch = false;
        this.logger = (0, internal_2.createChildLogger)({ logger, namespace: "OpDecompressor" });
    }
    isCompressedMessage(message) {
        if (message.compression === compressionDefinitions_js_1.CompressionAlgorithms.lz4) {
            return true;
        }
        /**
         * Back-compat self healing mechanism for ADO:3538, as loaders from
         * version client_v2.0.0-internal.1.2.0 to client_v2.0.0-internal.2.2.0 do not
         * support adding the proper compression metadata to compressed messages submitted
         * by the runtime. Should be removed after the loader reaches sufficient saturation
         * for a version greater or equal than client_v2.0.0-internal.2.2.0.
         *
         * The condition holds true for compressed messages, regardless of metadata. We are ultimately
         * looking for a message with a single property `packedContents` inside `contents`, of type 'string'
         * with a base64 encoded value.
         */
        try {
            if (message.contents !== null &&
                typeof message.contents === "object" &&
                Object.keys(message.contents).length === 1 &&
                typeof message.contents.packedContents ===
                    "string" &&
                message.contents.packedContents.length > 0 &&
                client_utils_1.IsoBuffer.from(message.contents.packedContents, "base64").toString("base64") === message.contents.packedContents) {
                this.logger.sendTelemetryEvent({
                    eventName: "LegacyCompression",
                    type: message.type,
                    batch: message.metadata?.batch,
                });
                return true;
            }
        }
        catch {
            return false;
        }
        return false;
    }
    get currentlyUnrolling() {
        return this.activeBatch;
    }
    /**
     * Decompress the given compressed message and store it to be subsequently unrolled.
     * The stored message will be of type `any[]` where each element represents a message's `contents`
     */
    decompressAndStore(message) {
        (0, internal_1.assert)(message.compression === undefined || message.compression === compressionDefinitions_js_1.CompressionAlgorithms.lz4, 0x511 /* Only lz4 compression is supported */);
        (0, internal_1.assert)(this.isCompressedMessage(message), 0x940 /* provided message should be compressed */);
        (0, internal_1.assert)(this.activeBatch === false, 0x4b8 /* shouldn't have multiple active batches */);
        this.activeBatch = true;
        const batchMetadata = message.metadata?.batch;
        if (batchMetadata === undefined) {
            this.isSingleMessageBatch = true;
        }
        else {
            (0, internal_1.assert)(batchMetadata === true, 0x941 /* invalid batch metadata */);
        }
        const contents = client_utils_1.IsoBuffer.from(message.contents.packedContents, "base64");
        const decompressedMessage = (0, lz4js_1.decompress)(contents);
        const intoString = (0, client_utils_1.Uint8ArrayToString)(decompressedMessage);
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
        this.rootMessageContents = JSON.parse(intoString);
    }
    /**
     * Unroll the next message from the decompressed content provided to {@link decompressAndStore}
     * @returns the unrolled `ISequencedDocumentMessage`
     */
    unroll(message) {
        (0, internal_1.assert)(this.currentlyUnrolling, 0x942 /* not currently unrolling */);
        (0, internal_1.assert)(this.rootMessageContents !== undefined, 0x943 /* missing rootMessageContents */);
        (0, internal_1.assert)(
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
        this.rootMessageContents.length > this.processedCount, 0x944 /* no more content to unroll */);
        const batchMetadata = message.metadata?.batch;
        if (batchMetadata === false || this.isSingleMessageBatch) {
            // End of compressed batch
            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
            const returnMessage = newMessage(message, this.rootMessageContents[this.processedCount]);
            this.activeBatch = false;
            this.isSingleMessageBatch = false;
            this.rootMessageContents = undefined;
            this.processedCount = 0;
            return returnMessage;
        }
        else if (batchMetadata === true) {
            // Start of compressed batch
            // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
            return newMessage(message, this.rootMessageContents[this.processedCount++]);
        }
        (0, internal_1.assert)(batchMetadata === undefined, 0x945 /* invalid batch metadata */);
        (0, internal_1.assert)(message.contents === undefined, 0x512 /* Expecting empty message */);
        // Continuation of compressed batch
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
        return newMessage(message, this.rootMessageContents[this.processedCount++]);
    }
}
exports.OpDecompressor = OpDecompressor;
// We should not be mutating the input message nor its metadata
const newMessage = (originalMessage, contents) => ({
    ...originalMessage,
    contents,
    compression: undefined,
    // TODO: It should already be the case that we're not modifying any metadata, not clear if/why this shallow clone should be required.
    metadata: originalMessage.metadata === undefined ? undefined : { ...originalMessage.metadata },
});
//# sourceMappingURL=opDecompressor.js.map