{"version":3,"sources":["src/common.speech/ReconnectContinuationState.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6DAA6D,CAAC;AACnG,OAAO,EAAE,YAAY,EAAE,MAAM,qDAAqD,CAAC;AAGnF;;;;;;;;;GASG;AACH,qBAAa,0BAA0B;IACnC,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,cAAc,CAAC,CAAS;IAChC,OAAO,CAAC,gBAAgB,CAAC,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;gBAE1B,eAAe,GAAE,MAAY;IAIhD;;OAEG;IACH,IAAW,eAAe,IAAI,MAAM,CAEnC;IAED;;;;OAIG;IACH,IAAW,YAAY,IAAI,MAAM,GAAG,SAAS,CAE5C;IAED;;;;;OAKG;IACH,IAAW,sBAAsB,IAAI,OAAO,CAI3C;IAED;;OAEG;IACI,KAAK,IAAI,IAAI;IAMpB;;;;;OAKG;IACI,WAAW,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IAI7C;;;;;;;;;OASG;IACI,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,CAAC,MAAM,CAAC,EAAE,eAAe,GAAE,MAAU,GAAG,IAAI;IA+B/F;;;OAGG;IACI,yBAAyB,IAAI,YAAY;IAQhD;;;OAGG;IACI,wBAAwB,IAAI,oBAAoB,GAAG,SAAS;CAoBtE","file":"ReconnectContinuationState.d.ts","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved.\r\n// Licensed under the MIT license.\r\n\r\nimport { IStringDictionary } from \"../common/Exports.js\";\r\nimport { HeaderNames } from \"./HeaderNames.js\";\r\nimport { CtsAudioContinuation } from \"./ServiceMessages/MultichannelAudio/CtsAudioContinuation.js\";\r\nimport { CtsAudioInfo } from \"./ServiceMessages/MultichannelAudio/CtsAudioInfo.js\";\r\nimport { CtsAudioStream } from \"./ServiceMessages/MultichannelAudio/CtsAudioStream.js\";\r\n\r\n/**\r\n * Tracks the per-turn state required by the reliable reconnect protocol.\r\n *\r\n * The service issues a continuation token, per-stream resume offsets and a service tag on its\r\n * responses. When a turn is interrupted before \"turn.end\", the client echoes these values back\r\n * in the next \"speech.context\" so the service can suppress duplicate results, correct media\r\n * offsets and correlate the reconnected turn.\r\n *\r\n * Intentionally dependency-free so it can be unit tested in isolation.\r\n */\r\nexport class ReconnectContinuationState {\r\n    private privToken?: string;\r\n    private privServiceTag?: string;\r\n    private privStreamOffset?: number;\r\n    private readonly privDefaultStreamId: string;\r\n\r\n    public constructor(defaultStreamId: string = \"1\") {\r\n        this.privDefaultStreamId = defaultStreamId;\r\n    }\r\n\r\n    /**\r\n     * The stream id used for the single logical audio stream the SDK currently sends.\r\n     */\r\n    public get defaultStreamId(): string {\r\n        return this.privDefaultStreamId;\r\n    }\r\n\r\n    /**\r\n     * The session-absolute resume offset (100ns ticks) most recently acknowledged by the\r\n     * service, or undefined if none has arrived. Used both to populate the outgoing\r\n     * continuation block and to trim the replay buffer.\r\n     */\r\n    public get streamOffset(): number | undefined {\r\n        return this.privStreamOffset;\r\n    }\r\n\r\n    /**\r\n     * True when the continuation block must be emitted. The block is emitted whenever any of\r\n     * the three signals is present (token, service tag or stream offset). The signals persist\r\n     * for the whole session, so the block keeps being\r\n     * emitted on reconnects and subsequent turns once the first turn.start has been seen.\r\n     */\r\n    public get hasPendingContinuation(): boolean {\r\n        return this.privToken !== undefined\r\n            || this.privServiceTag !== undefined\r\n            || this.privStreamOffset !== undefined;\r\n    }\r\n\r\n    /**\r\n     * Resets all state. Called when a brand new recognition (session) starts.\r\n     */\r\n    public reset(): void {\r\n        this.privToken = undefined;\r\n        this.privServiceTag = undefined;\r\n        this.privStreamOffset = undefined;\r\n    }\r\n\r\n    /**\r\n     * A turn has started (\"turn.start\"). The service tag arrives in the turn.start body (under\r\n     * \"$.context.serviceTag\"), not a header. The tag is stored unconditionally\r\n     * (empty string when absent) and the token/offset are NOT cleared: the offset is\r\n     * session-global and the token persists until the service issues a new one.\r\n     */\r\n    public onTurnStart(serviceTag?: string): void {\r\n        this.privServiceTag = serviceTag ?? \"\";\r\n    }\r\n\r\n    /**\r\n     * Captures the reliable-reconnect headers (continuation token and stream resume offset)\r\n     * from an inbound message. Header names are case-insensitive, so matching is too. The\r\n     * service tag is not a header; it comes from the turn.start body via onTurnStart().\r\n     *\r\n     * The per-stream offset header is turn-relative. It is rebased onto the session-absolute\r\n     * timeline using `turnStartOffset`\r\n     * (RequestSession.currentTurnAudioOffset) - the same base used to rebase every other\r\n     * service offset - so the stored offset stays in the same frame as results and replay.\r\n     */\r\n    public updateFromHeaders(headers: IStringDictionary<string>, turnStartOffset: number = 0): void {\r\n        if (!headers) {\r\n            return;\r\n        }\r\n\r\n        const tokenHeader = HeaderNames.ContinuationToken.toLowerCase();\r\n        const offsetHeader = HeaderNames.ContinuationAudioStreamOffset.toLowerCase();\r\n\r\n        for (const headerName in headers) {\r\n            if (!headerName) {\r\n                continue;\r\n            }\r\n            const lowerName = headerName.toLowerCase();\r\n            const value = headers[headerName];\r\n\r\n            if (lowerName === tokenHeader) {\r\n                this.privToken = value;\r\n            } else if (lowerName === offsetHeader) {\r\n                const relativeOffset = parseInt(value, 10);\r\n                if (!isNaN(relativeOffset)) {\r\n                    // Rebase onto the session-absolute timeline and store only when it advances,\r\n                    // so an out-of-order offset never regresses the resume position.\r\n                    const absoluteOffset = turnStartOffset + relativeOffset;\r\n                    if (this.privStreamOffset === undefined || absoluteOffset > this.privStreamOffset) {\r\n                        this.privStreamOffset = absoluteOffset;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Builds the \"$.audio.streams\" metadata section. This is always sent when the feature is\r\n     * enabled and is the prerequisite for the service to return continuation headers.\r\n     */\r\n    public buildAudioStreamsMetadata(): CtsAudioInfo {\r\n        const streams: Record<string, CtsAudioStream | null> = {};\r\n        // The stream is opted in with \"<id>\":null (not an empty object); only the null marker\r\n        // enables the continuation contract service-side.\r\n        streams[this.privDefaultStreamId] = null;\r\n        return { streams };\r\n    }\r\n\r\n    /**\r\n     * Builds the \"$.continuation\" section to resume an aborted turn, or undefined when this\r\n     * is a fresh turn (and therefore must not carry a continuation).\r\n     */\r\n    public buildContinuationContext(): CtsAudioContinuation | undefined {\r\n        if (!this.hasPendingContinuation) {\r\n            return undefined;\r\n        }\r\n\r\n        // Always emit \"token\" and \"previousServiceTag\" (defaulting to \"\") and add the\r\n        // audio stream offset only when one is known.\r\n        const continuation: CtsAudioContinuation = {\r\n            previousServiceTag: this.privServiceTag ?? \"\",\r\n            token: this.privToken ?? \"\"\r\n        };\r\n\r\n        if (this.privStreamOffset !== undefined) {\r\n            continuation.audio = {\r\n                streams: { [this.privDefaultStreamId]: { offset: this.privStreamOffset } }\r\n            };\r\n        }\r\n\r\n        return continuation;\r\n    }\r\n}\r\n"]}