"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpeculativeRequestManager = void 0;
const lifecycle_1 = require("../../../util/vs/base/common/lifecycle");
/**
 * Owns the lifecycle of NES speculative requests:
 *
 * - the in-flight `pending` speculative (the bet on a specific post-accept document state)
 * - the `scheduled` speculative deferred until its originating stream completes
 *
 * Centralizes cancellation with typed reasons so every triggered cancellation
 * (reject, supersede, doc-close, trajectory divergence, dispose, ...) goes through
 * one path and is logged on the request's log context.
 */
class SpeculativeRequestManager extends lifecycle_1.Disposable {
    constructor(_logger) {
        super();
        this._logger = _logger;
        this._pending = null;
        this._scheduled = null;
    }
    get pending() {
        return this._pending;
    }
    /** Replaces the current pending speculative; cancels the prior one as `Replaced`. */
    setPending(req) {
        if (this._pending && this._pending.request !== req.request) {
            this._cancelPending("replaced" /* SpeculativeCancelReason.Replaced */);
        }
        this._pending = req;
    }
    /** Detaches the pending speculative without cancelling — caller is consuming it. */
    consumePending() {
        this._pending = null;
    }
    schedule(s) {
        this._scheduled = s;
    }
    clearScheduled() {
        this._scheduled = null;
    }
    /**
     * Removes and returns the scheduled entry iff its `headerRequestId` matches.
     * Used by the streaming path so that each stream only ever consumes its own
     * schedule, never another stream's.
     */
    consumeScheduled(headerRequestId) {
        if (this._scheduled?.headerRequestId !== headerRequestId) {
            return null;
        }
        const s = this._scheduled;
        this._scheduled = null;
        return s;
    }
    cancelAll(reason) {
        this._scheduled = null;
        this._cancelPending(reason);
    }
    /** Cancels the pending speculative iff `(docId, postEditContent)` doesn't match. */
    cancelIfMismatch(docId, postEditContent, reason) {
        if (this._pending && (this._pending.docId !== docId || this._pending.postEditContent !== postEditContent)) {
            this._cancelPending(reason);
        }
    }
    /** Cancels the pending and clears any scheduled targeting this document. */
    onDocumentClosed(docId) {
        if (this._scheduled?.suggestion.result?.targetDocumentId === docId) {
            this._scheduled = null;
        }
        if (this._pending?.docId === docId) {
            this._cancelPending("documentClosed" /* SpeculativeCancelReason.DocumentClosed */);
        }
    }
    /**
     * Trajectory check. The pending speculative is alive iff the current document
     * value is a *type-through prefix* toward the speculative's `postEditContent`:
     *
     *     cur === trajectoryPrefix + middle + trajectorySuffix
     *     where middle is some prefix of trajectoryNewText
     *
     * If not, the user's edits cannot reach `postEditContent` via continued typing
     * and the speculative will never be consumed — cancel now.
     */
    onActiveDocumentChanged(docId, currentDocValue) {
        const p = this._pending;
        if (!p || p.docId !== docId) {
            return;
        }
        // Cheap structural failure: doc shorter than the unedited frame.
        if (currentDocValue.length < p.trajectoryPrefix.length + p.trajectorySuffix.length) {
            this._cancelPending("divergedFromTrajectoryForm" /* SpeculativeCancelReason.DivergedFromTrajectoryForm */);
            return;
        }
        if (!currentDocValue.startsWith(p.trajectoryPrefix)) {
            this._cancelPending("divergedFromTrajectoryPrefix" /* SpeculativeCancelReason.DivergedFromTrajectoryPrefix */);
            return;
        }
        if (!currentDocValue.endsWith(p.trajectorySuffix)) {
            this._cancelPending("divergedFromTrajectorySuffix" /* SpeculativeCancelReason.DivergedFromTrajectorySuffix */);
            return;
        }
        const middle = currentDocValue.slice(p.trajectoryPrefix.length, currentDocValue.length - p.trajectorySuffix.length);
        if (!p.trajectoryNewText.startsWith(middle)) {
            this._cancelPending("divergedFromTrajectoryMiddle" /* SpeculativeCancelReason.DivergedFromTrajectoryMiddle */);
        }
    }
    _cancelPending(reason) {
        const p = this._pending;
        if (!p) {
            return;
        }
        this._pending = null;
        const headerRequestId = p.request.headerRequestId;
        this._logger.trace(`cancelling speculative request: ${reason} (headerRequestId=${headerRequestId})`);
        p.request.logContext.addLog(`speculative request cancelled: ${reason}`);
        const cts = p.request.cancellationTokenSource;
        cts.cancel();
        // Dispose to release the cancel-event listeners that the in-flight
        // provider call hooked onto the token. Safe even though the runner may
        // observe cancellation asynchronously — `cancel()` already fired the event.
        cts.dispose();
    }
    dispose() {
        this.cancelAll("disposed" /* SpeculativeCancelReason.Disposed */);
        super.dispose();
    }
}
exports.SpeculativeRequestManager = SpeculativeRequestManager;
//# sourceMappingURL=speculativeRequestManager.js.map