"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Outbox = exports.getLongStack = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const batchManager_js_1 = require("./batchManager.js");
/**
 * Temporarily increase the stack limit while executing the provided action.
 * If a negative value is provided for `length`, no stack frames will be collected.
 * If Infinity is provided, all frames will be collected.
 *
 * ADO:4663 - add this to the common packages.
 *
 * @param action - action which returns an error
 * @param length - number of stack frames to collect, 50 if unspecified.
 * @returns the result of the action provided
 */
function getLongStack(action, length = 50) {
    const errorObj = Error;
    if (
    /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
    // ?? is not logically equivalent when the first clause returns false.
    (Object.getOwnPropertyDescriptor(errorObj, "stackTraceLimit") ||
        Object.getOwnPropertyDescriptor(Object.getPrototypeOf(errorObj), "stackTraceLimit"))?.writable !== true
    /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */
    ) {
        return action();
    }
    const originalStackTraceLimit = errorObj.stackTraceLimit;
    try {
        errorObj.stackTraceLimit = length;
        return action();
    }
    finally {
        errorObj.stackTraceLimit = originalStackTraceLimit;
    }
}
exports.getLongStack = getLongStack;
class Outbox {
    constructor(params) {
        this.params = params;
        this.batchRebasesToReport = 5;
        this.rebasing = false;
        /**
         * Track the number of ops which were detected to have a mismatched
         * reference sequence number, in order to self-throttle the telemetry events.
         *
         * This should be removed as part of ADO:2322
         */
        this.maxMismatchedOpsToReport = 3;
        this.mismatchedOpsReported = 0;
        this.mc = (0, internal_2.createChildMonitoringContext)({ logger: params.logger, namespace: "Outbox" });
        const isCompressionEnabled = this.params.config.compressionOptions.minimumBatchSizeInBytes !==
            Number.POSITIVE_INFINITY;
        // We need to allow infinite size batches if we enable compression
        const hardLimit = isCompressionEnabled ? Infinity : this.params.config.maxBatchSizeInBytes;
        this.mainBatch = new batchManager_js_1.BatchManager({ hardLimit, canRebase: true });
        this.blobAttachBatch = new batchManager_js_1.BatchManager({ hardLimit, canRebase: true });
        this.idAllocationBatch = new batchManager_js_1.BatchManager({ hardLimit, canRebase: false });
    }
    get messageCount() {
        return this.mainBatch.length + this.blobAttachBatch.length + this.idAllocationBatch.length;
    }
    get isEmpty() {
        return this.messageCount === 0;
    }
    /**
     * If we detect that the reference sequence number of the incoming message does not match
     * what was already in the batch managers, this means that batching has been interrupted so
     * we will flush the accumulated messages to account for that and create a new batch with the new
     * message as the first message.
     */
    maybeFlushPartialBatch() {
        const mainBatchSeqNums = this.mainBatch.sequenceNumbers;
        const blobAttachSeqNums = this.blobAttachBatch.sequenceNumbers;
        const idAllocSeqNums = this.idAllocationBatch.sequenceNumbers;
        (0, internal_1.assert)(this.params.config.disablePartialFlush ||
            ((0, batchManager_js_1.sequenceNumbersMatch)(mainBatchSeqNums, blobAttachSeqNums) &&
                (0, batchManager_js_1.sequenceNumbersMatch)(mainBatchSeqNums, idAllocSeqNums)), 0x58d /* Reference sequence numbers from both batches must be in sync */);
        const currentSequenceNumbers = this.params.getCurrentSequenceNumbers();
        if ((0, batchManager_js_1.sequenceNumbersMatch)(mainBatchSeqNums, currentSequenceNumbers) &&
            (0, batchManager_js_1.sequenceNumbersMatch)(blobAttachSeqNums, currentSequenceNumbers) &&
            (0, batchManager_js_1.sequenceNumbersMatch)(idAllocSeqNums, currentSequenceNumbers)) {
            // The reference sequence numbers are stable, there is nothing to do
            return;
        }
        if (++this.mismatchedOpsReported <= this.maxMismatchedOpsToReport) {
            this.mc.logger.sendTelemetryEvent({
                category: this.params.config.disablePartialFlush ? "error" : "generic",
                eventName: "ReferenceSequenceNumberMismatch",
                mainReferenceSequenceNumber: mainBatchSeqNums.referenceSequenceNumber,
                mainClientSequenceNumber: mainBatchSeqNums.clientSequenceNumber,
                blobAttachReferenceSequenceNumber: blobAttachSeqNums.referenceSequenceNumber,
                blobAttachClientSequenceNumber: blobAttachSeqNums.clientSequenceNumber,
                currentReferenceSequenceNumber: currentSequenceNumbers.referenceSequenceNumber,
                currentClientSequenceNumber: currentSequenceNumbers.clientSequenceNumber,
            }, getLongStack(() => new internal_2.UsageError("Submission of an out of order message")));
        }
        if (!this.params.config.disablePartialFlush) {
            this.flushAll();
        }
    }
    submit(message) {
        this.maybeFlushPartialBatch();
        this.addMessageToBatchManager(this.mainBatch, message);
    }
    submitBlobAttach(message) {
        this.maybeFlushPartialBatch();
        this.addMessageToBatchManager(this.blobAttachBatch, message);
        // If compression is enabled, we will always successfully receive
        // blobAttach ops and compress then send them at the next JS turn, regardless
        // of the overall size of the accumulated ops in the batch.
        // However, it is more efficient to flush these ops faster, preferably
        // after they reach a size which would benefit from compression.
        if (this.blobAttachBatch.contentSizeInBytes >=
            this.params.config.compressionOptions.minimumBatchSizeInBytes) {
            this.flushInternal(this.blobAttachBatch);
        }
    }
    submitIdAllocation(message) {
        this.maybeFlushPartialBatch();
        this.addMessageToBatchManager(this.idAllocationBatch, message);
    }
    addMessageToBatchManager(batchManager, message) {
        if (!batchManager.push(message, this.isContextReentrant(), this.params.getCurrentSequenceNumbers().clientSequenceNumber)) {
            throw new internal_2.GenericError("BatchTooLarge", /* error */ undefined, {
                opSize: message.contents?.length ?? 0,
                batchSize: batchManager.contentSizeInBytes,
                count: batchManager.length,
                limit: batchManager.options.hardLimit,
            });
        }
    }
    flush() {
        if (this.isContextReentrant()) {
            const error = new internal_2.UsageError("Flushing is not supported inside DDS event handlers");
            this.params.closeContainer(error);
            throw error;
        }
        this.flushAll();
    }
    flushAll() {
        this.flushInternal(this.idAllocationBatch);
        this.flushInternal(this.blobAttachBatch, true /* disableGroupedBatching */);
        this.flushInternal(this.mainBatch);
    }
    flushInternal(batchManager, disableGroupedBatching = false) {
        if (batchManager.empty) {
            return;
        }
        const rawBatch = batchManager.popBatch();
        const shouldGroup = !disableGroupedBatching && this.params.groupingManager.shouldGroup(rawBatch);
        if (batchManager.options.canRebase && rawBatch.hasReentrantOps === true && shouldGroup) {
            (0, internal_1.assert)(!this.rebasing, 0x6fa /* A rebased batch should never have reentrant ops */);
            // If a batch contains reentrant ops (ops created as a result from processing another op)
            // it needs to be rebased so that we can ensure consistent reference sequence numbers
            // and eventual consistency at the DDS level.
            this.rebase(rawBatch, batchManager);
            return;
        }
        // Did we disconnect? (i.e. is shouldSend false?)
        // If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
        // Because flush() is a task that executes async (on clean stack), we can get here in disconnected state.
        if (this.params.shouldSend()) {
            const processedBatch = this.compressBatch(shouldGroup ? this.params.groupingManager.groupBatch(rawBatch) : rawBatch);
            this.sendBatch(processedBatch);
        }
        this.persistBatch(rawBatch.content);
    }
    /**
     * Rebases a batch. All the ops in the batch are resubmitted to the runtime and
     * they will end up back in the same batch manager they were flushed from and subsequently flushed.
     *
     * @param rawBatch - the batch to be rebased
     */
    rebase(rawBatch, batchManager) {
        (0, internal_1.assert)(!this.rebasing, 0x6fb /* Reentrancy */);
        (0, internal_1.assert)(batchManager.options.canRebase, 0x9a7 /* BatchManager does not support rebase */);
        this.rebasing = true;
        for (const message of rawBatch.content) {
            this.params.reSubmit({
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                content: message.contents,
                localOpMetadata: message.localOpMetadata,
                opMetadata: message.metadata,
            });
        }
        if (this.batchRebasesToReport > 0) {
            this.mc.logger.sendTelemetryEvent({
                eventName: "BatchRebase",
                length: rawBatch.content.length,
                referenceSequenceNumber: rawBatch.referenceSequenceNumber,
            }, new internal_2.UsageError("BatchRebase"));
            this.batchRebasesToReport--;
        }
        this.flushInternal(batchManager);
        this.rebasing = false;
    }
    isContextReentrant() {
        return this.params.opReentrancy() && !this.rebasing;
    }
    /**
     * As necessary and enabled, compresses and chunks the given batch.
     *
     * @remarks - If chunking happens, a side effect here is that 1 or more chunks are queued immediately for sending in next JS turn.
     *
     * @param batch - Raw or Grouped batch to consider for compression/chunking
     * @returns Either (A) the original batch, (B) a compressed batch (same length as original),
     * or (C) a batch containing the last chunk (plus empty placeholders from compression if applicable).
     */
    compressBatch(batch) {
        if (batch.content.length === 0 ||
            this.params.config.compressionOptions === undefined ||
            this.params.config.compressionOptions.minimumBatchSizeInBytes >
                batch.contentSizeInBytes ||
            this.params.submitBatchFn === undefined) {
            // Nothing to do if the batch is empty or if compression is disabled or not supported, or if we don't need to compress
            return batch;
        }
        const compressedBatch = this.params.compressor.compressBatch(batch);
        if (this.params.splitter.isBatchChunkingEnabled) {
            return compressedBatch.contentSizeInBytes <= this.params.splitter.chunkSizeInBytes
                ? compressedBatch
                : this.params.splitter.splitFirstBatchMessage(compressedBatch);
        }
        if (compressedBatch.contentSizeInBytes >= this.params.config.maxBatchSizeInBytes) {
            throw new internal_2.GenericError("BatchTooLarge", /* error */ undefined, {
                batchSize: batch.contentSizeInBytes,
                compressedBatchSize: compressedBatch.contentSizeInBytes,
                count: compressedBatch.content.length,
                limit: this.params.config.maxBatchSizeInBytes,
                chunkingEnabled: this.params.splitter.isBatchChunkingEnabled,
                compressionOptions: JSON.stringify(this.params.config.compressionOptions),
                socketSize: (0, batchManager_js_1.estimateSocketSize)(batch),
            });
        }
        return compressedBatch;
    }
    /**
     * Sends the batch object to the container context to be sent over the wire.
     *
     * @param batch - batch to be sent
     */
    sendBatch(batch) {
        const length = batch.content.length;
        if (length === 0) {
            return;
        }
        const socketSize = (0, batchManager_js_1.estimateSocketSize)(batch);
        if (socketSize >= this.params.config.maxBatchSizeInBytes) {
            this.mc.logger.sendPerformanceEvent({
                eventName: "LargeBatch",
                length: batch.content.length,
                sizeInBytes: batch.contentSizeInBytes,
                socketSize,
            });
        }
        if (this.params.submitBatchFn === undefined) {
            // Legacy path - supporting old loader versions. Can be removed only when LTS moves above
            // version that has support for batches (submitBatchFn)
            (0, internal_1.assert)(batch.content[0].compression === undefined, 0x5a6 /* Compression should not have happened if the loader does not support it */);
            this.params.legacySendBatchFn(batch);
        }
        else {
            (0, internal_1.assert)(batch.referenceSequenceNumber !== undefined, 0x58e /* Batch must not be empty */);
            this.params.submitBatchFn(batch.content.map((message) => ({
                contents: message.contents,
                metadata: message.metadata,
                compression: message.compression,
                referenceSequenceNumber: message.referenceSequenceNumber,
            })), batch.referenceSequenceNumber);
        }
    }
    persistBatch(batch) {
        // Let the PendingStateManager know that a message was submitted.
        // In future, need to shift toward keeping batch as a whole!
        for (const message of batch) {
            this.params.pendingStateManager.onSubmitMessage(
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            message.contents, message.referenceSequenceNumber, message.localOpMetadata, message.metadata);
        }
    }
    checkpoint() {
        // This variable is declared with a specific type so that we have a standard import of the IBatchCheckpoint type.
        // When the type is inferred, the generated .d.ts uses a dynamic import which doesn't resolve.
        const mainBatch = this.mainBatch.checkpoint();
        return {
            mainBatch,
            idAllocationBatch: this.idAllocationBatch.checkpoint(),
            blobAttachBatch: this.blobAttachBatch.checkpoint(),
        };
    }
}
exports.Outbox = Outbox;
//# sourceMappingURL=outbox.js.map