"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = void 0;
const protocol_definitions_1 = require("@fluidframework/protocol-definitions");
const common_utils_1 = require("@fluidframework/common-utils");
const telemetry_utils_1 = require("@fluidframework/telemetry-utils");
const collections_1 = require("./collections");
const constants_1 = require("./constants");
const localReference_1 = require("./localReference");
const mergeTree_1 = require("./mergeTree");
const opBuilder_1 = require("./opBuilder");
const ops_1 = require("./ops");
const snapshotlegacy_1 = require("./snapshotlegacy");
const snapshotLoader_1 = require("./snapshotLoader");
const textSegment_1 = require("./textSegment");
const snapshotV1_1 = require("./snapshotV1");
function elapsedMicroseconds(trace) {
    return trace.trace().duration * 1000;
}
class Client {
    constructor(
    // Passing this callback would be unnecessary if Client were merged with SharedSegmentSequence
    specToSegment, logger, options) {
        this.specToSegment = specToSegment;
        this.logger = logger;
        this.measureOps = false;
        this.accumTime = 0;
        this.localTime = 0;
        this.localOps = 0;
        this.accumWindowTime = 0;
        this.accumWindow = 0;
        this.accumOps = 0;
        this.maxWindowTime = 0;
        this.clientNameToIds = new collections_1.RedBlackTree(mergeTree_1.compareStrings);
        this.shortClientIdMap = [];
        this.pendingConsensus = new Map();
        this.mergeTree = new mergeTree_1.MergeTree(options);
    }
    get mergeTreeDeltaCallback() { return this.mergeTree.mergeTreeDeltaCallback; }
    set mergeTreeDeltaCallback(callback) {
        this.mergeTree.mergeTreeDeltaCallback = callback;
    }
    get mergeTreeMaintenanceCallback() {
        return this.mergeTree.mergeTreeMaintenanceCallback;
    }
    set mergeTreeMaintenanceCallback(callback) {
        this.mergeTree.mergeTreeMaintenanceCallback = callback;
    }
    /**
     * The merge tree maintains a queue of segment groups for each local operation.
     * These segment groups track segments modified by an operation.
     * This method peeks the tail of that queue, and returns the segments groups there.
     * It is used to get the segment group(s) for the previous operations.
     * @param count - The number segment groups to get peek from the tail of the queue. Default 1.
     */
    peekPendingSegmentGroups(count = 1) {
        var _a, _b;
        if (count === 1) {
            return (_a = this.mergeTree.pendingSegments) === null || _a === void 0 ? void 0 : _a.last();
        }
        let taken = 0;
        return (_b = this.mergeTree.pendingSegments) === null || _b === void 0 ? void 0 : _b.some(() => {
            if (taken < count) {
                taken++;
                return true;
            }
            return false;
        }, true);
    }
    /**
     * Annotate a marker and call the callback on consensus.
     * @param marker - The marker to annotate
     * @param props - The properties to annotate the marker with
     * @param consensusCallback - The callback called when consensus is reached
     * @returns The annotate op if valid, otherwise undefined
     */
    annotateMarkerNotifyConsensus(marker, props, consensusCallback) {
        const combiningOp = {
            name: "consensus",
        };
        const annotateOp = this.annotateMarker(marker, props, combiningOp);
        if (annotateOp) {
            const consensusInfo = {
                callback: consensusCallback,
                marker,
            };
            this.pendingConsensus.set(marker.getId(), consensusInfo);
            return annotateOp;
        }
        else {
            return undefined;
        }
    }
    /**
     * Annotates the markers with the provided properties
     * @param marker - The marker to annotate
     * @param props - The properties to annotate the marker with
     * @param combiningOp - Optional. Specifies how to combine values for the property, such as "incr" for increment.
     * @returns The annotate op if valid, otherwise undefined
     */
    annotateMarker(marker, props, combiningOp) {
        const annotateOp = (0, opBuilder_1.createAnnotateMarkerOp)(marker, props, combiningOp);
        if (this.applyAnnotateRangeOp({ op: annotateOp })) {
            return annotateOp;
        }
        else {
            return undefined;
        }
    }
    /**
     * Annotates the range with the provided properties
     * @param start - The inclusive start position of the range to annotate
     * @param end - The exclusive end position of the range to annotate
     * @param props - The properties to annotate the range with
     * @param combiningOp - Specifies how to combine values for the property, such as "incr" for increment.
     * @returns The annotate op if valid, otherwise undefined
     */
    annotateRangeLocal(start, end, props, combiningOp) {
        const annotateOp = (0, opBuilder_1.createAnnotateRangeOp)(start, end, props, combiningOp);
        if (this.applyAnnotateRangeOp({ op: annotateOp })) {
            return annotateOp;
        }
        return undefined;
    }
    /**
     * Removes the range
     *
     * @param start - The inclusive start of the range to remove
     * @param end - The exclusive end of the range to remove
     */
    removeRangeLocal(start, end) {
        const removeOp = (0, opBuilder_1.createRemoveRangeOp)(start, end);
        if (this.applyRemoveRangeOp({ op: removeOp })) {
            return removeOp;
        }
        return undefined;
    }
    /**
     * @param pos - The position to insert the segment at
     * @param segment - The segment to insert
     */
    insertSegmentLocal(pos, segment) {
        if (segment.cachedLength <= 0) {
            return undefined;
        }
        const insertOp = (0, opBuilder_1.createInsertSegmentOp)(pos, segment);
        if (this.applyInsertOp({ op: insertOp })) {
            return insertOp;
        }
        return undefined;
    }
    /**
     * @param refPos - The reference position to insert the segment at
     * @param segment - The segment to insert
     */
    insertAtReferencePositionLocal(refPos, segment) {
        const pos = this.mergeTree.referencePositionToLocalPosition(refPos, this.getCurrentSeq(), this.getClientId());
        if (pos === localReference_1.LocalReference.DetachedPosition) {
            return undefined;
        }
        const op = (0, opBuilder_1.createInsertSegmentOp)(pos, segment);
        const opArgs = { op };
        let traceStart;
        if (this.measureOps) {
            traceStart = common_utils_1.Trace.start();
        }
        this.mergeTree.insertAtReferencePosition(refPos, segment, opArgs);
        this.completeAndLogOp(opArgs, this.getClientSequenceArgs(opArgs), { start: op.pos1 }, traceStart);
        return op;
    }
    walkSegments(handler, start, end, accum, splitRange = false) {
        this.mergeTree.mapRange({
            leaf: handler,
        }, this.getCurrentSeq(), this.getClientId(), accum, start, end, splitRange);
    }
    /**
     * Serializes the data required for garbage collection. The IFluidHandles stored in all segments that haven't
     * been removed represent routes to other objects. We serialize the data in these segments using the passed in
     * serializer which keeps track of all serialized handles.
     */
    serializeGCData(handle, handleCollectingSerializer) {
        this.mergeTree.walkAllSegments(this.mergeTree.root, (seg) => {
            // Only serialize segments that have not been removed.
            if (seg.removedSeq === undefined) {
                handleCollectingSerializer.stringify(seg.clone().toJSONObject(), handle);
            }
            return true;
        });
    }
    getCollabWindow() {
        return this.mergeTree.getCollabWindow();
    }
    /**
     * Returns the current position of a segment, and -1 if the segment
     * does not exist in this merge tree
     * @param segment - The segment to get the position of
     */
    getPosition(segment) {
        if ((segment === null || segment === void 0 ? void 0 : segment.parent) === undefined) {
            return -1;
        }
        return this.mergeTree.getPosition(segment, this.getCurrentSeq(), this.getClientId());
    }
    /**
     * @deprecated - use createReferencePosition instead
     */
    addLocalReference(lref) {
        return this.mergeTree.addLocalReference(lref);
    }
    /**
     * @deprecated - use removeReferencePosition instead
     */
    removeLocalReference(lref) {
        return this.removeLocalReferencePosition(lref);
    }
    createLocalReferencePosition(segment, offset, refType, properties) {
        return this.mergeTree.createLocalReferencePosition(segment, offset, refType, properties, this);
    }
    removeLocalReferencePosition(lref) {
        return this.mergeTree.removeLocalReferencePosition(lref);
    }
    localReferencePositionToPosition(lref) {
        return this.mergeTree.referencePositionToLocalPosition(lref);
    }
    /**
     * Given a position specified relative to a marker id, lookup the marker
     * and convert the position to a character position.
     * @param relativePos - Id of marker (may be indirect) and whether position is before or after marker.
     */
    posFromRelativePos(relativePos) {
        return this.mergeTree.posFromRelativePos(relativePos);
    }
    getMarkerFromId(id) {
        return this.mergeTree.getMarkerFromId(id);
    }
    /**
     * Performs the remove based on the provided op
     * @param opArgs - The ops args for the op
     * @returns True if the remove was applied. False if it could not be.
     */
    applyRemoveRangeOp(opArgs) {
        (0, common_utils_1.assert)(opArgs.op.type === ops_1.MergeTreeDeltaType.REMOVE, 0x02d /* "Unexpected op type on range remove!" */);
        const op = opArgs.op;
        const clientArgs = this.getClientSequenceArgs(opArgs);
        const range = this.getValidOpRange(op, clientArgs);
        if (!range) {
            return false;
        }
        let traceStart;
        if (this.measureOps) {
            traceStart = common_utils_1.Trace.start();
        }
        this.mergeTree.markRangeRemoved(range.start, range.end, clientArgs.referenceSequenceNumber, clientArgs.clientId, clientArgs.sequenceNumber, false, opArgs);
        this.completeAndLogOp(opArgs, clientArgs, range, traceStart);
        return true;
    }
    /**
     * Performs the annotate based on the provided op
     * @param opArgs - The ops args for the op
     * @returns True if the annotate was applied. False if it could not be.
     */
    applyAnnotateRangeOp(opArgs) {
        (0, common_utils_1.assert)(opArgs.op.type === ops_1.MergeTreeDeltaType.ANNOTATE, 0x02e /* "Unexpected op type on range annotate!" */);
        const op = opArgs.op;
        const clientArgs = this.getClientSequenceArgs(opArgs);
        const range = this.getValidOpRange(op, clientArgs);
        if (!range) {
            return false;
        }
        let traceStart;
        if (this.measureOps) {
            traceStart = common_utils_1.Trace.start();
        }
        this.mergeTree.annotateRange(range.start, range.end, op.props, op.combiningOp, clientArgs.referenceSequenceNumber, clientArgs.clientId, clientArgs.sequenceNumber, opArgs);
        this.completeAndLogOp(opArgs, clientArgs, range, traceStart);
        return true;
    }
    /**
     * Performs the insert based on the provided op
     * @param opArgs - The ops args for the op
     * @returns True if the insert was applied. False if it could not be.
     */
    applyInsertOp(opArgs) {
        (0, common_utils_1.assert)(opArgs.op.type === ops_1.MergeTreeDeltaType.INSERT, 0x02f /* "Unexpected op type on range insert!" */);
        const op = opArgs.op;
        const clientArgs = this.getClientSequenceArgs(opArgs);
        const range = this.getValidOpRange(op, clientArgs);
        if (!range) {
            return false;
        }
        let segments;
        if (op.seg) {
            segments = [this.specToSegment(op.seg)];
        }
        if (!segments || segments.length === 0) {
            return false;
        }
        let traceStart;
        if (this.measureOps) {
            traceStart = common_utils_1.Trace.start();
        }
        this.mergeTree.insertSegments(range.start, segments, clientArgs.referenceSequenceNumber, clientArgs.clientId, clientArgs.sequenceNumber, opArgs);
        this.completeAndLogOp(opArgs, clientArgs, range, traceStart);
        return true;
    }
    /**
     *
     * @param opArgs - The op args of the op to complete
     * @param clientArgs - The client args for the op
     * @param range - The range the op applied to
     * @param clockStart - Optional. The clock start if timing data should be updated.
     */
    completeAndLogOp(opArgs, clientArgs, range, traceStart) {
        if (!opArgs.sequencedMessage) {
            if (traceStart) {
                this.localTime += elapsedMicroseconds(traceStart);
                this.localOps++;
            }
        }
        else {
            (0, common_utils_1.assert)(this.mergeTree.getCollabWindow().currentSeq < clientArgs.sequenceNumber, 0x030 /* "Incoming remote op sequence# <= local collabWindow's currentSequence#" */);
            (0, common_utils_1.assert)(this.mergeTree.getCollabWindow().minSeq <= opArgs.sequencedMessage.minimumSequenceNumber, 0x031 /* "Incoming remote op minSequence# < local collabWindow's minSequence#" */);
            if (traceStart) {
                this.accumTime += elapsedMicroseconds(traceStart);
                this.accumOps++;
                this.accumWindow += (this.getCurrentSeq() - this.getCollabWindow().minSeq);
            }
        }
    }
    /**
     * Returns a valid range for the op, or undefined
     * @param op - The op to generate the range for
     * @param clientArgs - The client args for the op
     */
    getValidOpRange(op, clientArgs) {
        let start = op.pos1;
        if (start === undefined && op.relativePos1) {
            start = this.mergeTree.posFromRelativePos(op.relativePos1, clientArgs.referenceSequenceNumber, clientArgs.clientId);
        }
        let end = op.pos2;
        if (end === undefined && op.relativePos2) {
            end = this.mergeTree.posFromRelativePos(op.relativePos2, clientArgs.referenceSequenceNumber, clientArgs.clientId);
        }
        // Validate if local op
        if (clientArgs.clientId === this.getClientId()) {
            const length = this.getLength();
            const invalidPositions = [];
            // Validate start position
            //
            if (start === undefined
                || start < 0
                || start > length
                || start === length && op.type !== ops_1.MergeTreeDeltaType.INSERT) {
                invalidPositions.push("start");
            }
            // Validate end if not insert, or insert has end
            //
            if (op.type !== ops_1.MergeTreeDeltaType.INSERT || end !== undefined) {
                if (end === undefined || end <= start) {
                    invalidPositions.push("end");
                }
            }
            if (invalidPositions.length > 0) {
                throw new telemetry_utils_1.LoggingError("RangeOutOfBounds", {
                    usageError: true,
                    end,
                    invalidPositions: invalidPositions.toString(),
                    length,
                    opPos1: op.pos1,
                    opPos1Relative: op.relativePos1 !== undefined,
                    opPos2: op.pos2,
                    opPos2Relative: op.relativePos2 !== undefined,
                    opType: op.type,
                    start,
                });
            }
        }
        // start and end are guaranteed to be non-null here, otherwise we throw above.
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
        return { start, end };
    }
    /**
     * Gets the client args from the op if remote, otherwise uses the local clients info
     * @param sequencedMessage - The sequencedMessage to get the client sequence args for
     */
    getClientSequenceArgsForMessage(sequencedMessage) {
        // If there this no sequenced message, then the op is local
        // and unacked, so use this clients sequenced args
        //
        if (!sequencedMessage) {
            const segWindow = this.getCollabWindow();
            return {
                clientId: segWindow.clientId,
                referenceSequenceNumber: segWindow.currentSeq,
                sequenceNumber: this.getLocalSequenceNumber(),
            };
        }
        else {
            return {
                clientId: this.getOrAddShortClientId(sequencedMessage.clientId),
                referenceSequenceNumber: sequencedMessage.referenceSequenceNumber,
                sequenceNumber: sequencedMessage.sequenceNumber,
            };
        }
    }
    /**
     * Gets the client args from the op if remote, otherwise uses the local clients info
     * @param opArgs - The op arg to get the client sequence args for
     */
    getClientSequenceArgs(opArgs) {
        return this.getClientSequenceArgsForMessage(opArgs.sequencedMessage);
    }
    ackPendingSegment(opArgs) {
        const ackOp = (deltaOpArgs) => {
            let trace;
            if (this.measureOps) {
                trace = common_utils_1.Trace.start();
            }
            this.mergeTree.ackPendingSegment(deltaOpArgs);
            if (deltaOpArgs.op.type === ops_1.MergeTreeDeltaType.ANNOTATE) {
                if (deltaOpArgs.op.combiningOp && (deltaOpArgs.op.combiningOp.name === "consensus")) {
                    this.updateConsensusProperty(deltaOpArgs.op, deltaOpArgs.sequencedMessage);
                }
            }
            if (trace) {
                this.accumTime += elapsedMicroseconds(trace);
                this.accumOps++;
                this.accumWindow += (this.getCurrentSeq() - this.getCollabWindow().minSeq);
            }
        };
        if (opArgs.op.type === ops_1.MergeTreeDeltaType.GROUP) {
            for (const memberOp of opArgs.op.ops) {
                ackOp({
                    groupOp: opArgs.op,
                    op: memberOp,
                    sequencedMessage: opArgs.sequencedMessage,
                });
            }
        }
        else {
            ackOp(opArgs);
        }
    }
    // as functions are modified move them above the eslint-disabled waterline and lint them
    cloneFromSegments() {
        const clone = new Client(this.specToSegment, this.logger, this.mergeTree.options);
        const segments = [];
        const newRoot = this.mergeTree.blockClone(this.mergeTree.root, segments);
        clone.mergeTree.root = newRoot;
        return clone;
    }
    getOrAddShortClientId(longClientId) {
        if (!this.clientNameToIds.get(longClientId)) {
            this.addLongClientId(longClientId);
        }
        return this.getShortClientId(longClientId);
    }
    getShortClientId(longClientId) {
        return this.clientNameToIds.get(longClientId).data;
    }
    getLongClientId(shortClientId) {
        if (shortClientId >= 0) {
            return this.shortClientIdMap[shortClientId];
        }
        else {
            return "original";
        }
    }
    addLongClientId(longClientId) {
        this.clientNameToIds.put(longClientId, this.shortClientIdMap.length);
        this.shortClientIdMap.push(longClientId);
    }
    /**
     * During reconnect, we must find the positions to pending segments
     * relative to other pending segments. This methods computes that
     * position relative to a localSeq. Pending segments above the localSeq
     * will be ignored.
     *
     * @param segment - The segment to find the position for
     * @param localSeq - The localSeq to find the position of the segment at
     */
    findReconnectionPosition(segment, localSeq) {
        (0, common_utils_1.assert)(localSeq <= this.mergeTree.collabWindow.localSeq, 0x032 /* "localSeq greater than collab window" */);
        let segmentPosition = 0;
        /*
            Walk the segments up to the current segment, and calculate it's
            position taking into account local segments that were modified,
            after the current segment.

            TODO: Consider embedding this information into the tree for
            more efficient look up of pending segment positions.
        */
        this.mergeTree.walkAllSegments(this.mergeTree.root, (seg) => {
            // If we've found the desired segment, terminate the walk and return 'segmentPosition'.
            if (seg === segment) {
                return false;
            }
            // Otherwise, advance segmentPosition if the segment has been inserted and not removed
            // with respect to the given 'localSeq'.
            //
            // Note that all ACKed / remote ops are applied and we only need concern ourself with
            // determining if locally pending ops fall before/after the given 'localSeq'.
            if ((seg.localSeq === undefined || seg.localSeq <= localSeq) // Is inserted
                && (seg.removedSeq === undefined || seg.localRemovedSeq > localSeq) // Not removed
            ) {
                segmentPosition += seg.cachedLength;
            }
            return true;
        });
        return segmentPosition;
    }
    /**
     * Rebases a (local) position from the perspective `{ seq: seqNumberFrom, localSeq }` to the perspective
     * of the current sequence number. This is desirable when rebasing operations for reconnection.
     *
     * If the position refers to a segment/offset that was removed by some operation between `seqNumberFrom` and
     * the current sequence number, the returned position will align with the position of a reference given
     * `SlideOnRemove` semantics.
     */
    rebasePosition(pos, seqNumberFrom, localSeq) {
        (0, common_utils_1.assert)(localSeq <= this.mergeTree.collabWindow.localSeq, 0x300 /* localSeq greater than collab window */);
        let segment;
        let posAccumulated = 0;
        let offset = pos;
        const isInsertedInView = (seg) => (seg.seq !== undefined && seg.seq !== constants_1.UnassignedSequenceNumber && seg.seq <= seqNumberFrom)
            || (seg.localSeq !== undefined && seg.localSeq <= localSeq);
        const isRemovedFromView = ({ removedSeq, localRemovedSeq }) => (removedSeq !== undefined && removedSeq !== constants_1.UnassignedSequenceNumber && removedSeq <= seqNumberFrom)
            || (localRemovedSeq !== undefined && localRemovedSeq <= localSeq);
        this.mergeTree.walkAllSegments(this.mergeTree.root, (seg) => {
            (0, common_utils_1.assert)(seg.seq !== undefined || seg.localSeq !== undefined, 0x301 /* Either seq or localSeq should be defined */);
            segment = seg;
            if (isInsertedInView(seg) && !isRemovedFromView(seg)) {
                posAccumulated += seg.cachedLength;
                if (offset >= seg.cachedLength) {
                    offset -= seg.cachedLength;
                }
            }
            // Keep going while we've yet to reach the segment at the desired position
            return posAccumulated <= pos;
        });
        (0, common_utils_1.assert)(segment !== undefined, 0x302 /* No segment found */);
        const seqNumberTo = this.getCollabWindow().currentSeq;
        if ((segment.removedSeq !== undefined &&
            segment.removedSeq !== constants_1.UnassignedSequenceNumber &&
            segment.removedSeq <= seqNumberTo)
            || (segment.localRemovedSeq !== undefined && segment.localRemovedSeq <= localSeq)) {
            // Segment that the position was in has been removed: null out offset.
            offset = 0;
        }
        (0, common_utils_1.assert)(0 <= offset && offset < segment.cachedLength, 0x303 /* Invalid offset */);
        return this.findReconnectionPosition(segment, localSeq) + offset;
    }
    resetPendingDeltaToOps(resetOp, segmentGroup) {
        var _a, _b;
        (0, common_utils_1.assert)(!!segmentGroup, 0x033 /* "Segment group undefined" */);
        const NACKedSegmentGroup = (_a = this.mergeTree.pendingSegments) === null || _a === void 0 ? void 0 : _a.dequeue();
        (0, common_utils_1.assert)(segmentGroup === NACKedSegmentGroup, 0x034 /* "Segment group not at head of merge tree pending queue" */);
        const opList = [];
        // We need to sort the segments by ordinal, as the segments are not sorted in the segment group.
        // The reason they need them sorted, as they have the same local sequence number and which means
        // farther segments will  take into account nearer segments when calculating their position.
        // By sorting we ensure the nearer segment will be applied and sequenced before the father segments
        // so their recalculated positions will be correct.
        for (const segment of segmentGroup.segments.sort((a, b) => a.ordinal < b.ordinal ? -1 : 1)) {
            const segmentSegGroup = segment.segmentGroups.dequeue();
            (0, common_utils_1.assert)(segmentGroup === segmentSegGroup, 0x035 /* "Segment group not at head of segment pending queue" */);
            const segmentPosition = this.findReconnectionPosition(segment, segmentGroup.localSeq);
            let newOp;
            switch (resetOp.type) {
                case ops_1.MergeTreeDeltaType.ANNOTATE:
                    (0, common_utils_1.assert)(((_b = segment.propertyManager) === null || _b === void 0 ? void 0 : _b.hasPendingProperties()) === true, 0x036 /* "Segment has no pending properties" */);
                    // if the segment has been removed, there's no need to send the annotate op
                    // unless the remove was local, in which case the annotate must have come
                    // before the remove
                    if (segment.removedSeq === undefined || segment.localRemovedSeq !== undefined) {
                        newOp = (0, opBuilder_1.createAnnotateRangeOp)(segmentPosition, segmentPosition + segment.cachedLength, resetOp.props, resetOp.combiningOp);
                    }
                    break;
                case ops_1.MergeTreeDeltaType.INSERT:
                    (0, common_utils_1.assert)(segment.seq === constants_1.UnassignedSequenceNumber, 0x037 /* "Segment already has assigned sequence number" */);
                    let segInsertOp = segment;
                    if (typeof resetOp.seg === "object" && resetOp.seg.props !== undefined) {
                        segInsertOp = segment.clone();
                        segInsertOp.properties = resetOp.seg.props;
                    }
                    newOp = (0, opBuilder_1.createInsertSegmentOp)(segmentPosition, segInsertOp);
                    break;
                case ops_1.MergeTreeDeltaType.REMOVE:
                    if (segment.localRemovedSeq !== undefined) {
                        newOp = (0, opBuilder_1.createRemoveRangeOp)(segmentPosition, segmentPosition + segment.cachedLength);
                    }
                    break;
                default:
                    throw new Error(`Invalid op type`);
            }
            if (newOp) {
                const newSegmentGroup = { segments: [], localSeq: segmentGroup.localSeq };
                segment.segmentGroups.enqueue(newSegmentGroup);
                this.mergeTree.pendingSegments.enqueue(newSegmentGroup);
                opList.push(newOp);
            }
        }
        return opList;
    }
    applyRemoteOp(opArgs) {
        const op = opArgs.op;
        const msg = opArgs.sequencedMessage;
        this.getOrAddShortClientId(msg.clientId);
        switch (op.type) {
            case ops_1.MergeTreeDeltaType.INSERT:
                this.applyInsertOp(opArgs);
                break;
            case ops_1.MergeTreeDeltaType.REMOVE:
                this.applyRemoveRangeOp(opArgs);
                break;
            case ops_1.MergeTreeDeltaType.ANNOTATE:
                this.applyAnnotateRangeOp(opArgs);
                break;
            case ops_1.MergeTreeDeltaType.GROUP: {
                for (const memberOp of op.ops) {
                    this.applyRemoteOp({
                        op: memberOp,
                        groupOp: op,
                        sequencedMessage: msg,
                    });
                }
                break;
            }
            default:
                break;
        }
    }
    applyStashedOp(op) {
        let metadata;
        switch (op.type) {
            case ops_1.MergeTreeDeltaType.INSERT:
                this.applyInsertOp({ op });
                metadata = this.peekPendingSegmentGroups();
                break;
            case ops_1.MergeTreeDeltaType.REMOVE:
                this.applyRemoveRangeOp({ op });
                metadata = this.peekPendingSegmentGroups();
                break;
            case ops_1.MergeTreeDeltaType.ANNOTATE:
                this.applyAnnotateRangeOp({ op });
                metadata = this.peekPendingSegmentGroups();
                break;
            case ops_1.MergeTreeDeltaType.GROUP:
                return op.ops.map((o) => this.applyStashedOp(o));
            default:
                (0, common_utils_1.unreachableCase)(op, "unrecognized op type");
        }
        (0, common_utils_1.assert)(!!metadata, 0x2db /* "Applying op must generate a pending segment" */);
        return metadata;
    }
    applyMsg(msg, local = false) {
        var _a;
        // Ensure client ID is registered
        this.getOrAddShortClientId(msg.clientId);
        // Apply if an operation message
        if (msg.type === protocol_definitions_1.MessageType.Operation) {
            const opArgs = {
                op: msg.contents,
                sequencedMessage: msg,
            };
            if (((_a = opArgs.sequencedMessage) === null || _a === void 0 ? void 0 : _a.clientId) === this.longClientId || local) {
                this.ackPendingSegment(opArgs);
            }
            else {
                this.applyRemoteOp(opArgs);
            }
        }
        this.updateSeqNumbers(msg.minimumSequenceNumber, msg.sequenceNumber);
    }
    updateSeqNumbers(min, seq) {
        const collabWindow = this.mergeTree.getCollabWindow();
        // Equal is fine here due to SharedSegmentSequence<>.snapshotContent() potentially updating with same #
        (0, common_utils_1.assert)(collabWindow.currentSeq <= seq, 0x038 /* "Incoming op sequence# < local collabWindow's currentSequence#" */);
        collabWindow.currentSeq = seq;
        (0, common_utils_1.assert)(min <= seq, 0x039 /* "Incoming op sequence# < minSequence#" */);
        this.updateMinSeq(min);
    }
    /**
     * Resolves a remote client's position against the local sequence
     * and returns the remote client's position relative to the local
     * sequence
     * @param remoteClientPosition - The remote client's position to resolve
     * @param remoteClientRefSeq - The reference sequence number of the remote client
     * @param remoteClientId - The client id of the remote client
     */
    resolveRemoteClientPosition(remoteClientPosition, remoteClientRefSeq, remoteClientId) {
        const shortRemoteClientId = this.getOrAddShortClientId(remoteClientId);
        return this.mergeTree.resolveRemoteClientPosition(remoteClientPosition, remoteClientRefSeq, shortRemoteClientId);
    }
    /**
     *  Given an pending operation and segment group, regenerate the op, so it
     *  can be resubmitted
     * @param resetOp - The op to reset
     * @param segmentGroup - The segment group associated with the op
     */
    regeneratePendingOp(resetOp, segmentGroup) {
        const opList = [];
        if (resetOp.type === ops_1.MergeTreeDeltaType.GROUP) {
            if (Array.isArray(segmentGroup)) {
                (0, common_utils_1.assert)(resetOp.ops.length === segmentGroup.length, 0x03a /* "Number of ops in 'resetOp' must match the number of segment groups provided." */);
                for (let i = 0; i < resetOp.ops.length; i++) {
                    opList.push(...this.resetPendingDeltaToOps(resetOp.ops[i], segmentGroup[i]));
                }
            }
            else {
                // A group op containing a single op will pass a direct reference to 'segmentGroup'
                // rather than an array of segment groups.  (See 'peekPendingSegmentGroups()')
                (0, common_utils_1.assert)(resetOp.ops.length === 1, 0x03b /* "Number of ops in 'resetOp' must match the number of segment groups provided." */);
                opList.push(...this.resetPendingDeltaToOps(resetOp.ops[0], segmentGroup));
            }
        }
        else {
            (0, common_utils_1.assert)(resetOp.type !== ops_1.MergeTreeDeltaType.GROUP, 0x03c /* "Reset op has 'group' delta type!" */);
            (0, common_utils_1.assert)(!Array.isArray(segmentGroup), 0x03d /* "segmentGroup is array rather than singleton!" */);
            opList.push(...this.resetPendingDeltaToOps(resetOp, segmentGroup));
        }
        return opList.length === 1 ? opList[0] : (0, opBuilder_1.createGroupOp)(...opList);
    }
    createTextHelper() {
        return new textSegment_1.MergeTreeTextHelper(this.mergeTree);
    }
    summarize(runtime, handle, serializer, catchUpMsgs) {
        var _a;
        const deltaManager = runtime.deltaManager;
        const minSeq = deltaManager.minimumSequenceNumber;
        // Catch up to latest MSN, if we have not had a chance to do it.
        // Required for case where FluidDataStoreRuntime.attachChannel()
        // generates summary right after loading data store.
        this.updateSeqNumbers(minSeq, deltaManager.lastSequenceNumber);
        // One of the summaries (from SPO) I observed to have chunk.chunkSequenceNumber > minSeq!
        // Not sure why - need to catch it sooner
        (0, common_utils_1.assert)(this.getCollabWindow().minSeq === minSeq, 0x03e /* "minSeq mismatch between collab window and delta manager!" */);
        // Must continue to support legacy
        //       (See https://github.com/microsoft/FluidFramework/issues/84)
        if (((_a = this.mergeTree.options) === null || _a === void 0 ? void 0 : _a.newMergeTreeSnapshotFormat) === true) {
            (0, common_utils_1.assert)(catchUpMsgs === undefined || catchUpMsgs.length === 0, 0x03f /* "New format should not emit catchup ops" */);
            const snap = new snapshotV1_1.SnapshotV1(this.mergeTree, this.logger, (id) => this.getLongClientId(id));
            snap.extractSync();
            return snap.emit(serializer, handle);
        }
        else {
            const snap = new snapshotlegacy_1.SnapshotLegacy(this.mergeTree, this.logger);
            snap.extractSync();
            return snap.emit(catchUpMsgs, serializer, handle);
        }
    }
    async load(runtime, storage, serializer) {
        const loader = new snapshotLoader_1.SnapshotLoader(runtime, this, this.mergeTree, this.logger, serializer);
        return loader.initialize(storage);
    }
    getStackContext(startPos, rangeLabels) {
        return this.mergeTree.getStackContext(startPos, this.getCollabWindow().clientId, rangeLabels);
    }
    getLocalSequenceNumber() {
        const segWindow = this.getCollabWindow();
        if (segWindow.collaborating) {
            return constants_1.UnassignedSequenceNumber;
        }
        else {
            return constants_1.UniversalSequenceNumber;
        }
    }
    localTransaction(groupOp) {
        for (const op of groupOp.ops) {
            const opArgs = {
                op,
                groupOp,
            };
            switch (op.type) {
                case ops_1.MergeTreeDeltaType.INSERT:
                    this.applyInsertOp(opArgs);
                    break;
                case ops_1.MergeTreeDeltaType.ANNOTATE:
                    this.applyAnnotateRangeOp(opArgs);
                    break;
                case ops_1.MergeTreeDeltaType.REMOVE:
                    this.applyRemoveRangeOp(opArgs);
                    break;
                default:
                    break;
            }
        }
    }
    updateConsensusProperty(op, msg) {
        const markerId = op.relativePos1.id;
        const consensusInfo = this.pendingConsensus.get(markerId);
        if (consensusInfo) {
            consensusInfo.marker.addProperties(op.props, op.combiningOp, msg.sequenceNumber);
        }
        this.mergeTree.addMinSeqListener(msg.sequenceNumber, () => consensusInfo.callback(consensusInfo.marker));
    }
    updateMinSeq(minSeq) {
        let trace;
        if (this.measureOps) {
            trace = common_utils_1.Trace.start();
        }
        this.mergeTree.setMinSeq(minSeq);
        if (trace) {
            const elapsed = elapsedMicroseconds(trace);
            this.accumWindowTime += elapsed;
            if (elapsed > this.maxWindowTime) {
                this.maxWindowTime = elapsed;
            }
        }
    }
    getContainingSegment(pos, op) {
        const args = this.getClientSequenceArgsForMessage(op);
        return this.mergeTree.getContainingSegment(pos, args.referenceSequenceNumber, args.clientId);
    }
    /**
     * Returns the position to slide a reference to if a slide is required.
     * @param segoff - The segment and offset to slide from
     * @returns - segment and offset to slide the reference to
     */
    getSlideToSegment(segoff) {
        return this.mergeTree._getSlideToSegment(segoff);
    }
    getPropertiesAtPosition(pos) {
        let propertiesAtPosition;
        const segoff = this.getContainingSegment(pos);
        const seg = segoff.segment;
        if (seg) {
            propertiesAtPosition = seg.properties;
        }
        return propertiesAtPosition;
    }
    getRangeExtentsOfPosition(pos) {
        let posStart;
        let posAfterEnd;
        const segoff = this.getContainingSegment(pos);
        const seg = segoff.segment;
        if (seg) {
            posStart = this.getPosition(seg);
            posAfterEnd = posStart + seg.cachedLength;
        }
        return { posStart, posAfterEnd };
    }
    getCurrentSeq() {
        return this.getCollabWindow().currentSeq;
    }
    getClientId() {
        return this.getCollabWindow().clientId;
    }
    getLength() { return this.mergeTree.length; }
    startOrUpdateCollaboration(longClientId, minSeq = 0, currentSeq = 0) {
        // we should always have a client id if we are collaborating
        // if the client id is undefined we are likely bound to a detached
        // container, so we should keep going in local mode. once
        // the container attaches this will be called again on connect with the
        // client id
        if (longClientId !== undefined) {
            if (this.longClientId === undefined) {
                this.longClientId = longClientId;
                this.addLongClientId(this.longClientId);
                this.mergeTree.startCollaboration(this.getShortClientId(this.longClientId), minSeq, currentSeq);
            }
            else {
                const oldClientId = this.longClientId;
                const oldData = this.clientNameToIds.get(oldClientId).data;
                this.longClientId = longClientId;
                this.clientNameToIds.put(longClientId, oldData);
                this.shortClientIdMap[oldData] = longClientId;
            }
        }
    }
    findTile(startPos, tileLabel, preceding = true) {
        const clientId = this.getClientId();
        return this.mergeTree.findTile(startPos, clientId, tileLabel, preceding);
    }
}
exports.Client = Client;
//# sourceMappingURL=client.js.map