"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.SnapshotV1 = void 0;
const common_utils_1 = require("@fluidframework/common-utils");
const telemetry_utils_1 = require("@fluidframework/telemetry-utils");
const runtime_utils_1 = require("@fluidframework/runtime-utils");
const constants_1 = require("./constants");
const properties_1 = require("./properties");
const snapshotChunks_1 = require("./snapshotChunks");
const snapshotlegacy_1 = require("./snapshotlegacy");
const mergeTreeNodeWalk_1 = require("./mergeTreeNodeWalk");
class SnapshotV1 {
    constructor(mergeTree, logger, getLongClientId, filename, onCompletion) {
        var _a, _b;
        this.mergeTree = mergeTree;
        this.getLongClientId = getLongClientId;
        this.filename = filename;
        this.onCompletion = onCompletion;
        this.logger = telemetry_utils_1.ChildLogger.create(logger, "Snapshot");
        this.chunkSize = (_b = (_a = mergeTree === null || mergeTree === void 0 ? void 0 : mergeTree.options) === null || _a === void 0 ? void 0 : _a.mergeTreeSnapshotChunkSize) !== null && _b !== void 0 ? _b : SnapshotV1.chunkSize;
        const { currentSeq, minSeq } = mergeTree.collabWindow;
        this.header = {
            minSequenceNumber: minSeq,
            sequenceNumber: currentSeq,
            orderedChunkMetadata: [],
            totalLength: 0,
            totalSegmentCount: 0,
        };
        this.segments = [];
        this.segmentLengths = [];
        this.attributionCollections = [];
    }
    getSeqLengthSegs(allSegments, allLengths, attributionCollections, approxSequenceLength, startIndex = 0) {
        var _a;
        const segments = [];
        const collections = [];
        let length = 0;
        let segmentCount = 0;
        let hasAttribution = false;
        while (length < approxSequenceLength && startIndex + segmentCount < allSegments.length) {
            const pseg = allSegments[startIndex + segmentCount];
            segments.push(pseg);
            length += allLengths[startIndex + segmentCount];
            if (attributionCollections[startIndex + segmentCount]) {
                hasAttribution = true;
                collections.push({
                    attribution: attributionCollections[startIndex + segmentCount],
                    cachedLength: allLengths[startIndex + segmentCount],
                });
            }
            segmentCount++;
        }
        const attributionSerializer = (_a = this.mergeTree.attributionPolicy) === null || _a === void 0 ? void 0 : _a.serializer;
        (0, common_utils_1.assert)(!hasAttribution || attributionSerializer !== undefined, 0x55a /* attribution serializer must be provided when there are segments with attribution. */);
        return {
            version: "1",
            segmentCount,
            length,
            segments,
            startIndex,
            headerMetadata: undefined,
            attribution: hasAttribution
                ? attributionSerializer === null || attributionSerializer === void 0 ? void 0 : attributionSerializer.serializeAttributionCollections(collections)
                : undefined,
        };
    }
    /**
     * Emits the snapshot to an ISummarizeResult. If provided the optional IFluidSerializer will be used when
     * serializing the summary data rather than JSON.stringify.
     */
    emit(serializer, bind) {
        const chunks = [];
        this.header.totalSegmentCount = 0;
        this.header.totalLength = 0;
        do {
            const chunk = this.getSeqLengthSegs(this.segments, this.segmentLengths, this.attributionCollections, this.chunkSize, this.header.totalSegmentCount);
            chunks.push(chunk);
            this.header.totalSegmentCount += chunk.segmentCount;
            this.header.totalLength += chunk.length;
        } while (this.header.totalSegmentCount < this.segments.length);
        // The do while loop should have added at least one chunk
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const headerChunk = chunks.shift();
        headerChunk.headerMetadata = this.header;
        headerChunk.headerMetadata.orderedChunkMetadata = [{ id: snapshotlegacy_1.SnapshotLegacy.header }];
        const blobs = [];
        chunks.forEach((chunk, index) => {
            const id = `${snapshotlegacy_1.SnapshotLegacy.body}_${index}`;
            this.header.orderedChunkMetadata.push({ id });
            blobs.push([
                id,
                (0, snapshotChunks_1.serializeAsMaxSupportedVersion)(id, chunk, this.logger, this.mergeTree.options, serializer, bind),
            ]);
        });
        const builder = new runtime_utils_1.SummaryTreeBuilder();
        builder.addBlob(snapshotlegacy_1.SnapshotLegacy.header, (0, snapshotChunks_1.serializeAsMaxSupportedVersion)(snapshotlegacy_1.SnapshotLegacy.header, headerChunk, this.logger, this.mergeTree.options, serializer, bind));
        blobs.forEach((value) => {
            builder.addBlob(value[0], value[1]);
        });
        return builder.getSummaryTree();
    }
    extractSync() {
        const mergeTree = this.mergeTree;
        const minSeq = this.header.minSequenceNumber;
        // Helper to add the given `MergeTreeChunkV0SegmentSpec` to the snapshot.
        const pushSegRaw = (json, length, attribution) => {
            this.segments.push(json);
            this.segmentLengths.push(length);
            if (attribution) {
                this.attributionCollections.push(attribution);
            }
        };
        // Helper to serialize the given `segment` and add it to the snapshot (if a segment is provided).
        const pushSeg = (segment) => {
            if (segment) {
                if (segment.properties !== undefined &&
                    Object.keys(segment.properties).length === 0) {
                    segment.properties = undefined;
                    segment.propertyManager = undefined;
                }
                pushSegRaw(segment.toJSONObject(), segment.cachedLength, segment.attribution);
            }
        };
        let prev;
        const extractSegment = (segment) => {
            var _a;
            // Elide segments that do not need to be included in the snapshot.  A segment may be elided if
            // either condition is true:
            //   a) The segment has not yet been ACKed.  We do not need to snapshot unACKed segments because
            //      there is a pending insert op that will deliver the segment on reconnection.
            //   b) The segment was removed at or below the MSN.  Pending ops can no longer reference this
            //      segment, and therefore we can discard it.
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            if (segment.seq === constants_1.UnassignedSequenceNumber || segment.removedSeq <= minSeq) {
                return true;
            }
            // Next determine if the snapshot needs to preserve information required for merging the segment
            // (seq, client, etc.)  This information is only needed if the segment is above the MSN (and doesn't
            // have a pending remove.)
            if (
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            segment.seq <= minSeq && // Segment is below the MSN, and...
                (segment.removedSeq === undefined || // .. Segment has not been removed, or...
                    segment.removedSeq === constants_1.UnassignedSequenceNumber) // .. Removal op to be delivered on reconnect
            ) {
                // This segment is below the MSN, which means that future ops will not reference it.  Attempt to
                // coalesce the new segment with the previous (if any).
                if (!prev) {
                    // We do not have a previous candidate for coalescing.  Make the current segment the new candidate.
                    prev = segment;
                }
                else if (prev.canAppend(segment) &&
                    (0, properties_1.matchProperties)(prev.properties, segment.properties)) {
                    // We have a compatible pair.  Replace `prev` with the coalesced segment.  Clone to avoid
                    // modifying the segment instances currently in the MergeTree.
                    prev = prev.clone();
                    prev.append(segment.clone());
                }
                else {
                    // The segment pair could not be coalesced.  Record the `prev` segment in the snapshot
                    // and make the current segment the new candidate for coalescing.
                    pushSeg(prev);
                    prev = segment;
                }
            }
            else {
                // This segment needs to preserve its metadata as it may be referenced by future ops.  It's ineligible
                // for coalescing, so emit the 'prev' segment now (if any).
                pushSeg(prev);
                prev = undefined;
                if (segment.properties !== undefined &&
                    Object.keys(segment.properties).length === 0) {
                    segment.properties = undefined;
                    segment.propertyManager = undefined;
                }
                const raw = { json: segment.toJSONObject() };
                // If the segment insertion is above the MSN, record the insertion merge info.
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                if (segment.seq > minSeq) {
                    raw.seq = segment.seq;
                    raw.client = this.getLongClientId(segment.clientId);
                }
                // We have already dispensed with removed segments below the MSN and removed segments with unassigned
                // sequence numbers.  Any remaining removal info should be preserved.
                if (segment.removedSeq !== undefined) {
                    (0, common_utils_1.assert)(segment.removedSeq !== constants_1.UnassignedSequenceNumber &&
                        segment.removedSeq > minSeq, 0x065 /* "On removal info preservation, segment has invalid removed sequence number!" */);
                    raw.removedSeq = segment.removedSeq;
                    // back compat for when we split overlap and removed client
                    raw.removedClient =
                        segment.removedClientIds !== undefined
                            ? this.getLongClientId(segment.removedClientIds[0])
                            : undefined;
                    raw.removedClientIds = (_a = segment.removedClientIds) === null || _a === void 0 ? void 0 : _a.map((id) => this.getLongClientId(id));
                }
                // Sanity check that we are preserving either the seq < minSeq or a removed segment's info.
                (0, common_utils_1.assert)((raw.seq !== undefined && raw.client !== undefined) ||
                    (raw.removedSeq !== undefined && raw.removedClient !== undefined), 0x066 /* "Corrupted preservation of segment metadata!" */);
                // Record the segment with it's required metadata.
                pushSegRaw(raw, segment.cachedLength, segment.attribution);
            }
            return true;
        };
        (0, mergeTreeNodeWalk_1.walkAllChildSegments)(mergeTree.root, extractSegment);
        // If the last segment in the walk was coalescable, push it now.
        pushSeg(prev);
        return this.segments;
    }
    static async loadChunk(storage, path, logger, options, serializer) {
        const blob = await storage.readBlob(path);
        const chunkAsString = (0, common_utils_1.bufferToString)(blob, "utf8");
        return SnapshotV1.processChunk(path, chunkAsString, logger, options, serializer);
    }
    static processChunk(path, chunk, logger, options, serializer) {
        const chunkObj = serializer ? serializer.parse(chunk) : JSON.parse(chunk);
        return (0, snapshotChunks_1.toLatestVersion)(path, chunkObj, logger, options);
    }
}
exports.SnapshotV1 = SnapshotV1;
// Split snapshot into two entries - headers (small) and body (overflow) for faster loading initial content
// Please note that this number has no direct relationship to anything other than size of raw text (characters).
// As we produce json for the blob (and then send over the wire compressed), this number
// is really hard to correlate with any actual metric that matters (like bytes over the wire).
// For test with small number of chunks it would be closer to blob size,
// for very chunky text, blob size can easily be 4x-8x of that number.
SnapshotV1.chunkSize = 10000;
//# sourceMappingURL=snapshotV1.js.map